Archipelago/worlds/ladx/LADXR/mapgen/locationgen.py

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