using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using CompanionKit.Core; using Microsoft.CodeAnalysis; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] [assembly: AssemblyCompany("Beastwhispering.Core")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+c48b2eb6460f1b993d9ddb914cbdc5ff6f1d22c6")] [assembly: AssemblyProduct("Beastwhispering.Core")] [assembly: AssemblyTitle("Beastwhispering.Core")] [assembly: AssemblyMetadata("BuildStamp", "c48b2eb 2026-07-30")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] internal sealed class IsReadOnlyAttribute : Attribute { } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace Beastwhispering.Core { public enum CounterVerdict { Inactive, Expired, AlreadyCountered, Counter } public enum BraceHitVerdict { Counter, NoDealer, SelfHit, StatusTick, NotHostile } public struct BraceHitInputs { public string DealerUid; public string SelfUid; public bool SourceIsStatus; public bool DealerHostile; } public sealed class BraceState { private readonly HashSet _countered = new HashSet(StringComparer.Ordinal); public bool Open { get; private set; } public double ExpireAt { get; private set; } public IReadOnlyCollection CounteredUids => _countered; public int CounterCount { get; private set; } public void Begin(double now, double windowSeconds) { Open = true; ExpireAt = now + ((windowSeconds > 0.0) ? windowSeconds : 0.0); _countered.Clear(); CounterCount = 0; } public bool Active(double now) { if (Open) { return now < ExpireAt; } return false; } public double RemainingSeconds(double now) { if (!Active(now)) { return 0.0; } return ExpireAt - now; } public void End() { Open = false; _countered.Clear(); } public CounterVerdict TryCounter(string attackerUid, double now, bool perAttackerOnce) { if (!Open) { return CounterVerdict.Inactive; } if (now >= ExpireAt) { return CounterVerdict.Expired; } if (perAttackerOnce && !string.IsNullOrEmpty(attackerUid) && _countered.Contains(attackerUid)) { return CounterVerdict.AlreadyCountered; } if (perAttackerOnce && !string.IsNullOrEmpty(attackerUid)) { _countered.Add(attackerUid); } CounterCount++; return CounterVerdict.Counter; } } public static class BraceRules { public static BraceHitVerdict Eligible(in BraceHitInputs hit) { if (string.IsNullOrEmpty(hit.DealerUid)) { return BraceHitVerdict.NoDealer; } if (!string.IsNullOrEmpty(hit.SelfUid) && string.Equals(hit.DealerUid, hit.SelfUid, StringComparison.Ordinal)) { return BraceHitVerdict.SelfHit; } if (hit.SourceIsStatus) { return BraceHitVerdict.StatusTick; } if (!hit.DealerHostile) { return BraceHitVerdict.NotHostile; } return BraceHitVerdict.Counter; } } public enum BuffFoodKind { Damage, DecayRider } public sealed class BuffFoodDef { public string Key = ""; public int? ItemId; public BuffFoodKind Kind; public double PercentPerLevel; public double? DurationSeconds; public string Toast = ""; } public sealed class BuffFoodEntry { public string Species = ""; public List Foods = new List(); } public static class BuffFoods { public static Dictionary Parse(string json, Action warn = null) { return JsonTable.Read(json, warn, "species", "an object of species → buff-food arrays", ParseEntry); } private static BuffFoodEntry ParseEntry(string species, object value, Action warn) { if (!(value is List list)) { new FieldReader("'" + species + "'", warn).Say("value must be an ARRAY of buff-food objects — species skipped."); return null; } List list2 = new List(); for (int i = 0; i < list.Count; i++) { BuffFoodDef buffFoodDef = ParseRow(species, i, list[i], warn); if (buffFoodDef != null) { list2.Add(buffFoodDef); } } return new BuffFoodEntry { Species = species, Foods = list2 }; } private static BuffFoodDef ParseRow(string species, int index, object value, Action warn) { FieldReader fieldReader = new FieldReader($"'{species}' buff food #{index + 1}", warn); if (!(value is Dictionary dictionary)) { fieldReader.Say("must be an object — entry skipped."); return null; } BuffFoodDef buffFoodDef = new BuffFoodDef { Kind = BuffFoodKind.Damage }; bool flag = false; double? num = null; ItemKey val = default(ItemKey); foreach (KeyValuePair item in dictionary) { switch (item.Key.ToLowerInvariant()) { case "item": if (ItemKey.TryRead(item.Value, ref val)) { buffFoodDef.Key = ((ItemKey)(ref val)).Key; buffFoodDef.ItemId = ((ItemKey)(ref val)).ItemId; flag = true; } else { fieldReader.Wrong("item", "a number (ItemID) or non-empty string (display name)"); } break; case "kind": { if (item.Value is string name && TryParseKind(name, out var kind)) { buffFoodDef.Kind = kind; break; } fieldReader.Say($"unknown kind '{item.Value}' (valid: damage, decayRider) — entry skipped."); return null; } case "percentperlevel": { if (!fieldReader.Num("percentPerLevel", item.Value, out var result, "a number > 0", "entry skipped")) { return null; } if (!(result > 0.0)) { fieldReader.Wrong("percentPerLevel", "a number > 0", "entry skipped"); return null; } num = result; break; } case "durationseconds": { if (!fieldReader.Num("durationSeconds", item.Value, out var result2, "a number > 0 (seconds)", "entry skipped")) { return null; } if (!(result2 > 0.0)) { fieldReader.Wrong("durationSeconds", "a number > 0 (seconds)", "entry skipped"); return null; } buffFoodDef.DurationSeconds = result2; break; } case "toast": if (item.Value is string toast) { buffFoodDef.Toast = toast; } else { fieldReader.Wrong("toast", "a string", "ignored"); } break; default: fieldReader.Unknown(item.Key, "item, kind, percentPerLevel, durationSeconds, toast"); break; } } if (!flag) { fieldReader.Say("missing/invalid required 'item' — entry skipped."); return null; } if (!num.HasValue) { fieldReader.Say("missing required 'percentPerLevel' (> 0) — entry skipped."); return null; } buffFoodDef.PercentPerLevel = num.Value; return buffFoodDef; } private static bool TryParseKind(string name, out BuffFoodKind kind) { if (string.Equals(name, "damage", StringComparison.OrdinalIgnoreCase)) { kind = BuffFoodKind.Damage; return true; } if (string.Equals(name, "decayRider", StringComparison.OrdinalIgnoreCase)) { kind = BuffFoodKind.DecayRider; return true; } kind = BuffFoodKind.Damage; return false; } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } public static BuffFoodEntry Resolve(Dictionary table, string speciesId) { BuffFoodEntry result = default(BuffFoodEntry); if (!SpeciesTable.TryResolve(table, speciesId, ref result, (string)null)) { return null; } return result; } public static BuffFoodDef Match(BuffFoodEntry entry, int itemId, string itemName) { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) if (entry?.Foods == null) { return null; } foreach (BuffFoodDef food in entry.Foods) { ItemKey val = new ItemKey(food.Key, food.ItemId); if (((ItemKey)(ref val)).Matches(itemId, itemName)) { return food; } } return null; } public static BuffFoodDef DefFor(BuffFoodEntry entry, string slotKey) { if (entry?.Foods == null || string.IsNullOrEmpty(slotKey)) { return null; } foreach (BuffFoodDef food in entry.Foods) { if (string.Equals(food.Key, slotKey, StringComparison.OrdinalIgnoreCase)) { return food; } } return null; } public static double Percent(BuffFoodDef def, LoyaltyTier tier) { if (def != null) { return def.PercentPerLevel * (double)PetBuffs.Level(tier); } return 0.0; } public static float DamageFactor(BuffFoodDef def, LoyaltyTier tier, double secondsLeft) { if (def == null || def.Kind != BuffFoodKind.Damage || !(secondsLeft > 0.0)) { return 1f; } return (float)(1.0 + Percent(def, tier) / 100.0); } public static float DecayFraction(BuffFoodDef def, LoyaltyTier tier, double secondsLeft) { if (def == null || def.Kind != BuffFoodKind.DecayRider || !(secondsLeft > 0.0)) { return 0f; } return (float)(Percent(def, tier) / 100.0); } public static double ResolveDuration(BuffFoodDef def, double hungerSecondsPerDay) { return def?.DurationSeconds ?? hungerSecondsPerDay; } public static string Describe(BuffFoodDef def, LoyaltyTier tier, double secondsLeft) { if (def == null) { return "no buff food"; } double num = Percent(def, tier); string text = ((def.Kind == BuffFoodKind.DecayRider) ? string.Format(CultureInfo.InvariantCulture, "+{0:F1}% of total damage as Decay", num) : string.Format(CultureInfo.InvariantCulture, "+{0:F1}% pet damage", num)); return string.Format(CultureInfo.InvariantCulture, "{0}: {1} ({2} lvl {3}, {4:F0}s left)", def.Key, text, tier, PetBuffs.Level(tier), secondsLeft); } } public enum BwIdRegistry { ItemId, EffectPreset } public readonly struct BwIdRange { public int Start { get; } public int End { get; } public BwIdRange(int start, int end) { Start = start; End = end; } public bool Contains(int id) { if (id >= Start) { return id <= End; } return false; } public override string ToString() { return $"{Start}-{End}"; } } public readonly struct BwIdEntry { public int Id { get; } public string Key { get; } public string Family { get; } public BwIdRegistry Registry { get; } public string Name { get; } public BwIdEntry(int id, string key, string family, BwIdRegistry registry, string name) { Id = id; Key = key; Family = family; Registry = registry; Name = name; } public override string ToString() { return $"{Id} {Key} ({Family}, {Registry})"; } } public static class BwIds { public const int SkillHealPet = 87000; public const int SkillHuntAsOne = 87001; public const int SkillPetCommand = 87002; public const int SkillReleasePet = 87003; public const int SkillPetGift = 87004; public const int SkillForTheKill = 87005; public const int SkillScatology = 87006; public const int SkillBeastOfBurden = 87007; public const int SkillWildUnknown = 87008; public const int SkillCommunion = 87009; public const int StatusPetHungry = 87050; public const int StatusPetStarving = 87051; public const int StatusBondFraying = 87052; public const int StatusBondBroken = 87053; public const int StatusBondSteady = 87054; public const int StatusBondDevoted = 87055; public const int StatusPetCold = 87056; public const int StatusPetFreezing = 87057; public const int StatusPetHot = 87058; public const int StatusPetOverheating = 87059; public const int StatusPetScent = 87060; public const int StatusSynergy = 87061; public const int StatusKillFavor = 87062; public const int StatusCommunionBroken = 87063; public const int StatusCommunionFraying = 87064; public const int StatusCommunionSteady = 87065; public const int StatusCommunionDevoted = 87066; public const int StatusBuffCrystalPowder = 87067; public const int StatusBuffAmbraine = 87068; public const int StatusBuffGaberryWine = 87069; public const int StatusBuffDarkStone = 87070; public const int BlanketHeatingBlanket = 87100; public const int BlanketCoolingBlanket = 87101; public const int SigilFire = 87150; public const int SigilFrost = 87151; public const int SigilAir = 87152; public const int SigilBlood = 87153; public const int FeatherTattered = 87200; public const int FeatherRuffled = 87201; public const int FeatherSleek = 87202; public const int FeatherResplendent = 87203; public const int FletchTatteredPhysical = 87220; public const int FletchRuffledPhysical = 87221; public const int FletchSleekPhysical = 87222; public const int FletchResplendentRaw = 87223; public const int FletchTatteredEthereal = 87224; public const int FletchTatteredDecay = 87225; public const int FletchTatteredElectric = 87226; public const int FletchTatteredFrost = 87227; public const int FletchTatteredFire = 87228; public const int FletchRuffledEthereal = 87229; public const int FletchRuffledDecay = 87230; public const int FletchRuffledElectric = 87231; public const int FletchRuffledFrost = 87232; public const int FletchRuffledFire = 87233; public const int FletchSleekEthereal = 87234; public const int FletchSleekDecay = 87235; public const int FletchSleekElectric = 87236; public const int FletchSleekFrost = 87237; public const int FletchSleekFire = 87238; public const int TamingHyenaChow = 87300; public const int TamingHyenaScroll = 87301; public const int TamingPearlbirdChow = 87302; public const int TamingPearlbirdScroll = 87303; public const int TamingVeaberChow = 87304; public const int TamingVeaberScroll = 87305; public static readonly BwIdEntry[] All = new BwIdEntry[66] { new BwIdEntry(87000, "skill.heal-pet", "skills", BwIdRegistry.ItemId, "Heal Pet"), new BwIdEntry(87001, "skill.hunt-as-one", "skills", BwIdRegistry.ItemId, "Hunt as One"), new BwIdEntry(87002, "skill.pet-command", "skills", BwIdRegistry.ItemId, "Command Pet"), new BwIdEntry(87003, "skill.release-pet", "skills", BwIdRegistry.ItemId, "Release Pet"), new BwIdEntry(87004, "skill.pet-gift", "skills", BwIdRegistry.ItemId, "Gift of the Wild"), new BwIdEntry(87005, "skill.for-the-kill", "skills", BwIdRegistry.ItemId, "For the Kill"), new BwIdEntry(87006, "skill.scatology", "skills", BwIdRegistry.ItemId, "Scatology"), new BwIdEntry(87007, "skill.beast-of-burden", "skills", BwIdRegistry.ItemId, "Beast of Burden"), new BwIdEntry(87008, "skill.wild-unknown", "skills", BwIdRegistry.ItemId, "Wild Unknown"), new BwIdEntry(87009, "skill.communion", "skills", BwIdRegistry.ItemId, "Communion"), new BwIdEntry(87050, "status.pet-hungry", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87051, "status.pet-starving", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87052, "status.bond-fraying", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87053, "status.bond-broken", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87054, "status.bond-steady", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87055, "status.bond-devoted", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87056, "status.pet-cold", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87057, "status.pet-freezing", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87058, "status.pet-hot", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87059, "status.pet-overheating", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87060, "status.pet-scent", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87061, "status.synergy", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87062, "status.kill-favor", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87063, "status.communion-broken", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87064, "status.communion-fraying", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87065, "status.communion-steady", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87066, "status.communion-devoted", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87067, "status.buff-crystal-powder", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87068, "status.buff-ambraine", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87069, "status.buff-gaberry-wine", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87070, "status.buff-dark-stone", "statusPresets", BwIdRegistry.EffectPreset, null), new BwIdEntry(87100, "blanket.heating-blanket", "blankets", BwIdRegistry.ItemId, "Heating Blanket"), new BwIdEntry(87101, "blanket.cooling-blanket", "blankets", BwIdRegistry.ItemId, "Cooling Blanket"), new BwIdEntry(87150, "sigil.fire", "sigils", BwIdRegistry.ItemId, "Pet Fire Sigil"), new BwIdEntry(87151, "sigil.frost", "sigils", BwIdRegistry.ItemId, "Pet Frost Sigil"), new BwIdEntry(87152, "sigil.air", "sigils", BwIdRegistry.ItemId, "Pet Wind Sigil"), new BwIdEntry(87153, "sigil.blood", "sigils", BwIdRegistry.ItemId, "Pet Blood Sigil"), new BwIdEntry(87200, "feather.tattered", "feathers", BwIdRegistry.ItemId, "Tattered Pearlbird Feather"), new BwIdEntry(87201, "feather.ruffled", "feathers", BwIdRegistry.ItemId, "Ruffled Pearlbird Feather"), new BwIdEntry(87202, "feather.sleek", "feathers", BwIdRegistry.ItemId, "Sleek Pearlbird Feather"), new BwIdEntry(87203, "feather.resplendent", "feathers", BwIdRegistry.ItemId, "Resplendent Pearlbird Feather"), new BwIdEntry(87220, "fletch.tattered-physical", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87221, "fletch.ruffled-physical", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87222, "fletch.sleek-physical", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87223, "fletch.resplendent-raw", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87224, "fletch.tattered-ethereal", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87225, "fletch.tattered-decay", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87226, "fletch.tattered-electric", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87227, "fletch.tattered-frost", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87228, "fletch.tattered-fire", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87229, "fletch.ruffled-ethereal", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87230, "fletch.ruffled-decay", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87231, "fletch.ruffled-electric", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87232, "fletch.ruffled-frost", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87233, "fletch.ruffled-fire", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87234, "fletch.sleek-ethereal", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87235, "fletch.sleek-decay", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87236, "fletch.sleek-electric", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87237, "fletch.sleek-frost", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87238, "fletch.sleek-fire", "fletchEnchantments", BwIdRegistry.EffectPreset, null), new BwIdEntry(87300, "taming.hyena.chow", "taming", BwIdRegistry.ItemId, "Hyena Chow"), new BwIdEntry(87301, "taming.hyena.scroll", "taming", BwIdRegistry.ItemId, "Recipe: Hyena Chow"), new BwIdEntry(87302, "taming.pearlbird.chow", "taming", BwIdRegistry.ItemId, "Pearlbird Chow"), new BwIdEntry(87303, "taming.pearlbird.scroll", "taming", BwIdRegistry.ItemId, "Recipe: Pearlbird Chow"), new BwIdEntry(87304, "taming.veaber.chow", "taming", BwIdRegistry.ItemId, "Veaber Chow"), new BwIdEntry(87305, "taming.veaber.scroll", "taming", BwIdRegistry.ItemId, "Recipe: Veaber Chow") }; public static readonly BwIdRange[] Pool = new BwIdRange[1] { new BwIdRange(87000, 87999) }; public static bool TryGet(string key, out int id) { BwIdEntry[] all = All; for (int i = 0; i < all.Length; i++) { BwIdEntry bwIdEntry = all[i]; if (bwIdEntry.Key == key) { id = bwIdEntry.Id; return true; } } id = 0; return false; } public static bool TryFind(int id, out BwIdEntry entry) { BwIdEntry[] all = All; for (int i = 0; i < all.Length; i++) { BwIdEntry bwIdEntry = all[i]; if (bwIdEntry.Id == id) { entry = bwIdEntry; return true; } } entry = default(BwIdEntry); return false; } public static bool InPool(int id) { BwIdRange[] pool = Pool; foreach (BwIdRange bwIdRange in pool) { if (bwIdRange.Contains(id)) { return true; } } return false; } public static bool IsOurs(int id) { BwIdEntry entry; return TryFind(id, out entry); } public static IEnumerable Lines() { BwIdRange[] pool = Pool; foreach (BwIdRange bwIdRange in pool) { yield return $"pool {bwIdRange}"; } BwIdEntry[] all = All; for (int i = 0; i < all.Length; i++) { BwIdEntry bwIdEntry = all[i]; yield return bwIdEntry.ToString(); } } } public enum ComfortGate { NeverSampled, Ok, SystemOff, NoPetState, NoPlayer, NoPetSim, GameplayPaused, NoEnvironment, NoSampleTransform } public enum AmbientSource { None, Environment, Rediscovered } public static class ComfortGates { public static ComfortGate Evaluate(bool systemEnabled, bool hasPetState, bool hasEnvironment, bool hasSampleTransform) { if (!systemEnabled) { return ComfortGate.SystemOff; } if (!hasPetState) { return ComfortGate.NoPetState; } if (!hasEnvironment) { return ComfortGate.NoEnvironment; } if (!hasSampleTransform) { return ComfortGate.NoSampleTransform; } return ComfortGate.Ok; } public static string Code(ComfortGate gate) { return gate switch { ComfortGate.Ok => "ok", ComfortGate.SystemOff => "system-off", ComfortGate.NoPetState => "no-pet-state", ComfortGate.NoPlayer => "no-player", ComfortGate.NoPetSim => "no-pet-sim", ComfortGate.GameplayPaused => "gameplay-paused", ComfortGate.NoEnvironment => "no-env", ComfortGate.NoSampleTransform => "no-transform", _ => "never-sampled", }; } public static bool IsFault(ComfortGate gate) { if (gate != ComfortGate.Ok && gate != ComfortGate.SystemOff) { return gate != ComfortGate.GameplayPaused; } return false; } public static AmbientSource PreferredAmbientSource(bool envAvailable, bool rediscoveredAvailable) { if (envAvailable) { return AmbientSource.Environment; } if (rediscoveredAvailable) { return AmbientSource.Rediscovered; } return AmbientSource.None; } public static string SourceLabel(AmbientSource source) { return source switch { AmbientSource.Environment => "environment", AmbientSource.Rediscovered => "rediscovered-environment", _ => "none", }; } } public static class CommunionBadge { public const string NoBenefit = "no benefit yet — deepen the bond"; public static string Describe(IReadOnlyList benefits) { if (benefits == null || benefits.Count == 0) { return "no benefit yet — deepen the bond"; } return string.Join(", ", from b in benefits orderby (int)b.Stat select "+" + b.Amount.ToString("0.##", CultureInfo.InvariantCulture) + (PetBuffs.IsFlat(b.Stat) ? " " : "% ") + StatName(b.Stat)); } public static string Description(LoyaltyTier tier, string benefits) { object obj = tier switch { LoyaltyTier.Gone => "The communion is all but severed.", LoyaltyTier.Broken => "The communion is all but severed.", LoyaltyTier.Fraying => "The communion is fraying.", LoyaltyTier.Steady => "The communion holds steady.", LoyaltyTier.Devoted => "The communion runs deep.", _ => "The communion endures.", }; string text = ((string.IsNullOrEmpty(benefits) || benefits == "no benefit yet — deepen the bond") ? "No benefit yet — deepen the bond." : ("The bond grants you " + benefits + ".")); return (string?)obj + " " + text; } private static string StatName(PetBuffStat stat) { return PetBuffs.DisplayName(stat); } } internal static class EnumRead { public static bool TryName(string s, out T value) where T : struct { value = default(T); if (string.IsNullOrEmpty(s)) { return false; } if (!Enum.TryParse(s, ignoreCase: true, out var result)) { return false; } if (!Enum.IsDefined(typeof(T), result)) { return false; } value = result; return true; } } public sealed class TemperatureExposure { private double _timer; private int _dir; public double EscalateSeconds { get; } public double RecoverSeconds { get; } public ComfortStage Stage { get; private set; } public TemperatureExposure(double escalateSeconds = 30.0, double recoverSeconds = 15.0, ComfortStage startStage = ComfortStage.Comfortable) { EscalateSeconds = escalateSeconds; RecoverSeconds = recoverSeconds; Stage = startStage; } public ComfortStage Tick(int stepsOutside, double dt, double escalateMult = 1.0) { ComfortStage comfortStage = Temperature.Stage(stepsOutside); int num = comfortStage.CompareTo(Stage); if (num == 0) { _timer = 0.0; _dir = 0; return Stage; } if (num != _dir) { _timer = 0.0; _dir = num; } _timer += dt; double num2 = ((num > 0) ? (EscalateSeconds * Math.Max(1.0, escalateMult)) : RecoverSeconds); while (_timer >= num2 && comfortStage.CompareTo(Stage) == num) { _timer -= num2; Stage += num; } if (Stage == comfortStage) { _timer = 0.0; _dir = 0; } return Stage; } public ComfortStage Tick(TempStep temp, ComfortBand band, double dt) { return Tick(Temperature.StepsOutside(temp, band), dt); } } public sealed class HungerTimer { private int _daysApplied; public double SecondsPerDay { get; } public double SecondsSinceFed { get; private set; } public HungerTimer(double secondsPerDay, double secondsSinceFed = 0.0) { SecondsPerDay = secondsPerDay; SecondsSinceFed = secondsSinceFed; _daysApplied = ((secondsPerDay > 0.0) ? ((int)(secondsSinceFed / secondsPerDay)) : 0); } public void Feed() { SecondsSinceFed = 0.0; _daysApplied = 0; } public void Seed(double secondsSinceFed) { SecondsSinceFed = ((secondsSinceFed < 0.0) ? 0.0 : secondsSinceFed); _daysApplied = ((SecondsPerDay > 0.0) ? ((int)(SecondsSinceFed / SecondsPerDay)) : 0); } public int Tick(double dt) { SecondsSinceFed += dt; if (SecondsPerDay <= 0.0) { return 0; } int num = (int)(SecondsSinceFed / SecondsPerDay); int result = num - _daysApplied; _daysApplied = num; return result; } } public sealed class FletchEntry { public string Key; public string ArrowKey; public int? ArrowId; } public sealed class FeatherQuality { public string Quality; public LoyaltyTier Tier; public int Percent; public int FlatX; public int ItemId; public int EnchantmentId; public string ItemName => Quality + " Pearlbird Feather"; } public static class FeatherFletching { public struct StackSlot { public string Key; public int FreeSpace; public StackSlot(string key, int freeSpace) { Key = key ?? string.Empty; FreeSpace = freeSpace; } } public const int PhysicalDamageType = 0; public const int RawDamageType = 8; public const int ElementalVariantBase = 87224; private const int ElementalVariantCount = 15; public static readonly FeatherQuality[] Qualities = new FeatherQuality[4] { new FeatherQuality { Quality = "Tattered", Tier = LoyaltyTier.Broken, Percent = 10, FlatX = 2, ItemId = 87200, EnchantmentId = 87220 }, new FeatherQuality { Quality = "Ruffled", Tier = LoyaltyTier.Fraying, Percent = 20, FlatX = 3, ItemId = 87201, EnchantmentId = 87221 }, new FeatherQuality { Quality = "Sleek", Tier = LoyaltyTier.Steady, Percent = 30, FlatX = 5, ItemId = 87202, EnchantmentId = 87222 }, new FeatherQuality { Quality = "Resplendent", Tier = LoyaltyTier.Devoted, Percent = 40, FlatX = 7, ItemId = 87203, EnchantmentId = 87223 } }; public static FeatherQuality QualityFor(int loyalty) { LoyaltyTier loyaltyTier = Loyalty.Tier(loyalty); for (int num = Qualities.Length - 1; num > 0; num--) { if (loyaltyTier >= Qualities[num].Tier) { return Qualities[num]; } } return Qualities[0]; } public static int? PercentForItemId(int itemId) { FeatherQuality[] qualities = Qualities; foreach (FeatherQuality featherQuality in qualities) { if (featherQuality.ItemId == itemId) { return featherQuality.Percent; } } return null; } public static FeatherQuality QualityForItemId(int itemId) { FeatherQuality[] qualities = Qualities; foreach (FeatherQuality featherQuality in qualities) { if (featherQuality.ItemId == itemId) { return featherQuality; } } return null; } private static int IndexOf(FeatherQuality q) { if (q == null) { return -1; } for (int i = 0; i < Qualities.Length; i++) { if (Qualities[i] == q || Qualities[i].EnchantmentId == q.EnchantmentId) { return i; } } return -1; } public static int EnchantmentIdFor(FeatherQuality q, int damageType) { int num = IndexOf(q); if (num < 0) { return 0; } if (num == Qualities.Length - 1) { return q.EnchantmentId; } if (damageType <= 0 || damageType > 5) { return q.EnchantmentId; } return 87224 + num * 5 + (damageType - 1); } public static FeatherQuality QualityForEnchantmentId(int enchantmentId) { FeatherQuality[] qualities = Qualities; foreach (FeatherQuality featherQuality in qualities) { if (featherQuality.EnchantmentId == enchantmentId) { return featherQuality; } } if (enchantmentId >= 87224 && enchantmentId < 87239) { int num = (enchantmentId - 87224) / 5; if (num >= 0 && num < Qualities.Length) { return Qualities[num]; } } return null; } public static int? FlatTypeForEnchantmentId(int enchantmentId) { FeatherQuality featherQuality = Qualities[Qualities.Length - 1]; if (enchantmentId == featherQuality.EnchantmentId) { return 8; } for (int i = 0; i < Qualities.Length - 1; i++) { if (Qualities[i].EnchantmentId == enchantmentId) { return 0; } } if (enchantmentId >= 87224 && enchantmentId < 87239) { return (enchantmentId - 87224) % 5 + 1; } return null; } public static int PickFlatType(IList presentTypes, double roll) { if (presentTypes == null || presentTypes.Count == 0) { return 0; } List list = new List(presentTypes.Count); foreach (int presentType in presentTypes) { if (presentType >= 0 && presentType <= 5) { list.Add(presentType); } } if (list.Count == 0) { return 0; } if (list.Count == 1) { return list[0]; } int num = (int)(roll * (double)list.Count); if (num < 0) { num = 0; } if (num >= list.Count) { num = list.Count - 1; } return list[num]; } public static bool IsFletchEnchantment(int enchantmentId) { return QualityForEnchantmentId(enchantmentId) != null; } public static bool CanUpgradeFletch(int? existingPercent, int newPercent) { if (existingPercent.HasValue) { return newPercent > existingPercent.Value; } return true; } public static string DisplayNameFor(string baseName, int percent) { if (percent > 0) { return $"{baseName} (+{ClampPercent(percent)}%)"; } return baseName; } public static Dictionary Parse(string json, Action warn = null) { return JsonTable.Read(json, warn, "arrow", "an object of arrow → fletch objects", ParseEntry); } private static FletchEntry ParseEntry(string key, object value, Action warn) { FieldReader fieldReader = new FieldReader("'" + key + "'", warn); if (!(value is Dictionary dictionary)) { fieldReader.Say("value must be an object — arrow skipped."); return null; } FletchEntry fletchEntry = new FletchEntry { Key = key }; ItemKey val = default(ItemKey); foreach (KeyValuePair item in dictionary) { if (item.Key.ToLowerInvariant() == "arrow") { if (ItemKey.TryRead(item.Value, ref val)) { fletchEntry.ArrowKey = ((ItemKey)(ref val)).Key; fletchEntry.ArrowId = ((ItemKey)(ref val)).ItemId; } else { fieldReader.Wrong("arrow", "a number (ItemID) or non-empty string (display name)"); } } else { fieldReader.Unknown(item.Key, "arrow"); } } if (fletchEntry.ArrowKey == null) { fieldReader.Say("missing required 'arrow' — arrow skipped."); return null; } return fletchEntry; } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } public static int ClampPercent(int percent) { if (percent >= 0) { if (percent <= 100) { return percent; } return 100; } return 0; } public static string EnchantmentSetKey(IEnumerable enchantmentIds) { if (enchantmentIds == null) { return string.Empty; } List list = new List(enchantmentIds); if (list.Count == 0) { return string.Empty; } list.Sort(); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < list.Count; i++) { if (i > 0) { stringBuilder.Append(';'); } stringBuilder.Append(list[i].ToString(CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } public static bool ShouldMergeEnchantedStack(string incomingKey, IEnumerable sameIdStacks, out bool anyDifferentPartial, out int sameKeyCapacity) { anyDifferentPartial = false; sameKeyCapacity = 0; incomingKey = incomingKey ?? string.Empty; bool result = false; if (sameIdStacks != null) { foreach (StackSlot sameIdStack in sameIdStacks) { if (sameIdStack.FreeSpace > 0) { if (string.Equals(sameIdStack.Key ?? string.Empty, incomingKey, StringComparison.Ordinal)) { result = true; sameKeyCapacity += sameIdStack.FreeSpace; } else { anyDifferentPartial = true; } } } } return result; } } public static class FeedLadder { public const string Relic = "relic"; public const string BuffFood = "bufffood"; public const string Meal = "meal"; public static readonly IReadOnlyList Order = new string[3] { "relic", "bufffood", "meal" }; public static string Describe(IReadOnlyList order = null) { IReadOnlyList readOnlyList = order ?? Order; List list = new List(readOnlyList.Count); for (int i = 0; i < readOnlyList.Count; i++) { list.Add($"{i + 1} {readOnlyList[i]}"); } return string.Join(" -> ", list.ToArray()); } public static string Mismatch(IReadOnlyList registered) { if (registered == null) { return "the feed registry is null"; } if (registered.Count == Order.Count) { bool flag = true; for (int i = 0; i < Order.Count; i++) { if (!string.Equals(registered[i], Order[i], StringComparison.Ordinal)) { flag = false; break; } } if (flag) { return null; } } return "registered [" + Describe(registered) + "] but the declared ladder is [" + Describe() + "]"; } } public readonly struct DualListedFood { public string Species { get; } public string BuffKey { get; } public string DietKey { get; } public bool ByCategory { get; } public DualListedFood(string species, string buffKey, string dietKey, bool byCategory) { Species = species; BuffKey = buffKey; DietKey = dietKey; ByCategory = byCategory; } } public readonly struct FeedPlan { public bool FeedsAsMeal { get; } public bool AppliesBuff { get; } public bool IsMealPlusBuff { get { if (FeedsAsMeal) { return AppliesBuff; } return false; } } public int ConsumesOnAccept { get { if (!FeedsAsMeal && !AppliesBuff) { return 0; } return 1; } } public FeedPlan(bool feedsAsMeal, bool appliesBuff) { FeedsAsMeal = feedsAsMeal; AppliesBuff = appliesBuff; } } public static class FeedPrecedence { public static FoodEntry MatchingDietRow(IReadOnlyList diet, int itemId, string itemName, IReadOnlyCollection itemCategories) { if (diet == null) { return null; } foreach (FoodEntry item in diet) { if (item != null && PetDiet.Matches(item, itemId, itemName, itemCategories)) { return item; } } return null; } public static bool IsMeal(IReadOnlyList diet, int itemId, string itemName, IReadOnlyCollection itemCategories) { return MatchingDietRow(diet, itemId, itemName, itemCategories) != null; } public static FeedPlan Plan(bool hasBuffRow, FoodEntry dietRow) { return new FeedPlan(dietRow != null, hasBuffRow); } public static bool BuffFoodApplies(bool hasBuffRow, FoodEntry dietRow) { return hasBuffRow; } public static List DualListed(BuffFoodEntry entry, IReadOnlyList diet, Func> categoriesOf) { List list = new List(); if (entry?.Foods == null || diet == null) { return list; } foreach (BuffFoodDef food in entry.Foods) { if (food != null) { IReadOnlyCollection itemCategories = categoriesOf?.Invoke(food); FoodEntry foodEntry = MatchingDietRow(diet, food.ItemId.GetValueOrDefault(), food.Key, itemCategories); if (foodEntry != null) { list.Add(new DualListedFood(entry.Species, food.Key, foodEntry.Key, foodEntry.Category != null)); } } } return list; } } public readonly struct FieldReader { private readonly string _context; private readonly Action _warn; public string Context => _context; public Action Sink => _warn; public FieldReader(string context, Action warn) { _context = context; _warn = warn; } public void Say(string message) { if (_warn != null) { _warn(string.IsNullOrEmpty(_context) ? message : (_context + ": " + message)); } } public void Unknown(string key, string valid) { Say("unknown key '" + key + "' (valid: " + valid + ")."); } public void Wrong(string field, string expected, string fallback = null) { Say("'" + field + "' must be " + expected + (string.IsNullOrEmpty(fallback) ? "" : (" — " + fallback)) + "."); } public bool Num(string field, object value, out double result, string expected = null, string fallback = null) { if (value is double num) { result = num; return true; } result = 0.0; Wrong(field, expected ?? "a number", fallback); return false; } public bool Int(string field, object value, out int result, string expected = null, string fallback = null) { result = 0; if (!Num(field, value, out var result2, expected, fallback)) { return false; } result = (int)result2; return true; } public bool Str(string field, object value, out string result, string expected = null, string fallback = null) { if (value is string text && text.Trim().Length > 0) { result = text.Trim(); return true; } result = null; Wrong(field, expected ?? "a non-empty string", fallback); return false; } public bool Bool(string field, object value, out bool result, string expected = null, string fallback = null) { if (value is bool flag) { result = flag; return true; } result = false; Wrong(field, expected ?? "true or false", fallback); return false; } public bool Items(string field, object value, out List result, string expected = null, string fallback = null) { if (value is List list) { result = list; return true; } result = null; Wrong(field, expected ?? "an ARRAY", fallback); return false; } public bool Obj(string field, object value, out Dictionary result, string expected = null, string fallback = null) { if (value is Dictionary dictionary) { result = dictionary; return true; } result = null; Wrong(field, expected ?? "an object", fallback); return false; } public bool Enum(string field, object value, out T result, string expected = null, string fallback = null) where T : struct { if (value is string text && EnumRead.TryName(text.Trim(), out result)) { return true; } result = default(T); Wrong(field, expected ?? ("one of " + string.Join(", ", System.Enum.GetNames(typeof(T)))), fallback); return false; } } public static class FoodCategories { public const string Meat = "Meat"; public const string Fish = "Fish"; public const string Vegetable = "Vegetable"; public const string Egg = "Egg"; public const string Bread = "Bread"; public const string Mushroom = "Mushroom"; public const string RationIngredient = "RationIngredient"; public const string Water = "Water"; public static readonly string[] All = new string[8] { "Meat", "Fish", "Vegetable", "Egg", "Bread", "Mushroom", "RationIngredient", "Water" }; public static bool TryCanonical(string raw, out string canonical) { canonical = null; if (string.IsNullOrEmpty(raw)) { return false; } string text = Squash(raw); if (text.Length == 0) { return false; } string[] all = All; foreach (string text2 in all) { if (string.Equals(text, Squash(text2), StringComparison.OrdinalIgnoreCase)) { canonical = text2; return true; } } return false; } public static string Label(string category) { return "any " + Spaced(category); } public static string Spaced(string category) { if (string.IsNullOrEmpty(category)) { return category; } StringBuilder stringBuilder = new StringBuilder(category.Length + 4); for (int i = 0; i < category.Length; i++) { if (i > 0 && char.IsUpper(category[i]) && !char.IsUpper(category[i - 1])) { stringBuilder.Append(' '); } stringBuilder.Append(category[i]); } return stringBuilder.ToString(); } public static string ValidList() { return string.Join(", ", All); } private static string Squash(string s) { StringBuilder stringBuilder = new StringBuilder(s.Length); foreach (char c in s) { if (c != ' ' && c != '-' && c != '_') { stringBuilder.Append(c); } } return stringBuilder.ToString(); } } public sealed class FoodHexMeal { public string Key; public int? ItemId; public string HexId; } public sealed class FoodHexEntry { public string Species; public double? BuildUpPercent; public int? Window; public List Meals = new List(); } public struct HexBuildUp { public string HexId; public float Percent; public int MealCount; } public static class FoodHexes { public const int MaxWindow = 20; public const int DefaultWindow = 3; public const float DefaultBuildUpPercent = 20f; public static Dictionary Parse(string json, Action warn = null) { return JsonTable.Read(json, warn, "species", "an object of species → food-hex objects", ParseEntry); } private static FoodHexEntry ParseEntry(string species, object value, Action warn) { FieldReader f = new FieldReader("'" + species + "'", warn); if (!(value is Dictionary dictionary)) { f.Say("value must be an object — species skipped."); return null; } FoodHexEntry foodHexEntry = new FoodHexEntry { Species = species }; foreach (KeyValuePair item in dictionary) { switch (item.Key.ToLowerInvariant()) { case "builduppercent": { if (f.Num("buildUpPercent", item.Value, out var result2, "a number > 0", "using the config default")) { if (result2 > 0.0) { foodHexEntry.BuildUpPercent = result2; } else { f.Wrong("buildUpPercent", "a number > 0", "using the config default"); } } break; } case "window": { if (!f.Num("window", item.Value, out var result3, $"a number 1..{20}", "using the config default")) { break; } if (result3 >= 1.0) { foodHexEntry.Window = (int)result3; if (foodHexEntry.Window > 20) { f.Say($"'window' {foodHexEntry.Window} exceeds the hard max {20} — clamped."); foodHexEntry.Window = 20; } } else { f.Wrong("window", $"a number 1..{20}", "using the config default"); } break; } case "meals": { if (!f.Obj("meals", item.Value, out Dictionary result, "an OBJECT of food-key → hex-name pairs")) { break; } foreach (KeyValuePair item2 in result) { FoodHexMeal foodHexMeal = ParseMeal(f, item2.Key, item2.Value); if (foodHexMeal != null) { foodHexEntry.Meals.Add(foodHexMeal); } } break; } default: f.Unknown(item.Key, "buildUpPercent, window, meals"); break; } } if (foodHexEntry.Meals.Count == 0) { f.Say("missing/empty required 'meals' — species skipped."); return null; } return foodHexEntry; } private static FoodHexMeal ParseMeal(FieldReader f, string key, object value) { string text = (key ?? "").Trim(); if (text.Length == 0) { f.Say("empty meal key — entry skipped."); return null; } if (text.IndexOf('|') >= 0 || text.IndexOf('\t') >= 0 || text.IndexOf('\n') >= 0 || text.IndexOf('\r') >= 0) { f.Say("meal key '" + text + "' contains a reserved character ('|', tab, or newline) — entry skipped."); return null; } if (!(value is string text2) || text2.Trim().Length == 0) { f.Say("meal '" + text + "' must map to a non-empty status-effect name string — entry skipped."); return null; } ItemKey val = default(ItemKey); ItemKey.TryRead((object)text, ref val); return new FoodHexMeal { Key = ((ItemKey)(ref val)).Key, HexId = text2.Trim(), ItemId = ((ItemKey)(ref val)).ItemId }; } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } public static FoodHexEntry Resolve(Dictionary table, string speciesId) { FoodHexEntry result = default(FoodHexEntry); if (!SpeciesTable.TryResolve(table, speciesId, ref result, (string)null)) { return null; } return result; } public static string MatchMeal(FoodHexEntry entry, int itemId, string itemName) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) if (entry == null) { return null; } foreach (FoodHexMeal meal in entry.Meals) { ItemKey val = new ItemKey(meal.Key, meal.ItemId); if (((ItemKey)(ref val)).Matches(itemId, itemName)) { return meal.Key; } } return null; } public static int ClampWindow(int window) { if (window >= 1) { if (window <= 20) { return window; } return 20; } return 1; } public static void RecordMeal(List history, string mealKey) { if (history != null && !string.IsNullOrEmpty(mealKey)) { history.Add(mealKey); while (history.Count > 20) { history.RemoveAt(0); } } } public static List ComputeBuildups(FoodHexEntry entry, IReadOnlyList history, int configWindow, float configPercent) { List list = new List(); if (entry == null || history == null || history.Count == 0) { return list; } int num = ClampWindow(entry.Window ?? configWindow); float num2 = (float)(entry.BuildUpPercent ?? ((double)configPercent)); if (num2 <= 0f) { return list; } List list2 = new List(); Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); int num3 = 0; int num4 = history.Count - 1; while (num4 >= 0 && num3 < num) { string text = HexFor(entry, history[num4]); if (text != null) { num3++; if (dictionary.TryGetValue(text, out var value)) { dictionary[text] = value + 1; } else { dictionary[text] = 1; list2.Add(text); } } num4--; } foreach (string item in list2) { list.Add(new HexBuildUp { HexId = item, Percent = (float)dictionary[item] * num2, MealCount = dictionary[item] }); } return list; } private static string HexFor(FoodHexEntry entry, string mealKey) { if (string.IsNullOrEmpty(mealKey)) { return null; } foreach (FoodHexMeal meal in entry.Meals) { if (string.Equals(meal.Key, mealKey, StringComparison.OrdinalIgnoreCase)) { return meal.HexId; } } return null; } } public enum KillFavorStat { StaminaCostReduction, ManaCostReduction, Protection } public sealed class KillFavorDef { public KillFavorStat Stat; public float[] AmountByTier; } public sealed class ForTheKillEntry { public string Species; public string StatusName; public double? BuildupPercent; public KillFavorDef KillBuff; } public static class KillFavor { public const string StatusIdentifier = "BW_KillFavor"; public const int StatusNumId = 87062; public static float Amount(KillFavorDef def, LoyaltyTier tier) { if (def?.AmountByTier == null || tier == LoyaltyTier.Gone) { return 0f; } int num = (int)(tier - 1); if (num < 0 || num >= def.AmountByTier.Length) { return 0f; } return def.AmountByTier[num]; } } public static class ForTheKill { public const double MinBuildup = 1.0; public const double MaxBuildup = 100.0; public static Dictionary Parse(string json, Action warn = null) { return JsonTable.Read(json, warn, "species", "an object of species → forTheKill objects", ParseEntry); } private static ForTheKillEntry ParseEntry(string speciesKey, object value, Action warn) { FieldReader f = new FieldReader("'" + speciesKey + "'", warn); if (!(value is Dictionary dictionary)) { f.Say("value must be an object — species skipped."); return null; } ForTheKillEntry forTheKillEntry = new ForTheKillEntry { Species = speciesKey }; foreach (KeyValuePair item in dictionary) { switch (item.Key.ToLowerInvariant()) { case "status": { if (f.Str("status", item.Value, out string result2, "a non-empty status name string")) { forTheKillEntry.StatusName = result2; } break; } case "buildup": { if (f.Num("buildup", item.Value, out var result, null, "ignored (direct apply)")) { if (result < 1.0) { f.Say($"'buildup' {result} below {1.0} — clamped."); result = 1.0; } if (result > 100.0) { f.Say($"'buildup' {result} above {100.0} — clamped."); result = 100.0; } forTheKillEntry.BuildupPercent = result; } break; } case "killbuff": forTheKillEntry.KillBuff = ParseKillBuff(f, item.Value); break; default: f.Unknown(item.Key, "status, buildup, killBuff"); break; } } if (string.IsNullOrEmpty(forTheKillEntry.StatusName)) { f.Say("missing required 'status' — species skipped."); return null; } return forTheKillEntry; } private static KillFavorDef ParseKillBuff(FieldReader f, object value) { if (!(value is Dictionary dictionary)) { f.Wrong("killBuff", "an object", "ignored (strike only)"); return null; } KillFavorStat? killFavorStat = null; float[] array = null; foreach (KeyValuePair item in dictionary) { string text = item.Key.ToLowerInvariant(); KillFavorStat value2; if (!(text == "stat")) { if (text == "amounts") { if (item.Value is List { Count: 4 } list) { array = new float[4]; for (int i = 0; i < 4; i++) { array[i] = ((list[i] is double num) ? ((float)num) : 0f); } } else { f.Say("killBuff 'amounts' must be a 4-number array (Broken, Fraying, Steady, Devoted) — ignored."); } } else { f.Say("unknown killBuff key '" + item.Key + "' (valid: stat, amounts)."); } } else if (item.Value is string text2 && EnumRead.TryName(text2.Trim(), out value2)) { killFavorStat = value2; } else { f.Say($"killBuff 'stat' '{item.Value}' unknown " + "(valid: " + string.Join(", ", Enum.GetNames(typeof(KillFavorStat))) + ")."); } } if (!killFavorStat.HasValue || array == null) { f.Say("killBuff needs both 'stat' and a 4-number 'amounts' — ignored (strike only)."); return null; } return new KillFavorDef { Stat = killFavorStat.Value, AmountByTier = array }; } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } public static ForTheKillEntry Resolve(Dictionary table, string speciesId) { ForTheKillEntry result = default(ForTheKillEntry); if (!SpeciesTable.TryResolve(table, speciesId, ref result, (string)null)) { return null; } return result; } } public static class HealthRecovery { public const int MinLevel = 1; public const int MaxLevel = 5; public const int DefaultLevel = 1; public const double DurationSeconds = 600.0; private static readonly double[] Rates = new double[5] { 0.2, 0.25, 0.3, 0.4, 0.5 }; public static double RatePerSecond(int level) { if (level > 0) { return Rates[((level > 5) ? 5 : level) - 1]; } return 0.0; } public static int ClampLevel(int level) { if (level > 0) { if (level >= 1) { if (level <= 5) { return level; } return 5; } return 1; } return 0; } public static int EffectiveLevel(int? foodLevel, bool isTamingFood) { if (!isTamingFood) { return ClampLevel(foodLevel ?? 1); } return 5; } public static double TickHeal(int level, double dt) { if (!(dt > 0.0)) { return 0.0; } return RatePerSecond(level) * dt; } public static double TotalHeal(int level) { return RatePerSecond(level) * 600.0; } public static string Describe(int level, double secondsLeft) { return string.Format(CultureInfo.InvariantCulture, "level {0} (+{1} HP/s, {2}s left)", level, RatePerSecond(level), Math.Round(secondsLeft)); } } public static class HuntCooldown { public const float DivergenceEpsilon = 0.25f; public static float WantSkillCooldown(bool syncEnabled, bool hasSpeciesRow, float speciesCooldownSeconds, float baseCooldownSeconds) { float result = AtLeastZero(baseCooldownSeconds); if (!syncEnabled || !hasSpeciesRow) { return result; } return AtLeastZero(speciesCooldownSeconds); } public static float AdoptedCooldownSeconds(float committedRealCooldown, float tableCooldownSeconds) { if (!(committedRealCooldown > 0f)) { return AtLeastZero(tableCooldownSeconds); } return committedRealCooldown; } public static bool Diverged(float skillRemainingSeconds, float petRemainingSeconds, float epsilon = 0.25f) { return Math.Abs(skillRemainingSeconds - petRemainingSeconds) > AtLeastZero(epsilon); } public static double Drain(double secondsLeft, double dt) { if (secondsLeft <= 0.0) { return 0.0; } if (dt <= 0.0) { return secondsLeft; } double num = secondsLeft - dt; if (!(num > 0.0)) { return 0.0; } return num; } public static double EffectiveRemainder(bool syncEnabled, double savedSecondsLeft) { if (!syncEnabled || !(savedSecondsLeft > 0.0)) { return 0.0; } return savedSecondsLeft; } private static float AtLeastZero(float v) { if (!(v > 0f)) { return 0f; } return v; } } public static class JsonTable { public static string DuplicateMessage(string key, string keyNoun) { return "'" + key + "': duplicate " + keyNoun + " key — the later entry wins."; } public static string RootMessage(string rootShape) { return "root must be " + rootShape + "."; } public static Dictionary Read(string json, Action warn, string keyNoun, string rootShape, Func, T> entryFn) where T : class { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); if (!TryReadRoot(json, warn, keyNoun, rootShape, out Dictionary root)) { return dictionary; } ReadInto(root, dictionary, warn, keyNoun, null, entryFn); return dictionary; } public static bool TryReadRoot(string json, Action warn, string keyNoun, string rootShape, out Dictionary root) { root = null; if (string.IsNullOrEmpty(json) || json.Trim().Length == 0) { return false; } object obj; try { obj = Json.Parse(json, (Action)delegate(string k) { warn?.Invoke(DuplicateMessage(k, keyNoun)); }); } catch (FormatException ex) { warn?.Invoke(ex.Message); return false; } if (!(obj is Dictionary dictionary)) { warn?.Invoke(RootMessage(rootShape)); return false; } root = dictionary; return true; } public static void ReadInto(Dictionary source, Dictionary dest, Action warn, string keyNoun, string ctxPrefix, Func, T> entryFn) where T : class { if (source == null || dest == null || entryFn == null) { return; } foreach (KeyValuePair item in source) { T val = entryFn(item.Key, item.Value, warn); if (val != null) { if (dest.ContainsKey(item.Key)) { warn?.Invoke(DuplicateMessage(string.IsNullOrEmpty(ctxPrefix) ? item.Key : (ctxPrefix + "." + item.Key), keyNoun)); } dest[item.Key] = val; } } } } public enum LanternAction { None, Create, Destroy } public static class LanternRules { public static LanternAction Mirror(bool playerHas, bool bodyAlive, bool cloneAlive) { if (playerHas && bodyAlive && !cloneAlive) { return LanternAction.Create; } if (cloneAlive && (!playerHas || !bodyAlive)) { return LanternAction.Destroy; } return LanternAction.None; } } public enum LoyaltyTier { Gone, Broken, Fraying, Steady, Devoted } public enum LoyaltyEvent { FeedPreferred, FeedBondFood, DayWithoutFeeding, PetCriticallyHurt, PetDowned, DefeatedNearbyEnemy, CrossedRegionBonded } public static class Loyalty { public const int Min = 0; public const int Max = 100; public const double DefaultGainScale = 0.05; public static LoyaltyTier Tier(int value) { if (value <= 39) { if (value > 0) { if (value <= 14) { return LoyaltyTier.Broken; } return LoyaltyTier.Fraying; } return LoyaltyTier.Gone; } if (value <= 74) { return LoyaltyTier.Steady; } return LoyaltyTier.Devoted; } public static int DeltaFor(LoyaltyEvent ev, int speciesDailyDecay = 15) { return ev switch { LoyaltyEvent.FeedPreferred => 10, LoyaltyEvent.FeedBondFood => 20, LoyaltyEvent.DayWithoutFeeding => -speciesDailyDecay, LoyaltyEvent.PetCriticallyHurt => -10, LoyaltyEvent.PetDowned => -20, LoyaltyEvent.DefeatedNearbyEnemy => 5, LoyaltyEvent.CrossedRegionBonded => 5, _ => 0, }; } public static int Apply(int value, LoyaltyEvent ev, int speciesDailyDecay = 15) { return Clamp(value + DeltaFor(ev, speciesDailyDecay)); } public static int ApplyDelta(int value, int delta) { return Clamp(value + delta); } public static bool NextTier(int value, out LoyaltyTier next, out int atValue) { switch (Tier(value)) { case LoyaltyTier.Gone: next = LoyaltyTier.Broken; atValue = 1; return true; case LoyaltyTier.Broken: next = LoyaltyTier.Fraying; atValue = 15; return true; case LoyaltyTier.Fraying: next = LoyaltyTier.Steady; atValue = 40; return true; case LoyaltyTier.Steady: next = LoyaltyTier.Devoted; atValue = 75; return true; default: next = LoyaltyTier.Devoted; atValue = 100; return false; } } public static int Clamp(int value) { if (value >= 0) { if (value <= 100) { return value; } return 100; } return 0; } public static int BankGain(double scaledGain, ref double carry) { if (carry < 0.0 || double.IsNaN(carry) || double.IsInfinity(carry)) { carry = 0.0; } if (!(scaledGain > 0.0) || double.IsNaN(scaledGain) || double.IsInfinity(scaledGain)) { return 0; } double num = scaledGain + carry; if (num >= 100.0) { carry = 0.0; return 100; } int num2 = (int)Math.Floor(num); carry = num - (double)num2; return num2; } } public enum PetBuffStat { PhysicalDamage, EtherealDamage, DecayDamage, ElectricDamage, FrostDamage, FireDamage, MovementSpeed, BagCapacity } public sealed class PetBuffDef { public PetBuffStat Stat; public float AmountPerLevel; public float MaxAmount; } public readonly struct ResolvedBuff { public PetBuffStat Stat { get; } public float Amount { get; } public ResolvedBuff(PetBuffStat stat, float amount) { Stat = stat; Amount = amount; } } public static class PetBuffs { public static int Level(LoyaltyTier tier) { return (int)tier; } public static bool IsFlat(PetBuffStat stat) { return stat == PetBuffStat.BagCapacity; } public static string DisplayName(PetBuffStat stat) { return stat switch { PetBuffStat.PhysicalDamage => "Physical damage", PetBuffStat.EtherealDamage => "Ethereal damage", PetBuffStat.DecayDamage => "Decay damage", PetBuffStat.ElectricDamage => "Electric damage", PetBuffStat.FrostDamage => "Frost damage", PetBuffStat.FireDamage => "Fire damage", PetBuffStat.MovementSpeed => "movement speed", PetBuffStat.BagCapacity => "bag capacity", _ => stat.ToString(), }; } public static float Amount(PetBuffDef def, LoyaltyTier tier) { if (def == null) { return 0f; } int num = Level(tier); if (IsFlat(def.Stat)) { if (num <= 0) { return 0f; } float amountPerLevel = def.AmountPerLevel; float num2 = ((float.IsPositiveInfinity(def.MaxAmount) || def.MaxAmount < amountPerLevel) ? amountPerLevel : def.MaxAmount); if (num < Level(LoyaltyTier.Devoted)) { return amountPerLevel + (float)(num - 1) * ((num2 - amountPerLevel) / 4f); } return num2; } float num3 = def.AmountPerLevel * (float)num; if (!(num3 > def.MaxAmount)) { return num3; } return def.MaxAmount; } public static List Resolve(Dictionary> table, string speciesId, LoyaltyTier tier) { List list = new List(); if (!TryGet(table, speciesId, out List defs)) { return list; } foreach (PetBuffDef item in defs) { float num = Amount(item, tier); if (num > 0f) { list.Add(new ResolvedBuff(item.Stat, num)); } } return list; } public static float BagCapacityTotal(float speciesAmount, bool speciesEnabled, float beastOfBurdenAmount, bool beastOfBurdenLearned) { float num = 0f; if (speciesEnabled && speciesAmount > 0f) { num += speciesAmount; } if (beastOfBurdenLearned && beastOfBurdenAmount > 0f) { num += beastOfBurdenAmount; } return num; } public static Dictionary> Parse(string text, Action warn = null) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (TableLine item2 in SpeciesTable.Lines(text, '\n')) { string text2 = item2.Fields[0].Trim(); if (text2.Length == 0) { continue; } if (!EnumRead.TryName(text2, out var value)) { warn?.Invoke("'" + item2.Key + "': unknown buff stat '" + text2 + "' (valid: " + string.Join(", ", Enum.GetNames(typeof(PetBuffStat))) + ")."); } else { PetBuffDef item = new PetBuffDef { Stat = value, AmountPerLevel = SpeciesTable.FloatOr(item2.Fields, 1, 1f, warn, item2.Key), MaxAmount = SpeciesTable.FloatOr(item2.Fields, 2, float.PositiveInfinity, warn, item2.Key) }; if (!dictionary.TryGetValue(item2.Key, out var value2)) { value2 = (dictionary[item2.Key] = new List()); } value2.Add(item); } } return dictionary; } public static Dictionary> Merge(Dictionary> builtIn, Dictionary> overrides) { return SpeciesTable.Merge>(builtIn, overrides); } public static bool TryGet(Dictionary> table, string speciesId, out List defs) { return SpeciesTable.TryResolve>(table, speciesId, ref defs, (string)null); } } public enum ComfortSide { None, Cold, Hot } public sealed class ComfortRelief { public ComfortSide Side; public int ReliefSteps; public double EscalateMult; public bool Immune; } public readonly struct ComfortReading { public int StepsOutside { get; } public ComfortSide Side { get; } public double EscalateMult { get; } public int RawStepsOutside { get; } public bool Immune { get; } public static ComfortReading InBand => new ComfortReading(0, ComfortSide.None, 1.0, 0); public ComfortReading(int stepsOutside, ComfortSide side, double escalateMult, int rawStepsOutside, bool immune = false) { StepsOutside = stepsOutside; Side = side; EscalateMult = escalateMult; RawStepsOutside = rawStepsOutside; Immune = immune; } } public static class PetComfort { public static ComfortSide SideOf(TempStep ambient, ComfortBand band) { if (ambient < band.Min) { return ComfortSide.Cold; } if (ambient > band.Max) { return ComfortSide.Hot; } return ComfortSide.None; } public static ComfortReading Evaluate(TempStep ambient, ComfortBand band, ComfortRelief relief = null) { return Evaluate(ambient, band, (relief == null) ? null : new ComfortRelief[1] { relief }); } public static ComfortReading Evaluate(TempStep ambient, ComfortBand band, IReadOnlyList reliefs) { int num = Temperature.StepsOutside(ambient, band); if (num <= 0) { return ComfortReading.InBand; } ComfortSide comfortSide = SideOf(ambient, band); if (reliefs != null) { for (int i = 0; i < reliefs.Count; i++) { if (reliefs[i] != null && reliefs[i].Immune) { return new ComfortReading(0, comfortSide, 1.0, num, immune: true); } } } int num2 = 0; double num3 = 1.0; if (reliefs != null) { for (int j = 0; j < reliefs.Count; j++) { ComfortRelief comfortRelief = reliefs[j]; if (comfortRelief != null && comfortRelief.Side == comfortSide && comfortRelief.ReliefSteps > 0) { num2 += comfortRelief.ReliefSteps; if (comfortRelief.EscalateMult > num3) { num3 = comfortRelief.EscalateMult; } } } } int num4 = Math.Max(0, num - num2); double escalateMult = ((num4 > 0 && num2 > 0) ? num3 : 1.0); return new ComfortReading(num4, comfortSide, escalateMult, num); } } public static class SpeciesComfort { public const string DefaultKey = "Default"; public static Dictionary Parse(string text, Action warn = null) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (TableLine item in SpeciesTable.Lines(text, '\n')) { string[] fields = item.Fields; TempStep step; TempStep step2; if (fields.Length < 2) { warn?.Invoke("'" + item.Raw + "': expected Species=MinStep,MaxStep."); } else if (!TryParseStep(fields[0], out step) || !TryParseStep(fields[1], out step2)) { warn?.Invoke("'" + item.Key + "': unknown step name (valid: " + string.Join(", ", Enum.GetNames(typeof(TempStep))) + ")."); } else if (step > step2) { warn?.Invoke("'" + item.Key + "': inverted band " + fields[0].Trim() + ">" + fields[1].Trim() + " — line skipped."); } else { dictionary[item.Key] = new ComfortBand(step, step2); } } return dictionary; } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } public static ComfortBand Resolve(Dictionary table, string speciesId, ComfortBand fallback) { ComfortBand result = default(ComfortBand); if (!SpeciesTable.TryResolve(table, speciesId, ref result, "Default")) { return fallback; } return result; } private static bool TryParseStep(string s, out TempStep step) { return EnumRead.TryName(s.Trim(), out step); } } public sealed class BlanketDef { public string Key = ""; public ComfortSide Side; public int ReliefSteps; public double EscalateMult; public double DurationSeconds; public List Ingredients = new List(); public ComfortRelief ToRelief() { return new ComfortRelief { Side = Side, ReliefSteps = ReliefSteps, EscalateMult = EscalateMult }; } } public static class Blankets { public static Dictionary Parse(string text, Action warn = null) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_018a: Unknown result type (might be due to invalid IL or missing references) //IL_0172: Unknown result type (might be due to invalid IL or missing references) Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (TableLine item in SpeciesTable.Lines(text, '\n')) { string[] fields = item.Fields; if (fields.Length < 5) { warn?.Invoke("'" + item.Raw + "': expected Name=Side,ReliefSteps,EscalateMult,DurationSeconds,Ingredient…."); continue; } string text2 = fields[0].Trim(); if (!TryParseSide(text2, out var side) || side == ComfortSide.None) { warn?.Invoke("'" + item.Key + "': side must be Cold or Hot, got '" + text2 + "'."); continue; } BlanketDef blanketDef = new BlanketDef { Key = item.Key, Side = side, ReliefSteps = (int)SpeciesTable.DoubleOr(fields, 1, 3.0, warn, item.Key), EscalateMult = SpeciesTable.DoubleOr(fields, 2, 3.0, warn, item.Key), DurationSeconds = SpeciesTable.DoubleOr(fields, 3, 900.0, warn, item.Key) }; for (int i = 4; i < fields.Length; i++) { string text3 = fields[i].Trim(); if (text3.Length > 0) { blanketDef.Ingredients.Add(text3); } } if (blanketDef.Ingredients.Count == 0) { warn?.Invoke("'" + item.Key + "': no ingredients — line skipped (an uncraftable blanket helps no one)."); } else { dictionary[item.Key] = blanketDef; } } return dictionary; } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } private static bool TryParseSide(string s, out ComfortSide side) { return EnumRead.TryName(s, out side); } } public enum FoodKind { None, Preferred, Bond } public readonly struct FoodMatch { public static readonly FoodMatch None = new FoodMatch(FoodKind.None, 0); public FoodKind Kind { get; } public int LoyaltyGain { get; } public bool IsMatch => Kind != FoodKind.None; public FoodMatch(FoodKind kind, int loyaltyGain) { Kind = kind; LoyaltyGain = loyaltyGain; } } public enum FeedOutcome { Satiated, NotInterested, Fed } public sealed class FoodEntry { public string Key; public int? ItemId; public string Category; public FoodKind Kind; public int LoyaltyGain; public float? Heal; public int? HealthRecovery; } public sealed class DietEntry { public double? HungerSecondsPerDay; public List Foods; public DietEntry(List foods = null, double? hungerSecondsPerDay = null) { Foods = foods ?? new List(); HungerSecondsPerDay = hungerSecondsPerDay; } } public readonly struct FeedDecision { public FeedOutcome Outcome { get; } public FoodKind Kind { get; } public int LoyaltyGain { get; } public float Heal { get; } public int HealthRecoveryLevel { get; } public string MatchedKey { get; } public FeedDecision(FeedOutcome outcome, FoodKind kind, int loyaltyGain, float heal, int healthRecoveryLevel, string matchedKey) { Outcome = outcome; Kind = kind; LoyaltyGain = loyaltyGain; Heal = heal; HealthRecoveryLevel = healthRecoveryLevel; MatchedKey = matchedKey; } public FoodMatch ToFoodMatch() { if (Outcome != FeedOutcome.Fed) { return FoodMatch.None; } return new FoodMatch(Kind, LoyaltyGain); } } public static class PetDiet { public const string DefaultKey = "Default"; public const int DefaultPreferredLoyalty = 10; public const int DefaultBondLoyalty = 20; public static Dictionary Parse(string json, Action warn = null) { return JsonTable.Read(json, warn, "species", "an object of species → food arrays", ParseDiet); } private static DietEntry ParseDiet(string species, object value, Action warn) { if (value is List foods) { return new DietEntry(ParseFoods(species, foods, warn)); } FieldReader fieldReader = new FieldReader("'" + species + "'", warn); if (value is Dictionary dictionary) { double? hungerSecondsPerDay = null; List list = null; foreach (KeyValuePair item in dictionary) { string text = item.Key.ToLowerInvariant(); double result2; if (!(text == "hungersecondsperday")) { if (text == "foods") { if (fieldReader.Items("foods", item.Value, out List result, "an ARRAY of food objects", "species skipped")) { list = result; } } else { fieldReader.Unknown(item.Key, "hungerSecondsPerDay, foods"); } } else if (fieldReader.Num("hungerSecondsPerDay", item.Value, out result2, null, "using the global default")) { hungerSecondsPerDay = result2; if (result2 <= 0.0) { fieldReader.Say($"hungerSecondsPerDay {result2} disables hunger and decay — this pet stays permanently feedable and never loses loyalty to hunger."); } } } if (list == null) { fieldReader.Say("object form requires a 'foods' array — species skipped."); return null; } return new DietEntry(ParseFoods(species, list, warn), hungerSecondsPerDay); } fieldReader.Say("value must be a food ARRAY or an OBJECT with a 'foods' array — species skipped."); return null; } private static List ParseFoods(string species, List foods, Action warn) { List list = new List(); for (int i = 0; i < foods.Count; i++) { FoodEntry foodEntry = ParseFood(species, i, foods[i], warn); if (foodEntry != null) { list.Add(foodEntry); } } return list; } private static FoodEntry ParseFood(string species, int index, object value, Action warn) { FieldReader f = new FieldReader($"'{species}' food #{index + 1}", warn); if (!(value is Dictionary dictionary)) { f.Say("must be an object — entry skipped."); return null; } FoodEntry foodEntry = new FoodEntry { Kind = FoodKind.Preferred }; bool flag = false; bool flag2 = false; int? num = null; foreach (KeyValuePair item in dictionary) { switch (item.Key.ToLowerInvariant()) { case "item": flag = TryReadItem(f, item.Value, foodEntry); break; case "category": flag2 = TryReadCategory(f, item.Value, foodEntry); break; case "kind": { if (item.Value is string name && TryParseKind(name, out var kind)) { foodEntry.Kind = kind; } else { f.Say($"unknown kind '{item.Value}' (valid: Preferred, Bond) — using Preferred."); } break; } case "loyalty": { if (f.Int("loyalty", item.Value, out var result2, null, "using the kind default")) { num = result2; } break; } case "heal": { if (f.Num("heal", item.Value, out var result3, null, "using the global fallback")) { foodEntry.Heal = (float)result3; } break; } case "healthrecovery": { if (f.Int("healthRecovery", item.Value, out var result, $"a number {1}-{5}", $"using the default ({1})")) { int num2 = ((result < 1) ? 1 : ((result > 5) ? 5 : result)); if (num2 != result) { f.Say($"'healthRecovery' {result} out of range {1}-{5} — clamped to {num2}."); } foodEntry.HealthRecovery = num2; } break; } default: f.Unknown(item.Key, "item, category, kind, loyalty, heal, healthRecovery"); break; } } if (flag && flag2) { f.Say("has BOTH 'item' and 'category' — ambiguous, entry skipped (write two foods)."); return null; } if (!flag && !flag2) { f.Say("missing/invalid required 'item' (or 'category') — entry skipped."); return null; } foodEntry.LoyaltyGain = num ?? ((foodEntry.Kind == FoodKind.Bond) ? 20 : 10); return foodEntry; } private static bool TryReadItem(FieldReader f, object value, FoodEntry entry) { ItemKey val = default(ItemKey); if (ItemKey.TryRead(value, ref val)) { entry.Key = ((ItemKey)(ref val)).Key; entry.ItemId = ((ItemKey)(ref val)).ItemId; return true; } f.Wrong("item", "a number (ItemID) or non-empty string (display name)"); return false; } private static bool TryReadCategory(FieldReader f, object value, FoodEntry entry) { if (!(value is string text) || text.Trim().Length == 0) { f.Wrong("category", "a non-empty string (valid: " + FoodCategories.ValidList() + ")"); return false; } if (FoodCategories.TryCanonical(text, out string canonical)) { entry.Category = canonical; } else { entry.Category = text.Trim(); f.Say("unknown category '" + text.Trim() + "' (valid: " + FoodCategories.ValidList() + ") — the entry will never match."); } entry.Key = FoodCategories.Label(entry.Category); return true; } private static bool TryParseKind(string name, out FoodKind kind) { if (string.Equals(name, "Preferred", StringComparison.OrdinalIgnoreCase)) { kind = FoodKind.Preferred; return true; } if (string.Equals(name, "Bond", StringComparison.OrdinalIgnoreCase)) { kind = FoodKind.Bond; return true; } kind = FoodKind.Preferred; return false; } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } public static IReadOnlyList Resolve(Dictionary table, string speciesId) { DietEntry dietEntry = default(DietEntry); if (!SpeciesTable.TryResolve(table, speciesId, ref dietEntry, "Default") || dietEntry?.Foods == null) { return Array.Empty(); } return dietEntry.Foods; } public static double? HungerSecondsPerDay(Dictionary table, string speciesId) { DietEntry dietEntry = default(DietEntry); if (!SpeciesTable.TryResolve(table, speciesId, ref dietEntry, "Default")) { return null; } return dietEntry?.HungerSecondsPerDay; } public static FeedDecision Decide(bool satiated, IReadOnlyList diet, int itemId, string itemName, float fallbackHeal) { return Decide(satiated, diet, itemId, itemName, null, fallbackHeal); } public static FeedDecision Decide(bool satiated, IReadOnlyList diet, int itemId, string itemName, IReadOnlyCollection itemCategories, float fallbackHeal) { if (satiated) { return new FeedDecision(FeedOutcome.Satiated, FoodKind.None, 0, 0f, 0, null); } FoodEntry foodEntry = null; if (diet != null) { foreach (FoodEntry item in diet) { if (item != null && Matches(item, itemId, itemName, itemCategories)) { if (item.Kind == FoodKind.Bond) { foodEntry = item; break; } if (foodEntry == null) { foodEntry = item; } } } } if (foodEntry == null) { return new FeedDecision(FeedOutcome.NotInterested, FoodKind.None, 0, 0f, 0, null); } return new FeedDecision(FeedOutcome.Fed, foodEntry.Kind, foodEntry.LoyaltyGain, foodEntry.Heal ?? fallbackHeal, foodEntry.HealthRecovery ?? 1, foodEntry.Key); } public static bool Matches(FoodEntry entry, int itemId, string itemName) { return Matches(entry, itemId, itemName, null); } public static bool Matches(FoodEntry entry, int itemId, string itemName, IReadOnlyCollection itemCategories) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) if (entry == null) { return false; } if (entry.Category != null) { if (itemCategories == null) { return false; } foreach (string itemCategory in itemCategories) { if (string.Equals(itemCategory, entry.Category, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } ItemKey val = new ItemKey(entry.Key, entry.ItemId); return ((ItemKey)(ref val)).Matches(itemId, itemName); } } public enum GearSlot { Any = -1, Helmet, Chest, Legs, Foot, Hands, RightHand, LeftHand, Back, Quiver } public enum GearEffectKind { LoyaltyGainMult, LoyaltyDecayMult, PetHealthMult, PetDamageMult, PetDefenseMult, PetSpeedMult } public sealed class GearEffectDef { public GearEffectKind Kind; public float Value; } public sealed class GearEntry { public string ItemKey; public int? ItemId; public GearSlot Slot = GearSlot.Any; public string Species; public List Effects = new List(); } public readonly struct EquippedItem { public readonly int ItemId; public readonly string Name; public readonly GearSlot Slot; public EquippedItem(int itemId, string name, GearSlot slot) { ItemId = itemId; Name = name; Slot = slot; } } public sealed class GearEffects { public static readonly GearEffects None = new GearEffects(1f, 1f, new StatModifiers()); public float LoyaltyGainMult { get; } public float LoyaltyDecayMult { get; } public StatModifiers PetStats { get; } public bool IsIdentity { get { if (LoyaltyGainMult == 1f && LoyaltyDecayMult == 1f && PetStats.Health == 1f && PetStats.Damage == 1f && PetStats.Defense == 1f) { return PetStats.Speed == 1f; } return false; } } public GearEffects(float loyaltyGainMult, float loyaltyDecayMult, StatModifiers petStats) { LoyaltyGainMult = loyaltyGainMult; LoyaltyDecayMult = loyaltyDecayMult; PetStats = petStats ?? new StatModifiers(); } } public static class PetGear { public static Dictionary Parse(string json, Action warn = null) { return JsonTable.Read(json, warn, "item", "an object of item → gear-effect objects", ParseEntry); } private static GearEntry ParseEntry(string itemKey, object value, Action warn) { string text = (itemKey ?? "").Trim(); if (text.Length == 0) { warn?.Invoke("empty item key — entry skipped."); return null; } FieldReader f = new FieldReader("'" + text + "'", warn); if (!(value is Dictionary dictionary)) { f.Say("value must be an object — entry skipped."); return null; } ItemKey val = default(ItemKey); ItemKey.TryRead((object)text, ref val); GearEntry gearEntry = new GearEntry { ItemKey = ((ItemKey)(ref val)).Key, ItemId = ((ItemKey)(ref val)).ItemId }; foreach (KeyValuePair item in dictionary) { switch (item.Key.ToLowerInvariant()) { case "slot": { if (!f.Enum("slot", item.Value, out var result2, "name a slot (" + string.Join(", ", Enum.GetNames(typeof(GearSlot))) + ")", "entry skipped")) { return null; } gearEntry.Slot = result2; break; } case "species": { if (!f.Str("species", item.Value, out string result3, null, "entry skipped")) { return null; } gearEntry.Species = result3; break; } case "effects": { if (!f.Items("effects", item.Value, out List result, "an ARRAY of {kind, value} objects")) { break; } foreach (object item2 in result) { GearEffectDef gearEffectDef = ParseEffect(f, item2); if (gearEffectDef != null) { gearEntry.Effects.Add(gearEffectDef); } } break; } default: f.Unknown(item.Key, "slot, species, effects"); break; } } if (gearEntry.Effects.Count == 0) { f.Say("missing/empty required 'effects' — entry skipped."); return null; } return gearEntry; } private static GearEffectDef ParseEffect(FieldReader f, object value) { if (!(value is Dictionary dictionary)) { f.Say("each effect must be an object {kind, value} — one skipped."); return null; } if (!dictionary.TryGetValue("kind", out var value2) || !(value2 is string text)) { f.Say("an effect is missing a string 'kind' — skipped."); return null; } if (!EnumRead.TryName(text.Trim(), out var value3)) { f.Say("unknown effect kind '" + text + "' (valid: " + string.Join(", ", Enum.GetNames(typeof(GearEffectKind))) + ") — skipped (a table for a future build stays loadable)."); return null; } if (!dictionary.TryGetValue("value", out var value4) || !(value4 is double num) || num <= 0.0) { f.Say($"effect '{value3}' needs a numeric 'value' > 0 (a multiplier; 1.5 = +50%) — skipped."); return null; } return new GearEffectDef { Kind = value3, Value = (float)num }; } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } public static GearEffects Resolve(Dictionary table, IReadOnlyList worn, string speciesId, Action log = null) { if (table == null || table.Count == 0 || worn == null || worn.Count == 0) { return GearEffects.None; } float num = 1f; float num2 = 1f; StatModifiers statModifiers = new StatModifiers(); bool flag = false; foreach (GearEntry value in table.Values) { if (!MatchesWorn(value, worn, out var hit) || !SpeciesMatches(value.Species, speciesId)) { continue; } flag = true; foreach (GearEffectDef effect in value.Effects) { switch (effect.Kind) { case GearEffectKind.LoyaltyGainMult: num *= effect.Value; break; case GearEffectKind.LoyaltyDecayMult: num2 *= effect.Value; break; case GearEffectKind.PetHealthMult: statModifiers.Health *= effect.Value; break; case GearEffectKind.PetDamageMult: statModifiers.Damage *= effect.Value; break; case GearEffectKind.PetDefenseMult: statModifiers.Defense *= effect.Value; break; case GearEffectKind.PetSpeedMult: statModifiers.Speed *= effect.Value; break; } } log?.Invoke($"{value.ItemKey} (slot {hit.Slot}) -> {Describe(value)}"); } if (!flag) { return GearEffects.None; } return new GearEffects(num, num2, statModifiers); } private static bool MatchesWorn(GearEntry entry, IReadOnlyList worn, out EquippedItem hit) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) for (int i = 0; i < worn.Count; i++) { EquippedItem equippedItem = worn[i]; ItemKey val = new ItemKey(entry.ItemKey, entry.ItemId); if (((ItemKey)(ref val)).Matches(equippedItem.ItemId, equippedItem.Name) && (entry.Slot == GearSlot.Any || entry.Slot == equippedItem.Slot)) { hit = equippedItem; return true; } } hit = default(EquippedItem); return false; } public static bool SpeciesMatches(string filter, string speciesId) { return Species.NameMatches(speciesId, filter); } private static string Describe(GearEntry entry) { List list = new List(); foreach (GearEffectDef effect in entry.Effects) { list.Add($"{effect.Kind} x{effect.Value:0.###}"); } return string.Join(", ", list); } } public sealed class GiftDrop { public string Key; public int? ItemId; public int Qty = 1; public double Chance; public double ChanceAt100; } public sealed class PetGiftEntry { public string Species; public double NothingChance; public double NothingChanceAt100; public GiftDrop Default; public List Drops = new List(); } public sealed class GiftMix { public double[] DropChances; public double DefaultChance; public double NothingChance; } public sealed class GiftAuditFinding { public bool Error; public string Message; } public static class PetGifts { public const string DefaultKey = "Default"; private const double Epsilon = 1E-09; public static Dictionary Parse(string json, Action warn = null) { return JsonTable.Read(json, warn, "species", "an object of species → gift objects", ParseEntry); } private static PetGiftEntry ParseEntry(string speciesKey, object value, Action warn) { FieldReader f = new FieldReader("'" + speciesKey + "'", warn); if (!(value is Dictionary dictionary)) { f.Say("value must be an object — species skipped."); return null; } PetGiftEntry petGiftEntry = new PetGiftEntry { Species = speciesKey }; bool flag = false; foreach (KeyValuePair item in dictionary) { switch (item.Key.ToLowerInvariant()) { case "default": petGiftEntry.Default = ParseDefault(f, item.Value); break; case "drops": { if (!f.Items("drops", item.Value, out List result, "an ARRAY of drop objects")) { break; } foreach (object item2 in result) { GiftDrop giftDrop = ParseDrop(f, speciesKey, item2); if (giftDrop != null) { petGiftEntry.Drops.Add(giftDrop); } } break; } case "nothingchance": petGiftEntry.NothingChance = ReadChance(f, "nothingChance", item.Value, 0.0); break; case "nothingchanceat100": petGiftEntry.NothingChanceAt100 = ReadChance(f, "nothingChanceAt100", item.Value, 0.0); flag = true; break; default: f.Unknown(item.Key, "default, drops, nothingChance, nothingChanceAt100"); break; } } if (!flag) { petGiftEntry.NothingChanceAt100 = petGiftEntry.NothingChance; } if (petGiftEntry.Default == null) { f.Say("missing required 'default' drop — species skipped."); return null; } foreach (GiftAuditFinding item3 in Audit(petGiftEntry)) { f.Say(item3.Message); } return petGiftEntry; } private static GiftDrop ParseDefault(FieldReader f, object value) { if (value is Dictionary dictionary) { GiftDrop giftDrop = new GiftDrop(); foreach (KeyValuePair item in dictionary) { string text = item.Key.ToLowerInvariant(); if (!(text == "item")) { if (text == "qty") { giftDrop.Qty = ReadQty(f, "default", item.Value); } else { f.Say("unknown default key '" + item.Key + "' (valid: item, qty — the default's chance is computed, never authored)."); } } else { ReadItem(f, "default", item.Value, giftDrop); } } if (giftDrop.Key == null) { f.Say("'default' object missing a usable 'item'."); return null; } return giftDrop; } GiftDrop giftDrop2 = new GiftDrop(); if (!ReadItem(f, "default", value, giftDrop2)) { return null; } return giftDrop2; } private static GiftDrop ParseDrop(FieldReader f, string speciesKey, object value) { if (!(value is Dictionary dictionary)) { f.Say("each drop must be an object — drop skipped."); return null; } GiftDrop giftDrop = new GiftDrop(); bool flag = false; bool flag2 = false; foreach (KeyValuePair item in dictionary) { switch (item.Key.ToLowerInvariant()) { case "item": ReadItem(f, "drop", item.Value, giftDrop); break; case "chance": { if (f.Num("chance", item.Value, out var result, null, "drop skipped")) { giftDrop.Chance = result; flag = true; } break; } case "chanceat100": giftDrop.ChanceAt100 = ReadChance(f, "chanceAt100", item.Value, 0.0); flag2 = true; break; case "qty": giftDrop.Qty = ReadQty(f, "drop", item.Value); break; default: f.Say("unknown drop key '" + item.Key + "' (valid: item, chance, chanceAt100, qty)."); break; } } if (giftDrop.Key == null) { f.Say("drop missing required 'item' — drop skipped."); return null; } FieldReader fieldReader = new FieldReader("'" + speciesKey + "/" + giftDrop.Key + "'", f.Sink); if (!flag) { fieldReader.Say("drop missing required 'chance' — drop skipped."); return null; } if (!(giftDrop.Chance > 0.0) || giftDrop.Chance > 1.0) { fieldReader.Wrong("chance", "in (0,1]", "drop skipped"); return null; } if (!flag2) { giftDrop.ChanceAt100 = giftDrop.Chance; } return giftDrop; } private static bool ReadItem(FieldReader f, string where, object value, GiftDrop drop) { ItemKey val = default(ItemKey); if (ItemKey.TryRead(value, ref val)) { drop.Key = ((ItemKey)(ref val)).Key; drop.ItemId = ((ItemKey)(ref val)).ItemId; return true; } f.Say(where + " item must be a number or an item-name string."); return false; } private static double ReadChance(FieldReader f, string name, object value, double fallback) { if (f.Num(name, value, out var result, null, "using " + fallback.ToString("0.###", CultureInfo.InvariantCulture))) { if (result < 0.0 || result > 1.0) { f.Say("'" + name + "' " + result.ToString("0.###", CultureInfo.InvariantCulture) + " outside [0,1] — clamped."); if (!(result < 0.0)) { return 1.0; } return 0.0; } return result; } return fallback; } private static int ReadQty(FieldReader f, string where, object value) { if (value is double num && (int)num >= 1) { return (int)num; } f.Say(where + " 'qty' must be a whole number >= 1 — using 1."); return 1; } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } public static PetGiftEntry Resolve(Dictionary table, string speciesId) { PetGiftEntry result = default(PetGiftEntry); if (!SpeciesTable.TryResolve(table, speciesId, ref result, "Default")) { return null; } return result; } public static double Lerp(double at0, double at100, int loyalty) { double num = ((loyalty <= 0) ? 0.0 : ((loyalty >= 100) ? 1.0 : ((double)loyalty / 100.0))); return at0 + (at100 - at0) * num; } public static double ExplicitSum(PetGiftEntry e, int loyalty) { double num = 0.0; foreach (GiftDrop drop in e.Drops) { num += Lerp(drop.Chance, drop.ChanceAt100, loyalty); } return num; } public static double RawDefaultChance(PetGiftEntry e, int loyalty) { return 1.0 - ExplicitSum(e, loyalty) - Lerp(e.NothingChance, e.NothingChanceAt100, loyalty); } public static GiftMix EffectiveMix(PetGiftEntry e, int loyalty) { GiftMix giftMix = new GiftMix { DropChances = new double[e.Drops.Count] }; double num = 1.0; for (int i = 0; i < e.Drops.Count; i++) { double num2 = Lerp(e.Drops[i].Chance, e.Drops[i].ChanceAt100, loyalty); double num3 = ((num2 < num) ? num2 : num); giftMix.DropChances[i] = num3; num -= num3; } double num4 = Lerp(e.NothingChance, e.NothingChanceAt100, loyalty); giftMix.DefaultChance = ((num - num4 > 0.0) ? (num - num4) : 0.0); giftMix.NothingChance = num - giftMix.DefaultChance; return giftMix; } public static GiftDrop Roll(PetGiftEntry e, int loyalty, double roll01) { GiftMix giftMix = EffectiveMix(e, loyalty); double num = 0.0; for (int i = 0; i < e.Drops.Count; i++) { num += giftMix.DropChances[i]; if (roll01 < num) { return e.Drops[i]; } } num += giftMix.DefaultChance; if (roll01 < num) { return e.Default; } return null; } public static int? DefaultZeroLoyalty(PetGiftEntry e) { double num = RawDefaultChance(e, 0); double num2 = RawDefaultChance(e, 100); if (num > 1E-09 && num2 > 1E-09) { return null; } if (num <= 1E-09 && num2 <= 1E-09) { return 0; } int num3 = (int)Math.Ceiling(num / (num - num2) * 100.0); return (num3 >= 0) ? ((num3 > 100) ? 100 : num3) : 0; } public static List Audit(PetGiftEntry e) { List list = new List(); double num = ExplicitSum(e, 0); double num2 = ExplicitSum(e, 100); double v = Lerp(e.NothingChance, e.NothingChanceAt100, 0); double num3 = RawDefaultChance(e, 0); double num4 = RawDefaultChance(e, 100); if (num > 1.000000001 || num2 > 1.000000001) { list.Add(new GiftAuditFinding { Error = true, Message = "explicit drop chances alone sum to " + Fmt(Math.Max(num, num2)) + " at loyalty " + $"{((!(num >= num2)) ? 100 : 0)} (> 1.0) — later drops are truncated and never roll." }); } if (num3 <= 1E-09) { string text = ((num4 > 1E-09) ? $" (it only becomes possible at loyalty >= {DefaultZeroLoyalty(e)})" : ""); list.Add(new GiftAuditFinding { Error = true, Message = "explicit drop chances (" + Fmt(num) + ") + nothingChance (" + Fmt(v) + ") reach 1.0 at loyalty 0 — the default drop '" + e.Default.Key + "' can never be given" + text + "." }); } else if (num4 <= 1E-09) { list.Add(new GiftAuditFinding { Error = false, Message = "the default drop '" + e.Default.Key + "' phases out at loyalty >= " + $"{DefaultZeroLoyalty(e)} (explicit chances + nothingChance reach 1.0 there) — " + "fine if deliberate." }); } return list; } private static string Fmt(double v) { return v.ToString("0.###", CultureInfo.InvariantCulture); } } public enum GrowthGroup { Health, Damage, Defense, Speed } public sealed class PetGrowthDef { public GrowthGroup Group; public float PercentAt100; } public static class PetGrowth { public const float MaxPercent = 500f; public static Dictionary> Parse(string text, Action warn = null) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0169: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (TableLine item in SpeciesTable.Lines(text, '\n')) { string text2 = item.Fields[0].Trim(); if (text2.Length == 0) { continue; } if (!EnumRead.TryName(text2, out var value)) { warn?.Invoke("'" + item.Key + "': unknown growth group '" + text2 + "' (valid: " + string.Join(", ", Enum.GetNames(typeof(GrowthGroup))) + ") — line skipped."); continue; } float num = SpeciesTable.FloatOr(item.Fields, 1, 0f, warn, item.Key); if (num == 0f) { continue; } if (num < 0f) { warn?.Invoke($"'{item.Key}': negative growth {num} for {value} — growth only rises with " + "loyalty (a species malus is not this axis) — line skipped."); continue; } if (num > 500f) { warn?.Invoke($"'{item.Key}': growth {num}% for {value} above the {500f}% sanity cap — clamped."); num = 500f; } if (!dictionary.TryGetValue(item.Key, out var value2)) { value2 = (dictionary[item.Key] = new List()); } value2.Add(new PetGrowthDef { Group = value, PercentAt100 = num }); } return dictionary; } public static Dictionary> Merge(Dictionary> builtIn, Dictionary> overrides) { return SpeciesTable.Merge>(builtIn, overrides); } public static float Multiplier(PetGrowthDef def, int loyaltyValue) { if (def == null || def.PercentAt100 <= 0f) { return 1f; } float num = ((loyaltyValue <= 0) ? 0f : ((loyaltyValue >= 100) ? 1f : ((float)loyaltyValue / 100f))); return 1f + def.PercentAt100 * num / 100f; } public static StatModifiers Resolve(Dictionary> table, string speciesId, int loyaltyValue) { StatModifiers statModifiers = new StatModifiers(); if (table == null || speciesId == null) { return statModifiers; } List list = default(List); if (!SpeciesTable.TryResolve>(table, speciesId, ref list, (string)null)) { return statModifiers; } foreach (PetGrowthDef item in list) { float num = Multiplier(item, loyaltyValue); switch (item.Group) { case GrowthGroup.Health: statModifiers.Health *= num; break; case GrowthGroup.Damage: statModifiers.Damage *= num; break; case GrowthGroup.Defense: statModifiers.Defense *= num; break; case GrowthGroup.Speed: statModifiers.Speed *= num; break; } } return statModifiers; } } public enum PetHealthSource { None, LocalAnchor, ProxyMirror } public readonly struct PetHealthReading { public readonly PetHealthSource Source; public readonly float Current; public readonly float Max; public static readonly PetHealthReading Unknown = new PetHealthReading(PetHealthSource.None, 0f, 0f); public bool Known { get { if (Source != PetHealthSource.None) { return Max > 0f; } return false; } } public string SourceLabel => Source switch { PetHealthSource.LocalAnchor => "local-anchor", PetHealthSource.ProxyMirror => "proxy-mirror", _ => "unknown", }; public PetHealthReading(PetHealthSource source, float current, float max) { Source = source; Current = current; Max = max; } } public static class PetHealthProvider { public static PetHealthReading Resolve(bool localAnchorHasHealth, float localCurrent, float localMax, bool proxyMirrorLive, float proxyCurrent, float proxyMax) { if (localAnchorHasHealth && localMax > 0f) { return new PetHealthReading(PetHealthSource.LocalAnchor, localCurrent, localMax); } if (proxyMirrorLive && proxyMax > 0f) { return new PetHealthReading(PetHealthSource.ProxyMirror, proxyCurrent, proxyMax); } return PetHealthReading.Unknown; } public static double PersistFraction(bool useLocalFraction, double localFraction, bool proxyMirrorUsable, float proxyCurrent, float proxyMax, double fallback) { if (useLocalFraction) { return Clamp01(localFraction); } if (proxyMirrorUsable && proxyMax > 0f) { return Clamp01((double)proxyCurrent / (double)proxyMax); } return Clamp01(fallback); } private static double Clamp01(double v) { if (!double.IsNaN(v)) { if (!(v < 0.0)) { if (!(v > 1.0)) { return v; } return 1.0; } return 0.0; } return 1.0; } public static bool ProxyMirrorUsable(bool announced, bool haveSnapshot) { return announced && haveSnapshot; } } public enum PetPanelTone { Normal, Good, Warn, Bad, Muted } public sealed class PetPanelRow { public string Label; public string Value; public float Bar = float.NaN; public PetPanelTone Tone; } public sealed class PetPanelSection { public string Title; public List Rows = new List(); public PetPanelSection Add(string label, string value, float bar = float.NaN, PetPanelTone tone = PetPanelTone.Normal) { Rows.Add(new PetPanelRow { Label = label, Value = value, Bar = bar, Tone = tone }); return this; } } public sealed class PetPanelModel { public bool HasPet; public string Header = ""; public string SubHeader = ""; public string EmptyText = ""; public List Sections = new List(); } public sealed class PetBondFacts { public int LoyaltyValue; public Responsiveness Responsiveness; public float SpeedMultiplier; public float FollowSpeed; public double GainPercent = 100.0; public double GainCarry; } public sealed class PetHungerFacts { public double HungerSecondsSinceFed; public double HungerSecondsPerDay; public int DailyLoyaltyDecay; } public sealed class PetComfortFacts { public ComfortStage Comfort; public ComfortSide ComfortSide; public string BlanketName; public ComfortSide BlanketSide; public double BlanketSecondsLeft; public string DrinkName; public int DrinkColdSteps; public int DrinkHotSteps; public bool DrinkImmune; public double DrinkSecondsLeft; } public sealed class PetVitalsFacts { public bool HasBody; public bool BodyIsGhost; public bool Reforming; public bool HasAnchor; public float HealthCurrent; public float HealthMax; public float HealthFactor; public int RelicStacks; public string RelicLabel = ""; public int HealthRecoveryLevel; public double HealthRecoverySecondsLeft; } public sealed class PetOffenseFacts { public float DamageFactor; public float BaseAttackDamage; public float[] AttackDamageProfile; public float AttackInterval; public float AttackRange; public float AggroRange; public bool HasSpecialAttack; public string SpecialStatusEffect; public float SpecialBuildupPercent; public float SpecialDamageMultiplier; public float SpecialCooldownSeconds; public float SpecialReadySeconds; public bool SpecialIsRanged; public bool SpecialIsBrace; public bool FoodHexAvailable; public List FoodHexMix; public string BuffFoodName; public string BuffFoodEffect; public double BuffFoodSecondsLeft; } public sealed class PetDefenseFacts { public float[] ResistancesPct; } public sealed class PetGiftFacts { public bool BuffsEnabled; public List Buffs; public List NextTierBuffs; } public sealed class PetNowFacts { public string CombatTargetName; public float DistanceToPlayer; } public sealed class PetPanelInput { public bool HasPet; public string SpeciesId; public string PetName; public PetBondFacts Bond = new PetBondFacts(); public PetHungerFacts Hunger = new PetHungerFacts(); public PetComfortFacts Climate = new PetComfortFacts(); public PetVitalsFacts Vitals = new PetVitalsFacts(); public PetOffenseFacts Offense = new PetOffenseFacts(); public PetDefenseFacts Defense = new PetDefenseFacts(); public PetGiftFacts Gifts = new PetGiftFacts(); public PetNowFacts Now = new PetNowFacts(); } public static class PetPanel { public static readonly string[] DamageTypeNames = new string[6] { "Physical", "Ethereal", "Decay", "Electric", "Frost", "Fire" }; public const string NoPetText = "No companion travels with you.\n\nEarn a wild creature's trust to bond with it."; public static PetPanelModel Build(PetPanelInput i) { PetPanelModel petPanelModel = new PetPanelModel { HasPet = i.HasPet }; if (!i.HasPet) { petPanelModel.EmptyText = "No companion travels with you.\n\nEarn a wild creature's trust to bond with it."; return petPanelModel; } LoyaltyTier tier = Loyalty.Tier(i.Bond.LoyaltyValue); petPanelModel.Header = (string.IsNullOrEmpty(i.PetName) ? (i.SpeciesId ?? "") : i.PetName); petPanelModel.SubHeader = BodyState(i.Vitals); petPanelModel.Sections.Add(Vitals(i.Vitals, i.Bond)); petPanelModel.Sections.Add(Bond(i.Bond, i.Hunger, i.Climate, tier)); petPanelModel.Sections.Add(Offense(i.Offense)); petPanelModel.Sections.Add(Defenses(i.Defense)); petPanelModel.Sections.Add(BondGifts(i.Gifts, i.Bond)); petPanelModel.Sections.Add(Now(i.Now)); return petPanelModel; } private static string BodyState(PetVitalsFacts v) { if (!v.HasBody && v.Reforming) { return "Re-forming — the bond endures while its body recovers"; } if (!v.HasBody) { return "Bodiless — searching for a form to return in"; } if (v.BodyIsGhost) { return "A spectral stand-in walks in its place"; } return "At your side"; } private static PetPanelSection Vitals(PetVitalsFacts v, PetBondFacts bond) { PetPanelSection petPanelSection = new PetPanelSection { Title = "Vitals" }; if (v.HasAnchor && v.HealthMax > 0f) { float num = v.HealthCurrent / v.HealthMax; PetPanelTone tone = ((num < 0.2f) ? PetPanelTone.Bad : ((num < 0.5f) ? PetPanelTone.Warn : PetPanelTone.Normal)); petPanelSection.Add("Health", F0(v.HealthCurrent) + " / " + F0(v.HealthMax), Clamp01(num), tone); if (num < 0.2f) { petPanelSection.Add("", "Badly wounded — it fights on loyalty alone", float.NaN, PetPanelTone.Bad); } } else { petPanelSection.Add("Health", "—", float.NaN, PetPanelTone.Muted); petPanelSection.Add("", v.HasBody ? "Its combat vigor is still gathering" : "No body to wound right now", float.NaN, PetPanelTone.Muted); } petPanelSection.Add("Toughness", "×" + F2(v.HealthFactor) + " from loyalty", float.NaN, PetPanelTone.Muted); if (v.HealthRecoveryLevel > 0 && v.HealthRecoverySecondsLeft > 0.0) { petPanelSection.Add("Recovering", $"+{HealthRecovery.RatePerSecond(v.HealthRecoveryLevel):0.##} HP/s ({Duration(v.HealthRecoverySecondsLeft)} left)", Clamp01((float)(v.HealthRecoverySecondsLeft / 600.0)), PetPanelTone.Good); } if (v.RelicStacks > 0) { int num2 = SpeciesRelics.ClampStacks(v.RelicStacks); string arg = (string.IsNullOrEmpty(v.RelicLabel) ? "" : (" (" + v.RelicLabel + ")")); petPanelSection.Add("Relics", $"{num2} / {5} consumed{arg}", (float)num2 / 5f, PetPanelTone.Good); } return petPanelSection; } private static PetPanelSection Bond(PetBondFacts bond, PetHungerFacts hunger, PetComfortFacts climate, LoyaltyTier tier) { PetPanelSection petPanelSection = new PetPanelSection { Title = "Bond" }; petPanelSection.Add("Loyalty", $"{bond.LoyaltyValue} / {100} ({tier})", (float)bond.LoyaltyValue / 100f, TierTone(tier)); if (Loyalty.NextTier(bond.LoyaltyValue, out var next, out var atValue)) { petPanelSection.Add("Next tier", $"{next} at {atValue} (+{atValue - bond.LoyaltyValue})", float.NaN, PetPanelTone.Muted); } else { petPanelSection.Add("Next tier", "Devoted — the bond holds fast", float.NaN, PetPanelTone.Good); } double num = ((bond.GainCarry > 0.0 && bond.GainCarry < 1.0) ? bond.GainCarry : 0.0); if ((bond.GainPercent > 0.0 && bond.GainPercent < 100.0) || num > 0.0) { string text = "gains at " + F2d(bond.GainPercent) + "%"; string value = ((bond.LoyaltyValue >= 100) ? ("the bond is full — " + text) : ("+" + F2d(num) + " banked toward the next point · " + text)); petPanelSection.Add("Bond progress", value, (bond.LoyaltyValue >= 100) ? float.NaN : ((float)num), PetPanelTone.Muted); } if (hunger.HungerSecondsPerDay > 0.0) { double num2 = Math.Max(0.0, hunger.HungerSecondsSinceFed); double num3 = num2 % hunger.HungerSecondsPerDay; double seconds = hunger.HungerSecondsPerDay - num3; bool flag = num2 >= hunger.HungerSecondsPerDay; petPanelSection.Add("Last fed", (num2 < 1.0) ? "just now" : (Duration(num2) + " ago"), float.NaN, flag ? PetPanelTone.Bad : PetPanelTone.Normal); petPanelSection.Add("Appetite", flag ? "Hungry — the bond frays" : "Sated", Clamp01((float)(num3 / hunger.HungerSecondsPerDay)), (!flag) ? PetPanelTone.Good : PetPanelTone.Bad); petPanelSection.Add("Hunger pang", $"in {Duration(seconds)} (−{hunger.DailyLoyaltyDecay} loyalty)", float.NaN, PetPanelTone.Muted); } petPanelSection.Add("Comfort", ComfortText(climate.Comfort, climate.ComfortSide), float.NaN, ComfortTone(climate.Comfort)); if (!string.IsNullOrEmpty(climate.BlanketName)) { bool flag2 = climate.ComfortSide != ComfortSide.None && climate.BlanketSide != ComfortSide.None && climate.BlanketSide != climate.ComfortSide; string text2 = Duration(Math.Max(0.0, climate.BlanketSecondsLeft)); petPanelSection.Add("Wrapped in", flag2 ? (climate.BlanketName + " (" + text2 + " left — no help against " + ((climate.ComfortSide == ComfortSide.Cold) ? "the cold" : "the heat") + ")") : (climate.BlanketName + " (" + text2 + " left)"), float.NaN, (!flag2) ? PetPanelTone.Good : PetPanelTone.Warn); } if (!string.IsNullOrEmpty(climate.DrinkName)) { bool flag3 = !climate.DrinkImmune && ((climate.ComfortSide == ComfortSide.Cold && climate.DrinkColdSteps == 0) || (climate.ComfortSide == ComfortSide.Hot && climate.DrinkHotSteps == 0)); string text3 = Duration(Math.Max(0.0, climate.DrinkSecondsLeft)); petPanelSection.Add("Sipped", climate.DrinkImmune ? (climate.DrinkName + " (" + text3 + " left — immune to the weather)") : (flag3 ? (climate.DrinkName + " (" + text3 + " left — no help against " + ((climate.ComfortSide == ComfortSide.Cold) ? "the cold" : "the heat") + ")") : (climate.DrinkName + " (" + text3 + " left)")), float.NaN, (!flag3) ? PetPanelTone.Good : PetPanelTone.Warn); } petPanelSection.Add("Mood", MoodText(bond.Responsiveness), float.NaN, MoodTone(bond.Responsiveness)); petPanelSection.Add("Stride", F1(bond.FollowSpeed * bond.SpeedMultiplier) + " m/s (×" + F2(bond.SpeedMultiplier) + ")", float.NaN, PetPanelTone.Muted); return petPanelSection; } private static PetPanelSection Offense(PetOffenseFacts o) { PetPanelSection petPanelSection = new PetPanelSection { Title = "Offense" }; float num = o.BaseAttackDamage * Math.Max(0f, o.DamageFactor); petPanelSection.Add("Attack", DamageText(num, o.AttackDamageProfile) + " (" + F0(o.BaseAttackDamage) + " ×" + F2(o.DamageFactor) + ")"); if (o.AttackInterval > 0f) { petPanelSection.Add("Rhythm", "every " + F1(o.AttackInterval) + "s (~" + F1(num / o.AttackInterval) + "/s)", float.NaN, PetPanelTone.Muted); } petPanelSection.Add("Reach", F1(o.AttackRange) + " m", float.NaN, PetPanelTone.Muted); petPanelSection.Add("Engages within", F1(o.AggroRange) + " m", float.NaN, PetPanelTone.Muted); if (o.HasSpecialAttack) { petPanelSection.Add("Signature strike", o.SpecialIsBrace ? ("Brace counter — each attacker riposted for " + DamageText(num * Math.Max(0f, o.SpecialDamageMultiplier), o.AttackDamageProfile) + " (×" + F2(o.SpecialDamageMultiplier) + ")") : (DamageText(num * Math.Max(0f, o.SpecialDamageMultiplier), o.AttackDamageProfile) + " (×" + F2(o.SpecialDamageMultiplier) + ")" + (o.SpecialIsRanged ? " — ranged bolt" : ""))); petPanelSection.Add("Inflicts", InflictsText(o, out var tone), float.NaN, tone); petPanelSection.Add("Signature cooldown", (o.SpecialReadySeconds <= 0f) ? ("Ready (" + F0(o.SpecialCooldownSeconds) + "s between uses)") : ("ready in " + F0(o.SpecialReadySeconds) + "s"), float.NaN, (o.SpecialReadySeconds <= 0f) ? PetPanelTone.Good : PetPanelTone.Warn); } else { petPanelSection.Add("Signature strike", "none known for this species", float.NaN, PetPanelTone.Muted); } if (o.BuffFoodSecondsLeft > 0.0 && !string.IsNullOrEmpty(o.BuffFoodName)) { string text = Duration(Math.Max(0.0, o.BuffFoodSecondsLeft)); petPanelSection.Add("Empowered by", string.IsNullOrEmpty(o.BuffFoodEffect) ? (o.BuffFoodName + " (" + text + " left)") : (o.BuffFoodName + " (" + o.BuffFoodEffect + ", " + text + " left)"), float.NaN, PetPanelTone.Good); } return petPanelSection; } private static string InflictsText(PetOffenseFacts o, out PetPanelTone tone) { List list = new List(); string[] array = SpecialAttackTable.SplitStatuses(o.SpecialStatusEffect); if (array.Length != 0) { string text = string.Join(" or ", array); list.Add((o.SpecialBuildupPercent > 0f) ? (F0(o.SpecialBuildupPercent) + "% " + text + " build-up") : text); } if (o.FoodHexMix != null && o.FoodHexMix.Count > 0) { List list2 = new List(); foreach (HexBuildUp item in o.FoodHexMix) { list2.Add(F0(item.Percent) + "% " + item.HexId); } list.Add(string.Join(" + ", list2) + " build-up"); } if (list.Count > 0) { tone = PetPanelTone.Good; return string.Join(" + ", list); } tone = PetPanelTone.Muted; if (!o.FoodHexAvailable) { return "—"; } return "— (recent meals imbue hexes — feed it)"; } private static PetPanelSection Defenses(PetDefenseFacts d) { PetPanelSection petPanelSection = new PetPanelSection { Title = "Defenses" }; if (d.ResistancesPct == null) { petPanelSection.Add("Resistances", "— (no combat body)", float.NaN, PetPanelTone.Muted); return petPanelSection; } int num = Math.Min(DamageTypeNames.Length, d.ResistancesPct.Length); for (int i = 0; i < num; i++) { float num2 = d.ResistancesPct[i]; petPanelSection.Add(DamageTypeNames[i], F0(num2) + "%", float.NaN, (num2 > 0f) ? PetPanelTone.Good : ((num2 < 0f) ? PetPanelTone.Bad : PetPanelTone.Muted)); } return petPanelSection; } private static PetPanelSection BondGifts(PetGiftFacts gifts, PetBondFacts bond) { PetPanelSection petPanelSection = new PetPanelSection { Title = "Bond gifts" }; if (!gifts.BuffsEnabled) { petPanelSection.Add("Passive gifts", "disabled in config", float.NaN, PetPanelTone.Muted); return petPanelSection; } bool flag = false; if (gifts.Buffs != null) { foreach (ResolvedBuff buff in gifts.Buffs) { flag = true; petPanelSection.Add("Your " + StatText(buff.Stat), PetBuffs.IsFlat(buff.Stat) ? ("+" + P(buff.Amount)) : ("+" + P(buff.Amount) + "%"), float.NaN, PetPanelTone.Good); } } if (!flag) { petPanelSection.Add("Passive gifts", "none at this bond", float.NaN, PetPanelTone.Muted); } if (gifts.NextTierBuffs != null && Loyalty.NextTier(bond.LoyaltyValue, out var next, out var _)) { foreach (ResolvedBuff nextTierBuff in gifts.NextTierBuffs) { petPanelSection.Add($"At {next}", "+" + P(nextTierBuff.Amount) + (PetBuffs.IsFlat(nextTierBuff.Stat) ? "" : "%") + " " + StatText(nextTierBuff.Stat), float.NaN, PetPanelTone.Muted); } } return petPanelSection; } private static PetPanelSection Now(PetNowFacts now) { PetPanelSection petPanelSection = new PetPanelSection { Title = "Right now" }; petPanelSection.Add("Fighting", string.IsNullOrEmpty(now.CombatTargetName) ? "nothing — calm" : now.CombatTargetName, float.NaN, string.IsNullOrEmpty(now.CombatTargetName) ? PetPanelTone.Muted : PetPanelTone.Bad); if (!float.IsNaN(now.DistanceToPlayer)) { petPanelSection.Add("Distance to you", F1(now.DistanceToPlayer) + " m", float.NaN, PetPanelTone.Muted); } return petPanelSection; } private static PetPanelTone TierTone(LoyaltyTier t) { return t switch { LoyaltyTier.Devoted => PetPanelTone.Good, LoyaltyTier.Steady => PetPanelTone.Normal, LoyaltyTier.Fraying => PetPanelTone.Warn, _ => PetPanelTone.Bad, }; } private static string ComfortText(ComfortStage c, ComfortSide side = ComfortSide.None) { string text = side switch { ComfortSide.Hot => "the heat", ComfortSide.Cold => "the cold", _ => "the climate", }; return c switch { ComfortStage.Comfortable => "Comfortable", ComfortStage.Uneasy => (side == ComfortSide.None) ? "Uneasy" : ("Uneasy in " + text), ComfortStage.Suffering => "Suffering from " + text, _ => "Critical — it cannot endure " + text, }; } private static PetPanelTone ComfortTone(ComfortStage c) { return c switch { ComfortStage.Comfortable => PetPanelTone.Good, ComfortStage.Uneasy => PetPanelTone.Warn, _ => PetPanelTone.Bad, }; } private static string MoodText(Responsiveness r) { return r switch { Responsiveness.Sharp => "Sharp — eager and quick", Responsiveness.Responsive => "Responsive", Responsiveness.Sluggish => "Sluggish — the bond is strained", _ => "Unwilling — it barely heeds you", }; } private static PetPanelTone MoodTone(Responsiveness r) { return r switch { Responsiveness.Sharp => PetPanelTone.Good, Responsiveness.Responsive => PetPanelTone.Normal, Responsiveness.Sluggish => PetPanelTone.Warn, _ => PetPanelTone.Bad, }; } private static string StatText(PetBuffStat s) { return PetBuffs.DisplayName(s); } public static string Duration(double seconds) { if (seconds < 0.0) { seconds = 0.0; } long num = (long)Math.Round(seconds); long num2 = num / 3600; long num3 = num % 3600 / 60; long num4 = num % 60; if (num2 > 0) { return string.Format(CultureInfo.InvariantCulture, "{0}h {1}m", num2, num3); } if (num3 > 0) { return string.Format(CultureInfo.InvariantCulture, "{0}m {1}s", num3, num4); } return string.Format(CultureInfo.InvariantCulture, "{0}s", num4); } private static float Clamp01(float v) { if (!(v < 0f)) { if (!(v > 1f)) { return v; } return 1f; } return 0f; } private static string DamageText(float total, float[] profile) { if (profile == null) { return F0(total) + " Physical"; } int num = Math.Min(DamageTypeNames.Length, profile.Length); float[] array = CreatureAttributes.DistributeTotal(profile, total, DamageTypeNames.Length); List list = new List(2); for (int i = 0; i < num; i++) { if (profile[i] > 0f) { list.Add(F0(array[i]) + " " + DamageTypeNames[i]); } } if (list.Count <= 0) { return F0(total) + " Physical"; } return string.Join(" + ", list); } private static string F0(float v) { return v.ToString("F0", CultureInfo.InvariantCulture); } private static string F1(float v) { return v.ToString("0.#", CultureInfo.InvariantCulture); } private static string F2(float v) { return v.ToString("0.##", CultureInfo.InvariantCulture); } private static string F2d(double v) { return v.ToString("0.##", CultureInfo.InvariantCulture); } private static string P(float v) { return v.ToString("0.##", CultureInfo.InvariantCulture); } } public static class PetPower { public const float NeutralPercent = 100f; public const float DefaultUngovernedPercent = 50f; public const float MinPercent = 0f; public const float MaxPercent = 1000f; public static List> ParseOverrides(string spec) { return SpeciesFloats.Parse(spec); } public static float PercentFor(List> overrides, string speciesId, float globalPercent, out string matchedKey) { float num = SpeciesFloats.ForKeyed(overrides, speciesId, ref matchedKey); if (float.IsNaN(num)) { matchedKey = null; return globalPercent; } return num; } public static float PercentFor(List> overrides, string speciesId, float globalPercent) { string matchedKey; return PercentFor(overrides, speciesId, globalPercent, out matchedKey); } public static float Scale(float percent) { if (float.IsNaN(percent)) { return 1f; } float num = Clamp(percent, 0f, 1000f); if (num != 100f) { return num * 0.01f; } return 1f; } public static float Govern(bool hasBreakthrough, float ungovernedPercent) { if (!hasBreakthrough) { return Scale(ungovernedPercent); } return 1f; } public static bool IsOutOfRange(float percent) { if (!float.IsNaN(percent)) { if (!(percent < 0f)) { return percent > 1000f; } return true; } return false; } private static float Clamp(float v, float lo, float hi) { if (!(v < lo)) { if (!(v > hi)) { return v; } return hi; } return lo; } } public enum PetDeathMode { Permanent, KnockedOut, Disabled } public enum DeathOutcome { Dies, Collapses, AutoRevives } public readonly struct RegionCrossOutcome { public bool Allowed { get; } public bool BondBroken { get; } public int LoyaltyDelta { get; } public RegionCrossOutcome(bool allowed, bool bondBroken, int loyaltyDelta) { Allowed = allowed; BondBroken = bondBroken; LoyaltyDelta = loyaltyDelta; } } public static class PetRules { public const int KnockedOutLeftDownedPenalty = 30; public static DeathOutcome OnZeroHealth(PetDeathMode mode) { return mode switch { PetDeathMode.Permanent => DeathOutcome.Dies, PetDeathMode.KnockedOut => DeathOutcome.Collapses, _ => DeathOutcome.AutoRevives, }; } public static RegionCrossOutcome OnRegionCross(bool hasBreakthrough, bool firstCrossingOfPair) { if (!hasBreakthrough) { return new RegionCrossOutcome(allowed: false, bondBroken: true, 0); } return new RegionCrossOutcome(allowed: true, bondBroken: false, firstCrossingOfPair ? Loyalty.DeltaFor(LoyaltyEvent.CrossedRegionBonded) : 0); } public static int ApplyKnockedOutLeftDowned(int loyalty) { return Loyalty.ApplyDelta(loyalty, -30); } public static bool HasAbandoned(int loyalty) { return loyalty <= 0; } } public sealed class PetScavengeEntry { public string Species; public List ContainerCandidates = new List(); public List ContainerIds = new List(); public int[] SlotsPerTier = PetScavenge.DefaultSlotsPerTier; } public static class PetScavenge { public sealed class ScavengeCandidate { public string Label; public string Species; public PetScavengeEntry Entry; public LoyaltyTier Tier; } public sealed class ScavengePick { public ScavengeCandidate Winner; public int ExtraSlots; public readonly List MatchedButZero = new List(); } public const int TierCount = 5; public const int MaxSlotsPerTier = 10; public static readonly int[] DefaultSlotsPerTier = new int[5] { 0, 1, 1, 2, 3 }; public static Dictionary Parse(string json, Action warn = null) { return JsonTable.Read(json, warn, "species", "an object of species → scavenge objects", ParseEntry); } private static PetScavengeEntry ParseEntry(string speciesKey, object value, Action warn) { FieldReader f = new FieldReader("'" + speciesKey + "'", warn); if (!(value is Dictionary dictionary)) { f.Say("value must be an object — species skipped."); return null; } PetScavengeEntry petScavengeEntry = new PetScavengeEntry { Species = speciesKey }; foreach (KeyValuePair item in dictionary) { string text = item.Key.ToLowerInvariant(); if (!(text == "containers")) { if (text == "slotspertier") { petScavengeEntry.SlotsPerTier = ParseSlots(f, item.Value); } else { f.Unknown(item.Key, "containers, slotsPerTier"); } } else { ParseContainers(f, item.Value, petScavengeEntry); } } if (petScavengeEntry.ContainerCandidates.Count == 0 && petScavengeEntry.ContainerIds.Count == 0) { f.Say("missing required 'containers' — species skipped."); return null; } return petScavengeEntry; } private static void ParseContainers(FieldReader f, object value, PetScavengeEntry entry) { List list = new List(); if (value is List collection) { list.AddRange(collection); } else { list.Add(value); } ItemKey val = default(ItemKey); foreach (object item in list) { if (item is string text && text.Trim().Length == 0) { continue; } if (ItemKey.TryRead(item, ref val)) { if (((ItemKey)(ref val)).ItemId.HasValue) { if (!entry.ContainerIds.Contains(((ItemKey)(ref val)).ItemId.Value)) { entry.ContainerIds.Add(((ItemKey)(ref val)).ItemId.Value); } } else { entry.ContainerCandidates.Add(((ItemKey)(ref val)).Key.Trim()); } } else { f.Say("'containers' entries must be numbers or strings — one skipped."); } } } private static int[] ParseSlots(FieldReader f, object value) { if (!(value is List { Count: not 0 } list)) { f.Wrong("slotsPerTier", "a non-empty array of numbers", "using the default [" + string.Join(", ", ToStrings(DefaultSlotsPerTier)) + "]"); return DefaultSlotsPerTier; } if (list.Count > 5) { f.Say($"'slotsPerTier' has {list.Count} values but there are {5} loyalty tiers — extras ignored."); } int[] array = new int[5]; int num = 0; for (int i = 0; i < 5; i++) { if (i < list.Count) { if (list[i] is double num2) { int num3 = (int)num2; if (num3 < 0) { f.Say($"slotsPerTier[{i}] is negative — clamped to 0."); num3 = 0; } if (num3 > 10) { f.Say($"slotsPerTier[{i}] = {num3} exceeds the sanity cap — clamped to {10}."); num3 = 10; } num = num3; } else { f.Wrong($"slotsPerTier[{i}]", "a number", $"using {num}"); } } array[i] = num; } return array; } private static string[] ToStrings(int[] a) { string[] array = new string[a.Length]; for (int i = 0; i < a.Length; i++) { array[i] = a[i].ToString(CultureInfo.InvariantCulture); } return array; } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } public static PetScavengeEntry Resolve(Dictionary table, string speciesId) { PetScavengeEntry result = default(PetScavengeEntry); if (!SpeciesTable.TryResolve(table, speciesId, ref result, (string)null)) { return null; } return result; } public static int ExtraSlots(PetScavengeEntry entry, LoyaltyTier tier) { if (entry == null) { return 0; } int[] array = entry.SlotsPerTier ?? DefaultSlotsPerTier; if (array.Length == 0) { return 0; } int num = (int)tier; if (num < 0) { num = 0; } if (num >= array.Length) { num = array.Length - 1; } int num2 = array[num]; if (num2 >= 0) { if (num2 <= 10) { return num2; } return 10; } return 0; } public static bool MatchesContainer(PetScavengeEntry entry, int itemId, string liveName) { if (entry == null) { return false; } foreach (int containerId in entry.ContainerIds) { if (containerId == itemId) { return true; } } if (string.IsNullOrEmpty(liveName)) { return false; } string text = liveName.Trim(); if (text.Length == 0) { return false; } foreach (string containerCandidate in entry.ContainerCandidates) { if (string.Equals(containerCandidate, text, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public static LoyaltyTier TierFromLevel(int level) { if (level < 0) { level = 0; } if (level >= 5) { level = 4; } return (LoyaltyTier)level; } public static ScavengePick PickBonus(IList candidates, int itemId, string liveName) { ScavengePick scavengePick = new ScavengePick(); if (candidates == null) { return scavengePick; } foreach (ScavengeCandidate candidate in candidates) { if (candidate != null && MatchesContainer(candidate.Entry, itemId, liveName)) { int num = ExtraSlots(candidate.Entry, candidate.Tier); if (num > 0) { scavengePick.Winner = candidate; scavengePick.ExtraSlots = num; return scavengePick; } scavengePick.MatchedButZero.Add(candidate); } } return scavengePick; } } public enum Compass8 { N, NE, E, SE, S, SW, W, NW } public enum ScentStrength { Faint, Strong } public sealed class SenseDef { public string Key; public string Kind = "gatherable"; public string Trigger = "sniff"; public List ItemCandidates = new List(); public int? ItemId; public List CharacterCandidates = new List(); public List FactionCandidates = new List(); public List ComponentCandidates = new List(); public string Scope = "any"; public float Radius = 60f; public float CooldownSeconds = 300f; public float NearRadius = 20f; } public sealed class PetSenseEntry { public string Species; public List Senses = new List(); } public struct ScentHit { public string SenseKey; public string TargetId; public float Dx; public float Dz; public float Distance; } public sealed class ScentAlert { public string SenseKey; public string TargetId; public Compass8 Direction; public ScentStrength Strength; public float Distance; } public static class PetSenses { public const string KindGatherable = "gatherable"; public const string KindCharacter = "character"; public const string TriggerSniff = "sniff"; public const string FactionPrefix = "faction:"; public const string ComponentPrefix = "component:"; public const string ScopeAny = "any"; public const string ScopeOverworld = "overworld"; public const float DefaultRadius = 60f; public const float MaxRadius = 200f; public const float DefaultCooldownSeconds = 300f; public const float DefaultNearRadius = 20f; public static bool ScopeAllows(string scope, bool sceneIsTownOrCity) { return !(string.Equals(scope, "overworld", StringComparison.OrdinalIgnoreCase) && sceneIsTownOrCity); } public static Dictionary Parse(string json, Action warn = null) { return JsonTable.Read(json, warn, "species", "an object of species → sense objects", ParseEntry); } private static PetSenseEntry ParseEntry(string speciesKey, object value, Action warn) { FieldReader f = new FieldReader("'" + speciesKey + "'", warn); if (!(value is Dictionary dictionary)) { f.Say("value must be an object — species skipped."); return null; } PetSenseEntry petSenseEntry = new PetSenseEntry { Species = speciesKey }; foreach (KeyValuePair item in dictionary) { if (string.Equals(item.Key, "senses", StringComparison.OrdinalIgnoreCase)) { if (!f.Items("senses", item.Value, out List result, "an ARRAY of sense objects")) { continue; } foreach (object item2 in result) { SenseDef senseDef = ParseSense(f, speciesKey, item2); if (senseDef != null) { petSenseEntry.Senses.Add(senseDef); } } } else { f.Unknown(item.Key, "senses"); } } if (petSenseEntry.Senses.Count == 0) { f.Say("no usable senses — species skipped."); return null; } return petSenseEntry; } private static SenseDef ParseSense(FieldReader f, string speciesKey, object value) { if (!(value is Dictionary dictionary)) { f.Say("each sense must be an object — sense skipped."); return null; } SenseDef senseDef = new SenseDef(); foreach (KeyValuePair item in dictionary) { switch (item.Key.ToLowerInvariant()) { case "key": if (item.Value is string text && text.Trim().Length > 0) { senseDef.Key = text.Trim(); } break; case "kind": if (item.Value is string text3) { senseDef.Kind = text3.Trim(); } break; case "trigger": if (item.Value is string text2) { senseDef.Trigger = text2.Trim(); } break; case "item": ParseItem(f, item.Value, senseDef); break; case "character": ParseCharacter(f, item.Value, senseDef); break; case "scope": if (item.Value is string text4) { senseDef.Scope = text4.Trim(); } break; case "radius": if (item.Value is double num3 && num3 > 0.0) { senseDef.Radius = (float)num3; } else { f.Wrong("radius", "a number > 0", $"using {60f:0}"); } break; case "cooldownseconds": if (item.Value is double num2 && num2 >= 0.0) { senseDef.CooldownSeconds = (float)num2; } else { f.Wrong("cooldownSeconds", "a number >= 0", $"using {300f:0}"); } break; case "nearradius": if (item.Value is double num && num > 0.0) { senseDef.NearRadius = (float)num; } else { f.Wrong("nearRadius", "a number > 0", $"using {20f:0}"); } break; default: f.Say("unknown sense key '" + item.Key + "' (valid: key, kind, trigger, item, character, scope, radius, cooldownSeconds, nearRadius)."); break; } } if (string.IsNullOrEmpty(senseDef.Key)) { f.Say("sense missing required 'key' — sense skipped."); return null; } FieldReader fieldReader = new FieldReader("'" + speciesKey + "/" + senseDef.Key + "'", f.Sink); if (!string.Equals(senseDef.Scope, "any", StringComparison.OrdinalIgnoreCase) && !string.Equals(senseDef.Scope, "overworld", StringComparison.OrdinalIgnoreCase)) { fieldReader.Say("unknown scope '" + senseDef.Scope + "' (this build: any, overworld) — sense skipped."); return null; } bool flag = string.Equals(senseDef.Kind, "gatherable", StringComparison.OrdinalIgnoreCase); bool flag2 = string.Equals(senseDef.Kind, "character", StringComparison.OrdinalIgnoreCase); if (!flag && !flag2) { fieldReader.Say("unknown kind '" + senseDef.Kind + "' (this build: gatherable, character) — sense skipped."); return null; } if (!string.Equals(senseDef.Trigger, "sniff", StringComparison.OrdinalIgnoreCase)) { fieldReader.Say("unknown trigger '" + senseDef.Trigger + "' (this build: sniff) — sense skipped."); return null; } if (flag) { if (!senseDef.ItemId.HasValue && senseDef.ItemCandidates.Count == 0) { fieldReader.Say("missing required 'item' — sense skipped."); return null; } if (senseDef.CharacterCandidates.Count > 0 || senseDef.FactionCandidates.Count > 0 || senseDef.ComponentCandidates.Count > 0) { fieldReader.Say("'character' entries are ignored on a gatherable sense."); } if (senseDef.Radius > 200f) { f.Say($"radius {senseDef.Radius:0.#} exceeds the engine's ~{200f:0}m gatherable " + "activeness ceiling (colliders beyond it are deactivated) — clamped."); senseDef.Radius = 200f; } } else { if (senseDef.CharacterCandidates.Count == 0 && senseDef.FactionCandidates.Count == 0 && senseDef.ComponentCandidates.Count == 0) { fieldReader.Say("missing required 'character' (name candidates, faction:X and/or component:X) — sense skipped."); return null; } if (senseDef.ItemId.HasValue || senseDef.ItemCandidates.Count > 0) { fieldReader.Say("'item' entries are ignored on a character sense."); } } return senseDef; } private static void ParseItem(FieldReader f, object value, SenseDef def) { List list = new List(); if (value is List collection) { list.AddRange(collection); } else { list.Add(value); } ItemKey val = default(ItemKey); foreach (object item in list) { if (item is string text && text.Trim().Length == 0) { continue; } if (ItemKey.TryRead(item, ref val)) { if (((ItemKey)(ref val)).ItemId.HasValue) { if (!def.ItemId.HasValue) { def.ItemId = ((ItemKey)(ref val)).ItemId; } } else { def.ItemCandidates.Add(((ItemKey)(ref val)).Key); } } else { f.Say("'item' entries must be numbers or strings — one skipped."); } } } private static void ParseCharacter(FieldReader f, object value, SenseDef def) { List list = new List(); if (value is List collection) { list.AddRange(collection); } else { list.Add(value); } foreach (object item in list) { if (!(item is string text)) { f.Say("'character' entries must be strings — one skipped."); continue; } string text2 = text.Trim(); if (text2.Length == 0) { continue; } if (text2.StartsWith("faction:", StringComparison.OrdinalIgnoreCase)) { string text3 = text2.Substring("faction:".Length).Trim(); if (text3.Length == 0) { f.Say("empty 'faction:' token — one skipped."); } else { def.FactionCandidates.Add(text3); } } else if (text2.StartsWith("component:", StringComparison.OrdinalIgnoreCase)) { string text4 = text2.Substring("component:".Length).Trim(); if (text4.Length == 0) { f.Say("empty 'component:' token — one skipped."); } else { def.ComponentCandidates.Add(text4); } } else { def.CharacterCandidates.Add(text2); } } } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } public static PetSenseEntry Resolve(Dictionary table, string speciesId) { PetSenseEntry result = default(PetSenseEntry); if (!SpeciesTable.TryResolve(table, speciesId, ref result, (string)null)) { return null; } return result; } public static bool MatchesName(SenseDef def, string liveName) { if (def == null || string.IsNullOrEmpty(liveName)) { return false; } string text = liveName.Trim(); if (text.Length == 0) { return false; } foreach (string itemCandidate in def.ItemCandidates) { if (string.Equals(itemCandidate, text, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public static bool MatchesCharacter(SenseDef def, string liveName) { if (def == null || string.IsNullOrEmpty(liveName)) { return false; } string text = liveName.Trim(); if (text.Length == 0) { return false; } foreach (string characterCandidate in def.CharacterCandidates) { if (string.Equals(characterCandidate, text, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public static bool MatchesFaction(SenseDef def, string factionName) { if (def == null || string.IsNullOrEmpty(factionName)) { return false; } foreach (string factionCandidate in def.FactionCandidates) { if (string.Equals(factionCandidate, factionName.Trim(), StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public static Compass8 Bucket(float dx, float dz, float northYawOffsetDeg = 0f) { double num = Math.Atan2(dx, dz) * (180.0 / Math.PI) - (double)northYawOffsetDeg; num %= 360.0; if (num < 0.0) { num += 360.0; } return (Compass8)((int)Math.Floor((num + 22.5) / 45.0) % 8); } public static string DirectionLabel(Compass8 dir) { return dir switch { Compass8.N => "north", Compass8.NE => "north-east", Compass8.E => "east", Compass8.SE => "south-east", Compass8.S => "south", Compass8.SW => "south-west", Compass8.W => "west", _ => "north-west", }; } } public sealed class ScentTracker { public const float SwitchMargin = 5f; public const int MissesToClear = 2; private const int CooldownMapCap = 64; private readonly Dictionary _lastAlertAt = new Dictionary(StringComparer.Ordinal); private int _misses; public ScentAlert Current { get; private set; } public void Clear() { Current = null; _misses = 0; _lastAlertAt.Clear(); } public ScentAlert Decide(IReadOnlyList hits, Func defFor, double now, float northYawOffsetDeg = 0f) { if (hits == null || hits.Count == 0) { if (Current != null && ++_misses >= 2) { Current = null; } return null; } _misses = 0; int num = 0; for (int i = 1; i < hits.Count; i++) { if (hits[i].Distance < hits[num].Distance) { num = i; } } int index = num; if (Current != null && hits[num].TargetId != Current.TargetId) { int num2 = -1; for (int j = 0; j < hits.Count; j++) { if (hits[j].TargetId == Current.TargetId) { num2 = j; break; } } if (num2 >= 0 && hits[num].Distance > hits[num2].Distance - 5f) { index = num2; } } ScentHit scentHit = hits[index]; SenseDef senseDef = defFor?.Invoke(scentHit.SenseKey); if (senseDef == null) { Current = null; return null; } Current = new ScentAlert { SenseKey = scentHit.SenseKey, TargetId = scentHit.TargetId, Direction = PetSenses.Bucket(scentHit.Dx, scentHit.Dz, northYawOffsetDeg), Strength = ((scentHit.Distance <= senseDef.NearRadius) ? ScentStrength.Strong : ScentStrength.Faint), Distance = scentHit.Distance }; if (_lastAlertAt.TryGetValue(scentHit.TargetId, out var value) && now - value < (double)senseDef.CooldownSeconds) { return null; } if (_lastAlertAt.Count >= 64 && !_lastAlertAt.ContainsKey(scentHit.TargetId)) { _lastAlertAt.Clear(); } _lastAlertAt[scentHit.TargetId] = now; return Current; } public double CooldownRemaining(string targetId, float cooldownSeconds, double now) { if (targetId == null || !_lastAlertAt.TryGetValue(targetId, out var value)) { return 0.0; } return Math.Max(0.0, (double)cooldownSeconds - (now - value)); } } public struct PetTick { public double Dt; public int TempStepsOutsideComfort; public ComfortSide TempSide; public double TempEscalateMult; } public struct PetStatus { public int LoyaltyValue; public LoyaltyTier Loyalty; public ComfortStage Comfort; public ComfortSide ComfortSide; public Responsiveness Responsiveness; public bool Abandoned; public int HungerDecayDays; public int LoyaltyBeforeHungerDecay; public double SelfHealHp; } public sealed class PetTuning { public double HungerSecondsPerDay = 1200.0; public double TempEscalateSeconds = 30.0; public double TempRecoverSeconds = 15.0; public int SpeciesDailyDecay = 15; public double SatiationFraction = 0.25; public double UneasyDecayMult = 1.5; public double SufferingDecayMult = 2.0; public double LoyaltyGainScale = 0.05; } public sealed class PetSimulation { public GearEffects Gear = GearEffects.None; private readonly PetTuning _t; private readonly TemperatureExposure _temp; private readonly HungerTimer _hunger; private ComfortSide _side; public PetSave State { get; } public double HungerFraction { get { if (!(_hunger.SecondsPerDay > 0.0)) { return 0.0; } return _hunger.SecondsSinceFed / _hunger.SecondsPerDay; } } public bool IsSatiated { get { if (_hunger.SecondsPerDay > 0.0 && _t.SatiationFraction > 0.0) { return HungerFraction < _t.SatiationFraction; } return false; } } public double HungerSecondsPerDay => _hunger.SecondsPerDay; public double LoyaltyGainCarry => State.LoyaltyGainCarry; public PetSimulation(PetSave state, PetTuning tuning = null, double? speciesHungerSecondsPerDay = null) { State = state; _t = tuning ?? new PetTuning(); _temp = new TemperatureExposure(_t.TempEscalateSeconds, _t.TempRecoverSeconds, state.TemperatureStage); _hunger = new HungerTimer(speciesHungerSecondsPerDay ?? _t.HungerSecondsPerDay, state.HungerSecondsSinceFed); } public PetStatus Tick(PetTick input) { ComfortStage comfortStage = _temp.Tick(input.TempStepsOutsideComfort, input.Dt, input.TempEscalateMult); _side = input.TempSide; if (State.BlanketSecondsLeft > 0.0) { State.BlanketSecondsLeft -= input.Dt; if (State.BlanketSecondsLeft <= 0.0) { State.BlanketSecondsLeft = 0.0; State.BlanketKey = ""; } } if (State.DrinkSecondsLeft > 0.0) { State.DrinkSecondsLeft -= input.Dt; if (State.DrinkSecondsLeft <= 0.0) { State.DrinkSecondsLeft = 0.0; State.DrinkKey = ""; } } if (State.BuffFoodSecondsLeft > 0.0) { State.BuffFoodSecondsLeft -= input.Dt; if (State.BuffFoodSecondsLeft <= 0.0) { State.BuffFoodSecondsLeft = 0.0; State.BuffFoodKey = ""; } } double selfHealHp = 0.0; if (State.HealthRecoverySecondsLeft > 0.0) { selfHealHp = HealthRecovery.TickHeal(State.HealthRecoveryLevel, Math.Min(input.Dt, State.HealthRecoverySecondsLeft)); State.HealthRecoverySecondsLeft -= input.Dt; if (State.HealthRecoverySecondsLeft <= 0.0) { State.HealthRecoverySecondsLeft = 0.0; State.HealthRecoveryLevel = 0; } } int num = _hunger.Tick(input.Dt); int loyaltyValue = State.LoyaltyValue; int speciesDailyDecay = (int)Math.Round((double)_t.SpeciesDailyDecay * DecayMult(comfortStage)); for (int i = 0; i < num; i++) { State.LoyaltyValue = Loyalty.ApplyDelta(State.LoyaltyValue, ScaleGain(Loyalty.DeltaFor(LoyaltyEvent.DayWithoutFeeding, speciesDailyDecay))); } State.TemperatureStage = comfortStage; State.HungerSecondsSinceFed = _hunger.SecondsSinceFed; PetStatus result = Status(); result.HungerDecayDays = num; result.LoyaltyBeforeHungerDecay = loyaltyValue; result.SelfHealHp = selfHealHp; return result; } private double DecayMult(ComfortStage stage) { switch (stage) { case ComfortStage.Uneasy: return _t.UneasyDecayMult; case ComfortStage.Suffering: case ComfortStage.Critical: return _t.SufferingDecayMult; default: return 1.0; } } public PetStatus Feed(FoodMatch food) { _hunger.Feed(); State.HungerSecondsSinceFed = 0.0; if (food.IsMatch) { State.LoyaltyValue = Loyalty.ApplyDelta(State.LoyaltyValue, ScaleGain(food.LoyaltyGain)); } return Status(); } public PetStatus SetLoyalty(int value) { State.LoyaltyValue = ((value >= 0) ? ((value > 100) ? 100 : value) : 0); return Status(); } public PetStatus SetHungerFraction(double fraction) { double secondsSinceFed = ((fraction < 0.0) ? 0.0 : fraction) * _hunger.SecondsPerDay; _hunger.Seed(secondsSinceFed); State.HungerSecondsSinceFed = _hunger.SecondsSinceFed; return Status(); } public void RecordMeal(string mealKey) { FoodHexes.RecordMeal(State.RecentMeals, mealKey); } public PetStatus OnLoyaltyEvent(LoyaltyEvent ev) { State.LoyaltyValue = Loyalty.ApplyDelta(State.LoyaltyValue, ScaleGain(Loyalty.DeltaFor(ev, _t.SpeciesDailyDecay))); return Status(); } public int ScaleGain(int delta) { if (delta < 0) { float loyaltyDecayMult = Gear.LoyaltyDecayMult; if (loyaltyDecayMult != 1f) { return (int)Math.Round((float)delta * loyaltyDecayMult, MidpointRounding.AwayFromZero); } return delta; } if (delta == 0) { return 0; } if (State.LoyaltyValue >= 100) { return 0; } return Loyalty.BankGain((double)delta * (double)Gear.LoyaltyGainMult * _t.LoyaltyGainScale, ref State.LoyaltyGainCarry); } public PetStatus Status() { LoyaltyTier loyalty = Loyalty.Tier(State.LoyaltyValue); return new PetStatus { LoyaltyValue = State.LoyaltyValue, Loyalty = loyalty, Comfort = State.TemperatureStage, ComfortSide = _side, Responsiveness = ResponsivenessRules.Derive(loyalty, State.TemperatureStage), Abandoned = PetRules.HasAbandoned(State.LoyaltyValue) }; } } public sealed class StatTuningPolicy { public float HealthAt0 = 0.5f; public float HealthAt100 = 1.5f; public float DamageAt0 = 1f; public float DamageAt100 = 1f; public float DefenseAt0 = 1f; public float DefenseAt100 = 1f; public float SpeedAt0 = 1f; public float SpeedAt100 = 1f; } public sealed class StatModifiers { public float Health = 1f; public float Damage = 1f; public float Defense = 1f; public float Speed = 1f; public StatModifiers CombinedWith(StatModifiers other) { if (other == null) { return this; } return new StatModifiers { Health = Health * other.Health, Damage = Damage * other.Damage, Defense = Defense * other.Defense, Speed = Speed * other.Speed }; } } public static class PetStatTuning { public static float Factor(int loyaltyValue, float at0, float at100) { float num = ((loyaltyValue <= 0) ? 0f : ((loyaltyValue >= 100) ? 1f : ((float)loyaltyValue / 100f))); return at0 + (at100 - at0) * num; } public static CreatureAttributes Effective(CreatureAttributes captured, int loyaltyValue, StatTuningPolicy policy, StatModifiers extra = null) { if (captured == null) { return null; } policy = policy ?? new StatTuningPolicy(); float num = Factor(loyaltyValue, policy.HealthAt0, policy.HealthAt100) * (extra?.Health ?? 1f); float num2 = Factor(loyaltyValue, policy.DamageAt0, policy.DamageAt100) * (extra?.Damage ?? 1f); float num3 = Factor(loyaltyValue, policy.DefenseAt0, policy.DefenseAt100) * (extra?.Defense ?? 1f); float num4 = Factor(loyaltyValue, policy.SpeedAt0, policy.SpeedAt100) * (extra?.Speed ?? 1f); CreatureAttributes val = captured.Clone(); val.MaxHealth *= num; val.MoveSpeed *= num4; val.Impact *= num2; val.ImpactResistance *= num3; val.Barrier *= num3; val.ProtectionAll *= num3; val.StatusResistance = StatusResistPolicy.Cap(ScaleResist(val.StatusResistance, num3)); for (int i = 0; i < 9; i++) { val.Resist[i] = ScaleResist(val.Resist[i], num3); val.Protection[i] *= num3; val.Damage[i] *= num2; } return val; } private static float ScaleResist(float v, float defense) { if (!(v >= 0f)) { return Clamp(v / ((defense < 0.01f) ? 0.01f : defense), -100f, 0f); } return Clamp(v * defense, 0f, 100f); } private static float Clamp(float v, float lo, float hi) { if (!(v < lo)) { if (!(v > hi)) { return v; } return hi; } return lo; } } public sealed class PetStatusIconDef { public PetIcon Icon; public string Id = ""; public int NumId; public string Name = ""; public bool Malus; } public static class PetStatusIconDefs { public static readonly PetStatusIconDef[] All = new PetStatusIconDef[19] { new PetStatusIconDef { Icon = PetIcon.Hungry, Id = "BW_PetHungry", NumId = 87050, Name = "Hungry Companion", Malus = true }, new PetStatusIconDef { Icon = PetIcon.Starving, Id = "BW_PetStarving", NumId = 87051, Name = "Starving Companion", Malus = true }, new PetStatusIconDef { Icon = PetIcon.BondFraying, Id = "BW_BondFraying", NumId = 87052, Name = "Fraying Bond", Malus = true }, new PetStatusIconDef { Icon = PetIcon.BondBroken, Id = "BW_BondBroken", NumId = 87053, Name = "Broken Bond", Malus = true }, new PetStatusIconDef { Icon = PetIcon.BondSteady, Id = "BW_BondSteady", NumId = 87054, Name = "Steady Bond", Malus = false }, new PetStatusIconDef { Icon = PetIcon.BondDevoted, Id = "BW_BondDevoted", NumId = 87055, Name = "Devoted Bond", Malus = false }, new PetStatusIconDef { Icon = PetIcon.Cold, Id = "BW_PetCold", NumId = 87056, Name = "Cold Companion", Malus = true }, new PetStatusIconDef { Icon = PetIcon.Freezing, Id = "BW_PetFreezing", NumId = 87057, Name = "Freezing Companion", Malus = true }, new PetStatusIconDef { Icon = PetIcon.Hot, Id = "BW_PetHot", NumId = 87058, Name = "Hot Companion", Malus = true }, new PetStatusIconDef { Icon = PetIcon.Overheating, Id = "BW_PetOverheating", NumId = 87059, Name = "Overheating Companion", Malus = true }, new PetStatusIconDef { Icon = PetIcon.Scent, Id = "BW_PetScent", NumId = 87060, Name = "Scent Trail", Malus = false }, new PetStatusIconDef { Icon = PetIcon.CommunionBroken, Id = "BW_CommunionBroken", NumId = 87063, Name = "Communion — Broken", Malus = true }, new PetStatusIconDef { Icon = PetIcon.CommunionFraying, Id = "BW_CommunionFraying", NumId = 87064, Name = "Communion — Fraying", Malus = true }, new PetStatusIconDef { Icon = PetIcon.CommunionSteady, Id = "BW_CommunionSteady", NumId = 87065, Name = "Communion — Steady", Malus = false }, new PetStatusIconDef { Icon = PetIcon.CommunionDevoted, Id = "BW_CommunionDevoted", NumId = 87066, Name = "Communion — Devoted", Malus = false }, new PetStatusIconDef { Icon = PetIcon.BuffCrystalPowder, Id = "BW_BuffCrystalPowder", NumId = 87067, Name = "Crystal-Fed Companion", Malus = false }, new PetStatusIconDef { Icon = PetIcon.BuffAmbraine, Id = "BW_BuffAmbraine", NumId = 87068, Name = "Ambraine-Fed Companion", Malus = false }, new PetStatusIconDef { Icon = PetIcon.BuffGaberryWine, Id = "BW_BuffGaberryWine", NumId = 87069, Name = "Wine-Fed Companion", Malus = false }, new PetStatusIconDef { Icon = PetIcon.BuffDarkStone, Id = "BW_BuffDarkStone", NumId = 87070, Name = "Stone-Fed Companion", Malus = false } }; } public enum PetIcon { None, Hungry, Starving, BondFraying, BondBroken, BondSteady, BondDevoted, Cold, Freezing, Hot, Overheating, Scent, CommunionBroken, CommunionFraying, CommunionSteady, CommunionDevoted, BuffCrystalPowder, BuffAmbraine, BuffGaberryWine, BuffDarkStone } public static class PetStatusIcons { public const double DefaultHungryAtFraction = 0.75; public const int CrystalPowderItemId = 6600040; public const int AmbraineItemId = 4000430; public const int GaberryWineItemId = 4100590; public const int DarkStoneItemId = 6500031; public static PetIcon HungerIcon(double hungerFraction, double hungryAt = 0.75) { if (hungerFraction >= 1.0) { return PetIcon.Starving; } if (hungryAt > 0.0 && hungerFraction >= hungryAt) { return PetIcon.Hungry; } return PetIcon.None; } public static PetIcon LoyaltyIcon(LoyaltyTier tier) { return tier switch { LoyaltyTier.Gone => PetIcon.BondBroken, LoyaltyTier.Broken => PetIcon.BondBroken, LoyaltyTier.Fraying => PetIcon.BondFraying, LoyaltyTier.Steady => PetIcon.BondSteady, LoyaltyTier.Devoted => PetIcon.BondDevoted, _ => PetIcon.None, }; } public static PetIcon CommunionIcon(LoyaltyTier tier) { return tier switch { LoyaltyTier.Gone => PetIcon.CommunionBroken, LoyaltyTier.Broken => PetIcon.CommunionBroken, LoyaltyTier.Fraying => PetIcon.CommunionFraying, LoyaltyTier.Steady => PetIcon.CommunionSteady, LoyaltyTier.Devoted => PetIcon.CommunionDevoted, _ => PetIcon.None, }; } public static PetIcon BondIcon(LoyaltyTier tier, bool communionKnown) { if (!communionKnown) { return LoyaltyIcon(tier); } return CommunionIcon(tier); } public static PetIcon TemperatureIcon(ComfortStage stage, ComfortSide side) { if (stage == ComfortStage.Comfortable || side == ComfortSide.None) { return PetIcon.None; } bool flag = stage >= ComfortStage.Suffering; if (side != ComfortSide.Cold) { if (!flag) { return PetIcon.Hot; } return PetIcon.Overheating; } if (!flag) { return PetIcon.Cold; } return PetIcon.Freezing; } public static PetIcon ScentIcon(bool scentActive) { if (!scentActive) { return PetIcon.None; } return PetIcon.Scent; } public static PetIcon BuffFoodIcon(int itemId) { return itemId switch { 6600040 => PetIcon.BuffCrystalPowder, 4000430 => PetIcon.BuffAmbraine, 4100590 => PetIcon.BuffGaberryWine, 6500031 => PetIcon.BuffDarkStone, _ => PetIcon.None, }; } } public static class RegionCrossings { public static string SanitizeToken(string s) { if (!string.IsNullOrEmpty(s)) { return s.Replace('+', ' ').Replace('|', ' ').Replace('\t', ' ') .Replace('\n', ' ') .Replace('\r', ' ') .Trim(); } return ""; } public static string Canonical(string a, string b) { string text = SanitizeToken(a).ToLowerInvariant(); string text2 = SanitizeToken(b).ToLowerInvariant(); if (string.CompareOrdinal(text, text2) > 0) { return text2 + "+" + text; } return text + "+" + text2; } public static bool Contains(List pairs, string a, string b) { return pairs?.Contains(Canonical(a, b)) ?? false; } public static bool TryRecord(List pairs, string a, string b) { if (pairs == null) { return false; } string item = Canonical(a, b); if (pairs.Contains(item)) { return false; } pairs.Add(item); return true; } public static string Flatten(List pairs) { if (pairs != null && pairs.Count != 0) { return string.Join("|", pairs); } return ""; } public static void Parse(string cell, List into, Action warn = null) { if (string.IsNullOrEmpty(cell) || into == null) { return; } string[] array = cell.Split(new char[1] { '|' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length != 0) { int num = text.IndexOf('+'); string text2 = ((num < 0) ? "" : text.Substring(0, num)); string text3 = ((num < 0) ? "" : text.Substring(num + 1)); if (SanitizeToken(text2).Length == 0 || SanitizeToken(text3).Length == 0) { warn?.Invoke("malformed crossed-region pair '" + text + "' (want a+b) — skipped."); } else { TryRecord(into, text2, text3); } } } } } public enum ReleaseRuling { Proceed, NeedConfirm, ConfirmExpired } public static class ReleaseGate { public const double DefaultWindowSeconds = 30.0; public const double DefaultMinDwellSeconds = 1.0; public const double NotArmed = double.NegativeInfinity; public static ReleaseRuling Evaluate(int loyalty, int threshold, double confirmArmedAtSeconds, double nowSeconds, out bool rearm, double windowSeconds = 30.0, double minDwellSeconds = 1.0) { rearm = false; if (threshold < 0) { return ReleaseRuling.Proceed; } if (loyalty >= 0 && loyalty < threshold) { return ReleaseRuling.Proceed; } if (windowSeconds <= 0.0 || double.IsNaN(windowSeconds)) { windowSeconds = 30.0; } if (double.IsNaN(minDwellSeconds) || minDwellSeconds < 0.0) { minDwellSeconds = 0.0; } if (minDwellSeconds >= windowSeconds) { minDwellSeconds = ((1.0 < windowSeconds) ? 1.0 : 0.0); } if (double.IsNaN(confirmArmedAtSeconds) || double.IsInfinity(confirmArmedAtSeconds) || confirmArmedAtSeconds < 0.0 || confirmArmedAtSeconds > nowSeconds) { rearm = true; return ReleaseRuling.NeedConfirm; } double num = nowSeconds - confirmArmedAtSeconds; if (num < minDwellSeconds) { return ReleaseRuling.NeedConfirm; } if (num <= windowSeconds) { return ReleaseRuling.Proceed; } rearm = true; return ReleaseRuling.ConfirmExpired; } public static bool Holds(ReleaseRuling ruling) { return ruling != ReleaseRuling.Proceed; } } public enum Responsiveness { Sharp, Responsive, Sluggish, Unwilling } public static class ResponsivenessRules { public static Responsiveness Derive(LoyaltyTier loyalty, ComfortStage comfort) { return (Responsiveness)Math.Max(loyalty switch { LoyaltyTier.Devoted => 0, LoyaltyTier.Steady => 1, LoyaltyTier.Fraying => 2, _ => 3, }, comfort switch { ComfortStage.Comfortable => 0, ComfortStage.Uneasy => 1, ComfortStage.Suffering => 2, _ => 3, }); } } public static class ResponsivenessEffects { public static float SpeedMultiplier(Responsiveness r) { return r switch { Responsiveness.Sharp => 1.15f, Responsiveness.Responsive => 1f, Responsiveness.Sluggish => 0.6f, Responsiveness.Unwilling => 0.35f, _ => 1f, }; } public static float DamageFactor(Responsiveness r) { return r switch { Responsiveness.Sharp => 1.25f, Responsiveness.Responsive => 1f, Responsiveness.Sluggish => 0.6f, Responsiveness.Unwilling => 0.25f, _ => 1f, }; } } public static class RestHeal { public const double FullHealHours = 8.0; public static double HealFraction(double sleepHours) { if (double.IsNaN(sleepHours) || sleepHours <= 0.0) { return 0.0; } double num = sleepHours / 8.0; if (!(num > 1.0)) { return num; } return 1.0; } public static bool IsFullHeal(double sleepHours) { return HealFraction(sleepHours) >= 1.0; } public static double HealAmount(double sleepHours, double maxHp) { if (double.IsNaN(maxHp) || maxHp <= 0.0) { return 0.0; } return HealFraction(sleepHours) * maxHp; } } public sealed class PetSave { public string SpeciesId = ""; public string Name = ""; public int LoyaltyValue; public double HungerSecondsSinceFed; public ComfortStage TemperatureStage; public bool Downed; public CreatureAttributes Attributes; public string BlanketKey = ""; public double BlanketSecondsLeft; public string DrinkKey = ""; public double DrinkSecondsLeft; public string BuffFoodKey = ""; public double BuffFoodSecondsLeft; public List RecentMeals = new List(); public double SpecialCooldownSecondsLeft; public double SigilCooldownSecondsLeft; public int RelicStacks; public List CrossedRegionPairs = new List(); public double HealthFraction = 1.0; public int HealthRecoveryLevel; public double HealthRecoverySecondsLeft; public double LoyaltyGainCarry; public LoyaltyTier Tier => Loyalty.Tier(LoyaltyValue); } public sealed class SaveFile { public const int CurrentVersion = 12; public int Version = 12; public List Pets = new List(); public IEnumerable Active => Pets.Where((PetSave p) => p.LoyaltyValue > 0); public int Prune() { int count = Pets.Count; Pets.RemoveAll((PetSave p) => p.LoyaltyValue <= 0); return count - Pets.Count; } } public static class SaveCodec { private const char Sep = '\t'; public static string Serialize(SaveFile f, Action warn = null) { SaveColumn[] all = SaveColumns.All; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append('v').Append(f.Version.ToString(CultureInfo.InvariantCulture)).Append('\n'); foreach (PetSave pet in f.Pets) { for (int i = 0; i < all.Length; i++) { if (i > 0) { stringBuilder.Append('\t'); } stringBuilder.Append(all[i].Write(pet, warn)); } stringBuilder.Append('\n'); } return stringBuilder.ToString(); } public static SaveFile Deserialize(string text, Action warn = null) { SaveFile saveFile = new SaveFile { Pets = new List() }; if (string.IsNullOrWhiteSpace(text)) { return saveFile; } string[] array = text.Replace("\r", "").Split(new char[1] { '\n' }); int i = 0; if (array.Length != 0 && array[0].StartsWith("v")) { if (int.TryParse(array[0].Substring(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && result > 0) { saveFile.Version = result; } else { warn?.Invoke($"unreadable version header '{array[0]}' — assuming current (v{12})."); } i = 1; } for (; i < array.Length; i++) { if (array[i].Length == 0) { continue; } string[] array2 = array[i].Split(new char[1] { '\t' }); int num = SaveColumns.CountFor(1); if (array2.Length < num) { warn?.Invoke($"save line {i} has {array2.Length} column(s), need {num} — skipped."); continue; } int num2 = SaveColumns.All.Length; int num3 = ((saveFile.Version >= 1 && saveFile.Version <= 12) ? SaveColumns.CountFor(saveFile.Version) : num2); if (array2.Length > num2) { warn?.Invoke($"save line {i} has {array2.Length} column(s), beyond the {num2} this codec knows (v{12}) — a newer save? extra columns ignored."); } else if (array2.Length > num3) { warn?.Invoke($"save line {i} has {array2.Length} column(s) but its v{saveFile.Version} header implies {num3} — append-only discipline expects a version bump."); } else if (array2.Length < num3) { warn?.Invoke($"save line {i} has {array2.Length} column(s) but its v{saveFile.Version} header implies {num3} — truncated row? the {num3 - array2.Length} missing column(s) read as their pre-feature defaults."); } PetSave petSave = new PetSave(); SaveColumn[] all = SaveColumns.All; foreach (SaveColumn saveColumn in all) { if (array2.Length > saveColumn.Index) { saveColumn.Read(array2[saveColumn.Index], petSave, warn); } } saveFile.Pets.Add(petSave); } return saveFile; } } internal sealed class SaveColumn { public readonly int Index; public readonly int SinceVersion; public readonly Func, string> Write; public readonly Action> Read; public SaveColumn(int index, int since, Func, string> write, Action> read) { Index = index; SinceVersion = since; Write = write; Read = read; } } internal static class SaveColumns { internal static readonly SaveColumn[] All; internal static int CountFor(int version) { return All.Count((SaveColumn col) => col.SinceVersion <= version); } static SaveColumns() { All = Build(); for (int i = 0; i < All.Length; i++) { if (All[i].Index != i) { throw new InvalidOperationException($"save column registry broken: entry {i} declares index {All[i].Index}."); } } for (int j = 1; j < All.Length; j++) { if (All[j].SinceVersion < All[j - 1].SinceVersion) { throw new InvalidOperationException($"save column registry broken: column {j} version {All[j].SinceVersion} precedes its neighbour."); } } if (All[All.Length - 1].SinceVersion != 12) { throw new InvalidOperationException($"save column registry broken: newest column is v{All[All.Length - 1].SinceVersion} but SaveFile.CurrentVersion is v{12}."); } if (CountFor(12) != All.Length) { throw new InvalidOperationException("save column registry broken: current-version count != registry size."); } } private static SaveColumn[] Build() { return new SaveColumn[17] { new SaveColumn(0, 1, (PetSave p, Action warn) => San(p.SpeciesId), delegate(string c, PetSave p, Action warn) { p.SpeciesId = c; }), new SaveColumn(1, 1, (PetSave p, Action warn) => San(p.Name), delegate(string c, PetSave p, Action warn) { p.Name = c; }), new SaveColumn(2, 1, (PetSave p, Action warn) => p.LoyaltyValue.ToString(CultureInfo.InvariantCulture), delegate(string c, PetSave p, Action warn) { int num = ParseInt(c, warn, "loyalty"); int num2 = ((num >= 0) ? ((num > 100) ? 100 : num) : 0); if (num2 != num) { warn?.Invoke($"loyalty {num} out of range — clamped to {num2}."); } p.LoyaltyValue = num2; }), new SaveColumn(3, 1, (PetSave p, Action warn) => Num(p.HungerSecondsSinceFed, warn, "hunger seconds"), delegate(string c, PetSave p, Action warn) { p.HungerSecondsSinceFed = ParseDouble(c, warn, "hunger seconds"); }), new SaveColumn(4, 1, delegate(PetSave p, Action warn) { int temperatureStage = (int)p.TemperatureStage; return temperatureStage.ToString(CultureInfo.InvariantCulture); }, delegate(string c, PetSave p, Action warn) { int num = ParseInt(c, warn, "temperature stage"); int num2 = ((num >= 0) ? ((num > 3) ? 3 : num) : 0); if (num2 != num) { warn?.Invoke($"temperature stage {num} out of range — clamped to {num2}."); } p.TemperatureStage = (ComfortStage)num2; }), new SaveColumn(5, 1, (PetSave p, Action warn) => (!p.Downed) ? "0" : "1", delegate(string c, PetSave p, Action warn) { if (c != "0" && c != "1") { warn?.Invoke("unreadable downed flag '" + c + "' — using false."); } p.Downed = c == "1"; }), new SaveColumn(6, 2, (PetSave p, Action warn) => (p.Attributes == null) ? "" : p.Attributes.Flatten(warn), delegate(string c, PetSave p, Action warn) { p.Attributes = CreatureAttributes.Parse(c, warn); }), TimedSlot(7, 3, "blanket", (PetSave p) => p.BlanketKey, (PetSave p) => p.BlanketSecondsLeft, delegate(PetSave p, string key, double secs, Action warn) { p.BlanketKey = key; p.BlanketSecondsLeft = secs; }), new SaveColumn(8, 4, (PetSave p, Action warn) => FlattenMeals(p), delegate(string c, PetSave p, Action warn) { ParseMeals(c, p); }), TimedSlot(9, 5, "drink", (PetSave p) => p.DrinkKey, (PetSave p) => p.DrinkSecondsLeft, delegate(PetSave p, string key, double secs, Action warn) { p.DrinkKey = key; p.DrinkSecondsLeft = secs; }), new SaveColumn(10, 6, (PetSave p, Action warn) => (!(p.SpecialCooldownSecondsLeft > 0.0)) ? "" : p.SpecialCooldownSecondsLeft.ToString("R", CultureInfo.InvariantCulture), delegate(string c, PetSave p, Action warn) { if (c.Length != 0) { double num = ParseDouble(c, warn, "special cooldown seconds"); if (num < 0.0) { warn?.Invoke("negative special cooldown seconds '" + c + "' — clamped to 0 (ready)."); num = 0.0; } p.SpecialCooldownSecondsLeft = num; } }), new SaveColumn(11, 7, (PetSave p, Action warn) => (p.RelicStacks <= 0) ? "" : p.RelicStacks.ToString(CultureInfo.InvariantCulture), delegate(string c, PetSave p, Action warn) { if (c.Length != 0) { int num = ParseInt(c, warn, "relic stacks"); int num2 = SpeciesRelics.ClampStacks(num); if (num2 != num) { warn?.Invoke($"relic stacks {num} out of range — clamped to {num2}."); } p.RelicStacks = num2; } }), new SaveColumn(12, 8, (PetSave p, Action warn) => RegionCrossings.Flatten(p.CrossedRegionPairs), delegate(string c, PetSave p, Action warn) { RegionCrossings.Parse(c, p.CrossedRegionPairs, warn); }), TimedSlot(13, 9, "buff food", (PetSave p) => p.BuffFoodKey, (PetSave p) => p.BuffFoodSecondsLeft, delegate(PetSave p, string key, double secs, Action warn) { p.BuffFoodKey = key; p.BuffFoodSecondsLeft = secs; }), new SaveColumn(14, 10, (PetSave p, Action warn) => (!(p.HealthFraction < 1.0)) ? "" : Num(p.HealthFraction, warn, "health fraction"), delegate(string c, PetSave p, Action warn) { if (c.Length != 0) { double num = ParseDouble(c, warn, "health fraction"); double num2 = ((num < 0.0) ? 0.0 : ((num > 1.0) ? 1.0 : num)); if (num2 != num) { warn?.Invoke($"health fraction {num} out of range — clamped to {num2}."); } p.HealthFraction = num2; } }), TimedSlot(15, 11, "health recovery", (PetSave p) => (p.HealthRecoveryLevel <= 0) ? "" : p.HealthRecoveryLevel.ToString(CultureInfo.InvariantCulture), (PetSave p) => p.HealthRecoverySecondsLeft, delegate(PetSave p, string key, double secs, Action warn) { if (!int.TryParse(key, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || result <= 0) { warn?.Invoke("unreadable health recovery level '" + key + "' — buff dropped."); } else { int num = HealthRecovery.ClampLevel(result); if (num != result) { warn?.Invoke($"health recovery level {result} out of range — clamped to {num}."); } p.HealthRecoveryLevel = num; p.HealthRecoverySecondsLeft = secs; } }), new SaveColumn(16, 12, (PetSave p, Action warn) => (!(p.LoyaltyGainCarry > 0.0)) ? "" : Num(p.LoyaltyGainCarry, warn, "loyalty gain carry"), delegate(string c, PetSave p, Action warn) { if (c.Length != 0) { double num = ParseDouble(c, warn, "loyalty gain carry"); double num2 = ((num < 0.0) ? 0.0 : ((num >= 1.0) ? 0.0 : num)); if (num2 != num) { warn?.Invoke($"loyalty gain carry {num} out of range [0,1) — reset to 0."); } p.LoyaltyGainCarry = num2; } }) }; } private static SaveColumn TimedSlot(int index, int since, string what, Func key, Func secondsLeft, Action> store) { return new SaveColumn(index, since, (PetSave p, Action warn) => FlattenBuff(key(p), secondsLeft(p)), delegate(string c, PetSave p, Action warn) { if (ParseBuff(c, warn, what, out string key2, out double secs)) { store(p, key2, secs, warn); } }); } internal static string San(string s) { if (!string.IsNullOrEmpty(s)) { return s.Replace('\t', ' ').Replace('\n', ' ').Replace('\r', ' '); } return s; } internal static string Num(double v, Action warn, string field) { if (double.IsNaN(v) || double.IsInfinity(v)) { warn?.Invoke($"non-finite {field} '{v}' — writing 0."); return "0"; } return v.ToString("R", CultureInfo.InvariantCulture); } internal static string FlattenBuff(string key, double secondsLeft) { if (!string.IsNullOrEmpty(key) && !(secondsLeft <= 0.0) && !double.IsNaN(secondsLeft) && !double.IsInfinity(secondsLeft)) { return San(key) + ":" + secondsLeft.ToString("R", CultureInfo.InvariantCulture); } return ""; } internal static bool ParseBuff(string s, Action warn, string what, out string key, out double secs) { key = ""; secs = 0.0; if (string.IsNullOrEmpty(s)) { return false; } int num = s.LastIndexOf(':'); if (num <= 0) { warn?.Invoke("malformed " + what + " cell '" + s + "' (want key:secondsLeft) — buff dropped."); return false; } secs = ParseDouble(s.Substring(num + 1), warn, what + " seconds"); if (secs <= 0.0) { return false; } key = s.Substring(0, num); return true; } internal static string FlattenMeals(PetSave p) { if (p.RecentMeals != null && p.RecentMeals.Count != 0) { return string.Join("|", p.RecentMeals.Select(San)); } return ""; } internal static void ParseMeals(string s, PetSave pet) { if (string.IsNullOrEmpty(s)) { return; } string[] array = s.Split(new char[1] { '|' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length > 0) { FoodHexes.RecordMeal(pet.RecentMeals, text); } } } internal static int ParseInt(string s, Action warn = null, string field = null) { if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return result; } warn?.Invoke("unreadable " + (field ?? "integer") + " '" + s + "' — using 0."); return 0; } internal static double ParseDouble(string s, Action warn = null, string field = null) { if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return result; } warn?.Invoke("unreadable " + (field ?? "number") + " '" + s + "' — using 0."); return 0.0; } } public readonly struct PetSaveMark { public readonly int Loyalty; public readonly string BlanketKey; public readonly string DrinkKey; public readonly string BuffFoodKey; public readonly string HealthRecoveryKey; public readonly double SpecialCooldown; public PetSaveMark(int loyalty, string blanketKey, string drinkKey, string buffFoodKey, string healthRecoveryKey, double specialCooldown) { Loyalty = loyalty; BlanketKey = blanketKey ?? ""; DrinkKey = drinkKey ?? ""; BuffFoodKey = buffFoodKey ?? ""; HealthRecoveryKey = healthRecoveryKey ?? ""; SpecialCooldown = specialCooldown; } } public static class SavePolicy { public static bool ShouldSave(in PetSaveMark current, in PetSaveMark lastSaved, double secondsSinceLastSave, double heartbeatSeconds) { if (current.Loyalty != lastSaved.Loyalty) { return true; } (string, string)[] array = new(string, string)[4] { (current.BlanketKey, lastSaved.BlanketKey), (current.DrinkKey, lastSaved.DrinkKey), (current.BuffFoodKey, lastSaved.BuffFoodKey), (current.HealthRecoveryKey, lastSaved.HealthRecoveryKey) }; for (int i = 0; i < array.Length; i++) { var (a, b) = array[i]; if (!string.Equals(a, b, StringComparison.Ordinal)) { return true; } } if (current.SpecialCooldown > 0.0 || lastSaved.SpecialCooldown > 0.0) { return true; } return secondsSinceLastSave >= heartbeatSeconds; } } public sealed class SigilDef { public string Key; public List ItemCandidates = new List(); public int? ItemId; public float Radius = 1f; } public sealed class SigilHex { public string HexId; public float Percent; } public sealed class SigilSynergyDef { public string SigilKey; public string Trigger = "specialHit"; public string Kind = "hexBuildup"; public string Mode = "add"; public List Hexes = new List(); } public sealed class SigilSpeciesEntry { public string Species; public Dictionary BySigil = new Dictionary(StringComparer.OrdinalIgnoreCase); } public sealed class SigilSynergyTable { public Dictionary Sigils = new Dictionary(StringComparer.OrdinalIgnoreCase); public Dictionary Species = new Dictionary(StringComparer.OrdinalIgnoreCase); } public static class SigilSynergies { public const float DefaultRadius = 1f; public const string RegistryKey = "sigils"; public const string TriggerSpecialHit = "specialHit"; public const string KindHexBuildup = "hexBuildup"; public const string ModeAdd = "add"; public const string ModeReplace = "replace"; public static SigilSynergyTable Parse(string json, Action warn = null) { SigilSynergyTable sigilSynergyTable = new SigilSynergyTable(); if (!JsonTable.TryReadRoot(json, warn, "species", "an object ('sigils' registry + species entries)", out Dictionary root)) { return sigilSynergyTable; } Dictionary dictionary = new Dictionary(StringComparer.Ordinal); foreach (KeyValuePair item in root) { if (string.Equals(item.Key, "sigils", StringComparison.OrdinalIgnoreCase)) { ParseRegistry(item.Value, sigilSynergyTable.Sigils, warn); } else { dictionary[item.Key] = item.Value; } } JsonTable.ReadInto(dictionary, sigilSynergyTable.Species, warn, "species", null, ParseSpecies); return sigilSynergyTable; } private static void ParseRegistry(object value, Dictionary sigils, Action warn) { if (!(value is Dictionary source)) { new FieldReader("'sigils'", warn).Say("value must be an object of sigil key → definition — registry skipped."); } else { JsonTable.ReadInto(source, sigils, warn, "sigil", "sigils", ParseSigilDef); } } private static SigilDef ParseSigilDef(string key, object value, Action warn) { string text = (key ?? "").Trim(); if (text.Length == 0) { new FieldReader("'sigils'", warn).Say("empty sigil key — entry skipped."); return null; } FieldReader f = new FieldReader("'sigils." + text + "'", warn); if (!(value is Dictionary dictionary)) { f.Say("value must be an object — sigil skipped."); return null; } SigilDef sigilDef = new SigilDef { Key = text }; foreach (KeyValuePair item in dictionary) { string text2 = item.Key.ToLowerInvariant(); if (!(text2 == "item")) { if (text2 == "radius") { if (item.Value is double num && num > 0.0) { sigilDef.Radius = (float)num; } else { f.Wrong("radius", "a number > 0", $"using {1f}"); } } else { f.Unknown(item.Key, "item, radius"); } } else { AddItemCandidates(f, item.Value, sigilDef); } } if (sigilDef.ItemCandidates.Count == 0) { f.Say("missing/empty required 'item' — sigil skipped."); return null; } return sigilDef; } private static void AddItemCandidates(FieldReader f, object value, SigilDef def) { if (value is List list) { { foreach (object item in list) { AddItemCandidate(f, item, def); } return; } } AddItemCandidate(f, value, def); } private static void AddItemCandidate(FieldReader f, object value, SigilDef def) { string text = ((value is double num) ? num.ToString("0", CultureInfo.InvariantCulture) : (value as string)?.Trim()); if (string.IsNullOrEmpty(text)) { f.Say("'item' candidates must be non-empty strings or numbers — one skipped."); return; } def.ItemCandidates.Add(text); if (!def.ItemId.HasValue && int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { def.ItemId = result; } } private static SigilSpeciesEntry ParseSpecies(string species, object value, Action warn) { FieldReader fSpecies = new FieldReader("'" + species + "'", warn); if (!(value is Dictionary source)) { fSpecies.Say("value must be an object of sigil key → synergy — species skipped."); return null; } SigilSpeciesEntry sigilSpeciesEntry = new SigilSpeciesEntry { Species = species }; JsonTable.ReadInto(source, sigilSpeciesEntry.BySigil, warn, "sigil", species, (string sigilKey, object v, Action w) => ParseSynergy(fSpecies, species, sigilKey, v)); if (sigilSpeciesEntry.BySigil.Count == 0) { fSpecies.Say("no usable synergies — species skipped."); return null; } return sigilSpeciesEntry; } private static SigilSynergyDef ParseSynergy(FieldReader fSpecies, string species, string sigilKey, object value) { if (string.IsNullOrEmpty((sigilKey ?? "").Trim())) { fSpecies.Say("empty sigil key — synergy skipped."); return null; } FieldReader fieldReader = new FieldReader("'" + species + "." + sigilKey + "'", fSpecies.Sink); if (!(value is Dictionary dictionary)) { fieldReader.Say("value must be an object — synergy skipped."); return null; } SigilSynergyDef sigilSynergyDef = new SigilSynergyDef { SigilKey = sigilKey.Trim() }; foreach (KeyValuePair item in dictionary) { switch (item.Key.ToLowerInvariant()) { case "trigger": if (item.Value is string text && string.Equals(text.Trim(), "specialHit", StringComparison.OrdinalIgnoreCase)) { sigilSynergyDef.Trigger = "specialHit"; break; } fieldReader.Say(string.Format("unknown trigger '{0}' — synergy skipped (this build supports: {1}).", item.Value, "specialHit")); return null; case "kind": if (item.Value is string text3 && string.Equals(text3.Trim(), "hexBuildup", StringComparison.OrdinalIgnoreCase)) { sigilSynergyDef.Kind = "hexBuildup"; break; } fieldReader.Say(string.Format("unknown kind '{0}' — synergy skipped (this build supports: {1}).", item.Value, "hexBuildup")); return null; case "mode": if (item.Value is string text2 && (string.Equals(text2.Trim(), "add", StringComparison.OrdinalIgnoreCase) || string.Equals(text2.Trim(), "replace", StringComparison.OrdinalIgnoreCase))) { sigilSynergyDef.Mode = text2.Trim().ToLowerInvariant(); break; } fieldReader.Say(string.Format("unknown mode '{0}' — synergy skipped (valid: {1}, {2}).", item.Value, "add", "replace")); return null; case "hexes": { if (!fieldReader.Obj("hexes", item.Value, out Dictionary result, "an OBJECT of hex-name → percent pairs")) { break; } foreach (KeyValuePair item2 in result) { if (string.IsNullOrEmpty((item2.Key ?? "").Trim())) { fieldReader.Say("empty hex name — dropped."); } else if (item2.Value is double num && num > 0.0) { sigilSynergyDef.Hexes.Add(new SigilHex { HexId = item2.Key.Trim(), Percent = (float)num }); } else { fieldReader.Say("hex '" + item2.Key + "' must map to a number > 0 — dropped."); } } break; } default: fieldReader.Unknown(item.Key, "trigger, kind, mode, hexes"); break; } } if (sigilSynergyDef.Hexes.Count == 0) { fieldReader.Say("missing/empty required 'hexes' — synergy skipped."); return null; } return sigilSynergyDef; } public static SigilSynergyTable Merge(SigilSynergyTable builtIn, SigilSynergyTable overrides) { return new SigilSynergyTable { Sigils = SpeciesTable.Merge(builtIn?.Sigils, overrides?.Sigils), Species = SpeciesTable.Merge(builtIn?.Species, overrides?.Species) }; } public static SigilSpeciesEntry Resolve(SigilSynergyTable table, string speciesId) { SigilSpeciesEntry result = default(SigilSpeciesEntry); if (!SpeciesTable.TryResolve(table?.Species, speciesId, ref result, (string)null)) { return null; } return result; } public static void Audit(SigilSynergyTable table, Action warn) { if (table == null) { return; } foreach (SigilSpeciesEntry value in table.Species.Values) { foreach (string key in value.BySigil.Keys) { if (!table.Sigils.ContainsKey(key)) { warn?.Invoke("'" + value.Species + "." + key + "': no such sigil in the 'sigils' registry — that synergy can never fire."); } } } } public static List ActiveSynergies(SigilSpeciesEntry entry, IReadOnlyList activeSigilKeys, string trigger) { List list = new List(); if (entry == null || activeSigilKeys == null || activeSigilKeys.Count == 0) { return list; } HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (string activeSigilKey in activeSigilKeys) { if (!string.IsNullOrEmpty(activeSigilKey) && hashSet.Add(activeSigilKey) && entry.BySigil.TryGetValue(activeSigilKey, out SigilSynergyDef value) && string.Equals(value.Trigger, trigger, StringComparison.OrdinalIgnoreCase)) { list.Add(value); } } return list; } public static List ComposeBuildups(List foodHexes, IReadOnlyList synergies) { if (synergies == null || synergies.Count == 0) { return foodHexes ?? new List(); } bool flag = false; foreach (SigilSynergyDef synergy in synergies) { if (string.Equals(synergy.Mode, "replace", StringComparison.OrdinalIgnoreCase)) { flag = true; break; } } List list = new List(); Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); if (!flag && foodHexes != null) { foreach (HexBuildUp foodHex in foodHexes) { dictionary[foodHex.HexId] = list.Count; list.Add(foodHex); } } foreach (SigilSynergyDef synergy2 in synergies) { if (!string.Equals(synergy2.Kind, "hexBuildup", StringComparison.OrdinalIgnoreCase)) { continue; } foreach (SigilHex hex in synergy2.Hexes) { if (dictionary.TryGetValue(hex.HexId, out var value)) { HexBuildUp value2 = list[value]; value2.Percent += hex.Percent; list[value] = value2; } else { dictionary[hex.HexId] = list.Count; list.Add(new HexBuildUp { HexId = hex.HexId, Percent = hex.Percent, MealCount = 0 }); } } } return list; } } public sealed class SkillEchoDef { public string StatusName; public double? BuildupPercent; public double? DamageMult; public double? ImpactMult; } public sealed class SkillEchoEntry { public string Species; public Dictionary BySkill = new Dictionary(StringComparer.OrdinalIgnoreCase); } public static class SkillEchoes { public static readonly (string Name, int ItemId)[] Roster = new(string, int)[10] { ("Evasion Shot", 8100100), ("Mace Infusion", 8100270), ("Puncture", 8100290), ("Execution", 8100300), ("Juggernaut", 8100310), ("Moon Swipe", 8100320), ("Simeon's Gambit", 8100340), ("Pommel Counter", 8100362), ("Talus Cleaver", 8100380), ("Prismatic Flurry", 8201040) }; public const double MinBuildup = 1.0; public const double MaxBuildup = 100.0; public const double MinMult = 0.0; public const double MaxMult = 10.0; private static readonly HashSet _rosterIds = BuildIds(); private static readonly Dictionary _idByName = BuildNames(); private static HashSet BuildIds() { HashSet hashSet = new HashSet(); (string, int)[] roster = Roster; for (int i = 0; i < roster.Length; i++) { int item = roster[i].Item2; hashSet.Add(item); } return hashSet; } private static Dictionary BuildNames() { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); (string, int)[] roster = Roster; for (int i = 0; i < roster.Length; i++) { (string, int) tuple = roster[i]; string item = tuple.Item1; int item2 = tuple.Item2; dictionary[item] = item2; } return dictionary; } public static bool IsRosterSkill(int itemId) { return _rosterIds.Contains(itemId); } public static bool TryResolveName(string name, out int itemId, out string canonical) { itemId = 0; canonical = null; if (string.IsNullOrEmpty(name)) { return false; } string key = name.Trim(); if (!_idByName.TryGetValue(key, out itemId)) { return false; } (string, int)[] roster = Roster; for (int i = 0; i < roster.Length; i++) { (string, int) tuple = roster[i]; var (text, _) = tuple; if (tuple.Item2 == itemId) { canonical = text; break; } } return true; } public static string NameOf(int itemId) { (string, int)[] roster = Roster; for (int i = 0; i < roster.Length; i++) { (string, int) tuple = roster[i]; var (result, _) = tuple; if (tuple.Item2 == itemId) { return result; } } return null; } public static Dictionary Parse(string json, Action warn = null) { return JsonTable.Read(json, warn, "species", "an object of species → skill-echo objects", ParseEntry); } private static SkillEchoEntry ParseEntry(string speciesKey, object value, Action warn) { FieldReader fSpecies = new FieldReader("'" + speciesKey + "'", warn); if (!(value is Dictionary dictionary)) { fSpecies.Say("value must be an object of skill name → echo objects — species skipped."); return null; } SkillEchoEntry skillEchoEntry = new SkillEchoEntry { Species = speciesKey }; foreach (KeyValuePair item in dictionary) { if (!TryResolveName(item.Key, out int _, out string canonical)) { fSpecies.Say("'" + item.Key + "' is not a roster skill — skipped (valid: " + RosterNames() + ")."); continue; } SkillEchoDef skillEchoDef = ParseDef(fSpecies, speciesKey, canonical, item.Value); if (skillEchoDef != null) { if (skillEchoEntry.BySkill.ContainsKey(canonical)) { fSpecies.Say("duplicate skill '" + canonical + "' — the later entry wins."); } skillEchoEntry.BySkill[canonical] = skillEchoDef; } } if (skillEchoEntry.BySkill.Count == 0) { fSpecies.Say("no valid skill entries — species skipped (the code defaults already echo every roster skill)."); return null; } return skillEchoEntry; } private static SkillEchoDef ParseDef(FieldReader fSpecies, string speciesKey, string skillName, object value) { if (!(value is Dictionary dictionary)) { fSpecies.Say("'" + skillName + "' value must be an object — skill skipped."); return null; } FieldReader f = new FieldReader("'" + speciesKey + "' " + skillName, fSpecies.Sink); SkillEchoDef skillEchoDef = new SkillEchoDef(); foreach (KeyValuePair item in dictionary) { switch (item.Key.ToLowerInvariant()) { case "status": { if (f.Str("status", item.Value, out string result2, "a non-empty status name string")) { skillEchoDef.StatusName = result2; } break; } case "buildup": { if (f.Num("buildup", item.Value, out var result, null, "ignored (direct apply)")) { if (result < 1.0) { f.Say($"'buildup' {result} below {1.0} — clamped."); result = 1.0; } if (result > 100.0) { f.Say($"'buildup' {result} above {100.0} — clamped."); result = 100.0; } skillEchoDef.BuildupPercent = result; } break; } case "damage": skillEchoDef.DamageMult = ParseMult(f, "damage", item.Value); break; case "impact": skillEchoDef.ImpactMult = ParseMult(f, "impact", item.Value); break; default: f.Unknown(item.Key, "status, buildup, damage, impact"); break; } } if (skillEchoDef.BuildupPercent.HasValue && skillEchoDef.StatusName == null) { f.Say("'buildup' without a 'status' — ignored."); skillEchoDef.BuildupPercent = null; } if (skillEchoDef.StatusName == null && !skillEchoDef.DamageMult.HasValue && !skillEchoDef.ImpactMult.HasValue) { f.Say("empty override — kept, but the code defaults already cover every roster skill."); } return skillEchoDef; } private static double? ParseMult(FieldReader f, string field, object value) { if (!f.Num(field, value, out var result, null, "ignored (config default)")) { return null; } if (result < 0.0) { f.Say($"'{field}' {result} below {0.0} — clamped."); result = 0.0; } if (result > 10.0) { f.Say($"'{field}' {result} above {10.0} — clamped."); result = 10.0; } return result; } public static string RosterNames() { string[] array = new string[Roster.Length]; for (int i = 0; i < Roster.Length; i++) { array[i] = Roster[i].Name; } return string.Join(", ", array); } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } public static SkillEchoEntry Resolve(Dictionary table, string speciesId) { SkillEchoEntry result = default(SkillEchoEntry); if (!SpeciesTable.TryResolve(table, speciesId, ref result, (string)null)) { return null; } return result; } public static SkillEchoDef ForSkill(SkillEchoEntry entry, string canonicalName) { if (entry == null || canonicalName == null) { return null; } if (!entry.BySkill.TryGetValue(canonicalName, out SkillEchoDef value)) { return null; } return value; } public static double EffectiveDamageMult(SkillEchoDef def, double configDefault) { return Math.Max(0.0, def?.DamageMult ?? configDefault); } public static double EffectiveImpactMult(SkillEchoDef def, double configDefault) { return Math.Max(0.0, def?.ImpactMult ?? configDefault); } } public enum AttackKind { Melee, Ranged, Brace } public enum RangedRoute { Melee, Ranged, MeleeFallbackNoRig, MeleeFallbackDisabled } public enum BraceRoute { NotBrace, Brace, MeleeFallbackNoAnchor, MeleeFallbackDisabled } public sealed class SpecialAttackDef { public string TriggerParam; public float WindupSeconds; public string StatusEffectId; public float DamageMultiplier; public float CooldownSeconds; public float BuildupPercent; public AttackKind Kind; public string ProjectileFilter; public int ProjectileSkillId; public float TauntMinSeconds; public float TauntMaxSeconds; } public static class SpecialAttackTable { public static Dictionary Parse(string text, Action warn = null) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Unknown result type (might be due to invalid IL or missing references) //IL_0153: Unknown result type (might be due to invalid IL or missing references) //IL_0168: Unknown result type (might be due to invalid IL or missing references) //IL_0290: Unknown result type (might be due to invalid IL or missing references) //IL_0190: Unknown result type (might be due to invalid IL or missing references) //IL_01e9: Unknown result type (might be due to invalid IL or missing references) //IL_0263: Unknown result type (might be due to invalid IL or missing references) Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (TableLine item in SpeciesTable.Lines(text, '\n')) { string text2 = item.Fields[0].Trim(); if (text2.Length != 0 && TryParseKind(item.Fields, 5, AttackKind.Melee, warn, item.Key, out var kind)) { SpecialAttackDef specialAttackDef = new SpecialAttackDef { TriggerParam = text2, WindupSeconds = SpeciesTable.FloatOr(item.Fields, 1, 0.4f, warn, item.Key), StatusEffectId = ParseStringOr(item.Fields, 2, null), DamageMultiplier = SpeciesTable.FloatOr(item.Fields, 3, 1.5f, warn, item.Key), CooldownSeconds = SpeciesTable.FloatOr(item.Fields, 4, 8f, warn, item.Key), Kind = kind, ProjectileFilter = ParseStringOr(item.Fields, 6, null), ProjectileSkillId = SpeciesTable.IntOr(item.Fields, 7, 0, warn, item.Key), BuildupPercent = SpeciesTable.FloatOr(item.Fields, 8, 0f, warn, item.Key), TauntMinSeconds = SpeciesTable.FloatOr(item.Fields, 9, 0f, warn, item.Key), TauntMaxSeconds = SpeciesTable.FloatOr(item.Fields, 10, 0f, warn, item.Key) }; NormalizeTaunt(specialAttackDef, warn, item.Key); if (specialAttackDef.ProjectileSkillId > 0 && specialAttackDef.Kind != AttackKind.Ranged) { warn?.Invoke($"'{item.Key}': ProjectileSkillId {specialAttackDef.ProjectileSkillId} is set but Kind={specialAttackDef.Kind} — " + "the skill-prefab capture fallback only applies to Ranged rows, so the field is DEAD. Add Ranged in field 6 or remove field 8."); } if (specialAttackDef.ProjectileFilter != null && specialAttackDef.Kind == AttackKind.Brace) { warn?.Invoke("'" + item.Key + "': ProjectileFilter '" + specialAttackDef.ProjectileFilter + "' is set but Kind=Brace — a braced stance fires no projectile, so the field is DEAD."); } if (specialAttackDef.BuildupPercent != 0f && (specialAttackDef.BuildupPercent < 1f || specialAttackDef.BuildupPercent > 100f)) { float num = ((specialAttackDef.BuildupPercent < 1f) ? 1f : 100f); warn?.Invoke($"'{item.Key}': buildup {specialAttackDef.BuildupPercent} outside 1..100 — clamped to {num}."); specialAttackDef.BuildupPercent = num; } dictionary[item.Key] = specialAttackDef; } } return dictionary; } private static void NormalizeTaunt(SpecialAttackDef def, Action warn, string key) { if (def.TauntMinSeconds < 0f || def.TauntMaxSeconds < 0f) { warn?.Invoke($"'{key}': negative taunt seconds ({def.TauntMinSeconds}..{def.TauntMaxSeconds}) — " + "taunt axis dropped (a negative window is a typo, never a duration)."); def.TauntMinSeconds = (def.TauntMaxSeconds = 0f); return; } if (def.TauntMinSeconds > 0f && def.TauntMaxSeconds == 0f) { def.TauntMaxSeconds = def.TauntMinSeconds; } if ((!(def.TauntMaxSeconds > 0f) || def.TauntMinSeconds != 0f) && def.TauntMaxSeconds < def.TauntMinSeconds) { warn?.Invoke($"'{key}': taunt max {def.TauntMaxSeconds}s is below min {def.TauntMinSeconds}s — " + "raised to the min (loyalty must never SHORTEN the taunt)."); def.TauntMaxSeconds = def.TauntMinSeconds; } } public static float TauntSeconds(SpecialAttackDef def, int loyalty) { if (def == null) { return 0f; } if (def.TauntMinSeconds <= 0f && def.TauntMaxSeconds <= 0f) { return 0f; } return (float)PetGifts.Lerp(def.TauntMinSeconds, def.TauntMaxSeconds, loyalty); } private static string ParseStringOr(string[] fields, int index, string fallback) { if (index >= fields.Length) { return fallback; } string text = fields[index].Trim(); if (text.Length != 0) { return text; } return fallback; } private static bool TryParseKind(string[] fields, int index, AttackKind fallback, Action warn, string key, out AttackKind kind) { kind = fallback; if (index >= fields.Length) { return true; } string text = fields[index].Trim(); if (text.Length == 0) { return true; } if (text.Equals("Melee", StringComparison.OrdinalIgnoreCase)) { kind = AttackKind.Melee; return true; } if (text.Equals("Ranged", StringComparison.OrdinalIgnoreCase)) { kind = AttackKind.Ranged; return true; } if (text.Equals("Brace", StringComparison.OrdinalIgnoreCase)) { kind = AttackKind.Brace; return true; } warn?.Invoke("'" + key + "': unknown attack kind '" + text + "' (expected Melee, Ranged or Brace) — row skipped (a half-applied signature attack is worse than an absent one)."); return false; } public static string[] SplitStatuses(string statusField) { if (string.IsNullOrEmpty(statusField)) { return Array.Empty(); } string[] array = statusField.Split(new char[1] { '|' }); List list = new List(array.Length); string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string text = array2[i].Trim(); if (text.Length > 0) { list.Add(text); } } return list.ToArray(); } public static string PickStatus(string[] statuses, double roll) { if (statuses == null || statuses.Length == 0) { return null; } if (roll < 0.0) { roll = 0.0; } if (roll >= 1.0) { roll = 0.999999; } int num = (int)(roll * (double)statuses.Length); if (num >= statuses.Length) { num = statuses.Length - 1; } return statuses[num]; } public static RangedRoute Route(AttackKind kind, bool rangedEnabled, bool rigReady) { if (kind != AttackKind.Ranged) { return RangedRoute.Melee; } if (!rangedEnabled) { return RangedRoute.MeleeFallbackDisabled; } if (!rigReady) { return RangedRoute.MeleeFallbackNoRig; } return RangedRoute.Ranged; } public static BraceRoute RouteBrace(AttackKind kind, bool braceEnabled, bool anchorLive) { if (kind != AttackKind.Brace) { return BraceRoute.NotBrace; } if (!braceEnabled) { return BraceRoute.MeleeFallbackDisabled; } if (!anchorLive) { return BraceRoute.MeleeFallbackNoAnchor; } return BraceRoute.Brace; } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } public static bool TryGet(Dictionary table, string speciesId, out SpecialAttackDef def) { return SpeciesTable.TryResolve(table, speciesId, ref def, (string)null); } } public static class SpeciesAudit { public sealed class Tables { public IEnumerable Tamable; public IEnumerable Diet; public IEnumerable Donor; public IEnumerable Comfort; public IEnumerable Buffs; public IEnumerable SpecialAttacks; public IEnumerable FoodHex; public IEnumerable Gifts; public IEnumerable Senses; public IEnumerable Scavenge; public IEnumerable LoyaltyGrowth; public IEnumerable BuffFoods; public IEnumerable ForTheKill; public IEnumerable SkillEchoes; } public sealed class Finding { public bool IsWarning; public string Message; public Finding(bool isWarning, string message) { IsWarning = isWarning; Message = message; } } private sealed class OptionalAxis { public readonly string Table; public readonly List Keys; public readonly string Consequence; public OptionalAxis(string table, List keys, string consequence) { Table = table; Keys = keys; Consequence = consequence; } } public static List Run(Tables t) { List list = new List(); if (t == null || t.Tamable == null) { return list; } List keys = ToList(t.Diet); List keys2 = ToList(t.Donor); List keys3 = ToList(t.Comfort); List keys4 = ToList(t.Buffs); List keys5 = ToList(t.SpecialAttacks); List keys6 = ToList(t.FoodHex); bool hasDefault = HasDefaultRow(keys3); List list2 = new List(); AddIfSupplied(list2, "PetGifts", t.Gifts, "no Pet Gift drop table"); AddIfSupplied(list2, "PetSenses", t.Senses, "no scent sense"); AddIfSupplied(list2, "PetScavenge", t.Scavenge, "no scavenge bonus"); AddIfSupplied(list2, "SpeciesGrowth", t.LoyaltyGrowth, "no loyalty stat growth"); AddIfSupplied(list2, "BuffFoods", t.BuffFoods, "no feed-buff foods"); AddIfSupplied(list2, "ForTheKill", t.ForTheKill, "no For-the-Kill debuff or kill favor"); AddIfSupplied(list2, "SkillEchoes", t.SkillEchoes, "no skill echoes"); foreach (string item in t.Tamable) { if (string.IsNullOrEmpty(item)) { continue; } if (!Covered(item, keys)) { list.Add(new Finding(isWarning: true, "'" + item + "' is tamable but has no PetDiets entry — it can never be fed (the taming loop dead-ends: hunger only rises).")); } if (!Covered(item, keys2)) { list.Add(new Finding(isWarning: true, "'" + item + "' is tamable but has no DonorScenes entry — if its body is lost, re-form can't harvest a replacement.")); } if (!CoveredByComfort(item, keys3, hasDefault)) { list.Add(new Finding(isWarning: true, "'" + item + "' is tamable but has no SpeciesComfort row and the table has no Default row — comfort falls back to the built-in temperate band.")); } if (!Covered(item, keys4)) { list.Add(new Finding(isWarning: false, "'" + item + "' has no SpeciesBuffs entry — no passive bond buff (optional).")); } if (!Covered(item, keys5)) { list.Add(new Finding(isWarning: false, "'" + item + "' has no SpeciesSpecialAttacks entry — no Hunt-as-One signature attack (optional).")); } if (!Covered(item, keys6)) { list.Add(new Finding(isWarning: false, "'" + item + "' has no FoodHexes entry — no food-driven hex build-up (optional).")); } foreach (OptionalAxis item2 in list2) { if (!Covered(item, item2.Keys)) { list.Add(new Finding(isWarning: false, "'" + item + "' has no " + item2.Table + " entry — " + item2.Consequence + " (optional).")); } } } return list; } public static List UnknownKeys(string tableName, IEnumerable keys, IEnumerable knownSpecies, string reservedKey = null) { List list = new List(); List list2 = ToList(knownSpecies); if (keys == null || list2.Count == 0) { return list; } foreach (string key in keys) { if (string.IsNullOrEmpty(key) || (reservedKey != null && key.Equals(reservedKey, StringComparison.OrdinalIgnoreCase))) { continue; } bool flag = false; foreach (string item in list2) { if (!string.IsNullOrEmpty(item) && (Species.NameMatches(item, key) || Species.NameMatches(key, item))) { flag = true; break; } } if (!flag) { list.Add(new Finding(isWarning: true, tableName + " key '" + key + "' matches no known species (taming/donor tables) — the row can never apply (typo?).")); } } return list; } private static bool Covered(string species, List keys) { foreach (string key in keys) { if (!string.IsNullOrEmpty(key) && Species.NameMatches(species, key)) { return true; } } return false; } private static bool CoveredByComfort(string species, List keys, bool hasDefault) { if (hasDefault) { return true; } foreach (string key in keys) { if (!string.IsNullOrEmpty(key) && !IsDefaultRow(key) && Species.NameMatches(species, key)) { return true; } } return false; } private static bool HasDefaultRow(List keys) { foreach (string key in keys) { if (IsDefaultRow(key)) { return true; } } return false; } private static bool IsDefaultRow(string key) { return key?.Equals("Default", StringComparison.OrdinalIgnoreCase) ?? false; } private static void AddIfSupplied(List into, string table, IEnumerable keys, string consequence) { if (keys != null) { into.Add(new OptionalAxis(table, ToList(keys), consequence)); } } private static List ToList(IEnumerable keys) { if (keys == null) { return new List(); } return new List(keys); } } public sealed class RelicDef { public string Species; public int ItemId; public string ItemLabel; public string GrantPhrase; } public static class SpeciesRelics { public enum Ruling { NotThisRelic, Grant, AtCap } public const int MaxStacks = 5; public const float HpMultPerStack = 1.09f; public const float DamageMultPerStack = 1.054f; public const float ResistPointsPerStack = 5.4f; public const float ProtectionAllPerStack = 1.8f; public const float BarrierPerStack = 1.8f; public const float StatusResistPerStack = 1.8f; public static readonly RelicDef[] Roster = new RelicDef[4] { new RelicDef { Species = "Pearlbird", ItemId = 6600221, ItemLabel = "Pearlbird's Courage", GrantPhrase = "Courage swells" }, new RelicDef { Species = "Veaber", ItemId = 6600226, ItemLabel = "Leyline Figment", GrantPhrase = "Leyline light flickers in its eyes" }, new RelicDef { Species = "Hyena", ItemId = 6600230, ItemLabel = "Metalized Bones", GrantPhrase = "Iron settles into its bones" }, new RelicDef { Species = "Armored Hyena", ItemId = 6600230, ItemLabel = "Metalized Bones", GrantPhrase = "Iron settles into its plates" } }; public static Ruling RuleFeed(string speciesId, int itemId, int currentStacks) { if (Pairing(speciesId, itemId) == null) { return Ruling.NotThisRelic; } if (currentStacks < 5) { return Ruling.Grant; } return Ruling.AtCap; } private static RelicDef Pairing(string speciesId, int itemId) { if (string.IsNullOrEmpty(speciesId)) { return null; } RelicDef[] roster = Roster; foreach (RelicDef relicDef in roster) { if (relicDef.ItemId == itemId && string.Equals(speciesId, relicDef.Species, StringComparison.OrdinalIgnoreCase)) { return relicDef; } } return null; } public static RelicDef For(string speciesId) { if (string.IsNullOrEmpty(speciesId)) { return null; } RelicDef[] roster = Roster; foreach (RelicDef relicDef in roster) { if (string.Equals(speciesId, relicDef.Species, StringComparison.OrdinalIgnoreCase)) { return relicDef; } } return null; } public static int ClampStacks(int stacks) { if (stacks >= 0) { if (stacks <= 5) { return stacks; } return 5; } return 0; } public static void Apply(CreatureAttributes eff, int stacks) { if (eff != null && stacks > 0) { stacks = ClampStacks(stacks); float num = Pow(1.09f, stacks); float num2 = Pow(1.054f, stacks); eff.MaxHealth *= num; eff.ProtectionAll += 1.8f * (float)stacks; eff.Barrier += 1.8f * (float)stacks; eff.StatusResistance = StatusResistPolicy.Cap(eff.StatusResistance + 1.8f * (float)stacks); for (int i = 0; i < 9; i++) { eff.Resist[i] = Clamp(eff.Resist[i] + 5.4f * (float)stacks, -100f, 100f); eff.Damage[i] *= num2; } } } public static string Describe(int stacks) { stacks = ClampStacks(stacks); if (stacks <= 0) { return $"0/{5} stacks"; } return string.Format(CultureInfo.InvariantCulture, "{0}/{1} stacks: HP x{2:F3}, dmg x{3:F3}, resist +{4:F0}, protAll +{5:F0}, barrier +{6:F0}, statusRes +{7:F0}", stacks, 5, Pow(1.09f, stacks), Pow(1.054f, stacks), 5.4f * (float)stacks, 1.8f * (float)stacks, 1.8f * (float)stacks, 1.8f * (float)stacks); } private static float Pow(float b, int n) { float num = 1f; for (int i = 0; i < n; i++) { num *= b; } return num; } private static float Clamp(float v, float lo, float hi) { if (!(v < lo)) { if (!(v > hi)) { return v; } return hi; } return lo; } } public enum StrikeHalf { Pet, Player } public enum AttemptStatus { Pending, Granted, Failed } public static class Synergy { public const string StatusIdentifier = "BW_Synergy"; public const int StatusNumId = 87061; public const float BlockAngleDegrees = 60f; public const float ShieldBlockAngleDegrees = 80f; public static StrikeOutcome Judge(in JudgeInputs a) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) return StrikeJudge.Judge(ref a); } public static double DamageMult(int stacks, double percentPerStack) { if (stacks <= 0 || percentPerStack <= 0.0) { return 1.0; } return 1.0 + (double)stacks * percentPerStack * 0.01; } public static int ClampStacks(int stacks, int maxStacks) { if (maxStacks <= 0 || stacks <= 0) { return 0; } if (stacks <= maxStacks) { return stacks; } return maxStacks; } public static double FtkMult(double baseMult, double perStackPercent, int stacksConsumed) { if (baseMult <= 0.0) { baseMult = 1.0; } if (stacksConsumed < 0) { stacksConsumed = 0; } double num = 1.0 + ((perStackPercent <= 0.0) ? 0.0 : (perStackPercent * 0.01)) * (double)stacksConsumed; return baseMult * num; } } public sealed class SynergyAttempt { public double OpenedAt; public double WindowSeconds; public StrikeOutcome Pet { get; private set; } public StrikeOutcome Player { get; private set; } public AttemptStatus Status { get; private set; } public string Verdict => "pet=" + ((object)Pet/*cast due to .constrained prefix*/).ToString() + " player=" + ((object)Player/*cast due to .constrained prefix*/).ToString(); public SynergyAttempt(double openedAt, double windowSeconds) { OpenedAt = openedAt; WindowSeconds = ((windowSeconds > 0.0) ? windowSeconds : 0.0); } public AttemptStatus Report(StrikeHalf half, StrikeOutcome outcome) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Invalid comparison between Unknown and I4 //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Invalid comparison between Unknown and I4 if (Status != AttemptStatus.Pending) { return Status; } if ((int)outcome == 0) { return Status; } if (half == StrikeHalf.Pet) { if ((int)Pet == 0) { Pet = outcome; } } else if ((int)Player == 0) { Player = outcome; } if ((int)Pet != 0 && (int)Player != 0) { Status = (((int)Pet == 1 && (int)Player == 1) ? AttemptStatus.Granted : AttemptStatus.Failed); } return Status; } public AttemptStatus Tick(double now) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Invalid comparison between Unknown and I4 //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Invalid comparison between Unknown and I4 if (Status != AttemptStatus.Pending) { return Status; } if (now - OpenedAt < WindowSeconds) { return Status; } if ((int)Pet == 0) { Pet = (StrikeOutcome)7; } if ((int)Player == 0) { Player = (StrikeOutcome)7; } Status = (((int)Pet == 1 && (int)Player == 1) ? AttemptStatus.Granted : AttemptStatus.Failed); return Status; } } public sealed class TamingItemKey { public string Key; public int? ItemId; public string Category; public override string ToString() { return Key; } } public sealed class TamingEntry { public string Species; public string ChowName; public string ChowDesc; public int? ChowId; public int? ScrollId; public TamingItemKey CloneFrom; public TamingItemKey ScrollCloneFrom; public string Station; public List Ingredients = new List(); public int ResultCount; public double DropChance; } public static class TamingFoods { public const int MaxIngredients = 4; public const int DefaultResultCount = 3; public const string DefaultStation = "Cooking"; public const string DefaultCloneFrom = "Travel Ration"; public static readonly string[] Stations = new string[4] { "Alchemy", "Cooking", "Survival", "Forge" }; private static readonly CultureInfo Inv = CultureInfo.InvariantCulture; public static Dictionary Parse(string json, Action warn = null) { return JsonTable.Read(json, warn, "species", "an object of species → taming-food objects", ParseEntry); } private static TamingEntry ParseEntry(string species, object value, Action warn) { FieldReader f = new FieldReader("'" + species + "'", warn); if (!(value is Dictionary dictionary)) { f.Say("value must be an object — species skipped."); return null; } TamingEntry tamingEntry = new TamingEntry { Species = species, ChowName = species + " Chow", ChowDesc = "A special dish irresistible to a wild " + species + ". Use it within reach of one to tame it.", CloneFrom = new TamingItemKey { Key = "Travel Ration" }, Station = "Cooking", ResultCount = 3, DropChance = 1.0 }; foreach (KeyValuePair item in dictionary) { switch (item.Key.ToLowerInvariant()) { case "chowname": { if (f.Str("chowName", item.Value, out string result6, null, "using the default")) { tamingEntry.ChowName = result6; } break; } case "chowdesc": { if (f.Str("chowDesc", item.Value, out string result2, null, "using the default")) { tamingEntry.ChowDesc = result2; } break; } case "chowid": { if (f.Int("chowId", item.Value, out var result4, null, "deriving instead")) { tamingEntry.ChowId = result4; } break; } case "scrollid": { if (f.Int("scrollId", item.Value, out var result7, null, "deriving instead")) { tamingEntry.ScrollId = result7; } break; } case "clonefrom": tamingEntry.CloneFrom = ReadItemKey(f, "cloneFrom", item.Value) ?? tamingEntry.CloneFrom; break; case "scrollclonefrom": tamingEntry.ScrollCloneFrom = ReadItemKey(f, "scrollCloneFrom", item.Value); break; case "station": if (item.Value is string text && IsStation(text)) { tamingEntry.Station = Canonical(text); } else { f.Say(string.Format("unknown station '{0}' (valid: {1}) — using {2}.", item.Value, string.Join(", ", Stations), "Cooking")); } break; case "ingredients": { if (!f.Items("ingredients", item.Value, out List result5, "an ARRAY of item names/ids/category objects")) { break; } foreach (object item2 in result5) { TamingItemKey tamingItemKey = ReadIngredient(f, item2); if (tamingItemKey != null) { tamingEntry.Ingredients.Add(tamingItemKey); } } break; } case "resultcount": { if (f.Num("resultCount", item.Value, out var result3, "a number >= 1", $"using {3}")) { if (result3 >= 1.0) { tamingEntry.ResultCount = (int)result3; } else { f.Wrong("resultCount", "a number >= 1", $"using {3}"); } } break; } case "dropchance": { if (f.Num("dropChance", item.Value, out var result, "a number 0..1", "using 1.0")) { tamingEntry.DropChance = Clamp01(result); } break; } default: f.Unknown(item.Key, "chowName, chowDesc, chowId, scrollId, cloneFrom, scrollCloneFrom, station, ingredients, resultCount, dropChance"); break; } } if (tamingEntry.Ingredients.Count == 0) { f.Say("missing/empty required 'ingredients' — species skipped."); return null; } if (tamingEntry.Ingredients.Count > 4) { f.Say($"{tamingEntry.Ingredients.Count} ingredients, but the game has only {4} crafting slots — species skipped."); return null; } return tamingEntry; } private static TamingItemKey ReadItemKey(FieldReader f, string field, object value) { ItemKey val = default(ItemKey); if (ItemKey.TryRead(value, ref val)) { return new TamingItemKey { Key = ((ItemKey)(ref val)).Key, ItemId = ((ItemKey)(ref val)).ItemId }; } f.Say("'" + field + "' entries must be a number (ItemID) or non-empty string (display name)."); return null; } private static TamingItemKey ReadIngredient(FieldReader f, object value) { if (!(value is Dictionary dictionary)) { return ReadItemKey(f, "ingredients", value); } string text = null; foreach (KeyValuePair item in dictionary) { if (string.Equals(item.Key, "category", StringComparison.OrdinalIgnoreCase)) { text = item.Value as string; } else { f.Say("ingredient object has unknown key '" + item.Key + "' (valid: category)."); } } if (text == null || text.Trim().Length == 0) { f.Say("ingredient object needs a non-empty 'category' (valid: " + FoodCategories.ValidList() + ") — entry skipped."); return null; } if (!FoodCategories.TryCanonical(text, out string canonical)) { canonical = text.Trim(); f.Say("unknown ingredient category '" + canonical + "' (valid: " + FoodCategories.ValidList() + ") — the species will be refused at registration."); } return new TamingItemKey { Key = FoodCategories.Label(canonical), Category = canonical }; } private static bool IsStation(string name) { string[] stations = Stations; for (int i = 0; i < stations.Length; i++) { if (string.Equals(stations[i], name, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static string Canonical(string station) { string[] stations = Stations; foreach (string text in stations) { if (string.Equals(text, station, StringComparison.OrdinalIgnoreCase)) { return text; } } return "Cooking"; } private static double Clamp01(double v) { if (!(v < 0.0)) { if (!(v > 1.0)) { return v; } return 1.0; } return 0.0; } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { return SpeciesTable.Merge(builtIn, overrides); } public static TamingEntry Resolve(Dictionary table, string speciesId) { TamingEntry result = default(TamingEntry); if (!SpeciesTable.TryResolve(table, speciesId, ref result, (string)null)) { return null; } return result; } public static TamingEntry ForChowId(Dictionary table, int itemId) { if (table == null) { return null; } foreach (KeyValuePair item in table) { if (TamingIds.Resolve(item.Value).chowId == itemId) { return item.Value; } } return null; } public static bool ShouldDrop(double perSpeciesChance, double globalChance, double roll01) { double num = Clamp01(perSpeciesChance) * Clamp01(globalChance); if (!(num >= 1.0)) { return roll01 < num; } return true; } public static IEnumerable FingerprintLines(IEnumerable entries) { if (entries == null) { yield break; } foreach (TamingEntry entry in entries) { if (entry == null) { continue; } (int chowId, int scrollId) tuple = TamingIds.Resolve(entry); int item = tuple.chowId; int item2 = tuple.scrollId; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("tf:").Append(Norm(entry.Species)).Append(";chow=") .Append(item.ToString(Inv)) .Append(";scroll=") .Append(item2.ToString(Inv)) .Append(";drop=") .Append(entry.DropChance.ToString("0.######", Inv)) .Append(";station=") .Append(Norm(entry.Station)) .Append(";count=") .Append(entry.ResultCount.ToString(Inv)) .Append(";clone=") .Append(KeyOf(entry.CloneFrom)) .Append(";scrollclone=") .Append(KeyOf(entry.ScrollCloneFrom)) .Append(";ing="); if (entry.Ingredients != null) { for (int i = 0; i < entry.Ingredients.Count; i++) { if (i > 0) { stringBuilder.Append('|'); } stringBuilder.Append(KeyOf(entry.Ingredients[i])); } } yield return stringBuilder.ToString(); } } public static string RegisteredIdLine(string species, int chowId, int scrollId) { return $"{species}={chowId}/{scrollId}"; } public static IEnumerable HandshakeLines(IEnumerable registeredIdLines, IEnumerable entries) { if (registeredIdLines != null) { foreach (string registeredIdLine in registeredIdLines) { if (registeredIdLine != null) { yield return registeredIdLine; } } } foreach (string item in FingerprintLines(entries)) { yield return item; } } private static string KeyOf(TamingItemKey k) { if (k == null) { return "-"; } if (k.Category != null) { return "cat:" + Norm(k.Category); } if (k.ItemId.HasValue) { return "id:" + k.ItemId.Value.ToString(Inv); } return "name:" + Norm(k.Key); } private static string Norm(string s) { return (s ?? string.Empty).Trim().ToLowerInvariant(); } public static string RecipeUid(string species) { return "beastwhispering.taming." + (species ?? string.Empty).Trim().ToLowerInvariant().Replace(' ', '-'); } } public static class TamingIds { public static string LedgerKey(string species, string part) { return "taming." + (species ?? string.Empty).Trim().ToLowerInvariant().Replace(' ', '-') + "." + part; } public static int LedgerId(string species, string part) { if (!BwIds.TryGet(LedgerKey(species, part), out var id)) { return 0; } return id; } public static (int chowId, int scrollId) Resolve(TamingEntry entry) { return (chowId: entry.ChowId ?? LedgerId(entry.Species, "chow"), scrollId: entry.ScrollId ?? LedgerId(entry.Species, "scroll")); } public static List Validate(IEnumerable entries, Action warn = null) { Dictionary dictionary = new Dictionary(); List list = new List(); foreach (TamingEntry entry in entries) { if (entry != null) { var (num, num2) = Resolve(entry); string value; if (num == 0 || num2 == 0) { warn?.Invoke("'" + entry.Species + "': no id in data/ids/ledger.json for its " + ((num == 0) ? "chow" : "recipe scroll") + " — species skipped. Run `bwspecies build`, which allocates a pair for any species with a tamingFood block, and commit the ledger."); } else if (num == num2) { warn?.Invoke($"'{entry.Species}': chowId and scrollId are both {num} — species skipped; two items can't share an ItemID."); } else if (dictionary.TryGetValue(num, out value) || dictionary.TryGetValue(num2, out value)) { warn?.Invoke($"'{entry.Species}': ItemID collision with '{value}' (chow {num}/scroll {num2}) — species skipped; give it explicit chowId/scrollId in TamingFoods.json."); } else { dictionary[num] = entry.Species; dictionary[num2] = entry.Species; list.Add(entry); } } } return list; } } public enum TempStep { Coldest, VeryCold, Cold, Fresh, Neutral, Warm, Hot, VeryHot, Hottest } public readonly struct ComfortBand { public TempStep Min { get; } public TempStep Max { get; } public ComfortBand(TempStep min, TempStep max) { Min = min; Max = max; } } public enum ComfortStage { Comfortable, Uneasy, Suffering, Critical } public static class Temperature { public static int StepsOutside(TempStep temp, ComfortBand band) { if (temp < band.Min) { return band.Min - temp; } if (temp > band.Max) { return temp - band.Max; } return 0; } public static ComfortStage Stage(int stepsOutside) { if (stepsOutside > 0) { return stepsOutside switch { 1 => ComfortStage.Uneasy, 2 => ComfortStage.Suffering, _ => ComfortStage.Critical, }; } return ComfortStage.Comfortable; } public static ComfortStage Stage(TempStep temp, ComfortBand band) { return Stage(StepsOutside(temp, band)); } } public static class UntameableSpecies { public static readonly string[] Names = new string[1] { "Pearlbird Cutthroat" }; public const string Reason = "it is on the untameable-species list"; public static bool IsBlocked(string creatureName) { if (string.IsNullOrEmpty(creatureName)) { return false; } string[] names = Names; foreach (string text in names) { if (Species.NameEquals(creatureName, text)) { return true; } } return false; } } public enum WardAction { None, Apply, Remove } public static class WardRules { public static WardAction Mirror(bool playerHas, bool anchorHas, bool weApplied) { if (playerHas && !anchorHas) { return WardAction.Apply; } if (!playerHas && anchorHas && weApplied) { return WardAction.Remove; } return WardAction.None; } public static bool GrantOnEdge(bool playerHas, bool playerHadLastPoll) { if (playerHas) { return !playerHadLastPoll; } return false; } public static IReadOnlyList ParseCandidates(string raw) { List list = new List(); if (string.IsNullOrEmpty(raw)) { return list; } string[] array = raw.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length > 0 && !list.Contains(text)) { list.Add(text); } } return list; } } public sealed class WeatherFoodDef { public string Key = ""; public int? ItemId; public int ColdSteps; public int HotSteps; public double EscalateMult = 3.0; public double DurationSeconds = 240.0; public bool Universal; public bool Immune; public bool FromOverride; public List ToReliefs() { List list = new List(3); if (Immune) { list.Add(new ComfortRelief { Side = ComfortSide.None, ReliefSteps = 0, EscalateMult = 1.0, Immune = true }); } if (ColdSteps > 0) { list.Add(new ComfortRelief { Side = ComfortSide.Cold, ReliefSteps = ColdSteps, EscalateMult = EscalateMult }); } if (HotSteps > 0) { list.Add(new ComfortRelief { Side = ComfortSide.Hot, ReliefSteps = HotSteps, EscalateMult = EscalateMult }); } return list; } } public enum DrinkRuling { None, WithMeal, DrinkOnly } public static class WeatherFoods { public static Dictionary Parse(string json, Action warn = null) { return JsonTable.Read(json, warn, "item", "an object of item → weather-relief objects", ParseEntry); } private static WeatherFoodDef ParseEntry(string itemKey, object value, Action warn) { string text = (itemKey ?? "").Trim(); if (text.Length == 0) { warn?.Invoke("empty item key — entry skipped."); return null; } FieldReader f = new FieldReader("'" + text + "'", warn); if (!(value is Dictionary dictionary)) { f.Say("value must be an object — entry skipped."); return null; } ItemKey val = default(ItemKey); ItemKey.TryRead((object)text, ref val); WeatherFoodDef weatherFoodDef = new WeatherFoodDef { Key = ((ItemKey)(ref val)).Key, ItemId = ((ItemKey)(ref val)).ItemId }; foreach (KeyValuePair item in dictionary) { switch (item.Key.ToLowerInvariant()) { case "cold": if (!ReadSteps(f, "cold", item.Value, out weatherFoodDef.ColdSteps)) { return null; } break; case "hot": if (!ReadSteps(f, "hot", item.Value, out weatherFoodDef.HotSteps)) { return null; } break; case "escalate": { if (!f.Num("escalate", item.Value, out var result4, "a number >= 1", "entry skipped")) { return null; } if (result4 < 1.0) { f.Wrong("escalate", "a number >= 1", "entry skipped"); return null; } weatherFoodDef.EscalateMult = result4; break; } case "duration": { if (!f.Num("duration", item.Value, out var result2, "a number > 0 (seconds)", "entry skipped")) { return null; } if (result2 <= 0.0) { f.Wrong("duration", "a number > 0 (seconds)", "entry skipped"); return null; } weatherFoodDef.DurationSeconds = result2; break; } case "universal": { if (!f.Bool("universal", item.Value, out var result3, "true/false", "entry skipped")) { return null; } weatherFoodDef.Universal = result3; break; } case "immune": { if (!f.Bool("immune", item.Value, out var result, "true/false", "entry skipped")) { return null; } weatherFoodDef.Immune = result; break; } default: f.Unknown(item.Key, "cold, hot, escalate, duration, universal, immune"); break; } } if (weatherFoodDef.ColdSteps == 0 && weatherFoodDef.HotSteps == 0 && !weatherFoodDef.Universal && !weatherFoodDef.Immune) { f.Say("no relief on either side and not universal — dead entry skipped."); return null; } return weatherFoodDef; } private static bool ReadSteps(FieldReader f, string field, object value, out int steps) { steps = 0; if (value is double num && num >= 0.0 && num == Math.Floor(num)) { steps = (int)num; return true; } f.Wrong(field, "a whole number of relief steps >= 0", "entry skipped"); return false; } public static Dictionary Merge(Dictionary builtIn, Dictionary overrides) { Dictionary dictionary = SpeciesTable.Merge(builtIn, overrides); foreach (WeatherFoodDef value2 in dictionary.Values) { value2.FromOverride = false; } if (overrides != null) { foreach (string key in overrides.Keys) { if (dictionary.TryGetValue(key, out var value) && value != null) { value.FromOverride = true; } } } return dictionary; } public static WeatherFoodDef Match(Dictionary table, int itemId, string itemName) { //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) if (table == null || table.Count == 0) { return null; } for (int i = 0; i < 4; i++) { bool flag = i < 2; bool flag2 = (i & 1) == 0; foreach (WeatherFoodDef value in table.Values) { if (value != null && value.FromOverride == flag && value.ItemId.HasValue == flag2) { ItemKey val = new ItemKey(value.Key, value.ItemId); if (((ItemKey)(ref val)).Matches(itemId, itemName)) { return value; } } } } return null; } public static DrinkRuling RuleFeed(FeedOutcome dietOutcome, WeatherFoodDef def) { if (def == null) { return DrinkRuling.None; } if (dietOutcome == FeedOutcome.Fed) { return DrinkRuling.WithMeal; } if (!def.Universal) { return DrinkRuling.None; } return DrinkRuling.DrinkOnly; } } }