using System; using System.Collections.Generic; using KernelPanic.Core; using KernelPanic.Data; using UnityEngine; namespace KernelPanic.Meta { public static class PullResolver { public static event Action PullResolved; public static PullResolutionResult Resolve(IEnumerable pullResults, PullResolutionContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } context.SaveData ??= SaveData.CreateDefault(); context.SaveData.EnsureLists(); List outcomes = new(); if (pullResults != null) { foreach (DistroDefinition unit in pullResults) { outcomes.Add(ResolveOne(unit, context)); } } PullResolutionResult result = new(outcomes); PullResolved?.Invoke(result); return result; } public static PullResolutionResult Resolve(IEnumerable pullResultIds, PullResolutionContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } List units = new(); if (pullResultIds != null) { foreach (string id in pullResultIds) { units.Add(context.DistroDatabase == null ? null : context.DistroDatabase.FindById(id)); } } return Resolve(units, context); } public static PullResolutionResult ResolveAndSave(IEnumerable pullResults, PullResolutionContext context, SaveService saveService) { PullResolutionResult result = Resolve(pullResults, context); saveService?.Save(context.SaveData); return result; } public static PullResolutionResult ResolveAndSave(IEnumerable pullResultIds, PullResolutionContext context, SaveService saveService) { PullResolutionResult result = Resolve(pullResultIds, context); saveService?.Save(context.SaveData); return result; } private static PullResolutionOutcome ResolveOne(DistroDefinition unit, PullResolutionContext context) { if (unit == null || string.IsNullOrWhiteSpace(unit.Id)) { return new PullResolutionOutcome(null, PullOutcomeKind.Invalid, 0, Array.Empty()); } OwnedUnitSaveEntry owned = context.SaveData.FindOwnedUnit(unit.Id); bool ownedInCollection = context.Collection != null && context.Collection.IsOwned(unit.Id); if (owned == null && !ownedInCollection) { IReadOnlyList before = SnapshotUnlockedLanguages(context.Collection); context.SaveData.AddOwnedUnit(unit.Id, 1); context.Collection?.AddSilently(unit, 1); IReadOnlyList unlocked = NewlyUnlockedLanguages(before, context.Collection, unit); return new PullResolutionOutcome(unit.Id, PullOutcomeKind.Granted, 0, unlocked); } owned ??= context.SaveData.AddOwnedUnit(unit.Id, context.Collection?.GetVersion(unit.Id) ?? 1); int version = Mathf.Clamp(owned.version, 1, GachaTuning.MaxVersion); owned.version = version; bool overflow = version >= GachaTuning.MaxVersion; int merges = CalculateDupeMerges(overflow, unit.Id, context.FocusUnitId); owned.merges = Math.Max(0, owned.merges) + merges; if (GachaTuning.DupeConsolationBandwidth > 0) { context.SaveData.bandwidthBalance += GachaTuning.DupeConsolationBandwidth; } context.Collection?.AddSilently(unit, version); return new PullResolutionOutcome(unit.Id, overflow ? PullOutcomeKind.DupeOverflow : PullOutcomeKind.Dupe, merges, Array.Empty()); } private static int CalculateDupeMerges(bool overflow, string unitId, string focusUnitId) { float baseAmount = GachaTuning.MergesPerDupe; if (overflow) { baseAmount *= GachaTuning.MergesMaxVersionOverflowMultiplier; } int merges = Mathf.RoundToInt(baseAmount); if (!string.IsNullOrWhiteSpace(focusUnitId) && string.Equals(unitId, focusUnitId, StringComparison.OrdinalIgnoreCase)) { merges += GachaTuning.MergesOnCharacterBonus; } return Math.Max(1, merges); } private static IReadOnlyList SnapshotUnlockedLanguages(PlayerCollection collection) { List unlocked = new(); if (collection == null) { return unlocked; } IReadOnlyList languages = LanguageCatalog.All; for (int i = 0; i < languages.Count; i++) { if (LanguageUnlock.IsUnlocked(languages[i].Language, collection)) { unlocked.Add(languages[i].Language); } } return unlocked; } private static IReadOnlyList NewlyUnlockedLanguages(IReadOnlyList before, PlayerCollection collection, DistroDefinition unit) { List unlocked = new(); if (collection == null || unit == null) { return unlocked; } AddNewlyUnlockedLanguage(unit.PrimaryLanguage, before, collection, unlocked); AddNewlyUnlockedLanguage(unit.SecondaryLanguage, before, collection, unlocked); return unlocked; } private static void AddNewlyUnlockedLanguage(Language language, IReadOnlyList before, PlayerCollection collection, List unlocked) { if (!ContainsLanguage(unlocked, language) && !ContainsLanguage(before, language) && LanguageUnlock.IsUnlocked(language, collection)) { unlocked.Add(language); } } private static bool ContainsLanguage(IReadOnlyList languages, Language language) { for (int i = 0; i < languages.Count; i++) { if (languages[i] == language) { return true; } } return false; } } public sealed class PullResolutionContext { public PullResolutionContext(SaveData saveData, PlayerCollection collection, DistroDatabase distroDatabase = null, string focusUnitId = null) { SaveData = saveData; Collection = collection; DistroDatabase = distroDatabase; FocusUnitId = focusUnitId; } public SaveData SaveData { get; set; } public PlayerCollection Collection { get; } public DistroDatabase DistroDatabase { get; } public string FocusUnitId { get; } } public sealed class PullResolutionResult { public PullResolutionResult(IReadOnlyList outcomes) { Outcomes = outcomes ?? Array.Empty(); } public IReadOnlyList Outcomes { get; } } public readonly struct PullResolutionOutcome { public PullResolutionOutcome(string unitId, PullOutcomeKind kind, int mergesAwarded, IReadOnlyList languagesNewlyUnlocked) { UnitId = unitId; Kind = kind; MergesAwarded = mergesAwarded; LanguagesNewlyUnlocked = languagesNewlyUnlocked ?? Array.Empty(); } public string UnitId { get; } public PullOutcomeKind Kind { get; } public int MergesAwarded { get; } public IReadOnlyList LanguagesNewlyUnlocked { get; } } public enum PullOutcomeKind { Invalid, Granted, Dupe, DupeOverflow } }