Implement Grid Layout Shuffle
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,6 +30,7 @@ weights/
|
|||||||
/QUsb2Snes/
|
/QUsb2Snes/
|
||||||
/output/
|
/output/
|
||||||
/enemizer/
|
/enemizer/
|
||||||
|
visualizations/
|
||||||
|
|
||||||
base2current.json
|
base2current.json
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ class World(object):
|
|||||||
self.owswaps = {}
|
self.owswaps = {}
|
||||||
self.owcrossededges = {}
|
self.owcrossededges = {}
|
||||||
self.owwhirlpools = {}
|
self.owwhirlpools = {}
|
||||||
|
self.owgrid = {}
|
||||||
self.owflutespots = {}
|
self.owflutespots = {}
|
||||||
self.owsectors = {}
|
self.owsectors = {}
|
||||||
self.allow_flip_sanc = {}
|
self.allow_flip_sanc = {}
|
||||||
@@ -119,6 +120,7 @@ class World(object):
|
|||||||
set_player_attr('owswaps', [[],[],[]])
|
set_player_attr('owswaps', [[],[],[]])
|
||||||
set_player_attr('owcrossededges', [])
|
set_player_attr('owcrossededges', [])
|
||||||
set_player_attr('owwhirlpools', [])
|
set_player_attr('owwhirlpools', [])
|
||||||
|
set_player_attr('owgrid', None)
|
||||||
set_player_attr('owsectors', None)
|
set_player_attr('owsectors', None)
|
||||||
set_player_attr('allow_flip_sanc', False)
|
set_player_attr('allow_flip_sanc', False)
|
||||||
set_player_attr('remote_items', False)
|
set_player_attr('remote_items', False)
|
||||||
@@ -2345,12 +2347,11 @@ class OWEdge(object):
|
|||||||
def __init__(self, player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff):
|
def __init__(self, player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff):
|
||||||
self.player = player
|
self.player = player
|
||||||
self.name = name
|
self.name = name
|
||||||
self.type = DoorType.Open
|
|
||||||
self.direction = direction
|
self.direction = direction
|
||||||
self.terrain = terrain
|
self.terrain = terrain
|
||||||
|
self.parallel = None
|
||||||
self.specialEntrance = False
|
self.specialEntrance = False
|
||||||
self.specialExit = False
|
self.specialExit = False
|
||||||
self.deadEnd = False
|
|
||||||
|
|
||||||
# rom properties
|
# rom properties
|
||||||
self.owIndex = owIndex
|
self.owIndex = owIndex
|
||||||
@@ -2381,7 +2382,6 @@ class OWEdge(object):
|
|||||||
self.worldType = WorldType.Dark
|
self.worldType = WorldType.Dark
|
||||||
|
|
||||||
# logical properties
|
# logical properties
|
||||||
# self.connected = False # combine with Dest?
|
|
||||||
self.dest = None
|
self.dest = None
|
||||||
self.dependents = []
|
self.dependents = []
|
||||||
self.dead = False
|
self.dead = False
|
||||||
@@ -2398,9 +2398,6 @@ class OWEdge(object):
|
|||||||
def getTarget(self):
|
def getTarget(self):
|
||||||
return self.dest.specialID if self.dest.specialExit else self.dest.edge_id
|
return self.dest.specialID if self.dest.specialExit else self.dest.edge_id
|
||||||
|
|
||||||
def dead_end(self):
|
|
||||||
self.deadEnd = True
|
|
||||||
|
|
||||||
def coordInfo(self, midpoint, vram_loc):
|
def coordInfo(self, midpoint, vram_loc):
|
||||||
self.midpoint = midpoint
|
self.midpoint = midpoint
|
||||||
self.vramLoc = vram_loc
|
self.vramLoc = vram_loc
|
||||||
@@ -3434,40 +3431,31 @@ class Spoiler(object):
|
|||||||
outfile.write(f'{fairy}: {bottle}\n')
|
outfile.write(f'{fairy}: {bottle}\n')
|
||||||
|
|
||||||
if self.maps:
|
if self.maps:
|
||||||
if 'all' in self.settings or 'flute' in self.settings:
|
def write_map(type, title):
|
||||||
# flute shuffle
|
|
||||||
for player in range(1, self.world.players + 1):
|
for player in range(1, self.world.players + 1):
|
||||||
if ('flute', player) in self.maps:
|
if (type, player) in self.maps:
|
||||||
outfile.write('\n\nFlute Spots:\n\n')
|
outfile.write('\n\n' + title + '\n\n')
|
||||||
break
|
break
|
||||||
for player in range(1, self.world.players + 1):
|
for player in range(1, self.world.players + 1):
|
||||||
if ('flute', player) in self.maps:
|
if (type, player) in self.maps:
|
||||||
if self.world.players > 1:
|
if self.world.players > 1:
|
||||||
outfile.write(str('(Player ' + str(player) + ')\n')) # player name
|
outfile.write(str('(Player ' + str(player) + ')\n')) # player name
|
||||||
outfile.write(self.maps[('flute', player)]['text'])
|
outfile.write(self.maps[(type, player)]['text'])
|
||||||
|
|
||||||
|
if 'all' in self.settings or 'flute' in self.settings:
|
||||||
|
# flute shuffle
|
||||||
|
write_map('flute', 'Flute Spots:')
|
||||||
|
|
||||||
if 'all' in self.settings or 'overworld' in self.settings:
|
if 'all' in self.settings or 'overworld' in self.settings:
|
||||||
# overworld tile flips
|
# overworld tile flips
|
||||||
for player in range(1, self.world.players + 1):
|
write_map('swaps', 'OW Tile Flips:')
|
||||||
if ('swaps', player) in self.maps:
|
|
||||||
outfile.write('\n\nOW Tile Flips:\n\n')
|
|
||||||
break
|
|
||||||
for player in range(1, self.world.players + 1):
|
|
||||||
if ('swaps', player) in self.maps:
|
|
||||||
if self.world.players > 1:
|
|
||||||
outfile.write(str('(Player ' + str(player) + ')\n')) # player name
|
|
||||||
outfile.write(self.maps[('swaps', player)]['text'])
|
|
||||||
|
|
||||||
# crossed groups
|
# crossed groups
|
||||||
for player in range(1, self.world.players + 1):
|
write_map('groups', 'OW Crossed Groups:')
|
||||||
if ('groups', player) in self.maps:
|
|
||||||
outfile.write('\n\nOW Crossed Groups:\n\n')
|
# grid layout
|
||||||
break
|
write_map('layout_grid_lw', 'Light World Layout:')
|
||||||
for player in range(1, self.world.players + 1):
|
write_map('layout_grid_dw', 'Dark World Layout:')
|
||||||
if ('groups', player) in self.maps:
|
|
||||||
if self.world.players > 1:
|
|
||||||
outfile.write(str('(Player ' + str(player) + ')\n')) # player name
|
|
||||||
outfile.write(self.maps[('groups', player)]['text'])
|
|
||||||
|
|
||||||
if self.overworlds and ('all' in self.settings or 'overworld' in self.settings):
|
if self.overworlds and ('all' in self.settings or 'overworld' in self.settings):
|
||||||
outfile.write('\n\nOverworld Edges:\n\n')
|
outfile.write('\n\nOverworld Edges:\n\n')
|
||||||
|
|||||||
@@ -324,12 +324,20 @@ def create_owedges(world, player):
|
|||||||
|
|
||||||
world.owedges += edges
|
world.owedges += edges
|
||||||
world.initialize_owedges(edges)
|
world.initialize_owedges(edges)
|
||||||
|
set_parallel_owedge_links(world, player, edges)
|
||||||
|
|
||||||
def create_owedge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff):
|
def create_owedge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff):
|
||||||
if name not in OWExitTypes['OWEdge']:
|
if name not in OWExitTypes['OWEdge']:
|
||||||
OWExitTypes['OWEdge'].append(name)
|
OWExitTypes['OWEdge'].append(name)
|
||||||
return OWEdge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex)
|
return OWEdge(player, name, owIndex, direction, terrain, edge_id, owSlotIndex)
|
||||||
|
|
||||||
|
def set_parallel_owedge_links(world, player, edges):
|
||||||
|
for edge in edges:
|
||||||
|
if edge.name in parallel_links:
|
||||||
|
dw_edge = world.get_owedge(parallel_links[edge.name], player)
|
||||||
|
edge.parallel = dw_edge
|
||||||
|
dw_edge.parallel = edge
|
||||||
|
|
||||||
|
|
||||||
OWEdgeGroups = {
|
OWEdgeGroups = {
|
||||||
#(IsStandard, World, EdgeAxis, Terrain, HasParallel, NumberInGroup, CustomizerGroup)
|
#(IsStandard, World, EdgeAxis, Terrain, HasParallel, NumberInGroup, CustomizerGroup)
|
||||||
|
|||||||
@@ -114,33 +114,34 @@ def link_overworld(world, player):
|
|||||||
|
|
||||||
# restructure Maze Race/Suburb/Frog/Dig Game manually due to NP/P relationship
|
# restructure Maze Race/Suburb/Frog/Dig Game manually due to NP/P relationship
|
||||||
parallel_links_new = bidict(parallel_links) # shallow copy is enough (deep copy is broken)
|
parallel_links_new = bidict(parallel_links) # shallow copy is enough (deep copy is broken)
|
||||||
if world.owKeepSimilar[player]:
|
if world.owLayout[player] != 'grid':
|
||||||
del parallel_links_new['Maze Race ES']
|
if world.owKeepSimilar[player]:
|
||||||
del parallel_links_new['Kakariko Suburb WS']
|
del parallel_links_new['Maze Race ES']
|
||||||
for group in trimmed_groups.keys():
|
del parallel_links_new['Kakariko Suburb WS']
|
||||||
(std, region, axis, terrain, parallel, _, custom) = group
|
for group in trimmed_groups.keys():
|
||||||
if parallel == IsParallel.Yes:
|
(std, region, axis, terrain, parallel, _, custom) = group
|
||||||
|
if parallel == IsParallel.Yes:
|
||||||
|
(forward_edges, back_edges) = trimmed_groups[group]
|
||||||
|
if ['Maze Race ES'] in forward_edges:
|
||||||
|
forward_edges.remove(['Maze Race ES'])
|
||||||
|
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Maze Race ES'])
|
||||||
|
if ['Kakariko Suburb WS'] in back_edges:
|
||||||
|
back_edges.remove(['Kakariko Suburb WS'])
|
||||||
|
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Kakariko Suburb WS'])
|
||||||
|
trimmed_groups[group] = (forward_edges, back_edges)
|
||||||
|
else:
|
||||||
|
for group in trimmed_groups.keys():
|
||||||
|
(std, region, axis, terrain, _, _, custom) = group
|
||||||
(forward_edges, back_edges) = trimmed_groups[group]
|
(forward_edges, back_edges) = trimmed_groups[group]
|
||||||
if ['Maze Race ES'] in forward_edges:
|
if ['Dig Game EC', 'Dig Game ES'] in forward_edges:
|
||||||
forward_edges.remove(['Maze Race ES'])
|
forward_edges.remove(['Dig Game EC', 'Dig Game ES'])
|
||||||
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Maze Race ES'])
|
trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][0].append(['Dig Game ES'])
|
||||||
if ['Kakariko Suburb WS'] in back_edges:
|
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Dig Game EC'])
|
||||||
back_edges.remove(['Kakariko Suburb WS'])
|
if ['Frog WC', 'Frog WS'] in back_edges:
|
||||||
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Kakariko Suburb WS'])
|
back_edges.remove(['Frog WC', 'Frog WS'])
|
||||||
|
trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][1].append(['Frog WS'])
|
||||||
|
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Frog WC'])
|
||||||
trimmed_groups[group] = (forward_edges, back_edges)
|
trimmed_groups[group] = (forward_edges, back_edges)
|
||||||
else:
|
|
||||||
for group in trimmed_groups.keys():
|
|
||||||
(std, region, axis, terrain, _, _, custom) = group
|
|
||||||
(forward_edges, back_edges) = trimmed_groups[group]
|
|
||||||
if ['Dig Game EC', 'Dig Game ES'] in forward_edges:
|
|
||||||
forward_edges.remove(['Dig Game EC', 'Dig Game ES'])
|
|
||||||
trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][0].append(['Dig Game ES'])
|
|
||||||
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][0].append(['Dig Game EC'])
|
|
||||||
if ['Frog WC', 'Frog WS'] in back_edges:
|
|
||||||
back_edges.remove(['Frog WC', 'Frog WS'])
|
|
||||||
trimmed_groups[(std, region, axis, terrain, IsParallel.Yes, 1, custom)][1].append(['Frog WS'])
|
|
||||||
trimmed_groups[(std, region, axis, terrain, IsParallel.No, 1, custom)][1].append(['Frog WC'])
|
|
||||||
trimmed_groups[group] = (forward_edges, back_edges)
|
|
||||||
parallel_links_new = {**dict(parallel_links_new), **dict({e:p[0] for e, p in parallel_links_new.inverse.items()})}
|
parallel_links_new = {**dict(parallel_links_new), **dict({e:p[0] for e, p in parallel_links_new.inverse.items()})}
|
||||||
|
|
||||||
connected_edges = []
|
connected_edges = []
|
||||||
@@ -232,7 +233,7 @@ def link_overworld(world, player):
|
|||||||
if 'undefined_chance' in custom_crossed:
|
if 'undefined_chance' in custom_crossed:
|
||||||
undefined_chance = custom_crossed['undefined_chance']
|
undefined_chance = custom_crossed['undefined_chance']
|
||||||
|
|
||||||
if limited_crossed > -1:
|
if limited_crossed > -1 and world.owLayout[player] != 'grid':
|
||||||
# connect forced crossed non-parallel edges based on previously determined tile flips
|
# connect forced crossed non-parallel edges based on previously determined tile flips
|
||||||
for edge in swapped_edges:
|
for edge in swapped_edges:
|
||||||
if edge not in parallel_links_new:
|
if edge not in parallel_links_new:
|
||||||
@@ -264,7 +265,7 @@ def link_overworld(world, player):
|
|||||||
s[0x30], s[0x35],
|
s[0x30], s[0x35],
|
||||||
s[0x41], s[0x3a],s[0x3b],s[0x3c], s[0x3f])
|
s[0x41], s[0x3a],s[0x3b],s[0x3c], s[0x3f])
|
||||||
world.spoiler.set_map('groups', text_output, ow_crossed_tiles, player)
|
world.spoiler.set_map('groups', text_output, ow_crossed_tiles, player)
|
||||||
elif limited_crossed > -1 or (world.owLayout[player] == 'vanilla' and world.owCrossed[player] == 'unrestricted'):
|
elif (limited_crossed > -1 and world.owLayout[player] != 'grid') or (world.owLayout[player] == 'vanilla' and world.owCrossed[player] == 'unrestricted'):
|
||||||
crossed_candidates = list()
|
crossed_candidates = list()
|
||||||
for group in trimmed_groups.keys():
|
for group in trimmed_groups.keys():
|
||||||
(mode, wrld, _, terrain, parallel, _, _) = group
|
(mode, wrld, _, terrain, parallel, _, _) = group
|
||||||
@@ -426,7 +427,12 @@ def link_overworld(world, player):
|
|||||||
for (forward_edge, back_edge) in zip(forward_set, back_set):
|
for (forward_edge, back_edge) in zip(forward_set, back_set):
|
||||||
connect_two_way(world, forward_edge, back_edge, player, connected_edges)
|
connect_two_way(world, forward_edge, back_edge, player, connected_edges)
|
||||||
elif world.owLayout[player] == 'grid':
|
elif world.owLayout[player] == 'grid':
|
||||||
raise NotImplementedError()
|
from source.overworld.LayoutGenerator import generate_random_grid_layout
|
||||||
|
|
||||||
|
for exitname, destname in special_screen_connections:
|
||||||
|
connect_two_way(world, exitname, destname, player, connected_edges)
|
||||||
|
|
||||||
|
generate_random_grid_layout(world, player, connected_edges, ow_crossed_tiles if world.owCrossed[player] == 'grouped' else [], force_noncrossed, force_crossed, limited_crossed, undefined_chance / 100)
|
||||||
else:
|
else:
|
||||||
if world.owKeepSimilar[player] and world.owParallel[player]:
|
if world.owKeepSimilar[player] and world.owParallel[player]:
|
||||||
for exitname, destname in parallelsimilar_connections:
|
for exitname, destname in parallelsimilar_connections:
|
||||||
@@ -846,7 +852,7 @@ def connect_custom(world, connected_edges, groups, forced, player):
|
|||||||
if not validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed):
|
if not validate_crossed_allowed(parallel_forward_edge, parallel_back_edge, is_crossed):
|
||||||
raise GenerationException('Violation of force crossed rules on parallel unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge)
|
raise GenerationException('Violation of force crossed rules on parallel unresolved similars: \'%s\' <-> \'%s\'', forward_edge, back_edge)
|
||||||
|
|
||||||
def connect_two_way(world, edgename1, edgename2, player, connected_edges=None):
|
def connect_two_way(world, edgename1, edgename2, player, connected_edges=None, set_spoiler=True):
|
||||||
edge1 = world.get_entrance(edgename1, player)
|
edge1 = world.get_entrance(edgename1, player)
|
||||||
edge2 = world.get_entrance(edgename2, player)
|
edge2 = world.get_entrance(edgename2, player)
|
||||||
x = world.get_owedge(edgename1, player)
|
x = world.get_owedge(edgename1, player)
|
||||||
@@ -870,7 +876,7 @@ def connect_two_way(world, edgename1, edgename2, player, connected_edges=None):
|
|||||||
x.dest = y
|
x.dest = y
|
||||||
y.dest = x
|
y.dest = x
|
||||||
|
|
||||||
if world.owLayout[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none':
|
if set_spoiler and (world.owLayout[player] != 'vanilla' or world.owMixed[player] or world.owCrossed[player] != 'none'):
|
||||||
world.spoiler.set_overworld(edgename2, edgename1, 'both', player)
|
world.spoiler.set_overworld(edgename2, edgename1, 'both', player)
|
||||||
|
|
||||||
if connected_edges is not None:
|
if connected_edges is not None:
|
||||||
@@ -2336,6 +2342,11 @@ parallelsimilar_connections = [('Maze Race ES', 'Kakariko Suburb WS'),
|
|||||||
('Dig Game ES', 'Frog WS')
|
('Dig Game ES', 'Frog WS')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
special_screen_connections = [('Lost Woods NW', 'Master Sword Meadow SC'),
|
||||||
|
('Stone Bridge WC', 'Hobo EC'),
|
||||||
|
('Zora Waterfall NE', 'Zoras Domain SW')
|
||||||
|
]
|
||||||
|
|
||||||
# non shuffled overworld
|
# non shuffled overworld
|
||||||
default_connections = [('Lost Woods NW', 'Master Sword Meadow SC'),
|
default_connections = [('Lost Woods NW', 'Master Sword Meadow SC'),
|
||||||
('Lost Woods SW', 'Lost Woods Pass NW'),
|
('Lost Woods SW', 'Lost Woods Pass NW'),
|
||||||
|
|||||||
35
Rom.py
35
Rom.py
@@ -512,6 +512,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
|||||||
|
|
||||||
# patch flute spots
|
# patch flute spots
|
||||||
owFlags = 0
|
owFlags = 0
|
||||||
|
owFog = 0
|
||||||
if world.owFluteShuffle[player] == 'vanilla':
|
if world.owFluteShuffle[player] == 'vanilla':
|
||||||
flute_spots = default_flute_connections
|
flute_spots = default_flute_connections
|
||||||
else:
|
else:
|
||||||
@@ -554,6 +555,34 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
|||||||
owFlags |= 0x01
|
owFlags |= 0x01
|
||||||
write_int16s(rom, snes_to_pc(0x02EA5C), world.owwhirlpools[player])
|
write_int16s(rom, snes_to_pc(0x02EA5C), world.owwhirlpools[player])
|
||||||
|
|
||||||
|
# set custom overworld map layout and fog
|
||||||
|
if world.owLayout[player] == 'grid':
|
||||||
|
owFlags |= 0x06
|
||||||
|
owFog = 1 if world.owParallel[player] else 2
|
||||||
|
grid = world.owgrid[player]
|
||||||
|
all_rows = grid[0] + grid[1]
|
||||||
|
all_cells = sum(all_rows, [])
|
||||||
|
rom.write_bytes(0x153C80, all_cells)
|
||||||
|
for pos, cell_id in enumerate(sum(grid[0], [])):
|
||||||
|
rom.write_byte(0x153D00 + cell_id % 0x40, pos)
|
||||||
|
for pos, cell_id in enumerate(sum(grid[1], [])):
|
||||||
|
rom.write_byte(0x153D40 + cell_id % 0x40, pos)
|
||||||
|
elif world.owMixed[player]:
|
||||||
|
owFlags |= 0x02
|
||||||
|
owFog = 1
|
||||||
|
large_screen_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35, 0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75]
|
||||||
|
for cell_id in range(0x80):
|
||||||
|
if cell_id - 0x01 in large_screen_ids:
|
||||||
|
screen_id = cell_id - 0x01
|
||||||
|
elif cell_id - 0x08 in large_screen_ids:
|
||||||
|
screen_id = cell_id - 0x08
|
||||||
|
elif cell_id - 0x09 in large_screen_ids:
|
||||||
|
screen_id = cell_id - 0x09
|
||||||
|
else:
|
||||||
|
screen_id = cell_id
|
||||||
|
world_flag = 0x40 if screen_id in world.owswaps[player][0] else 0x00
|
||||||
|
rom.write_byte(0x153C80 + cell_id, cell_id ^ world_flag)
|
||||||
|
|
||||||
# patch overworld edges
|
# patch overworld edges
|
||||||
inverted_buffer = [0] * 0x82
|
inverted_buffer = [0] * 0x82
|
||||||
owMode = 0
|
owMode = 0
|
||||||
@@ -595,10 +624,11 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
|||||||
rom.write_byte(0x1539B0 + b + 9, world_flag)
|
rom.write_byte(0x1539B0 + b + 9, world_flag)
|
||||||
|
|
||||||
for edge in world.owedges:
|
for edge in world.owedges:
|
||||||
if edge.dest is not None and isinstance(edge.dest, OWEdge) and edge.player == player:
|
if edge.player == player:
|
||||||
write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc)
|
write_int16(rom, edge.getAddress() + 0x0a, edge.vramLoc)
|
||||||
if not edge.specialExit:
|
if not edge.specialExit:
|
||||||
rom.write_byte(0x1539A0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, edge.getTarget())
|
destination = edge.getTarget() if edge.dest is not None and isinstance(edge.dest, OWEdge) else 0xFF
|
||||||
|
rom.write_byte(0x1539A0 + (edge.specialID - 0x80) * 2 if edge.specialEntrance else edge.getAddress() + 0x0e, destination)
|
||||||
|
|
||||||
# patch bonk prizes
|
# patch bonk prizes
|
||||||
if world.shuffle_bonk_drops[player]:
|
if world.shuffle_bonk_drops[player]:
|
||||||
@@ -627,6 +657,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
|
|||||||
|
|
||||||
write_int16(rom, 0x150002, owMode)
|
write_int16(rom, 0x150002, owMode)
|
||||||
write_int16(rom, 0x150004, owFlags)
|
write_int16(rom, 0x150004, owFlags)
|
||||||
|
write_int16(rom, 0x150008, owFog)
|
||||||
|
|
||||||
# patch entrance/exits/holes
|
# patch entrance/exits/holes
|
||||||
for region in world.regions:
|
for region in world.regions:
|
||||||
|
|||||||
BIN
data/overworld/darkworld.png
Normal file
BIN
data/overworld/darkworld.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
BIN
data/overworld/lightworld.png
Normal file
BIN
data/overworld/lightworld.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 324 KiB |
1412
source/overworld/LayoutGenerator.py
Normal file
1412
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