Skip to content
297 changes: 241 additions & 56 deletions args/commands.py

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions constants/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@
RANDOM_UNIQUE_COMMAND = 98
NONE_COMMAND = 97 # none command id is 3 digits in base 10 (255), use a custom 2 digit value for args

# retired -com meta-mode prefixes ('-com fr 10.50.90'), recognized only to
# print a migration error pointing at the -comfr/-comfru/-compr/-compru flags
RETIRED_COM_MODES = ["fr", "fru", "pr", "pru"]

# command ids a probability can be declared for (-compr/-compru): every real
# menu command the randomizer hands out (not Revert/Leap/Mimic/Row/Def/Summon
# or the broken Empty slots), plus None (declared as 97, same as -com strings)
PROBABILITY_EXCLUDED_NAMES = ["Revert", "Leap", "Mimic", "Row", "Def", "Summon", "Empty", "Empty?", "None"]
PROBABILITY_COMMAND_IDS = [command_id for command_id, command_name in id_name.items()
if command_name not in PROBABILITY_EXCLUDED_NAMES]

# every character has four command slots in their initialization data ($ed7ca2 - $ed7ca5)
COMMAND_SLOT_COUNT = 4

# commands with a fixed position in the battle menu (fight -> skills -> magic -> item),
# in the order their percent chances are given to the full random modes
COMMON_COMMANDS = ["Fight", "Magic", "Item"]

COMMAND_OPTIONS = ["Morph", "Steal", "SwdTech", "Throw", "Tools", "Blitz", "Runic", "Lore", "Sketch", "Slot", "Dance", "Rage", "Leap"]
EXCLUDE_COMMANDS = ["Item", "Magic", "Revert", "Leap", "Mimic", "Row", "Def", "Summon", "Empty", "Empty?", "None"]

Expand Down
313 changes: 293 additions & 20 deletions data/commands.py

Large diffs are not rendered by default.

21 changes: 18 additions & 3 deletions data/natural_magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,28 @@ def call_check_spell_learn(space, learner, unique_label):
asm.JSR(natural_magic_check, asm.ABS),
)

def mod_learners(self):
def get_magic_users(self, possible_learners):
# characters who can cast their spells in battle. x magic opens the magic menu too,
# so it counts even without the magic command itself
magic_users = set(self.characters.get_characters_with_command("Magic"))
magic_users |= set(self.characters.get_characters_with_command("X Magic"))

return [learner for learner in possible_learners if learner in magic_users]

def random_learner(self, possible_learners):
import random

# prefer a character who can cast in battle, but fall back to any of them if the
# commands flag left nobody with a magic command. a character without one can still
# cast their natural magic outside of battle, so they are not a wasted pick
return random.choice(self.get_magic_users(possible_learners) or possible_learners)

def mod_learners(self):
from data.characters import Characters
possible_learners = list(range(Characters.CHARACTER_COUNT - 2)) # exclude gogo/umaro

if self.args.natural_magic1 == "random":
self.learner1 = random.choice(possible_learners)
self.learner1 = self.random_learner(possible_learners)
self.learner1_name = self.characters.get_name(self.learner1)
elif self.args.natural_magic1:
self.learner1 = self.characters.get_by_name(self.args.natural_magic1).id
Expand All @@ -133,7 +148,7 @@ def mod_learners(self):
pass

if self.args.natural_magic2 == "random":
self.learner2 = random.choice(possible_learners)
self.learner2 = self.random_learner(possible_learners)
self.learner2_name = self.characters.get_name(self.learner2)
elif self.args.natural_magic2:
self.learner2 = self.characters.get_by_name(self.args.natural_magic2).id
Expand Down
2 changes: 2 additions & 0 deletions menus/menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import menus.sell as sell
import menus.buy as buy
import menus.magic as magic
import menus.skills as skills
import menus.required_character_swap as required_character_swap

class Menus:
Expand All @@ -29,6 +30,7 @@ def __init__(self, characters, dances, rages, enemies):
self.sell_menu = sell.SellMenu()
self.buy_menu = buy.BuyMenu()
self.magic_menu = magic.MagicMenu()
self.skills_menu = skills.SkillsMenu()
self.required_character_swap = required_character_swap.RequiredCharacterSwap()

self.scrollbar_bugfix()
Expand Down
71 changes: 71 additions & 0 deletions menus/skills.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from memory.space import Reserve
import instruction.asm as asm

# The Skills submenu (main menu -> Skills -> character) enables its seven rows
# per character in a routine at C3/4D3D: every row starts greyed ($24 at
# $79-$7F), then each of the character's four command bytes is compared against
# a per-row command-id table at C3/4D78 (02 02 07 0A 0C 10 13) and a match
# enables the row ($20). The row byte is both the draw color and the selection
# gate (C3/208B: LDA $79,X : CMP #$20 : BNE deny).
#
# Vanilla keys the ESPERS row (index 0) to the Magic command id (02) -- the
# same test as the Magic row -- with one extra rule greying it for Gogo. With
# randomized commands a character can be esper-capable yet lack the Magic
# command, which wrongly locks them out of equipping espers (level-up stat
# bonuses, spell learning, out-of-battle casting).
#
# This rewrite keeps every other row keyed to its command, but enables the
# Espers row by character id alone: enabled for ids below Gogo (0x0C), greyed
# for Gogo/Umaro and any special record above them. Byte-for-byte the same 59
# bytes, rewritten in place: the command scan indexes the table from its second
# entry and stores to $7A,X (rows 1-6), freeing the tail for the id check.


class SkillsMenu:
def __init__(self):
self.espers_row_mod()

def espers_row_mod(self):
ROW_TABLE_MAGIC_ONWARD = 0xc34d79 # vanilla row table at C3/4D78, minus the espers entry
GOGO = 0x0c # gogo 0x0c, umaro 0x0d, moogles/specials above

space = Reserve(0x34d3d, 0x34d77, "skills menu row enable: espers row by character id")
space.write(
asm.LDA(0x24, asm.IMM8), # a = greyed
asm.LDX(0x00, asm.DIR), # x = 0 ($00 holds zero here, as vanilla relies on)
"GREY_LOOP",
asm.STA(0x79, asm.DIR_X),
asm.INX(),
asm.CPX(0x0007, asm.IMM16),
asm.BNE("GREY_LOOP"), # grey all seven rows

asm.JSR(0x4edd, asm.ABS), # y = character record base
asm.PHY(),
asm.LDX(0x0004, asm.IMM16), # four command slots
"COMMAND_LOOP",
asm.PHX(),
asm.LDX(0x00, asm.DIR), # x = 0: row table index (row 1 = Magic)
"ROW_LOOP",
asm.LDA(0x0016, asm.ABS_Y), # a = character's command byte
asm.CMP(ROW_TABLE_MAGIC_ONWARD, asm.LNG_X),
asm.BNE("NEXT_ROW"),
asm.LDA(0x20, asm.IMM8),
asm.STA(0x7a, asm.DIR_X), # enable matching row (magic..dance)
"NEXT_ROW",
asm.INX(),
asm.CPX(0x0006, asm.IMM16),
asm.BNE("ROW_LOOP"),
asm.INY(), # next command byte
asm.PLX(),
asm.DEX(),
asm.BNE("COMMAND_LOOP"),

asm.PLY(),
asm.LDA(0x0000, asm.ABS_Y), # a = character id
asm.CMP(GOGO, asm.IMM8),
asm.BCS("RETURN"), # gogo/umaro/specials: espers stays greyed
asm.LDA(0x20, asm.IMM8),
asm.STA(0x79, asm.DIR), # everyone else: espers always enabled
"RETURN",
asm.RTS(),
)
Loading
Loading