diff --git a/args/bosses.py b/args/bosses.py index 29bffd56..7fe1b107 100644 --- a/args/bosses.py +++ b/args/bosses.py @@ -1,3 +1,8 @@ +from data.bosses import BossLocations + + +DEFAULT_DRAGON_PROTOCOL = BossLocations.SHUFFLE +DEFAULT_STATUE_PROTOCOL = BossLocations.MIX def name(): return "Bosses" @@ -9,8 +14,17 @@ def parse(parser): help = "Boss battles shuffled") bosses_battles.add_argument("-bbr", "--boss-battles-random", action = "store_true", help = "Boss battles randomized") - bosses.add_argument("-bmbd", "--mix-bosses-dragons", action = "store_true", + + dragons = bosses.add_mutually_exclusive_group() + dragons.add_argument("-drloc", "--dragon-boss-location", default = DEFAULT_DRAGON_PROTOCOL, type = str.lower, choices = BossLocations.ALL, + help = "Decides which locations the eight dragon encounters can be fought") + dragons.add_argument("-bmbd", "--mix-bosses-dragons", action = "store_true", help = "Shuffle/randomize bosses and dragons together") + + statues = bosses.add_mutually_exclusive_group() + statues.add_argument("-stloc", "--statue-boss-location", default = DEFAULT_STATUE_PROTOCOL, type = str.lower, choices = BossLocations.ALL, + help = "Decides which locations the three statue encounters can be fought") + bosses.add_argument("-srp3", "--shuffle-random-phunbaba3", action = "store_true", help = "Apply Shuffle/Random to Phunbaba 3 (otherwise he will only appear in Mobliz WOR)") bosses.add_argument("-bnds", "--boss-normalize-distort-stats", action = "store_true", @@ -21,7 +35,15 @@ def parse(parser): help = "Undead status removed from bosses") def process(args): - pass + if args.mix_bosses_dragons: + args.dragon_boss_location = BossLocations.MIX + args.mix_bosses_dragons = None + # if neither shuffling or randomizing bosses, and we try to mix the dragons/statues, simply shuffle them instead + vanilla_locations = not (args.boss_battles_shuffle or args.boss_battles_random) + if vanilla_locations and args.dragon_boss_location == BossLocations.MIX: + args.dragon_boss_location = BossLocations.SHUFFLE + if vanilla_locations and args.statue_boss_location == BossLocations.MIX: + args.statue_boss_location = BossLocations.SHUFFLE def flags(args): flags = "" @@ -31,8 +53,14 @@ def flags(args): elif args.boss_battles_random: flags += " -bbr" - if args.mix_bosses_dragons: - flags += " -bmbd" + if args.dragon_boss_location: + flags += f" -drloc {args.dragon_boss_location}" + elif args.mix_bosses_dragons: + flags += f" -drloc {BossLocations.MIX}" + + if args.statue_boss_location: + flags += f" -stloc {args.statue_boss_location}" + if args.shuffle_random_phunbaba3: flags += " -srp3" if args.boss_normalize_distort_stats: @@ -51,9 +79,18 @@ def options(args): elif args.boss_battles_random: boss_battles = "Random" + dragon_battles = DEFAULT_DRAGON_PROTOCOL + if args.dragon_boss_location: + dragon_battles = args.dragon_boss_location.capitalize() + + statue_battles = DEFAULT_DRAGON_PROTOCOL + if args.statue_boss_location: + statue_battles = args.statue_boss_location.capitalize() + return [ ("Boss Battles", boss_battles), - ("Mix Bosses & Dragons", args.mix_bosses_dragons), + ("Dragons", dragon_battles), + ("Statues", statue_battles), ("Shuffle/Random Phunbaba 3", args.shuffle_random_phunbaba3), ("Normalize & Distort Stats", args.boss_normalize_distort_stats), ("Boss Experience", args.boss_experience), diff --git a/constants/maps.py b/constants/maps.py new file mode 100644 index 00000000..daed0525 --- /dev/null +++ b/constants/maps.py @@ -0,0 +1,12 @@ + + +id_name = { + 0x123 : "Guardian Room", # 291 + 0x14e : "Poltergeist Room", # 334 + 0x162 : "Goddess Room", # 354 + 0x163 : "Doom Room", # 355 + 0x19a : "Inferno Room", # 410 + 0x19b : "KT Final Switch Room" # 411 +} + +name_id = {v: k for k, v in id_name.items()} diff --git a/constants/objectives/condition_bits.py b/constants/objectives/condition_bits.py index bc46f131..d0821e20 100644 --- a/constants/objectives/condition_bits.py +++ b/constants/objectives/condition_bits.py @@ -68,6 +68,14 @@ NameBit("Zozo Tower", event_bit.GOT_ZOZO_REWARD), ] +check_bit += [ # Index + NameBit("Kefka's Tower Ambush", event_bit.DEFEATED_INFERNO), # 59 + NameBit("Kefka's Tower Guardian", event_bit.DEFEATED_GUARDIAN), # 60 + NameBit("KT Left Triad Statue", event_bit.DEFEATED_DOOM), # 61 + NameBit("KT Mid Triad Statue", event_bit.DEFEATED_POLTERGEIST), # 62 + NameBit("KT Right Triad Statue", event_bit.DEFEATED_GODDESS), # 63 +] + quest_bit = [ NameBit("Defeat Sealed Cave Ninja", event_bit.DEFEATED_NINJA_CAVE_TO_SEALED_GATE), NameBit("Help Injured Lad", event_bit.HELPED_INJURED_LAD), @@ -79,6 +87,7 @@ NameBit("Suplex A Train", event_bit.SUPLEXED_TRAIN), NameBit("Win An Auction", event_bit.WON_AN_AUCTION), NameBit("Win A Coliseum Match", event_bit.WON_A_COLISEUM_MATCH), + NameBit("Complete the KT Gauntlet", event_bit.COMPLETED_KT_GAUNTLET) ] boss_bit = [] diff --git a/constants/objectives/results.py b/constants/objectives/results.py index 2606d9b2..d0281e66 100644 --- a/constants/objectives/results.py +++ b/constants/objectives/results.py @@ -85,6 +85,9 @@ ], } +category_types["Kefka's Tower"] += [ResultType(90, "Unlock KT Gauntlet", "Unlock KT Gauntlet", None)] + + categories = list(category_types.keys()) id_type = {} diff --git a/constants/songs.py b/constants/songs.py new file mode 100644 index 00000000..4c147d9f --- /dev/null +++ b/constants/songs.py @@ -0,0 +1,5 @@ +id_name = { + 0x33 : "FierceBattle", # "Battle to the Death" - Atma/Statue battle theme +} + +name_id = {v: k for k, v in id_name.items()} diff --git a/constants/sound_effects.py b/constants/sound_effects.py new file mode 100644 index 00000000..9762a988 --- /dev/null +++ b/constants/sound_effects.py @@ -0,0 +1,11 @@ + + +id_name = { + 0x9a : "Ground Breaking", # 154 + 0xa6 : "Chest/Switch", # 166 + 0xa9 : "Umaro Body Slam", # 169 + 0xb5 : "Landing On Floor", # 181 + 0xba : "Falling", # 186 +} + +name_id = {v: k for k, v in id_name.items()} diff --git a/data/bosses.py b/data/bosses.py index 612fd968..87eafbdd 100644 --- a/data/bosses.py +++ b/data/bosses.py @@ -92,6 +92,25 @@ 396 : "Guardian", # defeatable guardian in kefka's tower 401 : "MagiMaster", } + +# These ids are repeated in normal_pack_name as well +# This is intentional as they are used to iterate over ALL bosses for things like objective conditions +statue_pack_name = { + 354 : "Doom", + 355 : "Goddess", + 356 : "Poltrgeist", +} +statue_formation_name = { + 468 : "Doom", + 469 : "Goddess", + 470 : "Poltrgeist", +} +statue_enemy_name = { + 295 : "Doom", + 296 : "Goddess", + 297 : "Poltrgeist", +} + normal_formation_name = { 79 : "Rizopas", 354 : "MagiMaster", @@ -255,3 +274,10 @@ enemy_name.update(removed_enemy_name) name_enemy = {v: k for k, v in enemy_name.items()} + +class BossLocations: + MIX = "mix" + ORIGINAL = "original" + SHUFFLE = "shuffle" + + ALL = [MIX, ORIGINAL, SHUFFLE] diff --git a/data/dialogs/dialogs.py b/data/dialogs/dialogs.py index 79803299..3a5a13c6 100644 --- a/data/dialogs/dialogs.py +++ b/data/dialogs/dialogs.py @@ -106,10 +106,10 @@ def read_multi_line_battle_dialogs(self): self.multi_line_battle_dialogs.append(dialog) def free(self): - import data.dialogs.free as free + from data.dialogs.free import multi_line_battle_dialogs self.free_multi_line_battle_dialogs = [] - for dialog_id in free.multi_line_battle_dialogs: + for dialog_id in multi_line_battle_dialogs: self.multi_line_battle_dialogs[dialog_id].text = "" self.free_multi_line_battle_dialogs.append(dialog_id) @@ -179,6 +179,12 @@ def objectives_mod(self): mlid = self.allocate_multi_line_battle(line1 + "" + line2 + "") self.multi_line_battle_objectives.append(mlid) + def create_dialog(self, text): + from data.dialogs.free import dialogs + id = dialogs.pop() + self.set_text(id, text) + return id + def mod(self): self.move_battle_messages() self.objectives_mod() diff --git a/data/dialogs/free.py b/data/dialogs/free.py index 758c73bb..cc803d54 100644 --- a/data/dialogs/free.py +++ b/data/dialogs/free.py @@ -1,3 +1,9 @@ + +dialogs = [] +dialogs += range(515, 528) # kefka assaulting thamasa +dialogs += range(550, 598) # sabin scenario, cyan kefka doma event +dialogs +=range(1767, 1878) # BANQUET + multi_line_battle_dialogs = [ 0, # WEDGE:Hey! What's the matter?Do you know something wedon't……? ... 1, # GIRL:…… diff --git a/data/enemy_formations.py b/data/enemy_formations.py index 22765e34..e6d334be 100644 --- a/data/enemy_formations.py +++ b/data/enemy_formations.py @@ -11,8 +11,10 @@ class EnemyFormations(): ENEMIES_END = 0xf83bf ENEMIES_SIZE = 15 - PHUNBABA3 = 422 - DOOM_GAZE = 463 + PHUNBABA3 = bosses.name_formation["Phunbaba 3"] + DOOM_GAZE = bosses.name_formation["Doom Gaze"] + ALL_STATUES = list(bosses.statue_formation_name) + ALL_DRAGONS = list(bosses.dragon_formation_name) PRESENTER = 433 COLISEUM = 575 diff --git a/data/enemy_packs.py b/data/enemy_packs.py index 5aa64871..6166c516 100644 --- a/data/enemy_packs.py +++ b/data/enemy_packs.py @@ -15,8 +15,9 @@ class EnemyPacks(): ZONE_EATER = 32 VELDT = 255 # placeholder for veldt in wob/wor - PHUNBABA3 = 386 - DOOM_GAZE = 349 + + PHUNBABA3 = bosses.name_pack["Phunbaba 3"] + DOOM_GAZE = bosses.name_pack["Doom Gaze"] def __init__(self, rom, args, formations): self.rom = rom @@ -35,18 +36,80 @@ def __init__(self, rom, args, formations): pack = EnemyPack2(pack2_index, self.pack2_data[pack2_index]) self.packs.append(pack) + # Returns the list of all boss packs that can be used during randomization def _replaceable_bosses(self): - replaceable = list(bosses.normal_pack_name) + dragon_packs = list(bosses.dragon_pack_name) + statue_packs = list(bosses.statue_pack_name) + boss_packs = list(bosses.normal_pack_name) + replaceable = [boss for boss in boss_packs if boss not in statue_packs and boss not in dragon_packs] + if not self.args.shuffle_random_phunbaba3: - replaceable.remove(self.PHUNBABA3) self.event_boss_replacements[self.PHUNBABA3] = self.PHUNBABA3 + if self.PHUNBABA3 in replaceable: + replaceable.remove(self.PHUNBABA3) + if not self.args.doom_gaze_no_escape: # if doom gaze can escape, don't shuffle/randomize him # possibly having multiple doom gazes while trying to keep track of hp is awkward # how would that work with him being in his original spot and the others? How to know when to get bahamut esper? - replaceable.remove(self.DOOM_GAZE) self.event_boss_replacements[self.DOOM_GAZE] = self.DOOM_GAZE - return replaceable + + if self.DOOM_GAZE in replaceable: + replaceable.remove(self.DOOM_GAZE) + + return replaceable + self._replaceable_dragons() + self._replaceable_statues() + + # Statue locations that become available for the general boss pool + def _replaceable_statues(self): + import random + statues = list(bosses.statue_pack_name) + random.shuffle(statues) + return statues if self.args.statue_boss_location == bosses.BossLocations.MIX else [] + + # Dragon locations that become available for the general boss pool + def _replaceable_dragons(self): + import random + statues = list(bosses.dragon_pack_name) + random.shuffle(statues) + return statues if self.args.dragon_boss_location == bosses.BossLocations.MIX else [] + + # As MIX is handled in the shuffle/random functions, this is for handling the other options + def _handle_original_shuffle_statues(self): + statues = list(bosses.statue_pack_name) + + if self.args.statue_boss_location == bosses.BossLocations.ORIGINAL: + for statue in statues: + self.event_boss_replacements[statue] = statue + elif self.args.statue_boss_location == bosses.BossLocations.SHUFFLE: + import random + replacements = statues.copy() + random.shuffle(statues) + random.shuffle(replacements) + + for statue in statues: + self.event_boss_replacements[replacements.pop()] = statue + else: + # boss assignment is handled in the shuffle/random functions + pass + + + # As MIX is handled in the shuffle/random functions, this is for handling the other options + def _handle_original_shuffle_dragons(self): + dragons = list(bosses.dragon_pack_name) + if self.args.dragon_boss_location == bosses.BossLocations.ORIGINAL: + for dragon in dragons: + self.event_boss_replacements[dragon] = dragon + elif self.args.dragon_boss_location == bosses.BossLocations.SHUFFLE: + import random + replacements = dragons.copy() + random.shuffle(dragons) + random.shuffle(replacements) + + for dragon in dragons: + self.event_boss_replacements[replacements.pop()] = dragon + else: + # boss assignment is handled in the shuffle/random functions + pass def phunbaba3_safety_check(self, bosses_possible): import random @@ -72,40 +135,20 @@ def phunbaba3_safety_check(self, bosses_possible): def shuffle_event_bosses(self): import random - self.event_boss_replacements = {} - - if self.args.mix_bosses_dragons: - bosses_dragons_to_replace = self._replaceable_bosses() + list(bosses.dragon_pack_name) - bosses_dragons_possible = bosses_dragons_to_replace.copy() - - random.shuffle(bosses_dragons_possible) - for index, boss in enumerate(bosses_dragons_to_replace): - self.event_boss_replacements[boss] = bosses_dragons_possible[index] - - self.phunbaba3_safety_check(bosses_dragons_to_replace) - else: - bosses_to_replace = self._replaceable_bosses() - bosses_possible = bosses_to_replace.copy() - random.shuffle(bosses_possible) - for index, boss in enumerate(bosses_to_replace): - self.event_boss_replacements[boss] = bosses_possible[index] + bosses_to_replace = self._replaceable_bosses() + bosses_possible = bosses_to_replace.copy() - dragons_to_replace = list(bosses.dragon_pack_name) - dragons_possible = dragons_to_replace.copy() + random.shuffle(bosses_possible) + for index, boss in enumerate(bosses_to_replace): + self.event_boss_replacements[boss] = bosses_possible[index] - random.shuffle(dragons_possible) - for index, dragon in enumerate(dragons_to_replace): - self.event_boss_replacements[dragon] = dragons_possible[index] - - self.phunbaba3_safety_check(bosses_to_replace) + self.phunbaba3_safety_check(bosses_to_replace) def randomize_event_bosses(self): import args, random, objectives from constants.objectives.conditions import names as possible_condition_names - self.event_boss_replacements = {} - boss_condition_name = "Boss" dragon_condition_name = "Dragon" dragons_condition_name = "Dragons" @@ -115,13 +158,19 @@ def randomize_event_bosses(self): required_boss_formations = set() required_dragon_formations = set() + required_statue_formations = set() + min_dragon_formations = 0 for objective in objectives: for condition in objective.conditions: if condition.NAME == boss_condition_name: - required_boss_formations.add(bosses.name_formation[condition.boss_name()]) + formation = condition.boss_formation + if formation in list(bosses.statue_formation_name): + required_statue_formations.add(formation) + else: + required_boss_formations.add(formation) elif condition.NAME == dragon_condition_name: - required_dragon_formations.add(bosses.name_formation[condition.dragon_name()]) + required_dragon_formations.add(condition.dragon_formation) elif condition.NAME == dragons_condition_name and condition.count > min_dragon_formations: min_dragon_formations = condition.count @@ -133,11 +182,14 @@ def randomize_event_bosses(self): required_dragon_formations |= set(random_dragon_formations) required_boss_packs = set() + required_statue_packs = set() for pack_id, pack_name in bosses.normal_pack_name.items(): formations = self.get_formations(pack_id) for formation_id in formations: if formation_id in required_boss_formations: required_boss_packs.add(pack_id) + elif formation_id in required_statue_formations: + required_statue_packs.add(pack_id) required_dragon_packs = set() for pack_id, pack_name in bosses.dragon_pack_name.items(): @@ -146,46 +198,28 @@ def randomize_event_bosses(self): if formation_id in required_dragon_formations: required_dragon_packs.add(pack_id) - if self.args.mix_bosses_dragons: - bosses_dragons_to_replace = self._replaceable_bosses() + list(bosses.dragon_pack_name) - random.shuffle(bosses_dragons_to_replace) - - for pack in required_boss_packs: - self.event_boss_replacements[bosses_dragons_to_replace.pop()] = pack - for pack in required_dragon_packs: - self.event_boss_replacements[bosses_dragons_to_replace.pop()] = pack - - # guarantee 8 dragons - dragons_possible = list(bosses.dragon_pack_name) - for index in range(len(required_dragon_packs), len(bosses.dragon_pack_name)): - self.event_boss_replacements[bosses_dragons_to_replace.pop()] = random.choice(dragons_possible) + # randomizing and shuffling + bosses_to_replace = self._replaceable_bosses() + random.shuffle(bosses_to_replace) + for pack in required_boss_packs: + self.event_boss_replacements[bosses_to_replace.pop()] = pack - bosses_possible = self._replaceable_bosses() - for boss in bosses_dragons_to_replace: - self.event_boss_replacements[boss] = random.choice(bosses_possible) - - self.phunbaba3_safety_check(bosses_possible + dragons_possible) - else: - bosses_to_replace = self._replaceable_bosses() - random.shuffle(bosses_to_replace) - for pack in required_boss_packs: + # If statue locations are not mixed, they will always + if self.args.statue_boss_location == bosses.BossLocations.MIX: + for pack in required_statue_packs: self.event_boss_replacements[bosses_to_replace.pop()] = pack - bosses_possible = self._replaceable_bosses() - for boss in bosses_to_replace: - self.event_boss_replacements[boss] = random.choice(bosses_possible) - - dragons_to_replace = list(bosses.dragon_pack_name) - random.shuffle(dragons_to_replace) + if self.args.dragon_boss_location == bosses.BossLocations.MIX: for pack in required_dragon_packs: - self.event_boss_replacements[dragons_to_replace.pop()] = pack + self.event_boss_replacements[bosses_to_replace.pop()] = pack - dragons_possible = list(bosses.dragon_pack_name) - for dragon in dragons_to_replace: - self.event_boss_replacements[dragon] = random.choice(dragons_possible) + random.shuffle(bosses_to_replace) + bosses_possible = self._replaceable_bosses() + for boss in bosses_to_replace: + self.event_boss_replacements[boss] = random.choice(bosses_possible) - self.phunbaba3_safety_check(bosses_possible) + self.phunbaba3_safety_check(bosses_possible) def randomize_packs(self, packs, boss_percent, no_phunbaba3 = False): exclude_bosses = None @@ -197,6 +231,18 @@ def randomize_packs(self, packs, boss_percent, no_phunbaba3 = False): else: exclude_bosses.append(self.formations.DOOM_GAZE) + # We only want statues and dragons to show up when they are intentionally + # mixed into the general boss pool + # Statues are currently seen as normal bosses in regards to scaling, + # but the long-term goal is to add their own scaling option so it + # makes most sense to begin treating these similarly to dragons. + if self.args.statue_boss_location != bosses.BossLocations.MIX: + exclude_bosses += self.formations.ALL_STATUES + + # This is more futureproofing in the event we consolidate dragons in the future + if self.args.dragon_boss_location != bosses.BossLocations.MIX: + exclude_bosses += self.formations.ALL_DRAGONS + import random for pack_id in packs: if random.random() < boss_percent: @@ -290,7 +336,7 @@ def has_enemy(self, pack_id, enemy_id): def get_event_boss_replacement(self, original_boss_name): original_boss_id = self.get_id(original_boss_name) - if self.event_boss_replacements is None: + if not original_boss_id in self.event_boss_replacements: return original_boss_id return self.event_boss_replacements[original_boss_id] @@ -302,12 +348,18 @@ def remove_extra_formations(self): pack.extra_formations[formation_index] = False def mod(self): + self.event_boss_replacements = { + self.DOOM_GAZE: self.DOOM_GAZE, + self.PHUNBABA3: self.PHUNBABA3 + } + if self.args.boss_battles_shuffle: self.shuffle_event_bosses() elif self.args.boss_battles_random: self.randomize_event_bosses() - else: - self.event_boss_replacements = None + + self._handle_original_shuffle_dragons() + self._handle_original_shuffle_statues() if not self.args.fixed_encounters_original: self.randomize_fixed() diff --git a/data/event_bit.py b/data/event_bit.py index b8775c9d..090b3f05 100644 --- a/data/event_bit.py +++ b/data/event_bit.py @@ -1,5 +1,5 @@ # NOTE: (address - 1e80) * 0x8 + bit -# e.g. (1eb7 - 1e80) * 0x8 + 0x1 = 1b9 (airship visible) +# e.g. (1eb7 - 1e80) * 0x8 + 0x1 = 1b9 (airship visible) # (1f43 - 1e80) * 0x8 + 0x3 = 61b (characters on narshe battlefield) DISABLE_SAVE_POINT_TUTORIAL = 0x133 @@ -182,6 +182,13 @@ DEFEATED_PHOENIX_CAVE_DRAGON = 0x120 # custom DEFEATED_FANATICS_TOWER_DRAGON = 0x121 # custom +# KT Battles +DEFEATED_GUARDIAN = 0x0bc +DEFEATED_INFERNO = 0x0bd +DEFEATED_DOOM = 0x072 +DEFEATED_GODDESS = 0x073 +DEFEATED_POLTERGEIST = 0x074 + LEFT_WEIGHT_PUSHED_KEFKA_TOWER = 0x063 RIGHT_WEIGHT_PUSHED_KEFKA_TOWER = 0x064 WEST_PATH_BLOCKED_KEFKA_TOWER = 0x065 # path to doom in switch room @@ -199,9 +206,21 @@ DISABLE_SPRINT = 0x1c1 DISABLE_MENU_ACCESS = 0x1c2 TEMP_SONG_OVERRIDE = 0x1cc +CONTINUE_MUSIC_DURING_BATTLE = 0x2bc ENABLE_Y_PARTY_SWITCHING = 0x1ce ALWAYS_CLEAR = 0x176 # this event_bit is always clear, used for branching +# Unused Bits +# bits 0x200-0x22e Used for banquet soldiers +# 2 bits 0x1bc-0x1bd Unused +# 3 bits 0x1c7-0x1c9 Unused +# 3 bits 0x2c1-0x2c3 Unused + +UNLOCKED_KT_GAUNTLET = 0x2c2 +COMPLETED_KT_GAUNTLET = 0x2c3 +# 8 bits 0x1e6-0x1ed Unused, as the SNES versions feature 20 rare item slots rather than 30 +GAUNTLET_IN_PROGRESS = 0x1e6 + from constants.objectives import MAX_OBJECTIVES for index in range(MAX_OBJECTIVES): globals()["OBJECTIVE" + str(index)] = 0xe0 + index diff --git a/data/items.py b/data/items.py index d3e0f5db..90e60615 100644 --- a/data/items.py +++ b/data/items.py @@ -318,6 +318,8 @@ def add_receive_dialog(self, item_id): self.dialogs.set_text(dialog_id, '< >Received “' + item_name + '”!') + return dialog_id + def print(self): for item in self.items: item.print() diff --git a/data/map_property.py b/data/map_property.py index d6eae884..8f267db6 100644 --- a/data/map_property.py +++ b/data/map_property.py @@ -1,3 +1,5 @@ + + class MapProperty: DATA_SIZE = 33 DATA_START = 0x2d8f00 @@ -16,12 +18,24 @@ def read(self): self.data = self.rom.get_bytes(self.data_start, self.DATA_SIZE) self.name_index = self.data[0] + + self.enable_warp = (self.data[1] & 0x02) >> 1 self.enable_random_encounters = (self.data[5] & 0x80) >> 7 + self.song = self.data[28] def write(self): + self.data[1] = (self.data[1] & ~0x02) | (self.enable_warp << 1) + self.data[5] = (self.data[5] & ~0x80) | (self.enable_random_encounters << 7) + self.data[28] = self.song self.rom.set_bytes(self.data_start, self.data) + def set_warp(self, value): + self.enable_warp = 1 if value else 0 + + def set_random_encounters(self, value): + self.enable_random_encounters = 1 if value else 0 + def print(self): print(f"{self.id}: {self.enable_random_encounters}") diff --git a/data/maps.py b/data/maps.py index 8c63f69f..46bbdf59 100644 --- a/data/maps.py +++ b/data/maps.py @@ -107,6 +107,9 @@ def get_chest_count(self, map_id): def set_chest_item(self, map_id, x, y, item_id): self.chests.set_item(map_id, x, y, item_id) + def get_chests(self, map_id): + return self.chests.map_chests[map_id] + def get_event_count(self, map_id): return (self.maps[map_id + 1]["events_ptr"] - self.maps[map_id]["events_ptr"]) // MapEvent.DATA_SIZE @@ -156,6 +159,12 @@ def print_long_exits(self, map_id): first_exit_id = (self.maps[map_id]["long_exits_ptr"] - self.maps[0]["long_exits_ptr"]) // LongMapExit.DATA_SIZE self.exits.print_long_exit_range(first_exit_id, self.get_long_exit_count(map_id)) + def disable_random_encounter(self, map_id): + self.properties[map_id].set_random_encounters(False) + + def disable_warp(self, map_id): + self.properties[map_id].set_warp(False) + def _fix_imperial_camp_boxes(self): # near the northern tent normally accessed by jumping over a wall # there is a box which can be walked into but not out of which causes the game to lock @@ -232,3 +241,4 @@ def write(self): npcs_ptr[0] = cur_map["npcs_ptr"] & 0xff npcs_ptr[1] = (cur_map["npcs_ptr"] & 0xff00) >> 8 self.rom.set_bytes(npcs_ptr_address, npcs_ptr) + \ No newline at end of file diff --git a/data/npc_bit.py b/data/npc_bit.py index da80622f..46f86570 100644 --- a/data/npc_bit.py +++ b/data/npc_bit.py @@ -1,5 +1,5 @@ # NOTE: (address - 1e80) * 0x8 + bit -# e.g. (1eb7 - 1e80) * 0x8 + 0x1 = 1b9 (airship visible) +# e.g. (1eb7 - 1e80) * 0x8 + 0x1 = 1b9 (airship visible) # (1f43 - 1e80) * 0x8 + 0x3 = 61b (characters on narshe battlefield) SOLDIER_DOORWAY_ARVIS_HOUSE = 0x688 @@ -145,7 +145,7 @@ DOOM_STATUE_KEFKA_TOWER = 0x6b0 GODDESS_STATUE_KEFKA_TOWER = 0x6b1 -POLTRGEIST_STATUE_KEFKA_TOWER = 0x6b2 +POLTERGEIST_STATUE_KEFKA_TOWER = 0x6b2 def byte(npc_bit): return npc_bit // 8 diff --git a/event/airship.py b/event/airship.py index 89b74097..9d51836f 100644 --- a/event/airship.py +++ b/event/airship.py @@ -1,3 +1,4 @@ +from data.npc import NPC from event.event import * class Airship(Event): diff --git a/event/kefka_tower.py b/event/kefka_tower.py index ba1b1264..24361041 100644 --- a/event/kefka_tower.py +++ b/event/kefka_tower.py @@ -1,6 +1,29 @@ +from asyncio import wait_for +from data.map_event import MapEvent +from data.npc import NPC from event.event import * import args +from constants.maps import name_id as map_name_id +from constants.songs import name_id as song_name_id +from constants.sound_effects import name_id as sfx_name_id +import data.event_bit as event_bits +from instruction.field.instructions import BranchIfEventBitSet +from instruction.vehicle import Branch +final_switch_map_id = map_name_id["KT Final Switch Room"] +inferno_room_id = map_name_id["Inferno Room"] +guardian_room_id = map_name_id["Guardian Room"] +poltergeist_room_id = map_name_id["Poltergeist Room"] +doom_room_id = map_name_id["Doom Room"] +goddess_room_id = map_name_id["Goddess Room"] + +def change_party(party): + return [ + field.SetParty(party), + field.RefreshEntities(), + field.UpdatePartyLeader(), + ] + class KefkaTower(Event): def name(self): return "Kefka's Tower" @@ -10,7 +33,7 @@ def init_rewards(self): def init_event_bits(self, space): space.write( - field.SetEventBit(npc_bit.POLTRGEIST_STATUE_KEFKA_TOWER), + field.SetEventBit(npc_bit.POLTERGEIST_STATUE_KEFKA_TOWER), ) import objectives @@ -21,6 +44,7 @@ def init_event_bits(self, space): ) def mod(self): + self.boss_rush_mod() self.statue_landing_mod() self.entrance_landing_mod() self.kefka_scene_mod() @@ -28,6 +52,11 @@ def mod(self): self.item = self.atma_reward.id self.atma_battle_mod() self.atma_mod() + self.inferno_mod() + self.guardian_mod() + self.doom_mod() + self.goddess_mod() + self.poltergeist_mod() self.inferno_battle_mod() if self.args.fix_boss_skip: @@ -37,7 +66,7 @@ def mod(self): self.guardian_battle_mod() self.doom_battle_mod() self.goddess_battle_mod() - self.poltrgeist_battle_mod() + self.poltergeist_battle_mod() self.kefka_battle_mod() self.final_kefka_access_mod() @@ -96,6 +125,7 @@ def statue_landing_mod(self): field_entity.AnimateKneeling(), field_entity.EnableWalkingAnimation(), ), + field.PlaySoundEffect(sfx_name_id.get('Chest/Switch')), field.SetParty(2), field.RefreshEntities(), @@ -113,6 +143,8 @@ def statue_landing_mod(self): field_entity.SetSpriteLayer(0), ), + field.PlaySoundEffect(sfx_name_id.get('Chest/Switch')), + field.SetParty(3), field.RefreshEntities(), field.UpdatePartyLeader(), @@ -122,33 +154,124 @@ def statue_landing_mod(self): field_entity.Move(direction.DOWN, 6), field_entity.AnimateKneeling(), ), + + field.PlaySoundEffect(sfx_name_id.get('Chest/Switch')), + field.Pause(0.75), - Read(0xa039c, 0xa039f), - field.LoadMap(0x163, direction.DOWN, default_music = True, - x = 35, y = 6, fade_in = True, entrance_event = True), - field.FreeScreen(), - Read(0xa03b0, 0xa03b9), + self.post_landing_src(0x163, 35, 6), ] - space = Write(Bank.CA, src, "kefka tower statue landing") + space = Write(Bank.F0, src, "kefka tower statue landing") self.statue_landing = space.start_address space = Reserve(0xa03ad, 0xa03af, "kefka tower the statues are up ahead", field.NOP()) + def invoke_kt_battle(self, original_pack_name): + boss_id = self.get_boss(original_pack_name, False) + return [ + field.InvokeBattle(boss_id, battle_sound = True, battle_animation = True), + ] + + # Trigger five bosses back-to-back + def boss_rush_mod(self): + src = [ + field.ReturnIfEventBitClear(event_bit.GAUNTLET_IN_PROGRESS), # return if we're not in gauntlet + field.StartSong(song_name_id['FierceBattle']), # subroutine will return if song is already playing, so will be called repeatedly + field.Return(), + ] + + + self.trigger_gauntlet_music = Write(Bank.F0, src, "Trigger gauntlet music").start_address + + self.inferno_cutscene = self.gauntlet_inferno_cutscene() + self.post_inferno_cutscene = self.gauntlet_post_inferno_cutscene() + self.guardian_cutscene = self.gauntlet_guardian_cutscene() + self.post_guardian_cutscene = self.gauntlet_post_guardian_cutscene() + self.doom_cutscene = self.gauntlet_doom_cutscene() + self.goddess_cutscene = self.gauntlet_goddess_cutscene() + self.poltergeist_cutscene = self.gauntlet_poltergeist_cutscene() + self.post_gauntlet_cutscene = self.gauntlet_post_battle_cutscene() + + # Uncomment these to debug/view specific cutscenes + # Must run with `-debug` flag to utilize this + debug_event_bits = [ + # field.SetEventBit(event_bit.DEFEATED_INFERNO), + # field.SetEventBit(event_bit.DEFEATED_GUARDIAN), + # field.SetEventBit(event_bit.DEFEATED_DOOM), + # field.SetEventBit(event_bit.DEFEATED_GODDESS), + # field.SetEventBit(event_bit.DEFEATED_POLTERGEIST), + ] if self.args.debug else [] + + src = [ + Read(0xa02d6, 0xa030a), + + debug_event_bits, + + field.SetEventBit(event_bit.CONTINUE_MUSIC_DURING_BATTLE), + field.SetEventBit(event_bit.GAUNTLET_IN_PROGRESS), + field.SetEventBit(event_bit.LEFT_WEIGHT_PUSHED_KEFKA_TOWER), + field.SetEventBit(event_bit.RIGHT_WEIGHT_PUSHED_KEFKA_TOWER), + field.ClearEventBit(npc_bit.LEFT_UNPUSHED_WEIGHT_KEFKA_TOWER), + field.SetEventBit(npc_bit.LEFT_PUSHED_WEIGHT_KEFKA_TOWER), + field.ClearEventBit(npc_bit.RIGHT_UNPUSHED_WEIGHT_KEFKA_TOWER), + field.SetEventBit(npc_bit.RIGHT_PUSHED_WEIGHT_KEFKA_TOWER), + field.ClearEventBit(npc_bit.CENTER_DOOR_BLOCK_KEFKA_TOWER), + + field.SetEventBit(event_bit.WEST_PATH_BLOCKED_KEFKA_TOWER), + field.SetEventBit(event_bit.EAST_PATH_BLOCKED_KEFKA_TOWER), + field.SetEventBit(event_bit.NORTH_PATH_OPEN_KEFKA_TOWER), + field.SetEventBit(event_bit.SOUTH_PATH_OPEN_KEFKA_TOWER), + field.SetEventBit(event_bit.CENTER_DOOR_KEFKA_TOWER), + field.SetEventBit(event_bit.LEFT_RIGHT_DOORS_KEFKA_TOWER), + field.SetEventBit(event_bit.TEMP_SONG_OVERRIDE), + ] + + # Gauntlet execution code - Breaking this up for visibility + src += [ + field.BranchIfEventBitSet(event_bit.DEFEATED_INFERNO, "GUARDIAN"), + field.Call(self.inferno_cutscene), + field.Call(self.post_inferno_cutscene), + "GUARDIAN", + field.BranchIfEventBitSet(event_bit.DEFEATED_GUARDIAN, "DOOM"), + field.Call(self.guardian_cutscene), + field.Call(self.post_guardian_cutscene), + "DOOM", + field.BranchIfEventBitSet(event_bit.DEFEATED_DOOM, "GODDESS"), + field.Call(self.doom_cutscene), + "GODDESS", + field.BranchIfEventBitSet(event_bit.DEFEATED_GODDESS, "POLTERGEIST"), + field.Call(self.goddess_cutscene), + "POLTERGEIST", + field.BranchIfEventBitSet(event_bit.DEFEATED_POLTERGEIST, "POST_GAUNTLET"), + field.Call(self.poltergeist_cutscene), + "POST_GAUNTLET", + field.Call(self.post_gauntlet_cutscene), + field.ClearEventBit(event_bit.CONTINUE_MUSIC_DURING_BATTLE), + field.ClearEventBit(event_bit.GAUNTLET_IN_PROGRESS), + field.SetEventBit(event_bit.COMPLETED_KT_GAUNTLET), + self.post_landing_src(final_switch_map_id, 103, 45), + ] + + space = Write(Bank.F0, src, "kefka tower gauntlet") + self.gauntlet_event = space.start_address + + def entrance_landing_mod(self): need_more_allies = 2982 self.dialogs.set_text(need_more_allies, "We need to find more allies.") - statues_entrance = 1287 - self.dialogs.set_text(statues_entrance, - " (Statues) (Entrance) (Not just yet)") + statues_entrance = self.dialogs.create_dialog(" (Statues) (Entrance) (Not just yet)") + gauntlet_entrance = self.dialogs.create_dialog(" (Gauntlet) (Entrance) (Not just yet)") + both_entrance = self.dialogs.create_dialog(" (Gauntlet) (Statues) (Entrance) (Not just yet)") space = Reserve(0xa01a2, 0xa02d5, "kefka tower first landing scene", field.NOP()) + space.add_label("GAUNTLET_LANDING", self.gauntlet_event) space.add_label("STATUE_LANDING", self.statue_landing) space.add_label("ENTRANCE_LANDING", space.end_address + 1) space.write( field.BranchIfEventWordLess(event_word.CHARACTERS_AVAILABLE, 3, "NEED_MORE_ALLIES"), - field.BranchIfEventBitSet(event_bit.UNLOCKED_KT_SKIP, "LANDING_MENU"), + field.BranchIfEventBitSet(event_bit.UNLOCKED_KT_SKIP, "STATUE_MENU_EVAL"), + field.BranchIfEventBitSet(event_bit.UNLOCKED_KT_GAUNTLET, "GAUNTLET_DIALOG"), field.Pause(2), # NOTE: load-bearing pause, without a pause or dialog before party select the game # enters an infinite loop. it seems like the game needs time to finish @@ -165,11 +288,24 @@ def entrance_landing_mod(self): vehicle.End(), field.Return(), - "LANDING_MENU", + "STATUE_MENU_EVAL", + field.BranchIfEventBitSet(event_bit.UNLOCKED_KT_GAUNTLET, "GAUNTLET_STATUES_DIALOG"), + field.Branch("STATUES_DIALOG"), + + "GAUNTLET_DIALOG", + field.DialogBranch(gauntlet_entrance, + dest1 = "GAUNTLET_LANDING", dest2 = "ENTRANCE_LANDING", dest3 = "CANCEL_LANDING"), + "STATUES_DIALOG", field.DialogBranch(statues_entrance, - dest1 = "STATUE_LANDING", dest2 = "ENTRANCE_LANDING", dest3 = "CANCEL_LANDING"), + dest1 = "STATUE_LANDING", dest2 = "ENTRANCE_LANDING", dest3 = "CANCEL_LANDING"), + "GAUNTLET_STATUES_DIALOG", + field.DialogBranch(both_entrance, + dest1 = "GAUNTLET_LANDING", dest2 = "STATUE_LANDING", dest3 = "ENTRANCE_LANDING", dest4 = "CANCEL_LANDING"), ) + + + def kefka_scene_mod(self): space = Reserve(0xc17ff, 0xc1801, "kefka tower defeat the statues, and magical power will not disappear", field.NOP()) @@ -243,6 +379,71 @@ def atma_battle_mod(self): field.InvokeBattle(boss_pack_id), ) + # Copy no less than 4 bytes between start_target and end_target + # This will be called after one of the kt encounters has completed, but just prior to finishing the check + def kt_encounter_objective_mod(self, boss_name, bit, start_target, end_target, description): + src = [] + src += Read(start_target, end_target) + + src += [ + "FINISH_OBJECTIVE", + field.SetEventBit(bit), + field.CheckObjectives(), + field.FreeScreen(), + field.Return(), + ] + post_battle = Write(Bank.F0, src, f"{boss_name} post-battle. 1) Set event bit. 2) Finish check") + + space = Reserve(start_target, end_target, description, asm.NOP()) + space.write([ + field.Call(post_battle.start_address) + ]) + + def guardian_mod(self): + self.kt_encounter_objective_mod( + "Guardian", + event_bit.DEFEATED_GUARDIAN, + 0xc186c, + 0xc186f, + "Guardian battle post-script, wait for fade, set bit", + ) + + def inferno_mod(self): + self.kt_encounter_objective_mod( + "Inferno", + event_bit.DEFEATED_INFERNO, + 0xc18ae, + 0xc18b1, + "Inferno battle post-script, fade in, wait, set bit", + ) + + def doom_mod(self): + self.kt_encounter_objective_mod( + "Doom", + event_bit.DEFEATED_DOOM, + 0xc16f0, + 0xc16f3, + "Doom battle post-script, Hide NPC 5, set npc bit", + ) + + def goddess_mod(self): + self.kt_encounter_objective_mod( + "Goddess", + event_bit.DEFEATED_GODDESS, + 0xc1730, + 0xc1733, + "Goddess battle post-script, Hide NPC 2, set npc bit", + ) + + def poltergeist_mod(self): + self.kt_encounter_objective_mod( + "Goddess", + event_bit.DEFEATED_POLTERGEIST, + 0xc1786, + 0xc1789, + "Poltergeist battle post-script, Hide NPCs, set npc bit", + ) + def atma_mod(self): src = [ Read(0xc18d3, 0xc18d6), # show save point, set save point npc bit @@ -253,7 +454,7 @@ def atma_mod(self): field.FinishCheck(), field.Return(), ] - space = Write(Bank.CC, src, "kefka tower atma reward") + space = Write(Bank.F0, src, "kefka tower atma reward") atma_reward = space.start_address space = Reserve(0xc18d3, 0xc18d6, "kefka tower after atma", field.NOP()) @@ -262,12 +463,16 @@ def atma_mod(self): ) def inferno_battle_mod(self): - boss_pack_id = self.get_boss("Inferno") - - space = Reserve(0xc18a3, 0xc18a9, "kefka tower invoke battle inferno", field.NOP()) - space.write( - field.InvokeBattle(boss_pack_id), - ) + boss_src = [ + field.Call(self.trigger_gauntlet_music), + self.invoke_kt_battle('Inferno'), + field.Return(), + ] + boss_space = Write(Bank.F0, boss_src, "trigger inferno fight, ") + space = Reserve(0xc18a2, 0xc18a9, "call inferno fight subroutine", asm.NOP()) + space.write([ + field.Call(boss_space.start_address) + ]) def inferno_skip_fix(self): # not sure why (stairs?) but this npc only blocks skipping the inferno event tile when entering from the east @@ -325,35 +530,55 @@ def poltergeist_skip_fix(self): invisible_block_npc_id = self.maps.append_npc(0x11f, invisible_block_npc) def guardian_battle_mod(self): - boss_pack_id = self.get_boss("Guardian") + boss_src = [ + field.Call(self.trigger_gauntlet_music), + self.invoke_kt_battle('Guardian'), + field.Return(), + ] + boss_space = Write(Bank.F0, boss_src, "trigger inferno fight, ") space = Reserve(0xc184a, 0xc1850, "kefka tower invoke battle guardian", field.NOP()) space.write( - field.InvokeBattle(boss_pack_id), + field.Call(boss_space.start_address), ) def doom_battle_mod(self): - boss_pack_id = self.get_boss("Doom") + boss_src = [ + field.Call(self.trigger_gauntlet_music), + self.invoke_kt_battle('Doom'), + field.Return(), + ] + boss_space = Write(Bank.F0, boss_src, "trigger inferno fight, ") space = Reserve(0xc16dc, 0xc16e2, "kefka tower invoke battle doom", field.NOP()) space.write( - field.InvokeBattle(boss_pack_id), + field.Call(boss_space.start_address), ) def goddess_battle_mod(self): - boss_pack_id = self.get_boss("Goddess") + boss_src = [ + field.Call(self.trigger_gauntlet_music), + self.invoke_kt_battle('Goddess'), + field.Return(), + ] + boss_space = Write(Bank.F0, boss_src, "trigger inferno fight, ") space = Reserve(0xc171c, 0xc1722, "kefka tower invoke battle goddess", field.NOP()) space.write( - field.InvokeBattle(boss_pack_id), + field.Call(boss_space.start_address), ) - def poltrgeist_battle_mod(self): - boss_pack_id = self.get_boss("Poltrgeist") + def poltergeist_battle_mod(self): + boss_src = [ + field.Call(self.trigger_gauntlet_music), + self.invoke_kt_battle('Poltrgeist'), + field.Return(), + ] + boss_space = Write(Bank.F0, boss_src, "trigger inferno fight, ") - space = Reserve(0xc1755, 0xc175b, "kefka tower invoke battle poltrgeist", field.NOP()) + space = Reserve(0xc1755, 0xc175b, "kefka tower invoke battle poltergeist", field.NOP()) space.write( - field.InvokeBattle(boss_pack_id), + field.Call(boss_space.start_address), ) def kefka_battle_mod(self): @@ -402,7 +627,7 @@ def final_kefka_access_mod(self): field.Branch(0xc1970), ] - space = Write(Bank.CC, src, "kefka tower 3 parties on final switches check") + space = Write(Bank.F0, src, "kefka tower 3 parties on final switches check") three_switches_check = space.start_address space = Reserve(0xc193f, 0xc196f, "kefka tower final kefka access check", field.NOP()) @@ -439,6 +664,17 @@ def final_scenes_mod(self): space = Reserve(0xa1330, 0xa1332, "kefka tower you mean terra too?", field.NOP()) space = Reserve(0xa133e, 0xa1340, "kefka tower come with me. i can lead you out", field.NOP()) + # Load character into the given map and position, then return control (with KT prompt) + def post_landing_src(self, map_id, map_x, map_y): + return [ + Read(0xa039c, 0xa039f), + field.LoadMap(map_id, direction.DOWN, default_music = True, + x = map_x, y = map_y, fade_in = True, entrance_event = True), + field.CheckObjectives(), + field.FreeScreen(), + Read(0xa03b0, 0xa03b9), + ] + def exit_mod(self): # for some reason poltrgeist's statue bit was set when left/right parties opened center door # and it was cleared when leaving or warping out @@ -448,3 +684,546 @@ def exit_mod(self): # leaving with crane or warp stone # warp stone call trace: c0c670 -> ca0039 -> ca0108 -> ca014f -> cc0ff6 -> cc0f7d space = Reserve(0xc0fbf, 0xc0fc0, "kefka tower exit clear poltrgeist statue bit", asm.NOP()) + + def gauntlet_inferno_cutscene(self): + src = [ + field.LoadMap(inferno_room_id, direction.DOWN, default_music = False, + x = 27, y = 18, fade_in = False, entrance_event = True), + field.HoldScreen(), + + field.SetPartyMap(1, inferno_room_id), + field.SetPartyMap(2, inferno_room_id), + field.SetPartyMap(3, inferno_room_id), + change_party(1), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetPosition(0, 0), + ), + + change_party(2), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetPosition(0, 0), + ), + + change_party(3), + # Move main party to east of inferno + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetPosition(39, 18), + ), + field.FadeInScreen(3), + field.EntityAct(field_entity.CAMERA, False, + field_entity.SetSpeed(field_entity.Speed.SLOW), + field_entity.Move(direction.RIGHT, 4), + ), + + field.Pause(1), + + field.EntityAct(field_entity.PARTY0, False, + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.Move(direction.LEFT, 8), + ), + field.Call(0xc1872), # Inferno event tile address + field.EntityAct(field_entity.PARTY0, False, + field_entity.AnimateAttack(), + ), + field.Return(), + ] + + space = Write(Bank.F0, src, "Inferno Gauntlet Cutscene") + return space.start_address + + # Need a cutscene for getting objectives after gauntlet, otherwise can show dialog behind black screen + def gauntlet_post_inferno_cutscene(self): + src = [ + field.EntityAct(field_entity.PARTY0, False, + field_entity.SetSpriteLayer(1), + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.Move(direction.LEFT, 1), + field_entity.MoveDiagonal(direction.LEFT, 1, direction.DOWN, 1), + field_entity.MoveDiagonal(direction.LEFT, 1, direction.DOWN, 1), + field_entity.MoveDiagonal(direction.LEFT, 1, direction.DOWN, 1), + field_entity.SetSpriteLayer(0), + field_entity.Move(direction.DOWN, 3), + field_entity.Move(direction.RIGHT, 2), + field_entity.Move(direction.DOWN, 3), + ), + field.Pause(0.5), + field.FadeOutScreen(4), + field.WaitForFade(), + field.Return() + ] + + space = Write(Bank.F0, src, "Post-Inferno Gauntlet Cutscene") + return space.start_address + + def gauntlet_guardian_cutscene(self): + src = [ + field.LoadMap(guardian_room_id, direction.DOWN, default_music = False, + x = 12, y = 14, fade_in = False, entrance_event = True), + field.HoldScreen(), + # Initialize sprite positions + [ + + # Move Guardian back 1 cell + field.EntityAct(0x10, False, + field_entity.SetPosition(11, 9) + ), + field.EntityAct(0x13, False, + field_entity.SetPosition(12, 9) + ), + field.EntityAct(0x16, False, + field_entity.SetPosition(13, 9) + ), + ], + # Camera position + field.EntityAct(field_entity.CAMERA, False, + field_entity.SetSpeed(field_entity.Speed.SLOW), + field_entity.Move(direction.UP, 2), + ), + # init party 1 position + field.SetPartyMap(1, guardian_room_id), + change_party(1), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetPosition(6, 15), + ), + # Party 1 picking magitek flowers + [ + field.EntityAct(field_entity.PARTY0, False, + field_entity.SetSpeed(field_entity.Speed.NORMAL), + field_entity.Move(direction.UP, 3), + field_entity.Move(direction.LEFT, 1), + field_entity.AnimateKneeling(), + field_entity.Pause(12), + + field_entity.Move(direction.DOWN, 2), + field_entity.AnimateKneeling(), + field_entity.Pause(12), + + field_entity.Move(direction.UP, 1), + field_entity.Move(direction.RIGHT, 2), + field_entity.Pause(8), + field_entity.AnimateStandingFront(), + field_entity.Pause(1), + field_entity.AnimateFrontRightHandOnHead(), + field_entity.Pause(4), + field_entity.AnimateFrontRightHandUp(), + field_entity.Pause(5), + field_entity.AnimateStandingFront(), + field_entity.Pause(5), + + field_entity.AnimateSurprised(), + field_entity.AnimateLowJump(), + field_entity.Pause(10), + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.Move(direction.LEFT, 1), + field_entity.Move(direction.DOWN, 3), + field_entity.SetPosition(0, 0) + ), + ], + # init party 3 position + field.SetPartyMap(3, guardian_room_id), + change_party(3), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetPosition(18, 14), + ), + # Party 3 walking around + [ + field.EntityAct(field_entity.PARTY0, False, + field_entity.SetSpeed(field_entity.Speed.SLOW), + field_entity.Move(direction.UP, 2), + field_entity.Move(direction.LEFT, 1), + field_entity.Pause(8), + field_entity.Move(direction.DOWN, 1), + field_entity.Turn(direction.LEFT), + field_entity.Pause(10), + field_entity.AnimateFrontRightHandOnHead(), + field_entity.Pause(3), + field_entity.AnimateFrontRightHandUp(), + field_entity.Pause(3), + field_entity.AnimateFrontRightHandOnHead(), + field_entity.Pause(3), + field_entity.AnimateFrontRightHandUp(), + field_entity.Pause(5), + field_entity.AnimateStandingFront(), + field_entity.Move(direction.RIGHT, 1), + field_entity.Move(direction.UP, 1), + field_entity.AnimateSurprised(), + field_entity.AnimateLowJump(), + field_entity.Pause(5), + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.Move(direction.DOWN, 4), + field_entity.SetPosition(0, 0) + ), + ], + # field.FadeInScreen(3), + field.SetPartyMap(2, guardian_room_id), + change_party(2), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetPosition(12, 17), + ), + field.FadeInScreen(3), + field.Pause(2), + # Party 2, the initiator + [ + # getting in position + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.Move(direction.UP, 3), + field_entity.AnimateFrontHandsUp(), + field_entity.Pause(5), + field_entity.AnimateStandingFront(), + field_entity.Pause(5), + field_entity.AnimateFrontHandsUp(), + field_entity.Pause(5), + field_entity.AnimateStandingFront(), + ), + # grabbing other party attention + field.EntityAct(field_entity.PARTY0, False, + field_entity.Pause(5), + field_entity.AnimateFrontHandsUp(), + field_entity.Pause(5), + field_entity.AnimateStandingFront(), + field_entity.Pause(5), + field_entity.AnimateFrontHandsUp(), + field_entity.Pause(5), + field_entity.AnimateStandingFront(), + ), + field.Pause(2), + # Reaction to alarm + field.EntityAct(field_entity.PARTY0, False, + field_entity.AnimateSurprised(), + field_entity.AnimateLowJump(), + field_entity.Pause(4), + field_entity.AnimateKnockedOut(), + field_entity.Pause(4), + field_entity.AnimateKneeling(), + field_entity.Pause(4), + field_entity.AnimateStandingHeadDown(), + field_entity.Pause(4), + field_entity.Turn(direction.DOWN), + field_entity.Pause(12), + field_entity.AnimateFingerWagStraightUp(), + field_entity.Pause(1), + field_entity.AnimateFingerWagToSide(), + field_entity.Pause(1), + field_entity.AnimateFingerWagStraightUp(), + field_entity.Pause(1), + field_entity.AnimateFingerWagToSide(), + field_entity.Pause(1), + field_entity.AnimateFingerWagStraightUp(), + field_entity.Pause(8), + field_entity.Move(direction.UP, 1), + ), + ], + field.Call(0xc1827), # original guardian event code + field.Return(), + ] + space = Write(Bank.F0, src, "Guardian Gauntlet Cutscene") + return space.start_address + + # Need a cutscene for getting objectives after gauntlet, otherwise can show dialog behind black screen + def gauntlet_post_guardian_cutscene(self): + src = [ + field.EntityAct(field_entity.PARTY0, False, + field_entity.Move(direction.UP, 8), + ), + field.PauseUnits(1), + field.FlashScreen(field.Flash.BLUE), + field.PlaySoundEffect(141), + field.FadeOutScreen(4), + field.WaitForFade(), + field.Return() + ] + + space = Write(Bank.F0, src, "Post-Guardian Gauntlet Cutscene") + return space.start_address + + + def gauntlet_doom_cutscene(self): + src = [ + field.SetPartyMap(1, doom_room_id), + change_party(1), + field.LoadMap(doom_room_id, direction.DOWN, default_music = False, + x = 64, y = 15, fade_in = False, entrance_event = True), + field.HoldScreen(), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetPosition(64, 21), + ), + field.FadeInScreen(3), + field.EntityAct(field_entity.CAMERA, False, + field_entity.SetSpeed(field_entity.Speed.SLOWEST), + field_entity.Move(direction.UP, 3), + ), + field.Pause(2), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.Move(direction.UP, 8), + field_entity.Move(direction.UP, 1) + ), + + field.Call(0xc16d6), + + field.EntityAct(field_entity.PARTY0, False, + field_entity.SetSpeed(field_entity.Speed.NORMAL), + field_entity.Move(direction.LEFT, 1), + field_entity.Move(direction.UP, 3), + ), + field.FadeOutScreen(3), + field.Pause(1), + field.Return(), + ] + + space = Write(Bank.F0, src, "Doom Gauntlet Cutscene") + return space.start_address + + def gauntlet_goddess_cutscene(self): + src = [ + field.SetPartyMap(3, goddess_room_id), + change_party(3), + field.LoadMap(goddess_room_id, direction.DOWN, default_music = False, + x = 12, y = 28, fade_in = False, entrance_event = True), + field.HoldScreen(), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetPosition(12, 40) + ), + field.FadeInScreen(3), + field.EntityAct(field_entity.CAMERA, False, + field_entity.SetSpeed(field_entity.Speed.SLOW), + field_entity.Move(direction.DOWN, 5) + ), + field.Pause(1.5), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.Move(direction.UP, 8) + ), + field.Call(0xc1716), + field.HoldScreen(), + field.EntityAct(field_entity.PARTY0, False, + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.Pause(8), + field_entity.Move(direction.UP, 2), + field_entity.EnableWalkingAnimation(), + field_entity.AnimateLowJump(), + ), + field.FadeOutScreen(4), + field.Pause(1), + field.Return(), + ] + + space = Write(Bank.F0, src, "Goddess Gauntlet Cutscene") + return space.start_address + + def gauntlet_poltergeist_cutscene(self): + chests = self.maps.get_chests(poltergeist_room_id) + chest = chests[7] + src = [ + field.SetPartyMap(2, goddess_room_id), + change_party(2), + field.HoldScreen(), + field.LoadMap(poltergeist_room_id, direction.DOWN, default_music = False, + x = 35, y = 22, fade_in = False, entrance_event = True), + field.EntityAct(field_entity.CAMERA, False, + field_entity.SetPosition(40, 15), + field_entity.SetSpeed(field_entity.Speed.SLOW), + field_entity.Move(direction.LEFT, 2), + field_entity.MoveDiagonal(direction.LEFT, 1, direction.UP, 1), + field_entity.MoveDiagonal(direction.LEFT, 1, direction.UP, 1), + field_entity.MoveDiagonal(direction.LEFT, 1, direction.UP, 1), + field_entity.MoveDiagonal(direction.LEFT, 1, direction.UP, 1), + field_entity.Move(direction.UP, 2), + ), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetPosition(27, 23) + ), + + field.FadeInScreen(3), + + field.BranchIfTreasureCollected(chest.bit, "IGNORE_CHEST"), + "COLLECT_CHEST", + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetSpeed(field_entity.Speed.NORMAL), + field_entity.Move(direction.DOWN, 2), + field_entity.Move(direction.RIGHT, 2), + field_entity.Move(direction.DOWN, 3), + field_entity.AnimateKneeling(), + ), + + field.CollectTreasure(poltergeist_room_id, chest.x, chest.y), + field.PlaySoundEffect(sfx_name_id.get('Chest/Switch')), + field.Dialog(self.items.add_receive_dialog(chest.contents), wait_for_input=False), + + field.EntityAct(field_entity.PARTY0, True, + field_entity.AnimateStandingFront(), + field_entity.Pause(2), + field_entity.Move(direction.UP, 3), + field_entity.Move(direction.RIGHT, 1), + field_entity.SetSpeed(field_entity.Speed.NORMAL), + field_entity.Move(direction.UP, 5), + ), + field.Branch("POST_TREASURE"), + + "IGNORE_CHEST", + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetSpeed(field_entity.Speed.NORMAL), + field_entity.Move(direction.DOWN, 2), + field_entity.Move(direction.RIGHT, 3), + field_entity.Move(direction.UP, 5), + ), + + "POST_TREASURE", + field.EntityAct(field_entity.PARTY0, True, + field.Pause(4), + field_entity.Turn(direction.DOWN), + field_entity.Pause(4), + field_entity.AnimateCloseEyes(), + field_entity.Pause(4), + field_entity.AnimateStandingHeadDown(), + field_entity.Pause(24), + field_entity.Turn(direction.DOWN), + field_entity.Pause(1), + field_entity.AnimateCloseEyes(), + field_entity.Pause(1), + field_entity.Turn(direction.DOWN), + field_entity.Pause(4), + field_entity.AnimateLowJump(), + field_entity.AnimatePowerStance(), + field_entity.Pause(12), + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.Move(direction.UP, 4), + + ), + field.Call(0xc174f), + + field.EntityAct(field_entity.PARTY0, False, + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.Move(direction.UP, 8), + ), + field.FadeOutScreen(4), + field.Pause(1), + field.Return(), + ] + space = Write(Bank.F0, src, "Poltergeist Gauntlet Cutscene") + return space.start_address + + def gauntlet_post_battle_cutscene(self): + party1_x = 103 + party1_y_dest = 45 + party1_y_offset = 10 + party1_y_start = party1_y_dest - party1_y_offset + + party2_x = 109 + party2_y_dest = 42 + party2_y_offset = 9 + party2_y_start = party2_y_dest - party2_y_offset + + party3_x = 115 + party3_y_dest = 44 + party3_y_offset = 8 + party3_y_start = party3_y_dest - party3_y_offset + src = [ + [ + # Load final + field.LoadMap(final_switch_map_id, direction.DOWN, default_music = False, + x = 109, y = 43, fade_in = False, entrance_event = False), + field.HoldScreen(), + field.FadeOutSong(255), + field.ClearEventBit(event_bit.TEMP_SONG_OVERRIDE), + + # Party 1 init position + [ + field.SetPartyMap(1, final_switch_map_id), + change_party(1), + Read(0xa0334, 0xa033c), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetPosition(party1_x, party1_y_start), + field_entity.AnimateFrontHandsUp(), + field_entity.SetSpeed(field_entity.Speed.FAST), + ), + ], + # Party 2 init position + [ + field.SetPartyMap(2, final_switch_map_id), + Read(0xa031e, 0xa0320), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetPosition(party2_x, party2_y_start), + field_entity.SetSpeed(field_entity.Speed.FAST), + ), + ], + # Party 3 init position + [ + field.SetPartyMap(3, final_switch_map_id), + Read(0xa0327, 0xa032d), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetPosition(party3_x, party3_y_start), + field_entity.SetSpeed(field_entity.Speed.FAST), + ), + ], + field.FadeInScreen(), + ], + # party 1 fall + [ + change_party(1), + field.PlaySoundEffect(sfx_name_id.get('Falling')), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.DisableWalkingAnimation(), + field_entity.AnimateSurprised(), + field_entity.Move(direction.DOWN, party1_y_offset - 2), + field_entity.Move(direction.DOWN, 2), + field_entity.AnimateKneeling(), + field_entity.EnableWalkingAnimation(), + ), + field.PlaySoundEffect(sfx_name_id.get('Umaro Body Slam')), + ], + field.Pause(0.5), + # party 2 fall + [ + change_party(2), + field.Pause(0.25), + field.PlaySoundEffect(sfx_name_id.get('Falling')), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.SetSpriteLayer(2), + field_entity.DisableWalkingAnimation(), + field_entity.AnimateSurprised(), + field_entity.Move(direction.DOWN, party2_y_offset - 2), # extra offset is added to the next entity script + field_entity.SetSpeed(field_entity.Speed.FASTEST), + field_entity.AnimateLowJump(), + field_entity.AnimateKnockedOut(), + ), + field.PauseUnits(3), + field.PlaySoundEffect(sfx_name_id.get('Chest/Switch')), + + field.EntityAct(field_entity.PARTY0, False, + field_entity.Pause(3), + field_entity.Move(direction.DOWN, 2), + field_entity.AnimateKneeling(), + field_entity.EnableWalkingAnimation(), + field_entity.SetSpriteLayer(0), + ), + field.PauseUnits(20), + field.PlaySoundEffect(sfx_name_id.get('Umaro Body Slam')), + ], + field.Pause(0.5), + # party 3 fall + [ + change_party(3), + field.Pause(0.25), + field.PlaySoundEffect(sfx_name_id.get('Falling')), + + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.DisableWalkingAnimation(), + field_entity.AnimateSurprised(), + field_entity.Move(direction.DOWN, party3_y_offset), + field_entity.AnimateKneeling(), + ), + field.PlaySoundEffect(sfx_name_id.get('Umaro Body Slam')), + ], + + field.Pause(0.75), + + field.Return(), + ] + + space = Write(Bank.F0, src, "Post-Gauntlet Cutscene") + return space.start_address diff --git a/instruction/field/custom.py b/instruction/field/custom.py index bdc6f5d5..f6132068 100644 --- a/instruction/field/custom.py +++ b/instruction/field/custom.py @@ -1,5 +1,5 @@ from memory.space import Bank, START_ADDRESS_SNES, Reserve, Write, Read -from instruction.event import _Instruction, _Branch +from instruction.event import EVENT_CODE_START, _Instruction, _Branch import instruction.asm as asm import instruction.c0 as c0 from enum import IntEnum @@ -7,7 +7,7 @@ def _set_opcode_address(opcode, address): FIRST_OPCODE = 0x35 opcode_table_address = 0x098c4 + (opcode - FIRST_OPCODE) * 2 - space = Reserve(opcode_table_address, opcode_table_address + 1, "field opcode table, {opcode} {hex(address)}") + space = Reserve(opcode_table_address, opcode_table_address + 1, f"field opcode table, {opcode} {hex(address)}") space.write( (address & 0xffff).to_bytes(2, "little"), ) @@ -231,3 +231,175 @@ def __init__(self, function_address, arg = 0): LongCall.__init__ = (lambda self, function_address, arg = 0 : super().__init__(opcode, function_address.to_bytes(3, "little"), arg)) self.__init__(function_address, arg) + +CHEST_BLOCK_SIZE = 5 +class CollectTreasure(_Instruction): + def __init__(self, map_id, x, y): + src = [ + "REMOTE_TREASURE", + asm.A16(), # REP #$20 + asm.LDA(0xeb, asm.DIR), # LDA $EB ; load map + asm.ASL(), # ASL A + asm.TAX(), # TAX + asm.LDA(0xed82f6, asm.LNG_X), # LDA $ED82F6,X ; load the pointer for the *next room* + asm.STA(0x1e, asm.DIR), # STA $1E $1e = next room pointer + asm.LDA(0xed82f4, asm.LNG_X), # LDA $ED82F4,X ; load the pointer for our current room + asm.TAX(), # TAX x = current room pointer + asm.TDC(), # TDC + asm.A8(), # SEP #$20 + asm.CPX(0x1e, asm.DIR), # CPX $1E ; do the two pointers match? if they do, this map has no treasure + asm.BEQ("TREASURE_WRAPUP1"), # We chose a map without a treasure chest - branch and exit if so + + "TREASURE_LOOP_AGAIN", # treasure_loop_again: + asm.LDA(0xed8634, asm.LNG_X), # LDA $ED8634,X ; load X coordinate of chest + asm.CMP(0xed, asm.DIR), # CMP $ED ; does it match? + asm.BNE("NO_CHEST"), # BNE no_chest ; we do have to have some kind of fail-safe in place + asm.LDA(0xed8635, asm.LNG_X), # LDA $ED8635,X ; load Y coordinate of chest + asm.CMP(0xee, asm.DIR), # CMP $EE ; does it match? + asm.BEQ("TREASURE_FOUND"), # BEQ treasure_found ; we have matched X and Y, let's get the contents + + "NO_CHEST", # no_chest: + asm.INX(), # INX + asm.INX(), # INX + asm.INX(), # INX + asm.INX(), # INX + asm.INX(), # INX + asm.CPX(0x1e, asm.DIR), # CPX $1E + asm.BNE("TREASURE_LOOP_AGAIN"), # BNE treasure_loop_again + # ; coming in, upper A is already 00 + "TREASURE_WRAPUP1", # treasure_wrapup1: this is a duplicate as the jump to the other is too far to safely make + asm.TDC(), # TDC + asm.LDA(0x05, asm.IMM8), # command size + asm.JMP(0x9b5c, asm.ABS), # next command + + "TREASURE_FOUND", # treasure_found: + asm.A16(), # REP #$20 + asm.LDA(0xed8638, asm.LNG_X), # LDA $ED8638,X ; load contents + asm.STA(0x1a, asm.DIR), # STA $1A + asm.LDA(0xed8636, asm.LNG_X), # LDA $ED8636,X ; load the byte and bit + asm.STA(0x1e, asm.DIR), # STA $1E + asm.AND(0x0007, asm.IMM16), # AND #$0007 + asm.TAX(), # TAX + asm.LDA(0x1e, asm.DIR), # LDA $1E + asm.AND(0x01f8, asm.IMM16), # AND #$01F8 + asm.LSR(), # LSR A + asm.LSR(), # LSR A + asm.LSR(), # LSR A + asm.TAY(), # TAY + asm.TDC(), # TDC + asm.A8(), # SEP #$20 + asm.LDA(0x1e40, asm.ABS_Y), # LDA $1E40,Y + asm.AND(0xc0bafc, asm.LNG_X), # AND $C0BAFC,X ; is this bit set? + asm.BNE("TREASURE_WRAPUP"), # chest has already been looted ; branch and exit if so + asm.LDA(0x1e40, asm.ABS_Y), # LDA $1E40,Y + asm.ORA(0xc0bafc, asm.LNG_X), # ORA $C0BAFC,X ; set this bit, meaning we have now opened this box + asm.STA(0x1e40, asm.ABS_Y), # STA $1E40,Y + asm.LDA(0x1f, asm.DIR), # LDA $1F + asm.BPL("NOT_GIL_TREASURE"), # BPL not_gil_treasure + asm.LDA(0x1a, asm.DIR), # LDA $1A ; load amount + asm.STA(0x4202, asm.ABS), # STA $4202 + asm.LDA(0x64, asm.IMM8), # LDA #$64 + asm.STA(0x4203, asm.ABS), # STA $4203 ; multiply it by 100 + asm.NOP(), # NOP + asm.NOP(), # NOP + asm.NOP(), # NOP + asm.NOP(), # NOP + asm.REP(0x21), # REP #$21 + asm.LDA(0x4216, asm.ABS), # LDA $4216 ; load product + asm.ADC(0x1860, asm.ABS), # ADC $1860 + asm.STA(0x1860, asm.ABS), # STA $1860 + asm.TDC(), # TDC + asm.A8(), # SEP #$20 + asm.BCC("GIL_NO_WRAP"), # BCC gil_no_wrap ; branch if result didn't wrap. meaning whatever we picked up didn't add to the third byte + asm.INC(0x1862, asm.ABS), # INC $1862 + + "GIL_NO_WRAP", # gil_no_wrap: + asm.LDA(0x7f, asm.IMM8), # LDA #$7F + asm.CMP(0x1860, asm.ABS), # CMP $1860 + asm.LDA(0x96, asm.IMM8), # LDA #$96 + asm.SBC(0x1861, asm.ABS), # SBC $1861 + asm.LDA(0x98, asm.IMM8), # LDA #$98 + asm.SBC(0x1862, asm.ABS), # SBC $1862 + asm.BCS("TREASURE_WRAPUP"), # BCS treasure_wrapup ; if carry is still set, we didn't overflow our GP. time to finish up + asm.LDX(0x967f, asm.IMM16), # LDX #$967F + asm.STX(0x1860, asm.ABS), # STX $1860 + asm.LDA(0x98, asm.IMM8), # LDA #$98 + asm.STA(0x1862, asm.ABS), # STA $1862 + asm.BRA("TREASURE_WRAPUP"), # BRA treasure_wrapup + + "NOT_GIL_TREASURE", # not_gil_treasure: + asm.BIT(0x40, asm.IMM8), # BIT #$40 ; item? + asm.BEQ("TREASURE_WRAPUP"), # BEQ treasure_wrapup ; if it isn't an item, it's an "Empty" or a MiaB, neither of which we need to do anything about here. not handling MiaB may be an oversight, but let's assume the end-user is smart enough not to remotely open one of those + asm.LDA(0x1a, asm.DIR), # LDA $1A + asm.JSR(0xacfc, asm.ABS), # JSR $ACFC ; add the item to inventory + + "TREASURE_WRAPUP", # treasure_wrapup: + asm.TDC(), # TDC + asm.LDA(0x05, asm.IMM8), # command size + asm.JMP(0x9b5c, asm.ABS), # next command + ] + + space = Write(Bank.C0, src, "custom loot_chest command") + address = space.start_address + + opcode = 0xec + _set_opcode_address(opcode, address) + + CollectTreasure.__init__ = lambda self, map_id, x, y : super().__init__(opcode, map_id.to_bytes(2, "little"), x, y) + self.__init__(map_id, x, y) + + def __str__(self): + return super().__str__(self.args) + +# Collect the contents (only if it hasn't been collected) +# If it has been collected, make jump to target destination +class BranchIfTreasureCollected(_Branch): + def __init__(self, chest_bit, destination): + src = [ + "CHECK_TREASURE", # check_treasure: + # ED xxxx aa bb cc + # xxxx is treasure bit. there's only #$2F treasure bytes + # aa bb cc is the event to jump to should a treasure already be open + asm.A16(), # REP #$20 + asm.LDA(0xeb, asm.DIR), # LDA $EB ; load our treasure byte/bit that we want to check + asm.PHA(), # PHA + asm.AND(0x0007, asm.IMM16), # AND #$0007 ; mask out the byte, keep the bits + asm.TAX(), # TAX + asm.PLA(), # PLA + asm.AND(0x01F8, asm.IMM16), # AND #$01F8 ; now mask out the bits, keep the byte + asm.LSR(), # LSR A + asm.LSR(), # LSR A + asm.LSR(), # LSR A + asm.TAY(), # TAY + asm.LDA(0x1e40, asm.ABS_Y), # LDA $1E40,Y ; load the treasure we are checking for + asm.AND(0xc0bafc, asm.ABS_X), # AND $C0BAFC,X ; is the bit we are checking for set? meaning, is this box already open? + asm.A8(), # SEP #$20 + asm.BNE("BRANCH_TO_DEST"), # BNE check_succeeds ; branch if so! + asm.BRA("RETURN"), + "BRANCH_TO_DEST", # check_succeeds: + asm.TDC(), # TDC + asm.LDX(0xed, asm.DIR), # LDX $ED load address + asm.STX(0xe5, asm.DIR), # STX $E5 store first 4 bytes + asm.LDA(0xef, asm.DIR), # LDA $EF st + asm.CLC(), # CLC + asm.ADC(0xca, asm.IMM8), # ADC #$CA + asm.STA(0xe7, asm.DIR), # STA $E7 + asm.JMP(0x9a6d, asm.ABS), # JMP $9A6D + "RETURN", + asm.TDC(), # TDC + asm.LDA(0x06, asm.IMM8), # LDA #$06 + asm.JMP(0x9b5c, asm.ABS), # JMP $9B5C - next command + ] + + space = Write(Bank.C0, src, "custom loot_treasure command") + address = space.start_address + + opcode = 0xed + _set_opcode_address(opcode, address) + + BranchIfTreasureCollected.__init__ = lambda self, chest_bit, destination : super().__init__(opcode, [chest_bit.to_bytes(2, "little")], destination) + + self.__init__(chest_bit, destination) + + def __str__(self): + return super().__str__(self.args) diff --git a/instruction/field/entity.py b/instruction/field/entity.py index f8888923..10618182 100644 --- a/instruction/field/entity.py +++ b/instruction/field/entity.py @@ -72,18 +72,62 @@ class AnimateAttacked(_Instruction): def __init__(self): super().__init__(0x0b) +class AnimateBattleStanding(_Instruction): + def __init__(self): + super().__init__(0x0c) + +class AnimateAirborneHandAtSide(_Instruction): + def __init__(self): + super().__init__(0x0d) +class AnimateAirborneHandInAir(_Instruction): + def __init__(self): + super().__init__(0x0e) + class AnimateHandsUp(_Instruction): def __init__(self): super().__init__(0x0f) +class AnimateCastingMouthClosed(_Instruction): + def __init__(self): + super().__init__(0x10) + +class AnimateCastingMouthOpen(_Instruction): + def __init__(self): + super().__init__(0x11) + class AnimateFrontHandsUp(_Instruction): def __init__(self): super().__init__(0x16) +class AnimateFacingUpHandsUp(_Instruction): + def __init__(self): + super().__init__(0x17) + +class AnimatePowerStance(_Instruction): + def __init__(self): + super().__init__(0x18) + + class AnimateFrontRightHandUp(_Instruction): def __init__(self): super().__init__(0x19) +class AnimateFrontRightHandOnHead(_Instruction): + def __init__(self): + super().__init__(0x1a) + +class AnimateBackRightHandOnHead(_Instruction): + def __init__(self): + super().__init__(0x1b) + +class AnimateBackRightHandUp(_Instruction): + def __init__(self): + super().__init__(0x1c) + +class AnimateHappy(_Instruction): + def __init__(self): + super().__init__(0x1d) + class AnimateSurprised(_Instruction): def __init__(self): super().__init__(0x1f) @@ -92,6 +136,34 @@ class AnimateStandingHeadDown(_Instruction): def __init__(self): super().__init__(0x20) +class AnimateFacingUpHeadDown(_Instruction): + def __init__(self): + super().__init__(0x21) + +class AnimateFacingLeftHeadDown(_Instruction): + def __init__(self): + super().__init__(0x22) + +class AnimateStandingLookingToSide(_Instruction): + def __init__(self): + super().__init__(0x23) + +class AnimateFingerWagToSide(_Instruction): + def __init__(self): + super().__init__(0x24) + +class AnimateFingerWagStraightUp(_Instruction): + def __init__(self): + super().__init__(0x25) + +class AnimateEmbracingSelf(_Instruction): + def __init__(self): + super().__init__(0x26) + +class AnimateTent(_Instruction): + def __init__(self): + super().__init__(0x27) + class AnimateKnockedOut(_Instruction): def __init__(self): super().__init__(0x28) diff --git a/objectives/conditions/boss.py b/objectives/conditions/boss.py index 8e465871..7a852dbf 100644 --- a/objectives/conditions/boss.py +++ b/objectives/conditions/boss.py @@ -1,14 +1,15 @@ from objectives.conditions._objective_condition import * from constants.objectives.condition_bits import boss_bit +from data.bosses import name_formation, name_pack class Condition(ObjectiveCondition): NAME = "Boss" def __init__(self, boss): self.boss = boss + self.boss_name = boss_bit[self.boss].name + self.boss_formation = name_formation[self.boss_name] + self.boss_pack = name_pack[self.boss_name] super().__init__(ConditionType.BattleBit, boss_bit[self.boss].bit) def __str__(self): - return super().__str__(self.boss) - - def boss_name(self): - return boss_bit[self.boss].name + return super().__str__(self.boss) \ No newline at end of file diff --git a/objectives/conditions/dragon.py b/objectives/conditions/dragon.py index 90c0ca84..37a36664 100644 --- a/objectives/conditions/dragon.py +++ b/objectives/conditions/dragon.py @@ -1,14 +1,16 @@ from objectives.conditions._objective_condition import * from constants.objectives.condition_bits import dragon_bit +from data.bosses import name_formation, name_pack class Condition(ObjectiveCondition): NAME = "Dragon" def __init__(self, dragon): self.dragon = dragon + self.dragon_name = dragon_bit[self.dragon].name + self.dragon_formation = name_formation[self.dragon_name] + self.dragon_pack = name_pack[self.dragon_name] super().__init__(ConditionType.BattleBit, dragon_bit[self.dragon].bit) def __str__(self): return super().__str__(self.dragon) - def dragon_name(self): - return dragon_bit[self.dragon].name diff --git a/objectives/results/unlock_kt_gauntlet.py b/objectives/results/unlock_kt_gauntlet.py new file mode 100644 index 00000000..768618e8 --- /dev/null +++ b/objectives/results/unlock_kt_gauntlet.py @@ -0,0 +1,19 @@ +from objectives.results._objective_result import * +import data.event_bit as event_bit + +class Field(field_result.Result): + def src(self): + return [ + field.SetEventBit(event_bit.UNLOCKED_KT_GAUNTLET), + ] + +class Battle(battle_result.Result): + def src(self): + return [ + battle_result.SetBit(event_bit.address(event_bit.UNLOCKED_KT_GAUNTLET), event_bit.UNLOCKED_KT_GAUNTLET), + ] + +class Result(ObjectiveResult): + NAME = "Unlock KT Gauntlet" + def __init__(self): + super().__init__(Field, Battle)