Files
alttpr-python/source/enemizer/Enemizer.py
aerinon 915e7dc416 Ability to enemize fairies
Fix for blue square in caves
Lower limit for sprites on tiles that support it, to help with pot lifting
Swamp waterway enemies fixed to not drop
2023-09-26 08:59:31 -06:00

503 lines
24 KiB
Python

import RaceRandom as random
from Utils import snes_to_pc
from source.dungeon.EnemyList import SpriteType, EnemySprite, sprite_translation
from source.dungeon.RoomList import Room010C
from source.enemizer.SpriteSheets import sub_group_choices
from source.enemizer.SpriteSheets import randomize_underworld_sprite_sheets, randomize_overworld_sprite_sheets
from source.enemizer.TilePattern import tile_patterns
shutter_sprites = {
0xb8: {0, 1, 2, 3, 4, 5}, 0xb: {4, 5, 6, 7, 8, 9}, 0x1b: {3, 4, 5}, 0x4b: {0, 3, 4}, 0x4: {9, 13, 14},
0x24: {3, 4, 5, 6}, # not sure about 6 - bunny beam under pot
0x28: {0, 1, 2, 3, 4}, 0xe: {0, 1, 2, 3}, 0x2e: {0, 1, 2, 3, 4, 5}, 0x3e: {1, 2}, 0x6e: {0, 1, 2, 3, 4},
0x31: {7, 8, 10}, 0x44: {2, 3, 5}, 0x45: {1, 2, 3}, 0x53: {5, 6, 8, 9, 10}, 0x75: {0, 2, 3, 4, 5},
0x85: {2, 3, 4, 5}, 0x5d: {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, 0x6b: {5, 6, 7, 8, 9, 10, 11, 12, 13},
0x6d: {0, 1, 2, 3, 4, 5, 6, 7, 8}, 0x7b: {2, 3, 4, 5, 8, 9, 10}, 0x7d: {4, 5, 6, 7, 8, 10}, 0x8d: {0, 1, 2, 3, 4},
0xa5: {0, 1, 2, 3, 4, 5, 6, 7}, 0x71: {0, 1}, 0xd8: {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
0xb0: {0, 1, 2, 3, 4, 5, 7, 8, 9, 10}, 0xc0: {0, 1, 2}, 0xe0: {0, 1, 2, 3}, 0xb2: {5, 6, 7, 10, 11},
0xd2: {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 0xef: {0, 1, 2}, 0x10c: {4, 5, 6, 7}, 0x123: {0, 1, 2, 3},
0xee: {0, 1, 2, 3, 4} # low health traversal
}
def setup_specific_requirements(data_tables):
requirements = data_tables.sprite_requirements
water_groups = set()
water_sub_groups = {0: set(), 1: set(), 2: set(), 3: set()}
killable_groups = set()
killable_sub_groups = {0: set(), 1: set(), 2: set(), 3: set()}
key_groups = set()
key_sub_groups = {0: set(), 1: set(), 2: set(), 3: set()}
for sid, requirement in requirements.items():
if isinstance(requirement, dict):
continue
if requirement.good_for_uw_water():
water_groups.update(requirement.groups)
for i in range(0, 4):
limited = [x for x in requirement.sub_groups[i] if x in sub_group_choices[i]]
water_sub_groups[i].update(limited)
if requirement.good_for_shutter([]):
killable_groups.update(requirement.groups)
for i in range(0, 4):
killable_sub_groups[i].update(requirement.sub_groups[i])
if requirement.can_drop:
key_groups.update(requirement.groups)
for i in range(0, 4):
key_sub_groups[i].update(requirement.sub_groups[i])
return water_groups, water_sub_groups, killable_groups, killable_sub_groups, key_groups, key_sub_groups
def get_possible_sheets(room_id, data_tables, specific, all_sheets, uw_sheets):
# forced sprites for room
requirements = data_tables.sprite_requirements
water_groups, water_sub_groups, killable_groups, killable_sub_groups, key_groups, key_sub_groups = specific
# forced_req = set()
key_needed = False
killable_needed = room_id in shutter_sprites
for sheet in all_sheets:
if room_id in sheet.room_set:
return [sheet]
match_all_room_groups = set()
match_all_sub_groups = {0: set(), 1: set(), 2: set(), 3: set()}
# match_all_sub_groups = {0: set(uw_sub_group_choices[0] + [70, 72]), 1: set(uw_sub_group_choices[1] + [13, 73]),
# 2: set(uw_sub_group_choices[2] + [19]), 3: set(uw_sub_group_choices[3] + [25, 68])}
for sprite in data_tables.uw_enemy_table.room_map[room_id]:
sprite_secondary = 0 if sprite.sub_type != SpriteType.Overlord else sprite.sub_type
key = (sprite.kind, sprite_secondary)
if key not in requirements:
continue
req = requirements[key]
if isinstance(req, dict):
req = req[room_id]
if req.static or not req.can_randomize or sprite.static:
if req.groups:
match_all_room_groups.intersection_update(req.groups)
if not match_all_room_groups:
match_all_room_groups = set(req.groups)
for i in range(0, 4):
if req.sub_groups[i]:
match_all_sub_groups[i].intersection_update(req.sub_groups[i])
if not match_all_sub_groups[i]:
match_all_sub_groups[i] = set(req.sub_groups[i])
# forced_req.add(req)
if sprite.drops_item:
key_needed = True
match_any_room_groups = set()
match_any_sub_groups = {0: set(), 1: set(), 2: set(), 3: set()}
exclude_all_groups = set()
exclude_all_sub_groups = {0: set(), 1: set(), 2: set(), 3: set()}
if room_id in data_tables.room_requirements:
required_groups = data_tables.room_requirements[room_id]
for idx, grp in enumerate(required_groups):
if grp is not None:
if isinstance(grp, tuple):
match_any_sub_groups[idx].update(grp)
else:
match_all_sub_groups[idx] = {grp}
if key_needed:
if key_groups:
match_any_room_groups.update(key_groups)
for i in range(0, 4):
if key_sub_groups[i]:
match_any_sub_groups[i].update(key_sub_groups[i])
elif killable_needed:
if killable_groups:
match_any_room_groups.update(killable_groups)
for i in range(0, 4):
if killable_sub_groups[i]:
match_any_sub_groups[i].update(killable_sub_groups[i])
possible_sheets = []
for sheet in uw_sheets:
if match_all_room_groups and sheet.id not in match_all_room_groups:
continue
if any(match_all_sub_groups[i] and sheet.sub_groups[i] not in match_all_sub_groups[i] for i in range(0, 4)):
continue
if exclude_all_groups and sheet.id in exclude_all_groups:
continue
if any(exclude_all_sub_groups[i] and sheet.sub_groups[i] in exclude_all_sub_groups[i] for i in range(0, 4)):
continue
if match_any_room_groups and sheet.id not in match_any_sub_groups:
continue
test_subs = [i for i in range(0, 4) if match_any_sub_groups[i]]
if test_subs and all(sheet.sub_groups[i] not in match_any_sub_groups[i] for i in test_subs):
continue
possible_sheets.append(sheet)
return possible_sheets
def get_possible_ow_sheets(area_id, all_sheets, ow_sheets, data_tables):
requirements = data_tables.sprite_requirements
for sheet in all_sheets:
if area_id in sheet.room_set:
return [sheet]
match_all_room_groups = set()
match_all_sub_groups = {0: set(), 1: set(), 2: set(), 3: set()}
for sprite in data_tables.ow_enemy_table[area_id]:
sprite_secondary = 0 if sprite.sub_type != SpriteType.Overlord else sprite.sub_type
key = (sprite.kind, sprite_secondary)
if key not in requirements:
continue
req = requirements[key]
if isinstance(req, dict):
req = req[area_id]
if req.static or not req.can_randomize:
if req.groups:
match_all_room_groups.intersection_update(req.groups)
if not match_all_room_groups:
match_all_room_groups = set(req.groups)
for i in range(0, 4):
if req.sub_groups[i]:
match_all_sub_groups[i].intersection_update(req.sub_groups[i])
if not match_all_sub_groups[i]:
match_all_sub_groups[i] = set(req.sub_groups[i])
possible_sheets = []
for sheet in ow_sheets:
if match_all_room_groups and sheet.id not in match_all_room_groups:
continue
if any(match_all_sub_groups[i] and sheet.sub_groups[i] not in match_all_sub_groups[i] for i in range(0, 4)):
continue
possible_sheets.append(sheet)
return possible_sheets
ignore_sheets_uw = {65, 69, 71, 78, 79, 82, 88, 98}
ignore_sheets_ow = {6}
def find_candidate_sprites(data_tables, sheet_range, uw=True):
requirements = data_tables.sprite_requirements
sprite_candidates = []
sheet_candidates = []
all_sheets = []
candidate_groups = set()
candidate_sub_groups = {0: set(), 1: set(), 2: set(), 3: set()}
for k, r in requirements.items():
if isinstance(r, dict):
continue
valid_flag = (uw and r.uw_valid) or (not uw and r.ow_valid)
if not r.static and valid_flag and not r.dont_use:
candidate_groups.update(r.groups)
for i in range(0, 4):
candidate_sub_groups[i].update(r.sub_groups[i])
sprite_candidates.append(k)
for num in sheet_range:
sheet = data_tables.sprite_sheets[num]
all_sheets.append(sheet)
if (uw and num in ignore_sheets_uw) or (not uw and num in ignore_sheets_ow):
continue
if candidate_groups and sheet not in candidate_groups:
continue
test_subs = [i for i in range(0, 4) if candidate_sub_groups[i]]
if test_subs and all(sheet.sub_groups[i] not in candidate_sub_groups[i] for i in test_subs):
continue
sheet_candidates.append(sheet)
return sprite_candidates, sheet_candidates, all_sheets
def get_possible_enemy_sprites(room_id, sheet, uw_sprites, data_tables):
ret = []
for sprite in uw_sprites:
requirement = data_tables.sprite_requirements[sprite]
if isinstance(requirement, dict):
requirement = requirement[room_id]
if sheet.valid_sprite(requirement) and requirement.can_spawn_in_room(room_id):
ret.append(requirement)
return ret
def get_possible_enemy_sprites_ow(sheet, sprites, data_tables):
ret = []
for sprite in sprites:
requirement = data_tables.sprite_requirements[sprite]
if isinstance(requirement, dict):
continue
if sheet.valid_sprite(requirement) and requirement.ow_valid:
ret.append(requirement)
return ret
def get_randomize_able_sprites(room_id, data_tables):
sprite_table = {}
for idx, sprite in enumerate(data_tables.uw_enemy_table.room_map[room_id]):
sprite_secondary = 0 if sprite.sub_type != SpriteType.Overlord else sprite.sub_type
key = (sprite.kind, sprite_secondary)
if key not in data_tables.sprite_requirements:
continue
req = data_tables.sprite_requirements[key]
if isinstance(req, dict):
continue
if not req.static and req.can_randomize and not sprite.static:
sprite_table[idx] = sprite
return sprite_table
def get_randomize_able_sprites_ow(area_id, data_tables):
sprite_table = {}
for idx, sprite in enumerate(data_tables.ow_enemy_table[area_id]):
sprite_secondary = 0 if sprite.sub_type != SpriteType.Overlord else sprite.sub_type
key = (sprite.kind, sprite_secondary)
if key not in data_tables.sprite_requirements:
continue
req = data_tables.sprite_requirements[key]
if isinstance(req, dict):
continue
if not req.static and req.can_randomize:
sprite_table[idx] = sprite
return sprite_table
sprite_limiter = {
EnemySprite.Debirando: 2,
EnemySprite.DebirandoPit: 2,
EnemySprite.AntiFairyCircle: 4
}
def exceeds_sprite_limit(limit, sprite):
return sprite_limiter[sprite.sprite]-1+limit > 15 if sprite.sprite in sprite_limiter else False
def randomize_underworld_rooms(data_tables, world, player, custom_uw):
any_enemy_logic = world.any_enemy_logic[player]
enemy_drops_active = world.dropshuffle[player] in ['underworld']
specific = setup_specific_requirements(data_tables)
uw_candidates, uw_sheets, all_sheets = find_candidate_sprites(data_tables, range(65, 124))
for room_id in range(0, 0x128):
if room_id in {0, 1, 3, 6, 7, 0xd, 0x14, 0x20, 0x29, 0x30, 0x33,
0x4d, 0x5a, 0x90, 0xa4, 0xac, 0xc8, 0xde}:
continue
current_sprites = data_tables.uw_enemy_table.room_map[room_id]
sprite_limit = sum(sprite_limiter[x.kind] if x.kind in sprite_limiter else 1 for x in current_sprites)
randomizeable_sprites = get_randomize_able_sprites(room_id, data_tables)
if not randomizeable_sprites:
candidate_sheets = get_possible_sheets(room_id, data_tables, specific, all_sheets, uw_sheets)
chosen_sheet = random.choice(candidate_sheets)
data_tables.room_headers[room_id].sprite_sheet = chosen_sheet.id - 0x40
if randomizeable_sprites:
candidate_sheets = get_possible_sheets(room_id, data_tables, specific, all_sheets, uw_sheets)
done = False
while not done:
chosen_sheet = random.choice(candidate_sheets)
data_tables.room_headers[room_id].sprite_sheet = chosen_sheet.id - 0x40
candidate_sprites = get_possible_enemy_sprites(room_id, chosen_sheet, uw_candidates, data_tables)
randomized = True
wallmaster_chosen = room_id in {0x0039, 0x0049, 0x0056, 0x0057, 0x0068, 0x008d}
for i, sprite in randomizeable_sprites.items():
if room_id in custom_uw and i in custom_uw[room_id]:
sprite.kind = sprite_translation[custom_uw[room_id][i]]
else:
# filter out water if necessary
candidate_sprites = [x for x in candidate_sprites if not x.water_only or sprite.water]
# filter out wallmaster if already on tile
if wallmaster_chosen:
candidate_sprites = [x for x in candidate_sprites if x.sprite != EnemySprite.Wallmaster]
candidate_sprites = [x for x in candidate_sprites if not exceeds_sprite_limit(sprite_limit, x)]
if sprite.drops_item:
forbidden = determine_forbidden(any_enemy_logic == 'none', room_id, True)
choice_list = [x for x in candidate_sprites if x.good_for_key_drop(forbidden)]
# terrorpin, deadrock, buzzblob, lynel, redmimic/eyegore
elif room_id in shutter_sprites and i in shutter_sprites[room_id]:
forbidden = determine_forbidden(any_enemy_logic != 'allow_all', room_id)
choice_list = [x for x in candidate_sprites if x.good_for_shutter(forbidden)]
else:
choice_list = [x for x in candidate_sprites if not x.water_only]
choice_list = filter_choices(choice_list, room_id, i, data_tables.uw_enemy_denials)
if enemy_drops_active:
choice_list = filter_choices(choice_list, room_id, i, data_tables.uw_enemy_drop_denials)
if len(choice_list) == 0:
randomized = False
break
weight = [data_tables.uw_weights[r.sprite] for r in choice_list]
chosen = random.choices(choice_list, weight, k=1)[0]
sprite.kind = chosen.sprite
if sprite.kind in sprite_limiter:
sprite_limit += sprite_limiter[sprite.kind]-1
if sprite.kind == EnemySprite.Wallmaster:
wallmaster_chosen = True
sprite.kind = 0x09
sprite.sub_type = SpriteType.Overlord
done = randomized
# done with sprites
# done with rooms
def determine_forbidden(forbid, room_id, drop_flag=False):
forbidden_set = set()
if forbid:
forbidden_set.update({EnemySprite.Terrorpin, EnemySprite.Deadrock, EnemySprite.Buzzblob,
EnemySprite.Lynel})
if drop_flag:
forbidden_set.add(EnemySprite.RedBari) # requires FireRod to Drop
# else: Not yet able to protect triggers, would change default GT tile room behavior
# forbidden_set.add(EnemySprite.AntiFairy) # can't drop anyway
if room_id not in {0x6b, 0x4b, 0x1b, 0xd8}: # mimics/eyegore are allowed in vanilla rooms
forbidden_set.add(EnemySprite.RedEyegoreMimic)
return forbidden_set
def filter_choices(options, room_id, sprite_idx, denials):
key = room_id, sprite_idx
return [x for x in options if key not in denials or x.sprite not in denials[key]]
def filter_water_phobic(options, sprite):
return [x for x in options if not x.water_phobic or not sprite.water]
def randomize_overworld_enemies(data_tables, custom_ow):
ow_candidates, ow_sheets, all_sheets = find_candidate_sprites(data_tables, range(1, 64), False)
areas_to_randomize = [0, 2, 3, 5, 7, 0xA, 0xF, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x1a, 0x1b, 0x1d, 0x1e, 0x22, 0x25, 0x28, 0x29, 0x2A, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x32, 0x33, 0x34, 0x35, 0x37, 0x3a, 0x3b, 0x3c, 0x3f]
area_list = areas_to_randomize + [x + 0x40 for x in areas_to_randomize] # light world + dark world
area_list += [0x80, 0x81] + [x + 0x90 for x in areas_to_randomize] # specials + post aga LW
for area_id in area_list:
randomizeable_sprites = get_randomize_able_sprites_ow(area_id, data_tables)
if not randomizeable_sprites:
candidate_sheets = get_possible_ow_sheets(area_id, all_sheets, ow_sheets, data_tables)
chosen_sheet = random.choice(candidate_sheets)
data_tables.overworld_sprite_sheets[area_id] = chosen_sheet
candidate_sprites = get_possible_enemy_sprites_ow(chosen_sheet, ow_candidates, data_tables)
else:
candidate_sheets = get_possible_ow_sheets(area_id, all_sheets, ow_sheets, data_tables)
chosen_sheet = random.choice(candidate_sheets)
data_tables.overworld_sprite_sheets[area_id] = chosen_sheet
candidate_sprites = get_possible_enemy_sprites_ow(chosen_sheet, ow_candidates, data_tables)
for i, sprite in randomizeable_sprites.items():
if area_id in custom_ow and i in custom_ow[area_id]:
sprite.kind = sprite_translation[custom_ow[area_id][i]]
else:
candidate_sprites = filter_choices(candidate_sprites, area_id, i, data_tables.ow_enemy_denials)
candidate_sprites = filter_water_phobic(candidate_sprites, sprite)
weight = [data_tables.ow_weights[r.sprite] for r in candidate_sprites]
chosen = random.choices(candidate_sprites, weight, k=1)[0]
sprite.kind = chosen.sprite
# randomize the bush sprite per area
weight = [data_tables.ow_weights[r.sprite] for r in candidate_sprites]
bush_sprite_choice = random.choices(candidate_sprites, weight, k=1)[0]
data_tables.bush_sprite_table[area_id] = bush_sprite_choice
# damage and health tables only go to F2
skip_sprites = {
EnemySprite.ArmosKnight, EnemySprite.Lanmolas, EnemySprite.Moldorm, EnemySprite.Mothula, EnemySprite.Arrghus,
EnemySprite.HelmasaurKing, EnemySprite.Vitreous, EnemySprite.TrinexxRockHead, EnemySprite.TrinexxFireHead,
EnemySprite.TrinexxIceHead, EnemySprite.Blind, EnemySprite.Kholdstare, EnemySprite.KholdstareShell,
EnemySprite.FallingIce, EnemySprite.Arrghi, EnemySprite.Agahnim, EnemySprite.Ganon,
EnemySprite.PositionTarget, EnemySprite.Boulders
}
def randomize_enemies(world, player):
if world.enemy_shuffle[player] != 'none':
data_tables = world.data_tables[player]
custom_uw, custom_ow = {}, {}
enemy_map = world.customizer.get_enemies() if world.customizer else None
if enemy_map and player in enemy_map:
if 'Underworld' in enemy_map[player]:
custom_uw = enemy_map[player]['Underworld']
if 'Overworld' in enemy_map[player]:
custom_ow = enemy_map[player]['Overworld']
randomize_underworld_sprite_sheets(data_tables.sprite_sheets, data_tables, custom_uw)
randomize_underworld_rooms(data_tables, world, player, custom_uw)
randomize_overworld_sprite_sheets(data_tables.sprite_sheets, data_tables, custom_ow)
randomize_overworld_enemies(data_tables, custom_ow)
# fix thief stats
# subclass_table = world.damage_table[player].damage_table['SubClassTable']
# subclass_table[EnemySprite.Thief] = subclass_table[EnemySprite.GreenEyegoreMimic]
# data_tables.enemy_stats[EnemySprite.Thief].health = 4
# could turn droppable on here if we wanted for killable theives
# health shuffle
if world.enemy_health[player] != 'default':
stats = world.data_tables[player].enemy_stats
min_health = {'easy': 1, 'normal': 2, 'hard': 2, 'expert': 4}
max_health = {'easy': 4, 'normal': 15, 'hard': 25, 'expert': 50}
min_h = min_health[world.enemy_health[player]]
max_h = max_health[world.enemy_health[player]]
for sprite, stat in stats.items():
if sprite == EnemySprite.Octorok4Way:
stat.health = stats[EnemySprite.Octorok].health # these guys share data
elif sprite == EnemySprite.GreenMimic:
stat.health = stats[EnemySprite.GreenEyegoreMimic].health # these share data
elif sprite == EnemySprite.RedMimic:
stat.health = stats[EnemySprite.RedEyegoreMimic].health # these share data
elif sprite not in skip_sprites:
if isinstance(stat.health, tuple):
stat.health = random.randint(min_h, max_h), random.randint(min_h, max_h)
else:
stat.health = random.randint(min_h, max_h)
if world.enemy_damage[player] != 'default':
stats = world.data_tables[player].enemy_stats
# randomize damage groupings
for sprite, stat in stats.items():
if sprite == EnemySprite.Octorok4Way:
stat.damage = stats[EnemySprite.Octorok].damage # these guys share data
elif sprite == EnemySprite.GreenMimic:
stat.damage = stats[EnemySprite.GreenEyegoreMimic].damage # these share data
elif sprite == EnemySprite.RedMimic:
stat.damage = stats[EnemySprite.RedEyegoreMimic].damage # these share data
elif sprite not in skip_sprites:
if isinstance(stat.damage, tuple):
stat.damage = random.randint(0, 8), random.randint(0, 8)
else:
stat.damage = random.randint(0, 8)
# randomize bump table
for i in range(0, 10):
max_damage = 64 if i == 9 or world.enemy_damage[player] == 'random' else 32
green_mail = random.randint(0, max_damage)
if world.enemy_damage[player] == 'random':
blue_mail = random.randint(0, max_damage)
red_mail = random.randint(0, max_damage)
else:
blue_mail = (green_mail * 3) // 4
red_mail = (green_mail * 3) // 8
world.data_tables[player].enemy_damage[i] = [green_mail, blue_mail, red_mail]
def write_enemy_shuffle_settings(world, player, rom):
if world.dropshuffle[player] in ['underworld']:
rom.write_byte(snes_to_pc(0x368109), 0x01)
if world.enemy_shuffle[player] != 'none':
# enable new mimics
rom.write_byte(snes_to_pc(0x368105), 0x01)
# killable thief
# rom.write_byte(snes_to_pc(0x368108), 0xc4)
# rom.write_byte(snes_to_pc(0x0DB237), 4) # health value - randomize it if killable, maybe
# mimic room barriers
data_tables = world.data_tables[player]
mimic_room = data_tables.room_list[0x10c] = Room010C
mimic_room.layer1[40].data[0] = 0x54 # rail adjust
mimic_room.layer1[40].data[1] = 0x9C
mimic_room.layer1[45].data[1] = 0xB0 # block adjust 1
mimic_room.layer1[47].data[1] = 0xD0 # block adjust 2
# random tile pattern
pattern_name, tile_pattern = random.choice(tile_patterns)
rom.write_byte(snes_to_pc(0x9BA1D), len(tile_pattern))
for idx, pair in enumerate(tile_pattern):
rom.write_byte(snes_to_pc(0x09BA2A + idx), (pair[0] + 3) * 16)
rom.write_byte(snes_to_pc(0x09BA40 + idx), (pair[1] + 4) * 16)
if world.enemy_shuffle[player] == 'random':
rom.write_byte(snes_to_pc(0x368100), 1) # randomize bushes