From 58051ac24b8baea73900d50983d033d07c78a7f3 Mon Sep 17 00:00:00 2001 From: Kiel <95580337+kielbasiago@users.noreply.github.com> Date: Wed, 26 Oct 2022 21:12:24 -0400 Subject: [PATCH 1/2] Feature: GAUNTLET --- constants/maps.py | 12 + constants/objectives/condition_bits.py | 3 +- constants/objectives/results.py | 2 + constants/songs.py | 5 + constants/sound_effects.py | 11 + data/dialogs/dialogs.py | 10 +- data/dialogs/free.py | 6 + data/enemy_packs.py | 12 + data/event_bit.py | 2 + data/items.py | 2 + data/map_property.py | 15 +- data/maps.py | 40 ++ data/npc_bit.py | 4 +- event/airship.py | 36 ++ event/kefka_tower.py | 664 ++++++++++++++++++++++- instruction/field/custom.py | 121 ++++- instruction/field/entity.py | 72 +++ objectives/results/unlock_kt_gauntlet.py | 19 + 18 files changed, 1006 insertions(+), 30 deletions(-) create mode 100644 constants/maps.py create mode 100644 constants/songs.py create mode 100644 constants/sound_effects.py create mode 100644 objectives/results/unlock_kt_gauntlet.py 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 095c7223..ec90ef74 100644 --- a/constants/objectives/condition_bits.py +++ b/constants/objectives/condition_bits.py @@ -87,7 +87,8 @@ 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("Reunite Gau and Father", event_bit.GAU_FATHER_REUNION), + NameBit("Reunite Gau and Father", event_bit.GAU_FATHER_REUNION), # 10 + NameBit("Complete the KT Gauntlet", event_bit.COMPLETED_KT_GAUNTLET) # 11 ] boss_bit = [] diff --git a/constants/objectives/results.py b/constants/objectives/results.py index d09bf6b6..6c0d0336 100644 --- a/constants/objectives/results.py +++ b/constants/objectives/results.py @@ -89,6 +89,8 @@ category_types["Command"].append(ResultType(59, "Magitek Upgrade", "Magitek Upgrade", None)) category_types["Item"].append(ResultType(60, "Sprint Shoes", "Sprint Shoes", None)) category_types["Kefka's Tower"] += [ResultType(61, "Unlock Perma KT Skip", "Unlock Perma KT Skip", None)] +category_types["Kefka's Tower"] += [ResultType(62, "Unlock KT Gauntlet", "Unlock KT Gauntlet", None)] + categories = list(category_types.keys()) 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/dialogs/dialogs.py b/data/dialogs/dialogs.py index 3cd1f4b8..3951b5a8 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_packs.py b/data/enemy_packs.py index ba17953d..0178afe8 100644 --- a/data/enemy_packs.py +++ b/data/enemy_packs.py @@ -240,6 +240,18 @@ def randomize_packs(self, packs, boss_percent, no_phunbaba3 = False): if self.args.dragon_boss_location != bosses.BossLocations.MIX: exclude_bosses += self.formations.ALL_DRAGONS + # 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: diff --git a/data/event_bit.py b/data/event_bit.py index 5b45a35a..5235500e 100644 --- a/data/event_bit.py +++ b/data/event_bit.py @@ -216,6 +216,8 @@ # 3 bits 0x1c7-0x1c9 Unused # 3 bits 0x2c1-0x2c3 Unused UNLOCKED_PERMA_KT_SKIP = 0x2c1 +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 from constants.objectives import MAX_OBJECTIVES diff --git a/data/items.py b/data/items.py index a03afa2f..f00dd309 100644 --- a/data/items.py +++ b/data/items.py @@ -339,6 +339,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..3a6855f2 100644 --- a/data/map_property.py +++ b/data/map_property.py @@ -1,3 +1,7 @@ + +ENABLE_WARP = 0x02 +ENABLE_RANDOM_ENCOUNTER = 0x80 + class MapProperty: DATA_SIZE = 33 DATA_START = 0x2d8f00 @@ -16,12 +20,21 @@ def read(self): self.data = self.rom.get_bytes(self.data_start, self.DATA_SIZE) self.name_index = self.data[0] - self.enable_random_encounters = (self.data[5] & 0x80) >> 7 + self.enable_warp = (self.data[1] & ENABLE_WARP) >> 1 + self.enable_random_encounters = (self.data[5] & ENABLE_RANDOM_ENCOUNTER) >> 7 self.song = self.data[28] def write(self): + self.data[1] &= self.enable_warp << 1 + self.data[5] &= 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 064797a1..16380a1d 100644 --- a/data/maps.py +++ b/data/maps.py @@ -109,6 +109,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 @@ -158,6 +161,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 @@ -235,3 +244,34 @@ 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) + + def add_save_point(self, map_id, x, y): + # npc for visual + npc = NPC() + npc.background_layer = 0 + npc.background_scrolls = 0 + npc.const_sprite = 1 + npc.direction = 3 + npc.event_bit = 3 + npc.event_byte = 102 + npc.map_layer = 1 + npc.movement = 0 + npc.no_face_on_trigger = 0 + npc.palette = 6 + npc.speed = 2 + npc.split_sprite = 1 + npc.sprite = 111 + npc.unknown1 = 0 + npc.unknown2 = 0 + npc.vehicle = 0 + npc.x = x + npc.y = y + + # event for save functionality + event = MapEvent() + event.set_event_address(0xc9aeb) # save script + event.x = x + event.y = y + + self.append_npc(map_id, npc) + self.add_event(map_id, event) \ 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..67fe0b51 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): @@ -19,6 +20,41 @@ def mod(self): self.unequip_party_members_npc_mod() self.inside_blackjack() self.return_to_airship() + if self.args.debug: + self.chest_test_mod() + + def chest_test_mod(self): + narshe_school_right_room = 107 # MIAB in chest 0, Tincture in chest 1 + miab_chest = self.maps.get_chests(narshe_school_right_room)[0] + tincture_chest = self.maps.get_chests(narshe_school_right_room)[1] + + chest_src = [ + field.CollectChest(narshe_school_right_room, miab_chest.x, miab_chest.y), + ] + miab = Write(Bank.CA, chest_src, "Trigger treasure chest") + + # Terra will loot the MIAB (currently does nothing, safely exits) + new_npc = NPC() + new_npc.x = 15 + new_npc.y = 7 + new_npc.sprite = 0 + new_npc.direction = direction.DOWN + new_npc.set_event_address(miab.start_address) + self.maps.append_npc(6, new_npc) + + # Kefka will loot the Tincture + chest_src = [ + field.CollectChest(narshe_school_right_room, tincture_chest.x, tincture_chest.y), + ] + tincture = Write(Bank.CA, chest_src, "Trigger treasure chest") + new_npc = NPC() + new_npc.x = 17 + new_npc.y = 7 + new_npc.sprite = 21 + new_npc.direction = direction.DOWN + new_npc.set_event_address(tincture.start_address) + self.maps.append_npc(6, new_npc) + def controls_mod(self): fly_wor_fc_cancel_dialog = 1315 diff --git a/event/kefka_tower.py b/event/kefka_tower.py index 28cea2cf..e75e714b 100644 --- a/event/kefka_tower.py +++ b/event/kefka_tower.py @@ -1,6 +1,30 @@ +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 +34,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 +45,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() @@ -42,7 +67,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() @@ -101,6 +126,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(), @@ -118,6 +144,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(), @@ -127,34 +155,134 @@ 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, party, original_pack_name, battle_sound = False): + 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): + # return the boss in place of the given boss_name + # e.g. bosses are shuffled, if Ultros is in Goddess spot, return Ultros + def get_replacement_formation(boss_name): + from data.bosses import pack_name + replacement = self.get_boss(boss_name, False) + location_boss = pack_name[replacement] + formation_id = self.enemies.formations.get_id(location_boss) + return self.enemies.formations.formations[formation_id] + + # If encounters are random, it could be a tell when a fight loses its music/victory dance + def disable_victory_dance(original_encounter_name): + formation = get_replacement_formation(original_encounter_name) + formation.disable_victory_dance = formation.disable_victory_dance if self.args.boss_battles_random else 1 + + def disable_battle_music(original_encounter_name): + formation = get_replacement_formation(original_encounter_name) + formation.disable_battle_music = formation.disable_battle_music if self.args.boss_battles_random else 1 + + def disable_all(boss_name): + disable_victory_dance(boss_name) + disable_battle_music(boss_name) + + disable_all("Inferno") + disable_all("Guardian") + disable_all("Doom") + disable_all("Goddess") + disable_all("Poltrgeist") + + self.inferno_cutscene = self.gauntlet_inferno_cutscene() + self.guardian_cutscene = self.gauntlet_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.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), + "GUARDIAN", + field.BranchIfEventBitSet(event_bit.DEFEATED_GUARDIAN, "DOOM"), + field.Call(self.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), + 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_PERMA_KT_SKIP, "LANDING_MENU"), - 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 @@ -171,11 +299,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()) @@ -256,6 +397,7 @@ def kt_encounter_objective_mod(self, boss_name, bit, start_target, end_target, d src += [ field.SetEventBit(bit), field.CheckObjectives(), + field.FreeScreen(), field.Return(), ] post_battle = Write(Bank['CC'], src, f"{boss_name} post-battle. 1) Set event bit. 2) Finish check") @@ -266,6 +408,7 @@ def kt_encounter_objective_mod(self, boss_name, bit, start_target, end_target, d ]) def guardian_mod(self): + self.rom.set_bytes(0xc186c, [asm.NOP(), asm.NOP()]) self.kt_encounter_objective_mod( "Guardian", event_bit.DEFEATED_GUARDIAN, @@ -275,6 +418,14 @@ def guardian_mod(self): ) def inferno_mod(self): + # CC/18A2 - Wait 15 frames + self.rom.set_byte(0xc18a2, 0xea) + + # IS THIS NEEDED? + # CC/18AE - Fade in + # CC/18AF - Wait for fade + self.rom.set_bytes(0xc18ae, [0xea, 0xea]) + self.kt_encounter_objective_mod( "Inferno", event_bit.DEFEATED_INFERNO, @@ -329,12 +480,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.StartSong(song_name_id['FierceBattle']), + self.invoke_kt_battle(3, 'Inferno', True), + field.Return(), + ] + boss_space = Write(Bank.CC, 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 @@ -415,10 +570,10 @@ def goddess_battle_mod(self): field.InvokeBattle(boss_pack_id), ) - def poltrgeist_battle_mod(self): + def poltergeist_battle_mod(self): boss_pack_id = self.get_boss("Poltrgeist") - 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), ) @@ -506,6 +661,18 @@ 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.SetEventBit(event_bit.COMPLETED_KT_GAUNTLET), + 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 @@ -515,3 +682,454 @@ 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 = [ + change_party(3), + field.LoadMap(inferno_room_id, direction.DOWN, default_music = False, + x = 27, y = 18, fade_in = False, entrance_event = True), + field.HoldScreen(), + + # 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.Return(), + ] + + space = Write(Bank.F0, src, "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 party positions + [ + change_party(1), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetSpeed(field_entity.Speed.NORMAL), + field_entity.SetPosition(6, 15), + field_entity.Turn(direction.UP), + ), + change_party(2), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.SetPosition(12, 17), + field_entity.Turn(direction.UP), + ), + + change_party(3), + field.EntityAct(field_entity.PARTY0, True, + field_entity.SetSpeed(field_entity.Speed.SLOW), + field_entity.SetPosition(18, 15), + field_entity.Turn(direction.UP), + ), + + # Move Guardian back 1 cell + field.EntityAct(0x10, False, + field_entity.SetPosition(11, 9) + ), + field.EntityAct(0x13, True, + field_entity.SetPosition(12, 9) + ), + field.EntityAct(0x16, True, + field_entity.SetPosition(13, 9) + ), + ], + field.Pause(0.25), + # Camera position + field.EntityAct(field_entity.CAMERA, False, + field_entity.SetSpeed(field_entity.Speed.SLOW), + field_entity.Move(direction.UP, 2), + ), + # Party 1 picking magitek flowers + [ + change_party(1), + 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) + ), + ], + # Party 3 walking around + [ + change_party(3), + field.EntityAct(field_entity.PARTY0, False, + field_entity.SetSpeed(field_entity.Speed.SLOW), + field_entity.SetPosition(18, 14), + 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), + # Party 2, the initiator + [ + change_party(2), + + field.Pause(2), + # getting in position + field.EntityAct(field_entity.PARTY0, True, + 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(5), + field_entity.AnimateKnockedOut(), + field_entity.Pause(13), + field_entity.AnimateKneeling(), + field_entity.Pause(10), + field_entity.AnimateStandingHeadDown(), + field_entity.Pause(4), + field_entity.AnimateStandingFront(), + field_entity.Pause(4), + field_entity.AnimatePowerStance(), + field_entity.AnimateLowJump(), + field_entity.Pause(6), + 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 + + + def gauntlet_doom_cutscene(self): + src = [ + 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 = [ + 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 = [ + 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.EntityAct(field_entity.PARTY0, False, + 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_entity.Pause(10), + 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, 3), + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.Move(direction.UP, 6), + ), + field.Pause(2), + [ + field.Dialog(self.items.add_receive_dialog(chest.contents), wait_for_input=False), + field.CollectChest(poltergeist_room_id, chest.x, chest.y), + field.PlaySoundEffect(sfx_name_id.get('Chest/Switch')), + ], + field.Pause(2), + field.Pause(1.5), + + 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 + [ + 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 + [ + 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 + [ + 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..a2dd56cd 100644 --- a/instruction/field/custom.py +++ b/instruction/field/custom.py @@ -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,122 @@ 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 CollectChest(_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_wrapup: + 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) + + CollectChest.__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) 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/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) From 21d967c01e9ad83585d2b0978179a23f7102380b Mon Sep 17 00:00:00 2001 From: Kiel <95580337+kielbasiago@users.noreply.github.com> Date: Wed, 26 Oct 2022 22:10:06 -0400 Subject: [PATCH 2/2] Add back guardian/inferno fade, but only when gauntlet not in progress --- data/event_bit.py | 1 + data/maps.py | 32 +------------------------------- event/airship.py | 35 ----------------------------------- event/kefka_tower.py | 32 +++++++++++++++++++++++++++----- 4 files changed, 29 insertions(+), 71 deletions(-) diff --git a/data/event_bit.py b/data/event_bit.py index 5235500e..22f29611 100644 --- a/data/event_bit.py +++ b/data/event_bit.py @@ -219,6 +219,7 @@ 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): diff --git a/data/maps.py b/data/maps.py index 16380a1d..32c7b807 100644 --- a/data/maps.py +++ b/data/maps.py @@ -244,34 +244,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) - - def add_save_point(self, map_id, x, y): - # npc for visual - npc = NPC() - npc.background_layer = 0 - npc.background_scrolls = 0 - npc.const_sprite = 1 - npc.direction = 3 - npc.event_bit = 3 - npc.event_byte = 102 - npc.map_layer = 1 - npc.movement = 0 - npc.no_face_on_trigger = 0 - npc.palette = 6 - npc.speed = 2 - npc.split_sprite = 1 - npc.sprite = 111 - npc.unknown1 = 0 - npc.unknown2 = 0 - npc.vehicle = 0 - npc.x = x - npc.y = y - - # event for save functionality - event = MapEvent() - event.set_event_address(0xc9aeb) # save script - event.x = x - event.y = y - - self.append_npc(map_id, npc) - self.add_event(map_id, event) \ No newline at end of file + \ No newline at end of file diff --git a/event/airship.py b/event/airship.py index 67fe0b51..9d51836f 100644 --- a/event/airship.py +++ b/event/airship.py @@ -20,41 +20,6 @@ def mod(self): self.unequip_party_members_npc_mod() self.inside_blackjack() self.return_to_airship() - if self.args.debug: - self.chest_test_mod() - - def chest_test_mod(self): - narshe_school_right_room = 107 # MIAB in chest 0, Tincture in chest 1 - miab_chest = self.maps.get_chests(narshe_school_right_room)[0] - tincture_chest = self.maps.get_chests(narshe_school_right_room)[1] - - chest_src = [ - field.CollectChest(narshe_school_right_room, miab_chest.x, miab_chest.y), - ] - miab = Write(Bank.CA, chest_src, "Trigger treasure chest") - - # Terra will loot the MIAB (currently does nothing, safely exits) - new_npc = NPC() - new_npc.x = 15 - new_npc.y = 7 - new_npc.sprite = 0 - new_npc.direction = direction.DOWN - new_npc.set_event_address(miab.start_address) - self.maps.append_npc(6, new_npc) - - # Kefka will loot the Tincture - chest_src = [ - field.CollectChest(narshe_school_right_room, tincture_chest.x, tincture_chest.y), - ] - tincture = Write(Bank.CA, chest_src, "Trigger treasure chest") - new_npc = NPC() - new_npc.x = 17 - new_npc.y = 7 - new_npc.sprite = 21 - new_npc.direction = direction.DOWN - new_npc.set_event_address(tincture.start_address) - self.maps.append_npc(6, new_npc) - def controls_mod(self): fly_wor_fc_cancel_dialog = 1315 diff --git a/event/kefka_tower.py b/event/kefka_tower.py index e75e714b..21f100aa 100644 --- a/event/kefka_tower.py +++ b/event/kefka_tower.py @@ -225,6 +225,7 @@ def disable_all(boss_name): debug_event_bits, + 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), @@ -260,6 +261,8 @@ def disable_all(boss_name): field.Call(self.poltergeist_cutscene), "POST_GAUNTLET", field.Call(self.post_gauntlet_cutscene), + field.ClearEventBit(event_bit.GAUNTLET_IN_PROGRESS), + field.SetEventBit(event_bit.COMPLETED_KT_GAUNTLET), self.post_landing_src(final_switch_map_id, 103, 45), ] @@ -281,6 +284,7 @@ def entrance_landing_mod(self): 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_PERMA_KT_SKIP, "STATUE_MENU_EVAL"), field.BranchIfEventBitSet(event_bit.UNLOCKED_KT_SKIP, "STATUE_MENU_EVAL"), field.BranchIfEventBitSet(event_bit.UNLOCKED_KT_GAUNTLET, "GAUNTLET_DIALOG"), @@ -392,9 +396,23 @@ def atma_battle_mod(self): # 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 = Read(start_target, end_target) + def kt_encounter_objective_mod(self, boss_name, bit, start_target, end_target, description, trigger_fade_in = False): + + src = [] + # Guardian/Inferno remove their fade outs for two reasons: + # 1) We need to make a map load before the fade in for the gauntlet after a battle completes + # 2) We need to make room to check for objective and set proper bit + # So we re-add the fade in, but only when the gauntlet isn't happening + if trigger_fade_in: + src += [ + field.BranchIfEventBitSet(event_bit.GAUNTLET_IN_PROGRESS, "FINISH_OBJECTIVE"), + field.FadeInScreen(), + field.WaitForFade() + ] + src += Read(start_target, end_target) + src += [ + "FINISH_OBJECTIVE", field.SetEventBit(bit), field.CheckObjectives(), field.FreeScreen(), @@ -408,6 +426,9 @@ def kt_encounter_objective_mod(self, boss_name, bit, start_target, end_target, d ]) def guardian_mod(self): + # Clear fade out, will manually trigger this in kt_encounter_objective_mod + # CC/186C Fade in + # CC/186D Wait for fade self.rom.set_bytes(0xc186c, [asm.NOP(), asm.NOP()]) self.kt_encounter_objective_mod( "Guardian", @@ -415,13 +436,14 @@ def guardian_mod(self): 0xc186c, 0xc186f, "Guardian battle post-script, wait for fade, set bit", + trigger_fade_in= True ) def inferno_mod(self): # CC/18A2 - Wait 15 frames self.rom.set_byte(0xc18a2, 0xea) - - # IS THIS NEEDED? + + # Clear fade out, will manually trigger this in kt_encounter_objective_mod # CC/18AE - Fade in # CC/18AF - Wait for fade self.rom.set_bytes(0xc18ae, [0xea, 0xea]) @@ -432,6 +454,7 @@ def inferno_mod(self): 0xc18ae, 0xc18b1, "Inferno battle post-script, fade in, wait, set bit", + trigger_fade_in = True ) def doom_mod(self): @@ -667,7 +690,6 @@ def post_landing_src(self, map_id, map_x, map_y): 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.SetEventBit(event_bit.COMPLETED_KT_GAUNTLET), field.CheckObjectives(), field.FreeScreen(), Read(0xa03b0, 0xa03b9),