diff --git a/args/advanced_checks.py b/args/advanced_checks.py new file mode 100644 index 00000000..d1016ebd --- /dev/null +++ b/args/advanced_checks.py @@ -0,0 +1,174 @@ + +from event.event_reward import RewardType + + +def name(): + return "Check Rewards" + +def parse(parser): + from constants.check_presets import preset_keys + advanced_checks = parser.add_argument_group("Check Rewards") + + presets = advanced_checks.add_mutually_exclusive_group() + presets.name = "Check Presets" + + presets.add_argument('-checks', "--check-preset", type = str, + choices = preset_keys, help = "A preset used to modify certain checks to rewards characters, espers, or items.") + + presets.add_argument("-nfce", "--no-free-characters-espers", action = "store_true", + help = "Remove character/esper rewards from: Auction House, Collapsing House, Figaro Castle Throne, Gau's Father's House, Kohlingen Inn, Mt. Zozo, Narshe Weapon Shop, Sealed Gate, South Figaro Basement, Tzen Thief, Zone Eater") + + advanced_checks.add_argument("-firr", "--force-item-rewards", type = str, + help = "Forces list of checks to give an ITEM reward") + + advanced_checks.add_argument("-ferr", "--force-esper-rewards", type = str, + help = "Forces list of checks to give an ESPER reward") + + advanced_checks.add_argument("-feirr", "--force-esper-item-rewards", type = str, + help = "Forces list of checks to give an (ESPER | ITEM) reward") + + advanced_checks.add_argument("-fcrr", "--force-character-rewards", type = str, + help = "Forces list of checks to give an CHARACTER reward") + + advanced_checks.add_argument("-dchar", "--dragons-as-characters", default = [0, 0], type = int, + nargs = 2, metavar = ("MIN", "MAX"), choices = range(6), + help = "Up to 5 dragons are guranteed to reward characters. The dragon will have the recruited character's sprite. Kefka's Tower and Phoenix Cave dragons cannot be characters.") + +character_title = "Character Checks" +esper_item_title = "Esper+Item Checks" +esper_title = "Esper Checks" +item_title = "Item Checks" + +def process(args): + from constants.check_presets import key_preset, NO_FREE_CHARACTERS_ESPERS + args.character_rewards = [] + args.esper_item_rewards = [] + args.esper_rewards = [] + args.item_rewards = [] + args._process_min_max("dragons_as_characters") + + if args.no_free_characters_espers: + args.check_preset = NO_FREE_CHARACTERS_ESPERS.key + + if args.check_preset: + check_preset = key_preset[args.check_preset] + bits = [int(check.bit) for check in check_preset.locations] + if check_preset.reward == RewardType.CHARACTER: + args.character_rewards = bits + if check_preset.reward == (RewardType.ESPER | RewardType.ITEM): + args.esper_item_rewards = bits + if check_preset.reward == RewardType.ESPER: + args.esper_rewards = bits + if check_preset.reward == RewardType.ITEM: + args.item_rewards = bits + else: + if args.force_character_rewards: + args.character_rewards = [int(check) for check in args.force_character_rewards.split(',')] + + if args.force_esper_item_rewards: + args.esper_item_rewards = [int(check) for check in args.force_esper_item_rewards.split(',')] + + if args.force_esper_rewards: + args.esper_rewards = [int(check) for check in args.force_esper_rewards.split(',')] + + if args.force_item_rewards: + args.item_rewards = [int(check) for check in args.force_item_rewards.split(',')] + + +def flags(args): + flags = "" + + if args.check_preset: + flags += f" -checks {args.check_preset}" + + if args.force_character_rewards: + flags += f" -fcrr {args.force_character_rewards}" + + if args.force_esper_item_rewards: + flags += f" -feirr {args.force_esper_item_rewards}" + + if args.force_esper_rewards: + flags += f" -ferr {args.force_esper_rewards}" + + if args.force_item_rewards: + flags += f" -firr {args.force_item_rewards}" + + if args.dragons_as_characters_min != 0 or args.dragons_as_characters_max != 0: + flags += f" -dchar {args.dragons_as_characters_min} {args.dragons_as_characters_max}" + + return flags + +def options(args): + opts = {} + if args.character_rewards: + opts[character_title] = args.character_rewards + if args.esper_item_rewards: + opts[esper_item_title] = args.esper_item_rewards + if args.esper_rewards: + opts[esper_title] = args.esper_rewards + if args.item_rewards: + opts[item_title] = args.item_rewards + + if args.dragons_as_characters: + opts['Dragon Characters'] = f"{args.dragons_as_characters_min}-{args.dragons_as_characters_max}" + + return [(key, value) for (key, value) in opts.items()] + +def _format_check_log_entries(check_ids): + from constants.checks import check_name + check_entries = [] + for check_id in check_ids: + check_entries.append(("", check_name[check_id])) + return check_entries + +def menu(args): + from menus.submenu_force_item_reward_checks import FlagsForceCharacterRewardChecks, FlagsForceEsperItemRewardChecks, FlagsForceEsperRewardChecks, FlagsForceItemRewardChecks + + entries = options(args) + for index, entry in enumerate(entries): + key, value = entry + if key == character_title: + if value: + entries[index] = (character_title, FlagsForceCharacterRewardChecks(character_title, value, args.check_preset)) # flags sub-menu + else: + entries[index] = (character_title, "None") + + if key == esper_item_title: + if value: + entries[index] = (esper_item_title, FlagsForceEsperItemRewardChecks(esper_item_title, value, args.check_preset)) # flags sub-menu + else: + entries[index] = (esper_item_title, "None") + + if key == esper_title: + if value: + entries[index] = (esper_title, FlagsForceEsperRewardChecks(esper_title, value, args.check_preset)) # flags sub-menu + else: + entries[index] = (esper_title, "None") + + if key == item_title: + if value: + entries[index] = (item_title, FlagsForceItemRewardChecks(item_title, value, args.check_preset)) # flags sub-menu + else: + entries[index] = (item_title, "None") + + return (name(), entries) + +def log(args): + from log import format_option + log = [name()] + + entries = options(args) + for entry in entries: + key, value = entry + if key == character_title or key == esper_item_title or key == esper_title or key == item_title: + if len(value) == 0: + entry = (key, "None") + else: + entry = (key, "") # The entries will show up on subsequent lines + log.append(format_option(*entry)) + for check_entry in _format_check_log_entries(value): + log.append(format_option(*check_entry)) + else: + log.append(format_option(*entry)) + + return log diff --git a/args/arguments.py b/args/arguments.py index 155e985a..21ee78f4 100644 --- a/args/arguments.py +++ b/args/arguments.py @@ -4,12 +4,13 @@ def __init__(self): self.groups = [ "settings", "objectives", - "starting_party", "characters", "swdtechs", "blitzes", "lores", "rages", "dances", "steal", "commands", + "starting_party", "characters", "swdtechs", "blitzes", "lores", "rages", "dances", "steal", "commands", "xpmpgp", "scaling", "bosses", "encounters", "boss_ai", "espers", "natural_magic", "starting_gold_items", "items", "shops", "chests", "graphics", "coliseum", "auction_house", "challenges", "bug_fixes", "misc", + "advanced_checks", ] self.group_modules = {} for group in self.groups: diff --git a/args/challenges.py b/args/challenges.py index 8ebbbc9f..aa608374 100644 --- a/args/challenges.py +++ b/args/challenges.py @@ -1,3 +1,6 @@ +from constants.checks import FIGARO_CASTLE_THRONE, KOHLINGEN_CAFE +from data.npc_bit import WEAPON_ELDER_NARSHE + def name(): return "Challenges" @@ -13,8 +16,6 @@ def parse(parser): help = "Ultima cannot be learned from espers/items/natural magic") challenges.add_argument("-nfps", "--no-free-paladin-shields", action = "store_true", help = "Paladin/Cursed Shields will not appear in coliseum/auction/shops/chests/events (Narshe WOR exclusive)") - challenges.add_argument("-nfce", "--no-free-characters-espers", action = "store_true", - help = "Remove character/esper rewards from: Auction House, Collapsing House, Figaro Castle Throne, Gau's Father's House, Kohlingen Inn, Narshe Weapon Shop, Sealed Gate, South Figaro Basement") challenges.add_argument("-pd", "--permadeath", action = "store_true", help = "Life spells cannot be learned. Fenix Downs unavailable (except from starting items). Buckets/inns/tents/events do not revive characters. Phoenix casts Life 3 on party instead of Life") @@ -34,8 +35,6 @@ def flags(args): flags += " -nu" if args.no_free_paladin_shields: flags += " -nfps" - if args.no_free_characters_espers: - flags += " -nfce" if args.permadeath: flags += " -pd" @@ -48,7 +47,6 @@ def options(args): ("No Illuminas", args.no_illuminas), ("No Ultima", args.no_ultima), ("No Free Paladin Shields", args.no_free_paladin_shields), - ("No Free Characters/Espers", args.no_free_characters_espers), ("Permadeath", args.permadeath), ] diff --git a/args/log.py b/args/log.py index 0160ae56..ddd1d63f 100644 --- a/args/log.py +++ b/args/log.py @@ -22,4 +22,5 @@ def log(): _log_tab("Magic", ["espers"], ["natural_magic"]) _log_tab("Items", ["starting_gold_items", "items"], ["shops", "chests"]) args.group_modules["graphics"].log(args) - _log_tab("Other", ["coliseum", "auction_house", "misc"], ["challenges", "bug_fixes"]) + _log_tab("Other", ["coliseum", "misc", "challenges"], ["auction_house", "bug_fixes"]) + _log_tab("Advanced", ["advanced_checks"], []) diff --git a/constants/check_presets.py b/constants/check_presets.py new file mode 100644 index 00000000..01ae0603 --- /dev/null +++ b/constants/check_presets.py @@ -0,0 +1,61 @@ +from constants.checks import * + +from collections import namedtuple +CheckPreset = namedtuple("CheckPreset", ["key", "name", "reward", "description", "locations"]) + +AH_CLOSED = CheckPreset( + "ah", + "Auction House is Closed", + RewardType.ITEM, + "The auction house will only have items available.", + [ + AUCTION1, + AUCTION2 + ] +) + +NO_FREE_CHARACTERS = CheckPreset( + 'nfchar', + "No Free Characters", + RewardType.ESPER | RewardType.ITEM, + "All free checks that can reward characters are guaranteed to reward an ESPER or ITEM", + [ + COLLAPSING_HOUSE, + FIGARO_CASTLE_THRONE, + GAUS_FATHERS_HOUSE, + KOHLINGEN_CAFE, + MT_ZOZO, + SEALED_GATE, + SOUTH_FIGARO_PRISONER, + ] +) + +NO_FREE_CHARACTERS_ESPERS = CheckPreset( + 'nfce', + "No Free C+E", + RewardType.ITEM, + "All free checks are guaranteed to reward an ITEM. Includes Auction House and Tzen Thief.", + [ + AUCTION1, + AUCTION2, + COLLAPSING_HOUSE, + FIGARO_CASTLE_THRONE, + GAUS_FATHERS_HOUSE, + KOHLINGEN_CAFE, + MT_ZOZO, + NARSHE_WEAPON_SHOP, + NARSHE_WEAPON_SHOP_MINES, + SEALED_GATE, + SOUTH_FIGARO_PRISONER, + TZEN_THIEF, + ] +) + +all_presets = [ + AH_CLOSED, + NO_FREE_CHARACTERS, + NO_FREE_CHARACTERS_ESPERS, +] + +preset_keys = [preset.key for preset in all_presets] +key_preset = {preset.key: preset for (idx, preset) in enumerate(all_presets)} diff --git a/constants/checks.py b/constants/checks.py new file mode 100644 index 00000000..349e4615 --- /dev/null +++ b/constants/checks.py @@ -0,0 +1,165 @@ +from collections import namedtuple +from data.characters import Characters +import data.event_bit as event_bit +import data.npc_bit as npc_bit +from event.event_reward import RewardType + +NameBit = namedtuple("NameBit", ["name", "bit", "reward_types", "gate_character"]) +CHAR_ESPER_REWARD = RewardType.CHARACTER | RewardType.ESPER +ANY_REWARD = RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM +ESPER_ITEM_REWARD = RewardType.ESPER | RewardType.ITEM +ITEM_REWARD = RewardType.ITEM + + +from constants.entities import ( + TERRA, LOCKE, CYAN, SHADOW, + EDGAR, SABIN, CELES, STRAGO, + RELM, SETZER, MOG, GAU, GOGO, + UMARO +) + +AUCTION1 = NameBit("Auction1", event_bit.AUCTION_BOUGHT_ESPER1, ESPER_ITEM_REWARD, None) +AUCTION2 = NameBit("Auction2", event_bit.AUCTION_BOUGHT_ESPER2, ESPER_ITEM_REWARD, None) +ANCIENT_CASTLE = NameBit("Ancient Castle", event_bit.GOT_RAIDEN, ANY_REWARD, EDGAR) +BAREN_FALLS = NameBit("Baren Falls", event_bit.NAMED_GAU, RewardType.CHARACTER, SABIN) +BURNING_HOUSE = NameBit("Burning House", event_bit.DEFEATED_FLAME_EATER, ANY_REWARD, STRAGO) +COLLAPSING_HOUSE = NameBit("Collapsing House", event_bit.FINISHED_COLLAPSING_HOUSE, ANY_REWARD, SABIN) +DARYLS_TOMB = NameBit("Daryl's Tomb", event_bit.DEFEATED_DULLAHAN, ANY_REWARD, SETZER) +DOMA_SIEGE = NameBit("Doma Siege", event_bit.FINISHED_DOMA_WOB, ANY_REWARD, CYAN) +DOMA_DREAM_DOOR = NameBit("Doma Dream Door", event_bit.DEFEATED_STOOGES, ESPER_ITEM_REWARD, CYAN) +DOMA_DREAM_AWAKEN = NameBit("Doma Dream Awaken", event_bit.FINISHED_DOMA_WOR, CHAR_ESPER_REWARD, CYAN) +DOMA_DREAM_THRONE = NameBit("Doma Dream Throne", event_bit.GOT_ALEXANDR, ESPER_ITEM_REWARD, CYAN) +EBOTS_ROCK = NameBit("Ebot's Rock", event_bit.DEFEATED_HIDON, ANY_REWARD, STRAGO) +ESPER_MOUNTAIN = NameBit("Esper Mountain", event_bit.DEFEATED_ULTROS_ESPER_MOUNTAIN, ANY_REWARD, RELM) +FANATICS_TOWER_LEADER = NameBit("Fanatic's Tower Leader", event_bit.DEFEATED_MAGIMASTER, ESPER_ITEM_REWARD, STRAGO) +FANATICS_TOWER_FOLLOWER = NameBit("Fanatic's Tower Follower", event_bit.RECRUITED_STRAGO_FANATICS_TOWER, CHAR_ESPER_REWARD, STRAGO) +FIGARO_CASTLE_THRONE = NameBit("Figaro Castle Throne", event_bit.NAMED_EDGAR, ANY_REWARD, EDGAR) +FIGARO_CASTLE_ENGINE = NameBit("Figaro Castle Engine", event_bit.DEFEATED_TENTACLES_FIGARO, ANY_REWARD, EDGAR) +FLOATING_CONT_ARRIVE = NameBit("Floating Cont. Arrive", event_bit.RECRUITED_SHADOW_FLOATING_CONTINENT, CHAR_ESPER_REWARD, SHADOW) +FLOATING_CONT_BEAST = NameBit("Floating Cont. Beast", event_bit.DEFEATED_ATMAWEAPON, ESPER_ITEM_REWARD, SHADOW) +FLOATING_CONT_ESCAPE = NameBit("Floating Cont. Escape", event_bit.FINISHED_FLOATING_CONTINENT, CHAR_ESPER_REWARD, SHADOW) +GAUS_FATHERS_HOUSE = NameBit("Gau's Father's House", event_bit.RECRUITED_SHADOW_GAU_FATHER_HOUSE, ANY_REWARD, SHADOW) +IMPERIAL_CAMP = NameBit("Imperial Camp", event_bit.FINISHED_IMPERIAL_CAMP, ANY_REWARD, SABIN) +KEFKAS_TOWER_CELL_BEAST = NameBit("Kefka's Tower Cell Beast", event_bit.DEFEATED_ATMA, ITEM_REWARD, None) +KOHLINGEN_CAFE = NameBit("Kohlingen Cafe", event_bit.RECRUITED_SHADOW_KOHLINGEN, ANY_REWARD, SETZER) +LETE_RIVER = NameBit("Lete River", event_bit.RODE_RAFT_LETE_RIVER, ANY_REWARD, TERRA) +LONE_WOLF_CHASE = NameBit("Lone Wolf Chase", event_bit.CHASING_LONE_WOLF7, ANY_REWARD, MOG) +LONE_WOLF_MOOGLE_ROOM = NameBit("Lone Wolf Moogle Room", event_bit.GOT_BOTH_REWARDS_LONE_WOLF, ANY_REWARD, MOG) +MAGITEK_FACTORY_TRASH = NameBit("Magitek Factory Trash", event_bit.GOT_IFRIT_SHIVA, ESPER_ITEM_REWARD, CELES) +MAGITEK_FACTORY_GUARD = NameBit("Magitek Factory Guard", event_bit.DEFEATED_NUMBER_024, ESPER_ITEM_REWARD, CELES) +MAGITEK_FACTORY_FINISH = NameBit("Magitek Factory Finish", event_bit.DEFEATED_CRANES, CHAR_ESPER_REWARD, CELES) +MOBLIZ_ATTACK = NameBit("Mobliz Attack", event_bit.RECRUITED_TERRA_MOBLIZ, ANY_REWARD, TERRA) +MT_KOLTS = NameBit("Mt. Kolts", event_bit.DEFEATED_VARGAS, ANY_REWARD, SABIN) +MT_ZOZO = NameBit("Mt. Zozo", event_bit.FINISHED_MT_ZOZO, ANY_REWARD, CYAN) +NARSHE_BATTLE = NameBit("Narshe Battle", event_bit.FINISHED_NARSHE_BATTLE, CHAR_ESPER_REWARD, None) +NARSHE_WEAPON_SHOP = NameBit("Narshe Weapon Shop", event_bit.GOT_RAGNAROK, ESPER_ITEM_REWARD, LOCKE) +NARSHE_WEAPON_SHOP_MINES = NameBit("Narshe Weapon Shop Mines", event_bit.GOT_BOTH_REWARDS_WEAPON_SHOP, ITEM_REWARD, LOCKE) +OPERA_HOUSE_DISRUPTION = NameBit("Opera House Disruption", event_bit.FINISHED_OPERA_DISRUPTION, ANY_REWARD, CELES) +OWZERS_MANSION = NameBit("Owzer's Mansion", event_bit.DEFEATED_CHADARNOOK, ANY_REWARD, RELM) +PHANTOM_TRAIN = NameBit("Phantom Train", event_bit.GOT_PHANTOM_TRAIN_REWARD, ANY_REWARD, SABIN) +PHOENIX_CAVE = NameBit("Phoenix Cave", event_bit.RECRUITED_LOCKE_PHOENIX_CAVE, ANY_REWARD, LOCKE) +SEALED_GATE = NameBit("Sealed Gate", npc_bit.BLOCK_SEALED_GATE, ANY_REWARD, TERRA) +SEARCH_THE_SKIES = NameBit("Search The Skies", event_bit.DEFEATED_DOOM_GAZE, ANY_REWARD, SETZER) +SERPENT_TRENCH = NameBit("Serpent Trench", event_bit.GOT_SERPENT_TRENCH_REWARD, ANY_REWARD, GAU) +SOUTH_FIGARO_PRISONER = NameBit("South Figaro Prisoner", event_bit.FREED_CELES, ANY_REWARD, CELES) +SOUTH_FIGARO_CAVE = NameBit("South Figaro Cave", event_bit.DEFEATED_TUNNEL_ARMOR, ANY_REWARD, LOCKE) +TRITOCH_CLIFF = NameBit("Tritoch Cliff", event_bit.GOT_TRITOCH, ESPER_ITEM_REWARD, None) +TZEN_THIEF = NameBit("Tzen Thief", event_bit.BOUGHT_ESPER_TZEN, ESPER_ITEM_REWARD, None) +UMAROS_CAVE = NameBit("Umaro's Cave", event_bit.RECRUITED_UMARO_WOR, ANY_REWARD, UMARO) +VELDT = NameBit("Veldt", event_bit.VELDT_REWARD_OBTAINED, CHAR_ESPER_REWARD, GAU) +VELDT_CAVE = NameBit("Veldt Cave", event_bit.DEFEATED_SR_BEHEMOTH, ANY_REWARD, SHADOW) +WHELK_GATE = NameBit("Whelk Gate", event_bit.DEFEATED_WHELK, ANY_REWARD, TERRA) +ZONE_EATER = NameBit("Zone Eater", event_bit.RECRUITED_GOGO_WOR, ANY_REWARD, GOGO) +ZOZO_TOWER = NameBit("Zozo Tower", event_bit.GOT_ZOZO_REWARD, ANY_REWARD, TERRA) + +ANCIENT_CASTLE_DRAGON = NameBit("Ancient Castle Dragon", event_bit.DEFEATED_ANCIENT_CASTLE_DRAGON, RewardType.ITEM, EDGAR) +FANATICS_TOWER_DRAGON = NameBit("Fanatic's Tower Dragon", event_bit.DEFEATED_FANATICS_TOWER_DRAGON, RewardType.ITEM, None) +KEFKAS_TOWER_DRAGON_G = NameBit("Kefka's Tower Dragon G", event_bit.DEFEATED_KEFKA_TOWER_DRAGON_G, RewardType.ITEM, None) +KEFKAS_TOWER_DRAGON_S = NameBit("Kefka's Tower Dragon S", event_bit.DEFEATED_KEFKA_TOWER_DRAGON_S, RewardType.ITEM, None) +MT_ZOZO_DRAGON = NameBit("Mt. Zozo Dragon", event_bit.DEFEATED_MT_ZOZO_DRAGON, RewardType.ITEM, CYAN) +NARSHE_DRAGON = NameBit("Narshe Dragon", event_bit.DEFEATED_NARSHE_DRAGON, RewardType.ITEM, None) +OPERA_HOUSE_DRAGON = NameBit("Opera House Dragon", event_bit.DEFEATED_OPERA_HOUSE_DRAGON, RewardType.ITEM, None) +PHOENIX_CAVE_DRAGON = NameBit("Phoenix Cave Dragon", event_bit.DEFEATED_PHOENIX_CAVE_DRAGON, RewardType.ITEM, None) + +# Checks +all_checks = [ + AUCTION1, + AUCTION2, + ANCIENT_CASTLE, + BAREN_FALLS, + BURNING_HOUSE, + COLLAPSING_HOUSE, + DARYLS_TOMB, + DOMA_SIEGE, + DOMA_DREAM_DOOR, + DOMA_DREAM_AWAKEN, + DOMA_DREAM_THRONE, + EBOTS_ROCK, + ESPER_MOUNTAIN, + FANATICS_TOWER_FOLLOWER, + FANATICS_TOWER_LEADER, + FIGARO_CASTLE_THRONE, + FIGARO_CASTLE_ENGINE, + FLOATING_CONT_ARRIVE, + FLOATING_CONT_BEAST, + FLOATING_CONT_ESCAPE, + GAUS_FATHERS_HOUSE, + IMPERIAL_CAMP, + KOHLINGEN_CAFE, + LETE_RIVER, + LONE_WOLF_CHASE, + LONE_WOLF_MOOGLE_ROOM, + MAGITEK_FACTORY_TRASH, + MAGITEK_FACTORY_GUARD, + MAGITEK_FACTORY_FINISH, + MOBLIZ_ATTACK, + MT_KOLTS, + MT_ZOZO, + NARSHE_BATTLE, + NARSHE_WEAPON_SHOP, + NARSHE_WEAPON_SHOP_MINES, + OPERA_HOUSE_DISRUPTION, + OWZERS_MANSION, + PHANTOM_TRAIN, + PHOENIX_CAVE, + SEALED_GATE, + SEARCH_THE_SKIES, + SERPENT_TRENCH, + SOUTH_FIGARO_PRISONER, + SOUTH_FIGARO_CAVE, + TRITOCH_CLIFF, + TZEN_THIEF, + UMAROS_CAVE, + VELDT, + VELDT_CAVE, + WHELK_GATE, + ZONE_EATER, + ZOZO_TOWER, + + # Dragons + ANCIENT_CASTLE_DRAGON, + FANATICS_TOWER_DRAGON, + KEFKAS_TOWER_DRAGON_G, + KEFKAS_TOWER_DRAGON_S, + MT_ZOZO_DRAGON, + NARSHE_DRAGON, + OPERA_HOUSE_DRAGON, + PHOENIX_CAVE_DRAGON, + + # KT + KEFKAS_TOWER_CELL_BEAST, +] + +# Used to determine some flag limitations (i.e. starting espers) +CHARACTER_ESPER_ONLY_REWARDS = len([check for check in all_checks if check.reward_types == RewardType.CHARACTER | RewardType.ESPER]) + +check_name = {check.bit: check.name for (idx, check) in enumerate(all_checks)} +name_check = {check.name: check.bit for (idx, check) in enumerate(all_checks)} + +RECRUITABLE_DRAGONS = [ + ANCIENT_CASTLE_DRAGON, + FANATICS_TOWER_DRAGON, + MT_ZOZO_DRAGON, + NARSHE_DRAGON, + OPERA_HOUSE_DRAGON, +] diff --git a/constants/gates.py b/constants/gates.py index 266b975c..9d7c2764 100644 --- a/constants/gates.py +++ b/constants/gates.py @@ -1,94 +1,54 @@ +import constants.checks as c +from constants.checks import all_checks +from constants.entities import ( + CELES, + CYAN, + EDGAR, + GAU, + GOGO, + LOCKE, + MOG, + RELM, + SABIN, + SETZER, + SHADOW, + STRAGO, + TERRA, + UMARO, +) + character_checks = { - "Celes" : [ - "Magitek Factory Trash", - "Magitek Factory Guard", - "Magitek Factory Finish", - "Opera House Disruption", - "South Figaro Prisoner", - ], - "Cyan" : [ - "Doma Siege", - "Doma Dream Door", - "Doma Dream Awaken", - "Doma Dream Throne", - "Mt. Zozo", - "Mt. Zozo Dragon", - ], - "Edgar" : [ - "Ancient Castle", - "Ancient Castle Dragon", - "Figaro Castle Throne", - "Figaro Castle Engine", - ], - "Gau" : [ - "Serpent Trench", - "Veldt", - ], - "Gogo" : [ - "Zone Eater", - ], - "Locke" : [ - "Narshe Weapon Shop", - "Narshe Weapon Shop Mines", - "Phoenix Cave", - "South Figaro Cave", - ], - "Mog" : [ - "Lone Wolf Chase", - "Lone Wolf Moogle Room", - ], - "Relm" : [ - "Esper Mountain", - "Owzer's Mansion", - ], - "Sabin" : [ - "Baren Falls", - "Collapsing House", - "Imperial Camp", - "Mt. Kolts", - "Phantom Train", - ], - "Setzer" : [ - "Daryl's Tomb", - "Kohlingen Cafe", - "Search The Skies", - ], - "Shadow" : [ - "Floating Cont. Arrive", - "Floating Cont. Beast", - "Floating Cont. Escape", - "Gau's Father's House", - "Veldt Cave", - ], - "Strago" : [ - "Burning House", - "Ebot's Rock", - "Fanatic's Tower Follower", - ], - "Terra" : [ - "Lete River", - "Mobliz Attack", - "Sealed Gate", - "Whelk Gate", - "Zozo Tower", - ], - "Umaro" : [ - "Umaro's Cave", - ], + "Celes" : [c for c in [check for check in all_checks if check.gate_character == CELES]], + "Cyan" : [c for c in [check for check in all_checks if check.gate_character == CYAN]], + "Edgar" : [c for c in [check for check in all_checks if check.gate_character == EDGAR]], + "Gau" : [c for c in [check for check in all_checks if check.gate_character == GAU]], + "Gogo" : [c for c in [check for check in all_checks if check.gate_character == GOGO]], + "Locke" : [c for c in [check for check in all_checks if check.gate_character == LOCKE]], + "Mog" : [c for c in [check for check in all_checks if check.gate_character == MOG]], + "Relm" : [c for c in [check for check in all_checks if check.gate_character == RELM]], + "Sabin" : [c for c in [check for check in all_checks if check.gate_character == SABIN]], + "Setzer" : [c for c in [check for check in all_checks if check.gate_character == SETZER]], + "Shadow" : [c for c in [check for check in all_checks if check.gate_character == SHADOW]], + "Strago" : [c for c in [check for check in all_checks if check.gate_character == STRAGO]], + "Terra" : [c for c in [check for check in all_checks if check.gate_character == TERRA]], + "Umaro" : [c for c in [check for check in all_checks if check.gate_character == UMARO]], + "" : [c for c in [check for check in all_checks if check.gate_character is None]], +} - "" : [ - "Auction1", - "Auction2", - "Fanatic's Tower Dragon", - "Fanatic's Tower Leader", - "Kefka's Tower Cell Beast", - "Kefka's Tower Dragon G", - "Kefka's Tower Dragon S", - "Narshe Battle", - "Narshe Dragon", - "Opera House Dragon", - "Phoenix Cave Dragon", - "Tritoch Cliff", - "Tzen Thief", - ], +character_check_names = { + "Celes" : [c.name for c in [check for check in all_checks if check.gate_character == CELES]], + "Cyan" : [c.name for c in [check for check in all_checks if check.gate_character == CYAN]], + "Edgar" : [c.name for c in [check for check in all_checks if check.gate_character == EDGAR]], + "Gau" : [c.name for c in [check for check in all_checks if check.gate_character == GAU]], + "Gogo" : [c.name for c in [check for check in all_checks if check.gate_character == GOGO]], + "Locke" : [c.name for c in [check for check in all_checks if check.gate_character == LOCKE]], + "Mog" : [c.name for c in [check for check in all_checks if check.gate_character == MOG]], + "Relm" : [c.name for c in [check for check in all_checks if check.gate_character == RELM]], + "Sabin" : [c.name for c in [check for check in all_checks if check.gate_character == SABIN]], + "Setzer" : [c.name for c in [check for check in all_checks if check.gate_character == SETZER]], + "Shadow" : [c.name for c in [check for check in all_checks if check.gate_character == SHADOW]], + "Strago" : [c.name for c in [check for check in all_checks if check.gate_character == STRAGO]], + "Terra" : [c.name for c in [check for check in all_checks if check.gate_character == TERRA]], + "Umaro" : [c.name for c in [check for check in all_checks if check.gate_character == UMARO]], + "" : [c.name for c in [check for check in all_checks if check.gate_character is None]], } diff --git a/constants/objectives/condition_bits.py b/constants/objectives/condition_bits.py index bc46f131..cbe39e00 100644 --- a/constants/objectives/condition_bits.py +++ b/constants/objectives/condition_bits.py @@ -1,84 +1,84 @@ -import data.event_bit as event_bit -import data.npc_bit as npc_bit import data.battle_bit as battle_bit +import constants.checks as checks +import constants.quests as quests from data.bosses import normal_formation_name, dragon_formation_name from collections import namedtuple NameBit = namedtuple("NameBit", ["name", "bit"]) check_bit = [ - NameBit("Ancient Castle", event_bit.GOT_RAIDEN), - NameBit("Ancient Castle Dragon", event_bit.DEFEATED_ANCIENT_CASTLE_DRAGON), - NameBit("Baren Falls", event_bit.NAMED_GAU), - NameBit("Burning House", event_bit.DEFEATED_FLAME_EATER), - NameBit("Collapsing House", event_bit.FINISHED_COLLAPSING_HOUSE), - NameBit("Daryl's Tomb", event_bit.DEFEATED_DULLAHAN), - NameBit("Doma Siege", event_bit.FINISHED_DOMA_WOB), - NameBit("Doma Dream Door", event_bit.DEFEATED_STOOGES), - NameBit("Doma Dream Awaken", event_bit.FINISHED_DOMA_WOR), - NameBit("Doma Dream Throne", event_bit.GOT_ALEXANDR), - NameBit("Ebot's Rock", event_bit.DEFEATED_HIDON), - NameBit("Esper Mountain", event_bit.DEFEATED_ULTROS_ESPER_MOUNTAIN), - NameBit("Fanatic's Tower Dragon", event_bit.DEFEATED_FANATICS_TOWER_DRAGON), - NameBit("Fanatic's Tower Leader", event_bit.DEFEATED_MAGIMASTER), - NameBit("Fanatic's Tower Follower", event_bit.RECRUITED_STRAGO_FANATICS_TOWER), - NameBit("Figaro Castle Throne", event_bit.NAMED_EDGAR), - NameBit("Figaro Castle Engine", event_bit.DEFEATED_TENTACLES_FIGARO), - NameBit("Floating Cont. Arrive", event_bit.RECRUITED_SHADOW_FLOATING_CONTINENT), - NameBit("Floating Cont. Beast", event_bit.DEFEATED_ATMAWEAPON), - NameBit("Floating Cont. Escape", event_bit.FINISHED_FLOATING_CONTINENT), - NameBit("Gau's Father's House", event_bit.RECRUITED_SHADOW_GAU_FATHER_HOUSE), - NameBit("Imperial Camp", event_bit.FINISHED_IMPERIAL_CAMP), - NameBit("Kefka's Tower Cell Beast", event_bit.DEFEATED_ATMA), - NameBit("Kefka's Tower Dragon G", event_bit.DEFEATED_KEFKA_TOWER_DRAGON_G), - NameBit("Kefka's Tower Dragon S", event_bit.DEFEATED_KEFKA_TOWER_DRAGON_S), - NameBit("Kohlingen Cafe", event_bit.RECRUITED_SHADOW_KOHLINGEN), - NameBit("Lete River", event_bit.RODE_RAFT_LETE_RIVER), - NameBit("Lone Wolf Chase", event_bit.CHASING_LONE_WOLF7), - NameBit("Lone Wolf Moogle Room", event_bit.GOT_BOTH_REWARDS_LONE_WOLF), - NameBit("Magitek Factory Trash", event_bit.GOT_IFRIT_SHIVA), - NameBit("Magitek Factory Guard", event_bit.DEFEATED_NUMBER_024), - NameBit("Magitek Factory Finish", event_bit.DEFEATED_CRANES), - NameBit("Mobliz Attack", event_bit.RECRUITED_TERRA_MOBLIZ), - NameBit("Mt. Kolts", event_bit.DEFEATED_VARGAS), - NameBit("Mt. Zozo", event_bit.FINISHED_MT_ZOZO), - NameBit("Mt. Zozo Dragon", event_bit.DEFEATED_MT_ZOZO_DRAGON), - NameBit("Narshe Battle", event_bit.FINISHED_NARSHE_BATTLE), - NameBit("Narshe Dragon", event_bit.DEFEATED_NARSHE_DRAGON), - NameBit("Narshe Weapon Shop", event_bit.GOT_RAGNAROK), - NameBit("Narshe Weapon Shop Mines", event_bit.GOT_BOTH_REWARDS_WEAPON_SHOP), - NameBit("Opera House Disruption", event_bit.FINISHED_OPERA_DISRUPTION), - NameBit("Opera House Dragon", event_bit.DEFEATED_OPERA_HOUSE_DRAGON), - NameBit("Owzer's Mansion", event_bit.DEFEATED_CHADARNOOK), - NameBit("Phantom Train", event_bit.GOT_PHANTOM_TRAIN_REWARD), - NameBit("Phoenix Cave", event_bit.RECRUITED_LOCKE_PHOENIX_CAVE), - NameBit("Phoenix Cave Dragon", event_bit.DEFEATED_PHOENIX_CAVE_DRAGON), - NameBit("Sealed Gate", npc_bit.BLOCK_SEALED_GATE), - NameBit("Search The Skies", event_bit.DEFEATED_DOOM_GAZE), - NameBit("Serpent Trench", event_bit.GOT_SERPENT_TRENCH_REWARD), - NameBit("South Figaro Prisoner", event_bit.FREED_CELES), - NameBit("South Figaro Cave", event_bit.DEFEATED_TUNNEL_ARMOR), - NameBit("Tritoch Cliff", event_bit.GOT_TRITOCH), - NameBit("Tzen Thief", event_bit.BOUGHT_ESPER_TZEN), - NameBit("Umaro's Cave", event_bit.RECRUITED_UMARO_WOR), - NameBit("Veldt", event_bit.VELDT_REWARD_OBTAINED), - NameBit("Veldt Cave", event_bit.DEFEATED_SR_BEHEMOTH), - NameBit("Whelk Gate", event_bit.DEFEATED_WHELK), - NameBit("Zone Eater", event_bit.RECRUITED_GOGO_WOR), - NameBit("Zozo Tower", event_bit.GOT_ZOZO_REWARD), + checks.ANCIENT_CASTLE, + checks.ANCIENT_CASTLE_DRAGON, + checks.BAREN_FALLS, + checks.BURNING_HOUSE, + checks.COLLAPSING_HOUSE, + checks.DARYLS_TOMB, + checks.DOMA_SIEGE, + checks.DOMA_DREAM_DOOR, + checks.DOMA_DREAM_AWAKEN, + checks.DOMA_DREAM_THRONE, + checks.EBOTS_ROCK, + checks.ESPER_MOUNTAIN, + checks.FANATICS_TOWER_DRAGON, + checks.FANATICS_TOWER_LEADER, + checks.FANATICS_TOWER_FOLLOWER, + checks.FIGARO_CASTLE_THRONE, + checks.FIGARO_CASTLE_ENGINE, + checks.FLOATING_CONT_ARRIVE, + checks.FLOATING_CONT_BEAST, + checks.FLOATING_CONT_ESCAPE, + checks.GAUS_FATHERS_HOUSE, + checks.IMPERIAL_CAMP, + checks.KEFKAS_TOWER_CELL_BEAST, + checks.KEFKAS_TOWER_DRAGON_G, + checks.KEFKAS_TOWER_DRAGON_S, + checks.KOHLINGEN_CAFE, + checks.LETE_RIVER, + checks.LONE_WOLF_CHASE, + checks.LONE_WOLF_MOOGLE_ROOM, + checks.MAGITEK_FACTORY_TRASH, + checks.MAGITEK_FACTORY_GUARD, + checks.MAGITEK_FACTORY_FINISH, + checks.MOBLIZ_ATTACK, + checks.MT_KOLTS, + checks.MT_ZOZO, + checks.MT_ZOZO_DRAGON, + checks.NARSHE_BATTLE, + checks.NARSHE_DRAGON, + checks.NARSHE_WEAPON_SHOP, + checks.NARSHE_WEAPON_SHOP_MINES, + checks.OPERA_HOUSE_DISRUPTION, + checks.OPERA_HOUSE_DRAGON, + checks.OWZERS_MANSION, + checks.PHANTOM_TRAIN, + checks.PHOENIX_CAVE, + checks.PHOENIX_CAVE_DRAGON, + checks.SEALED_GATE, + checks.SEARCH_THE_SKIES, + checks.SERPENT_TRENCH, + checks.SOUTH_FIGARO_PRISONER, + checks.SOUTH_FIGARO_CAVE, + checks.TRITOCH_CLIFF, + checks.TZEN_THIEF, + checks.UMAROS_CAVE, + checks.VELDT, + checks.VELDT_CAVE, + checks.WHELK_GATE, + checks.ZONE_EATER, + checks.ZOZO_TOWER, ] quest_bit = [ - NameBit("Defeat Sealed Cave Ninja", event_bit.DEFEATED_NINJA_CAVE_TO_SEALED_GATE), - NameBit("Help Injured Lad", event_bit.HELPED_INJURED_LAD), - NameBit("Let Cid Die", event_bit.CID_DIED), - NameBit("Pass Security Checkpoint", event_bit.FINISHED_NARSHE_CHECKPOINT), - NameBit("Perform In Opera", event_bit.FINISHED_OPERA_PERFORMANCE), - NameBit("Save Cid", event_bit.CID_SURVIVED), - NameBit("Set Zozo Clock", event_bit.SET_ZOZO_CLOCK), - 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), + quests.DEFEAT_SEALED_CAVE_NINJA, + quests.HELP_INJURED_LAD, + quests.LET_CID_DIE, + quests.PASS_SECURITY_CHECKPOINT, + quests.PERFORM_IN_OPERA, + quests.SAVE_CID, + quests.SET_ZOZO_CLOCK, + quests.SUPLEX_A_TRAIN, + quests.WIN_AN_AUCTION, + quests.WIN_A_COLISEUM_MATCH, ] boss_bit = [] diff --git a/constants/objectives/results.py b/constants/objectives/results.py index 2606d9b2..53825c01 100644 --- a/constants/objectives/results.py +++ b/constants/objectives/results.py @@ -58,7 +58,6 @@ ResultType(36, "Dragoon", "Dragoon", None), ResultType(37, "Dried Meat", "Dried Meat", None), ResultType(38, "Exp. Egg", "Exp. Egg", None), - ResultType(58, "High Tier Item", "High Tier Item", None), ResultType(40, "Illumina", "Illumina", None), ResultType(39, "Imp Set", "Imp Set", None), ResultType(41, "Rename Cards", "Rename Cards", None), @@ -85,6 +84,10 @@ ], } +category_types["Item"].append(ResultType(58, "High Tier Item", "High Tier Item", None)) +# 59 - MagiTek upgrade placeholder +category_types["Item"].append(ResultType(60, "Sprint Shoes", "Sprint Shoes", None)) + categories = list(category_types.keys()) id_type = {} diff --git a/constants/quests.py b/constants/quests.py new file mode 100644 index 00000000..ccf7968e --- /dev/null +++ b/constants/quests.py @@ -0,0 +1,15 @@ +from collections import namedtuple +import data.event_bit as event_bit + +NameBit = namedtuple("NameBit", ["name", "bit"]) + +DEFEAT_SEALED_CAVE_NINJA = NameBit("Defeat Sealed Cave Ninja", event_bit.DEFEATED_NINJA_CAVE_TO_SEALED_GATE) +HELP_INJURED_LAD = NameBit("Help Injured Lad", event_bit.HELPED_INJURED_LAD) +LET_CID_DIE = NameBit("Let Cid Die", event_bit.CID_DIED) +PASS_SECURITY_CHECKPOINT = NameBit("Pass Security Checkpoint", event_bit.FINISHED_NARSHE_CHECKPOINT) +PERFORM_IN_OPERA = NameBit("Perform In Opera", event_bit.FINISHED_OPERA_PERFORMANCE) +SAVE_CID = NameBit("Save Cid", event_bit.CID_SURVIVED) +SET_ZOZO_CLOCK = NameBit("Set Zozo Clock", event_bit.SET_ZOZO_CLOCK) +SUPLEX_A_TRAIN = NameBit("Suplex A Train", event_bit.SUPLEXED_TRAIN) +WIN_AN_AUCTION = NameBit("Win An Auction", event_bit.WON_AN_AUCTION) +WIN_A_COLISEUM_MATCH = NameBit("Win A Coliseum Match", event_bit.WON_A_COLISEUM_MATCH) diff --git a/data/dialogs/free.py b/data/dialogs/free.py index 758c73bb..20f510c2 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(1777, 1878) # BANQUET DIALOGS - Do not use these. These dialogs are repurposed for item dialogs in data/items.py - "available_dialogs" + multi_line_battle_dialogs = [ 0, # WEDGE:Hey! What's the matter?Do you know something wedon't……? ... 1, # GIRL:…… diff --git a/data/event_bit.py b/data/event_bit.py index b8775c9d..9915a3d8 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 @@ -162,6 +162,7 @@ DEFEATED_STOOGES = 0x0d8 FINISHED_DOMA_WOR = 0x0da GOT_ALEXANDR = 0x0db +RECEIVED_FANATICS_TOWER_REWARD = 0x2da DEFEATED_MAGIMASTER = 0x2db RECRUITED_STRAGO_FANATICS_TOWER = 0x0ba DEFEATED_DOOM_GAZE = 0x2a1 # custom diff --git a/data/maps.py b/data/maps.py index 8c63f69f..0625ab85 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 diff --git a/data/npc_bit.py b/data/npc_bit.py index da80622f..8a010a82 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 @@ -147,6 +147,9 @@ GODDESS_STATUE_KEFKA_TOWER = 0x6b1 POLTRGEIST_STATUE_KEFKA_TOWER = 0x6b2 +# bits [571-5f5] are unused +FANATICS_TOWER_SECONDARY_REWARD = 0x571 # custom, used for secondary reward at top of fanatics tower (if esper only) + def byte(npc_bit): return npc_bit // 8 diff --git a/event/ancient_castle.py b/event/ancient_castle.py index 4bbdd997..151f6a4a 100644 --- a/event/ancient_castle.py +++ b/event/ancient_castle.py @@ -1,3 +1,4 @@ +from constants.checks import ANCIENT_CASTLE from event.event import * class AncientCastle(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.EDGAR def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(ANCIENT_CASTLE) def init_event_bits(self, space): space.write( diff --git a/event/auction_house.py b/event/auction_house.py index 82fc0a53..0374ee78 100644 --- a/event/auction_house.py +++ b/event/auction_house.py @@ -44,12 +44,9 @@ def name(self): return "Auction House" def init_rewards(self): - if self.args.no_free_characters_espers: - self.reward1 = self.add_reward(RewardType.ITEM) - self.reward2 = self.add_reward(RewardType.ITEM) - else: - self.reward1 = self.add_reward(RewardType.ESPER | RewardType.ITEM) - self.reward2 = self.add_reward(RewardType.ESPER | RewardType.ITEM) + from constants.checks import AUCTION1, AUCTION2 + self.reward1 = self.add_reward(AUCTION1) + self.reward2 = self.add_reward(AUCTION2) def mod(self): self.requirements_mod() diff --git a/event/baren_falls.py b/event/baren_falls.py index b479b802..71338811 100644 --- a/event/baren_falls.py +++ b/event/baren_falls.py @@ -1,3 +1,4 @@ +from constants.checks import BAREN_FALLS from event.event import * class BarenFalls(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.SABIN def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(BAREN_FALLS) def mod(self): # delete row of events that trigger sabin/cyan dialog and shadow leaving (if in party) @@ -132,6 +133,12 @@ def character_mod(self, character): field.Call(finish_check), ) + from music.song_utils import get_character_theme + space = Reserve(0xbc0ff, 0xbc100, "play gau's theme") + space.write([ + field.StartSong(get_character_theme(character)), + ]) + def esper_item_mod(self, esper_item_instructions): space = Reserve(0xbc0f7, 0xbc1b7, "baren falls gau moving/naming", field.NOP()) space.write( diff --git a/event/burning_house.py b/event/burning_house.py index 8e1f5135..657622e2 100644 --- a/event/burning_house.py +++ b/event/burning_house.py @@ -1,3 +1,4 @@ +from constants.checks import BURNING_HOUSE from event.event import * # TODO: only trigger this event in wob @@ -10,7 +11,7 @@ def character_gate(self): return self.characters.STRAGO def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(BURNING_HOUSE) def init_event_bits(self, space): space.write( diff --git a/event/collapsing_house.py b/event/collapsing_house.py index 75cf0068..3c20ab7c 100644 --- a/event/collapsing_house.py +++ b/event/collapsing_house.py @@ -1,3 +1,4 @@ +from constants.checks import COLLAPSING_HOUSE from event.event import * class CollapsingHouse(Event): @@ -8,10 +9,7 @@ def character_gate(self): return self.characters.SABIN def init_rewards(self): - if self.args.no_free_characters_espers: - self.reward = self.add_reward(RewardType.ITEM) - else: - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(COLLAPSING_HOUSE) def init_event_bits(self, space): pass diff --git a/event/daryl_tomb.py b/event/daryl_tomb.py index ed25d979..1b7d673a 100644 --- a/event/daryl_tomb.py +++ b/event/daryl_tomb.py @@ -1,3 +1,4 @@ +from constants.checks import DARYLS_TOMB from event.event import * class DarylTomb(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.SETZER def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(DARYLS_TOMB) def mod(self): self.entrance_mod() diff --git a/event/doma_wob.py b/event/doma_wob.py index ba6f5f50..59784238 100644 --- a/event/doma_wob.py +++ b/event/doma_wob.py @@ -1,14 +1,15 @@ from event.event import * +from constants.checks import DOMA_SIEGE class DomaWOB(Event): def name(self): - return "Doma WOB" + return DOMA_SIEGE.name def character_gate(self): return self.characters.CYAN def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(DOMA_SIEGE) def init_event_bits(self, space): space.write( diff --git a/event/doma_wor.py b/event/doma_wor.py index ac17ea41..cc8b6625 100644 --- a/event/doma_wor.py +++ b/event/doma_wor.py @@ -2,15 +2,16 @@ class DomaWOR(Event): def name(self): - return "Doma WOR" + return "Doma Dream" def character_gate(self): return self.characters.CYAN def init_rewards(self): - self.reward1 = self.add_reward(RewardType.CHARACTER | RewardType.ESPER) - self.reward2 = self.add_reward(RewardType.ESPER | RewardType.ITEM) - self.reward3 = self.add_reward(RewardType.ESPER | RewardType.ITEM) + from constants.checks import DOMA_DREAM_DOOR, DOMA_DREAM_AWAKEN, DOMA_DREAM_THRONE + self.reward1 = self.add_reward(DOMA_DREAM_AWAKEN) + self.reward2 = self.add_reward(DOMA_DREAM_THRONE) + self.reward3 = self.add_reward(DOMA_DREAM_DOOR) def mod(self): self.cyan_phantom_train_npc_id = 0x10 @@ -45,6 +46,8 @@ def mod(self): self.cyan_character_mod(self.reward1.id) elif self.reward1.type == RewardType.ESPER: self.cyan_esper_mod(self.reward1.id) + elif self.reward1.type == RewardType.ITEM: + self.cyan_item_mod(self.reward1.id) self.finish_dream_awaken_mod() if self.reward2.type == RewardType.ESPER: @@ -261,6 +264,21 @@ def cyan_esper_mod(self, esper): field.Branch(space.end_address + 1), # skip nops ) + def cyan_item_mod(self, item): + self.random_cyan_npc_mod() + + space = Reserve(0xb9818, 0xb982f, "doma wor split up party after wrexsoul battle", field.NOP()) + space.write( + field.Branch(space.end_address + 1), # skip nops + ) + + space = Reserve(0xb99b4, 0xb99d4, "doma wor cyan touches sword", field.NOP()) + space.write( + field.AddItem(item), + field.Dialog(self.items.get_receive_dialog(item)), + field.Branch(space.end_address + 1), # skip nops + ) + def finish_dream_awaken_mod(self): if(self.args.flashes_remove_most): space = Reserve(0xb9a47, 0xb9a48, "doma wor peak swordmanship flash", field.NOP()) diff --git a/event/doom_gaze.py b/event/doom_gaze.py index ca7e7cf8..528935af 100644 --- a/event/doom_gaze.py +++ b/event/doom_gaze.py @@ -8,7 +8,8 @@ def character_gate(self): return self.characters.SETZER # gate for airship option def init_rewards(self): - self.reward = self.add_reward(RewardType.ESPER | RewardType.ITEM) + from constants.checks import SEARCH_THE_SKIES + self.reward = self.add_reward(SEARCH_THE_SKIES) def mod(self): self.magicite_npc_id = 0x12 @@ -18,6 +19,8 @@ def mod(self): if self.args.doom_gaze_no_escape: self.doom_gaze_battle_mod() + if self.reward.type == RewardType.CHARACTER: + self.character_mod(self.reward.id) if self.reward.type == RewardType.ESPER: self.esper_mod(self.reward.id) elif self.reward.type == RewardType.ITEM: @@ -75,6 +78,76 @@ def receive_reward_mod(self, reward_instructions): field.Call(receive_reward), ) + def character_mod(self, character): + start_addr = 0xa00a7 # begin magicite animation + end_addr = 0xa00b6 # end magicite animation + src = [ + field.EntityAct(self.magicite_npc_id, True, + # Character "Attacked" animation will often cause clipping with the airship + field_entity.SetSpriteLayer(1), + field_entity.DisableWalkingAnimation(), + field_entity.AnimateAttacked(), + ), + + # Keep character on the same path as the esper, but change animation once movement has stopped + self.rom.get_bytes(start_addr, end_addr - start_addr + 1), + field.EntityAct(self.magicite_npc_id, True, + field_entity.AnimateKneeling(), + ), + field.Return(), + ] + + + spit_out_reward = Write(Bank.F0, src, "doom gaze spits out character").start_address + + # Use preevious event space to call new subroutine recruiting character + space = Reserve(start_addr, end_addr, "doom gaze spits out magicite", asm.NOP()) + space.write([ + field.Call(spit_out_reward) + ]) + + space = Reserve(0xa00ca, 0xa00dc, "Character pulls reward toward them, hide item, show dialog.", asm.NOP()) + + self.magicite_npc.sprite = character + self.magicite_npc.palette = self.characters.get_palette(character) + self.magicite_npc.direction = direction.DOWN + self.magicite_npc.split_sprite = 0 + + self.receive_reward_mod([ + field.HideEntity(self.magicite_npc_id), + field.RecruitAndSelectParty(character), + field.FadeInScreen(), + + # Party performs small victory dance + field.EntityAct(field_entity.PARTY0, False, + field_entity.Turn(direction.LEFT), + field_entity.Pause(0), + field_entity.Turn(direction.UP), + field_entity.Pause(0), + field_entity.Turn(direction.RIGHT), + field_entity.Pause(0), + field_entity.Turn(direction.DOWN), + field_entity.Pause(0), + field_entity.Turn(direction.LEFT), + field_entity.Pause(2), + field_entity.AnimateArmsRaisedWalking(), + field_entity.Pause(4), + field_entity.Turn(direction.LEFT), + field_entity.Pause(4), + field_entity.AnimateArmsRaisedWalking(), + field_entity.Pause(4), + field_entity.Turn(direction.LEFT), + field_entity.Pause(4), + field_entity.AnimateArmsRaisedWalking(), + field_entity.Pause(4), + field_entity.Turn(direction.LEFT), + field_entity.Pause(4), + field_entity.AnimateArmsRaisedWalking(), + ), + field.WaitForFade(), + field.Pause(1) + ]) + def esper_mod(self, esper): self.receive_reward_mod([ field.Dialog(self.espers.get_receive_esper_dialog(esper)), diff --git a/event/ebots_rock.py b/event/ebots_rock.py index c0d0288b..77a9e6e7 100644 --- a/event/ebots_rock.py +++ b/event/ebots_rock.py @@ -1,3 +1,4 @@ +from constants.checks import EBOTS_ROCK from event.event import * class EbotsRock(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.STRAGO def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(EBOTS_ROCK) def init_event_bits(self, space): space.write( diff --git a/event/eight_dragons.py b/event/eight_dragons.py index de995128..3f69e4b1 100644 --- a/event/eight_dragons.py +++ b/event/eight_dragons.py @@ -1,13 +1,34 @@ +from constants.checks import ANCIENT_CASTLE_DRAGON, FANATICS_TOWER_DRAGON, KEFKAS_TOWER_DRAGON_G, KEFKAS_TOWER_DRAGON_S, MT_ZOZO_DRAGON, NARSHE_DRAGON, OPERA_HOUSE_DRAGON, PHOENIX_CAVE_DRAGON, RECRUITABLE_DRAGONS from event.event import * +from collections import namedtuple +DragonData = namedtuple("DragonData", ["name", "check", "battle_address", "countdown_address", "map_id", "npc_id"]) +dragon_data = [ + DragonData("Ice Dragon", NARSHE_DRAGON, 0xc36df, 0xc36ec, 34, 0x11), + DragonData("Storm Drgn", MT_ZOZO_DRAGON, 0xc43cd, 0xc43dc, 179, 0x10), + DragonData("Dirt Drgn", OPERA_HOUSE_DRAGON, 0xab6df, 0xab6f3, 231, 0x28), + DragonData("Gold Drgn", KEFKAS_TOWER_DRAGON_G, 0xc18f3, 0xc1900, 335, 0x10), + DragonData("Skull Drgn", KEFKAS_TOWER_DRAGON_S, 0xc1920, 0xc192d, 354, 0x14), + DragonData("Blue Drgn", ANCIENT_CASTLE_DRAGON, 0xc205b, 0xc2068, 408, 0x13), + DragonData("Red Dragon", PHOENIX_CAVE_DRAGON, 0xc2048, 0xc2055, 315, 0x10), + DragonData("White Drgn", FANATICS_TOWER_DRAGON, 0xc558b, 0xc559d, 368, 0x10), +] + class EightDragons(Event): def name(self): return "8 Dragons" def init_rewards(self): - self.item_rewards = [] - for dragon_index in range(self.enemies.DRAGON_COUNT): - self.item_rewards.append(self.add_reward(RewardType.ITEM)) + self.dragon_rewards = [] + + import random + dragon_character_count = random.randint(self.args.dragons_as_characters_min, self.args.dragons_as_characters_max) + character_rewards = [x.bit for x in random.sample(RECRUITABLE_DRAGONS, dragon_character_count)] + + for dragon in dragon_data: + reward = dragon.check.reward_types + bit = dragon.check.bit + self.dragon_rewards.append(self.add_reward(dragon.check, RewardType.CHARACTER if bit in character_rewards else reward)) def init_event_bits(self, space): space.write( @@ -15,25 +36,13 @@ def init_event_bits(self, space): ) def mod(self): - from collections import namedtuple - DragonData = namedtuple("DragonData", ["name", "event_bit", "battle_address", "countdown_address"]) - self.dragon_data = [ - DragonData("Ice Dragon", event_bit.DEFEATED_NARSHE_DRAGON, 0xc36df, 0xc36ec), - DragonData("Storm Drgn", event_bit.DEFEATED_MT_ZOZO_DRAGON, 0xc43cd, 0xc43dc), - DragonData("Dirt Drgn", event_bit.DEFEATED_OPERA_HOUSE_DRAGON, 0xab6df, 0xab6f3), - DragonData("Gold Drgn", event_bit.DEFEATED_KEFKA_TOWER_DRAGON_G, 0xc18f3, 0xc1900), - DragonData("Skull Drgn", event_bit.DEFEATED_KEFKA_TOWER_DRAGON_S, 0xc1920, 0xc192d), - DragonData("Blue Drgn", event_bit.DEFEATED_ANCIENT_CASTLE_DRAGON, 0xc205b, 0xc2068), - DragonData("Red Dragon", event_bit.DEFEATED_PHOENIX_CAVE_DRAGON, 0xc2048, 0xc2055), - DragonData("White Drgn", event_bit.DEFEATED_FANATICS_TOWER_DRAGON, 0xc558b, 0xc559d), - ] self.dialog_mod() self.dragon_battles_mod() self.dragon_rewards_mod() self.white_dragon_reward_mod() - for reward in self.item_rewards: + for reward in self.dragon_rewards: self.log_reward(reward) def dialog_mod(self): @@ -45,7 +54,7 @@ def dialog_mod(self): def dragon_battles_mod(self): call_size = 6 # invoke battle + call check game over - for dragon in self.dragon_data: + for dragon in dragon_data: boss_pack_id = self.get_boss(dragon.name) space = Reserve(dragon.battle_address, dragon.battle_address + call_size, @@ -55,24 +64,56 @@ def dragon_battles_mod(self): ) def dragon_rewards_mod(self): + for index, dragon in enumerate(dragon_data): + reward = self.dragon_rewards[index] + + if reward.type == RewardType.CHARACTER: + self.dragon_reward_mod(dragon, [ + field.RecruitAndSelectParty(reward.id), + field.RefreshEntities(), + field.FadeInScreen(), + ]) + self.dragon_character_mod(dragon, reward) + if reward.type == RewardType.ESPER: + self.dragon_reward_mod(dragon, [ + field.AddEsper(reward.id), + field.Dialog(self.espers.get_receive_esper_dialog(reward.id)) + ]) + if reward.type == RewardType.ITEM: + self.dragon_reward_mod(dragon, [ + field.AddItem(reward.id), + field.Dialog(self.items.get_receive_dialog(reward.id)), + ]) + + def dragon_character_mod(self, dragon, reward): + npc = self.maps.get_npc(dragon.map_id, dragon.npc_id) + npc.sprite = reward.id + npc.palette = self.characters.get_palette(reward.id) + + def dragon_reward_mod(self, dragon, reward_instructions): call_instr_size = 4 - for index, dragon in enumerate(self.dragon_data): - reward = self.item_rewards[index] - src = [ - field.AddItem(reward.id), - field.Dialog(self.items.get_receive_dialog(reward.id)), - field.SetEventBit(dragon.event_bit), - field.FinishCheck(), - field.Return(), - ] - space = Write(Bank.CC, src, f"8 dragons {dragon.name.lower()} receive reward") - receive_reward = space.start_address - - space = Reserve(dragon.countdown_address, dragon.countdown_address + call_instr_size - 1, - f"8 dragons call receive reward {hex(receive_reward)}", field.NOP()) - space.write( - field.Call(receive_reward), - ) + src = [ + field.SetEventBit(dragon.check.bit), + field.FinishCheck(), + field.Return(), + ] + call_instr_size = 4 + + src = reward_instructions + + src += [ + field.SetEventBit(dragon.check.bit), + field.FinishCheck(), + field.Return(), + ] + space = Write(Bank.CC, src, f"8 dragons {dragon.name.lower()} receive reward") + receive_reward = space.start_address + + space = Reserve(dragon.countdown_address, dragon.countdown_address + call_instr_size - 1, + f"8 dragons call receive reward {hex(receive_reward)}", field.NOP()) + space.write( + field.Call(receive_reward), + ) def white_dragon_reward_mod(self): # white dragon does not drop its item reward diff --git a/event/esper_mountain.py b/event/esper_mountain.py index 59c209e7..e924de19 100644 --- a/event/esper_mountain.py +++ b/event/esper_mountain.py @@ -1,3 +1,4 @@ +from constants.checks import ESPER_MOUNTAIN from event.event import * class EsperMountain(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.RELM def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(ESPER_MOUNTAIN) def init_event_bits(self, space): space.write( diff --git a/event/event.py b/event/event.py index 11f6ee13..0c1d9b34 100644 --- a/event/event.py +++ b/event/event.py @@ -1,3 +1,6 @@ + +from event.event_reward import RewardType, Reward +from data.item import Item from memory.space import Bank, Space, Reserve, Allocate, Free, Write, Read import data.direction as direction @@ -13,7 +16,6 @@ import instruction.vehicle as vehicle from instruction.event import EVENT_CODE_START -from event.event_reward import RewardType, Reward class Event(): def __init__(self, events, rom, args, dialogs, characters, items, maps, enemies, espers, shops): @@ -41,10 +43,42 @@ def character_gate(self): def characters_required(self): return 1 - def add_reward(self, possible_types): - new_reward = Reward(self, possible_types) - self.rewards.append(new_reward) - return new_reward + def get_reward_type(self, check_info, reward_type = None): + bit = check_info.bit + + assert bit + + if reward_type: + return reward_type + if bit in self.args.character_rewards: + return RewardType.CHARACTER + if bit in self.args.esper_item_rewards: + return RewardType.ESPER | RewardType.ITEM + if bit in self.args.esper_rewards: + return RewardType.ESPER + if bit in self.args.item_rewards: + return RewardType.ITEM + + return check_info.reward_types + + def add_item_reward(self): + reward = Reward(self, RewardType.ITEM) + self.rewards.append(reward) + return reward + + def add_character_reward(self): + reward = Reward(self, RewardType.CHARACTER) + self.rewards.append(reward) + return reward + + def add_reward(self, check, reward_type = None): + possible_types = self.get_reward_type(check, reward_type) + assert possible_types + + reward = Reward(self, possible_types) + reward.check = check + self.rewards.append(reward) + return reward def init_rewards(self): pass @@ -65,7 +99,7 @@ def log_reward(self, reward, prefix = "", suffix = ""): if reward.type == RewardType.CHARACTER: reward_string += self.characters.get_name(reward.id) elif reward.type == RewardType.ESPER: - reward_string += self.espers.get_name(reward.id) + reward_string += "*" + self.espers.get_name(reward.id) elif reward.type == RewardType.ITEM: reward_string += self.items.get_name(reward.id) self.rewards_log.append(reward_string + suffix) @@ -79,6 +113,7 @@ def log_string(self): log_string += f" {', '.join(self.rewards_log)}" if self.changes_log: log_string += '\n' + '\n'.join(self.changes_log) + return log_string def mod(self): diff --git a/event/event_reward.py b/event/event_reward.py index 0334886d..5c9daa0a 100644 --- a/event/event_reward.py +++ b/event/event_reward.py @@ -12,10 +12,26 @@ def __init__(self, event, possible_types): self.type = None self.event = event self.possible_types = possible_types + self.check = None def single_possible_type(self): return self.possible_types in RewardType + def is_type(self, type): + return self.type == type + + def is_none(self): + return self.is_type(RewardType.NONE) + + def is_character(self): + return self.is_type(RewardType.CHARACTER) + + def is_esper(self): + return self.is_type(RewardType.ESPER) + + def is_item(self): + return self.is_type(RewardType.ITEM) + def __str__(self): result = f"{self.id} {self.type} {self.event.name()}" @@ -29,7 +45,7 @@ def __str__(self): return result + " (" + ', '.join(possible_strings) + ")" -def choose_reward(possible_types, characters, espers, items): +def choose_reward(possible_types, characters, espers, items, exclude_character = None): import random all_types = [flag for flag in RewardType] @@ -39,7 +55,7 @@ def choose_reward(possible_types, characters, espers, items): for reward_type in all_types: if reward_type & possible_types: if reward_type == RewardType.CHARACTER and characters.get_available_count(): - return (characters.get_random_available(), reward_type) + return (characters.get_random_available(exclude = exclude_character ), reward_type) elif reward_type == RewardType.ESPER and espers.available(): return (espers.get_random_esper(), reward_type) elif reward_type == RewardType.ITEM: diff --git a/event/events.py b/event/events.py index cb0887b7..421f2b4c 100644 --- a/event/events.py +++ b/event/events.py @@ -1,5 +1,5 @@ from memory.space import Bank, Allocate -from event.event_reward import RewardType, Reward, choose_reward, weighted_reward_choice +from event.event_reward import RewardType, choose_reward, weighted_reward_choice import instruction.field as field class Events(): @@ -15,6 +15,9 @@ def __init__(self, rom, args, data): self.espers = data.espers self.shops = data.shops + self.pathing = "" #used for display but not in logic + self.pathingdict = {} + self.mod() def mod(self): @@ -52,12 +55,16 @@ def mod(self): if self.args.spoiler_log and (event.rewards_log or event.changes_log): log_strings.append(event.log_string()) + + log_strings.append("* = Esper/Magicite") space.write(field.Return()) if self.args.spoiler_log: from log import section section("Events", log_strings, []) + return events + def init_reward_slots(self, events): import random reward_slots = [] @@ -73,7 +80,7 @@ def init_reward_slots(self, events): def choose_single_possible_type_rewards(self, reward_slots): for slot in reward_slots: if slot.single_possible_type(): - slot.id, slot.type = choose_reward(slot.possible_types, self.characters, self.espers, self.items) + slot.id, slot.type = choose_reward(slot.possible_types, self.characters, self.espers, self.items, exclude_character=[slot.check.gate_character]) def choose_char_esper_possible_rewards(self, reward_slots): for slot in reward_slots: @@ -92,8 +99,10 @@ def character_gating_mod(self, events, name_event): # note: this includes start, which can get up to 4 characters self.choose_single_possible_type_rewards(reward_slots) - # find characters that were assigned to start - characters_available = [reward.id for reward in name_event["Start"].rewards] + readily_available_characters = [x.id for x in reward_slots if x.check and x.check.gate_character is None and x.possible_types == RewardType.CHARACTER] + + # find characters that were assigned to start as well as any ungated characters that were assigned in choose_single_possible_reward_types() above + characters_available = [reward.id for reward in name_event["Start"].rewards] + readily_available_characters # find all the rewards that can be a character character_slots = [] @@ -111,7 +120,8 @@ def character_gating_mod(self, events, name_event): unlocked_slot_iterations = [] for slot in character_slots: slot_empty = slot.id is None - gate_char_available = (slot.event.character_gate() in characters_available or slot.event.character_gate() is None) + gate_char = slot.check.gate_character if slot.check else None + gate_char_available = (gate_char in characters_available or gate_char is None) enough_chars_available = len(characters_available) >= slot.event.characters_required() if slot_empty and gate_char_available and enough_chars_available: if slot in slot_iterations: @@ -121,15 +131,27 @@ def character_gating_mod(self, events, name_event): unlocked_slots.append(slot) unlocked_slot_iterations.append(slot_iterations[slot]) + # this means an impossible start has occured. + # i.e. no character can be retrieved given the starting char + check availability + assert len(unlocked_slots) > 0 + # pick slot for the next character weighted by number of iterations each slot has been available slot_index = weighted_reward_choice(unlocked_slot_iterations, iteration) slot = unlocked_slots[slot_index] slot.id = self.characters.get_random_available() slot.type = RewardType.CHARACTER characters_available.append(slot.id) - self.characters.set_character_path(slot.id, slot.event.character_gate()) + self.characters.set_character_path(slot.id, slot.check.gate_character) iteration += 1 + # if self.args.debug: + # for event in events: + # for reward in event.rewards: + # if reward.type == RewardType.CHARACTER: + # self.pathing = self.pathing + "\n" + event.name() + ": " + self.characters.get_name(reward.id) + "/ " + self.characters.get_default_name(reward.id) + # self.pathingdict[self.characters.get_default_name(reward.id)] = event.name() + # self.print_pathing_tree() + # get all reward slots still available reward_slots = [reward for event in events for reward in event.rewards if reward.id is None] random.shuffle(reward_slots) # shuffle to prevent picking them in alphabetical order @@ -161,3 +183,30 @@ def open_world_mod(self, events): # choose the rest of the rewards, items given to events after all characters/events assigned self.choose_item_possible_rewards(reward_slots) + + def print_pathing_tree(self): + pathway_with_chars_list = [] + pathway_list = [] + + for x in range(14): + path = self.characters.get_character_path(x) + pathway_with_chars = "" + pathway = "" + + ### get the path leading to the character's location + for req_char_index in path: + character_location = self.pathingdict[self.characters.DEFAULT_NAME[req_char_index]] + character_name = self.characters.DEFAULT_NAME[req_char_index] + pathway += character_location + " -> " + pathway_with_chars += (character_name + " / " + character_location + " -> ") + + ### get the character's location + character_location = self.pathingdict[self.characters.DEFAULT_NAME[x]] + character_name = self.characters.DEFAULT_NAME[x] + pathway += character_location + pathway_with_chars += (character_name + " / " + character_location) + + print(pathway_with_chars) + pathway_with_chars_list.append(pathway_with_chars) + pathway_list.append(pathway) + diff --git a/event/fanatics_tower.py b/event/fanatics_tower.py index 68089485..cf53ed79 100644 --- a/event/fanatics_tower.py +++ b/event/fanatics_tower.py @@ -1,3 +1,6 @@ +from constants.checks import FANATICS_TOWER_FOLLOWER, FANATICS_TOWER_LEADER +from data.map_event import MapEvent +from data.npc import NPC from event.event import * class FanaticsTower(Event): @@ -7,25 +10,35 @@ def name(self): def character_gate(self): return self.characters.STRAGO + def init_event_bits(self, space): + space.write([ + field.SetEventBit(npc_bit.FANATICS_TOWER_SECONDARY_REWARD), + ]) + def init_rewards(self): - self.reward1 = self.add_reward(RewardType.CHARACTER | RewardType.ESPER) - self.reward2 = self.add_reward(RewardType.ITEM) + self.reward1 = self.add_reward(FANATICS_TOWER_FOLLOWER) + self.reward2 = self.add_reward(FANATICS_TOWER_LEADER) def mod(self): + self.top_treasure_room_id = 0x16e self.strago_npc_id = 0x13 self.strago_npc = self.maps.get_npc(0x16a, self.strago_npc_id) self.relm_event_mod() - self.tower_top_mod() self.magimaster_battle_mod() - if self.reward1.type == RewardType.CHARACTER: + if self.reward1.is_character(): self.character_mod(self.reward1.id) - elif self.reward1.type == RewardType.ESPER: + elif self.reward1.is_esper(): self.esper_mod(self.reward1.id) - elif self.reward1.type == RewardType.ITEM: + elif self.reward1.is_item(): self.item_mod(self.reward1.id) + if self.reward2.is_esper(): + self.tower_top_esper_mod() + elif self.reward2.is_item(): + self.tower_top_item_mod() + self.finish_magimaster_check_mod() self.finish_strago_check_mod() @@ -121,12 +134,62 @@ def relm_event_mod(self): space = Reserve(0xc5407, 0xc5408, "fanatics tower stop relm song before screen fade", field.NOP()) - def tower_top_mod(self): + def tower_top_esper_mod(self): + space = Reserve(0xc5548, 0xc554a, "fanatics tower master kefka's treasure", field.NOP()) + space = Reserve(0xc554d, 0xc554e, "fanatics tower long pause before magic master appears", field.NOP()) + + # Move the chest to inaccessible location, empty it.. + chest = self.maps.get_chests(0x16e)[0] + chest.type = chest.EMPTY + chest.contents = 255 + chest.x = 0 + chest.y = 0 + + # This event flips the bit to trigger the MagiMaster fight outside, + # And also spawns the cultists during the cutscene + # We copy the relevant code from this event to event_space below + self.maps.delete_event(self.top_treasure_room_id, 7, 8) + + # Place magicite in front of the chest + self.magicite_npc = NPC() + self.magicite_npc.sprite = 91 # Magicite + self.magicite_npc.palette = 2 + self.magicite_npc.split_sprite = 1 + self.magicite_npc.direction = direction.UP + self.magicite_npc.x = 7 + self.magicite_npc.y = 8 + self.magicite_npc.event_byte = npc_bit.event_byte(npc_bit.FANATICS_TOWER_SECONDARY_REWARD) + self.magicite_npc.event_bit = npc_bit.event_bit(npc_bit.FANATICS_TOWER_SECONDARY_REWARD) + + self.magicite_npc_id = self.maps.append_npc(self.top_treasure_room_id, self.magicite_npc) + + event_space = Allocate(Bank.CC, 40, "Give fanatics tower esper subroutine", asm.NOP()) + event_space.write([ + Read(0xc5440, 0xc5441), # Set npc bit hex(730) true (top treasure received) + Read(0xc5448, 0xc5449), # Set npc bit hex(1689) true (cultists outside treasure room) + field.AddEsper(self.reward2.id), + field.Dialog(self.espers.get_receive_esper_dialog(self.reward2.id)), + field.ClearEventBit(npc_bit.FANATICS_TOWER_SECONDARY_REWARD), + field.DeleteEntity(self.magicite_npc_id), + field.RefreshEntities(), + field.FinishCheck(), + field.Return(), + ]) + + # Re-using space from removed event above + space = Reserve(0xc5440, 0xc544a, "Call esper subroutine", asm.NOP()) + space.write([ + field.Call(event_space.start_address), + field.Return(), + ]) + self.magicite_npc.set_event_address(space.start_address) + + def tower_top_item_mod(self): space = Reserve(0xc5548, 0xc554a, "fanatics tower master kefka's treasure", field.NOP()) space = Reserve(0xc554d, 0xc554e, "fanatics tower long pause before magic master appears", field.NOP()) self.item = self.reward2.id - self.maps.set_chest_item(0x16e, 7, 7, self.item) + self.maps.set_chest_item(self.top_treasure_room_id, 7, 7, self.item) def magimaster_battle_mod(self): boss_pack_id = self.get_boss("MagiMaster") diff --git a/event/figaro_castle_wob.py b/event/figaro_castle_wob.py index 9b151f56..906457b5 100644 --- a/event/figaro_castle_wob.py +++ b/event/figaro_castle_wob.py @@ -1,17 +1,15 @@ from event.event import * +from constants.checks import FIGARO_CASTLE_THRONE class FigaroCastleWOB(Event): def name(self): - return "Figaro Castle WOB" + return FIGARO_CASTLE_THRONE.name def character_gate(self): return self.characters.EDGAR def init_rewards(self): - if self.args.no_free_characters_espers: - self.reward = self.add_reward(RewardType.ITEM) - else: - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(FIGARO_CASTLE_THRONE) def init_event_bits(self, space): space.write( diff --git a/event/figaro_castle_wor.py b/event/figaro_castle_wor.py index b0beeefb..a7cf76f6 100644 --- a/event/figaro_castle_wor.py +++ b/event/figaro_castle_wor.py @@ -1,14 +1,15 @@ from event.event import * +from constants.checks import FIGARO_CASTLE_ENGINE class FigaroCastleWOR(Event): def name(self): - return "Figaro Castle WOR" + return FIGARO_CASTLE_ENGINE.name def character_gate(self): return self.characters.EDGAR def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(FIGARO_CASTLE_ENGINE) def init_event_bits(self, space): if self.args.character_gating: diff --git a/event/floating_continent.py b/event/floating_continent.py index 927107cb..d75701ae 100644 --- a/event/floating_continent.py +++ b/event/floating_continent.py @@ -15,9 +15,11 @@ def character_gate(self): return self.characters.SHADOW def init_rewards(self): - self.reward1 = self.add_reward(RewardType.CHARACTER | RewardType.ESPER) - self.reward2 = self.add_reward(RewardType.ESPER | RewardType.ITEM) - self.reward3 = self.add_reward(RewardType.CHARACTER | RewardType.ESPER) + from constants.checks import FLOATING_CONT_ARRIVE, FLOATING_CONT_BEAST, FLOATING_CONT_ESCAPE + + self.reward1 = self.add_reward(FLOATING_CONT_ARRIVE) + self.reward2 = self.add_reward(FLOATING_CONT_BEAST) + self.reward3 = self.add_reward(FLOATING_CONT_ESCAPE) def mod(self): self.shadow_leaves_mod() @@ -35,6 +37,8 @@ def mod(self): self.ground_character_mod(self.reward1.id) elif self.reward1.type == RewardType.ESPER: self.ground_esper_mod(self.reward1.id) + elif self.reward1.type == RewardType.ITEM: + self.ground_item_mod(self.reward1.id) self.finish_ground_check() self.save_point_hole_mod() @@ -54,6 +58,8 @@ def mod(self): self.escape_character_mod(self.reward3.id) elif self.reward3.type == RewardType.ESPER: self.escape_esper_mod(self.reward3.id) + elif self.reward3.type == RewardType.ITEM: + self.escape_item_mod(self.reward3.id) self.log_reward(self.reward1) self.log_reward(self.reward2) @@ -183,7 +189,7 @@ def ground_character_mod(self, character): field.FadeInScreen(), ) - def ground_esper_mod(self, esper): + def ground_esper_mod(self, esper_id): self.ground_shadow_npc.sprite = 91 self.ground_shadow_npc.palette = 2 self.ground_shadow_npc.split_sprite = 1 @@ -191,12 +197,27 @@ def ground_esper_mod(self, esper): space = Reserve(0xad9b1, 0xad9ed, "floating continent add esper on ground", field.NOP()) space.write( - field.AddEsper(esper), - field.Dialog(self.espers.get_receive_esper_dialog(esper)), + field.AddEsper(esper_id), + field.Dialog(self.espers.get_receive_esper_dialog(esper_id)), field.DeleteEntity(self.ground_shadow_npc_id), field.Branch(space.end_address + 1), ) + def ground_item_mod(self, item_id): + self.ground_shadow_npc.sprite = 106 + self.ground_shadow_npc.palette = 6 + self.ground_shadow_npc.split_sprite = 1 + self.ground_shadow_npc.direction = direction.DOWN + + space = Reserve(0xad9b1, 0xad9ed, "floating continent add item on ground", field.NOP()) + space.write( + field.AddItem(item_id), + field.Dialog(self.items.get_receive_dialog(item_id)), + field.DeleteEntity(self.ground_shadow_npc_id), + field.Branch(space.end_address + 1), + ) + + def finish_ground_check(self): src = [ Read(0xad9ee, 0xad9f2), # clear ground npc bit, set shadow recruited bit, update party leader @@ -449,10 +470,9 @@ def escape_character_mod(self, character): field.FadeInScreen(), ]) - def escape_esper_mod(self, esper): + def escape_esper_item_mod(self): # use guest character to give esper reward guest_char_id = 0x0f - guest_char = self.maps.get_npc(0x189, guest_char_id) random_sprite = self.characters.get_random_esper_item_sprite() random_sprite_palette = self.characters.get_palette(random_sprite) @@ -464,10 +484,26 @@ def escape_esper_mod(self, esper): field.RefreshEntities(), ) + return guest_char_id + + def escape_esper_mod(self, esper_id): + guest_char_id = self.escape_esper_item_mod() + self.escape_mod(guest_char_id, [ field.DeleteEntity(guest_char_id), field.RefreshEntities(), field.LoadMap(0x06, direction.DOWN, default_music = True, x = 16, y = 6, fade_in = True, entrance_event = True), - field.AddEsper(esper), - field.Dialog(self.espers.get_receive_esper_dialog(esper)), + field.AddEsper(esper_id), + field.Dialog(self.espers.get_receive_esper_dialog(esper_id)), + ]) + + def escape_item_mod(self, item_id): + guest_char_id = self.escape_esper_item_mod() + + self.escape_mod(guest_char_id, [ + field.DeleteEntity(guest_char_id), + field.RefreshEntities(), + field.LoadMap(0x06, direction.DOWN, default_music = True, x = 16, y = 6, fade_in = True, entrance_event = True), + field.AddItem(item_id), + field.Dialog(self.items.get_receive_dialog(item_id)), ]) diff --git a/event/gau_father_house.py b/event/gau_father_house.py index f1bb4ea6..d5cf3253 100644 --- a/event/gau_father_house.py +++ b/event/gau_father_house.py @@ -1,17 +1,15 @@ from event.event import * +from constants.checks import GAUS_FATHERS_HOUSE class GauFatherHouse(Event): def name(self): - return "Gau Father House" + return GAUS_FATHERS_HOUSE.name def character_gate(self): return self.characters.SHADOW def init_rewards(self): - if self.args.no_free_characters_espers: - self.reward = self.add_reward(RewardType.ITEM) - else: - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(GAUS_FATHERS_HOUSE) def mod(self): self.shadow_npc_id = 0x10 diff --git a/event/imperial_camp.py b/event/imperial_camp.py index 8699478f..5d91d06a 100644 --- a/event/imperial_camp.py +++ b/event/imperial_camp.py @@ -1,3 +1,4 @@ +from constants.checks import IMPERIAL_CAMP from event.event import * class ImperialCamp(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.SABIN def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(IMPERIAL_CAMP) def init_event_bits(self, space): space.write( diff --git a/event/kefka_tower.py b/event/kefka_tower.py index ba1b1264..af3b2f65 100644 --- a/event/kefka_tower.py +++ b/event/kefka_tower.py @@ -1,12 +1,12 @@ +from constants.checks import KEFKAS_TOWER_CELL_BEAST from event.event import * -import args class KefkaTower(Event): def name(self): return "Kefka's Tower" def init_rewards(self): - self.atma_reward = self.add_reward(RewardType.ITEM) + self.atma_reward = self.add_reward(KEFKAS_TOWER_CELL_BEAST) def init_event_bits(self, space): space.write( diff --git a/event/kohlingen.py b/event/kohlingen.py index 0c366e32..6c616c2f 100644 --- a/event/kohlingen.py +++ b/event/kohlingen.py @@ -1,20 +1,18 @@ from event.event import * +from constants.checks import KOHLINGEN_CAFE # TODO use setzer npc instead of shadow's # for character reward show the animations setzer does in wor before daryl's tomb class Kohlingen(Event): def name(self): - return "Kohlingen" + return KOHLINGEN_CAFE.name def character_gate(self): return self.characters.SETZER def init_rewards(self): - if self.args.no_free_characters_espers: - self.reward = self.add_reward(RewardType.ITEM) - else: - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(KOHLINGEN_CAFE) def init_event_bits(self, space): space.write( diff --git a/event/lete_river.py b/event/lete_river.py index 09b9cf08..ffd239ba 100644 --- a/event/lete_river.py +++ b/event/lete_river.py @@ -1,3 +1,4 @@ +from constants.checks import LETE_RIVER from event.event import * class LeteRiver(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.TERRA def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(LETE_RIVER) def init_event_bits(self, space): space.write( diff --git a/event/lone_wolf.py b/event/lone_wolf.py index ce93f05b..cef68130 100644 --- a/event/lone_wolf.py +++ b/event/lone_wolf.py @@ -1,4 +1,11 @@ +from email.policy import default +from constants.checks import LONE_WOLF_CHASE, LONE_WOLF_MOOGLE_ROOM +from data.map_event import MapEvent from event.event import * +from music.song_utils import get_character_theme + +CHAR_ITEM = RewardType.CHARACTER | RewardType.ITEM +ESPER_ITEM = RewardType.ESPER | RewardType.ITEM class LoneWolf(Event): def name(self): @@ -8,8 +15,15 @@ def character_gate(self): return self.characters.MOG def init_rewards(self): - self.reward1 = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) - self.reward2 = self.add_reward(RewardType.ITEM) + import random + + # Ensure the two checks can only reward one character and one esper. + reward_char_type = bool(random.getrandbits(1)) + reward1_type = CHAR_ITEM if reward_char_type else ESPER_ITEM + reward2_type = ESPER_ITEM if reward_char_type else CHAR_ITEM + + self.reward1 = self.add_reward(LONE_WOLF_CHASE, reward1_type) + self.reward2 = self.add_reward(LONE_WOLF_MOOGLE_ROOM, reward2_type) def init_event_bits(self, space): space.write( @@ -17,6 +31,22 @@ def init_event_bits(self, space): field.ClearEventBit(npc_bit.MOG_MOOGLE_ROOM_WOR), ) + # Load into narshe cliffs (map 23, coordinates 25,20) and lone wolf will be triggerable + # After choosing lone wolf or mog, exit to the previous map and re-enter the cliff + # It will then take you to the room before the Moogle Room to easily test the full flow + # if self.args.debug: + # space.write( + # field.SetEventBit(572), # Spectate lone wolf cross the bridge to cliff + # field.ClearEventBit(573), # Haven't witnessed lone wolf event + # field.SetEventBit(831), # Visibility bits for lone wolf npcs + # field.SetEventBit(832), + # ) + # exit_maps = [x for x in self.maps.exits.long_exits if x.dest_map == 23] + # exit = next(iter(exit_maps)) + # exit.dest_map = 37 + # exit.dest_x = 14 + # exit.dest_y = 10 + def mod(self): self.mog_npc_id = 0x1c self.mog_npc = self.maps.get_npc(0x017, self.mog_npc_id) @@ -39,11 +69,20 @@ def mod(self): self.esper_mod(self.reward1.id) elif self.reward1.type == RewardType.ITEM: self.item_mod(self.reward1.id) - self.alternative_item_mod() - self.finish_check_mod() + + if self.reward2.type == RewardType.CHARACTER: + self.alternative_character_mod(self.reward2.id) + self.moogle_room_reward_mod() + elif self.reward2.type == RewardType.ESPER: + self.alternative_esper_mod(self.reward2.id) + self.moogle_room_reward_mod() + elif self.reward2.type == RewardType.ITEM: + self.alternative_item_mod(self.reward2.id) + self.moogle_room_reward_mod() self.moogle_room_entrance_event_mod() - self.moogle_room_reward_mod() + self.lone_wolf_hide_mod() + self.finish_check_mod() self.log_reward(self.reward1) self.log_reward(self.reward2) @@ -80,6 +119,9 @@ def character_mod(self, character): field.RecruitCharacter(character), ) + space = Reserve(0xcd607, 0xcd607, "Song played when recruiting mog") + space.write(get_character_theme(character)) + # move lone wolf falling up to make room for adding character # skip copying lone wolf take this dialog at [0xcd693,0xcd695] space = Reserve(0xcd61b, 0xcd67b, "lone wolf mog dialog and naming", field.NOP()) @@ -142,19 +184,166 @@ def item_mod(self, item): field.Dialog(self.items.get_receive_dialog(item)), ]) - def alternative_item_mod(self): - # item lone wolf will give as a reward for not picking self.reward1 - import data.text - item_name = data.text.convert(self.items.get_name(self.reward2.id), data.text.TEXT1) # item names are stored as TEXT2, dialogs are TEXT1 + def lone_wolf_character_sprite_mod(self, character): + map_sprites = [ + [20, 0x29], # Narshe: South Exterior (WoB) + [21, 0x10], # Narshe: North Exterior (WoB) + [23, 0x1a], # Narshe: Cliffs (WoB) + [23, 0x1b], # Narshe: Cliffs (WoB) + [30, 0x25], # Narshe: Chest room + [44, 0x10] # Narshe: Moove cave (WoR) + ] - self.dialogs.set_text(1765, "< >Grrrr…< >You'll never get this< >“" + item_name + "”!") - self.dialogs.set_text(1742, "< >Got “" + item_name + "”!") + for map_id, npc_id in map_sprites: + npc = self.maps.get_npc(map_id, npc_id) + npc.sprite = character + npc.palette = self.characters.get_palette(character) + + # lone wolf doesnt jump, but instead joins the party.. + def alternative_character_mod(self, character): + self.lone_wolf_dialog_character_mod(character) + self.lone_wolf_character_sprite_mod(character) + + # This will skip mog's fall/pause animations to view lone wolf cutscene + # if self.args.debug: + # space = Reserve(0xcd5a3, 0xcd5a3, "wait 30 frames (0.5s)", field.NOP()) + # space = Reserve(0xcd5a8, 0xcd5a8, "wait 30 frames (0.5s)", field.NOP()) + # space = Reserve(0xcd5ab, 0xcd5b3, "mog falling", field.NOP()) + + space = Reserve(0xcd594, 0xcd59f, "Lone wolf gives item, mog", field.NOP()) + space = Reserve(0xcd5a4, 0xcd5a7, "Party runs at mog", field.NOP()) + space = Reserve(0xcd5b6, 0xcd5bc, "Party running after Mog", field.NOP()) + space = Reserve(0xcd5bd, 0xcd5bd, "wait 60 frames (1s)", field.NOP()) + space = Reserve(0x0cd5c1, 0xcd5d0, "Lone wolf object script, jumps off cliff, plays sound", field.NOP()) + + # pick lone wolf up + src = [ + field.HideEntity(self.invisible_bridge_block_npc_id), + field.EntityAct(field_entity.PARTY0, False, + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.DisableWalkingAnimation(), + field_entity.Move(direction.LEFT, 1) + ), + # LW - Jump up from cliff + field.EntityAct(self.lone_wolf_npc_id, True, + field_entity.SetSpeed(field_entity.Speed.FAST), + field_entity.AnimateLowJump(), + field_entity.Move(direction.LEFT, 1), + field_entity.Turn(direction.DOWN), + field_entity.Pause(10) + ), + # LW - Blinks + field.EntityAct(self.lone_wolf_npc_id, True, + field_entity.AnimateCloseEyes(), + field_entity.Pause(1), + field_entity.Turn(direction.DOWN), + field_entity.Pause(1), + field_entity.AnimateCloseEyes(), + field_entity.Pause(1), + field_entity.Turn(direction.DOWN), + field_entity.Pause(5), + ), + # Party - Blinks + field.EntityAct(field_entity.PARTY0, True, + field_entity.AnimateCloseEyes(), + field_entity.Pause(1), + field_entity.Turn(direction.DOWN), + field_entity.Pause(1), + field_entity.AnimateCloseEyes(), + field_entity.Pause(1), + field_entity.Turn(direction.DOWN), + field_entity.Pause(5), + ), + + # LW - Attacks player + field.EntityAct(self.lone_wolf_npc_id, False, + field_entity.AnimateAttack(), + field_entity.AnimateLowJump(), + field_entity.Pause(3), + field_entity.Turn(direction.DOWN), + field_entity.EnableWalkingAnimation() + ), + # Player - React to attack + field.EntityAct(field_entity.PARTY0, True, + field_entity.Pause(2), + field_entity.Turn(direction.DOWN), + field_entity.AnimateSurprised(), + field_entity.AnimateLowJump(), + field_entity.Pause(12), + field_entity.Turn(direction.DOWN), + ), + field.SetEventBit(event_bit.TEMP_SONG_OVERRIDE), + field.StartSong(get_character_theme(character)), + + # LW - Laugh in player's face + field.EntityAct(self.lone_wolf_npc_id, True, + field_entity.LaughingOne(), + field_entity.Pause(1), + field_entity.LaughingTwo(), + field_entity.Pause(1), + field_entity.LaughingOne(), + field_entity.Pause(1), + field_entity.LaughingTwo(), + field_entity.Pause(1), + field_entity.LaughingOne(), + field_entity.Pause(1), + field_entity.LaughingTwo(), + field_entity.Pause(1), + field_entity.LaughingOne(), + field_entity.Pause(1), + field_entity.LaughingTwo(), + field_entity.Pause(1), + field_entity.Turn(direction.DOWN), + field_entity.Pause(12), + field_entity.AnimateFrontRightHandOnHead(), + field_entity.Pause(4), + field_entity.AnimateFrontRightHandUp(), + field_entity.Pause(8), + ), + field.HideEntity(self.lone_wolf_npc_id), + field.RecruitAndSelectParty(character), + field.RefreshEntities(), + field.FadeInScreen(), + field.FinishCheck(), + field.ClearEventBit(event_bit.TEMP_SONG_OVERRIDE), + field.Return() + ] + + character_space = Write(Bank.F0, src, "Recruit lone wolf") + + space.write([ + field.Call(character_space.start_address) + ]) + + def alternative_esper_mod(self, esper): + # esper lone wolf will give as a reward for not picking self.reward1 + self.lone_wolf_dialog_esper_mod(esper) + + esper_space = Allocate(Bank.CC, 50, "Receive esper from lone wolf", field.NOP()) + esper_space.write([ + field.AddEsper(esper, sound_effect=True), + field.Dialog(self.espers.get_receive_esper_dialog(esper)), + Read(0xcd59d, 0xcd59d), # Wait 30 frames + field.Return() + ]) + + space = Reserve(0xcd59a, 0xcd59f, "received item dialog; wait 0.5s; take item from lone wolf", field.NOP()) + space.write([ + field.Call(esper_space.start_address) + ]) + + def alternative_item_mod(self, item): + self.lone_wolf_dialog_item_mod(item) + + # item lone wolf will give as a reward for not picking self.reward1 space = Reserve(0xcd59f, 0xcd59f, "lone wolf item received", field.NOP()) space.write( - self.reward2.id, + item ) + # add pause after lone wolf jumps to wait for falling sound effect + def lone_wolf_hide_mod(self): space = Reserve(0xcd5be, 0xcd5c0, "item chosen dialog before lone wolf falls", field.NOP()) space.write( field.SetEventBit(npc_bit.MOG_MOOGLE_ROOM_WOR), @@ -173,10 +362,30 @@ def alternative_item_mod(self): space = Reserve(0xcd5d1, 0xcd5d6, "lone wolf hide npcs after fall", field.NOP()) space.write( - field.Call(hide_npcs), - field.Pause(1.5), + field.Call(hide_npcs) ) + def lone_wolf_dialog_character_mod(self, character): + import data.text + character_name = data.text.convert(self.characters.get_name(character), data.text.TEXT1) # data.text.convert(self.items.get_name(self.reward2.id), data.text.TEXT1) # item names are stored as TEXT2, dialogs are TEXT1 + + self.dialogs.set_text(1765, "< >Grrrr…< >You'll never get this< >" + character_name + "!") + # this dialog is explicitly not called later on when recruiting the character in the cave + self.dialogs.set_text(1742, f" Recruited {character_name}!”") + + def lone_wolf_dialog_esper_mod(self, esper): + import data.text + esper_name = data.text.convert(self.espers.get_name(esper), data.text.TEXT1) # item names are stored as TEXT2, dialogs are TEXT1 + + self.dialogs.set_text(1765, "< >Grrrr…< >You'll never get this< >“" + esper_name + "”!") + self.dialogs.set_text(1742, f" Received the Magicite “{esper_name}.”") + + def lone_wolf_dialog_item_mod(self, item): + import data.text + item_name = data.text.convert(self.items.get_name(item), data.text.TEXT1) # item names are stored as TEXT2, dialogs are TEXT1 + + self.dialogs.set_text(1765, "< >Grrrr…< >You'll never get this< >“" + item_name + "”!") + self.dialogs.set_text(1742, "< >Got “" + item_name + "”!") def finish_check_mod(self): src = [ field.ClearEventBit(npc_bit.LONE_WOLF_MOG_NARSHE_CLIFF), @@ -199,21 +408,39 @@ def finish_check_mod(self): def moogle_room_character_mod(self, character): src = [ - field.RecruitAndSelectParty(character), + field.SetEventBit(event_bit.TEMP_SONG_OVERRIDE), + field.StartSong(get_character_theme(character)), + + field.EntityAct(self.mog_moogle_room_npc_id, True, + field_entity.AnimateSurprised(), + field_entity.Pause(8), + field_entity.AnimateStandingFront(), + ), + field.RecruitAndSelectParty(character), field.HideEntity(self.mog_moogle_room_npc_id), field.ClearEventBit(npc_bit.MOG_MOOGLE_ROOM_WOR), field.SetEventBit(event_bit.GOT_BOTH_REWARDS_LONE_WOLF), field.RefreshEntities(), field.FadeInScreen(), field.FinishCheck(), + field.ClearEventBit(event_bit.TEMP_SONG_OVERRIDE), field.Return(), ] - space = Write(Bank.CC, src, "lone wolf moogle room npc character reward") + + space = Write(Bank.F0, src, "lone wolf moogle room npc character reward") return space.start_address def moogle_room_esper_item_mod(self, esper_item_instructions): src = [ + field.DisableEntityCollision(self.mog_moogle_room_npc_id), + field.EntityAct(self.mog_moogle_room_npc_id, True, + field_entity.AnimateLowJump(), + field_entity.Pause(8), + field_entity.SetSpeed(field_entity.Speed.FASTEST), + field_entity.Move(direction.DOWN, 8), + ), + esper_item_instructions, field.FadeOutScreen(), @@ -225,7 +452,8 @@ def moogle_room_esper_item_mod(self, esper_item_instructions): field.FinishCheck(), field.Return(), ] - space = Write(Bank.CC, src, "lone wolf moogle room npc esper/item reward") + + space = Write(Bank.F0, src, "lone wolf moogle room npc esper/item reward") return space.start_address def moogle_room_esper_mod(self, esper): @@ -241,41 +469,32 @@ def moogle_room_item_mod(self, item): ]) def moogle_room_reward_mod(self): - receive_reward = field.RETURN + mog_reward = field.RETURN if self.reward1.type == RewardType.CHARACTER: - receive_reward = self.moogle_room_character_mod(self.reward1.id) + mog_reward = self.moogle_room_character_mod(self.reward1.id) elif self.reward1.type == RewardType.ESPER: - receive_reward = self.moogle_room_esper_mod(self.reward1.id) + mog_reward = self.moogle_room_esper_mod(self.reward1.id) elif self.reward1.type == RewardType.ITEM: - receive_reward = self.moogle_room_item_mod(self.reward1.id) + mog_reward = self.moogle_room_item_mod(self.reward1.id) + + lone_wolf_reward = field.RETURN + if self.reward2.type == RewardType.CHARACTER: + lone_wolf_reward = self.moogle_room_character_mod(self.reward2.id) + if self.reward2.type == RewardType.ESPER: + lone_wolf_reward = self.moogle_room_esper_mod(self.reward2.id) + if self.reward2.type == RewardType.ITEM: + lone_wolf_reward = self.moogle_room_item_mod(self.reward2.id) src = [ field.BranchIfEventBitSet(event_bit.RECRUITED_MOG_WOB, "LONE_WOLF_FELL"), - field.EntityAct(self.mog_moogle_room_npc_id, True, - field_entity.AnimateSurprised(), - field_entity.Pause(8), - field_entity.AnimateStandingFront(), - ), - field.Call(receive_reward), + field.Call(mog_reward), field.Return(), - "LONE_WOLF_FELL", - field.DisableEntityCollision(self.mog_moogle_room_npc_id), - field.EntityAct(self.mog_moogle_room_npc_id, True, - field_entity.AnimateLowJump(), - field_entity.Pause(8), - field_entity.SetSpeed(field_entity.Speed.FASTEST), - field_entity.Move(direction.DOWN, 8), - ), - field.AddItem(self.reward2.id), - field.Dialog(1742), - field.HideEntity(self.mog_moogle_room_npc_id), - field.ClearEventBit(npc_bit.MOG_MOOGLE_ROOM_WOR), - field.SetEventBit(event_bit.GOT_BOTH_REWARDS_LONE_WOLF), - field.FinishCheck(), - field.Return(), + field.Call(lone_wolf_reward), + field.Return() ] - space = Write(Bank.CC, src, "lone wolf npc event second reward not chosen") + + space = Write(Bank.F0, src, "lone wolf npc event second reward not chosen") npc_event = space.start_address space = Reserve(0xc396c, 0xc3970, "lone wolf npc not saved second reward", field.NOP()) diff --git a/event/magitek_factory.py b/event/magitek_factory.py index edaca95a..bf46dd00 100644 --- a/event/magitek_factory.py +++ b/event/magitek_factory.py @@ -8,9 +8,10 @@ def character_gate(self): return self.characters.CELES def init_rewards(self): - self.reward1 = self.add_reward(RewardType.ESPER | RewardType.ITEM) - self.reward2 = self.add_reward(RewardType.ESPER | RewardType.ITEM) - self.reward3 = self.add_reward(RewardType.CHARACTER | RewardType.ESPER) + from constants.checks import MAGITEK_FACTORY_TRASH, MAGITEK_FACTORY_GUARD, MAGITEK_FACTORY_FINISH + self.reward1 = self.add_reward(MAGITEK_FACTORY_TRASH) + self.reward2 = self.add_reward(MAGITEK_FACTORY_GUARD) + self.reward3 = self.add_reward(MAGITEK_FACTORY_FINISH) def init_event_bits(self, space): space.write( @@ -47,6 +48,8 @@ def mod(self): self.character_mod(self.reward3.id) elif self.reward3.type == RewardType.ESPER: self.esper_mod(self.reward3.id) + elif self.reward3.type == RewardType.ITEM: + self.item_mod(self.reward3.id) self.crane_battle_mod() self.after_cranes_mod() @@ -320,6 +323,34 @@ def esper_mod(self, esper): field.Branch(space.end_address + 1), # skip nops ) + def esper_item_mod(self): + self.setzer_npc.sprite = self.characters.get_random_esper_item_sprite() + self.setzer_npc.palette = self.characters.get_palette(self.setzer_npc.sprite) + + space = Reserve(0xc819b, 0xc8302, "magitek factory add char and kefka cranes scene", field.NOP()) + return (space) + + + def esper_mod(self, esper): + (space) = self.esper_item_mod() + space.write( + field.AddEsper(esper), + field.Dialog(self.espers.get_receive_esper_dialog(esper)), + field.FadeOutScreen(), + field.WaitForFade(), + field.Branch(space.end_address + 1), # skip nops + ) + + def item_mod(self, item): + (space) = self.esper_item_mod() + space.write( + field.AddItem(item), + field.Dialog(self.items.get_receive_dialog(item)), + field.FadeOutScreen(), + field.WaitForFade(), + field.Branch(space.end_address + 1), # skip nops + ) + def crane_battle_mod(self): boss_pack_id = self.get_boss("Cranes") diff --git a/event/mobliz_wor.py b/event/mobliz_wor.py index d794ff8d..4fe45202 100644 --- a/event/mobliz_wor.py +++ b/event/mobliz_wor.py @@ -8,7 +8,8 @@ def character_gate(self): return self.characters.TERRA def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + from constants.checks import MOBLIZ_ATTACK + self.reward = self.add_reward(MOBLIZ_ATTACK) def init_event_bits(self, space): space.write( diff --git a/event/mt_kolts.py b/event/mt_kolts.py index cbaff87d..1835aaed 100644 --- a/event/mt_kolts.py +++ b/event/mt_kolts.py @@ -1,3 +1,4 @@ +from constants.checks import MT_KOLTS from event.event import * class MtKolts(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.SABIN def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(MT_KOLTS) def mod(self): self.vargas_npc_id = 0x10 diff --git a/event/mt_zozo.py b/event/mt_zozo.py index fdbdc6fb..e1af7dcc 100644 --- a/event/mt_zozo.py +++ b/event/mt_zozo.py @@ -1,3 +1,4 @@ +from constants.checks import MT_ZOZO from event.event import * class MtZozo(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.CYAN def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(MT_ZOZO) def init_event_bits(self, space): space.write( diff --git a/event/narshe_battle.py b/event/narshe_battle.py index 59777176..8fc592ee 100644 --- a/event/narshe_battle.py +++ b/event/narshe_battle.py @@ -1,3 +1,4 @@ +from constants.checks import NARSHE_BATTLE from event.event import * class NarsheBattle(Event): @@ -8,7 +9,7 @@ def characters_required(self): return 2 def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(NARSHE_BATTLE) def init_event_bits(self, space): space.write( diff --git a/event/narshe_wor.py b/event/narshe_wor.py index ce54f84d..21070c68 100644 --- a/event/narshe_wor.py +++ b/event/narshe_wor.py @@ -1,18 +1,17 @@ from event.event import * +from constants.checks import NARSHE_WEAPON_SHOP, NARSHE_WEAPON_SHOP_MINES class NarsheWOR(Event): def name(self): - return "Narshe WOR" + return NARSHE_WEAPON_SHOP.name def character_gate(self): return self.characters.LOCKE def init_rewards(self): - if self.args.no_free_characters_espers: - self.reward1 = self.add_reward(RewardType.ITEM) - else: - self.reward1 = self.add_reward(RewardType.ESPER | RewardType.ITEM) - self.reward2 = self.add_reward(RewardType.ITEM) + + self.reward1 = self.add_reward(NARSHE_WEAPON_SHOP) + self.reward2 = self.add_reward(NARSHE_WEAPON_SHOP_MINES) def init_event_bits(self, space): space.write( @@ -30,11 +29,10 @@ def mod(self): # allow doors to be unlocked without any condition self.unlock_doors_condition_mod([]) - self.item = self.reward2.id if self.reward1.type == RewardType.ESPER: - self.weapon_shop_esper_mod(self.reward1.id) + self.weapon_shop_esper_mod() elif self.reward1.type == RewardType.ITEM: - self.weapon_shop_item_mod(self.reward1.id) + self.weapon_shop_item_mod() self.cursed_shield_mod() @@ -102,13 +100,24 @@ def add_gating_condition(self): field.BranchIfEventBitClear(event_bit.character_recruited(self.character_gate()), LOCKED), ]) - def weapon_shop_mod(self, dialog_first_choice_text, reward_instructions): + def weapon_shop_mod(self, dialog_first_line): space = Reserve(0xc0b24, 0xc0b26, "narshe wor i wanted to give you this", field.NOP()) import data.text + reward1_esper = self.reward1.type == RewardType.ESPER + reward2_esper = self.reward2.type == RewardType.ESPER # item names stored as TEXT2, dialogs are TEXT1 - item_name = data.text.convert(self.items.get_name(self.item), data.text.TEXT1) - self.dialogs.set_text(1519, dialog_first_choice_text + " Make it “" + item_name + "”") + reward1_esper_line = lambda esper: f" Leave it the stone “" + data.text.convert(self.espers.get_name(esper) + "”", data.text.TEXT1) + reward2_esper_line = lambda esper: f" Make it the stone “" + data.text.convert(self.espers.get_name(esper) + "”", data.text.TEXT1) + get_item_line = lambda item: f" Make it “" + data.text.convert(self.items.get_name(item) + "”", data.text.TEXT1) + + add_esper = lambda esper: field.AddEsper(esper, sound_effect = False) + add_item = lambda item: field.AddItem(item, sound_effect = False) + + reward1 = reward1_esper_line(self.reward1.id) if reward1_esper else get_item_line(self.reward1.id) + reward2 = reward2_esper_line(self.reward2.id) if reward2_esper else get_item_line(self.reward2.id) + + self.dialogs.set_text(1519, dialog_first_line + reward1 + reward2 + "") # if esper or first item chosen, set event bit to know second item should be given by guard space = Reserve(0xc0b42, 0xc0b44, "narshe wor ragnarok esper right", field.NOP()) @@ -121,7 +130,8 @@ def weapon_shop_mod(self, dialog_first_choice_text, reward_instructions): ) src = [ - reward_instructions, + # add reward 1 based on item type + add_esper(self.reward1.id) if reward1_esper else add_item(self.reward1.id), field.SetEventBit(event_bit.GOT_RAGNAROK), field.SetEventBit(event_bit.CHOSE_RAGNAROK_ESPER), field.FinishCheck(), @@ -136,7 +146,8 @@ def weapon_shop_mod(self, dialog_first_choice_text, reward_instructions): ) src = [ - field.AddItem(self.item, sound_effect = False), + # add reward 2 based on item type + add_esper(self.reward2.id) if reward2_esper else add_item(self.reward2.id), field.SetEventBit(event_bit.GOT_RAGNAROK), field.FinishCheck(), field.Return(), @@ -149,7 +160,7 @@ def weapon_shop_mod(self, dialog_first_choice_text, reward_instructions): field.Call(choose_second_option), ) - def esper_room_mod(self, esper_item_instructions): + def behind_whelk_mod(self): from data.npc import NPC guard_npc = NPC() guard_npc.x = 74 @@ -158,20 +169,38 @@ def esper_room_mod(self, esper_item_instructions): guard_npc.palette = 0 guard_npc.direction = direction.DOWN guard_npc.speed = 3 - guard_npc.event_byte = 0x60 - guard_npc.event_bit = 0x05 + guard_npc.event_byte = npc_bit.event_byte(npc_bit.WHELK_GUARD_TRITOCH_NARSHE_WOB) # 0x60 + guard_npc.event_bit = npc_bit.event_bit(npc_bit.WHELK_GUARD_TRITOCH_NARSHE_WOB) # 0x05 guard_npc_id = self.maps.append_npc(0x02b, guard_npc) + add_esper = lambda esper_id: [ + field.AddEsper(esper_id), + field.Dialog(self.espers.get_receive_esper_dialog(esper_id)), + ] + + add_item = lambda item_id: [ + field.AddItem(item_id), + field.Dialog(self.items.get_receive_dialog(item_id)) + ] src = [ - field.BranchIfEventBitSet(event_bit.CHOSE_RAGNAROK_ESPER, "RECEIVE_ITEM"), - esper_item_instructions, + field.BranchIfEventBitSet(event_bit.CHOSE_RAGNAROK_ESPER, "RECEIVE_SECONDARY") + ] + + # Add reward one based on reward type + src += add_esper(self.reward1.id) if self.reward1.type == RewardType.ESPER else add_item(self.reward1.id) + + src += [ field.Branch("DELETE_GUARD"), + "RECEIVE_SECONDARY", + ] - "RECEIVE_ITEM", - field.AddItem(self.item), - field.Dialog(self.items.get_receive_dialog(self.item)), + # Add reward two based on reward type + src += [ + add_esper(self.reward2.id) if self.reward2.type == RewardType.ESPER else add_item(self.reward2.id), + ] + src += [ "DELETE_GUARD", field.FadeOutScreen(), field.WaitForFade(), @@ -183,24 +212,17 @@ def esper_room_mod(self, esper_item_instructions): field.FinishCheck(), field.Return(), ] + space = Write(Bank.CC, src, "narshe wor second weapon shop reward guard npc event") guard_event = space.start_address guard_npc.set_event_address(guard_event) - def weapon_shop_esper_mod(self, esper): - dialog_text = "This stone gives off an eerie aura! Leave it the stone “" + self.espers.get_name(esper) + "”" + def weapon_shop_esper_mod(self): + self.weapon_shop_mod("This stone gives off an eerie aura!") + self.behind_whelk_mod() - self.weapon_shop_mod(dialog_text, [ - field.AddEsper(esper, sound_effect = False), - ]) - - self.esper_room_mod([ - field.AddEsper(esper), - field.Dialog(self.espers.get_receive_esper_dialog(esper)), - ]) - - def weapon_shop_item_mod(self, item): + def weapon_shop_item_mod(self): magicite_npc_id = 0x11 magicite_npc = self.maps.get_npc(0x18, magicite_npc_id) magicite_npc.sprite = 106 @@ -208,18 +230,9 @@ def weapon_shop_item_mod(self, item): magicite_npc.split_sprite = 1 magicite_npc.direction = direction.DOWN - import data.text - item_name = data.text.convert(self.items.get_name(item), data.text.TEXT1) # item names are stored as TEXT2, dialogs are TEXT1 - dialog_text = "This gives off an eerie aura! Leave it “" + item_name + "”" + self.weapon_shop_mod("This gives off an eerie aura!") + self.behind_whelk_mod() - self.weapon_shop_mod(dialog_text, [ - field.AddItem(item, sound_effect = False), - ]) - - self.esper_room_mod([ - field.AddItem(item), - field.Dialog(self.items.get_receive_dialog(item)), - ]) def cursed_shield_mod(self): self.dialogs.set_text(1523, f"“Cursed Shld”…{self.items.cursed_shield_battles}") diff --git a/event/opera_house_wob.py b/event/opera_house_wob.py index 326655b7..39250353 100644 --- a/event/opera_house_wob.py +++ b/event/opera_house_wob.py @@ -8,7 +8,8 @@ def character_gate(self): return self.characters.CELES def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + from constants.checks import OPERA_HOUSE_DISRUPTION + self.reward = self.add_reward(OPERA_HOUSE_DISRUPTION) def init_event_bits(self, space): space.write( @@ -298,7 +299,7 @@ def after_battle_mod(self): space.write( field.Call(show_celes), ) - + # do not animate the now hidden party leader space = Reserve(0xac28a, 0xac28d, "opera house do not turn party leader up", field.NOP()) space = Reserve(0xac30d, 0xac312, "opera house do not move party leader up", field.NOP()) diff --git a/event/owzer_mansion.py b/event/owzer_mansion.py index c0e53dec..c3f37248 100644 --- a/event/owzer_mansion.py +++ b/event/owzer_mansion.py @@ -2,13 +2,14 @@ class OwzerMansion(Event): def name(self): - return "Owzer Mansion" + return "Owzer's Mansion" def character_gate(self): return self.characters.RELM def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + from constants.checks import OWZERS_MANSION + self.reward = self.add_reward(OWZERS_MANSION) def mod(self): self.relm_npc_id = 0x13 diff --git a/event/phantom_train.py b/event/phantom_train.py index 1ddf3425..204a1bb0 100644 --- a/event/phantom_train.py +++ b/event/phantom_train.py @@ -1,3 +1,4 @@ +from constants.checks import PHANTOM_TRAIN from event.event import * class PhantomTrain(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.SABIN def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(PHANTOM_TRAIN) def init_event_bits(self, space): space.write( diff --git a/event/phoenix_cave.py b/event/phoenix_cave.py index 41aa54d9..5b2124c3 100644 --- a/event/phoenix_cave.py +++ b/event/phoenix_cave.py @@ -1,3 +1,4 @@ +from constants.checks import PHOENIX_CAVE from event.event import * class PhoenixCave(Event): @@ -11,7 +12,7 @@ def characters_required(self): return 2 def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(PHOENIX_CAVE) def mod(self): self.locke_npc_id = 0x10 diff --git a/event/sealed_gate.py b/event/sealed_gate.py index 22c0015e..c2774c8e 100644 --- a/event/sealed_gate.py +++ b/event/sealed_gate.py @@ -1,3 +1,4 @@ +from constants.checks import SEALED_GATE from event.event import * class SealedGate(Event): @@ -8,10 +9,7 @@ def character_gate(self): return self.characters.TERRA def init_rewards(self): - if self.args.no_free_characters_espers: - self.reward = self.add_reward(RewardType.ITEM) - else: - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(SEALED_GATE) def init_event_bits(self, space): space.write( diff --git a/event/serpent_trench.py b/event/serpent_trench.py index 6afdeffa..01703efc 100644 --- a/event/serpent_trench.py +++ b/event/serpent_trench.py @@ -1,3 +1,4 @@ +from constants.checks import SERPENT_TRENCH from event.event import * class SerpentTrench(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.GAU def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(SERPENT_TRENCH) def mod(self): self.cave_mod() diff --git a/event/south_figaro.py b/event/south_figaro.py index 25dcb3f8..0bf9be4c 100644 --- a/event/south_figaro.py +++ b/event/south_figaro.py @@ -1,17 +1,15 @@ from event.event import * +from constants.checks import SOUTH_FIGARO_PRISONER class SouthFigaro(Event): def name(self): - return "South Figaro" + return SOUTH_FIGARO_PRISONER.name def character_gate(self): return self.characters.CELES def init_rewards(self): - if self.args.no_free_characters_espers: - self.reward = self.add_reward(RewardType.ITEM) - else: - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(SOUTH_FIGARO_PRISONER) def init_event_bits(self, space): space.write( diff --git a/event/south_figaro_cave_wob.py b/event/south_figaro_cave_wob.py index 225940bf..16ece62a 100644 --- a/event/south_figaro_cave_wob.py +++ b/event/south_figaro_cave_wob.py @@ -1,3 +1,4 @@ +from constants.checks import SOUTH_FIGARO_CAVE from event.event import * class SouthFigaroCaveWOB(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.LOCKE def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(SOUTH_FIGARO_CAVE) def init_event_bits(self, space): space.write( diff --git a/event/start.py b/event/start.py index 80f35b42..7298e40b 100644 --- a/event/start.py +++ b/event/start.py @@ -25,7 +25,7 @@ def init_rewards(self): # assign chosen character rewards for character_id in party: if character_id is not None: - reward = self.add_reward(RewardType.CHARACTER) + reward = self.add_character_reward() reward.id = character_id reward.type = RewardType.CHARACTER @@ -189,14 +189,10 @@ def start_items_mod(self): ] if self.args.debug: - src += [ - field.AddItem("Dried Meat", sound_effect = False), - field.AddItem("Dried Meat", sound_effect = False), - field.AddItem("Dried Meat", sound_effect = False), - field.AddItem("Warp Stone", sound_effect = False), - field.AddItem("Warp Stone", sound_effect = False), - field.AddItem("Warp Stone", sound_effect = False), - ] + from constants.items import name_id + for char in self.characters.characters: + char.init_relic1 = name_id["Moogle Charm"] + src += [ field.Return(), ] @@ -211,7 +207,6 @@ def start_game_mod(self): vehicle.SetPosition(84, 34), vehicle.LoadMap(0x06, direction.DOWN, default_music = True, x = 16, y = 6, entrance_event = True), - field.EntityAct(field_entity.PARTY0, True, field_entity.CenterScreen(), ), @@ -222,4 +217,4 @@ def start_game_mod(self): field.Return(), ] space = Write(Bank.CC, src, "start game") - self.start_game = space.start_address + self.start_game = space.start_address \ No newline at end of file diff --git a/event/tritoch.py b/event/tritoch.py index 428d71bc..863b543d 100644 --- a/event/tritoch.py +++ b/event/tritoch.py @@ -5,7 +5,8 @@ def name(self): return "Tritoch" def init_rewards(self): - self.reward = self.add_reward(RewardType.ESPER | RewardType.ITEM) + from constants.checks import TRITOCH_CLIFF + self.reward = self.add_reward(TRITOCH_CLIFF) def mod(self): self.dialog_mod() diff --git a/event/tzen.py b/event/tzen.py index 6e2434cc..e6d5e114 100644 --- a/event/tzen.py +++ b/event/tzen.py @@ -5,7 +5,8 @@ def name(self): return "Tzen" def init_rewards(self): - self.reward = self.add_reward(RewardType.ESPER | RewardType.ITEM) + from constants.checks import TZEN_THIEF + self.reward = self.add_reward(TZEN_THIEF) def init_event_bits(self, space): space.write( diff --git a/event/umaro_cave.py b/event/umaro_cave.py index eafe827b..2c7e1788 100644 --- a/event/umaro_cave.py +++ b/event/umaro_cave.py @@ -1,3 +1,4 @@ +from constants.checks import UMAROS_CAVE from event.event import * class UmaroCave(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.UMARO def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(UMAROS_CAVE) def mod(self): space = Reserve(0xcd6f5, 0xcd6f7, "umaro cave what's with this carving", field.NOP()) diff --git a/event/veldt.py b/event/veldt.py index 09327d29..29a80d3c 100644 --- a/event/veldt.py +++ b/event/veldt.py @@ -1,3 +1,4 @@ +from constants.checks import VELDT from event.event import * from event.veldt_helpers import * @@ -8,13 +9,17 @@ class Veldt(Event): def name(self): - return "Veldt" + return VELDT.name def character_gate(self): return self.characters.GAU def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER) + self.reward = self.add_reward(VELDT) + + # Veldt cannot currently be an item. + # This will catch the event either `-firr`` or `-feirr` flags are used to target veldt + assert self.reward.type != RewardType.ITEM def init_event_bits(self, space): # abusing init event bits space here... diff --git a/event/veldt_cave_wor.py b/event/veldt_cave_wor.py index 369cdd22..9408e676 100644 --- a/event/veldt_cave_wor.py +++ b/event/veldt_cave_wor.py @@ -8,7 +8,8 @@ def character_gate(self): return self.characters.SHADOW def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + from constants.checks import VELDT_CAVE + self.reward = self.add_reward(VELDT_CAVE) def mod(self): self.shadow_npc_id = 0x12 diff --git a/event/whelk.py b/event/whelk.py index 91966d70..f846ecc8 100644 --- a/event/whelk.py +++ b/event/whelk.py @@ -8,7 +8,8 @@ def character_gate(self): return self.characters.TERRA def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + from constants.checks import WHELK_GATE + self.reward = self.add_reward(WHELK_GATE) def init_event_bits(self, space): space.write( diff --git a/event/zone_eater.py b/event/zone_eater.py index fcb94b54..0a765040 100644 --- a/event/zone_eater.py +++ b/event/zone_eater.py @@ -1,3 +1,4 @@ +from constants.checks import ZONE_EATER from event.event import * class ZoneEater(Event): @@ -8,7 +9,7 @@ def character_gate(self): return self.characters.GOGO def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + self.reward = self.add_reward(ZONE_EATER) def mod(self): self.gogo_npc_id = 0x10 diff --git a/event/zozo.py b/event/zozo.py index 4937a024..370c2aa3 100644 --- a/event/zozo.py +++ b/event/zozo.py @@ -8,7 +8,8 @@ def character_gate(self): return self.characters.TERRA def init_rewards(self): - self.reward = self.add_reward(RewardType.CHARACTER | RewardType.ESPER | RewardType.ITEM) + from constants.checks import ZOZO_TOWER + self.reward = self.add_reward(ZOZO_TOWER) def init_event_bits(self, space): space.write( diff --git a/instruction/c3.py b/instruction/c3.py new file mode 100644 index 00000000..0e10ecc0 --- /dev/null +++ b/instruction/c3.py @@ -0,0 +1,24 @@ +from memory.space import Bank, START_ADDRESS_SNES, Space, Reserve, Allocate, Free, Write +import instruction.asm as asm + +# Allow Eggers jumps into C3 -- that is, enable calls to JSR routines from other banks +# Ref: https://www.ff6hacking.com/forums/thread-2301.html +def _eggers_jump_return_mod(): + src = [ + asm.RTS(), + asm.RTL() + ] + space = Write(Bank.C3, src, "C3 eggers jump return") + return space.start_address +eggers_jump_return = _eggers_jump_return_mod() + +# Eggers jump src to jump to the specified C3 subroutine and successfully return to another bank +def eggers_jump(c3addr): + src = [ + asm.PHK(), + asm.PER(0x0009), + asm.PEA(eggers_jump_return), + asm.PEA(c3addr-1), # return after execution + asm.JMP(eggers_jump_return + START_ADDRESS_SNES, asm.LNG), + ] + return src \ No newline at end of file diff --git a/instruction/field/entity.py b/instruction/field/entity.py index f8888923..3fc114c4 100644 --- a/instruction/field/entity.py +++ b/instruction/field/entity.py @@ -72,6 +72,11 @@ class AnimateAttacked(_Instruction): def __init__(self): super().__init__(0x0b) + +class AnimateArmsRaisedWalking(_Instruction): + def __init__(self): + super().__init__(0x0e) + class AnimateHandsUp(_Instruction): def __init__(self): super().__init__(0x0f) @@ -84,6 +89,18 @@ class AnimateFrontRightHandUp(_Instruction): def __init__(self): super().__init__(0x19) +class AnimateFrontRightHandOnHead(_Instruction): + def __init__(self): + super().__init__(0x1a) + +class LaughingOne(_Instruction): + def __init__(self): + super().__init__(0x1d) + +class LaughingTwo(_Instruction): + def __init__(self): + super().__init__(0x1e) + class AnimateSurprised(_Instruction): def __init__(self): super().__init__(0x1f) diff --git a/menus/checks.py b/menus/checks.py index 0ace86a3..318501e2 100644 --- a/menus/checks.py +++ b/menus/checks.py @@ -4,7 +4,7 @@ import args import data.event_bit as event_bit -from constants.gates import character_checks +from constants.gates import character_check_names import constants.objectives.condition_bits as condition_bits import menus.pregame_track_scroll_area as scroll_area @@ -43,7 +43,7 @@ def line_color_function(self, address, bit): def open_world_init(self): checks = [] - for group in character_checks.values(): + for group in character_check_names.values(): checks += group checks = sorted(checks) @@ -62,7 +62,7 @@ def character_gating_init(self): self.lines = [] self.line_skip_bits = [] - for character, checks in character_checks.items(): + for character, checks in character_check_names.items(): if not character: character = "Open" character_address = constant_ffff # always 0xffff diff --git a/menus/flags.py b/menus/flags.py index 206b3741..4b390d83 100644 --- a/menus/flags.py +++ b/menus/flags.py @@ -4,12 +4,14 @@ import args import menus.pregame_track_scroll_area as scroll_area +from data.text.text2 import text_value class Flags(scroll_area.ScrollArea): MENU_NUMBER = 12 def __init__(self): self.lines = [] + self.submenus = {} # dictionary of submenus. key = line number, value = ScrollArea derived class for _, group in args.group_modules.items(): if hasattr(group, "menu"): name, options = group.menu(args) @@ -19,6 +21,12 @@ def __init__(self): key, value = option key = " " + key.replace("&", "+") + + # if we're given a scroll area, save it as a sub-menu with a value of X …, where X is the number of items in the sub-menu + if isinstance(value, scroll_area.ScrollArea): + self.submenus[len(self.lines)] = value + value = f"{value.number_items} {chr(text_value['…'])}" + value = str(value) if value == "True": value = "T" diff --git a/menus/pregame.py b/menus/pregame.py index 1af6d27d..335e73c6 100644 --- a/menus/pregame.py +++ b/menus/pregame.py @@ -1,5 +1,6 @@ -from memory.space import Bank, Write, Reserve, Allocate, Read +from memory.space import START_ADDRESS_SNES, Bank, Write, Reserve, Allocate, Read import instruction.asm as asm +import instruction.c3 as c3 class PreGameMenu: MENU_NUMBER = 9 @@ -8,6 +9,7 @@ class PreGameMenu: def __init__(self, pregame_track): self.common = pregame_track + self.invoke_flags_submenu = {} self.mod() def draw_options_mod(self): @@ -46,10 +48,10 @@ def draw_options_mod(self): def initialize_mod(self): src = [ - asm.JSR(self.common.initialize, asm.ABS), + asm.JSL(self.common.initialize + START_ADDRESS_SNES), asm.JSR(self.draw_options, asm.ABS), - asm.JSR(self.common.upload_bg123ab, asm.ABS), + asm.JSL(self.common.upload_bg123ab + START_ADDRESS_SNES), asm.LDA(self.MENU_NUMBER, asm.IMM8), asm.STA(0x0200, asm.ABS), @@ -60,6 +62,7 @@ def initialize_mod(self): asm.STA(0x26, asm.DIR), # add fade in pregame menu to queue asm.JMP(0x3541, asm.ABS), # set brightness and refresh screen ] + # called by C3 JSR jump table space = Write(Bank.C3, src, "pregame initialize") self.initialize = space.start_address @@ -79,6 +82,14 @@ def invoke_flags_menu_mod(self): space = Write(Bank.C3, src, "pregame invoke flags") self.invoke_flags = space.start_address + def invoke_flags_submenu_mod(self, submenu_idx): + src = [ + asm.JSR(0x6a3c, asm.ABS), # clear BG3 a (workaround for bizhawk snes9x core bug) + asm.JMP(self.common.invoke_flags_submenu[submenu_idx], asm.ABS), + ] + space = Write(Bank.C3, src, "pregame invoke flag submenu") + self.invoke_flags_submenu[submenu_idx] = space.start_address + def sustain_mod(self): src = [ asm.JSR(0x2a21, asm.ABS), # reset game play time @@ -120,12 +131,21 @@ def sustain_mod(self): src = [ asm.JSR(self.common.refresh_sprites, asm.ABS), + # if in a scroll area, sustain it asm.LDA(0x0200, asm.ABS), asm.CMP(self.common.flags.MENU_NUMBER, asm.IMM8), asm.BEQ("SUSTAIN_SCROLL_AREA"), asm.CMP(self.common.objectives.MENU_NUMBER, asm.IMM8), asm.BEQ("SUSTAIN_SCROLL_AREA"), + ] + for submenu_idx in self.common.flags.submenus.keys(): + src += [ + asm.CMP(self.common.flags.submenus[submenu_idx].MENU_NUMBER, asm.IMM8), + asm.BEQ("SUSTAIN_SCROLL_AREA"), + ] + + src += [ asm.JSR(0x072d, asm.ABS), # handle d-pad asm.LDY(self.common.cursor_positions, asm.IMM16), asm.JSR(0x0640, asm.ABS), # update cursor position @@ -144,36 +164,40 @@ def sustain_mod(self): asm.JMP(options_table, asm.ABS_X_16), "SUSTAIN_SCROLL_AREA", - asm.LDA(0x0d, asm.DIR), + asm.LDA(0x09, asm.DIR), asm.BIT(0x80, asm.IMM8), # b pressed? - asm.BNE("EXIT_SCROLL_AREA"), + asm.BNE("EXIT_SCROLL_AREA"), # branch if so + + ] + + for submenu_id in self.common.flags.submenus.keys(): + src.extend(self.common.get_submenu_src(submenu_id, self.invoke_flags_submenu[submenu_id])) + + src += [ asm.JMP(self.common.sustain_scroll_area, asm.ABS), "EXIT_SCROLL_AREA", - asm.JSR(self.common.exit_scroll_area, asm.ABS), - asm.LDA(self.MENU_NUMBER, asm.IMM8), - asm.STA(0x0200, asm.ABS), - - "RETURN", - asm.RTS(), ] + src.extend(self.common.get_scroll_area_exit_src(self.MENU_NUMBER, self.invoke_flags)) + + # Called by C3 JSR jump table space = Write(Bank.C3, src, "pregame sustain") self.sustain = space.start_address def initialize_config_menu_mod(self): src = [ - asm.JSR(0x352f, asm.ABS), # reset - + c3.eggers_jump(0x352f), # displaced code: reset + asm.STZ(0x4A, asm.DIR), # displaced code: screen 1st asm.LDA(0xc0, asm.IMM8), # hdma channels 6 and 7 asm.TRB(0x43, asm.DIR), - asm.RTS(), + asm.RTL(), ] - space = Write(Bank.C3, src, "pregame initialize config menu reset uncondense") + space = Write(Bank.F0, src, "pregame initialize config menu reset uncondense") reset_uncondense = space.start_address - space = Reserve(0x31c7d, 0x31c7f, "pregame initialize config menu reset") + space = Reserve(0x31c7d, 0x31c81, "pregame initialize config menu reset", asm.NOP()) space.write( - asm.JSR(reset_uncondense, asm.ABS), + asm.JSL(reset_uncondense + START_ADDRESS_SNES), ) def exit_config_menu_mod(self): @@ -235,6 +259,8 @@ def mod(self): self.initialize_mod() self.invoke_objectives_menu_mod() self.invoke_flags_menu_mod() + for submenu_idx in self.common.flags.submenus.keys(): + self.invoke_flags_submenu_mod(submenu_idx) self.sustain_mod() self.initialize_config_menu_mod() diff --git a/menus/pregame_track.py b/menus/pregame_track.py index c26edf45..17dec766 100644 --- a/menus/pregame_track.py +++ b/menus/pregame_track.py @@ -1,5 +1,6 @@ from memory.space import Bank, START_ADDRESS_SNES, Reserve, Allocate, Write, Read import instruction.asm as asm +import instruction.c3 as c3 import args import menus.pregame_track_scroll_area as scroll_area @@ -22,9 +23,67 @@ def __init__(self, characters): self.progress = progress.Progress() self.flags = flags.Flags() + self.invoke_flags_submenu = {} self.characters = characters self.mod() + def get_submenu_src(self, submenu_id, invoke_submenu_addr): + SUBMENU_LABEL = f"SUBMENU_CHECK{submenu_id}" + SUBMENU_END_LABEL = f"SUBMENU_CHECK{submenu_id}_END" + HANDLE_SCROLLING_LABEL = f"HANDLE_SCROLLING_{submenu_id}" + # get the ASM for sustain_mod that checks whether we are in the flags menu + # and the A button is clicked to launch a submenu. + src = [ + # if on the flags menu, check A button press + asm.LDA(0x200, asm.ABS), + asm.CMP(self.flags.MENU_NUMBER, asm.IMM8), # in Flags menu? + asm.BNE(HANDLE_SCROLLING_LABEL), # branch if not + asm.LDA(0x08, asm.DIR), + asm.BIT(0x80, asm.IMM8), # a pressed? + asm.BEQ(HANDLE_SCROLLING_LABEL), # branch if not + ] + src += [ + SUBMENU_LABEL, + asm.LDA(0x4b, asm.DIR), # a = cursor index + asm.CMP(submenu_id, asm.IMM8), # is the cursor index = this submenu? + asm.BNE(SUBMENU_END_LABEL), # branch if not + asm.TDC(), + asm.JSR(0x0eb2, asm.ABS), # click sound + asm.JSL(self.exit_scroll_area + START_ADDRESS_SNES), # save current submenu position + asm.JMP(invoke_submenu_addr, asm.ABS), # load the flags submenu + SUBMENU_END_LABEL, + ] + src += [HANDLE_SCROLLING_LABEL] + return src + + def get_scroll_area_exit_src(self, destination_menu_number, invoke_flags_addr): + # Get the ASM for sustain_mod that handles exit from a scroll area, either returning to flags if in + # a flags submenu or to the given destination_menu_number otherwise. + src = [ + asm.JSR(0x0EA9, asm.ABS), # cursor sound + asm.JSL(self.exit_scroll_area + START_ADDRESS_SNES), # save current submenu position + asm.LDA(0x0200, asm.ABS), + ] + + for submenu_idx in self.flags.submenus.keys(): + # if current menu is a flags sub-menu, cause it to return to that, rather than main menu + src += [ + asm.CMP(self.flags.submenus[submenu_idx].MENU_NUMBER, asm.IMM8), # in Flags submenu? + asm.BEQ("INVOKE_FLAGS"), # branch if so + ] + + src += [ + asm.LDA(destination_menu_number, asm.IMM8), # queue up this menu + asm.STA(0x0200, asm.ABS), + "RETURN", + asm.RTS(), + + "INVOKE_FLAGS", + asm.JMP(invoke_flags_addr, asm.ABS), + ] + + return src + def draw_layout_mod(self): # layouts: 2 bytes for bg/tilemap/position, 1 byte inner width, 1 byte inner height # e.g. $5849 is start of bg2 tilemap a, add 0x42 for top left of visible screen area @@ -33,6 +92,7 @@ def draw_layout_mod(self): 0x8b, 0x58, # top-left to top-right 0x1c, 0x07, # 28x7 ] + # Note: keep in C3 as this is then used by the C3/0341 subroutine called below space = Write(Bank.C3, src, "pregame track top window layout") top_window_layout = space.start_address @@ -40,19 +100,20 @@ def draw_layout_mod(self): 0xcb, 0x5a, # x/y position 0x1c, 0x0f, # width/height (excluding border) ] + # Note: keep in C3 as this is then used by the C3/0341 subroutine called below space = Write(Bank.C3, src, "pregame track bottom window layout") bottom_window_layout = space.start_address src = [ - asm.JSR(0x6a15, asm.ABS), # clear BG1 A - asm.JSR(0x6a3c, asm.ABS), # clear BG3 A + c3.eggers_jump(0x6a15), # clear BG1 A + c3.eggers_jump(0x6a3c), # clear BG3 A asm.LDY(top_window_layout, asm.IMM16), - asm.JSR(0x0341, asm.ABS), # draw top window + c3.eggers_jump(0x0341), # draw top window asm.LDY(bottom_window_layout, asm.IMM16), - asm.JSR(0x0341, asm.ABS), # draw bottom window + c3.eggers_jump(0x0341), # draw bottom window asm.RTS(), ] - space = Write(Bank.C3, src, "pregame track draw layout") + space = Write(Bank.F0, src, "pregame track draw layout") self.draw_layout = space.start_address def decrease_line_height_mod(self): @@ -65,6 +126,7 @@ def decrease_line_height_mod(self): 0x0c, 0x14, 0x00, # config/flags 0x00, # end ] + # Keep in C3 as it's used by C3 subroutine called below space = Write(Bank.C3, src, "pregame track bg3 shift table") bg3_shift_table = space.start_address @@ -97,17 +159,17 @@ def draw_labels_mod(self): ) src = [ - asm.JSR(0xc2f7, asm.ABS), # set text color to blue + c3.eggers_jump(0xc2f7), # set text color to blue ] for label in labels: src += [ asm.LDY(label, asm.IMM16), - asm.JSR(0x02f9, asm.ABS), # draw text + c3.eggers_jump(0x02f9), # draw text ] src += [ asm.RTS(), ] - space = Write(Bank.C3, src, "pregame track draw labels") + space = Write(Bank.F0, src, "pregame track draw labels") self.draw_labels = space.start_address def draw_entry_mod(self): @@ -123,7 +185,15 @@ def draw_entry_mod(self): asm.BEQ("DRAW_PROGRESS"), asm.CMP(self.flags.MENU_NUMBER, asm.IMM8), asm.BEQ("DRAW_FLAG"), + ] + + for submenu_idx in self.flags.submenus.keys(): + src += [ + asm.CMP(self.flags.submenus[submenu_idx].MENU_NUMBER, asm.IMM8), + asm.BEQ(f"DRAW_FLAGS_SUBMENU{submenu_idx}"), + ] + src += [ "DRAW_ITEM", Read(0x37fa1, 0x37fa3), asm.JMP(0x7fa4, asm.ABS), @@ -140,6 +210,13 @@ def draw_entry_mod(self): "DRAW_FLAG", asm.JMP(self.flags.draw_line, asm.ABS), ] + + for submenu_idx in self.flags.submenus.keys(): + src += [ + f"DRAW_FLAGS_SUBMENU{submenu_idx}", + asm.JMP(self.flags.submenus[submenu_idx].draw_line, asm.ABS), + ] + space = Write(Bank.C3, src, "pregame track draw entry") draw_entry = space.start_address @@ -150,12 +227,12 @@ def draw_entry_mod(self): def upload_bg123ab_mod(self): src = [ - asm.JSR(0x0e28, asm.ABS), # upload bg1 a+b - asm.JSR(0x0e52, asm.ABS), # upload bg2 a+b - asm.JSR(0x0e6e, asm.ABS), # upload bg3 a+b - asm.RTS(), + c3.eggers_jump(0x0e28), # upload bg1 a+b + c3.eggers_jump(0x0e52), # upload bg2 a+b + c3.eggers_jump(0x0e6e), # upload bg3 a+b + asm.RTL(), ] - space = Write(Bank.C3, src, "pregame track upload bg123ab") + space = Write(Bank.F0, src, "pregame track upload bg123ab") self.upload_bg123ab = space.start_address def initialize_cursor_mod(self): @@ -165,6 +242,7 @@ def initialize_cursor_mod(self): 0x08, 0x35, # flags / progress 0x08, 0x41, # config / flags ] + # Keep in C3 -- used by subroutines space = Write(Bank.C3, src, "pregame track cursor positions") self.cursor_positions = space.start_address @@ -175,15 +253,16 @@ def initialize_cursor_mod(self): 0x01, # columns 0x04, # rows ] + # Keep in C3 -- used by subroutines space = Write(Bank.C3, src, "pregame track navigation data") navigation_data = space.start_address src = [ asm.LDY(navigation_data, asm.IMM16), - asm.JSR(0x05fe, asm.ABS), # load navigation data + c3.eggers_jump(0x05fe), # load navigation data asm.RTS(), ] - space = Write(Bank.C3, src, "pregame track update navigation data") + space = Write(Bank.F0, src, "pregame track update navigation data") self.update_navigation_data = space.start_address src = [ @@ -191,16 +270,16 @@ def initialize_cursor_mod(self): asm.STA(0x4e, asm.DIR), # cursor row = saved row asm.RTS(), ] - space = Write(Bank.C3, src, "pregame track remember cursor position") + space = Write(Bank.F0, src, "pregame track remember cursor position") self.remember_cursor_position = space.start_address src = [ asm.LDY(self.cursor_positions, asm.IMM16), - asm.JSR(0x0640, asm.ABS), # update cursor position - asm.JSR(0x07b0, asm.ABS), # add cursor to animation queue + c3.eggers_jump(0x0640), # update cursor position + c3.eggers_jump(0x07b0), # add cursor to animation queue asm.RTS(), ] - space = Write(Bank.C3, src, "pregame track update cursor position") + space = Write(Bank.F0, src, "pregame track update cursor position") self.update_cursor_position = space.start_address src = [ @@ -211,9 +290,10 @@ def initialize_cursor_mod(self): asm.JSR(self.remember_cursor_position, asm.ABS), "UPDATE_CURSOR_POSITION", - asm.JMP(self.update_cursor_position, asm.ABS), + asm.JSR(self.update_cursor_position, asm.ABS), + asm.RTS(), ] - space = Write(Bank.C3, src, "pregame track initialize cursor") + space = Write(Bank.F0, src, "pregame track initialize cursor") self.initialize_cursor = space.start_address def initialize_scroll_area_mod(self): @@ -237,6 +317,11 @@ def initialize_scroll_area_mod(self): src+= [ asm.JSL(START_ADDRESS_SNES + self.flags.initialize), ] + for submenu_idx in self.flags.submenus.keys(): + if self.flags.submenus[submenu_idx].initialize is not None: + src+= [ + asm.JSL(START_ADDRESS_SNES + self.flags.submenus[submenu_idx].initialize), + ] src += [ asm.STZ(0x4a, asm.DIR), # index of first row displayed @@ -249,8 +334,8 @@ def initialize_scroll_area_mod(self): asm.LDA(self.objectives.number_excess_lines, asm.IMM8), asm.STA(0x5c, asm.DIR), - asm.JSR(0x07b0, asm.ABS), # queue scrollbar animation - asm.JSR(0x091f, asm.ABS), # create scrollbar + c3.eggers_jump(0x07b0), # queue scrollbar animation + c3.eggers_jump(0x091f), # create scrollbar asm.A16(), asm.LDA(self.objectives.scrollbar_speed, asm.IMM16), asm.STA(0x7e354a, asm.LNG_X), @@ -266,7 +351,7 @@ def initialize_scroll_area_mod(self): asm.LDA(0x1d4e, asm.ABS), # load game config asm.AND(0x40, asm.IMM8), # cursor memory enabled? asm.BNE("REMEMBER_SCROLL_AREA"), # branch if so - asm.JSR(scroll_area.draw, asm.ABS), + c3.eggers_jump(scroll_area.draw), asm.RTS(), "REMEMBER_SCROLL_AREA", @@ -278,7 +363,15 @@ def initialize_scroll_area_mod(self): asm.BEQ("REMEMBER_PROGRESS"), asm.CMP(self.flags.MENU_NUMBER, asm.IMM8), asm.BEQ("REMEMBER_FLAGS"), + ] + + for submenu_idx in self.flags.submenus.keys(): + src += [ + asm.CMP(self.flags.submenus[submenu_idx].MENU_NUMBER, asm.IMM8), + asm.BEQ(f"REMEMBER_FLAGS_SUBMENU{submenu_idx}"), + ] + src += [ "REMEMBER_OBJECTIVES", asm.LDA(self.objectives.MENU_NUMBER, asm.IMM8), # load objectives menu number asm.STA(self.MEMORY_SCROLL_AREA_NUMBER, asm.ABS), # save in case no scroll area memory @@ -293,7 +386,14 @@ def initialize_scroll_area_mod(self): "REMEMBER_FLAGS", asm.JMP(self.flags.remember_draw, asm.ABS), ] - space = Write(Bank.C3, src, "pregame track initialize scroll area") + + for submenu_idx in self.flags.submenus.keys(): + src += [ + f"REMEMBER_FLAGS_SUBMENU{submenu_idx}", + asm.JMP(self.flags.submenus[submenu_idx].remember_draw, asm.ABS), + ] + + space = Write(Bank.F0, src, "pregame track initialize scroll area") self.initialize_scroll_area = space.start_address def InvokeScrollArea(self, scroll_area_menu): @@ -301,7 +401,7 @@ def InvokeScrollArea(self, scroll_area_menu): asm.LDA(scroll_area_menu.MENU_NUMBER, asm.IMM8), asm.STA(self.MEMORY_SCROLL_AREA_NUMBER, asm.ABS), asm.JSR(scroll_area_menu.invoke, asm.ABS), - asm.JSR(self.refresh_sprites, asm.ABS), + asm.JSR(self.refresh_sprites, asm.ABS), # JSL asm.RTS(), ] @@ -333,7 +433,15 @@ def invoke_flags_mod(self): space = Write(Bank.C3, src, "pregame track invoke flags") self.invoke_flags = space.start_address + def invoke_flags_submenu_mod(self, submenu_idx): + src = [ + self.InvokeScrollArea(self.flags.submenus[submenu_idx]), + ] + space = Write(Bank.C3, src, "pregame track invoke flags submenu") + self.invoke_flags_submenu[submenu_idx] = space.start_address + def sustain_scroll_area_mod(self): + # Called via JMP src = [ asm.TDC(), asm.STA(0x2a, asm.DIR), @@ -374,9 +482,9 @@ def exit_scroll_area_mod(self): asm.JSR(self.remember_cursor_position, asm.ABS), asm.JSR(self.update_cursor_position, asm.ABS), - asm.RTS(), + asm.RTL(), ] - space = Write(Bank.C3, src, "pregame track exit scroll area") + space = Write(Bank.F0, src, "pregame track exit scroll area") self.exit_scroll_area = space.start_address def load_sprite_palettes_mod(self): @@ -384,7 +492,7 @@ def load_sprite_palettes_mod(self): # copy the original palettes here so the sprite hash is not affected by custom palette changes palette_size = 16 * 2 # 16 colors, 2 bytes each palettes_size = palette_size * 6 # 6 palettes * 16 colors * 2 bytes - palette_space = Allocate(Bank.C3, palettes_size, "pregame track palettes") + palette_space = Allocate(Bank.F0, palettes_size, "pregame track palettes") palette0_address = palette_space.next_address palette_space.write( @@ -452,7 +560,7 @@ def load_sprite_palettes_mod(self): "LOAD_COLOR_LOOP_START", asm.A16(), - asm.LDA(palettes, asm.LNG_X), # load current color + asm.LDA(palettes + START_ADDRESS_SNES, asm.LNG_X), # load current color asm.STA(0x7e3149, asm.LNG_X), # store in ram asm.A8(), asm.STA(0x2122, asm.ABS), # store low byte in cgram @@ -463,10 +571,10 @@ def load_sprite_palettes_mod(self): asm.CPX(palettes_size, asm.IMM16), # all colors loaded? asm.BNE("LOAD_COLOR_LOOP_START"), # loop if not - asm.JSR(0x6ce9, asm.ABS), # load single pose for characters terra, locke, ..., ghost, kefka + c3.eggers_jump(0x6ce9), # load single pose for characters terra, locke, ..., ghost, kefka asm.RTS(), ] - space = Write(Bank.C3, src, "pregame track load sprite palettes") + space = Write(Bank.F0, src, "pregame track load sprite palettes") self.load_sprite_palettes = space.start_address def refresh_sprites_mod(self): @@ -480,7 +588,7 @@ def refresh_sprites_mod(self): src += [ x_start + x_spacing * index, ] - space = Write(Bank.C3, src, "pregame track refresh sprites x positions") + space = Write(Bank.F0, src, "pregame track refresh sprites x positions") x_positions_address = space.start_address y_start = 0x32 # higher is lower on screen @@ -490,20 +598,20 @@ def refresh_sprites_mod(self): src += [ y_start + entry.y_offset * 8, ] - space = Write(Bank.C3, src, "pregame track refresh sprites y positions") + space = Write(Bank.F0, src, "pregame track refresh sprites y positions") y_positions_address = space.start_address # if not zero, these palettes override sprite oam palettes (at 0xc31324) src = [ 0x00, 0x00, 0x00, 0x00, ] - space = Write(Bank.C3, src, "pregame track refresh sprites palettes address") + space = Write(Bank.F0, src, "pregame track refresh sprites palettes address") palettes_address = space.start_address src = [ HASH_CHARACTERS, ] - space = Write(Bank.C3, src, "pregame track refresh sprites characters address") + space = Write(Bank.F0, src, "pregame track refresh sprites characters address") characters_address = space.start_address # modified version of c31903 used for save/load menus @@ -546,6 +654,7 @@ def refresh_sprites_mod(self): asm.RTS(), ] + # Keep in C3 -- called by JMP methods that are called from C3 JSR jump table space = Write(Bank.C3, src, "pregame track refresh sprites") self.refresh_sprites = space.start_address @@ -576,8 +685,8 @@ def initialize_mod(self): asm.LDA(menu_flags, asm.IMM8), asm.STA(0x46, asm.DIR), - asm.JSR(0x352f, asm.ABS), # reset oam/queue/etc.. blank screen - asm.JSR(0x6904, asm.ABS), # reset BG1-3 x/y + c3.eggers_jump(0x352f), # reset oam/queue/etc.. blank screen + c3.eggers_jump(0x6904), # reset BG1-3 x/y asm.LDA(bg1_size_position, asm.IMM8), # a = BG1 size and position asm.STA(0x2107, asm.ABS), # BG1 64x32 at $0000 @@ -585,24 +694,24 @@ def initialize_mod(self): asm.STA(0x43, asm.DIR), # set active hdma channels asm.JSR(self.draw_layout, asm.ABS), - asm.JSR(self.decrease_line_height, asm.ABS), + c3.eggers_jump(self.decrease_line_height), asm.JSR(self.initialize_scroll_area, asm.ABS), - asm.JSR(0x6a19, asm.ABS), # clear BG1 b - asm.JSR(0x6a3c, asm.ABS), # clear BG3 a - asm.JSR(0x6a41, asm.ABS), # clear BG3 b - asm.JSR(0x6a46, asm.ABS), # clear BG3 c - asm.JSR(0x6a4b, asm.ABS), # clear BG3 d + c3.eggers_jump(0x6a19), # clear BG1 b + c3.eggers_jump(0x6a3c), # clear BG3 a + c3.eggers_jump(0x6a41), # clear BG3 b + c3.eggers_jump(0x6a46), # clear BG3 c + c3.eggers_jump(0x6a4b), # clear BG3 d asm.JSR(self.draw_labels, asm.ABS), - asm.JSR(0x6ca5, asm.ABS), # load cursor colors, skip loading status icon colors at 0x6c84 + c3.eggers_jump(0x6ca5), # load cursor colors, skip loading status icon colors at 0x6c84 asm.JSR(self.load_sprite_palettes, asm.ABS), asm.JSR(self.initialize_cursor, asm.ABS), - asm.RTS(), + asm.RTL(), ] - space = Write(Bank.C3, src, "pregame track initialize") + space = Write(Bank.F0, src, "pregame track initialize") self.initialize = space.start_address def wait_for_fade_mod(self): @@ -614,6 +723,7 @@ def wait_for_fade_mod(self): "LOAD_SPRITE_DATA", asm.JMP(self.refresh_sprites, asm.ABS), ] + # Keep in C3 -- called by C3 JSR jump table space = Write(Bank.C3, src, "pregame track wait for fade") self.wait_for_fade = space.start_address @@ -626,6 +736,7 @@ def fade_in_mod(self): asm.STA(0x26, asm.DIR), # add wait for fade to queue asm.JMP(self.refresh_sprites, asm.ABS), ] + # Keep in C3 -- called by C3 JSR jump table space = Write(Bank.C3, src, "pregame track fade in") self.fade_in = space.start_address @@ -638,6 +749,7 @@ def fade_out_mod(self): asm.STA(0x26, asm.DIR), # add wait for fade to queue asm.JMP(self.refresh_sprites, asm.ABS), # refresh sprites ] + # Keep in C3 -- called by C3 JSR jump table space = Write(Bank.C3, src, "pregame track fade out") self.fade_out = space.start_address @@ -668,11 +780,13 @@ def mod(self): self.invoke_checks_mod() self.invoke_progress_mod() self.invoke_flags_mod() - self.sustain_scroll_area_mod() + for submenu_idx in self.flags.submenus.keys(): + self.invoke_flags_submenu_mod(submenu_idx) self.exit_scroll_area_mod() + self.sustain_scroll_area_mod() self.initialize_mod() self.wait_for_fade_mod() self.fade_in_mod() self.fade_out_mod() - self.menu_commands_jump_table() + self.menu_commands_jump_table() \ No newline at end of file diff --git a/menus/pregame_track_scroll_area.py b/menus/pregame_track_scroll_area.py index 89ed581e..ac6bffa3 100644 --- a/menus/pregame_track_scroll_area.py +++ b/menus/pregame_track_scroll_area.py @@ -1,6 +1,7 @@ from memory.space import START_ADDRESS_SNES, Bank, Write import instruction.asm as asm import instruction.f0 as f0 +import instruction.c3 as c3 from enum import IntEnum from collections import namedtuple @@ -136,7 +137,7 @@ def invoke_mod(self): asm.AND(0x40, asm.IMM8), # cursor memory enabled? asm.BEQ("UPDATE_CURSOR"), # branch if not - asm.JSR(self.remember_cursor, asm.ABS), + asm.JSL(self.remember_cursor + START_ADDRESS_SNES), "UPDATE_CURSOR", asm.JSR(0x7d25, asm.ABS), # update cursor @@ -221,7 +222,7 @@ def draw_line_mod(self): asm.DEY(), # decrement count asm.BNE("LOOP_START"), # branch if more characters in line asm.STZ(0x2180, asm.ABS), # write end of string - asm.RTS(), + asm.RTL(), "WRITE_BLANK_LINE", asm.LDY(WIDTH, asm.IMM16), @@ -231,14 +232,14 @@ def draw_line_mod(self): asm.DEY(), asm.BNE("WRITE_BLANK_LINE_LOOP"), asm.STZ(0x2180, asm.ABS), - asm.RTS(), + asm.RTL(), ] - space = Write(Bank.C3, src, "pregame track scroll area write line") + space = Write(Bank.F0, src, "pregame track scroll area write line") write_line = space.start_address src = [ asm.JSL(START_ADDRESS_SNES + self.initialize_line), - asm.JSR(write_line, asm.ABS), + asm.JSL(START_ADDRESS_SNES + write_line), asm.JSR(0x37fd9, asm.ABS), # draw line asm.RTS(), ] @@ -252,10 +253,10 @@ def remember_cursor_mod(self): asm.LDA(0x4f, asm.DIR), asm.STA(0x4d, asm.DIR), asm.LDA(self.memory_page_position, asm.ABS), - asm.JSR(0x0e1e, asm.ABS), - asm.RTS(), + c3.eggers_jump(0x0e1e), + asm.RTL(), ] - space = Write(Bank.C3, src, "pregame track scroll area remember cursor") + space = Write(Bank.F0, src, "pregame track scroll area remember cursor") self.remember_cursor = space.start_address def remember_scrollbar_mod(self): @@ -268,9 +269,9 @@ def remember_scrollbar_mod(self): asm.STA(0x7e354a, asm.LNG_X), asm.A8(), - asm.RTS(), + asm.RTL(), ] - space = Write(Bank.C3, src, "pregame track scroll area remember scrollbar") + space = Write(Bank.F0, src, "pregame track scroll area remember scrollbar") self.remember_scrollbar = space.start_address def remember_draw_mod(self): @@ -278,13 +279,13 @@ def remember_draw_mod(self): asm.LDA(self.MENU_NUMBER, asm.IMM8), asm.STA(0x0200, asm.ABS), - asm.JSR(self.remember_cursor, asm.ABS), - asm.JSR(self.remember_scrollbar, asm.ABS), + asm.JSL(self.remember_cursor + START_ADDRESS_SNES), + asm.JSL(self.remember_scrollbar + START_ADDRESS_SNES), - asm.JSR(draw, asm.ABS), - asm.RTS(), + c3.eggers_jump(draw), + asm.RTS(), ] - space = Write(Bank.C3, src, "pregame track scroll area remember draw") + space = Write(Bank.F0, src, "pregame track scroll area remember draw") self.remember_draw = space.start_address def mod(self): diff --git a/menus/submenu_force_item_reward_checks.py b/menus/submenu_force_item_reward_checks.py new file mode 100644 index 00000000..b3607dcd --- /dev/null +++ b/menus/submenu_force_item_reward_checks.py @@ -0,0 +1,57 @@ + +import menus.pregame_track_scroll_area as scroll_area +from data.text.text2 import text_value +import instruction.f0 as f0 +from constants.gates import character_checks + +class FlagsForceRewardChecks(scroll_area.ScrollArea): + + def __init__(self, title, item_checks, check_preset): + self.number_items = len(item_checks) + self.lines = [] + + self.lines.append(scroll_area.Line(title, f0.set_blue_text_color)) + + if check_preset: + from constants.check_presets import key_preset + preset_title = key_preset[check_preset].name + + self.lines.append(scroll_area.Line("-------------------------", f0.set_user_text_color)) + self.lines.append(scroll_area.Line(f"{preset_title}", f0.set_user_text_color)) + self.lines.append(scroll_area.Line("-------------------------", f0.set_user_text_color)) + + self.lines.append(scroll_area.Line("", f0.set_user_text_color)) + + check_lines = FlagsForceRewardChecks._format_check_list_menu(item_checks) + for check in check_lines: + self.lines.append(scroll_area.Line(f"{check}", f0.set_user_text_color)) + + super().__init__() + + def _format_check_list_menu(check_ids): + from constants.checks import check_name + check_lines = [] + + for (group_name, checks) in [(group, checks) for (group, checks) in character_checks.items()]: + group_check_ids = [check.bit for check in checks] + group_checks = [check for check in check_ids if check in group_check_ids] + if len(group_checks) > 0: + check_lines.append(group_name) + for bit in group_checks: + check_lines.append(f"{' '}{check_name.get(bit)}") + check_lines.append("") + + return check_lines + + +class FlagsForceEsperRewardChecks(FlagsForceRewardChecks): + MENU_NUMBER = 16 + +class FlagsForceEsperItemRewardChecks(FlagsForceRewardChecks): + MENU_NUMBER = 17 + +class FlagsForceItemRewardChecks(FlagsForceRewardChecks): + MENU_NUMBER = 18 + +class FlagsForceCharacterRewardChecks(FlagsForceRewardChecks): + MENU_NUMBER = 19 diff --git a/menus/track.py b/menus/track.py index 10f04fbf..a1fc011c 100644 --- a/menus/track.py +++ b/menus/track.py @@ -1,5 +1,6 @@ -from memory.space import Bank, Write, Reserve, Allocate, Read +from memory.space import START_ADDRESS_SNES, Bank, Write, Reserve, Allocate, Read import instruction.asm as asm +import instruction.c3 as c3 class TrackMenu: MENU_NUMBER = 10 @@ -192,10 +193,10 @@ def invoke_mod(self): def initialize_mod(self): src = [ - asm.JSR(self.common.initialize, asm.ABS), + asm.JSL(self.common.initialize + START_ADDRESS_SNES), asm.JSR(self.draw_options, asm.ABS), - asm.JSR(self.common.upload_bg123ab, asm.ABS), + asm.JSL(self.common.upload_bg123ab + START_ADDRESS_SNES), asm.LDA(self.MENU_NUMBER, asm.IMM8), asm.STA(0x0200, asm.ABS), @@ -206,6 +207,7 @@ def initialize_mod(self): asm.STA(0x26, asm.DIR), # add fade in menu to queue asm.JMP(0x3541, asm.ABS), # set brightness and refresh screen ] + # called by C3 JSR jump table space = Write(Bank.C3, src, "track initialize") self.initialize = space.start_address @@ -222,6 +224,7 @@ def sustain_mod(self): src = [ asm.JSR(self.common.refresh_sprites, asm.ABS), + # if in a scroll-area menu, sustain the scroll area asm.LDA(0x0200, asm.ABS), asm.CMP(self.common.objectives.MENU_NUMBER, asm.IMM8), asm.BEQ("SUSTAIN_SCROLL_AREA"), @@ -231,7 +234,15 @@ def sustain_mod(self): asm.BEQ("SUSTAIN_SCROLL_AREA"), asm.CMP(self.common.flags.MENU_NUMBER, asm.IMM8), asm.BEQ("SUSTAIN_SCROLL_AREA"), + ] + + for submenu_idx in self.common.flags.submenus.keys(): + src += [ + asm.CMP(self.common.flags.submenus[submenu_idx].MENU_NUMBER, asm.IMM8), + asm.BEQ("SUSTAIN_SCROLL_AREA"), + ] + src += [ asm.JSR(0x072d, asm.ABS), # handle d-pad asm.LDY(self.common.cursor_positions, asm.IMM16), asm.JSR(0x0640, asm.ABS), # update cursor position @@ -261,19 +272,23 @@ def sustain_mod(self): asm.RTS(), "SUSTAIN_SCROLL_AREA", - asm.LDA(0x0d, asm.DIR), - asm.BIT(0x80, asm.IMM8), # b pressed? - asm.BNE("EXIT_SCROLL_AREA"), + asm.LDA(0x09, asm.DIR), + asm.BIT(0x80, asm.IMM8), # b pressed? + asm.BNE("EXIT_SCROLL_AREA"), # branch if so + ] + + for submenu_idx in self.common.flags.submenus.keys(): + src.extend(self.common.get_submenu_src(submenu_idx, self.common.invoke_flags_submenu[submenu_idx])) + + src += [ asm.JMP(self.common.sustain_scroll_area, asm.ABS), "EXIT_SCROLL_AREA", - asm.JSR(self.common.exit_scroll_area, asm.ABS), - asm.LDA(self.MENU_NUMBER, asm.IMM8), - asm.STA(0x0200, asm.ABS), - - "RETURN", - asm.RTS(), ] + + src.extend(self.common.get_scroll_area_exit_src(self.MENU_NUMBER, self.common.invoke_flags)) + + # Called by C3 JSR jump table space = Write(Bank.C3, src, "track sustain") self.sustain = space.start_address @@ -293,4 +308,4 @@ def mod(self): self.initialize_mod() self.sustain_mod() - self.menu_commands_jump_table() + self.menu_commands_jump_table() \ No newline at end of file diff --git a/music/song_utils.py b/music/song_utils.py new file mode 100644 index 00000000..5a7eef48 --- /dev/null +++ b/music/song_utils.py @@ -0,0 +1,25 @@ + +from constants.entities import TERRA, LOCKE, CYAN, SHADOW, EDGAR, SABIN, CELES, STRAGO, RELM, SETZER, MOG, GAU, GOGO, UMARO +from data.event_bit import TEMP_SONG_OVERRIDE +from instruction import field + +character_to_song = { + TERRA: 0x05, + SHADOW: 0x07, + STRAGO: 0x08, + GAU: 0x09, + EDGAR: 0x0a, + SABIN: 0x0a, + CYAN: 0x0c, + LOCKE: 0x0d, + RELM: 0x0f, + SETZER: 0x10, + CELES: 0x12, + GOGO: 0x2d, + UMARO: 0x30, + MOG: 0x31 +} + +# Return id for character theme given a character id +def get_character_theme(char_id): + return character_to_song[char_id]