public bool CheckEnemySpriteFitInBank(List<EnemyType> currentSprites, EnemyType spriteToAdd) { List<int> currentRows = new List<int>(); List<int> currentAddresses = new List<int>(); foreach (EnemyType e in currentSprites) { // Return false if enemy is already in the list if (spriteToAdd.ID == e.ID) { return false; } // Return false if the room restricts changing // Add the candidate enemy's sprite bank rows and pattern table addresses to their owns lists for (int i = 0; i < e.SpriteBankRows.Count; i++) { currentRows.Add(e.SpriteBankRows[i]); } for (int i = 0; i < e.PatternTableAddresses.Count; i++) { currentAddresses.Add(e.PatternTableAddresses[i]); } } for (int i = 0; i < currentRows.Count; i++) { for (int j = 0; j < spriteToAdd.SpriteBankRows.Count; j++) { if (currentRows[i] == spriteToAdd.SpriteBankRows[j]) { if (currentAddresses[i * 2] == spriteToAdd.PatternTableAddresses[j * 2] && currentAddresses[i * 2 + 1] == spriteToAdd.PatternTableAddresses[j * 2 + 1]) { // This enemy contains the same pattern table address as the one in the list, add it } else { return false; } } } } return true; }
private void Execute(Patch Patch, Random r) { foreach (SpriteBankRoomGroup sbrg in RoomGroups) { // Skip processing the room if every sprite bank row is taken if (sbrg.IsSpriteRestricted && sbrg.SpriteBankRowsRestriction.Count >= 6) { continue; } // Create valid random combination of enemies to place List <EnemyType> newEnemies = GenerateEnemyCombinations(sbrg, r); // No enemy can fit in this room for some reason, skip this room (GFX will be glitched) if (newEnemies.Count == 0) { continue; } // For each enemy ID (in the room, in the room-group), change to a random enemy from the new set for (int i = 0; i < sbrg.Rooms.Count; i++) { Room room = sbrg.Rooms[i]; for (int j = 0; j < room.EnemyInstances.Count; j++) { EnemyInstance instance = room.EnemyInstances[j]; int randomIndex = r.Next(newEnemies.Count); EnemyType newEnemyType = newEnemies[randomIndex]; byte newId = (byte)newEnemyType.ID; // When placing the last enemy, If room contains an activator, manually change the last spawn in the room to be its deactivator if (j == room.EnemyInstances.Count - 1) { EEnemyID?activator = room.GetActivatorIfOneHasBeenAdded(); if (activator != null) { newId = (byte)EnemyType.GetCorrespondingDeactivator((EEnemyID)activator); } // Also, if this last instance is an activator, try to replace it if (EnemyType.CheckIsActivator(newId)) { newId = TryReplaceActivator(newEnemies, newId); // Update the new enemy type because it may require different graphics if (!EnemyType.CheckIsDeactivator(newId)) { newEnemyType = newEnemies.Where(x => (byte)x.ID == newId).First(); } } } sbrg.NewEnemyTypes.Add(newEnemyType); // TODO: This all should be refactored. Use a hashtable of EnemyTypes and abolish "EnemyID". // If room contains only this one enemy and it is an activator // TODO: How does Clash stage work with the Pipis? They don't break normally. if ((room.EnemyInstances.Count == 1 && instance.HasNewActivator())) { // Try to replace it with a non-activator enemy //newId = TryReplaceActivator(newEnemies, newId); } // Last-minute adjustments to certain enemy spawns switch ((EEnemyID)newId) { case EEnemyID.Shrink: double randomSpawner = r.NextDouble(); if (randomSpawner < CHANCE_SHRINKSPAWNER) { newId = (byte)EEnemyID.Shrink_Spawner; } break; case EEnemyID.Shotman_Left: if (instance.IsFaceRight) { newId = (byte)EEnemyID.Shotman_Right; } break; default: break; } // Update object with new ID for future use room.EnemyInstances[j].EnemyID = newId; // Change the enemy ID in the ROM int IDposition = Stage0EnemyIDAddress + instance.StageNum * StageLength + instance.Offset; Patch.Add(IDposition, newId, $"{sbrg.Stage.ToString("G")} Stage Enemy #{instance.Offset} ID (Room {instance.RoomNum}) {((EEnemyID)instance.EnemyID).ToString("G")}"); // Change the enemy Y pos based on Air or Ground category int newY = newEnemyType.YAdjust; newY += (newEnemyType.IsYPosAir) ? instance.YAir : instance.YGround; IDposition = Stage0EnemyYAddress + instance.StageNum * StageLength + instance.Offset; Patch.Add(IDposition, (byte)newY, $"{sbrg.Stage.ToString("G")} Stage Enemy #{instance.Offset} Y (Room {instance.RoomNum}) {((EEnemyID)instance.EnemyID).ToString("G")}"); } } // Change sprite banks for the room foreach (EnemyType e in sbrg.NewEnemyTypes) { for (int i = 0; i < e.SpriteBankRows.Count; i++) { int rowInSlotAddress = sbrg.PatternAddressStart + e.SpriteBankRows[i] * 2; int patternTblPtr1 = e.PatternTableAddresses[2 * i]; int patternTblPtr2 = e.PatternTableAddresses[2 * i + 1]; Patch.Add(rowInSlotAddress, (byte)patternTblPtr1, $"{sbrg.Stage.ToString("G")} Stage Sprite Bank Slot ? Row {e.SpriteBankRows[i]} Indirect Address 1"); Patch.Add(rowInSlotAddress + 1, (byte)patternTblPtr2, $"{sbrg.Stage.ToString("G")} Stage Sprite Bank Slot ? Row {e.SpriteBankRows[i]} Indirect Address 2"); } } } // end foreach sbrg }
private List <EnemyType> GenerateEnemyCombinations(SpriteBankRoomGroup sbrg, Random r) { // Create a random enemy set List <EnemyType> NewEnemies = new List <EnemyType>(); List <EnemyType> PotentialEnemies = new List <EnemyType>(); bool done = false; bool hasActivator = false; while (!done) { foreach (EnemyType en in EnemyTypes) { // 1. Skip enemies that have exceeded the type's maximum // 2. Reduce the overall chance of certain types appearing by randomly skipping them // 3. Limit a room set to having at most one Activator enemy type double chance = 0.0; switch (en.ID) { case EEnemyID.Pipi_Activator: if (numPipis >= MAX_PIPIS) { continue; } chance = r.NextDouble(); if (chance > CHANCE_PIPI) { continue; } break; case EEnemyID.Mole_Activator: if (numMoles >= MAX_MOLES) { continue; } chance = r.NextDouble(); if (chance > CHANCE_MOLE) { continue; } break; case EEnemyID.M445_Activator: if (numM445s > MAX_M445S) { continue; } chance = r.NextDouble(); if (chance > CHANCE_M445) { continue; } break; case EEnemyID.Telly: chance = r.NextDouble(); if (chance > CHANCE_TELLY) { continue; } break; case EEnemyID.Springer: chance = r.NextDouble(); if (chance > CHANCE_SPRINGER) { continue; } break; default: break; } // Skip any additional activator enemies if (en.IsActivator && hasActivator) { continue; } // Reject certain enemy types for certain stages or rooms switch (sbrg.Stage) { case EStageID.HeatW1: // Moles don't display correctly in Heat or Wily 1. Also too annoying in Heat Yoku room. if (en.ID == EEnemyID.Mole_Activator) { continue; } // Reject Pipis appearing in Yoku block room if (en.ID == EEnemyID.Pipi_Activator && sbrg.ContainsRoom(2)) { continue; } // Reject M445s appearing in Yoku block room if (en.ID == EEnemyID.M445_Activator && sbrg.ContainsRoom(2)) { continue; } // Press doesn't display correctly in Wily 1 if (en.ID == EEnemyID.Press && sbrg.Rooms.Last().RoomNum >= 7) { continue; } break; case EStageID.AirW2: // Moles don't display correctly in Heat if (en.ID == EEnemyID.Mole_Activator && sbrg.Rooms[0].RoomNum < 7) { continue; } break; case EStageID.WoodW3: // Moles and Press don't display in Wood outside room if (en.ID == EEnemyID.Mole_Activator && sbrg.ContainsRoom(7)) { continue; } if (en.ID == EEnemyID.Press && sbrg.ContainsRoom(7)) { continue; } // Don't spawn Springer, Blocky, or Press underwater if (en.ID == EEnemyID.Springer && sbrg.ContainsRoom(0x11)) { continue; } if (en.ID == EEnemyID.Blocky && sbrg.ContainsRoom(0x11)) { continue; } if (en.ID == EEnemyID.Press && sbrg.ContainsRoom(0x11)) { continue; } break; case EStageID.BubbleW4: // Moles don't display correctly in Bubble if (en.ID == EEnemyID.Mole_Activator && sbrg.Rooms[0].RoomNum < 9) { continue; } // Press doesn't display correctly in Bubble if (en.ID == EEnemyID.Press && sbrg.Rooms[0].RoomNum < 9) { continue; } // Don't spawn Springer or Blocky underwater if (en.ID == EEnemyID.Springer && (sbrg.ContainsRoom(3) || sbrg.ContainsRoom(4))) { continue; } if (en.ID == EEnemyID.Blocky && (sbrg.ContainsRoom(3) || sbrg.ContainsRoom(4))) { continue; } break; case EStageID.Clash: // Mole bad GFX if (en.ID == EEnemyID.Mole_Activator) { continue; } // Press bad GFX if (en.ID == EEnemyID.Press) { continue; } break; default: break; } // If room has sprite restrictions, check if this enemy's sprite can be used // (i.e. certain rooms must use certain rows on the sprite table to draw mandatory objects or effects if (sbrg.IsSpriteRestricted) { // Check if this enemy uses the restricted row in the sprite bank List <int> commonRows = en.SpriteBankRows.Intersect(sbrg.SpriteBankRowsRestriction).ToList(); if (commonRows.Count != 0) { bool reject = false; for (int i = 0; i < en.SpriteBankRows.Count; i++) { int enemyRow = en.SpriteBankRows[i]; int indexOfRow = sbrg.SpriteBankRowsRestriction.IndexOf(enemyRow); // For a restricted sprite bank row, see if enemy uses the same sprite pattern if (indexOfRow > -1) { if (en.PatternTableAddresses[i * 2] == sbrg.PatternTableAddressesRestriction[indexOfRow * 2] && en.PatternTableAddresses[i * 2 + 1] == sbrg.PatternTableAddressesRestriction[indexOfRow * 2 + 1]) { // Enemy and the restricted sprite use the same pattern table, allow it // (do nothing) } else { // Enemy draws with this row, but using a different set of graphics. reject it. reject = true; break; } } } if (reject) { continue; } } } // Check if this enemy would fit in the sprite bank, given other new enemies already added if (CheckEnemySpriteFitInBank(NewEnemies, en)) { // Add enemy to set of possible enemies to place in PotentialEnemies.Add(en); } } // Unable to add any more enemies, done if (PotentialEnemies.Count == 0) { done = true; } else { // Choose a new enemy to add to the set from all possible new enemies to add EnemyType newEnemy = PotentialEnemies[r.Next(PotentialEnemies.Count)]; NewEnemies.Add(newEnemy); PotentialEnemies.Clear(); // Increase total count of certain enemy types to limit their appearance later switch (newEnemy.ID) { case EEnemyID.Pipi_Activator: numPipis++; break; case EEnemyID.Mole_Activator: numMoles++; break; case EEnemyID.M445_Activator: numM445s++; break; default: break; } // Flag the new enemy set as having an activator so that no more will be added if (newEnemy.IsActivator) { hasActivator = true; } } } return(NewEnemies); }