Beispiel #1
0
        public Assignment SplitAll()
        {
            // Add skills as item drops in the world. Esoteric texts still work, but they are removed from randomizer pool.
            Assignment ret = new Assignment();

            // First, create skill items, including editing item fmgs
            SortedDictionary <int, ItemKey> newSkills = new SortedDictionary <int, ItemKey>();
            SortedDictionary <int, ItemKey> oldSkills = new SortedDictionary <int, ItemKey>();

            Dictionary <int, ItemKey> texts = new Dictionary <int, ItemKey>
            {
                [0] = game.ItemForName("Shinobi Esoteric Text"),
                [1] = game.ItemForName("Prosthetic Esoteric Text"),
                [2] = game.ItemForName("Ashina Esoteric Text"),
                [3] = game.ItemForName("Senpou Esoteric Text"),
                [4] = game.ItemForName("Mushin Esoteric Text"),
            };

            // Note: there are some events in common which can be used to detect skills which are already granted by emevd.
            // But they all have a text of -1 so it's unnecessary to scan emevd for this.
            // For example:
            // Initialize Event (Event Slot ID: 4, Event ID: 450, Parameters: 6719){, 3, 2450, 620}
            // Initialize Event (Event Slot ID: 0, Event ID: 460, Parameters: 6710){, 2470, 600}

            PARAM.Row baseGood = game.Params["EquipParamGoods"][2470];
            int       baseId   = 6405;

            FMG itemName   = game.ItemFMGs["アイテム名"];
            FMG itemDesc   = game.ItemFMGs["アイテム説明"];
            FMG weaponName = game.ItemFMGs["武器名"];
            FMG weaponDesc = game.ItemFMGs["武器説明"];
            SortedDictionary <ItemKey, string> gameNames = game.Names();

            bool          explain       = false;
            HashSet <int> copiedWeapons = new HashSet <int>();

            foreach (PARAM.Row r in game.Params["SkillParam"].Rows)
            {
                int skillId = (int)r.ID;
                if (skillId >= 700)
                {
                    continue;
                }

                int text = (byte)r["Unk7"].Value;
                if (!texts.ContainsKey(text))
                {
                    continue;
                }

                int descItem = (int)r["SkilLDescriptionId"].Value;
                if (copiedWeapons.Contains(descItem))
                {
                    continue;
                }
                copiedWeapons.Add(descItem);

                PARAM.Row weaponRow = game.Params["EquipParamWeapon"][descItem];
                int       sortId    = (int)weaponRow["sortId"].Value;
                short     iconId    = (short)weaponRow["iconId"].Value;

                int       good    = baseId++;
                PARAM.Row newGood = game.AddRow("EquipParamGoods", good);
                GameEditor.CopyRow(baseGood, newGood);
                ItemKey goodKey = new ItemKey(ItemType.GOOD, good);
                newSkills[skillId] = goodKey;

                gameNames[goodKey] = weaponName[descItem];
                itemName[good]     = weaponName[descItem];
                itemDesc[good]     = weaponDesc[descItem];

                newGood["sortId"].Value = sortId;
                newGood["iconId"].Value = iconId;
                // These should be set in base row, but do this just in case
                // Don't show up in inventory
                newGood["goodsType"].Value = (byte)7;
                // But pop up on acquisition
                newGood["Unk20"].Value = (byte)6;

                if (explain)
                {
                    Console.WriteLine($"-- {r.ID} -> {good}: {descItem}, {weaponName[descItem]}");
                }
                ret.Assign[new ItemKey(ItemType.WEAPON, descItem)] = goodKey;
            }
            game.Params["EquipParamGoods"].Rows.Sort((a, b) => a.ID.CompareTo(b.ID));

            // Second, add event scripting to grant skills, with new common_func
            EMEVD common  = game.Emevds["common"];
            int   grantId = 11615600;

            EMEVD.Event   grantEv   = new EMEVD.Event(grantId);
            List <string> grantCmds = new List <string> {
                "IF Player Has/Doesn't Have Item (0,3,X0_4,1)", "Grant Skill (X4_4)"
            };

            for (int i = 0; i < grantCmds.Count; i++)
            {
                (EMEVD.Instruction ins, List <EMEVD.Parameter> ps) = events.ParseAddArg(grantCmds[i], i);
                grantEv.Instructions.Add(ins);
                grantEv.Parameters.AddRange(ps);
            }
            common.Events.Add(grantEv);

            int slot = 0;

            foreach (KeyValuePair <int, ItemKey> entry in newSkills)
            {
                common.Events[0].Instructions.Add(new EMEVD.Instruction(2000, 0, new List <object> {
                    slot++, grantId, entry.Value.ID, entry.Key
                }));
            }

            // Third, edit drops
            // Remove text drops
            ann.ItemGroups["remove"].AddRange(texts.Values);

            // Add skill drops
            foreach (ItemKey item in newSkills.Values)
            {
                data.AddLocationlessItem(item);
            }

            // Copy restrictions from the weapons over to the goods
            ann.CopyRestrictions(ret.Assign);
            foreach (KeyValuePair <ItemKey, ItemKey> entry in ret.Assign)
            {
                ItemKey weapon = entry.Key;
                ItemKey good   = entry.Value;
                // Mikiri Counter in hint log
                if (weapon.ID == 200300)
                {
                    ann.ItemGroups["upgradehints"].Add(good);
                }
                // Carp scalesmen
                if (!ann.ExcludeTags.ContainsKey(weapon))
                {
                    ann.ItemGroups["premium"].Add(good);
                }
            }

            // For balancing Dancing Dragon Mask, greatly reduce enemy xp drops
            // All NG bosses together give 93k XP. This gives enough for 45 skill points.
            // So allow 15 levels/3 AP upgrades, or 9 XP. (Next threshhold: 4 AP upgrades, 13k XP)
            // (Or not, since only got 9 levels in a full run, just double it.)
            foreach (PARAM.Row row in game.Params["NpcParam"].Rows)
            {
                row["Experience"].Value = (int)row["Experience"].Value / 5;
            }
            foreach (PARAM.Row row in game.Params["GameAreaParam"].Rows)
            {
                row["BonusExperience"].Value = (int)row["BonusExperience"].Value / 5;
            }

            // Also in this mode, acquire skills option is removed from Sculptor's Idols, in case it has been there from previous runs. Done in PermutationWriter.

            return(ret);
        }
Beispiel #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);
        }
Beispiel #3
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);
        }
Beispiel #4
0
        public Result FindItems(RandomizerOptions opt, AnnotationData ann, Graph g, Events events, string gameDir, FromGame game)
        {
            Dictionary <string, string>         itemsById = ann.KeyItems.ToDictionary(item => item.ID, item => item.Name);
            Dictionary <string, List <string> > itemAreas = g.ItemAreas;

            if (ann.LotLocations != null)
            {
                GameEditor editor = new GameEditor(game);
                editor.Spec.GameDir = gameDir;
                Dictionary <string, PARAM.Layout> layouts = editor.LoadLayouts();
                Dictionary <string, PARAM>        Params  = editor.LoadParams(layouts);

                Dictionary <int, PARAM.Row> lots = Params["ItemLotParam"].Rows.ToDictionary(r => (int)r.ID, r => r);
                foreach (KeyValuePair <int, string> entry in ann.LotLocations)
                {
                    int lot = entry.Key;
                    if (!g.Areas.ContainsKey(entry.Value))
                    {
                        throw new Exception($"Internal error in lot config for {entry.Key}: {entry.Value} does not exist");
                    }
                    while (true)
                    {
                        // It's also fine to not have a lot defined as long as all key items are found
                        if (!lots.TryGetValue(lot, out PARAM.Row row))
                        {
                            break;
                        }
                        for (int i = 1; i <= 8; i++)
                        {
                            int item = (int)row[$"lotItemId0{i}"].Value;
                            if (item == 0)
                            {
                                continue;
                            }
                            int category = (int)row[$"lotItemCategory0{i}"].Value;
                            category = Universe.LotTypes.TryGetValue((uint)category, out int value) ? value : -1;
                            if (category == -1)
                            {
                                continue;
                            }
                            string id = $"{category}:{item}";
                            if (opt["debuglots"])
                            {
                                Console.WriteLine($"{entry.Key} in {entry.Value} has {id}");
                            }
                            if (itemsById.TryGetValue(id, out string name))
                            {
                                if (itemAreas[name].Count > 0 && itemAreas[name][0] != entry.Value)
                                {
                                    throw new Exception($"Item {name} found in both {itemAreas[name][0]} and {entry.Value}");
                                }
                                itemAreas[name] = new List <string> {
                                    entry.Value
                                };
                            }
                        }
                        lot++;
                    }
                }
            }
            if (ann.Locations != null)
            {
                // It's a bit hacky, but should work from anywhere, probably
                GameEditor editor = new GameEditor(game);
                editor.Spec.GameDir   = $@"fogdist";
                editor.Spec.LayoutDir = $@"fogdist\Layouts";
                editor.Spec.NameDir   = $@"fogdist\Names";

                Dictionary <string, PARAM.Layout> layouts = editor.LoadLayouts();

                int dragonFlag = -1;
                Dictionary <string, PARAM> Params = editor.LoadParams(@"fogdist\Base\Data0.bdt", layouts, true);
                if (gameDir != null)
                {
                    string paramPath = $@"{gameDir}\Data0.bdt";
                    if (File.Exists(paramPath))
                    {
                        Params = editor.LoadParams(paramPath, layouts, true);
                    }

                    string commonEmevdPath = $@"{gameDir}\event\common.emevd.dcx";
                    if (File.Exists(commonEmevdPath))
                    {
                        EMEVD       commonEmevd = EMEVD.Read(commonEmevdPath);
                        EMEVD.Event flagEvent   = commonEmevd.Events.Find(e => e.ID == 13000904);
                        if (flagEvent != null)
                        {
                            Events.Instr check = events.Parse(flagEvent.Instructions[1]);
                            dragonFlag = (int)check[3];
                            if (opt["debuglots"])
                            {
                                Console.WriteLine($"Dragon flag: {dragonFlag}");
                            }
                        }
                    }
                }

                Dictionary <int, PARAM.Row> lots  = Params["ItemLotParam"].Rows.ToDictionary(r => (int)r.ID, r => r);
                Dictionary <int, PARAM.Row> shops = Params["ShopLineupParam"].Rows.ToDictionary(r => (int)r.ID, r => r);

                void setArea(string itemName, List <string> areas)
                {
                    if (opt["debuglots"])
                    {
                        Console.WriteLine($"-- name: {itemName}");
                    }
                    if (itemAreas[itemName].Count > 0 && !itemAreas[itemName].SequenceEqual(areas))
                    {
                        throw new Exception($"Item {itemName} found in both {string.Join(",", itemAreas[itemName])} and {string.Join(",", areas)}");
                    }
                    itemAreas[itemName] = areas;
                }

                foreach (KeyItemLoc loc in ann.Locations.Items)
                {
                    List <string> areas = loc.Area.Split(' ').ToList();
                    if (!areas.All(a => g.Areas.ContainsKey(a) || itemAreas.ContainsKey(a)))
                    {
                        // Currently happens with multi-area intersection lots/shops
                        throw new Exception($"Warning: Areas not found for {loc.Area} - {loc.DebugText[0]}");
                    }
                    List <int> lotIds = loc.Lots == null ? new List <int>() : loc.Lots.Split(' ').Select(i => int.Parse(i)).ToList();
                    foreach (int baseLot in lotIds)
                    {
                        int lot = baseLot;
                        while (true)
                        {
                            // It's also fine to not have a lot defined as long as all key items are found
                            if (!lots.TryGetValue(lot, out PARAM.Row row))
                            {
                                break;
                            }
                            for (int i = 1; i <= 8; i++)
                            {
                                int item = (int)row[$"ItemLotId{i}"].Value;
                                if (item == 0)
                                {
                                    continue;
                                }
                                uint category = (uint)row[$"LotItemCategory0{i}"].Value;
                                if (!Universe.LotTypes.TryGetValue(category, out int catVal))
                                {
                                    continue;
                                }
                                string id = $"{catVal}:{item}";
                                if (opt["debuglots"])
                                {
                                    Console.WriteLine($"lot {lot} in {loc.Area} has {id}");
                                }
                                if (itemsById.TryGetValue(id, out string name))
                                {
                                    setArea(name, areas);
                                }
                            }
                            if (dragonFlag > 0 && (int)row["getItemFlagId"].Value == dragonFlag)
                            {
                                setArea("pathofthedragon", areas);
                            }
                            lot++;
                        }
                    }
                    List <int> shopIds = loc.Shops == null ? new List <int>() : loc.Shops.Split(' ').Select(i => int.Parse(i)).ToList();
                    foreach (int shopId in shopIds)
                    {
                        // Not as fine for a shop to be missing, but also whatever
                        if (!shops.TryGetValue(shopId, out PARAM.Row row))
                        {
                            continue;
                        }
                        int    item   = (int)row["EquipId"].Value;
                        int    catVal = (byte)row["equipType"].Value;
                        string id     = $"{catVal}:{item}";
                        if (opt["debuglots"])
                        {
                            Console.WriteLine($"shop {shopId} in {loc.Area} has {id}");
                        }
                        if (itemsById.TryGetValue(id, out string name))
                        {
                            setArea(name, areas);
                        }
                        if (dragonFlag > 0 && (int)row["EventFlag"].Value == dragonFlag)
                        {
                            setArea("pathofthedragon", areas);
                        }
                    }
                }
            }
            // lots:.*[1-9]\r
            // Iterative approach for items which depend simply on other items
            // Recursion would look a lot nicer but lazy
            bool itemExpanded;

            do
            {
                itemExpanded = false;
                foreach (KeyValuePair <string, List <string> > entry in itemAreas)
                {
                    foreach (string dep in entry.Value.ToList())
                    {
                        if (itemAreas.TryGetValue(dep, out List <string> deps))
                        {
                            entry.Value.Remove(dep);
                            entry.Value.AddRange(deps);
                            itemExpanded = true;
                        }
                    }
                }
            }while (itemExpanded);

            if (opt["explain"] || opt["debuglots"])
            {
                foreach (Item item in ann.KeyItems)
                {
                    Console.WriteLine($"{item.Name} {item.ID}: default {item.Area}, found [{string.Join(", ", itemAreas[item.Name])}]");
                }
            }
            // Collect items in graph
            SortedSet <string> itemRecord = new SortedSet <string>();
            bool randomized = false;

            foreach (Item item in ann.KeyItems)
            {
                if (itemAreas[item.Name].Count == 0)
                {
                    if (item.HasTag("randomonly"))
                    {
                        itemAreas[item.Name] = new List <string> {
                            item.Area
                        };
                    }
                    else if (item.HasTag("hard") && !opt["hard"])
                    {
                        continue;
                    }
                    else
                    {
                        throw new Exception($"Couldn't find {item.Name} in item lots");
                    }
                }
                List <string> areas = itemAreas[item.Name];
                foreach (string area in areas)
                {
                    g.Nodes[area].Items.Add(item.Name);
                }
                if (!item.HasTag("randomonly"))
                {
                    if (areas.Count > 1 || areas[0] != item.Area)
                    {
                        randomized = true;
                    }
                    itemRecord.Add($"{item.Name}={string.Join(",", areas)}");
                }
            }
            return(new Result
            {
                Randomized = randomized,
                ItemHash = (RandomizerOptions.JavaStringHash($"{string.Join(";", itemRecord)}") % 99999).ToString().PadLeft(5, '0')
            });
        }