예제 #1
0
        public void GenerateAbyssBattleMSB(int mapLevel, List <int> bossCategoriesUsed)
        {
            // Basically, create thin New Londo MSB containing boss, then enable "done" flag.
            // EMEVD handles the rest.

            Map   newLondo = Maps.GetMap("NewLondoRuins");
            MSB1  msb      = MSB1.Read(ModMSBPath + $"{newLondo.MsbName}.msb");
            EMEVD emevd    = EMEVD.Read(ModEMEVDPath + $"{newLondo.EmevdName}.emevd.dcx");

            EMEVD.Event     constructor   = emevd.Events.Where(evt => evt.ID == 0).First();
            HashSet <int>   enemyModels   = new HashSet <int>();
            HashSet <int>   logicGoalIDs  = new HashSet <int>();
            HashSet <int>   battleGoalIDs = new HashSet <int>();
            MapPointManager pointManager  = new MapPointManager(newLondo.Name, Rand);

            // ABYSS BOSS
            int bossEntityID = newLondo.GetBossID(0);

            if (!Maps.BossLocations.Keys.Contains(bossEntityID))
            {
                throw new Exception($"Could not find information about location for (non-twin) boss ID {bossEntityID}.");
            }
            (GamePoint bossPoint, int emevdIndex) = Maps.BossLocations[bossEntityID];
            Boss  boss      = EnemyGenerator.GetRandomBoss(Rand, bossPoint.ArenaSize, bossCategoriesUsed);
            Enemy bossEnemy = EnemyGenerator.GetEnemy(boss.Name);

            MSB1.Part.Enemy bossPart = bossEnemy.GetMSBPart("Abyss Boss", bossEntityID, bossPoint, bossPoint.Angle, mapLevel, isRedPhantom: true);
            msb.Parts.Enemies.Add(bossPart);
            enemyModels.Add(bossEnemy.ModelID);
            logicGoalIDs.Add(Mod.GPARAM.AI[bossPart.ThinkParamID].LogicID);
            battleGoalIDs.Add(Mod.GPARAM.AI[bossPart.ThinkParamID].BattleID);

            // TWIN BOSSES:
            // If arena is Large or Giant, there's a chance of a twin boss (capping their combined aggression).
            // Mornstein ring makes it 100% chance.
            Boss twinBoss = null;

            if ((Roll(TwinBossOdds) || Run.GetFlag(GameFlag.MornsteinRingFlag)) && Maps.BossLocations.ContainsKey(bossEntityID + 1) && bossPoint.ArenaSize.In(ArenaSize.Large, ArenaSize.Giant) && boss.AggressionLevel < 5)
            {
                (GamePoint twinBossPoint, _) = Maps.BossLocations[bossEntityID + 1];
                List <Boss> twinOptions = new List <Boss>(EnemyGenerator.BossList.Where(
                                                              twin => twin.Category != boss.Category && twin.AggressionLevel + boss.AggressionLevel <= 5 && twin.RequiredArenaSize <= bossPoint.ArenaSize));
                twinBoss = twinOptions.GetRandomElement(Rand);

                Enemy twinBossEnemy = EnemyGenerator.GetEnemy(twinBoss.Name);

                var twinPart = twinBossEnemy.GetMSBPart($"Abyss Boss Twin", bossEntityID + 1, twinBossPoint, twinBossPoint.Angle, mapLevel, isRedPhantom: true);
                msb.Parts.Enemies.Add(twinPart);
                logicGoalIDs.Add(Mod.GPARAM.AI[twinPart.ThinkParamID].LogicID);
                battleGoalIDs.Add(Mod.GPARAM.AI[twinPart.ThinkParamID].BattleID);
                Run.BossCategoriesUsed.Add(twinBoss.Category);
                enemyModels.Add(twinBossEnemy.ModelID);

                Run.EnableFlag(newLondo.GetBossTwinFlag(0));
            }
            else
            {
                Run.DisableFlag(newLondo.GetBossTwinFlag(0));
            }

            // Inject name and item lot reward into EMEVD constructor.
            EMEVD.Instruction bossBattleCall = constructor.Instructions[emevdIndex];
            List <byte>       argData        = bossBattleCall.ArgData.ToList();
            int itemLotID = bossEnemy.RedPhantomItemLotParamID;

            argData[32] = (byte)itemLotID;
            argData[33] = (byte)(itemLotID >> 0x08);
            argData[34] = (byte)(itemLotID >> 0x10);
            argData[35] = (byte)(itemLotID >> 0x18);
            argData[52] = (byte)boss.NameTextID;
            argData[53] = (byte)(boss.NameTextID >> 0x08);
            if (twinBoss != null)
            {
                argData[54] = (byte)twinBoss.NameTextID;
                argData[55] = (byte)(twinBoss.NameTextID >> 0x08);
            }
            bossBattleCall.ArgData = argData.ToArray();

#if DEBUG
            Console.WriteLine($"    Updating models...");
#endif
            UpdateMSBModels(msb);

#if DEBUG
            Console.WriteLine($"    Writing MSB...");
#endif
            msb.Write(Mod.GameDir + $@"map\MapStudio\{newLondo.MsbName}.msb");  // No '.rls' copy needed.
#if DEBUG
            Console.WriteLine($"    Writing EMEVD...");
#endif
            emevd.Write(Mod.GameDir + $@"event\{newLondo.EmevdName}.emevd.dcx");

#if DEBUG
            Console.WriteLine($"    Writing LUABND...");
#endif
            WriteMapLUABND(newLondo.EmevdName, battleGoalIDs, logicGoalIDs);

#if DEBUG
            Console.WriteLine($"    Writing FFXBND...");
#endif
            WriteMapFFXBND(newLondo.MapID[0], enemyModels);
        }
예제 #2
0
        public void GenerateMapData(Map map, double redPhantomOdds)
        {
            // Generate and write MSB, LUABND, and tweaked EMEVD for given map.

#if DEBUG
            Console.WriteLine($"GENERATING MAP: {map.Name}");
#endif

            MSB1  msb   = MSB1.Read(ModMSBPath + $"{map.MsbName}.msb");
            EMEVD emevd = EMEVD.Read(ModEMEVDPath + $"{map.EmevdName}.emevd.dcx");

            EMEVD.Event constructor = emevd.Events.Where(evt => evt.ID == 0).First();

            HashSet <int> enemyModels   = new HashSet <int>();
            HashSet <int> logicGoalIDs  = new HashSet <int>(map.DefaultLogicScriptIDs);
            HashSet <int> battleGoalIDs = new HashSet <int>(map.DefaultBattleScriptIDs);

            MapPointManager pointManager      = new MapPointManager(map.Name, Rand);
            int             rarePhantomCount  = 0;
            int             basicPhantomCount = 0;

            string regionLabel;

            for (int i = 0; i < map.BossCount; i++)
            {
                // BOSSES

                int bossEntityID = map.GetBossID(i);
                if (!Maps.BossLocations.Keys.Contains(bossEntityID))
                {
                    throw new Exception($"Could not find information about location for (non-twin) boss ID {bossEntityID}.");
                }
                (GamePoint bossPoint, int emevdIndex) = Maps.BossLocations[bossEntityID];
                Boss boss = EnemyGenerator.GetRandomBoss(Rand, bossPoint.ArenaSize, Run.BossCategoriesUsed);
#if DEBUG
                Console.WriteLine($"Boss {i}: {boss.Name}");
#endif
                Enemy bossEnemy    = EnemyGenerator.GetEnemy(boss.Name);
                bool  isRedPhantom = map.Name == "ChasmOfTheAbyss" || boss.AlwaysRedPhantom; // Red phantom bosses are otherwise fought in Abyss battles only.

                var part = bossEnemy.GetMSBPart($"Boss {i}", bossEntityID, bossPoint, bossPoint.Angle, Run.MapLevel, isRedPhantom);
                msb.Parts.Enemies.Add(part);
                logicGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].LogicID);
                battleGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].BattleID);
                Run.BossCategoriesUsed.Add(boss.Category);
                enemyModels.Add(bossEnemy.ModelID);

                // TWIN BOSSES:
                // If arena is Large or Giant, there's a chance of a twin boss (capping their combined aggression).
                // Mornstein's ring makes it 100% chance whenever possible.
                Boss twinBoss = null;
                if ((Roll(TwinBossOdds) || Run.GetFlag(GameFlag.MornsteinRingFlag)) && Maps.BossLocations.ContainsKey(bossEntityID + 1) && bossPoint.ArenaSize.In(ArenaSize.Large, ArenaSize.Giant) && boss.AggressionLevel < 5)
                {
                    (GamePoint twinBossPoint, _) = Maps.BossLocations[bossEntityID + 1];
                    List <Boss> twinOptions = new List <Boss>(EnemyGenerator.BossList.Where(
                                                                  twin => twin.Category != boss.Category && twin.AggressionLevel + boss.AggressionLevel <= 5 && twin.RequiredArenaSize <= bossPoint.ArenaSize));
                    twinBoss = twinOptions.GetRandomElement(Rand);

                    Enemy twinBossEnemy    = EnemyGenerator.GetEnemy(twinBoss.Name);
                    bool  twinIsRedPhantom = map.Name == "ChasmOfTheAbyss" || twinBoss.AlwaysRedPhantom; // Red phantom bosses are otherwise fought in Abyss battles only.

                    var twinPart = twinBossEnemy.GetMSBPart($"Boss {i} Twin", bossEntityID + 1, twinBossPoint, twinBossPoint.Angle, Run.MapLevel, twinIsRedPhantom);
                    msb.Parts.Enemies.Add(twinPart);
                    logicGoalIDs.Add(Mod.GPARAM.AI[twinPart.ThinkParamID].LogicID);
                    battleGoalIDs.Add(Mod.GPARAM.AI[twinPart.ThinkParamID].BattleID);
                    Run.BossCategoriesUsed.Add(twinBoss.Category);
                    enemyModels.Add(twinBossEnemy.ModelID);

                    Run.EnableFlag(map.GetBossTwinFlag(i));
                }
                else
                {
                    Run.DisableFlag(map.GetBossTwinFlag(i));  // e.g. may have been set for New Londo Abyss boss previously
                }

                if (map.Name != "Lost Izalith")
                {
                    // Inject name and item lot reward into EMEVD constructor.
                    EMEVD.Instruction bossBattleCall = constructor.Instructions[emevdIndex];
                    List <byte>       argData        = bossBattleCall.ArgData.ToList();
                    int itemLotID = isRedPhantom ? bossEnemy.RedPhantomItemLotParamID : bossEnemy.ItemLotParamID;  // twin item lot ignored
                    argData[32] = (byte)itemLotID;
                    argData[33] = (byte)(itemLotID >> 0x08);
                    argData[34] = (byte)(itemLotID >> 0x10);
                    argData[35] = (byte)(itemLotID >> 0x18);
                    argData[52] = (byte)boss.NameTextID;
                    argData[53] = (byte)(boss.NameTextID >> 0x08);
                    if (twinBoss != null)
                    {
                        argData[54] = (byte)twinBoss.NameTextID;
                        argData[55] = (byte)(twinBoss.NameTextID >> 0x08);
                    }
                    bossBattleCall.ArgData = argData.ToArray();
                }
            }

            if (Roll(InvaderOdds))
            {
                // INVADER
                if (map.Name == "Blighttown")
                {
                    regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty";
                }
                else
                {
                    regionLabel = "";
                }

                int             invaderIndex    = Run.InvadersAvailable.GetRandomElement(Rand);
                int             invaderParamID  = 7000 + 10 * invaderIndex;
                int             invaderEntityID = map.BaseNPCEntityID + 50;
                GamePoint       spawnPoint      = pointManager.CheckOutRandomPoint("Invader", (int)ChrSize.Normal);
                GamePoint       triggerPoint    = pointManager.CheckOutRandomPointWithinDistance("Invader Trigger", spawnPoint, 20.0, 40.0);
                float           angle           = MapPointManager.GetFacingPoint(spawnPoint, triggerPoint);
                MSB1.Part.Enemy invader         = GetInvaderPart(invaderEntityID, spawnPoint, angle, invaderParamID, Run.MapLevel);
                msb.Parts.Enemies.Add(invader);
                Run.InvadersAvailable.Remove(invaderIndex);
                Run.InvadersUsed.Add(invaderIndex);
                Run.EnableFlag(GameFlag.InvaderUsedBaseFlag + invaderIndex);

                MSB1.Region triggerRegion = new MSB1.Region()
                {
                    Name     = "Invader Trigger",
                    Position = triggerPoint.Position - new Vector3(0.0f, 1.0f, 0.0f),  // 1.0 units below point
                    Shape    = new MSB1.Shape.Box()
                    {
                        Width = 20.0f, Depth = 20.0f, Height = 10.0f
                    },
                    EntityID = map.InvaderTriggerRegionID,
                };
                msb.Regions.Regions.Add(triggerRegion);

                MSB1.Region spawnRegion = new MSB1.Region()
                {
                    Name     = "Invader Spawn",
                    Position = spawnPoint.Position,
                    Shape    = new MSB1.Shape.Point(),
                    EntityID = map.InvaderSpawnPointID,
                };
                msb.Regions.Regions.Add(spawnRegion);
            }

            if (Roll(VeryRareEnemyOdds * (Run.GetFlag(GameFlag.MornsteinRingFlag) ? 2 : 1)))
            {
                // VERY RARE ENEMY
                if (map.Name == "Blighttown")
                {
                    regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty";
                }
                else
                {
                    regionLabel = "";
                }

                bool      isRedPhantom = Roll(redPhantomOdds);
                int       entityID     = map.BaseEntityID + VeryRareEnemyOffset;
                Enemy     enemy        = EnemyGenerator.GetRandomEnemyWithRarity(Rand, EnemyRarity.VeryRare, Run.MapLabels[map]);
                GamePoint point        = pointManager.CheckOutRandomPoint("VeryRare Enemy", (int)enemy.Size); // don't care about region label
                if (map.Name == "NewLondoRuins" && point.RegionLabel == "Lower")
                {
                    entityID += NewLondoVeryRareEnemyOffset;
                }
                var part = enemy.GetMSBPart("VeryRare Enemy 1", entityID, point, GetRandomAngle(), Run.MapLevel, isRedPhantom);
                msb.Parts.Enemies.Add(part);
                logicGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].LogicID);
                battleGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].BattleID);
                enemyModels.Add(enemy.ModelID);
            }

            if (!map.Name.In("PaintedWorld", "ChasmOfTheAbyss", "NewLondoRuins") && (Run.GetFlag(GameFlag.LoganRingFlag) || Roll(AbyssPortalOdds)))
            {
                // ABYSS PORTAL

                GamePoint        point          = pointManager.CheckOutRandomPoint("Abyss Portal", 0);
                MSB1.Part.Player portalWarpBack = new MSB1.Part.Player()
                {
                    Name         = "Portal Return",
                    Position     = point.Position,
                    Rotation     = new Vector3(0.0f, GetRandomAngle(), 0.0f),
                    EntityID     = map.PortalWarpPointID,
                    IsShadowSrc  = 1,
                    IsShadowDest = 1,
                    ModelName    = "c0000",
                };
                msb.Parts.Players.Add(portalWarpBack);
                MSB1.Region portalPoint = new MSB1.Region()
                {
                    Name     = "Portal VFX Point",
                    Position = point.Position + new Vector3(0.0f, 2.0f, 0.0f),
                    Shape    = new MSB1.Shape.Point(),
                    EntityID = -1,
                };
                msb.Regions.Regions.Add(portalPoint);
                MSB1.Event.SFX portalVFX = new MSB1.Event.SFX()
                {
                    Name       = "Portal VFX",
                    FFXID      = 120027,
                    RegionName = "Portal VFX Point",
                    PartName   = point.CollisionName,
                    EntityID   = map.PortalFXID,
                };
                msb.Events.SFXs.Add(portalVFX);
                MSB1.Part.Enemy portalTrigger = new MSB1.Part.Enemy()
                {
                    ModelName     = "c1000",
                    Name          = "Portal Trigger",
                    Position      = point.Position + new Vector3(0.0f, 0.5f, 0.0f),
                    EntityID      = map.PortalTriggerCharacterID,
                    NPCParamID    = 100000,
                    TalkID        = 0,
                    CollisionName = point.CollisionName,
                    ThinkParamID  = 1,
                };
                msb.Parts.Enemies.Add(portalTrigger);
            }

            if (!Run.MapsVisited.Contains(Maps.GetMap("PaintedWorld")) && Roll(PaintingOdds))
            {
                // Painting appears, in a random choice of its possible positions (pre-existing in base MSB).
                // TODO: 3-4 positions per map. Wherever it fits. Proper collision assignment, etc. Also prompt regions for simplicity.
            }

            foreach (Merchant merchant in CharacterGenerator.MerchantList)
            {
                // MERCHANTS
                if (Run.GetFlag(merchant.DeadFlag))
                {
                    continue;  // Merchant is dead and cannot appear again.
                }
                if (map.Name == "Blighttown")
                {
                    regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty";
                }
                else
                {
                    regionLabel = "";
                }

                double merchantOdds = merchant.Name == "Marvelous Chester" ? ChesterOdds : MerchantOdds;
                if (Roll(merchantOdds))
                {
                    if (map.Name == "NewLondoRuins")
                    {
                        regionLabel = "Upper";  // no merchants in lower
                    }
                    GamePoint point = pointManager.CheckOutRandomPointCustomNearbyRadius("Merchant", 2.0, 5, regionLabel);
                    var       part  = merchant.GetPart(point, pointManager.GetAngleFacingNearestPoint(point), map.TalkIDBase);
                    msb.Parts.Enemies.Add(part);
                    logicGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].LogicID);
                    battleGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].BattleID);
                }
            }

            int        chestCount        = Math.Max(0, Rand.Next(map.ChestCount - 2, map.ChestCount));
            List <int> usedChestTreasure = new List <int>();
            for (int i = 0; i < chestCount; i++)
            {
                // CHESTS
                if (map.Name == "Blighttown")
                {
                    regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty";
                }
                else
                {
                    regionLabel = "";
                }

                int       chestID = map.BaseChestEntityID + i;
                GamePoint point   = pointManager.CheckOutRandomPointCustomNearbyRadius("Chest", 2.0, 5, regionLabel);
                float     angle   = pointManager.GetAngleFacingNearestPoint(point);
                var       part    = GetChestPart(i, chestID, angle, point);
                msb.Parts.Objects.Add(part);

                var objAct = new MSB1.Event.ObjAct()
                {
                    Name           = $"Chest ObjAct {i}",
                    ObjActEntityID = map.BaseChestObjActFlagID + i,
                    ObjActPartName = $"Chest {i}",
                    EventFlagID    = 0,
                };
                msb.Events.ObjActs.Add(objAct);

                var treasure = new MSB1.Event.Treasure()
                {
                    Name             = $"Chest Treasure {i}",
                    TreasurePartName = $"Chest {i}",
                    PartName         = point.CollisionName,
                    InChest          = true,
                    StartDisabled    = true, // can't get the treasure to disable itself based on 'InChest' alone...
                };
                // Choose random treasure in unlocked range.
                treasure.ItemLots[0] = GetChestTreasureID(map.BaseChestItemLotID, usedChestTreasure);
                usedChestTreasure.Add(treasure.ItemLots[0]);
                msb.Events.Treasures.Add(treasure);
            }

            if (Roll(MimicOdds))
            {
                // MIMIC
                if (map.Name == "Blighttown")
                {
                    regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty";
                }
                else
                {
                    regionLabel = "";
                }

                bool isRedPhantom  = Roll(redPhantomOdds);
                int  mimicEntityID = map.BaseEntityID + MimicOffset;
                if (map.Name == "NewLondoRuins")
                {
                    regionLabel = "Upper";  // no Mimics in lower
                }
                Enemy     mimic = EnemyGenerator.GetEnemy("Mimic");
                GamePoint point = pointManager.CheckOutRandomPoint("Mimic", (int)mimic.Size, regionLabel);
                float     angle = pointManager.GetAngleFacingNearestPoint(point);
                var       part  = mimic.GetMSBPart("Mimic", mimicEntityID, point, angle, Run.MapLevel, isRedPhantom);
                msb.Parts.Enemies.Add(part);
                logicGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].LogicID);
                battleGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].BattleID);
                enemyModels.Add(mimic.ModelID);

                MSB1.Region mimicNest = new MSB1.Region()
                {
                    Name     = "Mimic Nest",
                    Position = new Vector3(point.Position.X, point.Position.Y - 0.1f, point.Position.Z),
                    Rotation = new Vector3(0.0f, angle, 0.0f),
                    Shape    = new MSB1.Shape.Box()
                    {
                        Width = 5.0f, Depth = 5.0f, Height = 2.0f
                    },
                    EntityID = mimicEntityID + 5,
                };
                msb.Regions.Regions.Add(mimicNest);
            }

            int        itemCount          = Math.Max(0, Rand.Next(map.ItemCorpseCount - 5, map.ItemCorpseCount));
            List <int> usedCorpseTreasure = new List <int>();
            for (int i = 0; i < itemCount; i++)
            {
                // ITEM CORPSES
                if (map.Name == "Blighttown")
                {
                    regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty";
                }
                else
                {
                    regionLabel = "";
                }

                GamePoint point      = pointManager.CheckOutRandomPointWeightedInverseNearby($"Item Corpse {i}", 0, regionLabel);
                int       pose       = Objects.CorpsePoses.GetRandomElement(Rand);
                float     poseOffset = Objects.CorpsePoseYOffsets[pose];

                MSB1.Part.Object corpse = new MSB1.Part.Object()
                {
                    ModelName     = "o0500", // Not using o0502 or o0504, as they have different pose enums.
                    Name          = $"Item Corpse {i}",
                    CollisionName = point.CollisionName,
                    Position      = new Vector3(point.Position.X, point.Position.Y + poseOffset, point.Position.Z),
                    Rotation      = new Vector3(0.0f, GetRandomAngle(), 0.0f),
                    EntityID      = -1, // not needed
                    ObjectPose    = (short)pose,
                };
                corpse.ClearDrawGroups();
                corpse.ClearDispGroups();
                msb.Parts.Objects.Add(corpse);

                MSB1.Event.Treasure treasure = new MSB1.Event.Treasure()
                {
                    Name             = $"Item Corpse Treasure {i}",
                    TreasurePartName = $"Item Corpse {i}",
                    PartName         = point.CollisionName,
                    InChest          = false,
                };
                // Assign random corpse treasure from unlocked ranges.
                treasure.ItemLots[0] = GetCorpseTreasureID(map.BaseCorpseItemLotID, usedCorpseTreasure);
                usedCorpseTreasure.Add(treasure.ItemLots[0]);
                msb.Events.Treasures.Add(treasure);
            }

            int          rareCount       = Math.Max(0, Rand.Next(map.RareEnemyCount - 3, map.RareEnemyCount));
            List <Enemy> usedRareEnemies = new List <Enemy>();
            for (int i = 0; i < rareCount; i++)
            {
                // RARE ENEMIES
                if (map.Name == "Blighttown")
                {
                    regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty";
                }
                else if (map.Name == "NewLondoRuins" && i == 0)
                {
                    regionLabel = "Upper";  // first rare enemy is always in Upper New Londo Ruins (for Holy Sigil drop)
                }
                else
                {
                    regionLabel = "";
                }

                Enemy enemy;
                if (usedRareEnemies.Count > MaxRareEnemyTypeCount)
                {
                    enemy = usedRareEnemies.GetRandomElement(Rand);
                }
                else
                {
                    enemy = EnemyGenerator.GetRandomEnemyWithRarity(Rand, EnemyRarity.Rare, Run.MapLabels[map]);
                    usedRareEnemies.Add(enemy);
                }

                bool isRedPhantom = rarePhantomCount < 15 && Roll(redPhantomOdds);

                GamePoint point = pointManager.CheckOutRandomPoint("Rare Enemy", (int)enemy.Size, regionLabel);

                int entityID = map.BaseEntityID + RareEnemyOffset + i;
                if (map.Name == "NewLondoRuins" && point.RegionLabel == "Lower")
                {
                    entityID += NewLondoLowerRareEnemyOffset;
                }

                MSB1.Part.Enemy enemyPart = enemy.GetMSBPart($"Rare Enemy {i}", entityID, point, GetRandomAngle(), Run.MapLevel, isRedPhantom);
                msb.Parts.Enemies.Add(enemyPart);
                logicGoalIDs.Add(Mod.GPARAM.AI[enemyPart.ThinkParamID].LogicID);
                battleGoalIDs.Add(Mod.GPARAM.AI[enemyPart.ThinkParamID].BattleID);
                enemyModels.Add(enemy.ModelID);

                if (isRedPhantom)
                {
                    rarePhantomCount++;
                }
            }

            int          basicEnemyCount        = Math.Max(0, Rand.Next(map.BasicEnemyCount - 5, map.BasicEnemyCount));
            int          basicNonPhantomCount   = 0;
            List <Enemy> usedCommonEnemyTypes   = new List <Enemy>();
            List <Enemy> usedUncommonEnemyTypes = new List <Enemy>();
            for (int i = 0; i < basicEnemyCount; i++)
            {
                // BASIC (COMMON/UNCOMMON) ENEMIES
                if (map.Name == "Blighttown")
                {
                    regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty";
                }
                else
                {
                    regionLabel = "";
                }

                Enemy       enemy;
                EnemyRarity rarity = Roll(UncommonEnemyOdds) ? EnemyRarity.Uncommon : EnemyRarity.Common;
                if (rarity == EnemyRarity.Common)
                {
                    if (usedCommonEnemyTypes.Count > MaxCommonEnemyTypeCount)
                    {
                        enemy = usedCommonEnemyTypes.GetRandomElement(Rand);
                    }
                    else
                    {
                        enemy = EnemyGenerator.GetRandomEnemyWithRarity(Rand, rarity, Run.MapLabels[map]);
                        usedCommonEnemyTypes.Add(enemy);
                    }
                }
                else
                {
                    if (usedUncommonEnemyTypes.Count > MaxUncommonEnemyTypeCount)
                    {
                        enemy = usedUncommonEnemyTypes.GetRandomElement(Rand);
                    }
                    else
                    {
                        enemy = EnemyGenerator.GetRandomEnemyWithRarity(Rand, rarity, Run.MapLabels[map]);
                        usedUncommonEnemyTypes.Add(enemy);
                    }
                }

                bool isRedPhantom = basicPhantomCount < 15 && Roll(redPhantomOdds);

                int       entityID = map.BaseEntityID + (isRedPhantom ? BasicPhantomOffset + basicPhantomCount : BasicEnemyOffset + basicNonPhantomCount);
                GamePoint point    = pointManager.CheckOutRandomPoint("Basic Enemy", (int)enemy.Size, regionLabel);
                if (map.Name == "NewLondoRuins" && point.RegionLabel == "Lower")
                {
                    entityID += isRedPhantom ? NewLondoLowerBasicPhantomOffset : NewLondoLowerBasicEnemyOffset;
                }
                MSB1.Part.Enemy enemyPart = enemy.GetMSBPart($"Basic Enemy {i}", entityID, point, GetRandomAngle(), Run.MapLevel, isRedPhantom);
                msb.Parts.Enemies.Add(enemyPart);
                logicGoalIDs.Add(Mod.GPARAM.AI[enemyPart.ThinkParamID].LogicID);
                battleGoalIDs.Add(Mod.GPARAM.AI[enemyPart.ThinkParamID].BattleID);
                enemyModels.Add(enemy.ModelID);

                if (isRedPhantom)
                {
                    basicPhantomCount++;
                }
                else
                {
                    basicNonPhantomCount++;
                }
            }

            int vagrantCount = Rand.Next(3);
            for (int i = 0; i < vagrantCount; i++)
            {
                // VAGRANTS (max of 2)
                if (map.Name == "Blighttown")
                {
                    regionLabel = Roll(BlighttownSwampOdds) ? "Swamp" : "Shanty";
                }
                else
                {
                    regionLabel = "";
                }

                if (map.Name == "NewLondoRuins")
                {
                    regionLabel = "Upper";  // No Vagrants in Lower New Londo
                }
                bool      isRedPhantom = Run.GetFlag(GameFlag.MornsteinRingFlag) ? Roll(0.2) : Roll(0.1);
                int       entityID     = map.BaseEntityID + VagrantOffset + i;
                Enemy     enemy        = EnemyGenerator.GetEnemy(Roll(0.5) ? "Good Vagrant" : "Evil Vagrant");
                GamePoint point        = pointManager.CheckOutRandomPoint("Vagrant", (int)enemy.Size, regionLabel);
                var       part         = enemy.GetMSBPart($"Vagrant {i}", entityID, point, GetRandomAngle(), Run.MapLevel, isRedPhantom);
                msb.Parts.Enemies.Add(part);
                logicGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].LogicID);
                battleGoalIDs.Add(Mod.GPARAM.AI[part.ThinkParamID].BattleID);
                enemyModels.Add(enemy.ModelID);
            }

#if DEBUG
            Console.WriteLine($"    Updating models...");
#endif
            UpdateMSBModels(msb);

            // Output MSB. Also output a '.rls' copy, so it can be reloaded and modified at bonfire creation.
#if DEBUG
            Console.WriteLine($"    Writing MSB...");
#endif
            msb.Write(Mod.GameDir + $@"map\MapStudio\{map.MsbName}.msb");
            msb.Write(Mod.GameDir + $@"map\MapStudio\{map.MsbName}.msb.rls");  // file to modify for bonfire creation
#if DEBUG
            Console.WriteLine($"    Writing EMEVD...");
#endif
            emevd.Write(Mod.GameDir + $@"event\{map.EmevdName}.emevd.dcx");

#if DEBUG
            Console.WriteLine($"    Writing LUABND...");
#endif
            WriteMapLUABND(map.EmevdName, battleGoalIDs, logicGoalIDs);

#if DEBUG
            Console.WriteLine($"    Writing FFXBND...");
#endif
            WriteMapFFXBND(map.MapID[0], enemyModels);
        }