FacetedFox icon

4-Esper Challenge generator

FacetedFox | PRO | 12/22/21 12:21:55 AM UTC (Edited) | 0 ⭐ | 434 👁️ | Never ⏰ | []
Python |

9.49 KB

|

None

|

0 👍

/

0 👎

#!/usr/bin/env python3
#
# Copyright (C) 2019-2022 Síle Ekaterin Liszka
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
# above copyright notice and this permission notice appear in all
# copies.
# 
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR 
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, 
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION 
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 
#
# This project was made possible by:
# * Lenophis, who told me where the 'give this esper' bytes are
# * ff6hacking.com, which hosts the data tables I used to pick from.
# * CanOfWorms, for figuring out the GBA offsets.
#
# ChangeLog:
# 0.3:
# - GBA support! Purportedly works for J, U, *and* E versions.
# 0.2:
# - Preliminary work toward supporting GBA ROMs. This needs data I
#   don't have to complete, however, but the cleanup it required was
#   useful.
# - Reworked learning rates. Rates should be overall lower now.
# - Majority of output moved to a spoilers file.
 
import sys
import io
 
from random import randint, seed, shuffle, random, choice, sample
from enum import IntEnum
from copy import copy
 
class Bonus(IntEnum):
    HP_10 = 0x00
    HP_30 = 0x01
    HP_50 = 0x02
    MP_10 = 0x03
    MP_30 = 0x04
    MP_50 = 0x05
    HP100 = 0x06
    LV_30 = 0x07
    LV_50 = 0x08
    Str_1 = 0x09
    Str_2 = 0x0A
    Spd_1 = 0x0B
    Spd_2 = 0x0C
    Vit_1 = 0x0D
    Vit_2 = 0x0E
    Mag_1 = 0x0F
    Mag_2 = 0x10
    Nil   = 0xFF
 
bonuses = {
    Bonus.HP_10: 'HP +10%',
    Bonus.HP_30: 'HP +30%',
    Bonus.HP_50: 'HP +50%',
    Bonus.MP_10: 'MP +10%',
    Bonus.MP_30: 'MP +30%',
    Bonus.MP_50: 'MP +50%',
    Bonus.HP100: 'HP +100%',
    Bonus.Str_1: 'Strength +1',
    Bonus.Str_2: 'Strength +2',
    Bonus.Spd_1: 'Speed +1',
    Bonus.Spd_2: 'Speed +2',
    Bonus.Mag_1: 'Magic +1',
    Bonus.Mag_2: 'Magic +2',
}
 
class Spell(IntEnum):
    Fire       = 0x00
    Blizzard   = 0x01
    Thunder    = 0x02
    Poison     = 0x03
    Drain      = 0x04
    Firera     = 0x05
    Blizzardra = 0x06
    Thunderra  = 0x07
    Bio        = 0x08
    Firega     = 0x09
    Blizzardga = 0x0A
    Thunderga  = 0x0B
    Break      = 0x0C
    Death      = 0x0D
    Holy       = 0x0E
    Flare      = 0x0F
    Gravity    = 0x10
    Gravityga  = 0x11
    Dezone     = 0x12
    Meteor     = 0x13
    Ultima     = 0x14
    Quake      = 0x15
    Tornado    = 0x16
    Meltdown   = 0x17
    Library    = 0x18
    Slow       = 0x19
    Rasp       = 0x1A
    Silence    = 0x1B
    Protect    = 0x1C
    Sleep      = 0x1D
    Confuse    = 0x1E
    Haste      = 0x1F
    Stop       = 0x20
    Berserk    = 0x21
    Float      = 0x22
    Kappa      = 0x23
    Reflect    = 0x24
    Shell      = 0x25
    Vanish     = 0x26
    Hastega    = 0x27
    Slowga     = 0x28
    Osmose     = 0x29
    Warp       = 0x2A
    Quick      = 0x2B
    Dispel     = 0x2C
    Cure       = 0x2D
    Curera     = 0x2E
    Curega     = 0x2F
    Raise      = 0x30
    Arise      = 0x31
    NullPoison = 0x32
    NullStatus = 0x33
    Regen      = 0x34
    Reraise    = 0x35
 
    Flood   = 54
    Gravija = 55
    Valor   = 56
 
    SNES_Count = 54
    GBA_Count  = 57
 
class Esper(IntEnum):
    Ramuh         =  0
    Ifrit         =  1
    Shiva         =  2
    Siren         =  3
    Jormungand    =  4
    Catoblepas    =  5
    Maduin        =  6
    Bismarck      =  7
    CatSidhe      =  8
    Quetzali      =  9
    Valigarmander = 10
    Odin          = 11
    Raiden        = 12
    Bahamut       = 13
    Alexander     = 14
    Jihad         = 15
    Ragnarok      = 16
    QiLin         = 17
    ZonaSeeker    = 18
    Carbunkle     = 19
    Phantom       = 20
    Seraphim      = 21
    Golem         = 22
    Unicorn       = 23
    Fenrir        = 24
    Lakshmi       = 25
    Phoenix       = 26
 
    Leviathan = 27
    Cactender = 28
    Diabolus  = 29
    Gilgamesh = 30
 
    SNES_Count = 27
    GBA_Count = 31
 
def roll_espers(max):
    return [Esper(i) for i in sample(list(range(max)), 4)]
 
spell_rate = [1, 1, 1, 2, 2, 3, 4, 3, 2, 2, 1, 1, 1]
 
def roll_spells(count, max, forbidden=[]):
    spells = []
    spell_base = []
 
    def add_spell(spell):
        spell_base.append(spell)
        spells.append([spell, choice(spell_rate) * 5])
 
    if randint(1, 100) < 50: add_spell(Spell.Curera)
    else:                    add_spell(Spell.Curega)
 
    num = randint(1, 100)
    if num < 34:
        add_spell(Spell.Fire)
        num2 = randint(1, 100)
        if num2 < 50: add_spell(Spell.Blizzardra)
        else:         add_spell(Spell.Thunderra)
    elif num < 67:
        add_spell(Spell.Blizzard)
        num2 = randint(1, 100)
        if num2 < 50: add_spell(Spell.Firera)
        else:         add_spell(Spell.Thunderra)
    else:
        add_spell(Spell.Thunder)
        num2 = randint(1, 100)
        if num2 < 50: add_spell(Spell.Firera)
        else:         add_spell(Spell.Blizzardra)
 
    add_spell(Spell.Protect)
    add_spell(Spell.Shell)
    add_spell(Spell.Haste)
 
    num = randint(1, 100)
    if num < 34:   add_spell(Spell.Raise)
    elif num < 67: add_spell(Spell.Arise)
 
    n = count - len(spells)
    while True:
        if n < 0:
            break
 
        spell = Spell(randint(0, max))
 
        if spell not in spell_base and spell not in forbidden:
            add_spell(spell)
            n -= 1
 
    return spells
 
def generate(rng_seed=None, mode='snes'):
    seed(rng_seed)
 
    if mode == 'snes':
        esper_max = Esper.SNES_Count - 1
        spell_max = Spell.SNES_Count - 1
    elif mode == 'gba':
        esper_max = Esper.GBA_Count - 1
        spell_max = Spell.GBA_Count - 1
 
    espers = roll_espers(esper_max)
    statup = [Bonus(i) for i in sample(list(bonuses.keys()), 4)]
 
    spells = roll_spells(20, spell_max)
    shuffle(spells)
 
    return {
        'espers': espers,
        'level+': statup,
        'spells': spells,
        'seed':   rng_seed
    }
 
remove = [
    0xA55DE, 0xB4DE3, 0xB52C9, 0xB5452,
    0xA5A2C, 0xB5B77, 0xB9A7C, 0xC1ED1,
    0xC1F85, 0xC2044, 0xC316C, 0xC37A8,
    0xC4ADA, 0xC5E08, 0xC5E08, 0xC79D9,
    0xC79E9, 0xCD78F
]
def write_rom(inname, outname, mode, data):
    print('Writing data for seed {} to {}...'.format(data['seed'], realname))
 
    offset = 0
    if mode == 'gba':
        offset = 0x70000
        spello = 0x62A480
        remove.extend((0xD2B52, 0xD2BD4, 0xD11ED, 0xD27A5))
    else:
        spello = 0x186e00
 
    rom = io.BytesIO()
 
    with open(inname, mode='rb') as f:
        rom.write(f.read())
 
    # erase all the slots we're removing
    # Bahamut
    rom.seek(0xA00DE + offset, 0)
    rom.write(b'\xFD' * 5)
    # sorcerous research facility
    rom.seek(0xC7C6D + offset, 0)
    rom.write(b'\xFD' * 11)
    # everybody else
    for i in remove:
        rom.seek(i + offset, 0)
        rom.write(b'\xFD' * 2)
 
    # these four are the espers given when you reach Tina in Zozo
    rom.seek(0xAA824 + offset, 0)
    rom.write(bytes([data['espers'][0] + 0x36]))
    rom.seek(0xAAC9C + offset, 0)
    rom.write(bytes([data['espers'][1] + 0x36]))
    rom.seek(0xAACAB + offset, 0)
    rom.write(bytes([data['espers'][2] + 0x36]))
    rom.seek(0xAACBA + offset, 0)
    rom.write(bytes([data['espers'][3] + 0x36]))
 
    # write esper data for the espers we're giving out
    for i in range(4):
        rom.seek(spello + (data['espers'][i] * 11), 0)
        for j in range(5):
            spell = data['spells'].pop(0)
            rom.write(bytes([spell[1], spell[0]]))
        rom.write(bytes([data['level+'][i]]))
 
    rom.seek(0, 0)
 
    with open(outname, mode='wb') as f:
        f.write(rom.read())
 
if __name__ == '__main__':
    from sys import argv
    from time import time
 
    now = str(time()).split('.')[0]
 
    if len(argv) == 3:
        rng_seed = argv[2]
    elif len(argv) < 2 or len(argv) > 3:
        print('Usage: {} <filename> [seed]'.format(argv[0]))
    else:
        rng_seed = now
 
    filename = argv[1]
 
    if filename.endswith('.gba'):
        mode = 'gba'
    else:
        mode = 'snes'
    print('Mode: {}'.format(mode))
 
    print('Seed: {}'.format(rng_seed))
    data = generate(rng_seed, mode)
 
    filefrags = filename.split('.')
    ext = filefrags[-1]
    filefrags[-1] = str(data['seed'])
    filefrags.append(ext)
    realname = '.'.join(filefrags)
    filefrags[-1] = 'txt'
    spoilers = '.'.join(filefrags)
 
    espers = []
    for i in range(4):
        esper = str(data['espers'][i]).split('.')[1]
        bonus = bonuses[data['level+'][i]]
        espers.append(esper + '(' + bonus + ')')
    
    spells = data['spells']
 
    with open(spoilers, mode='wt', encoding='utf8') as f:
        f.write('Seed: {}\n'.format(rng_seed))
        for i in range(4):
            f.write('{}\n'.format(espers[i]))
 
            for j in range(5):
                spell = data['spells'][(i * 5) + j]
                f.write('  {} ×{}\n'.format(str(spell[0]).split('.')[1], spell[1]))
 
    write_rom(filename, realname, mode, data)

Comments

  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎