Merge remote-tracking branch 'codemann/OverworldShuffle' into codemann_OverworldShuffle
This commit is contained in:
@@ -90,7 +90,8 @@ class CustomSettings(object):
|
||||
args.mystery = True
|
||||
else:
|
||||
settings = defaultdict(lambda: None, player_setting)
|
||||
args.ow_shuffle[p] = get_setting(settings['ow_shuffle'], args.ow_shuffle[p])
|
||||
args.ow_layout[p] = get_setting(settings['ow_layout'], args.ow_layout[p])
|
||||
args.ow_parallel[p] = get_setting(settings['ow_parallel'], args.ow_parallel[p])
|
||||
args.ow_terrain[p] = get_setting(settings['ow_terrain'], args.ow_terrain[p])
|
||||
args.ow_crossed[p] = get_setting(settings['ow_crossed'], args.ow_crossed[p])
|
||||
if args.ow_crossed[p] == 'chaos':
|
||||
@@ -101,6 +102,7 @@ class CustomSettings(object):
|
||||
args.ow_mixed[p] = get_setting(settings['ow_mixed'], args.ow_mixed[p])
|
||||
args.ow_whirlpool[p] = get_setting(settings['ow_whirlpool'], args.ow_whirlpool[p])
|
||||
args.ow_fluteshuffle[p] = get_setting(settings['ow_fluteshuffle'], args.ow_fluteshuffle[p])
|
||||
args.ow_fog[p] = get_setting(settings['ow_fog'], args.ow_fog[p])
|
||||
args.shuffle_followers[p] = get_setting(settings['shuffle_followers'], args.shuffle_followers[p])
|
||||
args.bonk_drops[p] = get_setting(settings['bonk_drops'], args.bonk_drops[p])
|
||||
args.shuffle[p] = get_setting(settings['shuffle'], args.shuffle[p])
|
||||
@@ -136,6 +138,14 @@ class CustomSettings(object):
|
||||
args.take_any[p] = 'random' if args.take_any[p] == 'none' else args.take_any[p]
|
||||
args.keyshuffle[p] = 'universal'
|
||||
|
||||
ow_shuffle = get_setting(settings['ow_shuffle'], args.ow_shuffle[p])
|
||||
if ow_shuffle == 'parallel':
|
||||
args.ow_layout = 'wild'
|
||||
args.ow_parallel = True
|
||||
elif ow_shuffle == 'full':
|
||||
args.ow_layout = 'wild'
|
||||
args.ow_parallel = False
|
||||
|
||||
args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p])
|
||||
args.standardize_palettes[p] = get_setting(settings['standardize_palettes'],
|
||||
args.standardize_palettes[p])
|
||||
@@ -254,6 +264,11 @@ class CustomSettings(object):
|
||||
return self.file_source['ow-edges']
|
||||
return None
|
||||
|
||||
def get_owgrid(self):
|
||||
if 'ow-grid' in self.file_source:
|
||||
return self.file_source['ow-grid']
|
||||
return None
|
||||
|
||||
def get_owcrossed(self):
|
||||
if 'ow-crossed' in self.file_source:
|
||||
return self.file_source['ow-crossed']
|
||||
@@ -357,13 +372,15 @@ class CustomSettings(object):
|
||||
self.world_rep['start_inventory'] = start_inv = {}
|
||||
for p in self.player_range:
|
||||
settings_dict[p] = {}
|
||||
settings_dict[p]['ow_shuffle'] = world.owShuffle[p]
|
||||
settings_dict[p]['ow_layout'] = world.owLayout[p]
|
||||
settings_dict[p]['ow_parallel'] = world.owParallel[p]
|
||||
settings_dict[p]['ow_terrain'] = world.owTerrain[p]
|
||||
settings_dict[p]['ow_crossed'] = world.owCrossed[p]
|
||||
settings_dict[p]['ow_keepsimilar'] = world.owKeepSimilar[p]
|
||||
settings_dict[p]['ow_mixed'] = world.owMixed[p]
|
||||
settings_dict[p]['ow_whirlpool'] = world.owWhirlpoolShuffle[p]
|
||||
settings_dict[p]['ow_fluteshuffle'] = world.owFluteShuffle[p]
|
||||
settings_dict[p]['ow_fog'] = world.owFog[p]
|
||||
settings_dict[p]['shuffle_followers'] = world.shuffle_followers[p]
|
||||
settings_dict[p]['bonk_drops'] = world.shuffle_bonk_drops[p]
|
||||
settings_dict[p]['shuffle'] = world.shuffle[p]
|
||||
|
||||
@@ -92,13 +92,15 @@ SETTINGSTOPROCESS = {
|
||||
"bombbag": "bombbag"
|
||||
},
|
||||
"overworld": {
|
||||
"overworldshuffle": "ow_shuffle",
|
||||
"layout": "ow_layout",
|
||||
"parallel": "ow_parallel",
|
||||
"terrain": "ow_terrain",
|
||||
"crossed": "ow_crossed",
|
||||
"keepsimilar": "ow_keepsimilar",
|
||||
"mixed": "ow_mixed",
|
||||
"whirlpool": "ow_whirlpool",
|
||||
"overworldflute": "ow_fluteshuffle"
|
||||
"overworldflute": "ow_fluteshuffle",
|
||||
"fog": "ow_fog"
|
||||
},
|
||||
"entrance": {
|
||||
"entranceshuffle": "shuffle",
|
||||
|
||||
@@ -2116,8 +2116,13 @@ class EnemyTable:
|
||||
self.room_map = defaultdict(list)
|
||||
self.special_bitmasks = None
|
||||
|
||||
def write_sprite_data_to_rom(self, rom):
|
||||
pointer_address = snes_to_pc(0x09D62E)
|
||||
def write_sprite_data_to_rom(self, rom, pointer_table):
|
||||
ow_data_end = pointer_table['ow_sprites'][0] + pointer_table['ow_sprites'][1]
|
||||
if ow_data_end > pointer_table['uw_sprites'][2]:
|
||||
# moving pointer table down
|
||||
pointer_table['uw_sprites'][2] = ow_data_end
|
||||
rom.write_bytes(snes_to_pc(pointer_table['uw_sprites'][3]), int16_as_bytes(ow_data_end & 0xFFFF))
|
||||
pointer_address = snes_to_pc(pointer_table['uw_sprites'][2])
|
||||
data_pointer = snes_to_pc(0x288000)
|
||||
empty_pointer = pc_to_snes(data_pointer) & 0xFFFF
|
||||
rom.write_bytes(data_pointer, [0x00, 0xff])
|
||||
|
||||
@@ -183,9 +183,9 @@ def init_sprite_requirements():
|
||||
SpriteRequirement(EnemySprite.Vulture).no_drop().sub_group(2, 0x12).exclude(NoFlyingRooms),
|
||||
SpriteRequirement(EnemySprite.CorrectPullSwitch).affix().sub_group(3, [0x52, 0x53]),
|
||||
SpriteRequirement(EnemySprite.WrongPullSwitch).affix().sub_group(3, [0x52, 0x53]),
|
||||
SpriteRequirement(EnemySprite.Octorok).aquaphobia().sub_group(2, [0xc, 0x18]),
|
||||
SpriteRequirement(EnemySprite.Octorok).sub_group(2, [0xc, 0x18]),
|
||||
SpriteRequirement(EnemySprite.Moldorm).exalt().sub_group(2, 0x30),
|
||||
SpriteRequirement(EnemySprite.Octorok4Way).aquaphobia().sub_group(2, 0xc),
|
||||
SpriteRequirement(EnemySprite.Octorok4Way).sub_group(2, 0xc),
|
||||
SpriteRequirement(EnemySprite.Cucco).immune().sub_group(3, [0x15, 0x50]).exclude(NoFlyingRooms),
|
||||
SpriteRequirement(EnemySprite.Buzzblob).sub_group(3, 0x11),
|
||||
SpriteRequirement(EnemySprite.Snapdragon).sub_group(0, 0x16).sub_group(2, 0x17),
|
||||
@@ -197,7 +197,7 @@ def init_sprite_requirements():
|
||||
.exclude(NoFlyingRooms).exclude({0x40}), # no anti-fairies in aga tower bridge room
|
||||
SpriteRequirement(EnemySprite.Wiseman).affix().sub_group(2, 0x4c),
|
||||
SpriteRequirement(EnemySprite.Hoarder).sub_group(3, 0x11).exclude({0x10c}),
|
||||
SpriteRequirement(EnemySprite.MiniMoldorm).aquaphobia().sub_group(1, 0x1e),
|
||||
SpriteRequirement(EnemySprite.MiniMoldorm).sub_group(1, 0x1e),
|
||||
SpriteRequirement(EnemySprite.Poe).no_drop().sub_group(3, 0x15).exclude(NoFlyingRooms),
|
||||
SpriteRequirement(EnemySprite.Smithy).affix().sub_group(1, 0x1d).sub_group(3, 0x15),
|
||||
SpriteRequirement(EnemySprite.Statue).stasis().immune().sub_group(3, [0x52, 0x53]),
|
||||
@@ -231,12 +231,12 @@ def init_sprite_requirements():
|
||||
SpriteRequirement(EnemySprite.Hoarder2).sub_group(3, 0x11).exclude({0x10c}),
|
||||
SpriteRequirement(EnemySprite.TutorialGuard).affix(),
|
||||
SpriteRequirement(EnemySprite.LightningGate).affix().sub_group(3, 0x3f),
|
||||
SpriteRequirement(EnemySprite.BlueGuard).aquaphobia().sub_group(1, [0xd, 0x49]).exclude(PitRooms),
|
||||
SpriteRequirement(EnemySprite.BlueGuard).aquaphobia().sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]),
|
||||
SpriteRequirement(EnemySprite.GreenGuard).aquaphobia().sub_group(1, 0x49).exclude(PitRooms),
|
||||
SpriteRequirement(EnemySprite.GreenGuard).aquaphobia().sub_group(1, 0x49).sub_group(2, 0x13),
|
||||
SpriteRequirement(EnemySprite.RedSpearGuard).aquaphobia().sub_group(1, [0xd, 0x49]).exclude(PitRooms),
|
||||
SpriteRequirement(EnemySprite.RedSpearGuard).aquaphobia().sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]),
|
||||
SpriteRequirement(EnemySprite.BlueGuard).sub_group(1, [0xd, 0x49]).exclude(PitRooms),
|
||||
SpriteRequirement(EnemySprite.BlueGuard).sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]),
|
||||
SpriteRequirement(EnemySprite.GreenGuard).sub_group(1, 0x49).exclude(PitRooms),
|
||||
SpriteRequirement(EnemySprite.GreenGuard).sub_group(1, 0x49).sub_group(2, 0x13),
|
||||
SpriteRequirement(EnemySprite.RedSpearGuard).sub_group(1, [0xd, 0x49]).exclude(PitRooms),
|
||||
SpriteRequirement(EnemySprite.RedSpearGuard).sub_group(1, [0xd, 0x49]).sub_group(2, [0x29, 0x13]),
|
||||
SpriteRequirement(EnemySprite.BluesainBolt).aquaphobia().sub_group(0, 0x46).sub_group(1, [0xd, 0x49]),
|
||||
SpriteRequirement(EnemySprite.UsainBolt).aquaphobia().sub_group(1, [0xd, 0x49]),
|
||||
SpriteRequirement(EnemySprite.BlueArcher).sub_group(0, 0x48).sub_group(1, 0x49),
|
||||
|
||||
@@ -147,139 +147,149 @@ door_addresses = {'Links House': (0x00, (0x0104, 0x2c
|
||||
'Lake Hylia Fortune Teller': (0x72, (0x0122, 0x35, 0x0380, 0x0c6a, 0x0a00, 0x0cb8, 0x0a58, 0x0cd7, 0x0a85, 0x06, 0xfa, 0x0000, 0x0000), 0x00),
|
||||
'Kakariko Gamble Game': (0x66, (0x0118, 0x29, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000), 0x00)}
|
||||
|
||||
ow_prize_table = {'Links House': (0x8b1, 0xb2d),
|
||||
'Desert Palace Entrance (South)': (0x108, 0xd70), 'Desert Palace Entrance (West)': (0x031, 0xca0),
|
||||
'Desert Palace Entrance (North)': (0x0e1, 0xba0), 'Desert Palace Entrance (East)': (0x191, 0xca0),
|
||||
'Eastern Palace': (0xf31, 0x620), 'Tower of Hera': (0x8D0, 0x080),
|
||||
'Hyrule Castle Entrance (South)': (0x820, 0x730), 'Hyrule Castle Entrance (West)': (0x740, 0x5D0),
|
||||
'Hyrule Castle Entrance (East)': (0x8f0, 0x5D0),
|
||||
'Agahnims Tower': (0x820, 0x5D0),
|
||||
'Thieves Town': (0x1d0, 0x780), 'Skull Woods First Section Door': (0x2e0, 0x280),
|
||||
'Skull Woods Second Section Door (East)': (0x200, 0x240),
|
||||
'Skull Woods Second Section Door (West)': (0x0c0, 0x1c0),
|
||||
'Skull Woods Final Section': (0x082, 0x0b0),
|
||||
'Skull Woods First Section Hole (West)': (0x200, 0x2b0),
|
||||
'Skull Woods First Section Hole (East)': (0x340, 0x2e0),
|
||||
'Skull Woods First Section Hole (North)': (0x320, 0x1e0),
|
||||
'Skull Woods Second Section Hole': (0x0f0, 0x0b0),
|
||||
'Ice Palace': (0xca0, 0xda0),
|
||||
'Misery Mire': (0x100, 0xca0),
|
||||
'Palace of Darkness': (0xf40, 0x620), 'Swamp Palace': (0x759, 0xED0),
|
||||
'Turtle Rock': (0xf11, 0x103),
|
||||
'Dark Death Mountain Ledge (West)': (0xb80, 0x180),
|
||||
'Dark Death Mountain Ledge (East)': (0xc80, 0x180),
|
||||
'Turtle Rock Isolated Ledge Entrance': (0xc00, 0x240),
|
||||
ow_prize_table = {'Lost Woods Gamble': (0x2A0, 0x080),
|
||||
'Lost Woods Hideout Drop': (0x338, 0x218),
|
||||
'Lost Woods Hideout Stump': (0x2A0, 0x2E0),
|
||||
'Lumberjack House': (0x538, 0x158),
|
||||
'Lumberjack Tree Tree': (0x4A0, 0x160),
|
||||
'Lumberjack Tree Cave': (0x550, 0x004),
|
||||
'Tower of Hera': (0x900, 0x0E0),
|
||||
'Spectacle Rock Cave Peak': (0x758, 0x1B7),
|
||||
'Spectacle Rock Cave (Bottom)': (0x6F0, 0x257),
|
||||
'Spectacle Rock Cave': (0x7C8, 0x257),
|
||||
'Death Mountain Return Cave (East)': (0x648, 0x278),
|
||||
'Old Man Cave (East)': (0x658, 0x387),
|
||||
'Old Man House (Top)': (0x8C8, 0x2D8),
|
||||
'Old Man House (Bottom)': (0x728, 0x3A7),
|
||||
'Paradox Cave (Top)': (0xDE0, 0x100),
|
||||
'Paradox Cave (Bottom)': (0xDC8, 0x358),
|
||||
'Paradox Cave (Middle)': (0xDDF, 0x3FF),
|
||||
'Fairy Ascension Cave (Top)': (0xCD8, 0x2B8),
|
||||
'Fairy Ascension Cave (Bottom)': (0xC78, 0x358),
|
||||
'Spiral Cave': (0xC78, 0x207),
|
||||
'Spiral Cave (Bottom)': (0xBC8, 0x2F7),
|
||||
'Mimic Cave': (0xD38, 0x207),
|
||||
'Hookshot Fairy': (0xD38, 0x358),
|
||||
'Death Mountain Return Cave (West)': (0x5B8, 0x247),
|
||||
'Old Man Cave (West)': (0x5B8, 0x348),
|
||||
'Waterfall of Wishing': (0xE80, 0x2C7),
|
||||
'Fortune Teller (Light)': (0x308, 0x5A8),
|
||||
'Bonk Rock Cave': (0x600, 0x517),
|
||||
'Sanctuary': (0x748, 0x4F4),
|
||||
'Sanctuary Grave': (0x860, 0x550),
|
||||
'Graveyard Cave': (0x8F8, 0x4A6),
|
||||
'Kings Grave': (0x978, 0x566),
|
||||
'North Fairy Cave Drop': (0xA40, 0x580),
|
||||
'North Fairy Cave': (0xAA8, 0x4A8),
|
||||
'Potion Shop': (0xCB8, 0x598),
|
||||
'Kakariko Well Drop': (0x030, 0x6C0),
|
||||
'Kakariko Well Cave': (0x0D8, 0x6F7),
|
||||
'Blinds Hideout': (0x1B8, 0x6F7),
|
||||
'Elder House (West)': (0x258, 0x6F7),
|
||||
'Elder House (East)': (0x2E8, 0x6F7),
|
||||
'Snitch Lady (West)': (0x0C8, 0x7E8),
|
||||
'Snitch Lady (East)': (0x338, 0x7E8),
|
||||
'Chicken House': (0x188, 0x8E8),
|
||||
'Sick Kids House': (0x278, 0x8E8),
|
||||
'Bush Covered House': (0x338, 0x8E8),
|
||||
'Light World Bomb Hut': (0x068, 0x9D7),
|
||||
'Kakariko Shop': (0x1C8, 0x9D7),
|
||||
'Tavern North': (0x2B8, 0x977),
|
||||
'Tavern (Front)': (0x2B8, 0x9F7),
|
||||
'Agahnims Tower': (0x7F0, 0x630),
|
||||
'Hyrule Castle Entrance (South)': (0x7F0, 0x750),
|
||||
'Hyrule Castle Entrance (West)': (0x758, 0x644),
|
||||
'Hyrule Castle Entrance (East)': (0x8E8, 0x644),
|
||||
'Hyrule Castle Secret Entrance Drop': (0x9D0, 0x680),
|
||||
'Hyrule Castle Secret Entrance Stairs': (0x8D0, 0x700),
|
||||
'Kakariko Well Drop': (0x030, 0x680),
|
||||
'Kakariko Well Cave': (0x060, 0x680),
|
||||
'Bat Cave Drop': (0x520, 0x8f0),
|
||||
'Bat Cave Cave': (0x560, 0x940),
|
||||
'Elder House (East)': (0x2b0, 0x6a0),
|
||||
'Elder House (West)': (0x230, 0x6a0),
|
||||
'North Fairy Cave Drop': (0xa40, 0x500),
|
||||
'North Fairy Cave': (0xa80, 0x440),
|
||||
'Lost Woods Hideout Drop': (0x290, 0x200),
|
||||
'Lost Woods Hideout Stump': (0x240, 0x280),
|
||||
'Lumberjack Tree Tree': (0x4e0, 0x140),
|
||||
'Lumberjack Tree Cave': (0x560, 0x004),
|
||||
'Two Brothers House (East)': (0x200, 0x0b60),
|
||||
'Two Brothers House (West)': (0x180, 0x0b60),
|
||||
'Sanctuary Grave': (0x820, 0x4c0),
|
||||
'Sanctuary': (0x720, 0x4a0),
|
||||
'Old Man Cave (West)': (0x580, 0x2c0),
|
||||
'Old Man Cave (East)': (0x620, 0x2c0),
|
||||
'Old Man House (Bottom)': (0x720, 0x320),
|
||||
'Old Man House (Top)': (0x820, 0x220),
|
||||
'Death Mountain Return Cave (East)': (0x600, 0x220),
|
||||
'Death Mountain Return Cave (West)': (0x500, 0x1c0),
|
||||
'Spectacle Rock Cave Peak': (0x720, 0x0a0),
|
||||
'Spectacle Rock Cave': (0x790, 0x1a0),
|
||||
'Spectacle Rock Cave (Bottom)': (0x710, 0x0a0),
|
||||
'Paradox Cave (Bottom)': (0xd80, 0x180),
|
||||
'Paradox Cave (Middle)': (0xd80, 0x380),
|
||||
'Paradox Cave (Top)': (0xd80, 0x020),
|
||||
'Fairy Ascension Cave (Bottom)': (0xcc8, 0x2a0),
|
||||
'Fairy Ascension Cave (Top)': (0xc00, 0x240),
|
||||
'Spiral Cave': (0xb80, 0x180),
|
||||
'Spiral Cave (Bottom)': (0xb80, 0x2c0),
|
||||
'Bumper Cave (Bottom)': (0x580, 0x2c0),
|
||||
'Bumper Cave (Top)': (0x500, 0x1c0),
|
||||
'Superbunny Cave (Top)': (0xd80, 0x020),
|
||||
'Superbunny Cave (Bottom)': (0xd00, 0x180),
|
||||
'Hookshot Cave': (0xc80, 0x0c0),
|
||||
'Hookshot Cave Back Entrance': (0xcf0, 0x004),
|
||||
'Ganons Tower': (0x8D0, 0x080),
|
||||
'Pyramid Hole': (0x820, 0x680),
|
||||
'Inverted Pyramid Hole': (0x820, 0x680),
|
||||
'Pyramid Entrance': (0x640, 0x7c0),
|
||||
'Inverted Pyramid Entrance': (0x6C0, 0x5D0),
|
||||
'Waterfall of Wishing': (0xe80, 0x280),
|
||||
'Dam': (0x759, 0xED0),
|
||||
'Blinds Hideout': (0x190, 0x6c0),
|
||||
'Bonk Fairy (Light)': (0x740, 0xa80),
|
||||
'Lake Hylia Fairy': (0xd40, 0x9f0),
|
||||
'Light Hype Fairy': (0x940, 0xc80),
|
||||
'Desert Fairy': (0x420, 0xe00),
|
||||
'Kings Grave': (0x920, 0x520),
|
||||
'Tavern North': (0x270, 0x900),
|
||||
'Chicken House': (0x120, 0x880),
|
||||
'Aginahs Cave': (0x2e0, 0xd00),
|
||||
'Sahasrahlas Hut': (0xcf0, 0x6c0),
|
||||
'Lake Hylia Shop': (0xbc0, 0xc00),
|
||||
'Capacity Upgrade': (0xca0, 0xda0),
|
||||
'Blacksmiths Hut': (0x4a0, 0x880),
|
||||
'Sick Kids House': (0x220, 0x880),
|
||||
'Lost Woods Gamble': (0x240, 0x080),
|
||||
'Fortune Teller (Light)': (0x2c0, 0x4c0),
|
||||
'Snitch Lady (East)': (0x310, 0x7a0),
|
||||
'Snitch Lady (West)': (0x080, 0x7a0),
|
||||
'Bush Covered House': (0x2e0, 0x880),
|
||||
'Tavern (Front)': (0x270, 0x980),
|
||||
'Light World Bomb Hut': (0x070, 0x980),
|
||||
'Kakariko Shop': (0x170, 0x980),
|
||||
'Cave 45': (0x440, 0xca0), 'Graveyard Cave': (0x8f0, 0x430),
|
||||
'Checkerboard Cave': (0x260, 0xc00),
|
||||
'Mini Moldorm Cave': (0xa40, 0xe80),
|
||||
'Long Fairy Cave': (0xf60, 0xb00),
|
||||
'Good Bee Cave': (0xec0, 0xc00),
|
||||
'20 Rupee Cave': (0xe80, 0xca0),
|
||||
'50 Rupee Cave': (0x4d0, 0xed0),
|
||||
'Ice Rod Cave': (0xe00, 0xc00),
|
||||
'Bonk Rock Cave': (0x5f0, 0x460),
|
||||
'Library': (0x270, 0xaa0),
|
||||
'Potion Shop': (0xc80, 0x4c0),
|
||||
'Hookshot Fairy': (0xd00, 0x180),
|
||||
'Pyramid Fairy': (0x740, 0x740),
|
||||
'East Dark World Hint': (0xf60, 0xb00),
|
||||
'Palace of Darkness Hint': (0xd60, 0x7c0),
|
||||
'Dark Lake Hylia Fairy': (0xd40, 0x9f0),
|
||||
'Dark Lake Hylia Ledge Fairy': (0xe00, 0xc00),
|
||||
'Dark Lake Hylia Ledge Spike Cave': (0xe80, 0xca0),
|
||||
'Dark Lake Hylia Ledge Hint': (0xec0, 0xc00),
|
||||
'Hype Cave': (0x940, 0xc80),
|
||||
'Bonk Fairy (Dark)': (0x740, 0xa80),
|
||||
'Brewery': (0x170, 0x980), 'C-Shaped House': (0x310, 0x7a0), 'Chest Game': (0x080, 0x7a0),
|
||||
'Hammer Peg Cave': (0x4c0, 0x940),
|
||||
'Red Shield Shop': (0x500, 0x680),
|
||||
'Dark Sanctuary Hint': (0x720, 0x4a0),
|
||||
'Fortune Teller (Dark)': (0x2c0, 0x4c0),
|
||||
'Dark World Shop': (0x2e0, 0x880),
|
||||
'Dark Lumberjack Shop': (0x4e0, 0x0d0),
|
||||
'Dark Potion Shop': (0xc80, 0x4c0),
|
||||
'Archery Game': (0x2f0, 0xaf0),
|
||||
'Mire Shed': (0x060, 0xc90),
|
||||
'Mire Hint': (0x2e0, 0xd00),
|
||||
'Mire Fairy': (0x1c0, 0xc90),
|
||||
'Spike Cave': (0x860, 0x180),
|
||||
'Dark Death Mountain Shop': (0xd80, 0x180),
|
||||
'Dark Death Mountain Fairy': (0x620, 0x2c0),
|
||||
'Mimic Cave': (0xc80, 0x180),
|
||||
'Big Bomb Shop': (0x8b1, 0xb2d),
|
||||
'Dark Lake Hylia Shop': (0xa40, 0xc40),
|
||||
'Lumberjack House': (0x580, 0x100),
|
||||
'Lake Hylia Fortune Teller': (0xa40, 0xc40),
|
||||
'Kakariko Gamble Game': (0x2f0, 0xaf0)}
|
||||
'Hyrule Castle Secret Entrance Stairs': (0x8C8, 0x718),
|
||||
'Inverted Pyramid Hole': (0x7F0, 0x6B0),
|
||||
'Inverted Pyramid Entrance': (0x6E8, 0x644),
|
||||
'Sahasrahlas Hut': (0xCF0, 0x747),
|
||||
'Eastern Palace': (0xF40, 0x680),
|
||||
'Blacksmiths Hut': (0x4E8, 0x8C8),
|
||||
'Bat Cave Drop': (0x538, 0x9A8),
|
||||
'Bat Cave Cave': (0x510, 0x930),
|
||||
'Two Brothers House (West)': (0x1B8, 0xB88),
|
||||
'Two Brothers House (East)': (0x238, 0xB88),
|
||||
'Library': (0x2B8, 0xAA7),
|
||||
'Kakariko Gamble Game': (0x348, 0xB78),
|
||||
'Bonk Fairy (Light)': (0x788, 0xA87),
|
||||
'Links House': (0x8B8, 0xB58),
|
||||
'Lake Hylia Fairy': (0xD58, 0xA57),
|
||||
'Long Fairy Cave': (0xF68, 0xB58),
|
||||
'Desert Palace Entrance (North)': (0x148, 0xC28),
|
||||
'Desert Palace Entrance (West)': (0x088, 0xCF8),
|
||||
'Desert Palace Entrance (East)': (0x1E8, 0xCF8),
|
||||
'Desert Palace Entrance (South)': (0x138, 0xD18),
|
||||
'Checkerboard Cave': (0x2E8, 0xCF7),
|
||||
'Aginahs Cave': (0x388, 0xDE8),
|
||||
'Cave 45': (0x448, 0xD86),
|
||||
'Light Hype Fairy': (0x988, 0xCF8),
|
||||
'Lake Hylia Fortune Teller': (0xA68, 0xCC8),
|
||||
'Lake Hylia Shop': (0xBF8, 0xC66),
|
||||
'Capacity Upgrade': (0xCC8, 0xDE8),
|
||||
'Mini Moldorm Cave': (0xA88, 0xEF8),
|
||||
'Ice Rod Cave': (0xE68, 0xC37),
|
||||
'Good Bee Cave': (0xEF8, 0xC36),
|
||||
'20 Rupee Cave': (0xEB8, 0xCC6),
|
||||
'Desert Fairy': (0x488, 0xE76),
|
||||
'50 Rupee Cave': (0x4F8, 0xF47),
|
||||
'Dam': (0x778, 0xF68),
|
||||
|
||||
'Skull Woods Final Section': (0x078, 0x0A8),
|
||||
'Skull Woods Second Section Door (West)': (0x0E0, 0x1C0),
|
||||
'Skull Woods Second Section Hole': (0x150, 0x120),
|
||||
'Skull Woods Second Section Door (East)': (0x208, 0x208),
|
||||
'Skull Woods First Section Door': (0x2C8, 0x2A8),
|
||||
'Skull Woods First Section Hole (North)': (0x320, 0x1e0),
|
||||
'Skull Woods First Section Hole (West)': (0x240, 0x380),
|
||||
'Skull Woods First Section Hole (East)': (0x340, 0x380),
|
||||
'Dark Lumberjack Shop': (0x538, 0x177),
|
||||
'Ganons Tower': (0x900, 0x0E0),
|
||||
'Dark Death Mountain Fairy': (0x658, 0x387),
|
||||
'Spike Cave': (0x9F8, 0x268),
|
||||
'Hookshot Cave Back Entrance': (0xD38, 0x038),
|
||||
'Hookshot Cave': (0xD48, 0x107),
|
||||
'Superbunny Cave (Top)': (0xDE0, 0x100),
|
||||
'Superbunny Cave (Bottom)': (0xD38, 0x358),
|
||||
'Dark Death Mountain Ledge (West)': (0xC78, 0x207),
|
||||
'Dark Death Mountain Ledge (East)': (0xD38, 0x207),
|
||||
'Turtle Rock Isolated Ledge Entrance': (0xCD8, 0x2B8),
|
||||
'Dark Death Mountain Shop': (0xDC8, 0x358),
|
||||
'Turtle Rock': (0xF48, 0x108),
|
||||
'Bumper Cave (Top)': (0x5B8, 0x247),
|
||||
'Bumper Cave (Bottom)': (0x5B8, 0x348),
|
||||
'Fortune Teller (Dark)': (0x308, 0x5A8),
|
||||
'Dark Sanctuary Hint': (0x748, 0x4F4),
|
||||
'Dark Potion Shop': (0xCB8, 0x598),
|
||||
'Chest Game': (0x0C8, 0x7E8),
|
||||
'Thieves Town': (0x1F0, 0x7E8),
|
||||
'C-Shaped House': (0x338, 0x7E8),
|
||||
'Brewery': (0x1C8, 0x9D7),
|
||||
'Dark World Shop': (0x338, 0x8E8),
|
||||
'Red Shield Shop': (0x538, 0x778),
|
||||
'Pyramid Hole': (0x800, 0x680),
|
||||
'Pyramid Entrance': (0x718, 0x7A8),
|
||||
'Pyramid Fairy': (0x7B8, 0x7A8),
|
||||
'Palace of Darkness': (0xF40, 0x680),
|
||||
'Palace of Darkness Hint': (0xD70, 0x878),
|
||||
'Hammer Peg Cave': (0x528, 0x9B6),
|
||||
'Archery Game': (0x348, 0xB78),
|
||||
'Bonk Fairy (Dark)': (0x788, 0xA87),
|
||||
'Big Bomb Shop': (0x8B8, 0xB58),
|
||||
'Dark Lake Hylia Fairy': (0xD58, 0xA57),
|
||||
'East Dark World Hint': (0xF68, 0xB58),
|
||||
'Mire Shed': (0x0A8, 0xCC7),
|
||||
'Misery Mire': (0x148, 0xCC7),
|
||||
'Mire Fairy': (0x1E8, 0xCC7),
|
||||
'Mire Hint': (0x388, 0xDE8),
|
||||
'Hype Cave': (0x988, 0xCF8),
|
||||
'Dark Lake Hylia Shop': (0xA68, 0xCC8),
|
||||
'Ice Palace': (0xCA8, 0xE28),
|
||||
'Dark Lake Hylia Ledge Fairy': (0xE68, 0xC37),
|
||||
'Dark Lake Hylia Ledge Hint': (0xEF8, 0xC36),
|
||||
'Dark Lake Hylia Ledge Spike Cave': (0xEB8, 0xCC6),
|
||||
'Swamp Palace': (0x778, 0xF68)}
|
||||
|
||||
default_connector_connections = [('Death Mountain Return Cave (West)', 'Death Mountain Return Cave Exit (West)'),
|
||||
('Death Mountain Return Cave (East)', 'Death Mountain Return Cave Exit (East)'),
|
||||
|
||||
@@ -634,7 +634,7 @@ def do_dark_sanc(entrances, exits, avail):
|
||||
forbidden.append('Links House')
|
||||
else:
|
||||
forbidden.append('Big Bomb Shop')
|
||||
if avail.world.owShuffle[avail.player] == 'vanilla':
|
||||
if avail.world.owLayout[avail.player] == 'vanilla':
|
||||
choices = [e for e in avail.world.districts[avail.player]['Northwest Dark World'].entrances if e not in forbidden and e in entrances]
|
||||
else:
|
||||
choices = [e for e in get_starting_entrances(avail) if e not in forbidden and e in entrances]
|
||||
@@ -678,7 +678,7 @@ def do_links_house(entrances, exits, avail, cross_world):
|
||||
forbidden.append(links_house_vanilla)
|
||||
forbidden.extend(Forbidden_Swap_Entrances)
|
||||
shuffle_mode = avail.world.shuffle[avail.player]
|
||||
if avail.world.owShuffle[avail.player] == 'vanilla':
|
||||
if avail.world.owLayout[avail.player] == 'vanilla':
|
||||
# simple shuffle -
|
||||
if shuffle_mode == 'simple':
|
||||
avail.links_on_mountain = True # taken care of by the logic below
|
||||
@@ -732,7 +732,7 @@ def do_links_house(entrances, exits, avail, cross_world):
|
||||
|
||||
# links on dm
|
||||
dm_spots = LH_DM_Connector_List.union(LH_DM_Exit_Forbidden)
|
||||
if links_house in dm_spots and avail.world.owShuffle[avail.player] == 'vanilla':
|
||||
if links_house in dm_spots and avail.world.owLayout[avail.player] == 'vanilla':
|
||||
if avail.links_on_mountain:
|
||||
return # connector is fine
|
||||
logging.getLogger('').warning(f'Links House is placed in tight area and is now unhandled. Report any errors that occur from here.')
|
||||
|
||||
1423
source/overworld/LayoutGenerator.py
Normal file
1423
source/overworld/LayoutGenerator.py
Normal file
File diff suppressed because it is too large
Load Diff
473
source/overworld/LayoutVisualizer.py
Normal file
473
source/overworld/LayoutVisualizer.py
Normal file
@@ -0,0 +1,473 @@
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
from PIL import Image, ImageDraw
|
||||
from BaseClasses import Direction, OWEdge
|
||||
from source.overworld.LayoutGenerator import Screen
|
||||
|
||||
def get_edge_lists(grid: List[List[List[int]]],
|
||||
overworld_screens: Dict[int, Screen],
|
||||
large_screen_quadrant_info: Dict[int, Dict]) -> Dict:
|
||||
"""
|
||||
Get list of edges for each cell and direction.
|
||||
|
||||
Args:
|
||||
grid: 3D list [world][row][col] containing screen IDs
|
||||
overworld_screens: Dict of screen_id -> Screen objects
|
||||
large_screen_quadrant_info: Dict of screen_id -> quadrant info for large screens
|
||||
|
||||
Returns:
|
||||
Dict mapping (world, row, col, direction) -> list of edges
|
||||
Each edge has a .dest property (None if unconnected)
|
||||
"""
|
||||
GRID_SIZE = 8
|
||||
edge_lists = {}
|
||||
|
||||
# Large screen base IDs
|
||||
large_screen_base_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35,
|
||||
0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75]
|
||||
|
||||
for world_idx in range(2):
|
||||
# Build a map of screen_id -> list of (row, col) positions for large screens
|
||||
large_screen_positions = {}
|
||||
for row in range(GRID_SIZE):
|
||||
for col in range(GRID_SIZE):
|
||||
screen_id = grid[world_idx][row][col]
|
||||
if screen_id != -1 and screen_id in large_screen_base_ids:
|
||||
if screen_id not in large_screen_positions:
|
||||
large_screen_positions[screen_id] = []
|
||||
large_screen_positions[screen_id].append((row, col))
|
||||
|
||||
for row in range(GRID_SIZE):
|
||||
for col in range(GRID_SIZE):
|
||||
screen_id = grid[world_idx][row][col]
|
||||
|
||||
if screen_id == -1:
|
||||
# Empty cell - no edges
|
||||
for direction in [Direction.North, Direction.South, Direction.East, Direction.West]:
|
||||
edge_lists[(world_idx, row, col, direction)] = []
|
||||
continue
|
||||
|
||||
screen = overworld_screens.get(screen_id)
|
||||
if not screen:
|
||||
for direction in [Direction.North, Direction.South, Direction.East, Direction.West]:
|
||||
edge_lists[(world_idx, row, col, direction)] = []
|
||||
continue
|
||||
|
||||
is_large = screen_id in large_screen_base_ids
|
||||
|
||||
if is_large:
|
||||
# For large screens, determine which quadrant this cell is
|
||||
# Find all positions of this large screen and determine quadrant
|
||||
positions = large_screen_positions.get(screen_id, [(row, col)])
|
||||
|
||||
# Determine quadrant by finding relative position
|
||||
# The quadrant is determined by which cells are adjacent
|
||||
quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE)
|
||||
|
||||
# Get edges for this quadrant
|
||||
if screen_id in large_screen_quadrant_info:
|
||||
quad_info = large_screen_quadrant_info[screen_id]
|
||||
|
||||
for direction in [Direction.North, Direction.South, Direction.East, Direction.West]:
|
||||
edges = quad_info.get(quadrant, {}).get(direction, [])
|
||||
edge_lists[(world_idx, row, col, direction)] = edges
|
||||
else:
|
||||
# No quadrant info - no edges
|
||||
for direction in [Direction.North, Direction.South, Direction.East, Direction.West]:
|
||||
edge_lists[(world_idx, row, col, direction)] = []
|
||||
else:
|
||||
# Small screen - get edges directly
|
||||
for direction in [Direction.North, Direction.South, Direction.East, Direction.West]:
|
||||
edges_in_dir = [e for e in screen.edges.values() if e.direction == direction]
|
||||
edge_lists[(world_idx, row, col, direction)] = edges_in_dir
|
||||
|
||||
return edge_lists
|
||||
|
||||
def determine_large_screen_quadrant(row: int, col: int, positions: List[tuple], grid_size: int) -> str:
|
||||
"""
|
||||
Determine which quadrant (NW, NE, SW, SE) a cell is in for a large screen.
|
||||
Handles wrapping correctly by checking adjacency patterns.
|
||||
|
||||
Args:
|
||||
row: Current cell row
|
||||
col: Current cell column
|
||||
positions: List of all (row, col) positions for this large screen
|
||||
grid_size: Size of the grid (8)
|
||||
|
||||
Returns:
|
||||
Quadrant string: "NW", "NE", "SW", or "SE"
|
||||
"""
|
||||
positions_set = set(positions)
|
||||
|
||||
# Check which adjacent cells also belong to this large screen
|
||||
has_right = ((row, (col + 1) % grid_size) in positions_set)
|
||||
has_below = (((row + 1) % grid_size, col) in positions_set)
|
||||
has_left = ((row, (col - 1) % grid_size) in positions_set)
|
||||
has_above = (((row - 1) % grid_size, col) in positions_set)
|
||||
|
||||
# Determine quadrant based on adjacency
|
||||
# NW: has right and below neighbors
|
||||
# NE: has left and below neighbors
|
||||
# SW: has right and above neighbors
|
||||
# SE: has left and above neighbors
|
||||
|
||||
if has_right and has_below:
|
||||
return "NW"
|
||||
elif has_left and has_below:
|
||||
return "NE"
|
||||
elif has_right and has_above:
|
||||
return "SW"
|
||||
elif has_left and has_above:
|
||||
return "SE"
|
||||
else:
|
||||
raise Exception("?")
|
||||
|
||||
def is_crossed_edge(edge: OWEdge, overworld_screens: Dict[int, Screen]) -> bool:
|
||||
if edge.dest is None:
|
||||
return False
|
||||
|
||||
source_screen = overworld_screens.get(edge.owIndex)
|
||||
dest_screen = overworld_screens.get(edge.dest.owIndex)
|
||||
return source_screen.dark_world != dest_screen.dark_world
|
||||
|
||||
def visualize_layout(grid: List[List[List[int]]], output_dir: str,
|
||||
overworld_screens: Dict[int, Screen],
|
||||
large_screen_quadrant_info: Dict[int, Dict]) -> None:
|
||||
# Constants
|
||||
GRID_SIZE = 8
|
||||
BORDER_WIDTH = 1
|
||||
OUTPUT_CELL_SIZE = 64 # Each cell in output is always 64x64 pixels
|
||||
|
||||
# Load the world images
|
||||
try:
|
||||
lightworld_img = Image.open("data/overworld/lightworld.png")
|
||||
darkworld_img = Image.open("data/overworld/darkworld.png")
|
||||
except FileNotFoundError as e:
|
||||
raise FileNotFoundError(f"World image not found: {e}. Ensure lightworld.png and darkworld.png are in the data/overworld directory.")
|
||||
|
||||
# Calculate source cell size from the base images
|
||||
# Each world image is 8x8 screens, so divide by 8 to get source cell size
|
||||
img_width, _ = lightworld_img.size
|
||||
SOURCE_CELL_SIZE = img_width // GRID_SIZE # Size of each cell in the source image
|
||||
|
||||
# Calculate dimensions for the output (always based on 64x64 cells)
|
||||
world_width = GRID_SIZE * OUTPUT_CELL_SIZE
|
||||
world_height = GRID_SIZE * OUTPUT_CELL_SIZE
|
||||
|
||||
# Create output image (two worlds side by side with a small gap)
|
||||
gap = 32
|
||||
output_width = world_width * 2 + gap
|
||||
output_height = world_height
|
||||
output_img = Image.new('RGB', (output_width, output_height), color='black')
|
||||
|
||||
# Large screen base IDs (defined once for reuse)
|
||||
large_screen_base_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35,
|
||||
0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75]
|
||||
|
||||
# Process both worlds
|
||||
for world_idx in range(2):
|
||||
x_offset = 0 if world_idx == 0 else (world_width + gap)
|
||||
|
||||
# Build a map of screen_id -> list of (row, col) positions for large screens
|
||||
large_screen_positions = {}
|
||||
for row in range(GRID_SIZE):
|
||||
for col in range(GRID_SIZE):
|
||||
screen_id = grid[world_idx][row][col]
|
||||
if screen_id != -1 and screen_id in large_screen_base_ids:
|
||||
if screen_id not in large_screen_positions:
|
||||
large_screen_positions[screen_id] = []
|
||||
large_screen_positions[screen_id].append((row, col))
|
||||
|
||||
# Process each cell in the grid individually
|
||||
# This handles wrapped large screens correctly by drawing each quadrant separately
|
||||
for row in range(GRID_SIZE):
|
||||
for col in range(GRID_SIZE):
|
||||
screen_id = grid[world_idx][row][col]
|
||||
|
||||
if screen_id == -1:
|
||||
# Empty cell - fill with black (already black from initialization)
|
||||
continue
|
||||
|
||||
is_large = screen_id in large_screen_base_ids
|
||||
|
||||
# Calculate source position in the world image
|
||||
source_row = (screen_id % 0x40) >> 3
|
||||
source_col = screen_id % 0x08
|
||||
world_img = lightworld_img if screen_id < 0x40 else darkworld_img
|
||||
|
||||
if is_large:
|
||||
# For large screens, determine which quadrant this cell represents
|
||||
positions = large_screen_positions.get(screen_id, [(row, col)])
|
||||
quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE)
|
||||
|
||||
# Map quadrant to source offset within the 2x2 large screen
|
||||
quadrant_offsets = {
|
||||
"NW": (0, 0),
|
||||
"NE": (1, 0),
|
||||
"SW": (0, 1),
|
||||
"SE": (1, 1)
|
||||
}
|
||||
q_col_offset, q_row_offset = quadrant_offsets[quadrant]
|
||||
|
||||
# Calculate source position for this quadrant
|
||||
source_x = (source_col + q_col_offset) * SOURCE_CELL_SIZE
|
||||
source_y = (source_row + q_row_offset) * SOURCE_CELL_SIZE
|
||||
|
||||
# Crop single cell from source (the specific quadrant)
|
||||
cropped = world_img.crop((
|
||||
source_x,
|
||||
source_y,
|
||||
source_x + SOURCE_CELL_SIZE,
|
||||
source_y + SOURCE_CELL_SIZE
|
||||
))
|
||||
else:
|
||||
# Small screen (1x1)
|
||||
source_x = source_col * SOURCE_CELL_SIZE
|
||||
source_y = source_row * SOURCE_CELL_SIZE
|
||||
|
||||
# Crop single cell from source
|
||||
cropped = world_img.crop((
|
||||
source_x,
|
||||
source_y,
|
||||
source_x + SOURCE_CELL_SIZE,
|
||||
source_y + SOURCE_CELL_SIZE
|
||||
))
|
||||
|
||||
# Resize to output size (64x64 pixels)
|
||||
resized = cropped.resize(
|
||||
(OUTPUT_CELL_SIZE, OUTPUT_CELL_SIZE),
|
||||
Image.LANCZOS
|
||||
)
|
||||
|
||||
# Paste into output at grid position
|
||||
dest_x = x_offset + col * OUTPUT_CELL_SIZE
|
||||
dest_y = row * OUTPUT_CELL_SIZE
|
||||
output_img.paste(resized, (dest_x, dest_y))
|
||||
|
||||
edge_lists = get_edge_lists(grid, overworld_screens, large_screen_quadrant_info)
|
||||
|
||||
# Draw borders and edge connection indicators after all screens are placed
|
||||
draw = ImageDraw.Draw(output_img)
|
||||
|
||||
# Size of the indicator squares
|
||||
INDICATOR_SIZE = 12
|
||||
|
||||
for world_idx in range(2):
|
||||
x_offset = 0 if world_idx == 0 else (world_width + gap)
|
||||
|
||||
# Build large screen positions map for this world
|
||||
large_screen_positions = {}
|
||||
for row in range(GRID_SIZE):
|
||||
for col in range(GRID_SIZE):
|
||||
screen_id = grid[world_idx][row][col]
|
||||
if screen_id != -1 and screen_id in large_screen_base_ids:
|
||||
if screen_id not in large_screen_positions:
|
||||
large_screen_positions[screen_id] = []
|
||||
large_screen_positions[screen_id].append((row, col))
|
||||
|
||||
# Draw borders for each cell
|
||||
for row in range(GRID_SIZE):
|
||||
for col in range(GRID_SIZE):
|
||||
screen_id = grid[world_idx][row][col]
|
||||
|
||||
if screen_id == -1:
|
||||
continue
|
||||
|
||||
is_large = screen_id in large_screen_base_ids
|
||||
|
||||
dest_x = x_offset + col * OUTPUT_CELL_SIZE
|
||||
dest_y = row * OUTPUT_CELL_SIZE
|
||||
|
||||
if is_large:
|
||||
# For large screens, determine which quadrant this cell is
|
||||
positions = large_screen_positions.get(screen_id, [(row, col)])
|
||||
quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE)
|
||||
|
||||
# Draw border only on the outer edges of the large screen
|
||||
# (not on internal edges between quadrants)
|
||||
# NW: draw top and left borders
|
||||
# NE: draw top and right borders
|
||||
# SW: draw bottom and left borders
|
||||
# SE: draw bottom and right borders
|
||||
|
||||
if quadrant in ["NW", "NE"]:
|
||||
# Draw top border
|
||||
draw.line([(dest_x, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y)], fill='black', width=BORDER_WIDTH)
|
||||
if quadrant in ["SW", "SE"]:
|
||||
# Draw bottom border
|
||||
draw.line([(dest_x, dest_y + OUTPUT_CELL_SIZE - 1), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH)
|
||||
if quadrant in ["NW", "SW"]:
|
||||
# Draw left border
|
||||
draw.line([(dest_x, dest_y), (dest_x, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH)
|
||||
if quadrant in ["NE", "SE"]:
|
||||
# Draw right border
|
||||
draw.line([(dest_x + OUTPUT_CELL_SIZE - 1, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH)
|
||||
else:
|
||||
# Small screen - draw border around single cell
|
||||
draw.rectangle(
|
||||
[dest_x, dest_y, dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1],
|
||||
outline='black',
|
||||
width=BORDER_WIDTH
|
||||
)
|
||||
|
||||
# Draw edge connection indicators for each cell
|
||||
for row in range(GRID_SIZE):
|
||||
for col in range(GRID_SIZE):
|
||||
screen_id = grid[world_idx][row][col]
|
||||
if screen_id == -1:
|
||||
continue
|
||||
|
||||
dest_x = x_offset + col * OUTPUT_CELL_SIZE
|
||||
dest_y = row * OUTPUT_CELL_SIZE
|
||||
|
||||
# Draw indicator for each direction (only if edges exist)
|
||||
# Use bright colors for visibility
|
||||
GREEN = (0, 255, 0) # Bright green
|
||||
YELLOW = (255, 255, 0) # Bright yellow
|
||||
RED = (255, 0, 0) # Bright red
|
||||
|
||||
# North indicators - positioned based on edge midpoint
|
||||
north_edges = edge_lists.get((world_idx, row, col, Direction.North), [])
|
||||
if north_edges:
|
||||
north_y = dest_y # Touch the top border
|
||||
|
||||
for edge in north_edges:
|
||||
# For north/south edges, midpoint gives the X coordinate
|
||||
# Take midpoint modulo 0x0200, range 0-0x01FF maps to full side
|
||||
midpoint = edge.midpoint % 0x0200
|
||||
# Map from game coordinate range to pixel position
|
||||
edge_x_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200
|
||||
edge_x = dest_x + edge_x_offset - INDICATOR_SIZE // 2
|
||||
|
||||
edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in north_edges if e.dest) else RED
|
||||
draw.rectangle(
|
||||
[edge_x, north_y, edge_x + INDICATOR_SIZE - 1, north_y + INDICATOR_SIZE - 1],
|
||||
fill=edge_color,
|
||||
outline='black'
|
||||
)
|
||||
|
||||
# Draw diagonal cross if edge crosses between worlds
|
||||
if edge.dest is not None and is_crossed_edge(edge, overworld_screens):
|
||||
draw.line(
|
||||
[edge_x, north_y, edge_x + INDICATOR_SIZE - 1, north_y + INDICATOR_SIZE - 1],
|
||||
fill='black',
|
||||
width=1
|
||||
)
|
||||
draw.line(
|
||||
[edge_x + INDICATOR_SIZE - 1, north_y, edge_x, north_y + INDICATOR_SIZE - 1],
|
||||
fill='black',
|
||||
width=1
|
||||
)
|
||||
|
||||
# South indicators - positioned based on edge midpoint
|
||||
south_edges = edge_lists.get((world_idx, row, col, Direction.South), [])
|
||||
if south_edges:
|
||||
south_y = dest_y + OUTPUT_CELL_SIZE - INDICATOR_SIZE # Touch the bottom border
|
||||
|
||||
for edge in south_edges:
|
||||
# For north/south edges, midpoint gives the X coordinate
|
||||
# Take midpoint modulo 0x0200, range 0-0x01FF maps to full side
|
||||
midpoint = edge.midpoint % 0x0200
|
||||
# Map from game coordinate range to pixel position
|
||||
edge_x_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200
|
||||
edge_x = dest_x + edge_x_offset - INDICATOR_SIZE // 2
|
||||
|
||||
edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in south_edges if e.dest) else RED
|
||||
draw.rectangle(
|
||||
[edge_x, south_y, edge_x + INDICATOR_SIZE - 1, south_y + INDICATOR_SIZE - 1],
|
||||
fill=edge_color,
|
||||
outline='black'
|
||||
)
|
||||
|
||||
# Draw diagonal cross if edge crosses between worlds
|
||||
if edge.dest is not None and is_crossed_edge(edge, overworld_screens):
|
||||
draw.line(
|
||||
[edge_x, south_y, edge_x + INDICATOR_SIZE - 1, south_y + INDICATOR_SIZE - 1],
|
||||
fill='black',
|
||||
width=1
|
||||
)
|
||||
draw.line(
|
||||
[edge_x + INDICATOR_SIZE - 1, south_y, edge_x, south_y + INDICATOR_SIZE - 1],
|
||||
fill='black',
|
||||
width=1
|
||||
)
|
||||
|
||||
# West indicators - positioned based on edge midpoint
|
||||
west_edges = edge_lists.get((world_idx, row, col, Direction.West), [])
|
||||
if west_edges:
|
||||
west_x = dest_x # Touch the left border
|
||||
|
||||
for edge in west_edges:
|
||||
# For west/east edges, midpoint gives the Y coordinate
|
||||
# Take midpoint modulo 0x0200, range 0-0x01FF maps to full side
|
||||
midpoint = edge.midpoint % 0x0200
|
||||
# Map from game coordinate range to pixel position
|
||||
edge_y_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200
|
||||
edge_y = dest_y + edge_y_offset - INDICATOR_SIZE // 2
|
||||
|
||||
edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in west_edges if e.dest) else RED
|
||||
draw.rectangle(
|
||||
[west_x, edge_y, west_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1],
|
||||
fill=edge_color,
|
||||
outline='black'
|
||||
)
|
||||
|
||||
# Draw diagonal cross if edge crosses between worlds
|
||||
if edge.dest is not None and is_crossed_edge(edge, overworld_screens):
|
||||
draw.line(
|
||||
[west_x, edge_y, west_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1],
|
||||
fill='black',
|
||||
width=1
|
||||
)
|
||||
draw.line(
|
||||
[west_x + INDICATOR_SIZE - 1, edge_y, west_x, edge_y + INDICATOR_SIZE - 1],
|
||||
fill='black',
|
||||
width=1
|
||||
)
|
||||
|
||||
# East indicators - positioned based on edge midpoint
|
||||
east_edges = edge_lists.get((world_idx, row, col, Direction.East), [])
|
||||
if east_edges:
|
||||
east_x = dest_x + OUTPUT_CELL_SIZE - INDICATOR_SIZE # Touch the right border
|
||||
|
||||
for edge in east_edges:
|
||||
# For west/east edges, midpoint gives the Y coordinate
|
||||
# Take midpoint modulo 0x0200, range 0-0x01FF maps to full side
|
||||
midpoint = edge.midpoint % 0x0200
|
||||
# Map from game coordinate range to pixel position
|
||||
edge_y_offset = (midpoint * OUTPUT_CELL_SIZE) // 0x0200
|
||||
edge_y = dest_y + edge_y_offset - INDICATOR_SIZE // 2
|
||||
|
||||
edge_color = GREEN if edge.dest is not None else YELLOW if any(e for e in east_edges if e.dest) else RED
|
||||
draw.rectangle(
|
||||
[east_x, edge_y, east_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1],
|
||||
fill=edge_color,
|
||||
outline='black'
|
||||
)
|
||||
|
||||
# Draw diagonal cross if edge crosses between worlds
|
||||
if edge.dest is not None and is_crossed_edge(edge, overworld_screens):
|
||||
draw.line(
|
||||
[east_x, edge_y, east_x + INDICATOR_SIZE - 1, edge_y + INDICATOR_SIZE - 1],
|
||||
fill='black',
|
||||
width=1
|
||||
)
|
||||
draw.line(
|
||||
[east_x + INDICATOR_SIZE - 1, edge_y, east_x, edge_y + INDICATOR_SIZE - 1],
|
||||
fill='black',
|
||||
width=1
|
||||
)
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Generate filename with timestamp
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"layout_{timestamp}.png"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
# Save the image
|
||||
output_img.save(filepath, "PNG")
|
||||
logging.getLogger('').info(f"Layout visualization saved to {filepath}")
|
||||
@@ -44,6 +44,11 @@ class DataTables:
|
||||
self.ow_enemy_table = None
|
||||
self.pot_secret_table = None
|
||||
self.overworld_sprite_sheets = None
|
||||
self.pointer_addresses = {
|
||||
# table: [data_start, data_size, pointer_table, references]
|
||||
'ow_sprites': [ 0x09CB41, None, (0x09C881, 0x09C901, 0x09CA21), None ],
|
||||
'uw_sprites': [ 0x09D92E, None, 0x09D62E, 0x09C298 ],
|
||||
}
|
||||
|
||||
# associated data
|
||||
self.sprite_requirements = None
|
||||
@@ -92,13 +97,6 @@ class DataTables:
|
||||
# bank 0A uses 372A bytes
|
||||
# bank 1F uses 77CE bytes: total is about a bank and a half
|
||||
# probably should reuse bank 1F if writing all the rooms out
|
||||
for sheet in self.sprite_sheets.values():
|
||||
sheet.write_to_rom(rom, snes_to_pc(0x00DB97)) # bank 00, SheetsTable_AA3
|
||||
if self.uw_enemy_table.size() > 0x2800:
|
||||
raise Exception('Sprite table is too big for current area')
|
||||
self.uw_enemy_table.write_sprite_data_to_rom(rom)
|
||||
self.uw_enemy_table.check_special_bitmasks_size()
|
||||
self.uw_enemy_table.write_special_bitmask_table(rom)
|
||||
for area_id, sheet in self.overworld_sprite_sheets.items():
|
||||
if area_id in [0x80, 0x81]:
|
||||
offset = area_id - 0x80 # 02E575 for special areas?
|
||||
@@ -110,7 +108,14 @@ class DataTables:
|
||||
# _00FAC1 is LW post-aga
|
||||
# _00FB01 is DW
|
||||
# _00FA41 is rain state
|
||||
for sheet in self.sprite_sheets.values():
|
||||
sheet.write_to_rom(rom, snes_to_pc(0x00DB97)) # bank 00, SheetsTable_AA3
|
||||
self.write_ow_sprite_data_to_rom(rom)
|
||||
if self.uw_enemy_table.size() > 0x2800:
|
||||
raise Exception('Sprite table is too big for current area')
|
||||
self.uw_enemy_table.write_sprite_data_to_rom(rom, self.pointer_addresses)
|
||||
self.uw_enemy_table.check_special_bitmasks_size()
|
||||
self.uw_enemy_table.write_special_bitmask_table(rom)
|
||||
for sprite, stats in self.enemy_stats.items():
|
||||
# write health to rom
|
||||
if stats.health is not None:
|
||||
@@ -148,13 +153,14 @@ class DataTables:
|
||||
|
||||
def write_ow_sprite_data_to_rom(self, rom):
|
||||
# calculate how big this table is going to be?
|
||||
# bytes = sum(1+len(x)*3 for x in self.ow_enemy_table.values() if len(x) > 0)+1
|
||||
bytes = sum(1+len(x)*3 for x in self.ow_enemy_table.values() if len(x) > 0)+1
|
||||
self.pointer_addresses['ow_sprites'][1] = bytes
|
||||
# ending_byte = 0x09CB3B + bytes
|
||||
max_per_state = {0: 0x40, 1: 0x90, 2: 0x8B} # dropped max on state 2 to steal space for a couple extra sprites (Murahdahla, extra tutorial guard)
|
||||
max_per_state = {0: 0x40, 1: 0x90, 2: 0x82} # dropped max on state 2 to steal space for extra sprites (Murahdahla, extra tutorial guard)
|
||||
|
||||
pointer_address = snes_to_pc(0x09C881)
|
||||
# currently borrowed 10 bytes, used 9 (2xMurah + TutorialGuard)
|
||||
data_pointer = snes_to_pc(0x09CB38) # was originally 0x09CB41 - stealing space for a couple extra sprites (Murahdahla, extra tutorial guard)
|
||||
pointer_address = snes_to_pc(self.pointer_addresses['ow_sprites'][2][0])
|
||||
self.pointer_addresses['ow_sprites'][0] = pointer_address + ((max_per_state[0] + max_per_state[1] + max_per_state[2]) * 2)
|
||||
data_pointer = self.pointer_addresses['ow_sprites'][0]
|
||||
empty_pointer = pc_to_snes(data_pointer) & 0xFFFF
|
||||
rom.write_byte(data_pointer, 0xff)
|
||||
cached_dark_world = {}
|
||||
@@ -187,6 +193,10 @@ class DataTables:
|
||||
data_pointer += len(data)
|
||||
rom.write_byte(data_pointer, 0xff)
|
||||
data_pointer += 1
|
||||
# Check if OW sprite data has overwritten the UW sprite pointer table
|
||||
max_allowed_address = snes_to_pc(0x09D62E)
|
||||
if data_pointer > max_allowed_address:
|
||||
raise Exception(f'OW sprite data will cause the UW sprite pointer table to overwrite the pots pointer table. Data end: {hex(pc_to_snes(data_pointer))}, Max allowed: $09D62E')
|
||||
|
||||
|
||||
special_health_table = {
|
||||
|
||||
@@ -121,8 +121,16 @@ def roll_settings(weights):
|
||||
ret.accessibility = get_choice('accessibility')
|
||||
ret.restrict_boss_items = get_choice('restrict_boss_items')
|
||||
|
||||
overworld_layout = get_choice('overworld_layout')
|
||||
ret.ow_layout = overworld_layout if overworld_layout != 'none' else 'vanilla'
|
||||
ret.ow_parallel = get_choice_bool('overworld_parallel')
|
||||
overworld_shuffle = get_choice('overworld_shuffle')
|
||||
ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla'
|
||||
if overworld_shuffle == 'parallel':
|
||||
ret.ow_layout = 'wild'
|
||||
ret.ow_parallel = True
|
||||
elif overworld_shuffle == 'full':
|
||||
ret.ow_layout = 'wild'
|
||||
ret.ow_parallel = False
|
||||
ret.ow_terrain = get_choice_bool('overworld_terrain')
|
||||
valid_options = {'none': 'none', 'polar': 'polar', 'grouped': 'polar', 'chaos': 'unrestricted', 'unrestricted': 'unrestricted'}
|
||||
ret.ow_crossed = get_choice('overworld_crossed')
|
||||
@@ -132,6 +140,7 @@ def roll_settings(weights):
|
||||
ret.ow_whirlpool = get_choice_bool('whirlpool_shuffle')
|
||||
overworld_flute = get_choice('flute_shuffle')
|
||||
ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla'
|
||||
ret.ow_fog = get_choice_bool('overworld_fog')
|
||||
ret.shuffle_followers = get_choice_bool('shuffle_followers')
|
||||
ret.bonk_drops = get_choice_bool('bonk_drops')
|
||||
entrance_shuffle = get_choice('entrance_shuffle')
|
||||
@@ -148,11 +157,7 @@ def roll_settings(weights):
|
||||
ret.door_self_loops = get_choice_bool('door_self_loops')
|
||||
ret.experimental = get_choice_bool('experimental')
|
||||
ret.collection_rate = get_choice_bool('collection_rate')
|
||||
|
||||
ret.dungeon_counters = get_choice_non_bool('dungeon_counters') if 'dungeon_counters' in weights else 'default'
|
||||
if ret.dungeon_counters == 'default':
|
||||
ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle != 'none' else 'off'
|
||||
|
||||
ret.pseudoboots = get_choice_bool('pseudoboots')
|
||||
ret.mirrorscroll = get_choice_bool('mirrorscroll')
|
||||
ret.shopsanity = get_choice_bool('shopsanity')
|
||||
|
||||
Reference in New Issue
Block a user