Links Awakening: Implement New Game (#1334)

Adds Link's Awakening: DX. Fully imports and forks LADXR, with permission - https://github.com/daid/LADXR
This commit is contained in:
zig-for
2023-03-21 01:26:03 +09:00
committed by GitHub
parent 67bf12369a
commit 81a239325d
180 changed files with 24191 additions and 2 deletions

View File

@@ -0,0 +1,147 @@
from ..romTables import ROMWithTables
from ..roomEditor import RoomEditor, ObjectWarp
from ..patches import overworld, core
from .tileset import loadTileInfo
from .map import Map, MazeGen
from .wfc import WFCMap, ContradictionException
from .roomgen import setup_room_types
from .imagegenerator import ImageGen
from .util import xyrange
from .locations.entrance import DummyEntrance
from .locationgen import LocationGenerator
from .logic import LogicGenerator
from .enemygen import generate_enemies
from ..assembler import ASM
def store_map(rom, the_map: Map):
# Move all exceptions to room FF
# Dig seashells
rom.patch(0x03, 0x220F, ASM("cp $DA"), ASM("cp $FF"))
rom.patch(0x03, 0x2213, ASM("cp $A5"), ASM("cp $FF"))
rom.patch(0x03, 0x2217, ASM("cp $74"), ASM("cp $FF"))
rom.patch(0x03, 0x221B, ASM("cp $3A"), ASM("cp $FF"))
rom.patch(0x03, 0x221F, ASM("cp $A8"), ASM("cp $FF"))
rom.patch(0x03, 0x2223, ASM("cp $B2"), ASM("cp $FF"))
# Force tile 04 under bushes and rocks, instead of conditionally tile 3, else seashells won't spawn.
rom.patch(0x14, 0x1655, 0x1677, "", fill_nop=True)
# Bonk trees
rom.patch(0x03, 0x0F03, ASM("cp $A4"), ASM("cp $FF"))
rom.patch(0x03, 0x0F07, ASM("cp $D2"), ASM("cp $FF"))
# Stairs under rocks
rom.patch(0x14, 0x1638, ASM("cp $52"), ASM("cp $FF"))
rom.patch(0x14, 0x163C, ASM("cp $04"), ASM("cp $FF"))
# Patch D6 raft game exit, just remove the exit.
re = RoomEditor(rom, 0x1B0)
re.removeObject(7, 0)
re.store(rom)
# Patch D8 back entrance, remove the outside part
re = RoomEditor(rom, 0x23A)
re.objects = [obj for obj in re.objects if not isinstance(obj, ObjectWarp)] + [ObjectWarp(1, 7, 0x23D, 0x58, 0x10)]
re.store(rom)
re = RoomEditor(rom, 0x23D)
re.objects = [obj for obj in re.objects if not isinstance(obj, ObjectWarp)] + [ObjectWarp(1, 7, 0x23A, 0x58, 0x10)]
re.store(rom)
for room in the_map:
for location in room.locations:
location.prepare(rom)
for n in range(0x00, 0x100):
sx = n & 0x0F
sy = ((n >> 4) & 0x0F)
if sx < the_map.w and sy < the_map.h:
tiles = the_map.get(sx, sy).tiles
else:
tiles = [4] * 80
tiles[44] = 0xC6
re = RoomEditor(rom, n)
# tiles = re.getTileArray()
re.objects = []
re.entities = []
room = the_map.get(sx, sy) if sx < the_map.w and sy < the_map.h else None
tileset = the_map.tilesets[room.tileset_id] if room else None
rom.banks[0x3F][0x3F00 + n] = tileset.main_id if tileset else 0x0F
rom.banks[0x21][0x02EF + n] = tileset.palette_id if tileset and tileset.palette_id is not None else 0x03
rom.banks[0x1A][0x2476 + n] = tileset.attr_bank if tileset and tileset.attr_bank else 0x22
rom.banks[0x1A][0x1E76 + n * 2] = (tileset.attr_addr & 0xFF) if tileset and tileset.attr_addr else 0x00
rom.banks[0x1A][0x1E77 + n * 2] = (tileset.attr_addr >> 8) if tileset and tileset.attr_addr else 0x60
re.animation_id = tileset.animation_id if tileset and tileset.animation_id is not None else 0x03
re.buildObjectList(tiles)
if room:
for idx, tile_id in enumerate(tiles):
if tile_id == 0x61: # Fix issues with the well being used as chimney as well and causing wrong warps
DummyEntrance(room, idx % 10, idx // 10)
re.entities += room.entities
room.locations.sort(key=lambda loc: (loc.y, loc.x, id(loc)))
for location in room.locations:
location.update_room(rom, re)
else:
re.objects.append(ObjectWarp(0x01, 0x10, 0x2A3, 0x50, 0x7C))
re.store(rom)
rom.banks[0x21][0x00BF:0x00BF+3] = [0, 0, 0] # Patch out the "load palette on screen transition" exception code.
# Fix some tile attribute issues
def change_attr(tileset, index, a, b, c, d):
rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 0] = a
rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 1] = b
rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 2] = c
rom.banks[the_map.tilesets[tileset].attr_bank][the_map.tilesets[tileset].attr_addr - 0x4000 + index * 4 + 3] = d
change_attr("mountains", 0x04, 6, 6, 6, 6)
change_attr("mountains", 0x27, 6, 6, 3, 3)
change_attr("mountains", 0x28, 6, 6, 3, 3)
change_attr("mountains", 0x6E, 1, 1, 1, 1)
change_attr("town", 0x59, 2, 2, 2, 2) # Roof tile wrong color
def generate(rom_filename, w, h):
rom = ROMWithTables(rom_filename)
overworld.patchOverworldTilesets(rom)
core.cleanup(rom)
tilesets = loadTileInfo(rom)
the_map = Map(w, h, tilesets)
setup_room_types(the_map)
MazeGen(the_map)
imggen = ImageGen(tilesets, the_map, rom)
imggen.enabled = False
wfcmap = WFCMap(the_map, tilesets) #, step_callback=imggen.on_step)
try:
wfcmap.initialize()
except ContradictionException as e:
print(f"Failed on setup {e.x // 10} {e.y // 8} {e.x % 10} {e.y % 8}")
imggen.on_step(wfcmap, err=(e.x, e.y))
return
imggen.on_step(wfcmap)
for x, y in xyrange(w, h):
for n in range(50):
try:
wfcmap.build(x * 10, y * 8, 10, 8)
imggen.on_step(wfcmap)
break
except ContradictionException as e:
print(f"Failed {x} {y} {e.x%10} {e.y%8} {n}")
imggen.on_step(wfcmap, err=(e.x, e.y))
wfcmap.clear()
if n == 49:
raise RuntimeError("Failed to fill chunk")
print(f"Done {x} {y}")
imggen.on_step(wfcmap)
wfcmap.store_tile_data(the_map)
LocationGenerator(the_map)
for room in the_map:
generate_enemies(room)
if imggen.enabled:
store_map(rom, the_map)
from mapexport import MapExport
MapExport(rom).export_all(w, h, dungeons=False)
rom.save("test.gbc")
return the_map

View File

@@ -0,0 +1,59 @@
from .tileset import walkable_tiles, entrance_tiles
import random
ENEMIES = {
"mountains": [
(0x0B,),
(0x0E,),
(0x29,),
(0x0E, 0x0E),
(0x0E, 0x0E, 0x23),
(0x0D,), (0x0D, 0x0D),
],
"egg": [],
"basic": [
(), (), (), (), (), (),
(0x09,), (0x09, 0x09), # octorock
(0x9B, 0x9B), (0x9B, 0x9B, 0x1B), # slimes
(0xBB, 0x9B), # bush crawler + slime
(0xB9,),
(0x0B, 0x23), # likelike + moblin
(0x14, 0x0B, 0x0B), # moblins + sword
(0x0B, 0x23, 0x23), # likelike + moblin
(0xAE, 0xAE), # flying octorock
(0xBA, ), # Bomber
(0x0D, 0x0D), (0x0D, ),
],
"town": [
(), (), (0x6C, 0x6E), (0x6E,), (0x6E, 0x6E),
],
"forest": [
(0x0B,), # moblins
(0x0B, 0x0B), # moblins
(0x14, 0x0B, 0x0B), # moblins + sword
],
"beach": [
(0xC6, 0xC6),
(0x0E, 0x0E, 0xC6),
(0x0E, 0x0E, 0x09),
],
"water": [],
}
def generate_enemies(room):
options = ENEMIES[room.tileset_id]
if not options:
return
positions = []
for y in range(1, 7):
for x in range(1, 9):
if room.tiles[x + y * 10] in walkable_tiles and room.tiles[x + (y - 1) * 10] not in entrance_tiles:
positions.append((x, y))
for type_id in random.choice(options):
if not positions:
return
x, y = random.choice(positions)
positions.remove((x, y))
room.entities.append((x, y, type_id))

View File

@@ -0,0 +1,95 @@
from .tileset import open_tiles, solid_tiles
def tx(x):
return x * 16 + x // 10
def ty(y):
return y * 16 + y // 8
class ImageGen:
def __init__(self, tilesets, the_map, rom):
self.tilesets = tilesets
self.map = the_map
self.rom = rom
self.image = None
self.draw = None
self.count = 0
self.enabled = False
self.__tile_cache = {}
def on_step(self, wfc, cur=None, err=None):
if not self.enabled:
return
if self.image is None:
import PIL.Image
import PIL.ImageDraw
self.image = PIL.Image.new("RGB", (self.map.w * 161, self.map.h * 129))
self.draw = PIL.ImageDraw.Draw(self.image)
self.image.paste(0, (0, 0, wfc.w * 16, wfc.h * 16))
for y in range(wfc.h):
for x in range(wfc.w):
cell = wfc.cell_data[(x, y)]
if len(cell.options) == 1:
tile_id = next(iter(cell.options))
room = self.map.get(x//10, y//8)
tile = self.get_tile(room.tileset_id, tile_id)
self.image.paste(tile, (tx(x), ty(y)))
else:
self.draw.text((tx(x) + 3, ty(y) + 3), f"{len(cell.options):2}", (255, 255, 255))
if cell.options.issubset(open_tiles):
self.draw.rectangle((tx(x), ty(y), tx(x) + 15, ty(y) + 15), outline=(0, 128, 0))
elif cell.options.issubset(solid_tiles):
self.draw.rectangle((tx(x), ty(y), tx(x) + 15, ty(y) + 15), outline=(0, 0, 192))
if cur:
self.draw.rectangle((tx(cur[0]),ty(cur[1]),tx(cur[0])+15,ty(cur[1])+15), outline=(0, 255, 0))
if err:
self.draw.rectangle((tx(err[0]),ty(err[1]),tx(err[0])+15,ty(err[1])+15), outline=(255, 0, 0))
self.image.save(f"_map/tmp{self.count:08}.png")
self.count += 1
def get_tile(self, tileset_id, tile_id):
tile = self.__tile_cache.get((tileset_id, tile_id), None)
if tile is not None:
return tile
import PIL.Image
tile = PIL.Image.new("L", (16, 16))
tileset = self.get_tileset(tileset_id)
metatile = self.rom.banks[0x1A][0x2749 + tile_id * 4:0x2749 + tile_id * 4+4]
def draw(ox, oy, t):
addr = (t & 0x3FF) << 4
tile_data = self.rom.banks[t >> 10][addr:addr+0x10]
for y in range(8):
a = tile_data[y * 2]
b = tile_data[y * 2 + 1]
for x in range(8):
v = 0
bit = 0x80 >> x
if a & bit:
v |= 0x01
if b & bit:
v |= 0x02
tile.putpixel((ox+x,oy+y), (255, 192, 128, 32)[v])
draw(0, 0, tileset[metatile[0]])
draw(8, 0, tileset[metatile[1]])
draw(0, 8, tileset[metatile[2]])
draw(8, 8, tileset[metatile[3]])
self.__tile_cache[(tileset_id, tile_id)] = tile
return tile
def get_tileset(self, tileset_id):
subtiles = [0] * 0x100
for n in range(0, 0x20):
subtiles[n] = (0x0F << 10) + (self.tilesets[tileset_id].main_id << 4) + n
for n in range(0x20, 0x80):
subtiles[n] = (0x0C << 10) + 0x100 + n
for n in range(0x80, 0x100):
subtiles[n] = (0x0C << 10) + n
addr = (0x000, 0x000, 0x2B0, 0x2C0, 0x2D0, 0x2E0, 0x2F0, 0x2D0, 0x300, 0x310, 0x320, 0x2A0, 0x330, 0x350, 0x360, 0x340, 0x370)[self.tilesets[tileset_id].animation_id or 3]
for n in range(0x6C, 0x70):
subtiles[n] = (0x0C << 10) + addr + n - 0x6C
return subtiles

View File

@@ -0,0 +1,203 @@
from .tileset import entrance_tiles, solid_tiles, walkable_tiles
from .map import Map
from .util import xyrange
from .locations.entrance import Entrance
from .locations.chest import Chest, FloorItem
from .locations.seashell import HiddenSeashell, DigSeashell, BonkSeashell
import random
from typing import List
all_location_constructors = (Chest, FloorItem, HiddenSeashell, DigSeashell, BonkSeashell)
def remove_duplicate_tile(tiles, to_find):
try:
idx0 = tiles.index(to_find)
idx1 = tiles.index(to_find, idx0 + 1)
tiles[idx1] = 0x04
except ValueError:
return
class Dijkstra:
def __init__(self, the_map: Map):
self.map = the_map
self.w = the_map.w * 10
self.h = the_map.h * 8
self.area = [-1] * (self.w * self.h)
self.distance = [0] * (self.w * self.h)
self.area_size = []
self.next_area_id = 0
def fill(self, start_x, start_y):
size = 0
todo = [(start_x, start_y, 0)]
while todo:
x, y, distance = todo.pop(0)
room = self.map.get(x // 10, y // 8)
tile_idx = (x % 10) + (y % 8) * 10
area_idx = x + y * self.w
if room.tiles[tile_idx] not in solid_tiles and self.area[area_idx] == -1:
size += 1
self.area[area_idx] = self.next_area_id
self.distance[area_idx] = distance
todo += [(x - 1, y, distance + 1), (x + 1, y, distance + 1), (x, y - 1, distance + 1), (x, y + 1, distance + 1)]
self.next_area_id += 1
self.area_size.append(size)
return self.next_area_id - 1
def dump(self):
print(self.area_size)
for y in range(self.map.h * 8):
for x in range(self.map.w * 10):
n = self.area[x + y * self.map.w * 10]
if n < 0:
print(' ', end='')
else:
print(n, end='')
print()
class EntranceInfo:
def __init__(self, room, x, y):
self.room = room
self.x = x
self.y = y
self.tile = room.tiles[x + y * 10]
@property
def map_x(self):
return self.room.x * 10 + self.x
@property
def map_y(self):
return self.room.y * 8 + self.y
class LocationGenerator:
def __init__(self, the_map: Map):
# Find all entrances
entrances: List[EntranceInfo] = []
for room in the_map:
# Prevent more then one chest or hole-entrance per map
remove_duplicate_tile(room.tiles, 0xA0)
remove_duplicate_tile(room.tiles, 0xC6)
for x, y in xyrange(10, 8):
if room.tiles[x + y * 10] in entrance_tiles:
entrances.append(EntranceInfo(room, x, y))
if room.tiles[x + y * 10] == 0xA0:
Chest(room, x, y)
todo_entrances = entrances.copy()
# Find a place to put the start position
start_entrances = [info for info in todo_entrances if info.room.tileset_id == "town"]
if not start_entrances:
start_entrances = entrances
start_entrance = random.choice(start_entrances)
todo_entrances.remove(start_entrance)
# Setup the start position and fill the basic dijkstra flood fill from there.
Entrance(start_entrance.room, start_entrance.x, start_entrance.y, "start_house")
reachable_map = Dijkstra(the_map)
reachable_map.fill(start_entrance.map_x, start_entrance.map_y)
# Find each entrance that is not reachable from any other spot, and flood fill from that entrance
for info in entrances:
if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == -1:
reachable_map.fill(info.map_x, info.map_y)
disabled_entrances = ["boomerang_cave", "seashell_mansion"]
house_entrances = ["rooster_house", "writes_house", "photo_house", "raft_house", "crazy_tracy", "witch", "dream_hut", "shop", "madambowwow", "kennel", "library", "ulrira", "trendy_shop", "armos_temple", "banana_seller", "ghost_house", "animal_house1", "animal_house2", "animal_house3", "animal_house4", "animal_house5"]
cave_entrances = ["madbatter_taltal", "bird_cave", "right_fairy", "moblin_cave", "hookshot_cave", "forest_madbatter", "castle_jump_cave", "rooster_grave", "prairie_left_cave1", "prairie_left_cave2", "prairie_left_fairy", "mamu", "armos_fairy", "armos_maze_cave", "prairie_madbatter", "animal_cave", "desert_cave"]
water_entrances = ["mambo", "heartpiece_swim_cave"]
phone_entrances = ["phone_d8", "writes_phone", "castle_phone", "mabe_phone", "prairie_left_phone", "prairie_right_phone", "prairie_low_phone", "animal_phone"]
dungeon_entrances = ["d7", "d8", "d6", "d5", "d4", "d3", "d2", "d1", "d0"]
connector_entrances = [("fire_cave_entrance", "fire_cave_exit"), ("left_to_right_taltalentrance", "left_taltal_entrance"), ("obstacle_cave_entrance", "obstacle_cave_outside_chest", "obstacle_cave_exit"), ("papahl_entrance", "papahl_exit"), ("multichest_left", "multichest_right", "multichest_top"), ("right_taltal_connector1", "right_taltal_connector2"), ("right_taltal_connector3", "right_taltal_connector4"), ("right_taltal_connector5", "right_taltal_connector6"), ("writes_cave_left", "writes_cave_right"), ("raft_return_enter", "raft_return_exit"), ("toadstool_entrance", "toadstool_exit"), ("graveyard_cave_left", "graveyard_cave_right"), ("castle_main_entrance", "castle_upper_left", "castle_upper_right"), ("castle_secret_entrance", "castle_secret_exit"), ("papahl_house_left", "papahl_house_right"), ("prairie_right_cave_top", "prairie_right_cave_bottom", "prairie_right_cave_high"), ("prairie_to_animal_connector", "animal_to_prairie_connector"), ("d6_connector_entrance", "d6_connector_exit"), ("richard_house", "richard_maze"), ("prairie_madbatter_connector_entrance", "prairie_madbatter_connector_exit")]
# For each area that is not yet reachable from the start area:
# add a connector cave from a reachable area to this new area.
reachable_areas = [0]
unreachable_areas = list(range(1, reachable_map.next_area_id))
retry_count = 10000
while unreachable_areas:
source = random.choice(reachable_areas)
target = random.choice(unreachable_areas)
source_entrances = [info for info in todo_entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == source]
target_entrances = [info for info in todo_entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == target]
if not source_entrances:
retry_count -= 1
if retry_count < 1:
raise RuntimeError("Failed to add connectors...")
continue
source_info = random.choice(source_entrances)
target_info = random.choice(target_entrances)
connector = random.choice(connector_entrances)
connector_entrances.remove(connector)
Entrance(source_info.room, source_info.x, source_info.y, connector[0])
todo_entrances.remove(source_info)
Entrance(target_info.room, target_info.x, target_info.y, connector[1])
todo_entrances.remove(target_info)
for extra_exit in connector[2:]:
info = random.choice(todo_entrances)
todo_entrances.remove(info)
Entrance(info.room, info.x, info.y, extra_exit)
unreachable_areas.remove(target)
reachable_areas.append(target)
# Find areas that only have a single entrance, and try to force something in there.
# As else we have useless dead ends, and that is no fun.
for area_id in range(reachable_map.next_area_id):
area_entrances = [info for info in entrances if reachable_map.area[info.map_x + info.map_y * reachable_map.w] == area_id]
if len(area_entrances) != 1:
continue
cells = []
for y in range(reachable_map.h):
for x in range(reachable_map.w):
if reachable_map.area[x + y * reachable_map.w] == area_id:
if the_map.get(x // 10, y // 8).tiles[(x % 10) + (y % 8) * 10] in walkable_tiles:
cells.append((reachable_map.distance[x + y * reachable_map.w], x, y))
cells.sort(reverse=True)
d, x, y = random.choice(cells[:10])
FloorItem(the_map.get(x // 10, y // 8), x % 10, y % 8)
# Find potential dungeon entrances
# Assign some dungeons
for n in range(4):
if not todo_entrances:
break
info = random.choice(todo_entrances)
todo_entrances.remove(info)
dungeon = random.choice(dungeon_entrances)
dungeon_entrances.remove(dungeon)
Entrance(info.room, info.x, info.y, dungeon)
# Assign something to all other entrances
for info in todo_entrances:
options = house_entrances if info.tile == 0xE2 else cave_entrances
entrance = random.choice(options)
options.remove(entrance)
Entrance(info.room, info.x, info.y, entrance)
# Go over each room, and assign something if nothing is assigned yet
todo_list = [room for room in the_map if not room.locations]
random.shuffle(todo_list)
done_count = {}
for room in todo_list:
options = []
# figure out what things could potentially be placed here
for constructor in all_location_constructors:
if done_count.get(constructor, 0) >= constructor.MAX_COUNT:
continue
xy = constructor.check_possible(room, reachable_map)
if xy is not None:
options.append((*xy, constructor))
if options:
x, y, constructor = random.choice(options)
constructor(room, x, y)
done_count[constructor] = done_count.get(constructor, 0) + 1

View File

@@ -0,0 +1,24 @@
from ...roomEditor import RoomEditor
from ..map import RoomInfo
class LocationBase:
MAX_COUNT = 9999
def __init__(self, room: RoomInfo, x, y):
self.room = room
self.x = x
self.y = y
room.locations.append(self)
def prepare(self, rom):
pass
def update_room(self, rom, re: RoomEditor):
pass
def connect_logic(self, logic_location):
raise NotImplementedError(self.__class__)
def get_item_pool(self):
raise NotImplementedError(self.__class__)

View File

@@ -0,0 +1,73 @@
from .base import LocationBase
from ..tileset import solid_tiles, open_tiles, walkable_tiles
from ...roomEditor import RoomEditor
from ...locations.all import HeartPiece, Chest as ChestLocation
import random
class Chest(LocationBase):
def __init__(self, room, x, y):
super().__init__(room, x, y)
room.tiles[x + y * 10] = 0xA0
def connect_logic(self, logic_location):
logic_location.add(ChestLocation(self.room.x + self.room.y * 16))
def get_item_pool(self):
return {None: 1}
@staticmethod
def check_possible(room, reachable_map):
# Check if we can potentially place a chest here, and what the best spot would be.
options = []
for y in range(1, 6):
for x in range(1, 9):
if room.tiles[x + y * 10 - 10] not in solid_tiles: # Chest needs to be against a "wall" at the top
continue
if room.tiles[x + y * 10] not in walkable_tiles or room.tiles[x + y * 10 + 10] not in walkable_tiles:
continue
if room.tiles[x - 1 + y * 10] not in solid_tiles and room.tiles[x - 1 + y * 10 + 10] not in open_tiles:
continue
if room.tiles[x + 1 + y * 10] not in solid_tiles and room.tiles[x + 1 + y * 10 + 10] not in open_tiles:
continue
idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w
if reachable_map.area[idx] == -1:
continue
options.append((reachable_map.distance[idx], x, y))
if not options:
return None
options.sort(reverse=True)
options = [(x, y) for d, x, y in options if d > options[0][0] - 4]
return random.choice(options)
class FloorItem(LocationBase):
def __init__(self, room, x, y):
super().__init__(room, x, y)
def update_room(self, rom, re: RoomEditor):
re.entities.append((self.x, self.y, 0x35))
def connect_logic(self, logic_location):
logic_location.add(HeartPiece(self.room.x + self.room.y * 16))
def get_item_pool(self):
return {None: 1}
@staticmethod
def check_possible(room, reachable_map):
# Check if we can potentially place a floor item here, and what the best spot would be.
options = []
for y in range(1, 7):
for x in range(1, 9):
if room.tiles[x + y * 10] not in walkable_tiles:
continue
idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w
if reachable_map.area[idx] == -1:
continue
options.append((reachable_map.distance[idx], x, y))
if not options:
return None
options.sort(reverse=True)
options = [(x, y) for d, x, y in options if d > options[0][0] - 4]
return random.choice(options)

View File

@@ -0,0 +1,107 @@
from ...locations.items import BOMB
from .base import LocationBase
from ...roomEditor import RoomEditor, Object, ObjectWarp
from ...entranceInfo import ENTRANCE_INFO
from ...assembler import ASM
from .entrance_info import INFO
class Entrance(LocationBase):
def __init__(self, room, x, y, entrance_name):
super().__init__(room, x, y)
self.entrance_name = entrance_name
self.entrance_info = ENTRANCE_INFO[entrance_name]
self.source_warp = None
self.target_warp_idx = None
self.inside_logic = None
def prepare(self, rom):
info = self.entrance_info
re = RoomEditor(rom, info.alt_room if info.alt_room is not None else info.room)
self.source_warp = re.getWarps()[info.index if info.index not in (None, "all") else 0]
re = RoomEditor(rom, self.source_warp.room)
for idx, warp in enumerate(re.getWarps()):
if warp.room == info.room or warp.room == info.alt_room:
self.target_warp_idx = idx
def update_room(self, rom, re: RoomEditor):
re.objects.append(self.source_warp)
target = RoomEditor(rom, self.source_warp.room)
warp = target.getWarps()[self.target_warp_idx]
warp.room = self.room.x | (self.room.y << 4)
warp.target_x = self.x * 16 + 8
warp.target_y = self.y * 16 + 18
target.store(rom)
def prepare_logic(self, configuration_options, world_setup, requirements_settings):
if self.entrance_name in INFO and INFO[self.entrance_name].logic is not None:
self.inside_logic = INFO[self.entrance_name].logic(configuration_options, world_setup, requirements_settings)
def connect_logic(self, logic_location):
if self.entrance_name not in INFO:
raise RuntimeError(f"WARNING: Logic connection to entrance unmapped! {self.entrance_name}")
if self.inside_logic:
req = None
if self.room.tiles[self.x + self.y * 10] == 0xBA:
req = BOMB
logic_location.connect(self.inside_logic, req)
if INFO[self.entrance_name].exits:
return [(name, logic(logic_location)) for name, logic in INFO[self.entrance_name].exits]
return None
def get_item_pool(self):
if self.entrance_name not in INFO:
return {}
return INFO[self.entrance_name].items or {}
class DummyEntrance(LocationBase):
def __init__(self, room, x, y):
super().__init__(room, x, y)
def update_room(self, rom, re: RoomEditor):
re.objects.append(ObjectWarp(0x01, 0x10, 0x2A3, 0x50, 0x7C))
def connect_logic(self, logic_location):
return
def get_item_pool(self):
return {}
class EggEntrance(LocationBase):
def __init__(self, room, x, y):
super().__init__(room, x, y)
def update_room(self, rom, re: RoomEditor):
# Setup the warps
re.objects.insert(0, Object(5, 3, 0xE1)) # Hide an entrance tile under the tile where the egg will open.
re.objects.append(ObjectWarp(0x01, 0x08, 0x270, 0x50, 0x7C))
re.entities.append((0, 0, 0xDE)) # egg song event
egg_inside = RoomEditor(rom, 0x270)
egg_inside.getWarps()[0].room = self.room.x
egg_inside.store(rom)
# Fix the alt room layout
alt = RoomEditor(rom, "Alt06")
tiles = re.getTileArray()
tiles[25] = 0xC1
tiles[35] = 0xCB
alt.buildObjectList(tiles, reduce_size=True)
alt.store(rom)
# Patch which room shows as Alt06
rom.patch(0x00, 0x31F1, ASM("cp $06"), ASM(f"cp ${self.room.x:02x}"))
rom.patch(0x00, 0x31F5, ASM("ld a, [$D806]"), ASM(f"ld a, [${0xD800 + self.room.x:04x}]"))
rom.patch(0x20, 0x2DE6, ASM("cp $06"), ASM(f"cp ${self.room.x:02x}"))
rom.patch(0x20, 0x2DEA, ASM("ld a, [$D806]"), ASM(f"ld a, [${0xD800 + self.room.x:04x}]"))
rom.patch(0x19, 0x0D1A, ASM("ld hl, $D806"), ASM(f"ld hl, ${0xD800 + self.room.x:04x}"))
def connect_logic(self, logic_location):
return
def get_item_pool(self):
return {}

View File

@@ -0,0 +1,341 @@
from ...locations.birdKey import BirdKey
from ...locations.chest import Chest
from ...locations.faceKey import FaceKey
from ...locations.goldLeaf import GoldLeaf
from ...locations.heartPiece import HeartPiece
from ...locations.madBatter import MadBatter
from ...locations.song import Song
from ...locations.startItem import StartItem
from ...locations.tradeSequence import TradeSequenceItem
from ...locations.seashell import Seashell
from ...locations.shop import ShopItem
from ...locations.droppedKey import DroppedKey
from ...locations.witch import Witch
from ...logic import *
from ...logic.dungeon1 import Dungeon1
from ...logic.dungeon2 import Dungeon2
from ...logic.dungeon3 import Dungeon3
from ...logic.dungeon4 import Dungeon4
from ...logic.dungeon5 import Dungeon5
from ...logic.dungeon6 import Dungeon6
from ...logic.dungeon7 import Dungeon7
from ...logic.dungeon8 import Dungeon8
from ...logic.dungeonColor import DungeonColor
def one_way(loc, req=None):
res = Location()
loc.connect(res, req, one_way=True)
return res
class EntranceInfo:
def __init__(self, *, items=None, logic=None, exits=None):
self.items = items
self.logic = logic
self.exits = exits
INFO = {
"start_house": EntranceInfo(items={None: 1}, logic=lambda c, w, r: Location().add(StartItem())),
"d0": EntranceInfo(
items={None: 2, KEY9: 3, MAP9: 1, COMPASS9: 1, STONE_BEAK9: 1, NIGHTMARE_KEY9: 1},
logic=lambda c, w, r: DungeonColor(c, w, r).entrance
),
"d1": EntranceInfo(
items={None: 3, KEY1: 3, MAP1: 1, COMPASS1: 1, STONE_BEAK1: 1, NIGHTMARE_KEY1: 1, HEART_CONTAINER: 1, INSTRUMENT1: 1},
logic=lambda c, w, r: Dungeon1(c, w, r).entrance
),
"d2": EntranceInfo(
items={None: 3, KEY2: 5, MAP2: 1, COMPASS2: 1, STONE_BEAK2: 1, NIGHTMARE_KEY2: 1, HEART_CONTAINER: 1, INSTRUMENT2: 1},
logic=lambda c, w, r: Dungeon2(c, w, r).entrance
),
"d3": EntranceInfo(
items={None: 4, KEY3: 9, MAP3: 1, COMPASS3: 1, STONE_BEAK3: 1, NIGHTMARE_KEY3: 1, HEART_CONTAINER: 1, INSTRUMENT3: 1},
logic=lambda c, w, r: Dungeon3(c, w, r).entrance
),
"d4": EntranceInfo(
items={None: 4, KEY4: 5, MAP4: 1, COMPASS4: 1, STONE_BEAK4: 1, NIGHTMARE_KEY4: 1, HEART_CONTAINER: 1, INSTRUMENT4: 1},
logic=lambda c, w, r: Dungeon4(c, w, r).entrance
),
"d5": EntranceInfo(
items={None: 5, KEY5: 3, MAP5: 1, COMPASS5: 1, STONE_BEAK5: 1, NIGHTMARE_KEY5: 1, HEART_CONTAINER: 1, INSTRUMENT5: 1},
logic=lambda c, w, r: Dungeon5(c, w, r).entrance
),
"d6": EntranceInfo(
items={None: 6, KEY6: 3, MAP6: 1, COMPASS6: 1, STONE_BEAK6: 1, NIGHTMARE_KEY6: 1, HEART_CONTAINER: 1, INSTRUMENT6: 1},
logic=lambda c, w, r: Dungeon6(c, w, r, raft_game_chest=False).entrance
),
"d7": EntranceInfo(
items={None: 4, KEY7: 3, MAP7: 1, COMPASS7: 1, STONE_BEAK7: 1, NIGHTMARE_KEY7: 1, HEART_CONTAINER: 1, INSTRUMENT7: 1},
logic=lambda c, w, r: Dungeon7(c, w, r).entrance
),
"d8": EntranceInfo(
items={None: 6, KEY8: 7, MAP8: 1, COMPASS8: 1, STONE_BEAK8: 1, NIGHTMARE_KEY8: 1, HEART_CONTAINER: 1, INSTRUMENT8: 1},
logic=lambda c, w, r: Dungeon8(c, w, r, back_entrance_heartpiece=False).entrance
),
"writes_cave_left": EntranceInfo(
items={None: 2},
logic=lambda c, w, r: Location().connect(
Location().add(Chest(0x2AE)), OR(FEATHER, ROOSTER, HOOKSHOT)
).connect(
Location().add(Chest(0x2AF)), POWER_BRACELET
),
exits=[("writes_cave_right", lambda loc: loc)],
),
"writes_cave_right": EntranceInfo(),
"castle_main_entrance": EntranceInfo(
items={None: 2},
logic=lambda c, w, r: Location().connect(
Location().add(GoldLeaf(0x2D2)), r.attack_hookshot_powder # in the castle, kill enemies
).connect(
Location().add(GoldLeaf(0x2C5)), AND(BOMB, r.attack_hookshot_powder) # in the castle, bomb wall to show enemy
),
exits=[("castle_upper_left", lambda loc: loc)],
),
"castle_upper_left": EntranceInfo(),
"castle_upper_right": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(GoldLeaf(0x2C6)), AND(POWER_BRACELET, r.attack_hookshot)),
),
"right_taltal_connector1": EntranceInfo(
logic=lambda c, w, r: Location(),
exits=[("right_taltal_connector2", lambda loc: loc)],
),
"right_taltal_connector2": EntranceInfo(),
"fire_cave_entrance": EntranceInfo(
logic=lambda c, w, r: Location(),
exits=[("fire_cave_exit", lambda loc: Location().connect(loc, COUNT(SHIELD, 2)))],
),
"fire_cave_exit": EntranceInfo(),
"graveyard_cave_left": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(HeartPiece(0x2DF)), OR(AND(BOMB, OR(HOOKSHOT, PEGASUS_BOOTS), FEATHER), ROOSTER)),
exits=[("graveyard_cave_right", lambda loc: Location().connect(loc, OR(FEATHER, ROOSTER)))],
),
"graveyard_cave_right": EntranceInfo(),
"raft_return_enter": EntranceInfo(
logic=lambda c, w, r: Location(),
exits=[("raft_return_exit", one_way)],
),
"raft_return_exit": EntranceInfo(),
"prairie_right_cave_top": EntranceInfo(
logic=lambda c, w, r: Location(),
exits=[("prairie_right_cave_bottom", lambda loc: loc), ("prairie_right_cave_high", lambda loc: Location().connect(loc, AND(BOMB, OR(FEATHER, ROOSTER))))],
),
"prairie_right_cave_bottom": EntranceInfo(),
"prairie_right_cave_high": EntranceInfo(),
"armos_maze_cave": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().add(Chest(0x2FC)),
),
"right_taltal_connector3": EntranceInfo(
logic=lambda c, w, r: Location(),
exits=[("right_taltal_connector4", lambda loc: one_way(loc, AND(OR(FEATHER, ROOSTER), HOOKSHOT)))],
),
"right_taltal_connector4": EntranceInfo(),
"obstacle_cave_entrance": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2BB)), AND(SWORD, OR(HOOKSHOT, ROOSTER))),
exits=[
("obstacle_cave_outside_chest", lambda loc: Location().connect(loc, SWORD)),
("obstacle_cave_exit", lambda loc: Location().connect(loc, AND(SWORD, OR(PEGASUS_BOOTS, ROOSTER))))
],
),
"obstacle_cave_outside_chest": EntranceInfo(),
"obstacle_cave_exit": EntranceInfo(),
"d6_connector_entrance": EntranceInfo(
logic=lambda c, w, r: Location(),
exits=[("d6_connector_exit", lambda loc: Location().connect(loc, OR(AND(HOOKSHOT, OR(FLIPPERS, AND(FEATHER, PEGASUS_BOOTS))), ROOSTER)))],
),
"d6_connector_exit": EntranceInfo(),
"multichest_left": EntranceInfo(
logic=lambda c, w, r: Location(),
exits=[
("multichest_right", lambda loc: loc),
("multichest_top", lambda loc: Location().connect(loc, BOMB)),
],
),
"multichest_right": EntranceInfo(),
"multichest_top": EntranceInfo(),
"prairie_madbatter_connector_entrance": EntranceInfo(
logic=lambda c, w, r: Location(),
exits=[("prairie_madbatter_connector_exit", lambda loc: Location().connect(loc, FLIPPERS))],
),
"prairie_madbatter_connector_exit": EntranceInfo(),
"papahl_house_left": EntranceInfo(
logic=lambda c, w, r: Location(),
exits=[("papahl_house_right", lambda loc: loc)],
),
"papahl_house_right": EntranceInfo(),
"prairie_to_animal_connector": EntranceInfo(
logic=lambda c, w, r: Location(),
exits=[("animal_to_prairie_connector", lambda loc: Location().connect(loc, PEGASUS_BOOTS))],
),
"animal_to_prairie_connector": EntranceInfo(),
"castle_secret_entrance": EntranceInfo(
logic=lambda c, w, r: Location(),
exits=[("castle_secret_exit", lambda loc: Location().connect(loc, FEATHER))],
),
"castle_secret_exit": EntranceInfo(),
"papahl_entrance": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().add(Chest(0x28A)),
exits=[("papahl_exit", lambda loc: loc)],
),
"papahl_exit": EntranceInfo(),
"right_taltal_connector5": EntranceInfo(
logic=lambda c, w, r: Location(),
exits=[("right_taltal_connector6", lambda loc: loc)],
),
"right_taltal_connector6": EntranceInfo(),
"toadstool_entrance": EntranceInfo(
items={None: 2},
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2BD)), SWORD).connect( # chest in forest cave on route to mushroom
Location().add(HeartPiece(0x2AB), POWER_BRACELET)), # piece of heart in the forest cave on route to the mushroom
exits=[("right_taltal_connector6", lambda loc: loc)],
),
"toadstool_exit": EntranceInfo(),
"richard_house": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2C8)), AND(COUNT(GOLD_LEAF, 5), OR(FEATHER, HOOKSHOT, ROOSTER))),
exits=[("richard_maze", lambda loc: Location().connect(loc, COUNT(GOLD_LEAF, 5)))],
),
"richard_maze": EntranceInfo(),
"left_to_right_taltalentrance": EntranceInfo(
exits=[("left_taltal_entrance", lambda loc: one_way(loc, OR(HOOKSHOT, ROOSTER)))],
),
"left_taltal_entrance": EntranceInfo(),
"boomerang_cave": EntranceInfo(), # TODO boomerang gift
"trendy_shop": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)), FOUND("RUPEES", 50))
),
"moblin_cave": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2E2)), AND(r.attack_hookshot_powder, r.miniboss_requirements[w.miniboss_mapping["moblin_cave"]]))
),
"prairie_madbatter": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(MadBatter(0x1E0)), MAGIC_POWDER)
),
"ulrira": EntranceInfo(),
"rooster_house": EntranceInfo(),
"animal_house2": EntranceInfo(),
"animal_house4": EntranceInfo(),
"armos_fairy": EntranceInfo(),
"right_fairy": EntranceInfo(),
"photo_house": EntranceInfo(),
"bird_cave": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(BirdKey()), OR(AND(FEATHER, COUNT(POWER_BRACELET, 2)), ROOSTER))
),
"mamu": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(Song(0x2FB)), AND(OCARINA, COUNT("RUPEES", 300)))
),
"armos_temple": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(FaceKey()), r.miniboss_requirements[w.miniboss_mapping["armos_temple"]])
),
"animal_house1": EntranceInfo(),
"madambowwow": EntranceInfo(),
"library": EntranceInfo(),
"kennel": EntranceInfo(
items={None: 1, TRADING_ITEM_RIBBON: 1},
logic=lambda c, w, r: Location().connect(Location().add(Seashell(0x2B2)), SHOVEL).connect(Location().add(TradeSequenceItem(0x2B2, TRADING_ITEM_DOG_FOOD)), TRADING_ITEM_RIBBON)
),
"dream_hut": EntranceInfo(
items={None: 2},
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2BF)), OR(SWORD, BOOMERANG, HOOKSHOT, FEATHER)).connect(Location().add(Chest(0x2BE)), AND(OR(SWORD, BOOMERANG, HOOKSHOT, FEATHER), PEGASUS_BOOTS))
),
"hookshot_cave": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2B3)), OR(HOOKSHOT, ROOSTER))
),
"madbatter_taltal": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(MadBatter(0x1E2)), MAGIC_POWDER)
),
"forest_madbatter": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(MadBatter(0x1E1)), MAGIC_POWDER)
),
"banana_seller": EntranceInfo(
items={TRADING_ITEM_DOG_FOOD: 1},
logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2FE, TRADING_ITEM_BANANAS)), TRADING_ITEM_DOG_FOOD)
),
"shop": EntranceInfo(
items={None: 2},
logic=lambda c, w, r: Location().connect(Location().add(ShopItem(0)), COUNT("RUPEES", 200)).connect(Location().add(ShopItem(1)), COUNT("RUPEES", 980))
),
"ghost_house": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(Seashell(0x1E3)), POWER_BRACELET)
),
"writes_house": EntranceInfo(
items={TRADING_ITEM_LETTER: 1},
logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2A8, TRADING_ITEM_BROOM)), TRADING_ITEM_LETTER)
),
"animal_house3": EntranceInfo(
items={TRADING_ITEM_HIBISCUS: 1},
logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2D9, TRADING_ITEM_LETTER)), TRADING_ITEM_HIBISCUS)
),
"animal_house5": EntranceInfo(
items={TRADING_ITEM_HONEYCOMB: 1},
logic=lambda c, w, r: Location().connect(Location().add(TradeSequenceItem(0x2D7, TRADING_ITEM_PINEAPPLE)), TRADING_ITEM_HONEYCOMB)
),
"crazy_tracy": EntranceInfo(
items={"MEDICINE2": 1},
logic=lambda c, w, r: Location().connect(Location().add(KeyLocation("MEDICINE2")), FOUND("RUPEES", 50))
),
"rooster_grave": EntranceInfo(
logic=lambda c, w, r: Location().connect(Location().add(DroppedKey(0x1E4)), AND(OCARINA, SONG3))
),
"desert_cave": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().connect(Location().add(HeartPiece(0x1E8)), BOMB)
),
"witch": EntranceInfo(
items={TOADSTOOL: 1},
logic=lambda c, w, r: Location().connect(Location().add(Witch()), TOADSTOOL)
),
"prairie_left_cave1": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().add(Chest(0x2CD))
),
"prairie_left_cave2": EntranceInfo(
items={None: 2},
logic=lambda c, w, r: Location().connect(Location().add(Chest(0x2F4)), PEGASUS_BOOTS).connect(Location().add(HeartPiece(0x2E5)), AND(BOMB, PEGASUS_BOOTS))
),
"castle_jump_cave": EntranceInfo(
items={None: 1},
logic=lambda c, w, r: Location().add(Chest(0x1FD))
),
"raft_house": EntranceInfo(),
"prairie_left_fairy": EntranceInfo(),
"seashell_mansion": EntranceInfo(), # TODO: Not sure if we can guarantee enough shells
}

View File

@@ -0,0 +1,172 @@
from ..logic import Location, PEGASUS_BOOTS, SHOVEL
from .base import LocationBase
from ..tileset import solid_tiles, open_tiles, walkable_tiles
from ...roomEditor import RoomEditor
from ...assembler import ASM
from ...locations.all import Seashell
import random
class HiddenSeashell(LocationBase):
def __init__(self, room, x, y):
super().__init__(room, x, y)
if room.tiles[x + y * 10] not in (0x20, 0x5C):
if random.randint(0, 1):
room.tiles[x + y * 10] = 0x20 # rock
else:
room.tiles[x + y * 10] = 0x5C # bush
def update_room(self, rom, re: RoomEditor):
re.entities.append((self.x, self.y, 0x3D))
def connect_logic(self, logic_location):
logic_location.add(Seashell(self.room.x + self.room.y * 16))
def get_item_pool(self):
return {None: 1}
@staticmethod
def check_possible(room, reachable_map):
# Check if we can potentially place a hidden seashell here
# First see if we have a nice bush or rock to hide under
options = []
for y in range(1, 7):
for x in range(1, 9):
if room.tiles[x + y * 10] not in {0x20, 0x5C}:
continue
idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w
if reachable_map.area[idx] == -1:
continue
options.append((reachable_map.distance[idx], x, y))
if not options:
# No existing bush, we can always add one. So find a nice spot
for y in range(1, 7):
for x in range(1, 9):
if room.tiles[x + y * 10] not in walkable_tiles:
continue
if room.tiles[x + y * 10] == 0x1E: # ocean edge
continue
idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w
if reachable_map.area[idx] == -1:
continue
options.append((reachable_map.distance[idx], x, y))
if not options:
return None
options.sort(reverse=True)
options = [(x, y) for d, x, y in options if d > options[0][0] - 4]
return random.choice(options)
class DigSeashell(LocationBase):
MAX_COUNT = 6
def __init__(self, room, x, y):
super().__init__(room, x, y)
if room.tileset_id == "beach":
room.tiles[x + y * 10] = 0x08
for ox, oy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
if room.tiles[x + ox + (y + oy) * 10] != 0x1E:
room.tiles[x + ox + (y + oy) * 10] = 0x24
else:
room.tiles[x + y * 10] = 0x04
for ox, oy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
room.tiles[x + ox + (y + oy) * 10] = 0x0A
def update_room(self, rom, re: RoomEditor):
re.entities.append((self.x, self.y, 0x3D))
if rom.banks[0x03][0x2210] == 0xFF:
rom.patch(0x03, 0x220F, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}"))
elif rom.banks[0x03][0x2214] == 0xFF:
rom.patch(0x03, 0x2213, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}"))
elif rom.banks[0x03][0x2218] == 0xFF:
rom.patch(0x03, 0x2217, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}"))
elif rom.banks[0x03][0x221C] == 0xFF:
rom.patch(0x03, 0x221B, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}"))
elif rom.banks[0x03][0x2220] == 0xFF:
rom.patch(0x03, 0x221F, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}"))
elif rom.banks[0x03][0x2224] == 0xFF:
rom.patch(0x03, 0x2223, ASM("cp $FF"), ASM(f"cp ${self.room.x | (self.room.y << 4):02x}"))
def connect_logic(self, logic_location):
logic_location.connect(Location().add(Seashell(self.room.x + self.room.y * 16)), SHOVEL)
def get_item_pool(self):
return {None: 1}
@staticmethod
def check_possible(room, reachable_map):
options = []
for y in range(1, 7):
for x in range(1, 9):
if room.tiles[x + y * 10] not in walkable_tiles:
continue
if room.tiles[x - 1 + y * 10] not in walkable_tiles:
continue
if room.tiles[x + 1 + y * 10] not in walkable_tiles:
continue
if room.tiles[x + (y - 1) * 10] not in walkable_tiles:
continue
if room.tiles[x + (y + 1) * 10] not in walkable_tiles:
continue
idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w
if reachable_map.area[idx] == -1:
continue
options.append((x, y))
if not options:
return None
return random.choice(options)
class BonkSeashell(LocationBase):
MAX_COUNT = 2
def __init__(self, room, x, y):
super().__init__(room, x, y)
self.tree_x = x
self.tree_y = y
for offsetx, offsety in [(-1, 0), (-1, 1), (2, 0), (2, 1), (0, -1), (1, -1), (0, 2), (1, 2)]:
if room.tiles[x + offsetx + (y + offsety) * 10] in walkable_tiles:
self.x += offsetx
self.y += offsety
break
def update_room(self, rom, re: RoomEditor):
re.entities.append((self.tree_x, self.tree_y, 0x3D))
if rom.banks[0x03][0x0F04] == 0xFF:
rom.patch(0x03, 0x0F03, ASM("cp $FF"), ASM(f"cp ${self.room.x|(self.room.y<<4):02x}"))
elif rom.banks[0x03][0x0F08] == 0xFF:
rom.patch(0x03, 0x0F07, ASM("cp $FF"), ASM(f"cp ${self.room.x|(self.room.y<<4):02x}"))
else:
raise RuntimeError("To many bonk seashells")
def connect_logic(self, logic_location):
logic_location.connect(Location().add(Seashell(self.room.x + self.room.y * 16)), PEGASUS_BOOTS)
def get_item_pool(self):
return {None: 1}
@staticmethod
def check_possible(room, reachable_map):
# Check if we can potentially place a hidden seashell here
# Find potential trees
options = []
for y in range(1, 6):
for x in range(1, 8):
if room.tiles[x + y * 10] != 0x25:
continue
if room.tiles[x + y * 10 + 1] != 0x26:
continue
if room.tiles[x + y * 10 + 10] != 0x27:
continue
if room.tiles[x + y * 10 + 11] != 0x28:
continue
idx = room.x * 10 + x + (room.y * 8 + y) * reachable_map.w
top_reachable = reachable_map.area[idx - reachable_map.w] != -1 or reachable_map.area[idx - reachable_map.w + 1] != -1
bottom_reachable = reachable_map.area[idx + reachable_map.w * 2] != -1 or reachable_map.area[idx + reachable_map.w * 2 + 1] != -1
left_reachable = reachable_map.area[idx - 1] != -1 or reachable_map.area[idx + reachable_map.w - 1] != -1
right_reachable = reachable_map.area[idx + 2] != -1 or reachable_map.area[idx + reachable_map.w + 2] != -1
if (top_reachable and bottom_reachable) or (left_reachable and right_reachable):
options.append((x, y))
if not options:
return None
return random.choice(options)

View File

@@ -0,0 +1,146 @@
from .map import Map
from .locations.entrance import Entrance
from ..logic import *
from .tileset import walkable_tiles, entrance_tiles
class LogicGenerator:
def __init__(self, configuration_options, world_setup, requirements_settings, the_map: Map):
self.w = the_map.w * 10
self.h = the_map.h * 8
self.map = the_map
self.logic_map = [None] * (self.w * self.h)
self.location_lookup = {}
self.configuration_options = configuration_options
self.world_setup = world_setup
self.requirements_settings = requirements_settings
self.entrance_map = {}
for room in the_map:
for location in room.locations:
self.location_lookup[(room.x * 10 + location.x, room.y * 8 + location.y)] = location
if isinstance(location, Entrance):
location.prepare_logic(configuration_options, world_setup, requirements_settings)
self.entrance_map[location.entrance_name] = location
start = self.entrance_map["start_house"]
self.start = Location()
self.egg = self.start # TODO
self.nightmare = Location()
self.windfish = Location().connect(self.nightmare, AND(MAGIC_POWDER, SWORD, OR(BOOMERANG, BOW)))
self.fill_walkable(self.start, start.room.x * 10 + start.x, start.room.y * 8 + start.y)
logic_str_map = {None: "."}
for y in range(self.h):
line = ""
for x in range(self.w):
if self.logic_map[x + y * self.w] not in logic_str_map:
logic_str_map[self.logic_map[x + y * self.w]] = chr(len(logic_str_map)+48)
line += logic_str_map[self.logic_map[x + y * self.w]]
print(line)
for room in the_map:
for location in room.locations:
if self.logic_map[(room.x * 10 + location.x) + (room.y * 8 + location.y) * self.w] is None:
raise RuntimeError(f"Location not mapped to logic: {room} {location.__class__.__name__} {location.x} {location.y}")
tmp = set()
def r(n):
if n in tmp:
return
tmp.add(n)
for item in n.items:
print(item)
for o, req in n.simple_connections:
r(o)
for o, req in n.gated_connections:
r(o)
r(self.start)
def fill_walkable(self, location, x, y):
tile_options = walkable_tiles | entrance_tiles
for x, y in self.flood_fill_logic(location, tile_options, x, y):
if self.logic_map[x + y * self.w] is not None:
continue
tile = self.map.get_tile(x, y)
if tile == 0x5C: # bush
other_location = Location()
location.connect(other_location, self.requirements_settings.bush)
self.fill_bush(other_location, x, y)
elif tile == 0x20: # rock
other_location = Location()
location.connect(other_location, POWER_BRACELET)
self.fill_rock(other_location, x, y)
elif tile == 0xE8: # pit
if self.map.get_tile(x - 1, y) in tile_options and self.map.get_tile(x + 1, y) in tile_options:
if self.logic_map[x - 1 + y * self.w] == location and self.logic_map[x + 1 + y * self.w] is None:
other_location = Location().connect(location, FEATHER)
self.fill_walkable(other_location, x + 1, y)
if self.logic_map[x - 1 + y * self.w] is None and self.logic_map[x + 1 + y * self.w] == location:
other_location = Location().connect(location, FEATHER)
self.fill_walkable(other_location, x - 1, y)
if self.map.get_tile(x, y - 1) in tile_options and self.map.get_tile(x, y + 1) in tile_options:
if self.logic_map[x + (y - 1) * self.w] == location and self.logic_map[x + (y + 1) * self.w] is None:
other_location = Location().connect(location, FEATHER)
self.fill_walkable(other_location, x, y + 1)
if self.logic_map[x + (y - 1) * self.w] is None and self.logic_map[x + (y + 1) * self.w] == location:
other_location = Location().connect(location, FEATHER)
self.fill_walkable(other_location, x, y - 1)
def fill_bush(self, location, x, y):
for x, y in self.flood_fill_logic(location, {0x5C}, x, y):
if self.logic_map[x + y * self.w] is not None:
continue
tile = self.map.get_tile(x, y)
if tile in walkable_tiles or tile in entrance_tiles:
other_location = Location()
location.connect(other_location, self.requirements_settings.bush)
self.fill_walkable(other_location, x, y)
def fill_rock(self, location, x, y):
for x, y in self.flood_fill_logic(location, {0x20}, x, y):
if self.logic_map[x + y * self.w] is not None:
continue
tile = self.map.get_tile(x, y)
if tile in walkable_tiles or tile in entrance_tiles:
other_location = Location()
location.connect(other_location, POWER_BRACELET)
self.fill_walkable(other_location, x, y)
def flood_fill_logic(self, location, tile_types, x, y):
assert self.map.get_tile(x, y) in tile_types
todo = [(x, y)]
entrance_todo = []
edge_set = set()
while todo:
x, y = todo.pop()
if self.map.get_tile(x, y) not in tile_types:
edge_set.add((x, y))
continue
if self.logic_map[x + y * self.w] is not None:
continue
self.logic_map[x + y * self.w] = location
if (x, y) in self.location_lookup:
room_location = self.location_lookup[(x, y)]
result = room_location.connect_logic(location)
if result:
entrance_todo += result
if x < self.w - 1 and self.logic_map[x + 1 + y * self.w] is None:
todo.append((x + 1, y))
if x > 0 and self.logic_map[x - 1 + y * self.w] is None:
todo.append((x - 1, y))
if y < self.h - 1 and self.logic_map[x + y * self.w + self.w] is None:
todo.append((x, y + 1))
if y > 0 and self.logic_map[x + y * self.w - self.w] is None:
if self.map.get_tile(x, y - 1) == 0xA0: # Chest, can only be collected from the south
self.location_lookup[(x, y - 1)].connect_logic(location)
self.logic_map[x + (y - 1) * self.w] = location
todo.append((x, y - 1))
for entrance_name, logic_connection in entrance_todo:
entrance = self.entrance_map[entrance_name]
entrance.connect_logic(logic_connection)
self.fill_walkable(logic_connection, entrance.room.x * 10 + entrance.x, entrance.room.y * 8 + entrance.y)
return edge_set

View File

@@ -0,0 +1,231 @@
import random
from .tileset import solid_tiles, open_tiles
from ..locations.items import *
PRIMARY_ITEMS = [POWER_BRACELET, SHIELD, BOW, HOOKSHOT, MAGIC_ROD, PEGASUS_BOOTS, OCARINA, FEATHER, SHOVEL, MAGIC_POWDER, BOMB, SWORD, FLIPPERS, SONG1]
SECONDARY_ITEMS = [BOOMERANG, RED_TUNIC, BLUE_TUNIC, MAX_POWDER_UPGRADE, MAX_BOMBS_UPGRADE, MAX_ARROWS_UPGRADE, GEL]
HORIZONTAL = 0
VERTICAL = 1
class RoomEdge:
def __init__(self, direction):
self.__solid = False
self.__open_range = None
self.direction = direction
self.__open_min = 2 if direction == HORIZONTAL else 1
self.__open_max = 8 if direction == HORIZONTAL else 7
def force_solid(self):
self.__open_min = -1
self.__open_max = -1
self.__open_range = None
self.__solid = True
def set_open_min(self, value):
if self.__open_min < 0:
return
self.__open_min = max(self.__open_min, value)
def set_open_max(self, value):
if self.__open_max < 0:
return
self.__open_max = min(self.__open_max, value)
def set_solid(self):
self.__open_range = None
self.__solid = True
def can_open(self):
return self.__open_min > -1
def set_open(self):
cnt = random.randint(1, self.__open_max - self.__open_min)
if random.randint(1, 100) < 50:
cnt = 1
offset = random.randint(self.__open_min, self.__open_max - cnt)
self.__open_range = (offset, offset + cnt)
self.__solid = False
def is_solid(self):
return self.__solid
def get_open_range(self):
return self.__open_range
def seed(self, wfc, x, y):
for offset, cell in self.__cells(wfc, x, y):
if self.__open_range and self.__open_range[0] <= offset < self.__open_range[1]:
cell.init_options.intersection_update(open_tiles)
elif self.__solid:
cell.init_options.intersection_update(solid_tiles)
def __cells(self, wfc, x, y):
if self.direction == HORIZONTAL:
for n in range(1, 9):
yield n, wfc.cell_data[(x + n, y)]
else:
for n in range(1, 7):
yield n, wfc.cell_data[(x, y + n)]
class RoomInfo:
def __init__(self, x, y):
self.x = x
self.y = y
self.tileset_id = "basic"
self.room_type = None
self.tiles = None
self.edge_left = None
self.edge_up = None
self.edge_right = RoomEdge(VERTICAL)
self.edge_down = RoomEdge(HORIZONTAL)
self.room_left = None
self.room_up = None
self.room_right = None
self.room_down = None
self.locations = []
self.entities = []
def __repr__(self):
return f"Room<{self.x} {self.y}>"
class Map:
def __init__(self, w, h, tilesets):
self.w = w
self.h = h
self.tilesets = tilesets
self.__rooms = [RoomInfo(x, y) for y in range(h) for x in range(w)]
for x in range(w):
for y in range(h):
room = self.get(x, y)
if x == 0:
room.edge_left = RoomEdge(VERTICAL)
else:
room.edge_left = self.get(x - 1, y).edge_right
if y == 0:
room.edge_up = RoomEdge(HORIZONTAL)
else:
room.edge_up = self.get(x, y - 1).edge_down
if x > 0:
room.room_left = self.get(x - 1, y)
if x < w - 1:
room.room_right = self.get(x + 1, y)
if y > 0:
room.room_up = self.get(x, y - 1)
if y < h - 1:
room.room_down = self.get(x, y + 1)
for x in range(w):
self.get(x, 0).edge_up.set_solid()
self.get(x, h-1).edge_down.set_solid()
for y in range(h):
self.get(0, y).edge_left.set_solid()
self.get(w-1, y).edge_right.set_solid()
def __iter__(self):
return iter(self.__rooms)
def get(self, x, y) -> RoomInfo:
assert 0 <= x < self.w and 0 <= y < self.h, f"{x} {y}"
return self.__rooms[x + y * self.w]
def get_tile(self, x, y):
return self.get(x // 10, y // 8).tiles[(x % 10) + (y % 8) * 10]
def get_item_pool(self):
item_pool = {}
for room in self.__rooms:
for location in room.locations:
print(room, location.get_item_pool(), location.__class__.__name__)
for k, v in location.get_item_pool().items():
item_pool[k] = item_pool.get(k, 0) + v
unmapped_count = item_pool.get(None, 0)
del item_pool[None]
for item in PRIMARY_ITEMS:
if item not in item_pool:
item_pool[item] = 1
unmapped_count -= 1
while item_pool[POWER_BRACELET] < 2:
item_pool[POWER_BRACELET] = item_pool.get(POWER_BRACELET, 0) + 1
unmapped_count -= 1
while item_pool[SHIELD] < 2:
item_pool[SHIELD] = item_pool.get(SHIELD, 0) + 1
unmapped_count -= 1
assert unmapped_count >= 0
for item in SECONDARY_ITEMS:
if unmapped_count > 0:
item_pool[item] = item_pool.get(item, 0) + 1
unmapped_count -= 1
# Add a heart container per 10 items "spots" left.
heart_piece_count = unmapped_count // 10
unmapped_count -= heart_piece_count * 4
item_pool[HEART_PIECE] = item_pool.get(HEART_PIECE, 0) + heart_piece_count * 4
# Add the rest as rupees
item_pool[RUPEES_50] = item_pool.get(RUPEES_50, 0) + unmapped_count
return item_pool
def dump(self):
for y in range(self.h):
for x in range(self.w):
if self.get(x, y).edge_right.is_solid():
print(" |", end="")
elif self.get(x, y).edge_right.get_open_range():
print(" ", end="")
else:
print(" ?", end="")
print()
for x in range(self.w):
if self.get(x, y).edge_down.is_solid():
print("-+", end="")
elif self.get(x, y).edge_down.get_open_range():
print(" +", end="")
else:
print("?+", end="")
print()
print()
class MazeGen:
UP = 0x01
DOWN = 0x02
LEFT = 0x04
RIGHT = 0x08
def __init__(self, the_map: Map):
self.map = the_map
self.visited = set()
self.visit(0, 0)
def visit(self, x, y):
self.visited.add((x, y))
neighbours = self.get_neighbours(x, y)
while any((x, y) not in self.visited for x, y, d in neighbours):
x, y, d = random.choice(neighbours)
if (x, y) not in self.visited:
if d == self.RIGHT and self.map.get(x, y).edge_left.can_open():
self.map.get(x, y).edge_left.set_open()
elif d == self.LEFT and self.map.get(x, y).edge_right.can_open():
self.map.get(x, y).edge_right.set_open()
elif d == self.DOWN and self.map.get(x, y).edge_up.can_open():
self.map.get(x, y).edge_up.set_open()
elif d == self.UP and self.map.get(x, y).edge_down.can_open():
self.map.get(x, y).edge_down.set_open()
self.visit(x, y)
def get_neighbours(self, x, y):
neighbours = []
if x > 0:
neighbours.append((x - 1, y, self.LEFT))
if x < self.map.w - 1:
neighbours.append((x + 1, y, self.RIGHT))
if y > 0:
neighbours.append((x, y - 1, self.UP))
if y < self.map.h - 1:
neighbours.append((x, y + 1, self.DOWN))
return neighbours

View File

@@ -0,0 +1,78 @@
from .map import Map
from .roomtype.town import Town
from .roomtype.mountain import Mountain, MountainEgg
from .roomtype.forest import Forest
from .roomtype.base import RoomType
from .roomtype.water import Water, Beach
import random
def is_area_clear(the_map: Map, x, y, w, h):
for y0 in range(y, y+h):
for x0 in range(x, x+w):
if 0 <= x0 < the_map.w and 0 <= y0 < the_map.h:
if the_map.get(x0, y0).room_type is not None:
return False
return True
def find_random_clear_area(the_map: Map, w, h, *, tries):
for n in range(tries):
x = random.randint(0, the_map.w - w)
y = random.randint(0, the_map.h - h)
if is_area_clear(the_map, x - 1, y - 1, w + 2, h + 2):
return x, y
return None, None
def setup_room_types(the_map: Map):
# Always make the rop row mountains.
egg_x = the_map.w // 2
for x in range(the_map.w):
if x == egg_x:
MountainEgg(the_map.get(x, 0))
else:
Mountain(the_map.get(x, 0))
# Add some beach.
width = the_map.w if random.random() < 0.5 else random.randint(max(2, the_map.w // 4), the_map.w // 2)
beach_x = 0 # current tileset doesn't allow anything else
for x in range(beach_x, beach_x+width):
# Beach(the_map.get(x, the_map.h - 2))
Beach(the_map.get(x, the_map.h - 1))
the_map.get(beach_x + width - 1, the_map.h - 1).edge_right.force_solid()
town_x, town_y = find_random_clear_area(the_map, 2, 2, tries=20)
if town_x is not None:
for y in range(town_y, town_y + 2):
for x in range(town_x, town_x + 2):
Town(the_map.get(x, y))
forest_w, forest_h = 2, 2
if random.random() < 0.5:
forest_w += 1
else:
forest_h += 1
forest_x, forest_y = find_random_clear_area(the_map, forest_w, forest_h, tries=20)
if forest_x is None:
forest_w, forest_h = 2, 2
forest_x, forest_y = find_random_clear_area(the_map, forest_w, forest_h, tries=20)
if forest_x is not None:
for y in range(forest_y, forest_y + forest_h):
for x in range(forest_x, forest_x + forest_w):
Forest(the_map.get(x, y))
# for n in range(5):
# water_w, water_h = 2, 1
# if random.random() < 0.5:
# water_w, water_h = water_h, water_w
# water_x, water_y = find_random_clear_area(the_map, water_w, water_h, tries=20)
# if water_x is not None:
# for y in range(water_y, water_y + water_h):
# for x in range(water_x, water_x + water_w):
# Water(the_map.get(x, y))
for y in range(the_map.h):
for x in range(the_map.w):
if the_map.get(x, y).room_type is None:
RoomType(the_map.get(x, y))

View File

@@ -0,0 +1,54 @@
from ..tileset import open_tiles
def plot_line(x0, y0, x1, y1):
dx = abs(x1 - x0)
sx = 1 if x0 < x1 else -1
dy = -abs(y1 - y0)
sy = 1 if y0 < y1 else -1
error = dx + dy
yield x0, y0
while True:
if x0 == x1 and y0 == y1:
break
e2 = 2 * error
if e2 >= dy:
error = error + dy
x0 = x0 + sx
yield x0, y0
if e2 <= dx:
error = error + dx
y0 = y0 + sy
yield x0, y0
yield x1, y1
class RoomType:
def __init__(self, room):
self.room = room
room.room_type = self
def seed(self, wfc, x, y):
open_points = []
r = self.room.edge_left.get_open_range()
if r:
open_points.append((x + 1, y + (r[0] + r[1]) // 2))
r = self.room.edge_right.get_open_range()
if r:
open_points.append((x + 8, y + (r[0] + r[1]) // 2))
r = self.room.edge_up.get_open_range()
if r:
open_points.append((x + (r[0] + r[1]) // 2, y + 1))
r = self.room.edge_down.get_open_range()
if r:
open_points.append((x + (r[0] + r[1]) // 2, y + 6))
if len(open_points) < 2:
return
mid_x = sum([x for x, y in open_points]) // len(open_points)
mid_y = sum([y for x, y in open_points]) // len(open_points)
for x0, y0 in open_points:
for px, py in plot_line(x0, y0, mid_x, mid_y):
wfc.cell_data[(px, py)].init_options.intersection_update(open_tiles)

View File

@@ -0,0 +1,28 @@
from .base import RoomType
from ..tileset import open_tiles
import random
class Forest(RoomType):
def __init__(self, room):
super().__init__(room)
room.tileset_id = "forest"
def seed(self, wfc, x, y):
if self.room.room_up and isinstance(self.room.room_up.room_type, Forest) and self.room.edge_up.get_open_range() is None:
self.room.edge_up.set_solid()
if self.room.room_left and isinstance(self.room.room_left.room_type, Forest) and self.room.edge_left.get_open_range() is None:
self.room.edge_left.set_solid()
if self.room.room_up and isinstance(self.room.room_up.room_type, Forest) and random.random() < 0.5:
door_x, door_y = x + 5 + random.randint(-1, 1), y + 3 + random.randint(-1, 1)
wfc.cell_data[(door_x, door_y)].init_options.intersection_update({0xE3})
self.room.edge_up.set_solid()
if self.room.edge_left.get_open_range() is not None:
for x0 in range(x + 1, door_x):
wfc.cell_data[(x0, door_y + 1)].init_options.intersection_update(open_tiles)
if self.room.edge_right.get_open_range() is not None:
for x0 in range(door_x + 1, x + 10):
wfc.cell_data[(x0, door_y + 1)].init_options.intersection_update(open_tiles)
else:
super().seed(wfc, x, y)

View File

@@ -0,0 +1,38 @@
from .base import RoomType
from ..locations.entrance import EggEntrance
import random
class Mountain(RoomType):
def __init__(self, room):
super().__init__(room)
room.tileset_id = "mountains"
room.edge_left.set_open_min(3)
room.edge_right.set_open_min(3)
def seed(self, wfc, x, y):
super().seed(wfc, x, y)
if y == 0:
if x == 0:
wfc.cell_data[(0, 1)].init_options.intersection_update({0})
if x == wfc.w - 10:
wfc.cell_data[(x + 9, 1)].init_options.intersection_update({0})
wfc.cell_data[(x + random.randint(3, 6), random.randint(0, 1))].init_options.intersection_update({0})
class MountainEgg(RoomType):
def __init__(self, room):
super().__init__(room)
room.tileset_id = "egg"
room.edge_left.force_solid()
room.edge_right.force_solid()
room.edge_down.set_open_min(5)
room.edge_down.set_open_max(6)
EggEntrance(room, 5, 4)
def seed(self, wfc, x, y):
super().seed(wfc, x, y)
wfc.cell_data[(x + 2, y + 1)].init_options.intersection_update({0x00})
wfc.cell_data[(x + 2, y + 2)].init_options.intersection_update({0xEF})
wfc.cell_data[(x + 5, y + 3)].init_options.intersection_update({0xAA})

View File

@@ -0,0 +1,16 @@
from .base import RoomType
from ..tileset import solid_tiles
import random
class Town(RoomType):
def __init__(self, room):
super().__init__(room)
room.tileset_id = "town"
def seed(self, wfc, x, y):
ex = x + 5 + random.randint(-1, 1)
ey = y + 3 + random.randint(-1, 1)
wfc.cell_data[(ex, ey)].init_options.intersection_update({0xE2})
wfc.cell_data[(ex - 1, ey - 1)].init_options.intersection_update(solid_tiles)
wfc.cell_data[(ex + 1, ey - 1)].init_options.intersection_update(solid_tiles)

View File

@@ -0,0 +1,30 @@
from .base import RoomType
import random
class Water(RoomType):
def __init__(self, room):
super().__init__(room)
room.tileset_id = "water"
# def seed(self, wfc, x, y):
# wfc.cell_data[(x + 5 + random.randint(-1, 1), y + 3 + random.randint(-1, 1))].init_options.intersection_update({0x0E})
class Beach(RoomType):
def __init__(self, room):
super().__init__(room)
room.tileset_id = "beach"
if self.room.room_down is None:
self.room.edge_left.set_open_max(4)
self.room.edge_right.set_open_max(4)
self.room.edge_up.set_open_min(4)
self.room.edge_up.set_open_max(6)
def seed(self, wfc, x, y):
if self.room.room_down is None:
for n in range(1, 9):
wfc.cell_data[(x + n, y + 5)].init_options.intersection_update({0x1E})
for n in range(1, 9):
wfc.cell_data[(x + n, y + 7)].init_options.intersection_update({0x1F})
super().seed(wfc, x, y)

View File

@@ -0,0 +1,253 @@
from typing import Dict, Set
from ..roomEditor import RoomEditor
animated_tiles = {0x0E, 0x1B, 0x1E, 0x1F, 0x44, 0x91, 0xCF, 0xD0, 0xD1, 0xD2, 0xD9, 0xDC, 0xE9, 0xEB, 0xEC, 0xED, 0xEE, 0xEF}
entrance_tiles = {0xE1, 0xE2, 0xE3, 0xBA, 0xC6}
solid_tiles = set()
open_tiles = set()
walkable_tiles = set()
vertical_edge_tiles = set()
horizontal_edge_tiles = set()
class TileInfo:
def __init__(self, key):
self.key = key
self.up = set()
self.right = set()
self.down = set()
self.left = set()
self.up_freq = {}
self.right_freq = {}
self.down_freq = {}
self.left_freq = {}
self.frequency = 0
def copy(self):
result = TileInfo(self.key)
result.up = self.up.copy()
result.right = self.right.copy()
result.down = self.down.copy()
result.left = self.left.copy()
result.up_freq = self.up_freq.copy()
result.right_freq = self.right_freq.copy()
result.down_freq = self.down_freq.copy()
result.left_freq = self.left_freq.copy()
result.frequency = self.frequency
return result
def remove(self, tile_id):
if tile_id in self.up:
self.up.remove(tile_id)
del self.up_freq[tile_id]
if tile_id in self.down:
self.down.remove(tile_id)
del self.down_freq[tile_id]
if tile_id in self.left:
self.left.remove(tile_id)
del self.left_freq[tile_id]
if tile_id in self.right:
self.right.remove(tile_id)
del self.right_freq[tile_id]
def update(self, other: "TileInfo", tile_filter: Set[int]):
self.frequency += other.frequency
self.up.update(other.up.intersection(tile_filter))
self.down.update(other.down.intersection(tile_filter))
self.left.update(other.left.intersection(tile_filter))
self.right.update(other.right.intersection(tile_filter))
for k, v in other.up_freq.items():
if k not in tile_filter:
continue
self.up_freq[k] = self.up_freq.get(k, 0) + v
for k, v in other.down_freq.items():
if k not in tile_filter:
continue
self.down_freq[k] = self.down_freq.get(k, 0) + v
for k, v in other.left_freq.items():
if k not in tile_filter:
continue
self.left_freq[k] = self.left_freq.get(k, 0) + v
for k, v in other.down_freq.items():
if k not in tile_filter:
continue
self.right_freq[k] = self.right_freq.get(k, 0) + v
def __repr__(self):
return f"<{self.key}>\n U{[f'{n:02x}' for n in self.up]}\n R{[f'{n:02x}' for n in self.right]}\n D{[f'{n:02x}' for n in self.down]}\n L{[f'{n:02x}' for n in self.left]}>"
class TileSet:
def __init__(self, *, main_id=None, animation_id=None):
self.main_id = main_id
self.animation_id = animation_id
self.palette_id = None
self.attr_bank = None
self.attr_addr = None
self.tiles: Dict[int, "TileInfo"] = {}
self.all: Set[int] = set()
def copy(self) -> "TileSet":
result = TileSet(main_id=self.main_id, animation_id=self.animation_id)
for k, v in self.tiles.items():
result.tiles[k] = v.copy()
result.all = self.all.copy()
return result
def remove(self, tile_id):
self.all.remove(tile_id)
del self.tiles[tile_id]
for k, v in self.tiles.items():
v.remove(tile_id)
# Look at the "other" tileset and merge information about tiles known in this tileset
def learn_from(self, other: "TileSet"):
for key, other_info in other.tiles.items():
if key not in self.all:
continue
self.tiles[key].update(other_info, self.all)
def combine(self, other: "TileSet"):
if other.main_id and not self.main_id:
self.main_id = other.main_id
if other.animation_id and not self.animation_id:
self.animation_id = other.animation_id
for key, other_info in other.tiles.items():
if key not in self.all:
self.tiles[key] = other_info.copy()
else:
self.tiles[key].update(other_info, self.all)
self.all.update(other.all)
def loadTileInfo(rom) -> Dict[str, TileSet]:
for n in range(0x100):
physics_flag = rom.banks[8][0x0AD4 + n]
if n == 0xEF:
physics_flag = 0x01 # One of the sky tiles is marked as a pit instead of solid, which messes with the generation of sky
if physics_flag in {0x00, 0x05, 0x06, 0x07}:
open_tiles.add(n)
walkable_tiles.add(n)
vertical_edge_tiles.add(n)
horizontal_edge_tiles.add(n)
elif physics_flag in {0x01, 0x04, 0x60}:
solid_tiles.add(n)
vertical_edge_tiles.add(n)
horizontal_edge_tiles.add(n)
elif physics_flag in {0x08}: # Bridge
open_tiles.add(n)
walkable_tiles.add(n)
elif physics_flag in {0x02}: # Stairs
open_tiles.add(n)
walkable_tiles.add(n)
horizontal_edge_tiles.add(n)
elif physics_flag in {0x03}: # Entrances
open_tiles.add(n)
elif physics_flag in {0x30}: # bushes/rocks
open_tiles.add(n)
elif physics_flag in {0x50}: # pits
open_tiles.add(n)
world_tiles = {}
for ry in range(0, 16):
for rx in range(0, 16):
tileset_id = rom.banks[0x3F][0x3F00 + rx + (ry << 4)]
re = RoomEditor(rom, rx | (ry << 4))
tiles = re.getTileArray()
for y in range(8):
for x in range(10):
tile_id = tiles[x+y*10]
world_tiles[(rx*10+x, ry*8+y)] = (tile_id, tileset_id, re.animation_id | 0x100)
# Fix up wrong tiles
world_tiles[(150, 24)] = (0x2A, world_tiles[(150, 24)][1], world_tiles[(150, 24)][2]) # Left of the raft house, a tree has the wrong tile.
rom_tilesets: Dict[int, TileSet] = {}
for (x, y), (key, tileset_id, animation_id) in world_tiles.items():
if key in animated_tiles:
if animation_id not in rom_tilesets:
rom_tilesets[animation_id] = TileSet(animation_id=animation_id&0xFF)
tileset = rom_tilesets[animation_id]
else:
if tileset_id not in rom_tilesets:
rom_tilesets[tileset_id] = TileSet(main_id=tileset_id)
tileset = rom_tilesets[tileset_id]
tileset.all.add(key)
if key not in tileset.tiles:
tileset.tiles[key] = TileInfo(key)
ti = tileset.tiles[key]
ti.frequency += 1
if (x, y - 1) in world_tiles:
tile_id = world_tiles[(x, y - 1)][0]
ti.up.add(tile_id)
ti.up_freq[tile_id] = ti.up_freq.get(tile_id, 0) + 1
if (x + 1, y) in world_tiles:
tile_id = world_tiles[(x + 1, y)][0]
ti.right.add(tile_id)
ti.right_freq[tile_id] = ti.right_freq.get(tile_id, 0) + 1
if (x, y + 1) in world_tiles:
tile_id = world_tiles[(x, y + 1)][0]
ti.down.add(tile_id)
ti.down_freq[tile_id] = ti.down_freq.get(tile_id, 0) + 1
if (x - 1, y) in world_tiles:
tile_id = world_tiles[(x - 1, y)][0]
ti.left.add(tile_id)
ti.left_freq[tile_id] = ti.left_freq.get(tile_id, 0) + 1
tilesets = {
"basic": rom_tilesets[0x0F].copy()
}
for key, tileset in rom_tilesets.items():
tilesets["basic"].learn_from(tileset)
tilesets["mountains"] = rom_tilesets[0x3E].copy()
tilesets["mountains"].combine(rom_tilesets[0x10B])
tilesets["mountains"].remove(0xB6) # Remove the raft house roof
tilesets["mountains"].remove(0xB7) # Remove the raft house roof
tilesets["mountains"].remove(0x66) # Remove the raft house roof
tilesets["mountains"].learn_from(rom_tilesets[0x1C])
tilesets["mountains"].learn_from(rom_tilesets[0x3C])
tilesets["mountains"].learn_from(rom_tilesets[0x30])
tilesets["mountains"].palette_id = 0x15
tilesets["mountains"].attr_bank = 0x27
tilesets["mountains"].attr_addr = 0x5A20
tilesets["egg"] = rom_tilesets[0x3C].copy()
tilesets["egg"].combine(tilesets["mountains"])
tilesets["egg"].palette_id = 0x13
tilesets["egg"].attr_bank = 0x27
tilesets["egg"].attr_addr = 0x5620
tilesets["forest"] = rom_tilesets[0x20].copy()
tilesets["forest"].palette_id = 0x00
tilesets["forest"].attr_bank = 0x25
tilesets["forest"].attr_addr = 0x4000
tilesets["town"] = rom_tilesets[0x26].copy()
tilesets["town"].combine(rom_tilesets[0x103])
tilesets["town"].palette_id = 0x03
tilesets["town"].attr_bank = 0x25
tilesets["town"].attr_addr = 0x4C00
tilesets["swamp"] = rom_tilesets[0x36].copy()
tilesets["swamp"].combine(rom_tilesets[0x103])
tilesets["swamp"].palette_id = 0x0E
tilesets["swamp"].attr_bank = 0x22
tilesets["swamp"].attr_addr = 0x7400
tilesets["beach"] = rom_tilesets[0x22].copy()
tilesets["beach"].combine(rom_tilesets[0x102])
tilesets["beach"].palette_id = 0x01
tilesets["beach"].attr_bank = 0x22
tilesets["beach"].attr_addr = 0x5000
tilesets["water"] = rom_tilesets[0x3E].copy()
tilesets["water"].combine(rom_tilesets[0x103])
tilesets["water"].learn_from(tilesets["basic"])
tilesets["water"].remove(0x7A)
tilesets["water"].remove(0xC8)
tilesets["water"].palette_id = 0x09
tilesets["water"].attr_bank = 0x22
tilesets["water"].attr_addr = 0x6400
return tilesets

View File

@@ -0,0 +1,5 @@
def xyrange(w, h):
for y in range(h):
for x in range(w):
yield x, y

View File

@@ -0,0 +1,250 @@
from .tileset import TileSet, solid_tiles, open_tiles, vertical_edge_tiles, horizontal_edge_tiles
from .map import Map
from typing import Set
import random
class ContradictionException(Exception):
def __init__(self, x, y):
self.x = x
self.y = y
class Cell:
def __init__(self, x, y, tileset: TileSet, options: Set[int]):
self.x = x
self.y = y
self.tileset = tileset
self.init_options = options
self.options = None
self.result = None
def __set_new_options(self, new_options):
if new_options != self.options:
if self.result is not None:
raise ContradictionException(self.x, self.y)
if not new_options:
raise ContradictionException(self.x, self.y)
self.options = new_options
return True
return False
def update_options_up(self, cell: "Cell") -> bool:
new_options = set()
for tile in cell.options:
new_options.update(cell.tileset.tiles[tile].up)
new_options.intersection_update(self.options)
if (self.y % 8) == 7:
if cell.options.issubset(solid_tiles):
new_options.intersection_update(solid_tiles)
if cell.options.issubset(open_tiles):
new_options.intersection_update(open_tiles)
return self.__set_new_options(new_options)
def update_options_right(self, cell: "Cell") -> bool:
new_options = set()
for tile in cell.options:
new_options.update(cell.tileset.tiles[tile].right)
new_options.intersection_update(self.options)
if (self.x % 10) == 0:
if cell.options.issubset(solid_tiles):
new_options.intersection_update(solid_tiles)
if cell.options.issubset(open_tiles):
new_options.intersection_update(open_tiles)
return self.__set_new_options(new_options)
def update_options_down(self, cell: "Cell") -> bool:
new_options = set()
for tile in cell.options:
new_options.update(cell.tileset.tiles[tile].down)
new_options.intersection_update(self.options)
if (self.y % 8) == 0:
if cell.options.issubset(solid_tiles):
new_options.intersection_update(solid_tiles)
if cell.options.issubset(open_tiles):
new_options.intersection_update(open_tiles)
return self.__set_new_options(new_options)
def update_options_left(self, cell: "Cell") -> bool:
new_options = set()
for tile in cell.options:
new_options.update(cell.tileset.tiles[tile].left)
new_options.intersection_update(self.options)
if (self.x % 10) == 9:
if cell.options.issubset(solid_tiles):
new_options.intersection_update(solid_tiles)
if cell.options.issubset(open_tiles):
new_options.intersection_update(open_tiles)
return self.__set_new_options(new_options)
def __repr__(self):
return f"Cell<{self.options}>"
class WFCMap:
def __init__(self, the_map: Map, tilesets, *, step_callback=None):
self.cell_data = {}
self.on_step = step_callback
self.w = the_map.w * 10
self.h = the_map.h * 8
for y in range(self.h):
for x in range(self.w):
tileset = tilesets[the_map.get(x//10, y//8).tileset_id]
new_cell = Cell(x, y, tileset, tileset.all.copy())
self.cell_data[(new_cell.x, new_cell.y)] = new_cell
for y in range(self.h):
self.cell_data[(0, y)].init_options.intersection_update(solid_tiles)
self.cell_data[(self.w-1, y)].init_options.intersection_update(solid_tiles)
for x in range(self.w):
self.cell_data[(x, 0)].init_options.intersection_update(solid_tiles)
self.cell_data[(x, self.h-1)].init_options.intersection_update(solid_tiles)
for x in range(0, self.w, 10):
for y in range(self.h):
self.cell_data[(x, y)].init_options.intersection_update(vertical_edge_tiles)
for x in range(9, self.w, 10):
for y in range(self.h):
self.cell_data[(x, y)].init_options.intersection_update(vertical_edge_tiles)
for y in range(0, self.h, 8):
for x in range(self.w):
self.cell_data[(x, y)].init_options.intersection_update(horizontal_edge_tiles)
for y in range(7, self.h, 8):
for x in range(self.w):
self.cell_data[(x, y)].init_options.intersection_update(horizontal_edge_tiles)
for sy in range(the_map.h):
for sx in range(the_map.w):
the_map.get(sx, sy).room_type.seed(self, sx*10, sy*8)
for sy in range(the_map.h):
for sx in range(the_map.w):
room = the_map.get(sx, sy)
room.edge_left.seed(self, sx * 10, sy * 8)
room.edge_right.seed(self, sx * 10 + 9, sy * 8)
room.edge_up.seed(self, sx * 10, sy * 8)
room.edge_down.seed(self, sx * 10, sy * 8 + 7)
def initialize(self):
for y in range(self.h):
for x in range(self.w):
cell = self.cell_data[x, y]
cell.options = cell.init_options.copy()
if self.on_step:
self.on_step(self)
propegation_set = set()
for y in range(self.h):
for x in range(self.w):
propegation_set.add((x, y))
self.propegate(propegation_set)
for y in range(self.h):
for x in range(self.w):
cell = self.cell_data[x, y]
cell.init_options = cell.options.copy()
def clear(self):
for y in range(self.h):
for x in range(self.w):
cell = self.cell_data[(x, y)]
if cell.result is None:
cell.options = cell.init_options.copy()
propegation_set = set()
for y in range(self.h):
for x in range(self.w):
cell = self.cell_data[(x, y)]
if cell.result is not None:
propegation_set.add((x, y))
self.propegate(propegation_set)
def random_pick(self, cell):
pick_list = list(cell.options)
if not pick_list:
raise ContradictionException(cell.x, cell.y)
freqs = {}
if (cell.x - 1, cell.y) in self.cell_data and len(self.cell_data[(cell.x - 1, cell.y)].options) == 1:
tile_id = next(iter(self.cell_data[(cell.x - 1, cell.y)].options))
for k, v in self.cell_data[(cell.x - 1, cell.y)].tileset.tiles[tile_id].right_freq.items():
freqs[k] = freqs.get(k, 0) + v
if (cell.x + 1, cell.y) in self.cell_data and len(self.cell_data[(cell.x + 1, cell.y)].options) == 1:
tile_id = next(iter(self.cell_data[(cell.x + 1, cell.y)].options))
for k, v in self.cell_data[(cell.x + 1, cell.y)].tileset.tiles[tile_id].left_freq.items():
freqs[k] = freqs.get(k, 0) + v
if (cell.x, cell.y - 1) in self.cell_data and len(self.cell_data[(cell.x, cell.y - 1)].options) == 1:
tile_id = next(iter(self.cell_data[(cell.x, cell.y - 1)].options))
for k, v in self.cell_data[(cell.x, cell.y - 1)].tileset.tiles[tile_id].down_freq.items():
freqs[k] = freqs.get(k, 0) + v
if (cell.x, cell.y + 1) in self.cell_data and len(self.cell_data[(cell.x, cell.y + 1)].options) == 1:
tile_id = next(iter(self.cell_data[(cell.x, cell.y + 1)].options))
for k, v in self.cell_data[(cell.x, cell.y + 1)].tileset.tiles[tile_id].up_freq.items():
freqs[k] = freqs.get(k, 0) + v
if freqs:
weights_list = [freqs.get(n, 1) for n in pick_list]
else:
weights_list = [cell.tileset.tiles[n].frequency for n in pick_list]
return random.choices(pick_list, weights_list)[0]
def build(self, start_x, start_y, w, h):
cell_todo_list = []
for y in range(start_y, start_y + h):
for x in range(start_x, start_x+w):
cell_todo_list.append(self.cell_data[(x, y)])
while cell_todo_list:
cell_todo_list.sort(key=lambda c: len(c.options))
l0 = len(cell_todo_list[0].options)
idx = 1
while idx < len(cell_todo_list) and len(cell_todo_list[idx].options) == l0:
idx += 1
idx = random.randint(0, idx - 1)
cell = cell_todo_list[idx]
if self.on_step:
self.on_step(self, cur=(cell.x, cell.y))
pick = self.random_pick(cell)
cell_todo_list.pop(idx)
cell.options = {pick}
self.propegate({(cell.x, cell.y)})
for y in range(start_y, start_y + h):
for x in range(start_x, start_x + w):
self.cell_data[(x, y)].result = next(iter(self.cell_data[(x, y)].options))
def propegate(self, propegation_set):
while propegation_set:
xy = next(iter(propegation_set))
propegation_set.remove(xy)
cell = self.cell_data[xy]
if not cell.options:
raise ContradictionException(cell.x, cell.y)
x, y = xy
if (x, y + 1) in self.cell_data and self.cell_data[(x, y + 1)].update_options_down(cell):
propegation_set.add((x, y + 1))
if (x + 1, y) in self.cell_data and self.cell_data[(x + 1, y)].update_options_right(cell):
propegation_set.add((x + 1, y))
if (x, y - 1) in self.cell_data and self.cell_data[(x, y - 1)].update_options_up(cell):
propegation_set.add((x, y - 1))
if (x - 1, y) in self.cell_data and self.cell_data[(x - 1, y)].update_options_left(cell):
propegation_set.add((x - 1, y))
def store_tile_data(self, the_map: Map):
for sy in range(the_map.h):
for sx in range(the_map.w):
tiles = []
for y in range(8):
for x in range(10):
cell = self.cell_data[(x+sx*10, y+sy*8)]
if cell.result is not None:
tiles.append(cell.result)
elif len(cell.options) == 0:
tiles.append(1)
else:
tiles.append(2)
the_map.get(sx, sy).tiles = tiles
def dump_option_count(self):
for y in range(self.h):
for x in range(self.w):
print(f"{len(self.cell_data[(x, y)].options):2x}", end="")
print()
print()