from ..roomEditor import RoomEditor, Object, ObjectWarp, ObjectHorizontal from ..assembler import ASM from ..locations import constants from typing import List # Room containing the boss BOSS_ROOMS = [ 0x106, 0x12b, 0x15a, 0x166, 0x185, 0x1bc, 0x223, # Note: unused room normally 0x234, 0x300, ] BOSS_ENTITIES = [ (3, 2, 0x59), (4, 2, 0x5C), (4, 3, 0x5B), None, (4, 3, 0x5D), (4, 3, 0x5A), None, (4, 3, 0x62), (5, 2, 0xF9), ] MINIBOSS_ENTITIES = { "ROLLING_BONES": [(8, 3, 0x81), (6, 3, 0x82)], "HINOX": [(5, 2, 0x89)], "DODONGO": [(3, 2, 0x60), (5, 2, 0x60)], "CUE_BALL": [(1, 1, 0x8e)], "GHOMA": [(2, 1, 0x5e), (2, 4, 0x5e)], "SMASHER": [(5, 2, 0x92)], "GRIM_CREEPER": [(4, 0, 0xbc)], "BLAINO": [(5, 3, 0xbe)], "AVALAUNCH": [(5, 1, 0xf4)], "GIANT_BUZZ_BLOB": [(4, 2, 0xf8)], "MOBLIN_KING": [(5, 5, 0xe4)], "ARMOS_KNIGHT": [(4, 3, 0x88)], } MINIBOSS_ROOMS = { 0: 0x111, 1: 0x128, 2: 0x145, 3: 0x164, 4: 0x193, 5: 0x1C5, 6: 0x228, 7: 0x23F, "c1": 0x30C, "c2": 0x303, "moblin_cave": 0x2E1, "armos_temple": 0x27F, } def fixArmosKnightAsMiniboss(rom): # Make the armos temple room with armos knight drop a ceiling key on kill. # This makes the door always open, but that's fine. rom.patch(0x14, 0x017F, "21", "81") # Do not change the drop from Armos knight into a ceiling key. rom.patch(0x06, 0x12E8, ASM("ld [hl], $30"), "", fill_nop=True) def getBossRoomStatusFlagLocation(dungeon_nr): if BOSS_ROOMS[dungeon_nr] >= 0x300: return 0xDDE0 - 0x300 + BOSS_ROOMS[dungeon_nr] return 0xD800 + BOSS_ROOMS[dungeon_nr] def fixDungeonItem(item_chest_id, dungeon_nr): if item_chest_id == constants.CHEST_ITEMS[constants.MAP]: return constants.CHEST_ITEMS["MAP%d" % (dungeon_nr + 1)] if item_chest_id == constants.CHEST_ITEMS[constants.COMPASS]: return constants.CHEST_ITEMS["COMPASS%d" % (dungeon_nr + 1)] if item_chest_id == constants.CHEST_ITEMS[constants.KEY]: return constants.CHEST_ITEMS["KEY%d" % (dungeon_nr + 1)] if item_chest_id == constants.CHEST_ITEMS[constants.NIGHTMARE_KEY]: return constants.CHEST_ITEMS["NIGHTMARE_KEY%d" % (dungeon_nr + 1)] if item_chest_id == constants.CHEST_ITEMS[constants.STONE_BEAK]: return constants.CHEST_ITEMS["STONE_BEAK%d" % (dungeon_nr + 1)] return item_chest_id def getCleanBossRoom(rom, dungeon_nr): re = RoomEditor(rom, BOSS_ROOMS[dungeon_nr]) new_objects = [] for obj in re.objects: if isinstance(obj, ObjectWarp): continue if obj.type_id == 0xBE: # Remove staircases continue if obj.type_id == 0x06: # Remove lava continue if obj.type_id == 0x1c: # Change D1 pits into normal pits obj.type_id = 0x01 if obj.type_id == 0x1e: # Change D1 pits into normal pits obj.type_id = 0xaf if obj.type_id == 0x1f: # Change D1 pits into normal pits obj.type_id = 0xb0 if obj.type_id == 0xF5: # Change open doors into closing doors. obj.type_id = 0xF1 new_objects.append(obj) # Make D4 room a valid fighting room by removing most content. if dungeon_nr == 3: new_objects = new_objects[:2] + [Object(1, 1, 0xAC), Object(8, 1, 0xAC), Object(1, 6, 0xAC), Object(8, 6, 0xAC)] # D7 has an empty room we use for most bosses, but it needs some adjustments. if dungeon_nr == 6: # Move around the unused and instrument room. rom.banks[0x14][0x03a0 + 6 + 1 * 8] = 0x00 rom.banks[0x14][0x03a0 + 7 + 2 * 8] = 0x2C rom.banks[0x14][0x03a0 + 7 + 3 * 8] = 0x23 rom.banks[0x14][0x03a0 + 6 + 5 * 8] = 0x00 rom.banks[0x14][0x0520 + 7 + 2 * 8] = 0x2C rom.banks[0x14][0x0520 + 7 + 3 * 8] = 0x23 rom.banks[0x14][0x0520 + 6 + 5 * 8] = 0x00 re.floor_object &= 0x0F new_objects += [ Object(4, 0, 0xF0), Object(1, 6, 0xBE), ObjectWarp(1, dungeon_nr, 0x22E, 24, 16) ] # Set the stairs towards the eagle tower top to our new room. r = RoomEditor(rom, 0x22E) r.objects[-1] = ObjectWarp(1, dungeon_nr, re.room, 24, 112) r.store(rom) # Remove the normal door to the instrument room r = RoomEditor(rom, 0x22e) r.removeObject(4, 0) r.store(rom) rom.banks[0x14][0x22e - 0x100] = 0x00 r = RoomEditor(rom, 0x22c) r.changeObject(0, 7, 0x03) r.changeObject(2, 7, 0x03) r.store(rom) re.objects = new_objects re.entities = [] return re def changeBosses(rom, mapping: List[int]): # Fix the color dungeon not properly warping to room 0 with the boss. for addr in range(0x04E0, 0x04E0 + 64): if rom.banks[0x14][addr] == 0x00 and addr not in {0x04E0 + 1 + 3 * 8, 0x04E0 + 2 + 6 * 8}: rom.banks[0x14][addr] = 0xFF # Fix the genie death not really liking pits/water. rom.patch(0x04, 0x0521, ASM("ld [hl], $81"), ASM("ld [hl], $91")) # For the sidescroll bosses, we need to update this check to be the evil eagle dungeon. # But if evil eagle is not there we still need to remove this check to make angler fish work in D7 dungeon_nr = mapping.index(6) if 6 in mapping else 0xFE rom.patch(0x02, 0x1FC8, ASM("cp $06"), ASM("cp $%02x" % (dungeon_nr if dungeon_nr < 8 else 0xff))) for dungeon_nr in range(9): target = mapping[dungeon_nr] if target == dungeon_nr: continue if target == 3: # D4 fish boss # If dungeon_nr == 6: use normal eagle door towards fish. if dungeon_nr == 6: # Add the staircase to the boss, and fix the warp back. re = RoomEditor(rom, 0x22E) for obj in re.objects: if isinstance(obj, ObjectWarp): obj.type_id = 2 obj.map_nr = 3 obj.room = 0x1EF obj.target_x = 24 obj.target_y = 16 re.store(rom) re = RoomEditor(rom, 0x1EF) re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, 0x22E, 24, 16) re.store(rom) else: # Set the proper room event flags rom.banks[0x14][BOSS_ROOMS[dungeon_nr] - 0x100] = 0x2A # Add the staircase to the boss, and fix the warp back. re = getCleanBossRoom(rom, dungeon_nr) re.objects += [Object(4, 4, 0xBE), ObjectWarp(2, 3, 0x1EF, 24, 16)] re.store(rom) re = RoomEditor(rom, 0x1EF) re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, BOSS_ROOMS[dungeon_nr], 72, 80) re.store(rom) # Patch the fish heart container to open up the right room. if dungeon_nr == 6: rom.patch(0x03, 0x1A0F, ASM("ld hl, $D966"), ASM("ld hl, $%04x" % (0xD800 + 0x22E))) else: rom.patch(0x03, 0x1A0F, ASM("ld hl, $D966"), ASM("ld hl, $%04x" % (getBossRoomStatusFlagLocation(dungeon_nr)))) # Patch the proper item towards the D4 boss rom.banks[0x3E][0x3800 + 0x01ff] = fixDungeonItem(rom.banks[0x3E][0x3800 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) rom.banks[0x3E][0x3300 + 0x01ff] = fixDungeonItem(rom.banks[0x3E][0x3300 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) elif target == 6: # Evil eagle rom.banks[0x14][BOSS_ROOMS[dungeon_nr] - 0x100] = 0x2A # Patch the eagle heart container to open up the right room. rom.patch(0x03, 0x1A04, ASM("ld hl, $DA2E"), ASM("ld hl, $%04x" % (getBossRoomStatusFlagLocation(dungeon_nr)))) # Add the staircase to the boss, and fix the warp back. re = getCleanBossRoom(rom, dungeon_nr) re.objects += [Object(4, 4, 0xBE), ObjectWarp(2, 6, 0x2F8, 72, 80)] re.store(rom) re = RoomEditor(rom, 0x2F8) re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, BOSS_ROOMS[dungeon_nr], 72, 80) re.store(rom) # Patch the proper item towards the D7 boss rom.banks[0x3E][0x3800 + 0x02E8] = fixDungeonItem(rom.banks[0x3E][0x3800 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) rom.banks[0x3E][0x3300 + 0x02E8] = fixDungeonItem(rom.banks[0x3E][0x3300 + BOSS_ROOMS[dungeon_nr]], dungeon_nr) else: rom.banks[0x14][BOSS_ROOMS[dungeon_nr] - 0x100] = 0x21 re = getCleanBossRoom(rom, dungeon_nr) re.entities = [BOSS_ENTITIES[target]] if target == 4: # For slime eel, we need to setup the right wall tiles. rom.banks[0x20][0x2EB3 + BOSS_ROOMS[dungeon_nr] - 0x100] = 0x06 if target == 5: # Patch facade so he doesn't use the spinning tiles, which is a problem for the sprites. rom.patch(0x04, 0x121D, ASM("cp $14"), ASM("cp $00")) rom.patch(0x04, 0x1226, ASM("cp $04"), ASM("cp $00")) rom.patch(0x04, 0x127F, ASM("cp $14"), ASM("cp $00")) if target == 7: pass # For hot head, add some lava (causes graphical glitches) # re.animation_id = 0x06 # re.objects += [ # ObjectHorizontal(3, 2, 0x06, 4), # ObjectHorizontal(2, 3, 0x06, 6), # ObjectHorizontal(2, 4, 0x06, 6), # ObjectHorizontal(3, 5, 0x06, 4), # ] re.store(rom) def readBossMapping(rom): mapping = [] for dungeon_nr in range(9): r = RoomEditor(rom, BOSS_ROOMS[dungeon_nr]) if r.entities: mapping.append(BOSS_ENTITIES.index(r.entities[0])) elif isinstance(r.objects[-1], ObjectWarp) and r.objects[-1].room == 0x1ef: mapping.append(3) elif isinstance(r.objects[-1], ObjectWarp) and r.objects[-1].room == 0x2f8: mapping.append(6) else: mapping.append(dungeon_nr) return mapping def changeMiniBosses(rom, mapping): # Fix avalaunch not working when entering a room from the left or right rom.patch(0x03, 0x0BE0, ASM(""" ld [hl], $50 ld hl, $C2D0 add hl, bc ld [hl], $00 jp $4B56 """), ASM(""" ld a, [hl] sub $08 ld [hl], a ld hl, $C2D0 add hl, bc ld [hl], b ; b is always zero here ret """), fill_nop=True) # Fix avalaunch waiting until the room event is done (and not all rooms have a room event on enter) rom.patch(0x36, 0x1C14, ASM("ret z"), "", fill_nop=True) # Fix giant buzz blob waiting until the room event is done (and not all rooms have a room event on enter) rom.patch(0x36, 0x153B, ASM("ret z"), "", fill_nop=True) # Remove the powder fairy from giant buzz blob rom.patch(0x36, 0x14F7, ASM("jr nz, $05"), ASM("jr $05")) # Do not allow the force barrier in D3 dodongo room rom.patch(0x14, 0x14AC, 0x14B5, ASM("jp $7FE0"), fill_nop=True) rom.patch(0x14, 0x3FE0, "00" * 0x20, ASM(""" ld a, [$C124] ; room transition ld hl, $C17B or [hl] ret nz ldh a, [$F6] ; room cp $45 ; check for D3 dodogo room ret z cp $7F ; check for armos temple room ret z jp $54B5 """), fill_nop=True) # Patch smasher to spawn the ball closer, so it doesn't spawn on the wall in the armos temple rom.patch(0x06, 0x0533, ASM("add a, $30"), ASM("add a, $20")) for target, name in mapping.items(): re = RoomEditor(rom, MINIBOSS_ROOMS[target]) re.entities = [e for e in re.entities if e[2] == 0x61] # Only keep warp, if available re.entities += MINIBOSS_ENTITIES[name] if re.room == 0x228 and name != "GRIM_CREEPER": for x in range(3, 7): for y in range(0, 3): re.removeObject(x, y) if name == "CUE_BALL": re.objects += [ Object(3, 3, 0x2c), ObjectHorizontal(4, 3, 0x22, 2), Object(6, 3, 0x2b), Object(3, 4, 0x2a), ObjectHorizontal(4, 4, 0x21, 2), Object(6, 4, 0x29), ] if name == "BLAINO": # BLAINO needs a warp object to hit you to the entrance of the dungeon. if len(re.getWarps()) < 1: # Default to start house. target = (0x10, 0x2A3, 0x50, 0x7c) if 0x100 <= re.room < 0x11D: #D1 target = (0, 0x117, 80, 80) elif 0x11D <= re.room < 0x140: #D2 target = (1, 0x136, 80, 80) elif 0x140 <= re.room < 0x15D: #D3 target = (2, 0x152, 80, 80) elif 0x15D <= re.room < 0x180: #D4 target = (3, 0x174, 80, 80) elif 0x180 <= re.room < 0x1AC: #D5 target = (4, 0x1A1, 80, 80) elif 0x1B0 <= re.room < 0x1DE: #D6 target = (5, 0x1D4, 80, 80) elif 0x200 <= re.room < 0x22D: #D7 target = (6, 0x20E, 80, 80) elif 0x22D <= re.room < 0x26C: #D8 target = (7, 0x25D, 80, 80) elif re.room >= 0x300: #D0 target = (0xFF, 0x312, 80, 80) elif re.room == 0x2E1: #Moblin cave target = (0x15, 0x2F0, 0x50, 0x7C) elif re.room == 0x27F: #Armos temple target = (0x16, 0x28F, 0x50, 0x7C) re.objects.append(ObjectWarp(1, *target)) if name == "DODONGO": # Remove breaking floor tiles from the room. re.objects = [obj for obj in re.objects if obj.type_id != 0xDF] if name == "ROLLING_BONES" and target == 2: # Make rolling bones pass trough walls so it does not get stuck here. rom.patch(0x03, 0x02F1 + 0x81, "84", "95") re.store(rom) def readMiniBossMapping(rom): mapping = {} for key, room in MINIBOSS_ROOMS.items(): r = RoomEditor(rom, room) for me_key, me_data in MINIBOSS_ENTITIES.items(): if me_data[-1][2] == r.entities[-1][2]: mapping[key] = me_key return mapping def doubleTrouble(rom): for n in range(0x316): if n == 0x2FF: continue re = RoomEditor(rom, n) # Bosses if re.hasEntity(0x59): # Moldorm (TODO; double heart container drop) re.removeEntities(0x59) re.entities += [(3, 2, 0x59), (4, 2, 0x59)] re.store(rom) if re.hasEntity(0x5C): # Ghini re.removeEntities(0x5C) re.entities += [(3, 2, 0x5C), (4, 2, 0x5C)] re.store(rom) if re.hasEntity(0x5B): # slime eye re.removeEntities(0x5B) re.entities += [(3, 2, 0x5B), (6, 2, 0x5B)] re.store(rom) if re.hasEntity(0x65): # angler fish re.removeEntities(0x65) re.entities += [(6, 2, 0x65), (6, 5, 0x65)] re.store(rom) # Slime eel bugs out on death if duplicated. # if re.hasEntity(0x5D): # slime eel # re.removeEntities(0x5D) # re.entities += [(6, 2, 0x5D), (6, 5, 0x5D)] # re.store(rom) if re.hasEntity(0x5A): # facade (TODO: Drops two hearts, shared health?) re.removeEntities(0x5A) re.entities += [(2, 3, 0x5A), (6, 3, 0x5A)] re.store(rom) # Evil eagle causes a crash, and messes up the intro sequence and generally is just a mess if I spawn multiple # if re.hasEntity(0x63): # evil eagle # re.removeEntities(0x63) # re.entities += [(3, 4, 0x63), (2, 4, 0x63)] # re.store(rom) # # Remove that links movement is blocked # rom.patch(0x05, 0x2258, ASM("ldh [$A1], a"), "0000") # rom.patch(0x05, 0x1AE3, ASM("ldh [$A1], a"), "0000") # rom.patch(0x05, 0x1C5D, ASM("ldh [$A1], a"), "0000") # rom.patch(0x05, 0x1C8D, ASM("ldh [$A1], a"), "0000") # rom.patch(0x05, 0x1CAF, ASM("ldh [$A1], a"), "0000") if re.hasEntity(0x62): # hot head (TODO: Drops thwo hearts) re.removeEntities(0x62) re.entities += [(2, 2, 0x62), (4, 4, 0x62)] re.store(rom) if re.hasEntity(0xF9): # hardhit beetle re.removeEntities(0xF9) re.entities += [(2, 2, 0xF9), (5, 4, 0xF9)] re.store(rom) # Minibosses if re.hasEntity(0x89): re.removeEntities(0x89) re.entities += [(2, 3, 0x89), (6, 3, 0x89)] re.store(rom) if re.hasEntity(0x81): re.removeEntities(0x81) re.entities += [(2, 3, 0x81), (6, 3, 0x81)] re.store(rom) if re.hasEntity(0x60): dodongo = [e for e in re.entities if e[2] == 0x60] x = (dodongo[0][0] + dodongo[1][0]) // 2 y = (dodongo[0][1] + dodongo[1][1]) // 2 re.entities += [(x, y, 0x60)] re.store(rom) if re.hasEntity(0x8e): re.removeEntities(0x8e) re.entities += [(1, 1, 0x8e), (7, 1, 0x8e)] re.store(rom) if re.hasEntity(0x92): re.removeEntities(0x92) re.entities += [(2, 3, 0x92), (4, 3, 0x92)] re.store(rom) if re.hasEntity(0xf4): re.removeEntities(0xf4) re.entities += [(2, 1, 0xf4), (6, 1, 0xf4)] re.store(rom) if re.hasEntity(0xf8): re.removeEntities(0xf8) re.entities += [(2, 2, 0xf8), (6, 2, 0xf8)] re.store(rom) if re.hasEntity(0xe4): re.removeEntities(0xe4) re.entities += [(5, 2, 0xe4), (5, 5, 0xe4)] re.store(rom) if re.hasEntity(0x88): # Armos knight (TODO: double item drop) re.removeEntities(0x88) re.entities += [(3, 3, 0x88), (6, 3, 0x88)] re.store(rom) if re.hasEntity(0x87): # Lanmola (TODO: killing one drops the item, and marks as done) re.removeEntities(0x87) re.entities += [(2, 2, 0x87), (1, 1, 0x87)] re.store(rom)