Example #1
0
        public void ProcessEnemyPreset(GameData game, Dictionary <int, EnemyInfo> infos, List <EnemyCategory> cats, Dictionary <int, EnemyData> defaultData)
        {
            // Process enemy names
            HashSet <string> eligibleNames = new HashSet <string>();

            foreach (EnemyCategory cat in cats)
            {
                eligibleNames.Add(cat.Name);
                if (cat.Instance != null)
                {
                    eligibleNames.UnionWith(cat.Instance);
                }
                if (cat.Partition != null)
                {
                    eligibleNames.UnionWith(cat.Partition);
                }
                if (cat.Partial != null)
                {
                    eligibleNames.UnionWith(cat.Partial);
                }
            }
            Dictionary <int, string>         primaryName    = new Dictionary <int, string>();
            Dictionary <string, List <int> > enemiesForName = new Dictionary <string, List <int> >();
            bool debugNames = false;
            // Guardian Ape is both a boss and a helper, so try to avoid the helper ape getting pulled into the category
            HashSet <string> bossNames = new HashSet <string>(infos.Values.Where(i => i.Class == EnemyClass.Boss && i.ExtraName != null).Select(i => i.ExtraName));

            foreach (EnemyInfo info in infos.Values)
            {
                // Do not let some enemies be randomized at this point, many will prevent the game from being completeable.
                if (info.Class == EnemyClass.None)
                {
                    continue;
                }
                List <string> names = new List <string>();
                // Add all names. The first name added will be the primary name.
                if (info.ExtraName != null)
                {
                    names.Add(info.ExtraName);
                }
                if (defaultData.TryGetValue(info.ID, out EnemyData data))
                {
                    string model = game.ModelName(data.Model);
                    if (info.Class != EnemyClass.Boss && info.Category != null)
                    {
                        foreach (string cat in Regex.Split(info.Category, @"\s*;\s*"))
                        {
                            names.Add($"{cat} {model}");
                        }
                    }
                    if (info.Class == EnemyClass.Boss ? info.ExtraName == null : !bossNames.Contains(model))
                    {
                        names.Add(model);
                    }
                    if (info.Class == EnemyClass.Miniboss || info.Class == EnemyClass.Basic)
                    {
                        names.Add($"{info.Class} {model}");
                    }
                }
                names.RemoveAll(n =>
                {
                    if (!eligibleNames.Contains(n))
                    {
                        if (debugNames)
                        {
                            Console.WriteLine($"Name removed for {info.ID}: [{n}]");
                        }
                        return(true);
                    }
                    return(false);
                });
                if (names.Count > 0)
                {
                    primaryName[info.ID] = names[0];
                }
                names = names.SelectMany(n => new[] { n, $"{n} {info.ID}" }).ToList();
                names.Add(info.ID.ToString());
                if (info.Class == EnemyClass.Boss || info.Class == EnemyClass.Miniboss || info.Class == EnemyClass.Basic)
                {
                    names.Add($"{info.Class}");
                }
                if (info.Class != EnemyClass.Helper)
                {
                    // This is mainly used for "Oops All Any" so it should not include unkillable helpers
                    // like Immortal Centipede or Corrupted Monk Illusion.
                    names.Add($"Any");
                }
                if (debugNames)
                {
                    Console.WriteLine($"-- Names: {string.Join("; ", names)}");
                }
                foreach (string name in names)
                {
                    AddMulti(enemiesForName, name, info.ID);
                }
            }
            bool generateEnemyList = false;

            if (generateEnemyList)
            {
                foreach (EnemyClass c in new[] { EnemyClass.Boss, EnemyClass.TutorialBoss, EnemyClass.Miniboss, EnemyClass.FoldingMonkey, EnemyClass.Basic })
                {
                    string map = null;
                    foreach (EnemyInfo info in infos.Values)
                    {
                        if (info.Class == c && primaryName.TryGetValue(info.ID, out string name))
                        {
                            string enemyMap = game.LocationNames[game.Locations[defaultData[info.ID].Map]];
                            if (map != enemyMap)
                            {
                                map = enemyMap;
                                Console.WriteLine($"  # {map}");
                            }
                            Console.WriteLine($"  {name} {info.ID}: any");
                        }
                    }
                    Console.WriteLine();
                }
            }
            foreach (EnemyCategory cat in cats)
            {
                if (cat.Contains == null)
                {
                    continue;
                }
                List <int> combinedIds = new List <int>();
                foreach (string sub in cat.Contains)
                {
                    if (enemiesForName.TryGetValue(sub, out List <int> specialIds))
                    {
                        combinedIds.AddRange(specialIds);
                    }
                }
                if (combinedIds.Count > 0)
                {
                    enemiesForName[cat.Name] = combinedIds;
                }
            }

            // Process the config with these names
            List <string> errors = new List <string>();

            List <int> getIds(string name)
            {
                if (!enemiesForName.TryGetValue(name, out List <int> ids))
                {
                    string findId = "";
                    if (int.TryParse(name.Split(' ').Last(), out int id))
                    {
                        if (primaryName.TryGetValue(id, out string name2))
                        {
                            findId = $". Did you mean {name2} {id}?";
                        }
                        else
                        {
                            List <string> alts = enemiesForName.Select(e => e.Key).Where(e => e.EndsWith(id.ToString())).ToList();
                            if (alts.Count > 0)
                            {
                                findId = $". Did you mean {string.Join(", ", alts)}?";
                            }
                        }
                    }
                    errors.Add($"Unrecognized enemy name \"{name}\"{findId}");
                    return(new List <int>());
                }
                return(ids.ToList());
            }

            List <List <int> > getMultiIds(string name)
            {
                List <List <int> > ids = new List <List <int> >();

                foreach (string n in Regex.Split(name, @"\s*;\s*").ToList())
                {
                    ids.Add(getIds(n));
                }
                return(ids);
            }

            // Fill in non-randomized ids. The individual enemy config can also add to this.
            if (DontRandomize != null && DontRandomize.ToLowerInvariant() != "none")
            {
                DontRandomizeIDs.UnionWith(getMultiIds(DontRandomize).SelectMany(i => i));
            }
            if (RemoveSource != null && RemoveSource.ToLowerInvariant() != "none")
            {
                RemoveSourceIDs.UnionWith(getMultiIds(RemoveSource).SelectMany(i => i));
            }

            // Process the specific enemy map config
            bool debug = false;

            if (Enemies != null)
            {
                foreach (KeyValuePair <string, string> entry in Enemies)
                {
                    // For now, validate the config before checking if we can continue. This could be relaxed in the future, or in release builds.
                    List <int> targets = getIds(entry.Key);
                    if (targets.Count > 1 && debug)
                    {
                        Console.WriteLine($"Note: Enemy assigment {entry.Key}: {entry.Value} produced {targets.Count} targets");
                    }
                    if (entry.Value.ToLowerInvariant() == "any")
                    {
                        continue;
                    }
                    else if (entry.Value.ToLowerInvariant() == "norandom")
                    {
                        DontRandomizeIDs.UnionWith(targets);
                        continue;
                    }
                    List <int> sources = getIds(entry.Value);
                    if (sources.Count > 0)
                    {
                        // Allow the primary key to not be a unique enemy. This may produce some weird results.
                        foreach (int target in targets)
                        {
                            AddMulti(EnemyIDs, target, sources);
                        }
                    }
                }
            }

            bool poolFilter(int id)
            {
                return(!DontRandomizeIDs.Contains(id) && !RemoveSourceIDs.Contains(id));
            }

            // If oops all mode, fill in oops all ids. And copy them to pools.
            if (OopsAll != null && OopsAll.ToLowerInvariant() != "none")
            {
                OopsAllIDs.AddRange(getMultiIds(OopsAll).SelectMany(i => i).Where(poolFilter).Distinct());
                if (debug)
                {
                    Console.WriteLine($"Oops All: {string.Join("; ", OopsAllIDs.Select(i => primaryName.TryGetValue(i, out string n) ? n : i.ToString()))}");
                }
            }

            // Pool filtering
            int filterMulti(List <List <int> > groups, Predicate <int> filter)
            {
                int removed      = 0;
                int groupRemoved = groups.RemoveAll(group =>
                {
                    removed += group.RemoveAll(i => !filter(i));
                    return(group.Count == 0);
                });

                removed += groupRemoved;
                return(removed);
            }

            // For all enemy groups, fill in their ids
            void processPool(PoolAssignment pool, string type)
            {
                if (pool.Weight < 0)
                {
                    pool.Weight = 0;
                    errors.Add($"Pool for {type} \"{pool.Pool}\" must specify a positive Weight");
                }
                if (pool.Pool == null)
                {
                    errors.Add($"Pool for {type} must include a Pool specification");
                    pool.Weight = 0;
                    return;
                }
                if (pool.Pool.ToLowerInvariant() == "default")
                {
                    return;
                }
                pool.PoolGroups = getMultiIds(pool.Pool);
                filterMulti(pool.PoolGroups, poolFilter);
                if (pool.PoolGroups.Count == 0)
                {
                    pool.Weight = 0;
                }
            }

            List <PoolAssignment> processPools(List <PoolAssignment> pools, string type)
            {
                if (pools == null || pools.Count == 1 && pools[0].Pool.ToLowerInvariant() == "default")
                {
                    if (OopsAllIDs.Count > 0)
                    {
                        return(new List <PoolAssignment>
                        {
                            new PoolAssignment
                            {
                                Weight = 100,
                                Pool = OopsAll,
                                PoolGroups = new List <List <int> > {
                                    OopsAllIDs
                                },
                            },
                        });
                    }
                    else
                    {
                        return(null);
                    }
                }
                foreach (PoolAssignment pool in pools)
                {
                    processPool(pool, type);
                }
                return(pools);
            }

            Boss          = processPools(Boss, "Boss");
            Miniboss      = processPools(Miniboss, "Miniboss");
            Basic         = processPools(Basic, "Basic");
            Add           = processPools(Add, "Add");
            FoldingMonkey = processPools(FoldingMonkey, "FoldingMonkey");
            // Also copy 'basic' into 'add' if not specified, removing multi-phase enemies where possible
            if (Add == null && Basic != null)
            {
                Add = Basic.Select(p => p.Copy()).ToList();
                int removed = 0;
                foreach (PoolAssignment pool in Add)
                {
                    if (pool.PoolGroups.Count != 0)
                    {
                        removed += filterMulti(pool.PoolGroups, i => (infos[i].Class != EnemyClass.Boss && infos[i].Class != EnemyClass.Miniboss) || infos[i].HasTag("reasonable"));
                        if (pool.PoolGroups.Count == 0)
                        {
                            pool.Weight = 0;
                        }
                    }
                }
                if (removed == 0)
                {
                    Add = null;
                }
            }

            HandleErrors(errors);
        }