mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
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:
147
worlds/ladx/LADXR/mapgen/__init__.py
Normal file
147
worlds/ladx/LADXR/mapgen/__init__.py
Normal 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
|
||||
59
worlds/ladx/LADXR/mapgen/enemygen.py
Normal file
59
worlds/ladx/LADXR/mapgen/enemygen.py
Normal 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))
|
||||
95
worlds/ladx/LADXR/mapgen/imagegenerator.py
Normal file
95
worlds/ladx/LADXR/mapgen/imagegenerator.py
Normal 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
|
||||
203
worlds/ladx/LADXR/mapgen/locationgen.py
Normal file
203
worlds/ladx/LADXR/mapgen/locationgen.py
Normal 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
|
||||
24
worlds/ladx/LADXR/mapgen/locations/base.py
Normal file
24
worlds/ladx/LADXR/mapgen/locations/base.py
Normal 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__)
|
||||
73
worlds/ladx/LADXR/mapgen/locations/chest.py
Normal file
73
worlds/ladx/LADXR/mapgen/locations/chest.py
Normal 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)
|
||||
107
worlds/ladx/LADXR/mapgen/locations/entrance.py
Normal file
107
worlds/ladx/LADXR/mapgen/locations/entrance.py
Normal 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 {}
|
||||
341
worlds/ladx/LADXR/mapgen/locations/entrance_info.py
Normal file
341
worlds/ladx/LADXR/mapgen/locations/entrance_info.py
Normal 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
|
||||
}
|
||||
172
worlds/ladx/LADXR/mapgen/locations/seashell.py
Normal file
172
worlds/ladx/LADXR/mapgen/locations/seashell.py
Normal 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)
|
||||
146
worlds/ladx/LADXR/mapgen/logic.py
Normal file
146
worlds/ladx/LADXR/mapgen/logic.py
Normal 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
|
||||
231
worlds/ladx/LADXR/mapgen/map.py
Normal file
231
worlds/ladx/LADXR/mapgen/map.py
Normal 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
|
||||
78
worlds/ladx/LADXR/mapgen/roomgen.py
Normal file
78
worlds/ladx/LADXR/mapgen/roomgen.py
Normal 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))
|
||||
54
worlds/ladx/LADXR/mapgen/roomtype/base.py
Normal file
54
worlds/ladx/LADXR/mapgen/roomtype/base.py
Normal 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)
|
||||
28
worlds/ladx/LADXR/mapgen/roomtype/forest.py
Normal file
28
worlds/ladx/LADXR/mapgen/roomtype/forest.py
Normal 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)
|
||||
38
worlds/ladx/LADXR/mapgen/roomtype/mountain.py
Normal file
38
worlds/ladx/LADXR/mapgen/roomtype/mountain.py
Normal 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})
|
||||
16
worlds/ladx/LADXR/mapgen/roomtype/town.py
Normal file
16
worlds/ladx/LADXR/mapgen/roomtype/town.py
Normal 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)
|
||||
30
worlds/ladx/LADXR/mapgen/roomtype/water.py
Normal file
30
worlds/ladx/LADXR/mapgen/roomtype/water.py
Normal 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)
|
||||
253
worlds/ladx/LADXR/mapgen/tileset.py
Normal file
253
worlds/ladx/LADXR/mapgen/tileset.py
Normal 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
|
||||
5
worlds/ladx/LADXR/mapgen/util.py
Normal file
5
worlds/ladx/LADXR/mapgen/util.py
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
def xyrange(w, h):
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
yield x, y
|
||||
250
worlds/ladx/LADXR/mapgen/wfc.py
Normal file
250
worlds/ladx/LADXR/mapgen/wfc.py
Normal 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()
|
||||
Reference in New Issue
Block a user