diff --git a/args/challenges.py b/args/challenges.py index 50acd3eb..44fd76f3 100644 --- a/args/challenges.py +++ b/args/challenges.py @@ -10,6 +10,8 @@ def parse(parser): challenges.add_argument("-nil", "--no-illuminas", action = "store_true", help = "Illuminas will not appear in coliseum/auction/shops/chests/events") ultima = challenges.add_mutually_exclusive_group() + challenges.add_argument("-noshoes", "--no-sprint-shoes", action = "store_true", + help = "Sprint Shoes will not appear in coliseum/auction/shops/chests") ultima.add_argument("-nu", "--no-ultima", action = "store_true", help = "Ultima cannot be learned from espers/items/natural magic") ultima.add_argument("-u254", "--ultima-254-mp", action = "store_true", @@ -68,6 +70,9 @@ def flags(args): flags += " -nee" if args.no_illuminas: flags += " -nil" + if args.no_sprint_shoes: + flags += " -noshoes" + if args.no_ultima: flags += " -nu" elif args.ultima_254_mp: @@ -96,6 +101,7 @@ def options(args): ("No Exp Eggs", args.no_exp_eggs), ("No Illuminas", args.no_illuminas), ("Ultima", ultima), + ("No Sprint Shoes", args.no_sprint_shoes), ("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/misc.py b/args/misc.py index 525e253a..43c32153 100644 --- a/args/misc.py +++ b/args/misc.py @@ -1,10 +1,11 @@ + + def name(): return "Misc." def parse(parser): misc = parser.add_argument_group("Misc.") - misc.add_argument("-as", "--auto-sprint", action = "store_true", - help = "Player always sprints. Sprint Shoes have no effect") + misc.add_argument("-ond", "--original-name-display", action = "store_true", help = "Display original character names in party and party select menus") misc.add_argument("-rr", "--random-rng", action = "store_true", @@ -16,6 +17,15 @@ def parse(parser): misc.add_argument("-warp", "--warp-all", action = "store_true", help = "All characters start with Warp learned. Warp costs 0 MP. Useful for seeds that limit Warp Stone access") + from data.movement import ALL + movement = misc.add_mutually_exclusive_group() + movement.name = "Movement" + movement.add_argument("-move", "--movement", type = str.lower, choices = ALL, + help = "Player movement options") + # Completely ignore this argument, and default to auto sprint when -move is not defined + misc.add_argument("-as", "--auto-sprint", action = "store_true", + help = "DEPRECATED - Use `-move as` instead. Player always sprints. Sprint Shoes have no effect") + event_timers = misc.add_mutually_exclusive_group() event_timers.add_argument("-etr", "--event-timers-random", action = "store_true", help = "Collapsing House, Opera House, and Floating Continent timers randomized") @@ -57,8 +67,8 @@ def process(args): def flags(args): flags = "" - if args.auto_sprint: - flags += " -as" + if args.movement: + flags += f" -move {args.movement}" if args.original_name_display: flags += " -ond" if args.random_rng: @@ -127,8 +137,15 @@ def options(args): elif args.y_npc_remove: y_npc = "Remove" + from data.movement import key_name, AUTO_SPRINT + # Similar logic is present in the init fn of settings/movement.py + if args.movement: + movement = key_name[args.movement] + else: + movement = key_name[AUTO_SPRINT] + return [ - ("Auto Sprint", args.auto_sprint), + ("Movement", movement), ("Original Name Display", args.original_name_display), ("Random RNG", args.random_rng), ("Random Clock", args.random_clock), diff --git a/args/starting_gold_items.py b/args/starting_gold_items.py index dcf3c8ac..b769e8a3 100644 --- a/args/starting_gold_items.py +++ b/args/starting_gold_items.py @@ -9,6 +9,9 @@ def parse(parser): starting_gold_items.add_argument("-smc", "--start-moogle-charms", default = 0, type = int, choices = range(4), metavar = "COUNT", help = "Start game with %(metavar)s Moogle Charms. Overrides No Moogle Charms option") + starting_gold_items.add_argument("-sshoes", "--start-sprint-shoes", default = 0, type = int, choices = range(4), metavar = "COUNT", + help = "Start game with %(metavar)s Sprint Shoes. Overrides No Sprint Shoes option") + starting_gold_items.add_argument("-sws", "--start-warp-stones", default = 0, type = int, choices = range(11), metavar = "COUNT", help = "Start game with %(metavar)s Warp Stones") starting_gold_items.add_argument("-sfd", "--start-fenix-downs", default = 0, type = int, choices = range(11), metavar = "COUNT", @@ -28,6 +31,8 @@ def flags(args): flags += f" -gp {args.gold}" if args.start_moogle_charms != 0: flags += f" -smc {args.start_moogle_charms}" + if args.start_sprint_shoes != 0: + flags += f" -sshoes {args.start_sprint_shoes}" if args.start_warp_stones != 0: flags += f" -sws {args.start_warp_stones}" if args.start_fenix_downs != 0: @@ -43,6 +48,7 @@ def options(args): return [ ("Start Gold", args.gold), ("Start Moogle Charms", args.start_moogle_charms), + ("Start Sprint Shoes", args.start_sprint_shoes), ("Start Warp Stones", args.start_warp_stones), ("Start Fenix Downs", args.start_fenix_downs), ("Start Tools", args.start_tools), diff --git a/constants/objectives/results.py b/constants/objectives/results.py index e44717e4..445bb88d 100644 --- a/constants/objectives/results.py +++ b/constants/objectives/results.py @@ -87,6 +87,7 @@ #Additional results category_types["Command"].append(ResultType(59, "Magitek Upgrade", "Magitek Upgrade", None)) +category_types["Item"].append(ResultType(60, "Sprint Shoes", "Sprint Shoes", None)) category_types["Auto"].append(ResultType(61, "Auto Dog Block", "Auto Dog Block", None)) category_types["Auto"].append(ResultType(62, "Auto Life 3", "Auto Life 3", None)) diff --git a/data/items.py b/data/items.py index e625868d..a03afa2f 100644 --- a/data/items.py +++ b/data/items.py @@ -297,6 +297,11 @@ def get_excluded(self): exclude.append(name_id["Exp. Egg"]) if self.args.no_illuminas: exclude.append(name_id["Illumina"]) + + from data.movement import AUTO_SPRINT, B_DASH + # Sprint Shoes are a literal dead item if any of these options + if self.args.no_sprint_shoes or self.args.movement in [AUTO_SPRINT, B_DASH]: + exclude.append(name_id["Sprint Shoes"]) if self.args.no_free_paladin_shields: exclude.append(name_id["Paladin Shld"]) exclude.append(name_id["Cursed Shld"]) diff --git a/data/movement.py b/data/movement.py new file mode 100644 index 00000000..b8c52601 --- /dev/null +++ b/data/movement.py @@ -0,0 +1,34 @@ + +from enum import IntEnum + + +class MovementSpeed(IntEnum): + WALK = 2 # Original FF6 walk speed + SPRINT = 3 # Original FF6 sprint speed + DASH = 4 # Custom, move twice as fast as sprint. + +ORIGINAL = 'og' +AUTO_SPRINT = 'as' +B_DASH = 'bd' +SPRINT_SHOES_B_DASH = 'ssbd' + +name_key = { + # WALK by default + # SPRINT with sprint shoes equipped + 'ORIGINAL' : ORIGINAL, + # SPRINT by default + # WALK when holding B + 'AUTO_SPRINT' : AUTO_SPRINT, + # SPRINT by default + # DASH when holding B + 'B-DASH' : B_DASH, + # SPRINT by default + # DASH when holding B with sprint shoes equipped + # WALK when holding B without sprint shoes equipped + 'SPR SHOE B-DASH' : SPRINT_SHOES_B_DASH, +} + +key_name = {v: k for k, v in name_key.items()} + +ALL = [ORIGINAL, AUTO_SPRINT, B_DASH, SPRINT_SHOES_B_DASH] + diff --git a/event/start.py b/event/start.py index f63faeed..39f9b4b1 100644 --- a/event/start.py +++ b/event/start.py @@ -187,7 +187,10 @@ def start_items_mod(self): src += [ field.AddItem("Moogle Charm", sound_effect = False), ] - + for mc in range(self.args.start_sprint_shoes): + src += [ + field.AddItem("Sprint Shoes", sound_effect = False), + ] for ws in range(self.args.start_warp_stones): src += [ field.AddItem("Warp Stone", sound_effect = False), diff --git a/objectives/results/sprint_shoes.py b/objectives/results/sprint_shoes.py new file mode 100644 index 00000000..4318850c --- /dev/null +++ b/objectives/results/sprint_shoes.py @@ -0,0 +1,19 @@ +from objectives.results._objective_result import * +from data.item_names import name_id as item_name_id + +class Field(field_result.Result): + def src(self): + return [ + field.AddItem(item_name_id["Sprint Shoes"]), + ] + +class Battle(battle_result.Result): + def src(self): + return [ + battle_result.AddItem(item_name_id["Sprint Shoes"]), + ] + +class Result(ObjectiveResult): + NAME = "Sprint Shoes" + def __init__(self): + super().__init__(Field, Battle) diff --git a/settings/__init__.py b/settings/__init__.py index 87a91b2b..d0355987 100644 --- a/settings/__init__.py +++ b/settings/__init__.py @@ -1,5 +1,5 @@ -from settings.auto_sprint import AutoSprint from settings.initial_spells import InitialSpells +from settings.movement import Movement from settings.random_rng import RandomRNG from settings.permadeath import Permadeath from settings.y_npc import YNPC @@ -11,8 +11,8 @@ __all__ = ["Settings"] class Settings: def __init__(self): - self.auto_sprint = AutoSprint() self.initial_spells = InitialSpells() + self.movement = Movement() self.random_rng = RandomRNG() self.permadeath = Permadeath() self.y_npc = YNPC() diff --git a/settings/auto_sprint.py b/settings/auto_sprint.py deleted file mode 100644 index e03252db..00000000 --- a/settings/auto_sprint.py +++ /dev/null @@ -1,36 +0,0 @@ -from memory.space import Reserve -import instruction.asm as asm -import args - -class AutoSprint: - def __init__(self): - if args.auto_sprint: - self.mod() - - def mod(self): - # set sprint by default, b button to walk, sprint shoes do nothing - - WALK_SPEED = 2 - SPRINT_SPEED = 3 - - CONTROLLER1_BYTE2 = 0x4219 - B_BUTTON_MASK = 0x80 - FIELD_RAM_SPEED = 0x0875 - - src = [ - asm.LDA(CONTROLLER1_BYTE2, asm.ABS), - asm.AND(B_BUTTON_MASK, asm.IMM8), - asm.BNE("WALK"), # branch if b button down - - "SPRINT", - asm.LDA(SPRINT_SPEED, asm.IMM8), - asm.BRA("STORE_SPEED"), - - "WALK", - asm.LDA(WALK_SPEED, asm.IMM8), - - "STORE_SPEED", - asm.STA(FIELD_RAM_SPEED, asm.ABS_Y), - ] - space = Reserve(0x04e21, 0x04e37, "auto sprint", asm.NOP()) - space.write(src) diff --git a/settings/movement.py b/settings/movement.py new file mode 100644 index 00000000..de9b3e9f --- /dev/null +++ b/settings/movement.py @@ -0,0 +1,164 @@ +from memory.space import Allocate, Bank, Reserve, Write +import instruction.asm as asm +from data.movement import AUTO_SPRINT, B_DASH, ORIGINAL, SPRINT_SHOES_B_DASH, MovementSpeed + +class Movement: + def __init__(self): + import args + self.movement = args.movement or AUTO_SPRINT + + if self.movement != ORIGINAL: + self.mod() + + def mod(self): + length = 0 + src = [] + + src = self.get_auto_sprint_src() + + space = Write(Bank.F0, src, "Sprint subroutine") + + src = [ + asm.JSL(space.start_address_snes), + ] + space = Reserve(0x04e21, 0x04e37, "auto sprint", asm.NOP()) + space.write(src) + + self.sliding_dash_fix() + + + def get_auto_sprint_src(self): + import args + CURRENT_MAP_BYTE = 0x82 # 2 bytes + OWZERS_MANSION_ID = 0x00CF # the door room can create visual artifacts on the map while dashing + CONTROLLER1_BYTE2 = 0x4219 + SPRINT_SHOES_BYTE = 0x11df + SPRINT_SHOES_MASK = 0x20 + B_BUTTON_MASK = 0x80 + FIELD_RAM_SPEED = 0x0875 + + # moving at dash speed in Owzer's door room, or carrying it out via the door glitch will cause graphical artifacting randomly. + # Simply disabling B button in Owzers to keep it consistent in WC. Will not worry about the door glitch + owzers_src = [ + "CHECK_OWZERS", + asm.A16(), # set register A bit size to 16 + asm.LDA(CURRENT_MAP_BYTE, asm.ABS), # if current map owzers mansion, disable the b-button + asm.CMP(OWZERS_MANSION_ID, asm.IMM16), + asm.BEQ("STORE_DEFAULT"), + asm.LDA(0x0000, asm.IMM16), # clear A, otherwise will cause issues in albrook/imperial base + asm.A8(), + ] + + src = [ + "B_BUTTON_CHECK", + asm.LDA(CONTROLLER1_BYTE2, asm.ABS), + asm.AND(B_BUTTON_MASK, asm.IMM8), + asm.BEQ("STORE_DEFAULT"), # do nothing if b pressed + ] + + if self.movement == AUTO_SPRINT: + src += [ + "ON_B_BUTTON", + asm.LDA(MovementSpeed.WALK, asm.IMM8), + asm.BRA("STORE"), + ] + elif self.movement == B_DASH: + src += owzers_src + src += [ + "ON_B_BUTTON", + asm.LDA(MovementSpeed.DASH, asm.IMM8), + asm.BRA("STORE"), + ] + + elif self.movement == SPRINT_SHOES_B_DASH: + src += owzers_src + src += [ + "ON_B_BUTTON", + asm.LDA(SPRINT_SHOES_BYTE, asm.ABS), # If sprint shoes equipped, store secondary movement speed + asm.AND(SPRINT_SHOES_MASK, asm.IMM8), + asm.BEQ("WALK") + ] + src += [ + "DASH", + asm.LDA(MovementSpeed.DASH, asm.IMM8), + asm.BRA("STORE"), + "WALK", + asm.LDA(MovementSpeed.WALK, asm.IMM8), + asm.BRA("STORE"), + ] + + src += [ + "STORE_DEFAULT", + asm.A8(), + asm.LDA(MovementSpeed.SPRINT, asm.IMM8), + + "STORE", + asm.STA(FIELD_RAM_SPEED, asm.ABS_Y), # store speed in ram + asm.RTL(), # return + ] + + return src + + + # DIRECTION VALUE + # $087F ------dd + # d: facing direction + # 00 = up + # 01 = right + # 10 = down + # 11 = left + + # https://silentenigma.neocities.org/ff6/index.html + # Will leave bits of documentation about in the event neocities does not stand the test of time + + # With dash enabled, this causes a bug that the player will appear to be standing still when + # running down or right at move speed 5. This is because two sprite instances are thrown out of + # the animation cycle when running at that speed; while Up/Left correctly omits the standing + # sprite, Down/Right omits the stepping sprites, since the offsets lag by an iteration. + def sliding_dash_fix(self): + DIRECTION_VALUE = 0x087f + + # C0/0000: 4A LSR ; Shift offset bits right + # C0/0001: 4A LSR ; Shift offset bits right + # C0/0002: 48 PHA ; Push offset value to stack + # C0/0003: B9 7F 08 LDA $087F,y ; Load direction value + # C0/0006: C9 01 CMP #$01 ; Check if direction is Right + # C0/0008: F0 07 BEQ $D69E ; Branch if the direction is Right + # C0/000A: C9 02 CMP #$02 ; Check if the direction is Down + # C0/000C: F0 03 BEQ $D69E ; Branch if the direction is Down + # C0/000E: 68 PLA ; Pull offset back off of stack + # C0/000F: 80 02 BRA $D6A0 ; Branch to the third LSR + # C0/0011: 68 PLA ; Pull offset back off of stack + # C0/0012: 1A INC ; Increase the offset value by 1 + # C0/0013: 4A LSR ; Shift offset bits right + # C0/0014: 60 RTS ; Return from subfunction + subroutine_src = [ + asm.LSR(), + asm.LSR(), + asm.PHA(), + asm.LDA(DIRECTION_VALUE, asm.ABS_Y), + asm.DEC(), + asm.BEQ("FACING_RIGHT"), + asm.DEC(), + asm.BNE("RETURN"), + "FACING_RIGHT", + asm.PLA(), + asm.INC(), + asm.PHA(), + "RETURN", + asm.PLA(), + asm.LSR(), + asm.RTS(), + ] + subroutine_space = Allocate(Bank.C0, 20, "walking speed calculation", asm.NOP()) + subroutine_space.write(subroutine_src) + + src = [ + asm.JSR(subroutine_space.start_address, asm.ABS) + ] + + space = Reserve(0x5885, 0x5887, "Sprite offset calculation 1", asm.NOP()) + space.write(src) + + space = Reserve(0x5892, 0x5894, "Sprite offset calculation 2") + space.write(src)