Links Awakening: Implement New Game (#1334)

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

View File

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

View File

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

View File

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

View File

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

View File

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