forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
781 lines
36 KiB
Python
781 lines
36 KiB
Python
"""Apply Boss Locations."""
|
|
|
|
from randomizer.Enums.EnemySubtypes import EnemySubtype
|
|
from randomizer.Enums.Settings import CrownEnemyDifficulty, DamageAmount, WinConditionComplex
|
|
from randomizer.Lists.EnemyTypes import EnemyMetaData, enemy_location_list
|
|
from randomizer.Enums.Enemies import Enemies
|
|
from randomizer.Enums.Locations import Locations
|
|
from randomizer.Enums.Maps import Maps
|
|
from randomizer.Patching.Patcher import LocalROM
|
|
from randomizer.Patching.Library.Assets import getPointerLocation, TableNames
|
|
|
|
|
|
class PkmnSnapEnemy:
|
|
"""Class which determines if an enemy is available for the pkmn snap goal."""
|
|
|
|
def __init__(self, enemy):
|
|
"""Initialize with given parameters."""
|
|
self.enemy = enemy
|
|
if enemy in (
|
|
Enemies.KasplatDK,
|
|
Enemies.KasplatDiddy,
|
|
Enemies.KasplatLanky,
|
|
Enemies.KasplatTiny,
|
|
Enemies.KasplatChunky,
|
|
Enemies.Book,
|
|
Enemies.EvilTomato,
|
|
):
|
|
# Always spawned, not in pool
|
|
self.spawned = True
|
|
else:
|
|
self.spawned = False
|
|
self.default = self.spawned
|
|
|
|
def addEnemy(self):
|
|
"""Add enemy as spawned."""
|
|
self.spawned = True
|
|
|
|
def reset(self):
|
|
"""Reset enemy to default state."""
|
|
self.spawned = self.default
|
|
|
|
|
|
class Spawner:
|
|
"""Class which stores information pertaining to a spawner."""
|
|
|
|
def __init__(self, enemy_id: int, offset: int, index: int):
|
|
"""Initialize with given parameters."""
|
|
self.enemy_id = enemy_id
|
|
self.offset = offset
|
|
self.index = index
|
|
|
|
|
|
pkmn_snap_enemies = [
|
|
PkmnSnapEnemy(Enemies.Kaboom),
|
|
PkmnSnapEnemy(Enemies.BeaverBlue),
|
|
PkmnSnapEnemy(Enemies.Book),
|
|
PkmnSnapEnemy(Enemies.Klobber),
|
|
PkmnSnapEnemy(Enemies.ZingerCharger),
|
|
PkmnSnapEnemy(Enemies.Klump),
|
|
PkmnSnapEnemy(Enemies.KlaptrapGreen),
|
|
PkmnSnapEnemy(Enemies.ZingerLime),
|
|
PkmnSnapEnemy(Enemies.KlaptrapPurple),
|
|
PkmnSnapEnemy(Enemies.KlaptrapRed),
|
|
PkmnSnapEnemy(Enemies.BeaverGold),
|
|
PkmnSnapEnemy(Enemies.MushroomMan),
|
|
PkmnSnapEnemy(Enemies.Ruler),
|
|
PkmnSnapEnemy(Enemies.RoboKremling),
|
|
PkmnSnapEnemy(Enemies.Kremling),
|
|
PkmnSnapEnemy(Enemies.KasplatDK),
|
|
PkmnSnapEnemy(Enemies.KasplatDiddy),
|
|
PkmnSnapEnemy(Enemies.KasplatLanky),
|
|
PkmnSnapEnemy(Enemies.KasplatTiny),
|
|
PkmnSnapEnemy(Enemies.KasplatChunky),
|
|
PkmnSnapEnemy(Enemies.Guard),
|
|
PkmnSnapEnemy(Enemies.ZingerRobo),
|
|
PkmnSnapEnemy(Enemies.Krossbones),
|
|
PkmnSnapEnemy(Enemies.Shuri),
|
|
PkmnSnapEnemy(Enemies.Gimpfish),
|
|
PkmnSnapEnemy(Enemies.MrDice0),
|
|
PkmnSnapEnemy(Enemies.SirDomino),
|
|
PkmnSnapEnemy(Enemies.MrDice1),
|
|
PkmnSnapEnemy(Enemies.FireballGlasses),
|
|
PkmnSnapEnemy(Enemies.SpiderSmall),
|
|
PkmnSnapEnemy(Enemies.Bat),
|
|
PkmnSnapEnemy(Enemies.EvilTomato),
|
|
PkmnSnapEnemy(Enemies.Ghost),
|
|
PkmnSnapEnemy(Enemies.Pufftup),
|
|
PkmnSnapEnemy(Enemies.Kosha),
|
|
PkmnSnapEnemy(Enemies.Bug),
|
|
PkmnSnapEnemy(Enemies.ZingerFlamethrower),
|
|
PkmnSnapEnemy(Enemies.Scarab),
|
|
]
|
|
|
|
valid_maps = [
|
|
Maps.JapesMountain,
|
|
Maps.JungleJapes,
|
|
Maps.JapesTinyHive,
|
|
Maps.JapesLankyCave,
|
|
Maps.AztecTinyTemple,
|
|
Maps.HideoutHelm,
|
|
Maps.AztecDonkey5DTemple,
|
|
Maps.AztecDiddy5DTemple,
|
|
Maps.AztecLanky5DTemple,
|
|
Maps.AztecTiny5DTemple,
|
|
Maps.AztecChunky5DTemple,
|
|
Maps.AztecLlamaTemple,
|
|
Maps.FranticFactory,
|
|
Maps.FactoryPowerHut,
|
|
Maps.GloomyGalleon,
|
|
Maps.GalleonSickBay,
|
|
Maps.JapesUnderGround,
|
|
Maps.Isles,
|
|
Maps.FactoryCrusher,
|
|
Maps.AngryAztec,
|
|
Maps.GalleonSealRace,
|
|
Maps.JapesBaboonBlast,
|
|
Maps.AztecBaboonBlast,
|
|
Maps.Galleon2DShip,
|
|
Maps.Galleon5DShipDiddyLankyChunky,
|
|
Maps.Galleon5DShipDKTiny,
|
|
Maps.GalleonTreasureChest,
|
|
Maps.GalleonMermaidRoom,
|
|
Maps.FungiForest,
|
|
Maps.GalleonLighthouse,
|
|
Maps.GalleonMechafish,
|
|
Maps.ForestAnthill,
|
|
Maps.GalleonBaboonBlast,
|
|
Maps.ForestMinecarts,
|
|
Maps.ForestMillAttic,
|
|
Maps.ForestRafters,
|
|
Maps.ForestMillAttic,
|
|
Maps.ForestThornvineBarn,
|
|
# Maps.ForestSpider, # Causes a lot of enemies to fall into the pit of their own volition
|
|
Maps.ForestMillFront,
|
|
Maps.ForestMillBack,
|
|
Maps.ForestLankyMushroomsRoom,
|
|
Maps.CrystalCaves,
|
|
Maps.CavesDonkeyIgloo,
|
|
Maps.CavesDiddyIgloo,
|
|
Maps.CavesLankyIgloo,
|
|
Maps.CavesTinyIgloo,
|
|
# Maps.CavesChunkyIgloo, # Fireball with glasses is here
|
|
Maps.CavesDonkeyCabin,
|
|
Maps.CavesDiddyLowerCabin,
|
|
Maps.CavesDiddyUpperCabin,
|
|
Maps.CavesLankyCabin,
|
|
Maps.CavesTinyCabin,
|
|
Maps.CavesChunkyCabin,
|
|
Maps.CreepyCastle,
|
|
Maps.CastleBallroom,
|
|
Maps.CavesRotatingCabin,
|
|
Maps.CavesFrozenCastle,
|
|
Maps.CastleCrypt,
|
|
Maps.CastleMausoleum,
|
|
Maps.CastleUpperCave,
|
|
Maps.CastleLowerCave,
|
|
Maps.CastleTower,
|
|
Maps.CastleMinecarts,
|
|
Maps.FactoryBaboonBlast,
|
|
Maps.CastleMuseum,
|
|
Maps.CastleLibrary,
|
|
Maps.CastleDungeon,
|
|
Maps.CastleTree,
|
|
Maps.CastleShed,
|
|
Maps.CastleTrashCan,
|
|
Maps.JungleJapesLobby,
|
|
Maps.AngryAztecLobby,
|
|
Maps.FranticFactoryLobby,
|
|
Maps.GloomyGalleonLobby,
|
|
Maps.FungiForestLobby,
|
|
Maps.CrystalCavesLobby,
|
|
Maps.CreepyCastleLobby,
|
|
Maps.HideoutHelmLobby,
|
|
Maps.GalleonSubmarine,
|
|
Maps.CavesBaboonBlast,
|
|
Maps.CastleBaboonBlast,
|
|
Maps.ForestBaboonBlast,
|
|
Maps.IslesSnideRoom,
|
|
Maps.ForestGiantMushroom,
|
|
Maps.ForestLankyZingersRoom,
|
|
Maps.CastleBoss,
|
|
]
|
|
crown_maps = [
|
|
Maps.JapesCrown,
|
|
Maps.AztecCrown,
|
|
Maps.FactoryCrown,
|
|
Maps.GalleonCrown,
|
|
Maps.ForestCrown,
|
|
Maps.CavesCrown,
|
|
Maps.CastleCrown,
|
|
Maps.HelmCrown,
|
|
Maps.SnidesCrown,
|
|
Maps.LobbyCrown,
|
|
]
|
|
minigame_maps_easy = [
|
|
# Maps.BusyBarrelBarrageEasy,
|
|
# Maps.BusyBarrelBarrageHard,
|
|
# Maps.BusyBarrelBarrageNormal,
|
|
# Maps.HelmBarrelDiddyKremling, # Only kremlings activate the switch
|
|
Maps.HelmBarrelChunkyHidden,
|
|
Maps.HelmBarrelChunkyShooting,
|
|
]
|
|
minigame_maps_beatable = [Maps.MadMazeMaulEasy, Maps.MadMazeMaulNormal, Maps.MadMazeMaulHard, Maps.MadMazeMaulInsane]
|
|
minigame_maps_nolimit = [
|
|
Maps.HelmBarrelLankyMaze,
|
|
Maps.StashSnatchEasy,
|
|
Maps.StashSnatchNormal,
|
|
Maps.StashSnatchHard,
|
|
Maps.StashSnatchInsane,
|
|
]
|
|
minigame_maps_beavers = [Maps.BeaverBotherEasy, Maps.BeaverBotherNormal, Maps.BeaverBotherHard]
|
|
minigame_maps_total = minigame_maps_easy.copy()
|
|
minigame_maps_total.extend(minigame_maps_beatable)
|
|
minigame_maps_total.extend(minigame_maps_nolimit)
|
|
minigame_maps_total.extend(minigame_maps_beavers)
|
|
bbbarrage_maps = (Maps.BusyBarrelBarrageEasy, Maps.BusyBarrelBarrageNormal, Maps.BusyBarrelBarrageHard)
|
|
banned_speed_maps = list(bbbarrage_maps).copy() + minigame_maps_beavers.copy()
|
|
banned_size_maps = list(bbbarrage_maps).copy() + minigame_maps_beavers.copy() + [Maps.ForestAnthill, Maps.CavesDiddyLowerCabin, Maps.CavesTinyCabin, Maps.HelmBarrelChunkyShooting]
|
|
replacement_priority = {
|
|
EnemySubtype.GroundSimple: [EnemySubtype.GroundBeefy, EnemySubtype.Water, EnemySubtype.Air],
|
|
EnemySubtype.GroundBeefy: [EnemySubtype.GroundSimple, EnemySubtype.Water, EnemySubtype.Air],
|
|
EnemySubtype.Water: [EnemySubtype.Air, EnemySubtype.GroundSimple, EnemySubtype.GroundBeefy],
|
|
EnemySubtype.Air: [EnemySubtype.GroundSimple, EnemySubtype.GroundBeefy, EnemySubtype.Water],
|
|
}
|
|
banned_enemy_maps = {
|
|
Enemies.Book: [Maps.CavesDonkeyCabin, Maps.JapesLankyCave, Maps.AngryAztecLobby],
|
|
Enemies.Kosha: [Maps.CavesDiddyLowerCabin, Maps.CavesTinyCabin],
|
|
Enemies.Guard: [Maps.CavesDiddyLowerCabin, Maps.CavesTinyIgloo, Maps.CavesTinyCabin],
|
|
}
|
|
ENABLE_BBBARRAGE_ENEMY_RANDO = False
|
|
|
|
|
|
def resetPkmnSnap():
|
|
"""Reset Pokemon Snap Listing."""
|
|
for enemy in pkmn_snap_enemies:
|
|
enemy.reset()
|
|
|
|
|
|
def setPkmnSnapEnemy(focused_enemy):
|
|
"""Set enemy to being spawned."""
|
|
for enemy in pkmn_snap_enemies:
|
|
if enemy.enemy == focused_enemy:
|
|
enemy.addEnemy()
|
|
|
|
|
|
MAP_DIFFICULTY_ORDER = (
|
|
Maps.JapesCrown,
|
|
Maps.AztecCrown,
|
|
Maps.FactoryCrown,
|
|
Maps.GalleonCrown,
|
|
Maps.ForestCrown,
|
|
Maps.CavesCrown,
|
|
Maps.CastleCrown,
|
|
Maps.HelmCrown,
|
|
Maps.SnidesCrown,
|
|
Maps.LobbyCrown,
|
|
)
|
|
|
|
|
|
def getCrownEnemyDifficultyFromMap(settings, map_id: Maps) -> CrownEnemyDifficulty:
|
|
"""Get the crown enemy difficulty for a map."""
|
|
if map_id not in MAP_DIFFICULTY_ORDER:
|
|
raise Exception("Suggested map is not in the difficulty mapping.")
|
|
placement_index = MAP_DIFFICULTY_ORDER.index(map_id)
|
|
return settings.crown_difficulties[placement_index]
|
|
|
|
|
|
def getCrownEnemyCount(map_id: Maps) -> int:
|
|
"""Get the amount of enemies in a crown map."""
|
|
if map_id in (Maps.GalleonCrown, Maps.LobbyCrown, Maps.HelmCrown):
|
|
return 4
|
|
return 3
|
|
|
|
|
|
ANNOYING_ENEMIES = (Enemies.Klump, Enemies.Kosha, Enemies.Klobber)
|
|
|
|
|
|
def getBalancedCrownEnemyRando(spoiler, crown_setting: CrownEnemyDifficulty, damage_ohko_setting):
|
|
"""Get array of weighted enemies."""
|
|
# this library will contain a list for every enemy it needs to generate
|
|
enemy_swaps_library = {}
|
|
|
|
if crown_setting == CrownEnemyDifficulty.vanilla:
|
|
return {}
|
|
# library of every crown map. will have a list of all enemies to put in those maps.
|
|
enemy_swaps_library = {
|
|
Maps.JapesCrown: [],
|
|
Maps.AztecCrown: [],
|
|
Maps.FactoryCrown: [],
|
|
Maps.GalleonCrown: [],
|
|
Maps.ForestCrown: [],
|
|
Maps.CavesCrown: [],
|
|
Maps.CastleCrown: [],
|
|
Maps.HelmCrown: [],
|
|
Maps.SnidesCrown: [],
|
|
Maps.LobbyCrown: [],
|
|
}
|
|
# make 5 lists of enemies, per category.
|
|
every_enemy = [] # every enemy (that can appear in crown battles)
|
|
disruptive_max_1 = [] # anything that isn't... "2" disruptive (because disruptive is 1, at most)
|
|
disruptive_at_most_kasplat = [] # anything that isn't marked as "disruptive"
|
|
disruptive_0 = [] # the easiest enemies
|
|
legacy_hard_mode = [] # legacy map with the exact same balance as the old "Hard" mode
|
|
|
|
# Determine whether any crown-enabled enemies have been selected
|
|
crown_enemy_found = False
|
|
for enemy in EnemyMetaData:
|
|
if enemy in spoiler.settings.enemies_selected:
|
|
if EnemyMetaData[enemy].crown_enabled:
|
|
if enemy is not Enemies.GetOut:
|
|
crown_enemy_found = True
|
|
break
|
|
# Determine whether only GetOut is the only selected enemy that can appear in crown battles
|
|
# If True, guarantees that there is 1 GetOut in every crown battle
|
|
oops_all_get_out = False
|
|
if crown_enemy_found is False:
|
|
if Enemies.GetOut in spoiler.settings.enemies_selected:
|
|
if damage_ohko_setting is False:
|
|
oops_all_get_out = True
|
|
# fill in the lists with the possibilities that belong in them.
|
|
for enemy in EnemyMetaData:
|
|
if EnemyMetaData[enemy].crown_enabled and enemy != Enemies.GetOut:
|
|
if enemy in spoiler.settings.enemies_selected or (not crown_enemy_found):
|
|
every_enemy.append(enemy)
|
|
if EnemyMetaData[enemy].disruptive <= 1:
|
|
disruptive_max_1.append(enemy)
|
|
if EnemyMetaData[enemy].kasplat is True:
|
|
disruptive_at_most_kasplat.append(enemy)
|
|
elif EnemyMetaData[enemy].disruptive == 0:
|
|
disruptive_at_most_kasplat.append(enemy)
|
|
disruptive_0.append(enemy)
|
|
# Make sure every list is populated, even if too few crown-enabled enemies have been selected
|
|
# This breaks the crown balancing, but what the player wants, the player gets
|
|
if len(disruptive_max_1) == 0:
|
|
disruptive_max_1.extend(every_enemy.copy())
|
|
for enemy in EnemyMetaData:
|
|
if EnemyMetaData[enemy].disruptive > 1:
|
|
EnemyMetaData[enemy].disruptive = 1
|
|
if len(disruptive_at_most_kasplat) == 0:
|
|
disruptive_at_most_kasplat.extend(disruptive_max_1.copy())
|
|
if len(disruptive_0) == 0:
|
|
disruptive_0.extend(disruptive_at_most_kasplat)
|
|
for enemy in EnemyMetaData:
|
|
if EnemyMetaData[enemy].disruptive > 0:
|
|
EnemyMetaData[enemy].disruptive = 0
|
|
# the legacy_hard_mode list is trickier to fill, but here goes:
|
|
bias = 2
|
|
for enemy in EnemyMetaData.keys():
|
|
if EnemyMetaData[enemy].crown_enabled:
|
|
if enemy in spoiler.settings.enemies_selected or crown_enemy_found is False:
|
|
base_weight = EnemyMetaData[enemy].crown_weight
|
|
weight_diff = abs(base_weight - bias)
|
|
new_weight = abs(10 - weight_diff)
|
|
if enemy == Enemies.GetOut:
|
|
new_weight = 1
|
|
if damage_ohko_setting is False or enemy is not Enemies.GetOut:
|
|
for count in range(new_weight):
|
|
legacy_hard_mode.append(enemy)
|
|
# picking enemies to put in the crown battles
|
|
for map_id in enemy_swaps_library:
|
|
difficulty = getCrownEnemyDifficultyFromMap(spoiler.settings, map_id)
|
|
if difficulty == CrownEnemyDifficulty.easy:
|
|
enemy_swaps_library[map_id].append(spoiler.settings.random.choice(disruptive_max_1))
|
|
if oops_all_get_out is True:
|
|
enemy_swaps_library[map_id].append(Enemies.GetOut)
|
|
else:
|
|
enemy_swaps_library[map_id].append(spoiler.settings.random.choice(disruptive_0))
|
|
enemy_swaps_library[map_id].append(spoiler.settings.random.choice(disruptive_0))
|
|
if map_id == Maps.GalleonCrown or map_id == Maps.LobbyCrown or map_id == Maps.HelmCrown:
|
|
enemy_swaps_library[map_id].append(spoiler.settings.random.choice(disruptive_0))
|
|
elif difficulty == CrownEnemyDifficulty.medium:
|
|
new_enemy = 0
|
|
count_disruptive = 0
|
|
count_kasplats = 0
|
|
number_of_enemies = 3
|
|
if map_id == Maps.GalleonCrown or map_id == Maps.LobbyCrown or map_id == Maps.HelmCrown:
|
|
number_of_enemies = 4
|
|
for count in range(number_of_enemies):
|
|
if count == 0 and oops_all_get_out is True:
|
|
new_enemy = Enemies.GetOut
|
|
elif count_disruptive == 0:
|
|
if count_kasplats < 2:
|
|
new_enemy = spoiler.settings.random.choice(every_enemy)
|
|
elif count_kasplats == 2:
|
|
new_enemy = spoiler.settings.random.choice(disruptive_max_1)
|
|
elif count_kasplats == 3:
|
|
new_enemy = spoiler.settings.random.choice(disruptive_0)
|
|
elif count_disruptive == 1:
|
|
if count_kasplats < 2:
|
|
new_enemy = spoiler.settings.random.choice(disruptive_max_1)
|
|
elif count_kasplats == 2:
|
|
new_enemy = spoiler.settings.random.choice(disruptive_0)
|
|
elif count_disruptive == 2:
|
|
if count_kasplats == 0:
|
|
new_enemy = spoiler.settings.random.choice(disruptive_at_most_kasplat)
|
|
elif count_kasplats == 1:
|
|
new_enemy = spoiler.settings.random.choice(disruptive_0)
|
|
if count_kasplats > 3 or (count_kasplats > 2 and count_disruptive > 1) or (count_kasplats == 2 and count_disruptive == 2):
|
|
print("This is a mistake in the crown enemy algorithm. Report this to the devs.")
|
|
new_enemy = Enemies.BeaverGold
|
|
# We picked a new enemy, let's update our information and add it to the list
|
|
if EnemyMetaData[new_enemy].kasplat is True:
|
|
count_kasplats = count_kasplats + 1
|
|
count_disruptive = EnemyMetaData[new_enemy].disruptive + count_disruptive
|
|
enemy_swaps_library[map_id].append(new_enemy)
|
|
elif difficulty == CrownEnemyDifficulty.hard:
|
|
number_of_enemies = getCrownEnemyCount(map_id)
|
|
get_out_spawned_this_hard_map = False
|
|
legacy_hard_mode_copy = legacy_hard_mode.copy()
|
|
for count in range(number_of_enemies):
|
|
if (not oops_all_get_out) and crown_setting == CrownEnemyDifficulty.progressive:
|
|
legacy_hard_mode_copy = [possible_enemy for possible_enemy in legacy_hard_mode_copy if possible_enemy != Enemies.GetOut]
|
|
if count == 0 and oops_all_get_out:
|
|
enemy_to_place = Enemies.GetOut
|
|
get_out_spawned_this_hard_map = True
|
|
else:
|
|
if get_out_spawned_this_hard_map:
|
|
legacy_hard_mode_copy = [possible_enemy for possible_enemy in legacy_hard_mode_copy if possible_enemy != Enemies.GetOut]
|
|
enemy_to_place = spoiler.settings.random.choice(legacy_hard_mode_copy)
|
|
if enemy_to_place in ANNOYING_ENEMIES:
|
|
no_annoying_enemies = [e for e in legacy_hard_mode_copy if e not in ANNOYING_ENEMIES]
|
|
if len(no_annoying_enemies) > 0: # Make sure we're not going to be picking from an empty list
|
|
legacy_hard_mode_copy = no_annoying_enemies.copy()
|
|
if enemy_to_place == Enemies.GetOut:
|
|
get_out_spawned_this_hard_map = True
|
|
enemy_swaps_library[map_id].append(enemy_to_place)
|
|
# one last shuffle, to make sure any enemy can spawn in any spot
|
|
for map_id in enemy_swaps_library:
|
|
if len(enemy_swaps_library[map_id]) > 0:
|
|
spoiler.settings.random.shuffle(enemy_swaps_library[map_id])
|
|
return enemy_swaps_library
|
|
|
|
|
|
def writeEnemy(spoiler, ROM_COPY: LocalROM, cont_map_spawner_address: int, new_enemy_id: int, spawner: Spawner, cont_map_id: Maps, crown_timer: int = 0):
|
|
"""Write enemy to ROM."""
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset)
|
|
ROM_COPY.writeMultipleBytes(new_enemy_id, 1)
|
|
# Enemy fixes
|
|
if new_enemy_id in EnemyMetaData.keys():
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0x10)
|
|
ROM_COPY.writeMultipleBytes(EnemyMetaData[new_enemy_id].aggro, 1)
|
|
if new_enemy_id == Enemies.RoboKremling:
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xB)
|
|
ROM_COPY.writeMultipleBytes(0xC8, 1)
|
|
elif new_enemy_id == Enemies.SpiderSmall:
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0x1)
|
|
ROM_COPY.writeMultipleBytes(0, 1)
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xB)
|
|
ROM_COPY.writeMultipleBytes(0, 1)
|
|
# Spawning fixes
|
|
# Prevent respawn anim if that's how they initially appear
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0x12)
|
|
init_respawn_state = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
if init_respawn_state == 3:
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0x12)
|
|
ROM_COPY.writeMultipleBytes(0, 1)
|
|
# Prevent them respawning
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0x14)
|
|
ROM_COPY.writeMultipleBytes(0, 1)
|
|
elif new_enemy_id == Enemies.Kaboom:
|
|
# Fix their time to uh-oh timer
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xA)
|
|
ROM_COPY.writeMultipleBytes(140, 2)
|
|
|
|
if (cont_map_id in crown_maps or cont_map_id in minigame_maps_total) and EnemyMetaData[new_enemy_id].air:
|
|
height = 300
|
|
if cont_map_id in crown_maps:
|
|
height = int(spoiler.settings.random.uniform(250, 300))
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0x6)
|
|
ROM_COPY.writeMultipleBytes(height, 2)
|
|
if cont_map_id in crown_maps and new_enemy_id == Enemies.GetOut:
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xA)
|
|
get_out_timer = 20
|
|
if crown_timer > 20:
|
|
damage_mult = 1
|
|
damage_amts = {DamageAmount.double: 2, DamageAmount.quad: 4, DamageAmount.ohko: 12}
|
|
if spoiler.settings.damage_amount in damage_amts:
|
|
damage_mult = damage_amts[spoiler.settings.damage_amount]
|
|
get_out_timer = spoiler.settings.random.randint(int(crown_timer / (12 / damage_mult)) + 1, crown_timer - 1)
|
|
if get_out_timer == 0:
|
|
get_out_timer = 1
|
|
ROM_COPY.writeMultipleBytes(get_out_timer, 1)
|
|
ROM_COPY.writeMultipleBytes(get_out_timer, 1)
|
|
# Scale Adjustment
|
|
if (EnemyMetaData[new_enemy_id].default_size is not None) and (cont_map_id not in banned_size_maps):
|
|
scale = EnemyMetaData[new_enemy_id].default_size
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xF)
|
|
if cont_map_id == Maps.JapesTinyHive:
|
|
# Is a mini monkey map, where we'd expect to see enemy sizes to be bigger to fit thematically
|
|
scale = min(255, int(2.5 * scale))
|
|
if new_enemy_id not in (Enemies.Gimpfish, Enemies.Shuri): # Game is dumb
|
|
if cont_map_id not in MAP_DIFFICULTY_ORDER:
|
|
if spoiler.settings.randomize_enemy_sizes:
|
|
lower_b = int(scale * 0.3)
|
|
if cont_map_id == Maps.CavesDiddyUpperCabin:
|
|
upper_b = scale
|
|
else:
|
|
upper_b = min(255, int(1.5 * scale))
|
|
chosen_scale = spoiler.settings.random.randint(lower_b, upper_b)
|
|
ROM_COPY.writeMultipleBytes(chosen_scale, 1)
|
|
elif spoiler.settings.normalize_enemy_sizes:
|
|
ROM_COPY.writeMultipleBytes(scale, 1)
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xF)
|
|
default_scale = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
if EnemyMetaData[new_enemy_id].size_cap > 0:
|
|
if default_scale > EnemyMetaData[new_enemy_id].size_cap:
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xF)
|
|
ROM_COPY.writeMultipleBytes(EnemyMetaData[new_enemy_id].size_cap, 1)
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xF)
|
|
pre_size = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
if pre_size < EnemyMetaData[new_enemy_id].bbbarrage_min_scale and cont_map_id in bbbarrage_maps and ENABLE_BBBARRAGE_ENEMY_RANDO:
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xF)
|
|
ROM_COPY.writeMultipleBytes(EnemyMetaData[new_enemy_id].bbbarrage_min_scale, 1)
|
|
if new_enemy_id in (Enemies.KlaptrapPurple, Enemies.KlaptrapRed) and cont_map_id == Maps.CavesDiddyLowerCabin:
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xF)
|
|
ROM_COPY.write(75)
|
|
# Speed Adjustment
|
|
if spoiler.settings.enemy_speed_rando:
|
|
if cont_map_id not in banned_speed_maps:
|
|
min_speed = EnemyMetaData[new_enemy_id].min_speed
|
|
max_speed = EnemyMetaData[new_enemy_id].max_speed
|
|
if min_speed > 0 and max_speed > 0:
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xD)
|
|
agg_speed = spoiler.settings.random.randint(min_speed, max_speed)
|
|
ROM_COPY.writeMultipleBytes(agg_speed, 1)
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xC)
|
|
ROM_COPY.writeMultipleBytes(spoiler.settings.random.randint(min_speed, agg_speed), 1)
|
|
if cont_map_id in bbbarrage_maps and ENABLE_BBBARRAGE_ENEMY_RANDO:
|
|
# Reduce Speeds
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xC)
|
|
speeds = []
|
|
for x in range(2):
|
|
speeds.append(int.from_bytes(ROM_COPY.readBytes(1), "big"))
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xC)
|
|
for x in speeds:
|
|
ROM_COPY.writeMultipleBytes(int(x * 0.75), 1)
|
|
elif cont_map_id in minigame_maps_beavers and new_enemy_id == Enemies.BeaverGold:
|
|
for speed_offset in [0xC, 0xD]:
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + speed_offset)
|
|
default_speed = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
new_speed = int(default_speed * 1.1)
|
|
if new_speed > 255:
|
|
new_speed = 255
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + speed_offset)
|
|
ROM_COPY.writeMultipleBytes(new_speed, 1)
|
|
# Fix Tiny 5DI enemy to not respawn
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0x13)
|
|
id = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
if cont_map_id == Maps.CavesTinyIgloo and id == 2:
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0x14)
|
|
ROM_COPY.writeMultipleBytes(0, 1) # Disable respawning
|
|
|
|
|
|
def randomize_enemies_0(spoiler):
|
|
"""Determine randomized enemies."""
|
|
data = {}
|
|
noise_management_dict = {} # Prevent known game freezes
|
|
pkmn = []
|
|
resetPkmnSnap()
|
|
for loc in enemy_location_list:
|
|
if enemy_location_list[loc].enable_randomization:
|
|
sound_safeguard = False
|
|
map = enemy_location_list[loc].map
|
|
if map not in data:
|
|
data[map] = []
|
|
noise_management_dict[map] = 0
|
|
sound_safeguard = noise_management_dict[map] > 2
|
|
new_enemy = enemy_location_list[loc].placeNewEnemy(spoiler.settings.random, spoiler.settings.enemies_selected, True, sound_safeguard)
|
|
if map == Maps.ForestAnthill or not enemy_location_list[loc].respawns and EnemyMetaData[new_enemy].audio_engine_burden:
|
|
noise_management_dict[map] += 1
|
|
if enemy_location_list[loc].respawns:
|
|
setPkmnSnapEnemy(new_enemy)
|
|
data[map].append(
|
|
{
|
|
"enemy": new_enemy,
|
|
"speeds": [enemy_location_list[loc].idle_speed, enemy_location_list[loc].aggro_speed],
|
|
"id": enemy_location_list[loc].id,
|
|
"location": Locations(loc).name,
|
|
}
|
|
)
|
|
spoiler.enemy_rando_data = data
|
|
for enemy in pkmn_snap_enemies:
|
|
pkmn.append(enemy.spawned)
|
|
spoiler.pkmn_snap_data = pkmn
|
|
|
|
|
|
def randomize_enemies(spoiler, ROM_COPY: LocalROM):
|
|
"""Write replaced enemies to ROM."""
|
|
# Define Enemy Classes, Used for detection of if an enemy will be replaced
|
|
enemy_classes = {}
|
|
for enemy in EnemyMetaData:
|
|
data = EnemyMetaData[enemy]
|
|
if data.e_type != EnemySubtype.NoType and data.placeable:
|
|
if data.e_type not in enemy_classes:
|
|
enemy_classes[data.e_type] = []
|
|
enemy_classes[data.e_type].append(enemy)
|
|
|
|
# Define Enemies that can be placed in those classes
|
|
enemy_placement_classes = {}
|
|
banned_classes = []
|
|
for enemy_class in enemy_classes:
|
|
class_list = []
|
|
for enemy in enemy_classes[enemy_class]:
|
|
if enemy in spoiler.settings.enemies_selected:
|
|
class_list.append(enemy)
|
|
if len(class_list) == 0:
|
|
# Nothing present, use backup
|
|
for repl_type in replacement_priority[enemy_class]:
|
|
if len(class_list) == 0:
|
|
for enemy in enemy_classes[repl_type]:
|
|
if enemy in spoiler.settings.enemies_selected:
|
|
class_list.append(enemy)
|
|
if len(class_list) > 0:
|
|
enemy_placement_classes[enemy_class] = class_list.copy()
|
|
else:
|
|
# Replace Nothing
|
|
banned_classes.append(enemy_class)
|
|
for enemy_class in banned_classes:
|
|
del enemy_classes[enemy_class]
|
|
# Crown Enemy Stuff
|
|
crown_enemies_library = {}
|
|
crown_enemies = []
|
|
for enemy in EnemyMetaData:
|
|
if EnemyMetaData[enemy].crown_enabled is True:
|
|
crown_enemies.append(enemy)
|
|
if spoiler.settings.enemy_rando or spoiler.settings.crown_enemy_difficulty != CrownEnemyDifficulty.vanilla:
|
|
boolean_damage_is_ohko = spoiler.settings.damage_amount == DamageAmount.ohko
|
|
crown_enemies_library = getBalancedCrownEnemyRando(spoiler, spoiler.settings.crown_enemy_difficulty, boolean_damage_is_ohko)
|
|
minigame_enemies_simple = []
|
|
minigame_enemies_beatable = []
|
|
minigame_enemies_nolimit = []
|
|
minigame_enemies_beavers = []
|
|
for enemy in EnemyMetaData:
|
|
if EnemyMetaData[enemy].minigame_enabled:
|
|
minigame_enemies_nolimit.append(enemy)
|
|
if EnemyMetaData[enemy].beaver:
|
|
minigame_enemies_beavers.append(enemy)
|
|
if EnemyMetaData[enemy].killable:
|
|
minigame_enemies_beatable.append(enemy)
|
|
if EnemyMetaData[enemy].simple:
|
|
minigame_enemies_simple.append(enemy)
|
|
for cont_map_id in range(216):
|
|
cont_map_spawner_address = getPointerLocation(TableNames.Spawners, cont_map_id)
|
|
vanilla_spawners = []
|
|
ROM_COPY.seek(cont_map_spawner_address)
|
|
fence_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
offset = 2
|
|
if fence_count > 0:
|
|
for x in range(fence_count):
|
|
ROM_COPY.seek(cont_map_spawner_address + offset)
|
|
point_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
offset += (point_count * 6) + 2
|
|
ROM_COPY.seek(cont_map_spawner_address + offset)
|
|
point0_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
offset += (point0_count * 10) + 6
|
|
ROM_COPY.seek(cont_map_spawner_address + offset)
|
|
spawner_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
# Generate Enemy Swaps lists
|
|
enemy_swaps = {}
|
|
for enemy_class in enemy_classes:
|
|
arr = []
|
|
for x in range(spawner_count):
|
|
arr.append(spoiler.settings.random.choice(enemy_placement_classes[enemy_class]))
|
|
enemy_swaps[enemy_class] = arr
|
|
offset += 2
|
|
for _ in range(spawner_count):
|
|
ROM_COPY.seek(cont_map_spawner_address + offset)
|
|
enemy_id = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
ROM_COPY.seek(cont_map_spawner_address + offset + 0x13)
|
|
enemy_index = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
init_offset = offset
|
|
ROM_COPY.seek(cont_map_spawner_address + offset + 0x11)
|
|
extra_count = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
offset += 0x16 + (extra_count * 2)
|
|
vanilla_spawners.append(Spawner(enemy_id, init_offset, enemy_index))
|
|
if spoiler.settings.enemy_rando and cont_map_id in spoiler.enemy_rando_data:
|
|
referenced_spawner = None
|
|
for enemy in spoiler.enemy_rando_data[cont_map_id]:
|
|
for spawner in vanilla_spawners:
|
|
if spawner.index == enemy["id"]:
|
|
referenced_spawner = spawner
|
|
break
|
|
if referenced_spawner is not None:
|
|
writeEnemy(spoiler, ROM_COPY, cont_map_spawner_address, enemy["enemy"], referenced_spawner, cont_map_id, 0)
|
|
if spoiler.settings.enemy_rando and cont_map_id in minigame_maps_total:
|
|
tied_enemy_list = []
|
|
if cont_map_id in minigame_maps_easy:
|
|
tied_enemy_list = minigame_enemies_simple.copy()
|
|
if cont_map_id in bbbarrage_maps and ENABLE_BBBARRAGE_ENEMY_RANDO:
|
|
if Enemies.KlaptrapGreen in tied_enemy_list:
|
|
tied_enemy_list.remove(Enemies.KlaptrapGreen) # Remove Green Klaptrap out of BBBarrage pool
|
|
elif cont_map_id in minigame_maps_beatable:
|
|
tied_enemy_list = minigame_enemies_beatable.copy()
|
|
elif cont_map_id in minigame_maps_nolimit:
|
|
tied_enemy_list = minigame_enemies_nolimit.copy()
|
|
elif cont_map_id in minigame_maps_beavers:
|
|
tied_enemy_list = minigame_enemies_beavers.copy()
|
|
for spawner in vanilla_spawners:
|
|
if spawner.enemy_id in tied_enemy_list:
|
|
new_enemy_id = spoiler.settings.random.choice(tied_enemy_list)
|
|
# Balance beaver bother so it's a 4:1 ratio of blue to gold beavers, guarantee 1 gold
|
|
if cont_map_id in minigame_maps_beavers:
|
|
if spawner.index == 1:
|
|
new_enemy_id = Enemies.BeaverGold
|
|
else:
|
|
selection = spoiler.settings.random.uniform(0, 1)
|
|
new_enemy_id = Enemies.BeaverBlue
|
|
if selection < 0.2:
|
|
new_enemy_id = Enemies.BeaverGold
|
|
writeEnemy(spoiler, ROM_COPY, cont_map_spawner_address, new_enemy_id, spawner, cont_map_id, 0)
|
|
if spoiler.settings.crown_enemy_difficulty != CrownEnemyDifficulty.vanilla and cont_map_id in crown_maps:
|
|
# Determine Crown Timer
|
|
limits = {
|
|
CrownEnemyDifficulty.easy: 5,
|
|
CrownEnemyDifficulty.medium: 15,
|
|
CrownEnemyDifficulty.hard: 30,
|
|
}
|
|
difficulty = getCrownEnemyDifficultyFromMap(spoiler.settings, cont_map_id)
|
|
low_limit = limits.get(difficulty, 5)
|
|
crown_timer = spoiler.settings.random.randint(low_limit, low_limit + 30)
|
|
# Place Enemies
|
|
for spawner in vanilla_spawners:
|
|
if spawner.enemy_id in crown_enemies:
|
|
new_enemy_id = crown_enemies_library[cont_map_id].pop()
|
|
writeEnemy(spoiler, ROM_COPY, cont_map_spawner_address, new_enemy_id, spawner, cont_map_id, crown_timer)
|
|
elif spawner.enemy_id == Enemies.BattleCrownController:
|
|
ROM_COPY.seek(cont_map_spawner_address + spawner.offset + 0xB)
|
|
ROM_COPY.writeMultipleBytes(crown_timer, 1) # Determine Crown length. DK64 caps at 255 seconds
|
|
if spoiler.settings.win_condition_item == WinConditionComplex.krem_kapture:
|
|
# Pkmn snap handler
|
|
values = [0, 0, 0, 0, 0]
|
|
# In some cases, the Pkmn Snap data hasn't yet been initialized (enemy rando disabled)
|
|
# so we use the default values
|
|
if len(spoiler.pkmn_snap_data) == 0:
|
|
# Pkmn Snap Default Enemies
|
|
spoiler.pkmn_snap_data = [
|
|
True, # Kaboom
|
|
True, # Blue Beaver
|
|
True, # Book
|
|
True, # Klobber
|
|
True, # Zinger (Charger)
|
|
True, # Klump
|
|
True, # Klaptrap (Green)
|
|
True, # Zinger (Bomber)
|
|
True, # Klaptrap (Purple)
|
|
False, # Klaptrap (Red)
|
|
False, # Gold Beaver
|
|
True, # Mushroom Man
|
|
True, # Ruler
|
|
True, # Robo-Kremling
|
|
True, # Kremling
|
|
True, # Kasplat (DK)
|
|
True, # Kasplat (Diddy)
|
|
True, # Kasplat (Lanky)
|
|
True, # Kasplat (Tiny)
|
|
True, # Kasplat (Chunky)
|
|
False, # Kop
|
|
True, # Robo-Zinger
|
|
True, # Krossbones
|
|
True, # Shuri
|
|
True, # Gimpfish
|
|
True, # Mr. Dice (Green)
|
|
True, # Sir Domino
|
|
True, # Mr. Dice (Red)
|
|
True, # Fireball w/ Glasses
|
|
True, # Small Spider
|
|
True, # Bat
|
|
True, # Tomato
|
|
True, # Ghost
|
|
True, # Pufftup
|
|
True, # Kosha
|
|
]
|
|
for enemy_index, spawned in enumerate(spoiler.pkmn_snap_data):
|
|
if spawned:
|
|
offset = enemy_index >> 3
|
|
shift = enemy_index & 7
|
|
values[offset] |= 1 << shift
|
|
ROM_COPY.seek(spoiler.settings.rom_data + 0x117)
|
|
for value in values:
|
|
ROM_COPY.writeMultipleBytes(value, 1)
|