Adds Link's Awakening: DX. Fully imports and forks LADXR, with permission - https://github.com/daid/LADXR
		
			
				
	
	
		
			204 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			204 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
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
 |