Merge remote-tracking branch 'codemann/OverworldShuffle' into codemann_OverworldShuffle
This commit is contained in:
@@ -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}")
|
||||
Reference in New Issue
Block a user