* Change vanilla_emblem_pieces to randomize_emblem_pieces * Add jungle slider and starting tools options * Update option name and add preset * GICU changes * unnecessary * Update Options.py * Fix has_all * Update Options.py * Update Options.py * Some potenitial logic changes * Oops * Oops 2 * Cups choice options * typos * Logic tweaks * Ice Titan and Superboss changes * Suggested change and one more * Updating some other option descriptions for clarity/typos * Update Locations.py * commit * SYNTHESIS * commit * commit * commit * Add command to change communication path I'm not a python programmer, so do excuse the code etiquette. This aims to allow Linux users to communicate to their proton directory. * commit * commit * commit * commit * commit * commit * commit * commit * Update Client.py * Update Locations.py * Update Regions.py * commit * commit * commit * Update Rules.py * commit * commit * commit * commit logic changes and linux fix from other branch * commit * commit * Update __init__.py * Update Rules.py * commit * commit * commit * commit * add starting accessory setting * fix starting accessories bug * Update Locations.py * commit * add ap cost rando * fix some problem locations * add raft materials * Update Client.py * OK WORK THIS TIME PLEASE * Corrected typos * setting up for logic difficulty * commit 1 * commit 2 * commit 3 * minor error fix * some logic changes and fixed some typos * tweaks * commit * SYNTHESIS * commit * commit * commit * commit * commit * commit * commit * commit * commit * commit * commit * Update Client.py * Update Locations.py * Update Regions.py * commit * commit * commit * Update Rules.py * commit * commit * commit * commit logic changes and linux fix from other branch * commit * commit * Update __init__.py * Update Rules.py * commit * commit * commit * commit * add starting accessory setting * fix starting accessories bug * Update Locations.py * commit * add ap cost rando * fix some problem locations * add raft materials * Update Client.py * cleanup * commit 4 * tweaks 2 * tweaks 3 * Reset * Update __init__.py * Change vanilla_emblem_pieces to randomize_emblem_pieces * Add jungle slider and starting tools options * unnecessary * Vanilla Puppies Part 1 The easy part * Update __init__.py I'm not certain this is the exact right chest for Tea Party Garden, Waterfall Cavern, HT Cemetery, or Neverland Hold but logically it's the same. Will do a test run later and fix if need be * Vanilla Puppies Part 3 Wrong toggle cause I just copied over Emblem Pieces oops * Vanilla Puppies Part 4 Forgor commented out code * Vanilla Puppies Part 5 I now realize how this works and that what I had before was redundant * Update __init__.py Learning much about strings * cleanup * Update __init__.py Only missed one! * Update option name and add preset * GICU changes * Update Options.py * Fix has_all * Update Options.py * Update Options.py * Cups choice options * typos * Ice Titan and Superboss changes * Some potenitial logic changes * Oops * Oops 2 * Logic tweaks * Suggested change and one more * Updating some other option descriptions for clarity/typos * Update Locations.py * Add command to change communication path I'm not a python programmer, so do excuse the code etiquette. This aims to allow Linux users to communicate to their proton directory. * Moving over changes from REVAMP * whoops * Fix patch files on the website * Update test_goal.py * commit * Update worlds/kh1/__init__.py Co-authored-by: Scipio Wright <scipiowright@gmail.com> * change some default options * Missed a condition * let's try that * Update Options.py * unnecessary sub check * Some more cleanup * tuples * add icon * merge cleanup * merge cleanup 2 * merge clean up 3 * Update Data.py * Fix cups option * commit * Update Rules.py * Update Rules.py * phantom tweak * review commit * minor fixes * review 2 * minor typo fix * minor logic tweak * Update Client.py * Update __init__.py * Update Rules.py * Olympus Cup fixes * Update Options.py * even MORE tweaks * commit * Update Options.py * Update has_x_worlds * Update Rules.py * commit * Update Options.py * Update Options.py * Update Options.py * tweak 5 * Add Stacking Key Items and Halloween Town Key Item Bundle * Update worlds/kh1/Rules.py Co-authored-by: Scipio Wright <scipiowright@gmail.com> * Update Rules.py * commit * Update worlds/kh1/__init__.py Co-authored-by: Scipio Wright <scipiowright@gmail.com> * Update __init__.py * Update __init__.py * whoops * Update Rules.py * Update Rules.py * Fix documentation styling * Clean up option help text * Reordering options so they're consistent and fixing a logic bug when EOTW Unlock is item but door is emblems * Make have x world logic consider if the player has HAW on or not * Fix Atlantica beginner logic things, vanilla keyblade stats being broken, and some behind boss locations * Fix vanilla puppy option * hotfix for crabclaw logic * Fix defaults and some boss locations * Fix server spam * Remove 3 High Jump Item Workshop Logic, small client changes * Updates for PR --------- Co-authored-by: esutley <ecsutley@gmail.com> Co-authored-by: Goblin God <37878138+esutley@users.noreply.github.com> Co-authored-by: River Buizel <4911928+rocket0634@users.noreply.github.com> Co-authored-by: omnises <OmnisGamers@gmail.com> Co-authored-by: Omnises Nihilis <38057571+Omnises@users.noreply.github.com> Co-authored-by: Scipio Wright <scipiowright@gmail.com>
615 lines
38 KiB
Python
615 lines
38 KiB
Python
import logging
|
|
import re
|
|
from typing import List
|
|
from math import ceil
|
|
|
|
from BaseClasses import Tutorial
|
|
from worlds.AutoWorld import WebWorld, World
|
|
from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups
|
|
from .Locations import KH1Location, location_table, get_locations_by_type, location_name_groups
|
|
from .Options import KH1Options, kh1_option_groups
|
|
from .Regions import connect_entrances, create_regions
|
|
from .Rules import set_rules
|
|
from .Presets import kh1_option_presets
|
|
from worlds.LauncherComponents import Component, components, Type, launch as launch_component, icon_paths
|
|
from .GenerateJSON import generate_json
|
|
from .Data import VANILLA_KEYBLADE_STATS, VANILLA_PUPPY_LOCATIONS, CHAR_TO_KH, VANILLA_ABILITY_AP_COSTS, WORLD_KEY_ITEMS
|
|
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
|
|
|
|
def launch_client():
|
|
from .Client import launch
|
|
launch_component(launch, name="KH1 Client")
|
|
|
|
|
|
components.append(Component("KH1 Client", "KH1Client", func=launch_client, component_type=Type.CLIENT, icon="kh1_heart"))
|
|
|
|
icon_paths["kh1_heart"] = f"ap:{__name__}/icons/kh1_heart.png"
|
|
|
|
|
|
class KH1Web(WebWorld):
|
|
theme = "ocean"
|
|
tutorials = [Tutorial(
|
|
"Multiworld Setup Guide",
|
|
"A guide to setting up the Kingdom Hearts Randomizer software on your computer."
|
|
"This guide covers single-player, multiworld, and related software.",
|
|
"English",
|
|
"kh1_en.md",
|
|
"kh1/en",
|
|
["Gicu"]
|
|
)]
|
|
option_groups = kh1_option_groups
|
|
options_presets = kh1_option_presets
|
|
|
|
|
|
class KH1World(World):
|
|
"""
|
|
Kingdom Hearts is an action RPG following Sora on his journey
|
|
through many worlds to find Riku and Kairi.
|
|
"""
|
|
game = "Kingdom Hearts"
|
|
options_dataclass = KH1Options
|
|
options: KH1Options
|
|
topology_present = True
|
|
web = KH1Web()
|
|
|
|
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
|
location_name_to_id = {name: data.code for name, data in location_table.items()}
|
|
item_name_groups = item_name_groups
|
|
location_name_groups = location_name_groups
|
|
fillers = {}
|
|
fillers.update(get_items_by_category("Item"))
|
|
fillers.update(get_items_by_category("Camping"))
|
|
fillers.update(get_items_by_category("Stat Ups"))
|
|
slot_2_levels: list[int]
|
|
keyblade_stats: list[dict[str, int]]
|
|
starting_accessory_locations: list[str]
|
|
starting_accessories: list[str]
|
|
ap_costs: list[dict[str, str | int | bool]]
|
|
|
|
def __init__(self, multiworld, player):
|
|
super(KH1World, self).__init__(multiworld, player)
|
|
self.slot_2_levels = None
|
|
self.keyblade_stats = None
|
|
self.starting_accessory_locations = None
|
|
self.starting_accessories = None
|
|
self.ap_costs = None
|
|
|
|
def create_items(self):
|
|
self.place_predetermined_items()
|
|
# Handle starting worlds
|
|
starting_worlds = []
|
|
if self.options.starting_worlds > 0:
|
|
possible_starting_worlds = ["Wonderland", "Olympus Coliseum", "Deep Jungle", "Agrabah", "Monstro", "Halloween Town", "Neverland", "Hollow Bastion"]
|
|
if self.options.atlantica:
|
|
possible_starting_worlds.append("Atlantica")
|
|
if self.options.destiny_islands:
|
|
possible_starting_worlds.append("Destiny Islands")
|
|
if self.options.end_of_the_world_unlock == "item":
|
|
possible_starting_worlds.append("End of the World")
|
|
starting_worlds = self.random.sample(possible_starting_worlds, min(self.options.starting_worlds.value, len(possible_starting_worlds)))
|
|
for starting_world in starting_worlds:
|
|
self.multiworld.push_precollected(self.create_item(starting_world))
|
|
|
|
# Handle starting tools
|
|
starting_tools = []
|
|
if self.options.starting_tools:
|
|
starting_tools = ["Scan", "Dodge Roll"]
|
|
self.multiworld.push_precollected(self.create_item("Scan"))
|
|
self.multiworld.push_precollected(self.create_item("Dodge Roll"))
|
|
|
|
# Handle starting party member accessories
|
|
starting_party_member_accessories = []
|
|
starting_party_member_locations = []
|
|
starting_party_member_locations = self.get_starting_accessory_locations()
|
|
starting_party_member_accessories = self.get_starting_accessories()
|
|
for i in range(len(starting_party_member_locations)):
|
|
self.get_location(self.starting_accessory_locations[i]).place_locked_item(self.create_item(self.starting_accessories[i]))
|
|
|
|
item_pool: List[KH1Item] = []
|
|
possible_level_up_item_pool = []
|
|
level_up_item_pool = []
|
|
|
|
# Calculate Level Up Items
|
|
# Fill pool with mandatory items
|
|
for _ in range(self.options.item_slot_increase):
|
|
level_up_item_pool.append("Item Slot Increase")
|
|
for _ in range(self.options.accessory_slot_increase):
|
|
level_up_item_pool.append("Accessory Slot Increase")
|
|
|
|
# Create other pool
|
|
for _ in range(self.options.strength_increase):
|
|
possible_level_up_item_pool.append("Strength Increase")
|
|
for _ in range(self.options.defense_increase):
|
|
possible_level_up_item_pool.append("Defense Increase")
|
|
for _ in range(self.options.hp_increase):
|
|
possible_level_up_item_pool.append("Max HP Increase")
|
|
for _ in range(self.options.mp_increase):
|
|
possible_level_up_item_pool.append("Max MP Increase")
|
|
for _ in range(self.options.ap_increase):
|
|
possible_level_up_item_pool.append("Max AP Increase")
|
|
|
|
# Fill remaining pool with items from other pool
|
|
self.random.shuffle(possible_level_up_item_pool)
|
|
level_up_item_pool = level_up_item_pool + possible_level_up_item_pool[:(99 - len(level_up_item_pool))]
|
|
|
|
level_up_locations = list(get_locations_by_type("Level Slot 1").keys())
|
|
self.random.shuffle(level_up_item_pool)
|
|
current_level_index_for_placing_stats = self.options.force_stats_on_levels.value - 2 # Level 2 is index 0, Level 3 is index 1, etc
|
|
if self.options.remote_items.current_key == "off" and self.options.force_stats_on_levels.value != 2:
|
|
logging.info(f"{self.player_name}'s value {self.options.force_stats_on_levels.value} for force_stats_on_levels was changed\n"
|
|
f"Set to 2 as remote_items if \"off\"")
|
|
self.options.force_stats_on_levels.value = 2
|
|
current_level_index_for_placing_stats = 0
|
|
while len(level_up_item_pool) > 0 and current_level_index_for_placing_stats < self.options.level_checks: # With all levels in location pool, 99 level ups so need to go index 0-98
|
|
self.get_location(level_up_locations[current_level_index_for_placing_stats]).place_locked_item(self.create_item(level_up_item_pool.pop()))
|
|
current_level_index_for_placing_stats += 1
|
|
|
|
|
|
|
|
# Calculate prefilled locations and items
|
|
exclude_items = ["Final Door Key", "Lucky Emblem"]
|
|
if not self.options.randomize_emblem_pieces:
|
|
exclude_items = exclude_items + ["Emblem Piece (Flame)", "Emblem Piece (Chest)", "Emblem Piece (Fountain)", "Emblem Piece (Statue)"]
|
|
|
|
total_locations = len(self.multiworld.get_unfilled_locations(self.player))
|
|
|
|
non_filler_item_categories = ["Key", "Magic", "Worlds", "Trinities", "Cups", "Summons", "Abilities", "Shared Abilities", "Keyblades", "Accessory", "Weapons", "Puppies"]
|
|
if self.options.hundred_acre_wood:
|
|
non_filler_item_categories.append("Torn Pages")
|
|
for name, data in item_table.items():
|
|
quantity = data.max_quantity
|
|
if data.category not in non_filler_item_categories:
|
|
continue
|
|
if name in starting_worlds or name in starting_tools or name in starting_party_member_accessories:
|
|
continue
|
|
if self.options.stacking_world_items and name in WORLD_KEY_ITEMS.keys() and name not in ("Crystal Trident", "Jack-In-The-Box"): # Handling these special cases separately
|
|
item_pool += [self.create_item(WORLD_KEY_ITEMS[name]) for _ in range(0, 1)]
|
|
elif self.options.halloween_town_key_item_bundle and name == "Jack-In-The-Box":
|
|
continue
|
|
elif name == "Puppy":
|
|
if self.options.randomize_puppies:
|
|
item_pool += [self.create_item(name) for _ in range(ceil(99/self.options.puppy_value.value))]
|
|
elif name == "Atlantica":
|
|
if self.options.atlantica:
|
|
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
|
elif name == "Mermaid Kick":
|
|
if self.options.atlantica and self.options.extra_shared_abilities:
|
|
item_pool += [self.create_item(name) for _ in range(0, 2)]
|
|
else:
|
|
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
|
elif name == "Crystal Trident":
|
|
if self.options.atlantica:
|
|
if self.options.stacking_world_items:
|
|
item_pool += [self.create_item(WORLD_KEY_ITEMS[name]) for _ in range(0, 1)]
|
|
else:
|
|
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
|
elif name == "High Jump":
|
|
if self.options.extra_shared_abilities:
|
|
item_pool += [self.create_item(name) for _ in range(0, 3)]
|
|
else:
|
|
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
|
elif name == "Progressive Glide":
|
|
if self.options.extra_shared_abilities:
|
|
item_pool += [self.create_item(name) for _ in range(0, 4)]
|
|
else:
|
|
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
|
elif name == "End of the World":
|
|
if self.options.end_of_the_world_unlock.current_key == "item":
|
|
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
|
elif name == "EXP Zero":
|
|
if self.options.exp_zero_in_pool:
|
|
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
|
elif name == "Postcard":
|
|
if self.options.randomize_postcards.current_key == "chests":
|
|
item_pool += [self.create_item(name) for _ in range(0, 3)]
|
|
if self.options.randomize_postcards.current_key == "all":
|
|
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
|
elif name == "Orichalcum":
|
|
item_pool += [self.create_item(name) for _ in range(0, self.options.orichalcum_in_pool.value)]
|
|
elif name == "Mythril":
|
|
item_pool += [self.create_item(name) for _ in range(0, self.options.mythril_in_pool.value)]
|
|
elif name == "Destiny Islands":
|
|
if self.options.destiny_islands:
|
|
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
|
elif name == "Raft Materials":
|
|
if self.options.destiny_islands:
|
|
item_pool += [self.create_item(name) for _ in range(0, self.options.materials_in_pool.value)]
|
|
elif name not in exclude_items:
|
|
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
|
|
|
for i in range(self.determine_lucky_emblems_in_pool()):
|
|
item_pool += [self.create_item("Lucky Emblem")]
|
|
|
|
while len(item_pool) < total_locations and len(level_up_item_pool) > 0:
|
|
item_pool += [self.create_item(level_up_item_pool.pop())]
|
|
|
|
# Fill any empty locations with filler items.
|
|
while len(item_pool) < total_locations:
|
|
item_pool.append(self.create_item(self.get_filler_item_name()))
|
|
|
|
self.multiworld.itempool += item_pool
|
|
|
|
def place_predetermined_items(self) -> None:
|
|
if self.options.final_rest_door_key.current_key not in ["puppies", "postcards", "lucky_emblems"]:
|
|
goal_dict = {
|
|
"sephiroth": "Olympus Coliseum Defeat Sephiroth Ansem's Report 12",
|
|
"unknown": "Hollow Bastion Defeat Unknown Ansem's Report 13",
|
|
"final_rest": "End of the World Final Rest Chest"
|
|
}
|
|
goal_location_name = goal_dict[self.options.final_rest_door_key.current_key]
|
|
elif self.options.final_rest_door_key.current_key == "postcards":
|
|
lpad_number = str(self.options.required_postcards).rjust(2, "0")
|
|
goal_location_name = "Traverse Town Mail Postcard " + lpad_number + " Event"
|
|
elif self.options.final_rest_door_key.current_key == "puppies":
|
|
required_puppies = self.options.required_puppies.value
|
|
goal_location_name = "Traverse Town Piano Room Return " + str(required_puppies) + " Puppies"
|
|
if required_puppies == 50 or required_puppies == 99:
|
|
goal_location_name = goal_location_name + " Reward 2"
|
|
if self.options.final_rest_door_key.current_key != "lucky_emblems":
|
|
self.get_location(goal_location_name).place_locked_item(self.create_item("Final Door Key"))
|
|
self.get_location("Final Ansem").place_locked_item(self.create_event("Victory"))
|
|
|
|
if not self.options.randomize_emblem_pieces:
|
|
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Flame)").place_locked_item(self.create_item("Emblem Piece (Flame)"))
|
|
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Statue)").place_locked_item(self.create_item("Emblem Piece (Statue)"))
|
|
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Fountain)").place_locked_item(self.create_item("Emblem Piece (Fountain)"))
|
|
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Chest)").place_locked_item(self.create_item("Emblem Piece (Chest)"))
|
|
if self.options.randomize_postcards != "all":
|
|
self.get_location("Traverse Town Item Shop Postcard").place_locked_item(self.create_item("Postcard"))
|
|
self.get_location("Traverse Town 1st District Safe Postcard").place_locked_item(self.create_item("Postcard"))
|
|
self.get_location("Traverse Town Gizmo Shop Postcard 1").place_locked_item(self.create_item("Postcard"))
|
|
self.get_location("Traverse Town Gizmo Shop Postcard 2").place_locked_item(self.create_item("Postcard"))
|
|
self.get_location("Traverse Town Item Workshop Postcard").place_locked_item(self.create_item("Postcard"))
|
|
self.get_location("Traverse Town 3rd District Balcony Postcard").place_locked_item(self.create_item("Postcard"))
|
|
self.get_location("Traverse Town Geppetto's House Postcard").place_locked_item(self.create_item("Postcard"))
|
|
if self.options.randomize_postcards.current_key == "vanilla":
|
|
self.get_location("Traverse Town 1st District Accessory Shop Roof Chest").place_locked_item(self.create_item("Postcard"))
|
|
self.get_location("Traverse Town 2nd District Boots and Shoes Awning Chest").place_locked_item(self.create_item("Postcard"))
|
|
self.get_location("Traverse Town 1st District Blue Trinity Balcony Chest").place_locked_item(self.create_item("Postcard"))
|
|
if not self.options.randomize_puppies:
|
|
if self.options.puppy_value.value != 3:
|
|
self.options.puppy_value.value = 3
|
|
logging.info(f"{self.player_name}'s value of {self.options.puppy_value.value} for puppy value was changed to 3 as Randomize Puppies is OFF")
|
|
for i, location in enumerate(VANILLA_PUPPY_LOCATIONS):
|
|
self.get_location(location).place_locked_item(self.create_item("Puppy"))
|
|
|
|
def get_filler_item_name(self) -> str:
|
|
weights = [data.weight for data in self.fillers.values()]
|
|
return self.random.choices([filler for filler in self.fillers.keys()], weights)[0]
|
|
|
|
def fill_slot_data(self) -> dict:
|
|
slot_data = {
|
|
"atlantica": bool(self.options.atlantica),
|
|
"auto_attack": bool(self.options.auto_attack),
|
|
"auto_save": bool(self.options.auto_save),
|
|
"bad_starting_weapons": bool(self.options.bad_starting_weapons),
|
|
"beep_hack": bool(self.options.beep_hack),
|
|
"consistent_finishers": bool(self.options.consistent_finishers),
|
|
"cups": str(self.options.cups.current_key),
|
|
"day_2_materials": int(self.options.day_2_materials.value),
|
|
"death_link": str(self.options.death_link.current_key),
|
|
"destiny_islands": bool(self.options.destiny_islands),
|
|
"donald_death_link": bool(self.options.donald_death_link),
|
|
"early_skip": bool(self.options.early_skip),
|
|
"end_of_the_world_unlock": str(self.options.end_of_the_world_unlock.current_key),
|
|
"exp_multiplier": int(self.options.exp_multiplier.value)/16,
|
|
"exp_zero_in_pool": bool(self.options.exp_zero_in_pool),
|
|
"extra_shared_abilities": bool(self.options.extra_shared_abilities),
|
|
"fast_camera": bool(self.options.fast_camera),
|
|
"faster_animations": bool(self.options.faster_animations),
|
|
"final_rest_door_key": str(self.options.final_rest_door_key.current_key),
|
|
"force_stats_on_levels": int(self.options.force_stats_on_levels.value),
|
|
"four_by_three": bool(self.options.four_by_three),
|
|
"goofy_death_link": bool(self.options.goofy_death_link),
|
|
"halloween_town_key_item_bundle": bool(self.options.halloween_town_key_item_bundle),
|
|
"homecoming_materials": int(self.options.homecoming_materials.value),
|
|
"hundred_acre_wood": bool(self.options.hundred_acre_wood),
|
|
"interact_in_battle": bool(self.options.interact_in_battle),
|
|
"jungle_slider": bool(self.options.jungle_slider),
|
|
"keyblades_unlock_chests": bool(self.options.keyblades_unlock_chests),
|
|
"level_checks": int(self.options.level_checks.value),
|
|
"logic_difficulty": str(self.options.logic_difficulty.current_key),
|
|
"materials_in_pool": int(self.options.materials_in_pool.value),
|
|
"max_ap_cost": int(self.options.max_ap_cost.value),
|
|
"min_ap_cost": int(self.options.min_ap_cost.value),
|
|
"mythril_in_pool": int(self.options.mythril_in_pool.value),
|
|
"mythril_price": int(self.options.mythril_price.value),
|
|
"one_hp": bool(self.options.one_hp),
|
|
"orichalcum_in_pool": int(self.options.orichalcum_in_pool.value),
|
|
"orichalcum_price": int(self.options.orichalcum_price.value),
|
|
"puppy_value": int(self.options.puppy_value.value),
|
|
"randomize_ap_costs": str(self.options.randomize_ap_costs.current_key),
|
|
"randomize_emblem_pieces": bool(self.options.exp_zero_in_pool),
|
|
"randomize_party_member_starting_accessories": bool(self.options.randomize_party_member_starting_accessories),
|
|
"randomize_postcards": str(self.options.randomize_postcards.current_key),
|
|
"randomize_puppies": str(self.options.randomize_puppies.current_key),
|
|
"remote_items": str(self.options.remote_items.current_key),
|
|
"remote_location_ids": self.get_remote_location_ids(),
|
|
"required_lucky_emblems_door": self.determine_lucky_emblems_required_to_open_final_rest_door(),
|
|
"required_lucky_emblems_eotw": self.determine_lucky_emblems_required_to_open_end_of_the_world(),
|
|
"required_postcards": int(self.options.required_postcards.value),
|
|
"required_puppies": int(self.options.required_puppies.value),
|
|
"seed": self.multiworld.seed_name,
|
|
"shorten_go_mode": bool(self.options.shorten_go_mode),
|
|
"slot_2_level_checks": int(self.options.slot_2_level_checks.value),
|
|
"stacking_world_items": bool(self.options.stacking_world_items),
|
|
"starting_items": [item.code for item in self.multiworld.precollected_items[self.player]],
|
|
"starting_tools": bool(self.options.starting_tools),
|
|
"super_bosses": bool(self.options.super_bosses),
|
|
"synthesis_item_name_byte_arrays": self.get_synthesis_item_name_byte_arrays(),
|
|
"unlock_0_volume": bool(self.options.unlock_0_volume),
|
|
"unskippable": bool(self.options.unskippable),
|
|
"warp_anywhere": bool(self.options.warp_anywhere)
|
|
}
|
|
return slot_data
|
|
|
|
def create_item(self, name: str) -> KH1Item:
|
|
data = item_table[name]
|
|
return KH1Item(name, data.classification, data.code, self.player)
|
|
|
|
def create_event(self, name: str) -> KH1Item:
|
|
data = event_item_table[name]
|
|
return KH1Item(name, data.classification, data.code, self.player)
|
|
|
|
def set_rules(self):
|
|
set_rules(self)
|
|
|
|
def create_regions(self):
|
|
create_regions(self)
|
|
|
|
def connect_entrances(self):
|
|
connect_entrances(self)
|
|
|
|
def generate_output(self, output_directory: str):
|
|
"""
|
|
Generates the json file for use with mod generator.
|
|
"""
|
|
generate_json(self, output_directory)
|
|
|
|
def generate_early(self):
|
|
self.determine_level_checks()
|
|
|
|
value_names = ["Lucky Emblems to Open End of the World", "Lucky Emblems to Open Final Rest Door", "Lucky Emblems in Pool"]
|
|
initial_lucky_emblem_settings = [self.options.required_lucky_emblems_eotw.value, self.options.required_lucky_emblems_door.value, self.options.lucky_emblems_in_pool.value]
|
|
self.change_numbers_of_lucky_emblems_to_consider()
|
|
new_lucky_emblem_settings = [self.options.required_lucky_emblems_eotw.value, self.options.required_lucky_emblems_door.value, self.options.lucky_emblems_in_pool.value]
|
|
for i in range(3):
|
|
if initial_lucky_emblem_settings[i] != new_lucky_emblem_settings[i]:
|
|
logging.info(f"{self.player_name}'s value {initial_lucky_emblem_settings[i]} for \"{value_names[i]}\" was invalid\n"
|
|
f"Setting \"{value_names[i]}\" value to {new_lucky_emblem_settings[i]}")
|
|
|
|
value_names = ["Day 2 Materials", "Homecoming Materials", "Materials in Pool"]
|
|
initial_materials_settings = [self.options.day_2_materials.value, self.options.homecoming_materials.value, self.options.materials_in_pool.value]
|
|
self.change_numbers_of_materials_to_consider()
|
|
new_materials_settings = [self.options.day_2_materials.value, self.options.homecoming_materials.value, self.options.materials_in_pool.value]
|
|
for i in range(3):
|
|
if initial_materials_settings[i] != new_materials_settings[i]:
|
|
logging.info(f"{self.player_name}'s value {initial_materials_settings[i]} for \"{value_names[i]}\" was invalid\n"
|
|
f"Setting \"{value_names[i]}\" value to {new_materials_settings[i]}")
|
|
|
|
if self.options.stacking_world_items.value and not self.options.halloween_town_key_item_bundle.value:
|
|
logging.info(f"{self.player_name}'s value {self.options.halloween_town_key_item_bundle.value} for Halloween Town Key Item Bundle must be TRUE when Stacking World Items is on. Setting to TRUE")
|
|
self.options.halloween_town_key_item_bundle.value = True
|
|
|
|
def change_numbers_of_lucky_emblems_to_consider(self) -> None:
|
|
if self.options.end_of_the_world_unlock == "lucky_emblems" and self.options.final_rest_door_key == "lucky_emblems":
|
|
self.options.required_lucky_emblems_eotw.value, self.options.required_lucky_emblems_door.value, self.options.lucky_emblems_in_pool.value = sorted(
|
|
[self.options.required_lucky_emblems_eotw.value, self.options.required_lucky_emblems_door.value, self.options.lucky_emblems_in_pool.value])
|
|
|
|
elif self.options.end_of_the_world_unlock == "lucky_emblems":
|
|
self.options.required_lucky_emblems_eotw.value, self.options.lucky_emblems_in_pool.value = sorted(
|
|
[self.options.required_lucky_emblems_eotw.value, self.options.lucky_emblems_in_pool.value])
|
|
|
|
elif self.options.final_rest_door_key == "lucky_emblems":
|
|
self.options.required_lucky_emblems_door.value, self.options.lucky_emblems_in_pool.value = sorted(
|
|
[self.options.required_lucky_emblems_door.value, self.options.lucky_emblems_in_pool.value])
|
|
|
|
def determine_lucky_emblems_in_pool(self) -> int:
|
|
if self.options.end_of_the_world_unlock == "lucky_emblems" or self.options.final_rest_door_key == "lucky_emblems":
|
|
return self.options.lucky_emblems_in_pool.value
|
|
return 0
|
|
|
|
def determine_lucky_emblems_required_to_open_end_of_the_world(self) -> int:
|
|
if self.options.end_of_the_world_unlock == "lucky_emblems":
|
|
return self.options.required_lucky_emblems_eotw.value
|
|
return -1
|
|
|
|
def determine_lucky_emblems_required_to_open_final_rest_door(self) -> int:
|
|
if self.options.final_rest_door_key == "lucky_emblems":
|
|
return self.options.required_lucky_emblems_door.value
|
|
return -1
|
|
|
|
def change_numbers_of_materials_to_consider(self) -> None:
|
|
if self.options.destiny_islands:
|
|
self.options.day_2_materials.value, self.options.homecoming_materials.value, self.options.materials_in_pool.value = sorted(
|
|
[self.options.day_2_materials.value, self.options.homecoming_materials.value, self.options.materials_in_pool.value])
|
|
|
|
def get_remote_location_ids(self):
|
|
remote_location_ids = []
|
|
for location in self.multiworld.get_filled_locations(self.player):
|
|
if location.name != "Final Ansem":
|
|
location_data = location_table[location.name]
|
|
if self.options.remote_items.current_key == "full":
|
|
if location_data.type != "Starting Accessory":
|
|
remote_location_ids.append(location_data.code)
|
|
elif self.player == location.item.player and location.item.name != "Victory":
|
|
item_data = item_table[location.item.name]
|
|
if location_data.type == "Chest":
|
|
if item_data.type in ["Stats"]:
|
|
remote_location_ids.append(location_data.code)
|
|
if location_data.type == "Reward":
|
|
if item_data.type in ["Stats"]:
|
|
remote_location_ids.append(location_data.code)
|
|
if location_data.type == "Static":
|
|
if item_data.type not in ["Item"]:
|
|
remote_location_ids.append(location_data.code)
|
|
if location_data.type == "Level Slot 1":
|
|
if item_data.category not in ["Level Up", "Limited Level Up"]:
|
|
remote_location_ids.append(location_data.code)
|
|
if location_data.type == "Level Slot 2":
|
|
if item_data.category not in ["Level Up", "Limited Level Up", "Abilities"]:
|
|
remote_location_ids.append(location_data.code)
|
|
if location_data.type == "Synth":
|
|
if item_data.type not in ["Item"]:
|
|
remote_location_ids.append(location_data.code)
|
|
if location_data.type == "Prize":
|
|
if item_data.type not in ["Item"]:
|
|
remote_location_ids.append(location_data.code)
|
|
return remote_location_ids
|
|
|
|
def get_slot_2_levels(self):
|
|
if self.slot_2_levels is None:
|
|
self.slot_2_levels = []
|
|
if self.options.max_level_for_slot_2_level_checks - 1 > self.options.level_checks.value:
|
|
logging.info(f"{self.player_name}'s value of {self.options.max_level_for_slot_2_level_checks.value} for max level for slot 2 level checks is invalid as it exceeds their value of {self.options.level_checks.value} for Level Checks\n"
|
|
f"Setting max level for slot 2 level checks's value to {self.options.level_checks.value + 1}")
|
|
self.options.max_level_for_slot_2_level_checks.value = self.options.level_checks.value + 1
|
|
if self.options.slot_2_level_checks.value > self.options.level_checks.value:
|
|
logging.info(f"{self.player_name}'s value of {self.options.slot_2_level_checks.value} for slot 2 level checks is invalid as it exceeds their value of {self.options.level_checks.value} for Level Checks\n"
|
|
f"Setting slot 2 level check's value to {self.options.level_checks.value}")
|
|
self.options.slot_2_level_checks.value = self.options.level_checks.value
|
|
if self.options.slot_2_level_checks > self.options.max_level_for_slot_2_level_checks - 1:
|
|
logging.info(f"{self.player_name}'s value of {self.options.slot_2_level_checks.value} for slot 2 level checks is invalid as it exceeds their value of {self.options.max_level_for_slot_2_level_checks.value} for Max Level for Slot 2 Level Checks\n"
|
|
f"Setting slot 2 level check's value to {self.options.max_level_for_slot_2_level_checks.value - 1}")
|
|
self.options.slot_2_level_checks.value = self.options.max_level_for_slot_2_level_checks.value - 1
|
|
# Range is exclusive of the top, so if max_level_for_slot_2_level_checks is 2 then the top end of the range needs to be 3 as the only level it can choose is 2.
|
|
self.slot_2_levels = self.random.sample(range(2,self.options.max_level_for_slot_2_level_checks.value + 1), self.options.slot_2_level_checks.value)
|
|
return self.slot_2_levels
|
|
|
|
def get_keyblade_stats(self):
|
|
# Create keyblade stat array from vanilla
|
|
keyblade_stats = [x.copy() for x in VANILLA_KEYBLADE_STATS]
|
|
# Handle shuffling keyblade stats
|
|
if self.options.keyblade_stats != "vanilla":
|
|
if self.options.keyblade_stats == "randomize":
|
|
# Fix any minimum and max values from settings
|
|
min_str_bonus = min(self.options.keyblade_min_str.value, self.options.keyblade_max_str.value)
|
|
max_str_bonus = max(self.options.keyblade_min_str.value, self.options.keyblade_max_str.value)
|
|
self.options.keyblade_min_str.value = min_str_bonus
|
|
self.options.keyblade_max_str.value = max_str_bonus
|
|
min_crit_rate = min(self.options.keyblade_min_crit_rate.value, self.options.keyblade_max_crit_rate.value)
|
|
max_crit_rate = max(self.options.keyblade_min_crit_rate.value, self.options.keyblade_max_crit_rate.value)
|
|
self.options.keyblade_min_crit_rate.value = min_crit_rate
|
|
self.options.keyblade_max_crit_rate.value = max_crit_rate
|
|
min_crit_str = min(self.options.keyblade_min_crit_str.value, self.options.keyblade_max_crit_str.value)
|
|
max_crit_str = max(self.options.keyblade_min_crit_str.value, self.options.keyblade_max_crit_str.value)
|
|
self.options.keyblade_min_crit_str.value = min_crit_str
|
|
self.options.keyblade_max_crit_str.value = max_crit_str
|
|
min_recoil = min(self.options.keyblade_min_recoil.value, self.options.keyblade_max_recoil.value)
|
|
max_recoil = max(self.options.keyblade_min_recoil.value, self.options.keyblade_max_recoil.value)
|
|
self.options.keyblade_min_recoil.value = min_recoil
|
|
self.options.keyblade_max_recoil.value = max_recoil
|
|
min_mp_bonus = min(self.options.keyblade_min_mp.value, self.options.keyblade_max_mp.value)
|
|
max_mp_bonus = max(self.options.keyblade_min_mp.value, self.options.keyblade_max_mp.value)
|
|
self.options.keyblade_min_mp.value = min_mp_bonus
|
|
self.options.keyblade_max_mp.value = max_mp_bonus
|
|
if self.options.bad_starting_weapons:
|
|
starting_weapons = keyblade_stats[:4]
|
|
other_weapons = keyblade_stats[4:]
|
|
else:
|
|
starting_weapons = []
|
|
other_weapons = keyblade_stats
|
|
for keyblade in other_weapons:
|
|
keyblade["STR"] = self.random.randint(min_str_bonus, max_str_bonus)
|
|
keyblade["CRR"] = self.random.randint(min_crit_rate, max_crit_rate)
|
|
keyblade["CRB"] = self.random.randint(min_crit_str, max_crit_str)
|
|
keyblade["REC"] = self.random.randint(min_recoil, max_recoil)
|
|
keyblade["MP"] = self.random.randint(min_mp_bonus, max_mp_bonus)
|
|
keyblade_stats = starting_weapons + other_weapons
|
|
elif self.options.keyblade_stats == "shuffle":
|
|
if self.options.bad_starting_weapons:
|
|
starting_weapons = keyblade_stats[:4]
|
|
other_weapons = keyblade_stats[4:]
|
|
self.random.shuffle(other_weapons)
|
|
keyblade_stats = starting_weapons + other_weapons
|
|
else:
|
|
self.random.shuffle(keyblade_stats)
|
|
return keyblade_stats
|
|
|
|
def determine_level_checks(self):
|
|
# Handle if remote_items is off and level_checks > number of stats items
|
|
total_level_up_items = min(99,
|
|
self.options.strength_increase.value +\
|
|
self.options.defense_increase.value +\
|
|
self.options.hp_increase.value +\
|
|
self.options.mp_increase.value +\
|
|
self.options.ap_increase.value +\
|
|
self.options.accessory_slot_increase.value +\
|
|
self.options.item_slot_increase.value)
|
|
if self.options.level_checks.value > total_level_up_items and self.options.remote_items.current_key == "off":
|
|
logging.info(f"{self.player_name}'s value {self.options.level_checks.value} for level_checks was changed.\n"
|
|
f"This value cannot be more than the number of stat items in the pool when \"remote_items\" is \"off\".\n"
|
|
f"Set to be equal to number of stat items in pool, {total_level_up_items}.")
|
|
self.options.level_checks.value = total_level_up_items
|
|
|
|
def get_synthesis_item_name_byte_arrays(self):
|
|
# Get synth item names to show in synthesis menu
|
|
synthesis_byte_arrays = []
|
|
for location in self.multiworld.get_filled_locations(self.player):
|
|
if location.name != "Final Ansem":
|
|
location_data = location_table[location.name]
|
|
if location_data.type == "Synth":
|
|
item_name = re.sub('[^A-Za-z0-9 ]+', '',str(location.item.name.replace("Progressive", "Prog")))[:14]
|
|
byte_array = []
|
|
for character in item_name:
|
|
byte_array.append(CHAR_TO_KH[character])
|
|
synthesis_byte_arrays.append(byte_array)
|
|
return synthesis_byte_arrays
|
|
|
|
def get_starting_accessory_locations(self):
|
|
if self.starting_accessory_locations is None:
|
|
if self.options.randomize_party_member_starting_accessories:
|
|
self.starting_accessory_locations = list(get_locations_by_type("Starting Accessory").keys())
|
|
if not self.options.atlantica:
|
|
self.starting_accessory_locations.remove("Ariel Starting Accessory 1")
|
|
self.starting_accessory_locations.remove("Ariel Starting Accessory 2")
|
|
self.starting_accessory_locations.remove("Ariel Starting Accessory 3")
|
|
self.starting_accessory_locations = self.random.sample(self.starting_accessory_locations, 10)
|
|
else:
|
|
self.starting_accessory_locations = []
|
|
return self.starting_accessory_locations
|
|
|
|
def get_starting_accessories(self):
|
|
if self.starting_accessories is None:
|
|
if self.options.randomize_party_member_starting_accessories:
|
|
self.starting_accessories = list(get_items_by_category("Accessory").keys())
|
|
self.starting_accessories = self.random.sample(self.starting_accessories, 10)
|
|
else:
|
|
self.starting_accessories = []
|
|
return self.starting_accessories
|
|
|
|
def get_ap_costs(self):
|
|
if self.ap_costs is None:
|
|
ap_costs = VANILLA_ABILITY_AP_COSTS.copy()
|
|
if self.options.randomize_ap_costs.current_key == "shuffle":
|
|
possible_costs = []
|
|
for ap_cost in VANILLA_ABILITY_AP_COSTS:
|
|
if ap_cost["Randomize"]:
|
|
possible_costs.append(ap_cost["AP Cost"])
|
|
self.random.shuffle(possible_costs)
|
|
for ap_cost in ap_costs:
|
|
if ap_cost["Randomize"]:
|
|
ap_cost["AP Cost"] = possible_costs.pop(0)
|
|
elif self.options.randomize_ap_costs.current_key == "randomize":
|
|
for ap_cost in ap_costs:
|
|
if ap_cost["Randomize"]:
|
|
ap_cost["AP Cost"] = self.random.randint(self.options.min_ap_cost.value, self.options.max_ap_cost.value)
|
|
elif self.options.randomize_ap_costs.current_key == "distribute":
|
|
total_ap_value = 0
|
|
for ap_cost in VANILLA_ABILITY_AP_COSTS:
|
|
if ap_cost["Randomize"]:
|
|
total_ap_value = total_ap_value + ap_cost["AP Cost"]
|
|
for ap_cost in ap_costs:
|
|
if ap_cost["Randomize"]:
|
|
total_ap_value = total_ap_value - self.options.min_ap_cost.value
|
|
ap_cost["AP Cost"] = self.options.min_ap_cost.value
|
|
while total_ap_value > 0:
|
|
ap_cost = self.random.choice(ap_costs)
|
|
if ap_cost["Randomize"]:
|
|
if ap_cost["AP Cost"] < self.options.max_ap_cost.value:
|
|
amount_to_add = self.random.randint(1, min(self.options.max_ap_cost.value - ap_cost["AP Cost"], total_ap_value))
|
|
ap_cost["AP Cost"] = ap_cost["AP Cost"] + amount_to_add
|
|
total_ap_value = total_ap_value - amount_to_add
|
|
self.ap_costs = ap_costs
|
|
return self.ap_costs
|