Kingdom Hearts: Adding a bunch of new features (#5078)

* 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>
This commit is contained in:
gaithern
2025-09-10 16:49:32 -05:00
committed by GitHub
parent 78b529fc23
commit 1322ce866e
15 changed files with 4043 additions and 2775 deletions

View File

@@ -1,23 +1,29 @@
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_category, location_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
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))
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):
@@ -54,6 +60,19 @@ class KH1World(World):
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()
@@ -63,12 +82,29 @@ class KH1World(World):
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 = []
@@ -94,19 +130,26 @@ class KH1World(World):
# 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[:(100 - len(level_up_item_pool))]
level_up_locations = list(get_locations_by_category("Levels").keys())
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_for_placing_stats = self.options.force_stats_on_levels.value
while len(level_up_item_pool) > 0 and current_level_for_placing_stats <= self.options.level_checks:
self.get_location(level_up_locations[current_level_for_placing_stats - 1]).place_locked_item(self.create_item(level_up_item_pool.pop()))
current_level_for_placing_stats += 1
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
prefilled_items = []
if self.options.vanilla_emblem_pieces:
prefilled_items = prefilled_items + ["Emblem Piece (Flame)", "Emblem Piece (Chest)", "Emblem Piece (Fountain)", "Emblem Piece (Statue)"]
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))
@@ -117,27 +160,29 @@ class KH1World(World):
quantity = data.max_quantity
if data.category not in non_filler_item_categories:
continue
if name in starting_worlds:
if name in starting_worlds or name in starting_tools or name in starting_party_member_accessories:
continue
if data.category == "Puppies":
if self.options.puppies == "triplets" and "-" in name:
item_pool += [self.create_item(name) for _ in range(quantity)]
if self.options.puppies == "individual" and "Puppy" in name:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
if self.options.puppies == "full" and name == "All Puppies":
item_pool += [self.create_item(name) for _ in range(0, quantity)]
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:
if 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)]
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:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
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)]
@@ -154,11 +199,26 @@ class KH1World(World):
elif name == "EXP Zero":
if self.options.exp_zero_in_pool:
item_pool += [self.create_item(name) for _ in range(0, quantity)]
elif name not in prefilled_items:
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_reports_in_pool()):
item_pool += [self.create_item("Ansem's Report " + str(i+1))]
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())]
@@ -170,63 +230,117 @@ class KH1World(World):
self.multiworld.itempool += item_pool
def place_predetermined_items(self) -> None:
goal_dict = {
"sephiroth": "Olympus Coliseum Defeat Sephiroth Ansem's Report 12",
"unknown": "Hollow Bastion Defeat Unknown Ansem's Report 13",
"postcards": "Traverse Town Mail Postcard 10 Event",
"final_ansem": "Final Ansem",
"puppies": "Traverse Town Piano Room Return 99 Puppies Reward 2",
"final_rest": "End of the World Final Rest Chest"
}
self.get_location(goal_dict[self.options.goal.current_key]).place_locked_item(self.create_item("Victory"))
if self.options.vanilla_emblem_pieces:
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 = {"xpmult": int(self.options.exp_multiplier)/16,
"required_reports_eotw": self.determine_reports_required_to_open_end_of_the_world(),
"required_reports_door": self.determine_reports_required_to_open_final_rest_door(),
"door": self.options.final_rest_door.current_key,
"seed": self.multiworld.seed_name,
"advanced_logic": bool(self.options.advanced_logic),
"hundred_acre_wood": bool(self.options.hundred_acre_wood),
slot_data = {
"atlantica": bool(self.options.atlantica),
"goal": str(self.options.goal.current_key)}
if self.options.randomize_keyblade_stats:
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_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
slot_data["keyblade_stats"] = ""
for i in range(22):
if i < 4 and self.options.bad_starting_weapons:
slot_data["keyblade_stats"] = slot_data["keyblade_stats"] + "1,0,"
else:
str_bonus = int(self.random.randint(min_str_bonus, max_str_bonus))
mp_bonus = int(self.random.randint(min_mp_bonus, max_mp_bonus))
slot_data["keyblade_stats"] = slot_data["keyblade_stats"] + str(str_bonus) + "," + str(mp_bonus) + ","
slot_data["keyblade_stats"] = slot_data["keyblade_stats"][:-1]
if self.options.donald_death_link:
slot_data["donalddl"] = ""
if self.options.goofy_death_link:
slot_data["goofydl"] = ""
if self.options.keyblades_unlock_chests:
slot_data["chestslocked"] = ""
else:
slot_data["chestsunlocked"] = ""
if self.options.interact_in_battle:
slot_data["interactinbattle"] = ""
"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:
@@ -241,45 +355,260 @@ class KH1World(World):
set_rules(self)
def create_regions(self):
create_regions(self.multiworld, self.player, self.options)
create_regions(self)
def connect_entrances(self):
connect_entrances(self.multiworld, self.player)
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):
value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"]
initial_report_settings = [self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value]
self.change_numbers_of_reports_to_consider()
new_report_settings = [self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value]
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_report_settings[i] != new_report_settings[i]:
logging.info(f"{self.player_name}'s value {initial_report_settings[i]} for \"{value_names[i]}\" was invalid\n"
f"Setting \"{value_names[i]}\" value to {new_report_settings[i]}")
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_reports_to_consider(self) -> None:
if self.options.end_of_the_world_unlock == "reports" and self.options.final_rest_door == "reports":
self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value = sorted(
[self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value])
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 == "reports":
self.options.required_reports_eotw.value, self.options.reports_in_pool.value = sorted(
[self.options.required_reports_eotw.value, self.options.reports_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 == "reports":
self.options.required_reports_door.value, self.options.reports_in_pool.value = sorted(
[self.options.required_reports_door.value, self.options.reports_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_reports_in_pool(self) -> int:
if self.options.end_of_the_world_unlock == "reports" or self.options.final_rest_door == "reports":
return self.options.reports_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_reports_required_to_open_end_of_the_world(self) -> int:
if self.options.end_of_the_world_unlock == "reports":
return self.options.required_reports_eotw.value
return 14
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_reports_required_to_open_final_rest_door(self) -> int:
if self.options.final_rest_door == "reports":
return self.options.required_reports_door.value
return 14
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