forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
794 lines
37 KiB
Python
794 lines
37 KiB
Python
import json
|
|
import pkgutil
|
|
from BaseClasses import Entrance, Location, MultiWorld, Region
|
|
from typing import TYPE_CHECKING, Any, List, NamedTuple
|
|
from operator import attrgetter
|
|
|
|
from .Options import GaribLogic, DifficultyLogic
|
|
from .Rules import move_lookup, switches_to_event_items, access_methods_to_rules
|
|
from worlds.generic.Rules import add_rule, set_rule
|
|
|
|
if TYPE_CHECKING:
|
|
from . import GloverWorld
|
|
else:
|
|
GloverWorld = object
|
|
|
|
|
|
#Level name, followed by the region indexes of each checkpoint
|
|
levels_in_order = [
|
|
["AtlH", 0],
|
|
["Atl1", 0, 0],
|
|
["Atl2", 0, 4, 8],
|
|
["Atl3", 0, 3, 10],
|
|
["Atl!", 0],
|
|
["Atl?", 0],
|
|
["CrnH", 0],
|
|
["Crn1", 0, 2, 5, 9],
|
|
["Crn2", 0, 3, 6, 13, 15],
|
|
["Crn3", 0, 5, 9, 13],
|
|
["Crn!", 0],
|
|
["Crn?", 0],
|
|
["PrtH", 0],
|
|
["Prt1", 0, 11, 15],
|
|
["Prt2", 0, 3, 5],
|
|
["Prt3", 0, 5, 15, 19],
|
|
["Prt!", 0],
|
|
["Prt?", 0],
|
|
["PhtH", 0],
|
|
["Pht1", 0, 9, 12],
|
|
["Pht2", 0, 13, 18, 19],
|
|
["Pht3", 0, 5, 8, 11],
|
|
["Pht!", 0],
|
|
["Pht?", 0],
|
|
["FoFH", 0],
|
|
["FoF1", 0, 6, 13],
|
|
["FoF2", 0, 2, 7],
|
|
["FoF3", 0, 3, 4, 11, 17],
|
|
["FoF!", 0],
|
|
["FoF?", 0],
|
|
["OoWH", 0],
|
|
["OoW1", 0, 9],
|
|
["OoW2", 0],
|
|
["OoW3", 0, 2, 12, 17],
|
|
["OoW!", 0],
|
|
["OoW?", 0],
|
|
["Hubworld", 0],
|
|
["Castle Cave", 0],
|
|
["Training", 0]
|
|
]
|
|
|
|
file = pkgutil.get_data(__name__, "Logic.json").decode("utf-8")
|
|
|
|
#List of worlds, dictionary of levels, dictionary of locations and regions in those levels
|
|
#If it's a list, it's a location. Contains first entry AP_ID/ID, and all other entries methods
|
|
#If it's a dictionary, it's a region. B for Ball, D for No Ball, I for Local ID
|
|
#B/D in region has AP_ID/IDs, followed by methods as per Locations
|
|
logic_data : list[
|
|
dict[str,
|
|
dict[str,
|
|
dict[str,
|
|
list[
|
|
dict[str, any]] |
|
|
int] |
|
|
list[
|
|
dict[str, any]
|
|
]]]] = json.loads(file)
|
|
|
|
class AccessMethod(NamedTuple):
|
|
region_index : int
|
|
ball_in_region : bool
|
|
difficulty : int
|
|
required_items : list[str]
|
|
|
|
location_type_lookup = ["SWITCH", "GARIB", "LIFE", "CHECKPOINT", "POTION", "GOAL", "TIP", "LOADING_ZONE", "REGION", "MISC", "ENEMY", "INSECT"]
|
|
|
|
class LocationData(NamedTuple):
|
|
name : str
|
|
#The same index as in location type lookup
|
|
type : int
|
|
default_region : int
|
|
default_needs_ball : bool
|
|
ap_ids : List[int]
|
|
rom_ids : List[int]
|
|
methods : List[AccessMethod]
|
|
|
|
def remove_higher_difficulty_methods(self : GloverWorld, check_methods : list[AccessMethod]) -> list[AccessMethod]:
|
|
out_methods : list[AccessMethod] = []
|
|
#Find something of the correct difficulty based on logic methods
|
|
difficulty_cutoff : int = 3
|
|
match self.options.difficulty_logic:
|
|
case DifficultyLogic.option_intended:
|
|
difficulty_cutoff = 0
|
|
case DifficultyLogic.option_easy_tricks:
|
|
difficulty_cutoff = 1
|
|
case DifficultyLogic.option_hard_tricks:
|
|
difficulty_cutoff = 2
|
|
for each_method in check_methods:
|
|
#Difficulty Logic
|
|
if each_method.difficulty <= difficulty_cutoff:
|
|
out_methods.append(each_method)
|
|
return out_methods
|
|
|
|
|
|
def create_location_data(self : GloverWorld, check_name : str, check_info : list, level_name : str) -> list[LocationData]:
|
|
prefix = level_name + ": "
|
|
outputs : List[LocationData] = []
|
|
methods : List[AccessMethod] = []
|
|
for check_index in range(1, len(check_info)):
|
|
check_method = check_info[check_index]
|
|
new_method = create_access_method(self, check_method, level_name)
|
|
if new_method != None:
|
|
methods.append(new_method)
|
|
#Only create if there's a method for it
|
|
methods = remove_higher_difficulty_methods(self, methods)
|
|
if len(methods) == 0:
|
|
return outputs
|
|
ap_ids : list[int] = []
|
|
rom_ids : list[int] = []
|
|
for each_id in non_blank_ap_ids(check_info[0]["AP_IDS"]):
|
|
ap_ids.append(int(each_id, 0))
|
|
for each_id in non_blank_ap_ids(check_info[0]["IDS"]):
|
|
if each_id == "" or each_id == "N/A" or each_id == "?":
|
|
continue
|
|
if each_id.isdigit():
|
|
rom_ids.append(int(each_id, 0))
|
|
else:
|
|
rom_ids.append(-1)
|
|
|
|
#Is it an enemy?
|
|
enemy_with_garibs : bool = False
|
|
if check_info[0]["TYPE"] == 10:
|
|
#If so, are there garibs with this enemy?
|
|
enemy_with_garibs = len(ap_ids) > check_info[0]["COUNT"]
|
|
|
|
#When you have enemies with garibs
|
|
if enemy_with_garibs:
|
|
#Enemies get the first half of the array, garibs create the second
|
|
garib_ap_ids = []
|
|
garib_rom_ids = []
|
|
for _ in range(check_info[0]["COUNT"]):
|
|
garib_ap_ids.append(ap_ids.pop())
|
|
garib_rom_ids.append(rom_ids.pop())
|
|
garib_ap_ids.reverse()
|
|
garib_rom_ids.reverse()
|
|
garib_suffix = " Garib"
|
|
if check_info[0]["COUNT"] > 1:
|
|
garib_suffix += "s"
|
|
outputs.append(LocationData(prefix + check_name.removesuffix("s") + garib_suffix, 1, check_info[0]["REGION"], check_info[0]["NEEDS_BALL"], garib_ap_ids, garib_rom_ids, methods))
|
|
outputs.append(LocationData(prefix + check_name, 10, check_info[0]["REGION"], check_info[0]["NEEDS_BALL"], ap_ids, rom_ids, methods))
|
|
#All other checks
|
|
else:
|
|
outputs.append(LocationData(prefix + check_name, check_info[0]["TYPE"], check_info[0]["REGION"], check_info[0]["NEEDS_BALL"], ap_ids, rom_ids, methods))
|
|
return outputs
|
|
|
|
class RegionPair(NamedTuple):
|
|
name : str
|
|
base_id : int
|
|
ball_region_methods : list[AccessMethod]
|
|
no_ball_region_methods : list[AccessMethod]
|
|
ball_region_exists : bool
|
|
no_ball_region_exists : bool
|
|
|
|
def create_region_pair(self : GloverWorld, check_info : dict, check_name : str, level_name : str) -> RegionPair:
|
|
prefix = level_name + ": "
|
|
player : int = self.player
|
|
multiworld : MultiWorld = self.multiworld
|
|
region_name = prefix + check_name
|
|
ball_region_methods : list[AccessMethod] = []
|
|
no_ball_region_methods : list[AccessMethod] = []
|
|
base_id : int = check_info["I"]
|
|
ball_region = Region(region_name + " W/Ball", player, multiworld, level_name)
|
|
for index, check_pairing in enumerate(check_info["B"]):
|
|
#Skip the settings entry
|
|
if index == 0:
|
|
continue
|
|
#All other entries are methods
|
|
new_method = create_access_method(self, check_pairing, level_name)
|
|
if new_method != None:
|
|
ball_region_methods.append(new_method)
|
|
|
|
no_ball_region = Region(region_name, player, multiworld, level_name)
|
|
for index, check_pairing in enumerate(check_info["D"]):
|
|
#Skip the settings entry
|
|
if index == 0:
|
|
continue
|
|
new_method = create_access_method(self, check_pairing, level_name)
|
|
if new_method != None:
|
|
no_ball_region_methods.append(new_method)
|
|
|
|
#Ball regions that work
|
|
ball_region_exists : bool = False
|
|
ball_region_methods = remove_higher_difficulty_methods(self, ball_region_methods)
|
|
if len(ball_region_methods) > 0:
|
|
multiworld.regions.append(ball_region)
|
|
ball_region_exists = True
|
|
ball_region_methods = list(filter(lambda a, b = base_id, c = True: self_ref_region(a, b, c), ball_region_methods))
|
|
|
|
#No ball regions that exist
|
|
no_ball_region_exists : bool = False
|
|
no_ball_region_methods = remove_higher_difficulty_methods(self, no_ball_region_methods)
|
|
if len(no_ball_region_methods) > 0:
|
|
multiworld.regions.append(no_ball_region)
|
|
no_ball_region_exists = True
|
|
no_ball_region_methods = list(filter(lambda a, b = base_id, c = True: self_ref_region(a, b, c), no_ball_region_methods))
|
|
|
|
return RegionPair(region_name, base_id, ball_region_methods, no_ball_region_methods, ball_region_exists, no_ball_region_exists)
|
|
|
|
def self_ref_region(check_method : AccessMethod, base_region : int, base_ball : bool) -> bool:
|
|
return len(check_method.required_items) > 0 or check_method.region_index != base_region or check_method.ball_in_region != base_ball
|
|
|
|
def connect_region_pairs(self : GloverWorld, pairs : List[RegionPair]):
|
|
multiworld : MultiWorld = self.multiworld
|
|
player : int = self.player
|
|
for each_pair in pairs:
|
|
#If there's a ball region
|
|
if each_pair.ball_region_exists:
|
|
#Construct it
|
|
ball_region = multiworld.get_region(each_pair.name + " W/Ball", player)
|
|
#Gather methods
|
|
pair_ball_connections : dict[Region, list[AccessMethod]] = {}
|
|
for each_method in each_pair.ball_region_methods:
|
|
region_to_connect : Region = get_region_from_method(multiworld, player, pairs, each_method)
|
|
if region_to_connect == None:
|
|
continue
|
|
if not region_to_connect in pair_ball_connections.keys():
|
|
pair_ball_connections[region_to_connect] = [each_method]
|
|
#Create the rules using methods
|
|
for each_connection, methods in pair_ball_connections.items():
|
|
entrance : Entrance | Any = each_connection.connect(ball_region)
|
|
access_methods_to_rules(self, methods, entrance)
|
|
#If there's a no ball region
|
|
if each_pair.no_ball_region_exists:
|
|
#Construct it
|
|
pair_no_ball_connections : dict[Region, list[AccessMethod]] = {}
|
|
for each_method in each_pair.no_ball_region_methods:
|
|
region_to_connect : Region = get_region_from_method(multiworld, player, pairs, each_method)
|
|
if region_to_connect == None:
|
|
continue
|
|
if not region_to_connect in pair_no_ball_connections.keys():
|
|
pair_no_ball_connections[region_to_connect] = [each_method]
|
|
no_ball_region = multiworld.get_region(each_pair.name, player)
|
|
#Create the rules using methods
|
|
for each_connection, methods in pair_no_ball_connections.items():
|
|
entrance : Entrance | Any = each_connection.connect(no_ball_region)
|
|
access_methods_to_rules(self, methods, entrance)
|
|
|
|
#Ball region always leads to no ball region
|
|
if each_pair.ball_region_exists and each_pair.no_ball_region_exists:
|
|
ball_region.connect(no_ball_region)
|
|
|
|
class RegionLevel(NamedTuple):
|
|
name : str
|
|
#region : Region | None
|
|
starting_checkpoint : int
|
|
map_regions : List[RegionPair]
|
|
start_without_ball : bool
|
|
|
|
def create_region_level(self : GloverWorld, level_name : str, checkpoint_for_use : int | None, checkpoint_entry_pairs : list, map_regions : List[RegionPair]):
|
|
#By default, the region level leads to the first checkpoint's region
|
|
multiworld : MultiWorld = self.multiworld
|
|
player : int = self.player
|
|
|
|
default_checkpoint : int = 1
|
|
region : Region = Region(level_name, player, multiworld)
|
|
|
|
#Get the checkpoint from the core levels
|
|
if type(checkpoint_for_use) is int:
|
|
default_checkpoint = checkpoint_for_use
|
|
|
|
#See if you get the ball with the checkpoint
|
|
start_without_ball : bool = level_name[3:4] == "1" or level_name[:3] == "OoW"
|
|
|
|
#If you can get the ball in the level, you can use it to checkpoint warp with it to a later spot.
|
|
ball_regions : list[Region] = []
|
|
if start_without_ball:
|
|
ball_access_location = region.add_event(level_name + ": Ball", level_name + " Ball").location
|
|
for region_index, each_region_pair in enumerate(map_regions):
|
|
if each_region_pair.ball_region_exists:
|
|
ball_region = multiworld.get_region(each_region_pair.name + " W/Ball", player)
|
|
ball_regions.append(ball_region)
|
|
if region_index == 0:
|
|
set_rule(ball_access_location, lambda state, in_player = player, ball_reg = ball_region.name : state.can_reach_region(ball_reg, in_player))
|
|
else:
|
|
add_rule(ball_access_location, lambda state, in_player = player, ball_reg = ball_region.name : state.can_reach_region(ball_reg, in_player), "or")
|
|
|
|
#Assign entrances required by checkpoints here
|
|
for checkpoint_number in range(1, len(checkpoint_entry_pairs)):
|
|
#Create a checkpoint connection
|
|
checkpoint_name : str = level_name + " Checkpoint " + str(checkpoint_number)
|
|
for each_region_pair in map_regions:
|
|
#If the checkpoint leads to that region
|
|
if each_region_pair.base_id == checkpoint_entry_pairs[checkpoint_number]:
|
|
is_default = default_checkpoint == checkpoint_number
|
|
|
|
#If you normally spawn without the ball, the logic goes here
|
|
if start_without_ball:
|
|
if each_region_pair.no_ball_region_exists:
|
|
connecting_region : Region = multiworld.get_region(each_region_pair.name, player)
|
|
checkpoint_bridge(self, region, connecting_region, checkpoint_name, is_default)
|
|
|
|
#You can theoretically always spawn with the ball, if you can reach it
|
|
if each_region_pair.ball_region_exists:
|
|
connecting_region : Region = multiworld.get_region(each_region_pair.name + " W/Ball", player)
|
|
checkpoint_bridge(self, region, connecting_region, checkpoint_name, is_default, start_without_ball, ball_regions)
|
|
|
|
multiworld.regions.append(region)
|
|
return RegionLevel(level_name, default_checkpoint, map_regions, start_without_ball)
|
|
|
|
def checkpoint_bridge(self : GloverWorld, region : Region, connecting_region : Region, checkpoint_name : str, is_default : bool, requires_ball_access = False, ball_regions : list[Region] = []):
|
|
name_suffix : str = ""
|
|
if requires_ball_access:
|
|
name_suffix += " W/Ball"
|
|
entrance : Entrance | Any = region.connect(connecting_region, checkpoint_name + name_suffix)
|
|
requirements : list[str] = []
|
|
#The default checkpoint's always useable
|
|
if not is_default:
|
|
requirements.append(checkpoint_name)
|
|
if requires_ball_access:
|
|
ball_access = checkpoint_name.split(" ")[0]+ " Ball"
|
|
requirements.append(ball_access)
|
|
#Notify that getting the ball can let you use the checkpoint for the region sweeper
|
|
for each_ball_region in ball_regions:
|
|
self.multiworld.register_indirect_condition(each_ball_region, entrance)
|
|
if len(requirements) > 0:
|
|
#Other ones need the checkpoint item
|
|
set_rule(entrance, lambda state, reqs = requirements, in_player = self.player : state.has_all(reqs, in_player))
|
|
|
|
def create_access_method(self, info : dict, level_name : str) -> AccessMethod:#
|
|
required_moves : list = []
|
|
for each_key, each_result in info.items():
|
|
if each_key.startswith("mv"):
|
|
each_move = move_lookup[each_result]
|
|
required_moves.append(each_move)
|
|
if each_key.startswith("ck"):
|
|
required_moves.append(level_name + " " + each_result)
|
|
#Remove Power Ball methods
|
|
if "Power Ball" in required_moves and (not self.options.include_power_ball and self.starting_ball != "Power Ball"):
|
|
return None
|
|
#Combine Not Bowling and Not Crystal into Not Bowling Or Crystal
|
|
if "Not Bowling" in required_moves and "Not Crystal" in required_moves:
|
|
required_moves.remove("Not Bowling")
|
|
required_moves.remove("Not Crystal")
|
|
required_moves.append("Not Bowling or Crystal")
|
|
#Make sure there's a jump required for double jumps
|
|
if not "Jump" in required_moves and ("Double Jump" in required_moves):
|
|
required_moves.append("Jump")
|
|
#Here's the access method!
|
|
return AccessMethod(info["regionIndex"], info["ballRequirement"], info["trickDifficulty"], required_moves)
|
|
|
|
def assign_locations_to_regions(self : GloverWorld, region_level : RegionLevel, map_regions : List[RegionPair], location_data_list : List[LocationData], target_score : int):
|
|
player : int = self.player
|
|
multiworld : MultiWorld = self.multiworld
|
|
score_locations : list[Location] = []
|
|
for each_location_data in location_data_list:
|
|
#Should this location be generated?
|
|
ap_ids : list[int] = each_location_data.ap_ids
|
|
match each_location_data.type:
|
|
case 0:
|
|
#Switches
|
|
if not self.options.switches_checks:
|
|
ap_ids.clear()
|
|
case 3:
|
|
#Checkpoints don't give their starting location
|
|
if region_level.starting_checkpoint == int(each_location_data.name[-1]):
|
|
continue
|
|
#As Events
|
|
if not self.options.checkpoint_checks:
|
|
ap_ids.clear()
|
|
case 6:
|
|
#Tip hints
|
|
if self.options.mr_hints:
|
|
self.tip_locations[each_location_data.name] = ap_ids[0]
|
|
#Tips
|
|
if not self.options.mr_tip_checks:
|
|
continue
|
|
case 7:
|
|
#Loading Zones
|
|
if not self.options.bonus_levels:
|
|
#Bonus loading zones
|
|
if each_location_data.name.endswith("Entry Bonus"):
|
|
continue
|
|
#case 9:
|
|
#Misc
|
|
case 10:
|
|
#Enemysanity
|
|
if not self.options.enemysanity:
|
|
ap_ids.clear()
|
|
case 11:
|
|
#Insectity
|
|
if not self.options.insectity:
|
|
ap_ids.clear()
|
|
|
|
rules_applied : bool = False
|
|
|
|
#Is this a mono location?
|
|
location_regions : dict[str, list[AccessMethod]] = {}
|
|
for each_method in each_location_data.methods:
|
|
region_index = each_method.region_index
|
|
region_exists : bool = False
|
|
region_name : str
|
|
for each_pair in map_regions:
|
|
#If you're in the right region
|
|
if region_index == each_pair.base_id:
|
|
#That's a valid region name
|
|
region_name = each_pair.name
|
|
if each_method.ball_in_region:
|
|
region_name = region_name + " W/Ball"
|
|
region_exists = each_pair.ball_region_exists
|
|
else:
|
|
region_exists = each_pair.no_ball_region_exists
|
|
#If the region exists, assign it to the location regions
|
|
if region_exists:
|
|
if not region_name in location_regions:
|
|
location_regions[region_name] = []
|
|
location_regions[region_name].append(each_method)
|
|
#Depending on if it is or not
|
|
region_for_use : Region
|
|
if len(location_regions) > 1:
|
|
#Multi location construction creates a shared region to reach this location
|
|
region_for_use = Region(each_location_data.name + " Region", player, multiworld, region_level.name)
|
|
#Apply the rules here
|
|
rules_applied = True
|
|
for each_region_name, each_region_methods in location_regions.items():
|
|
#Only use the methods relevant to this entrance from the target region
|
|
entrance : Entrance | Any = multiworld.get_region(each_region_name, player).connect(region_for_use, each_location_data.name + " from " + each_region_name)
|
|
access_methods_to_rules(self, each_region_methods, entrance)
|
|
elif len(location_regions) == 1:
|
|
#Single location construction assigns to the specific element in the RegionPair
|
|
region_for_use = multiworld.get_region(list(location_regions.keys())[0], player)
|
|
else:
|
|
#If there are no methods, continue
|
|
continue
|
|
|
|
#Construct location data
|
|
if each_location_data.type == 1:
|
|
#Garibsanity
|
|
if self.options.garib_logic == GaribLogic.option_garibsanity:
|
|
for each_garib_index in range(len(each_location_data.ap_ids)):
|
|
each_garib = each_location_data.ap_ids[each_garib_index]
|
|
location : Location = Location(player, self.location_id_to_name[each_garib], each_garib, region_for_use)
|
|
region_for_use.locations.append(location)
|
|
if not rules_applied:
|
|
access_methods_to_rules(self, each_location_data.methods, location)
|
|
#Garibs give score
|
|
score_locations.append(location)
|
|
#Garib Groups
|
|
elif self.options.garib_logic == GaribLogic.option_garib_groups:
|
|
#Regular Locations
|
|
group_offset : int = each_location_data.ap_ids[0]
|
|
if len(ap_ids) > 1:
|
|
group_offset += 10000
|
|
location : Location = Location(player, each_location_data.name, group_offset, region_for_use)
|
|
region_for_use.locations.append(location)
|
|
if not rules_applied:
|
|
access_methods_to_rules(self, each_location_data.methods, location)
|
|
#Garibs give score
|
|
score_locations.append(location)
|
|
#All Garibs in Level
|
|
else:
|
|
#It's an event location, with an event item
|
|
location : Location = Location(player, each_location_data.name, None, region_for_use)
|
|
region_for_use.locations.append(location)
|
|
if not rules_applied:
|
|
access_methods_to_rules(self, each_location_data.methods, location)
|
|
#These are used to create star gate unlock logic
|
|
location.place_locked_item(self.create_event(each_location_data.name + " Reached"))
|
|
score_locations.append(location)
|
|
else:
|
|
#Regular Locations
|
|
address : int | None = None
|
|
#Single AP Item
|
|
if len(each_location_data.ap_ids) == 1:
|
|
address = each_location_data.ap_ids[0]
|
|
location : Location = Location(player, each_location_data.name, address, region_for_use)
|
|
region_for_use.locations.append(location)
|
|
if not rules_applied:
|
|
access_methods_to_rules(self, each_location_data.methods, location)
|
|
#Lives, Potions and Enemies give Score
|
|
if each_location_data.type in [2, 4, 10]:
|
|
score_locations.append(location)
|
|
score_locations.append(location)
|
|
#Multiple AP Items
|
|
elif len(each_location_data.ap_ids) > 1:
|
|
for each_index in range(len(each_location_data.ap_ids)):
|
|
each_ap_id = each_location_data.ap_ids[each_index]
|
|
location_name : str = each_location_data.name.removesuffix("s")
|
|
location_name += " " + str(each_index + 1)
|
|
location : Location = Location(player, location_name, each_ap_id, region_for_use)
|
|
region_for_use.locations.append(location)
|
|
if not rules_applied:
|
|
access_methods_to_rules(self, each_location_data.methods, location)
|
|
#Lives, Potions and Enemies give Score
|
|
if each_location_data.type in [2, 4, 10]:
|
|
score_locations.append(location)
|
|
score_locations.append(location)
|
|
else:
|
|
#Event Item
|
|
location : Location = Location(player, each_location_data.name, address, region_for_use)
|
|
region_for_use.locations.append(location)
|
|
if not rules_applied:
|
|
access_methods_to_rules(self, each_location_data.methods, location)
|
|
match each_location_data.type:
|
|
#Switches with no paired level event store their level event
|
|
#items as event items rather than AP items.
|
|
case 0:
|
|
new_event_item : str = switches_to_event_items[each_location_data.name]
|
|
location.place_locked_item(self.create_event(new_event_item))
|
|
#Enemies contribute to score even if disabled
|
|
case 10:
|
|
new_event_item : str = each_location_data.name.replace(":", "")
|
|
location.place_locked_item(self.create_event(new_event_item))
|
|
score_locations.append(location)
|
|
#Checkpoints & Level Warps
|
|
case _:
|
|
new_event_item : str = each_location_data.name.replace(":", "")
|
|
location.place_locked_item(self.create_event(new_event_item))
|
|
|
|
#Create score addresses anywhere that score is something you can get
|
|
#(AKA any non-boss level, excluding Space Boss)
|
|
world_index = 1 + self.world_from_string(region_level.name)
|
|
level_index = self.level_from_string(region_level.name)
|
|
if (world_index == 6 or level_index != 4) and level_index != 0:
|
|
#Score location at root
|
|
score_address = None
|
|
if target_score != 0:
|
|
score_address = ((world_index * 10) + level_index) * 100000
|
|
level_root = self.get_region(region_level.name)
|
|
score_location = Location(player, region_level.name + ": Score", score_address, level_root)
|
|
if score_address == None:
|
|
score_location.place_locked_item(self.create_event(region_level.name + " Score"))
|
|
level_root.locations.append(score_location)
|
|
for each_index, each_location in enumerate(score_locations):
|
|
if each_index == 0:
|
|
set_rule(score_location, lambda state, plr = player, scl = each_location: state.can_reach(scl, plr))
|
|
else:
|
|
add_rule(score_location, lambda state, plr = player, scl = each_location: state.can_reach(scl, plr), "or")
|
|
|
|
def get_region_from_method(multiworld : MultiWorld, player : int, region_pairs : List[RegionPair], method : AccessMethod) -> Region:
|
|
for each_pair in region_pairs:
|
|
lookup_name = each_pair.name
|
|
if method.ball_in_region:
|
|
lookup_name += " W/Ball"
|
|
if each_pair.base_id == method.region_index:
|
|
if method.ball_in_region:
|
|
if not each_pair.ball_region_exists:
|
|
return None
|
|
elif not each_pair.no_ball_region_exists:
|
|
return None
|
|
return multiworld.get_region(lookup_name, player)
|
|
raise IndexError(region_pairs[0].name.split(':')[0] + " method calls for region indexed " + str(method.region_index) + " that does not exist!")
|
|
|
|
def build_data(self : GloverWorld) -> List[RegionLevel]:
|
|
all_levels : List[RegionLevel] = []
|
|
|
|
#Build Logic
|
|
loc_con_index = 0
|
|
for world_index, each_world in enumerate(logic_data):
|
|
world_prefix : str = create_world_prefix(self.world_prefixes, world_index)
|
|
#Go over the Glover worlds
|
|
for level_index, level_key in enumerate(each_world):
|
|
if level_index == 5 and not self.options.bonus_levels:
|
|
loc_con_index += 1
|
|
continue
|
|
each_level = each_world[level_key]
|
|
checkpoint_entry_pairs : list = levels_in_order[loc_con_index]
|
|
loc_con_index += 1
|
|
level_prefix = create_level_prefix(self.level_prefixes, world_index, level_index)
|
|
level_name : str = world_prefix + level_prefix
|
|
prefix : str = level_name + ": "
|
|
map_regions : List[RegionPair] = []
|
|
location_data_list : List[LocationData] = []
|
|
|
|
#Bonus levels
|
|
if not (level_index == 5 and not self.options.bonus_levels):
|
|
for check_name in each_level:
|
|
check_info = each_level[check_name]
|
|
#Location
|
|
if type(check_info) is list:
|
|
location_data_list.extend(create_location_data(self, check_name, check_info, level_name))
|
|
#In-Level Region
|
|
if type(check_info) is dict:
|
|
new_region_pair = create_region_pair(self, check_info, check_name, level_name)
|
|
map_regions.append(new_region_pair)
|
|
|
|
#Sort the in-level regions
|
|
map_regions = sorted(map_regions, key=attrgetter('base_id'))
|
|
connect_region_pairs(self, map_regions)
|
|
|
|
#Create the level info attached to it
|
|
checkpoint_for_use : int | None = None
|
|
if level_index > 0 and level_index < 4 and world_index < 6:
|
|
checkpoint_for_use = self.spawn_checkpoint[(world_index * 3) + (level_index - 1)]
|
|
region_level : RegionLevel = create_region_level(self, level_name, checkpoint_for_use, checkpoint_entry_pairs, map_regions)
|
|
|
|
#Target Score
|
|
target_score = 0
|
|
if level_name in self.options.level_scores.value:
|
|
target_score = self.options.level_scores.value[level_name]
|
|
|
|
#Attach the locations to the regions
|
|
assign_locations_to_regions(self, region_level, map_regions, location_data_list, target_score)
|
|
|
|
#Aside from the wayrooms, the hubworld and the castle cave, levels have star marks
|
|
if (world_index < 6 and not level_index == 0) or level_index == 2:
|
|
create_star_mark(self, level_index, world_index, prefix, location_data_list)
|
|
|
|
#Append it to the level list
|
|
all_levels.append(region_level)
|
|
|
|
return all_levels
|
|
|
|
def create_star_mark(self, level_index : int, world_index : int, prefix : str, location_data_list : List[LocationData]):
|
|
player : int = self.player
|
|
|
|
#Does this location have a star mark item, or a random one?
|
|
star_mark_ap_id : int | None = None
|
|
if self.options.portalsanity:
|
|
#They contain a random item
|
|
star_mark_ap_id = 30000 + level_index + (world_index * 10)
|
|
secondary_condition : str
|
|
#Does it unlock via garibs?
|
|
level_has_garibs : bool = level_index != 4 and world_index < 6
|
|
if level_has_garibs:
|
|
#Yes
|
|
secondary_condition = "All Garibs"
|
|
else:
|
|
#No
|
|
secondary_condition = "Completion"
|
|
#Which means, don't give them a second item here
|
|
star_mark_ap_id = None
|
|
#Otherwise, they contain the star marks
|
|
menu_region : Region = self.multiworld.get_region("Menu", player)
|
|
star_mark_location = Location(player, prefix + secondary_condition, star_mark_ap_id, menu_region)
|
|
#Boss levels and the well just give you it if you reach the goal
|
|
if not level_has_garibs:
|
|
goal_location : Location
|
|
#Boss levels look for the location 'Boss'
|
|
if level_index == 4:
|
|
goal_location = self.multiworld.get_location(prefix + "Boss", player)
|
|
else:
|
|
#The tutorial well looks for the locaiton 'Goal'
|
|
goal_location = self.multiworld.get_location(prefix + "Goal", player)
|
|
set_rule(star_mark_location, lambda state, for_completion = goal_location: state.can_reach(for_completion, player))
|
|
#Level garibs means you take all garib methods from before
|
|
elif self.options.garib_logic == GaribLogic.option_level_garibs:
|
|
all_garibs_rule(self, star_mark_location, location_data_list)
|
|
menu_region.locations.append(star_mark_location)
|
|
|
|
def all_garibs_rule(self, star_mark_location : Location, location_data_list : List[LocationData]):
|
|
#If it's all garibs in level logic, logic construction looks diffrent
|
|
garib_location_names : List[str] = []
|
|
for each_garib_location in location_data_list:
|
|
#Skip non-garibs obviously
|
|
if not each_garib_location.type == 1:
|
|
continue
|
|
garib_location_names.append(each_garib_location.name + " Reached")
|
|
set_rule(star_mark_location, lambda state, required_garibs = garib_location_names: state.has_all(required_garibs, self.player))
|
|
|
|
|
|
def build_location_pairings(base_name : str, check_info : dict, ap_ids : list[str]) -> list[list]:
|
|
#Nothing at all
|
|
if len(ap_ids) == 0:
|
|
return []
|
|
#If the location data accounts for 1 location
|
|
if len(ap_ids) == 1:
|
|
return [[base_name, ap_ids[0]]]
|
|
#A single enemy
|
|
if len(ap_ids) == 2 and check_info["TYPE"] == 10:
|
|
if check_info["COUNT"] == 1:
|
|
return [[base_name, ap_ids[0]], [base_name + " Garib", ap_ids[1]]]
|
|
output : list[list] = []
|
|
#If the location accounts for multiple locations
|
|
for each_ap_id_index, each_ap_id in enumerate(ap_ids):
|
|
sublocation_name : str = base_name.removesuffix("s")
|
|
#Not enemies
|
|
if check_info["TYPE"] != 10:
|
|
sublocation_name += " " + str(each_ap_id_index + 1)
|
|
else:
|
|
#Enemies
|
|
if each_ap_id_index < check_info["COUNT"]:
|
|
sublocation_name += " " + str(each_ap_id_index + 1)
|
|
else:
|
|
sublocation_name += " Garib " + str(each_ap_id_index + 1 - check_info["COUNT"])
|
|
output.append([sublocation_name, each_ap_id])
|
|
return output
|
|
|
|
def non_blank_ap_ids(ap_ids : list[str]) -> list[str]:
|
|
return list(filter(lambda a: a != "", ap_ids))
|
|
|
|
def create_world_prefix(world_prefixes : list[str], index : int) -> str:
|
|
if index < 6:
|
|
return world_prefixes[index]
|
|
else:
|
|
return ""
|
|
|
|
def create_level_prefix(level_prefixes : list[str], world_index : int, level_index : int) -> str:
|
|
if world_index == 6:
|
|
match level_index:
|
|
case 0:
|
|
return "Hubworld"
|
|
case 1:
|
|
return "Castle Cave"
|
|
case 2:
|
|
return "Training"
|
|
else:
|
|
return level_prefixes[level_index]
|
|
|
|
def generate_location_information(world_prefixes : list[str], level_prefixes : list[str]) -> list:
|
|
location_name_to_id : dict = {}
|
|
#Setup the location types here
|
|
location_name_groups : dict = {}
|
|
for each_type in location_type_lookup:
|
|
if each_type == "LOADING_ZONE" or each_type == "REGION":
|
|
continue
|
|
group_name = each_type.title()
|
|
location_name_groups[group_name] = []
|
|
location_name_groups["Score"] = []
|
|
location_name_groups["Crystals"] = []
|
|
#Each World
|
|
for each_world_index, each_world in enumerate(logic_data):
|
|
world_prefix : str = create_world_prefix(world_prefixes, each_world_index)
|
|
for level_key, level_data in each_world.items():
|
|
level_prefix : str = create_level_prefix(level_prefixes, each_world_index, int(level_key[-1]))
|
|
level_name = world_prefix + level_prefix
|
|
prefix : str = level_name + ": "
|
|
if prefix == "Castle Cave: ":
|
|
continue
|
|
location_name_groups[level_name] = []
|
|
for location_name in level_data:
|
|
#Not regions
|
|
if type(level_data[location_name]) is dict:
|
|
continue
|
|
#Only locations remain
|
|
ap_ids : list[str] = level_data[location_name][0]["AP_IDS"]
|
|
ap_ids = non_blank_ap_ids(ap_ids)
|
|
for each_pairing in build_location_pairings(prefix + location_name, level_data[location_name][0], ap_ids):
|
|
#Name to ID
|
|
location_name_to_id[each_pairing[0]] = int(each_pairing[1], 0)
|
|
#Name Groups
|
|
location_name_groups[level_name].append(each_pairing[0])
|
|
location_type : str = location_type_lookup[level_data[location_name][0]["TYPE"]]
|
|
location_name_groups[location_type.title()].append(each_pairing[0])
|
|
#Garib Groups
|
|
if level_data[location_name][0]["TYPE"] == 1 and len(ap_ids) > 1:
|
|
group_id : int = int(ap_ids[0], 0) + 10000
|
|
location_name_to_id[prefix + location_name] = group_id
|
|
#Enemy Garib Groups
|
|
if level_data[location_name][0]["TYPE"] == 10:
|
|
enemy_count = level_data[location_name][0]["COUNT"]
|
|
if enemy_count < len(ap_ids) and enemy_count > 1:
|
|
group_id : int = int(ap_ids[enemy_count], 0) + 10000
|
|
location_name_to_id[prefix + location_name.removesuffix("s") + " Garibs"] = group_id
|
|
#Levels with garibs in them
|
|
if each_world_index < 6:
|
|
match level_key:
|
|
case "l1":
|
|
location_name_to_id[prefix + "All Garibs"] = 30000 + (each_world_index * 10) + 1
|
|
case "l2":
|
|
location_name_to_id[prefix + "All Garibs"] = 30000 + (each_world_index * 10) + 2
|
|
case "l3":
|
|
location_name_to_id[prefix + "All Garibs"] = 30000 + (each_world_index * 10) + 3
|
|
case "l5":
|
|
location_name_to_id[prefix + "All Garibs"] = 30000 + (each_world_index * 10) + 5
|
|
#Scores
|
|
for world_index, world_prefix in enumerate(world_prefixes, 1):
|
|
for level_index, level_prefix in enumerate(level_prefixes):
|
|
level_score_address = 100000 * ((world_index * 10) + level_index)
|
|
if (level_index != 4 or world_index == 6) and level_index != 0:
|
|
level_name = world_prefix + level_prefix
|
|
prefix = level_name + ": "
|
|
location_name_to_id[prefix + "Score"] = level_score_address
|
|
location_name_groups["Score"].append(prefix + "Score")
|
|
location_name_groups[level_name].append(prefix + "Score")
|
|
for each_score in range(10000, 100000000, 10000):
|
|
location_name_to_id[str(each_score) + " Score"] = 100000000 + each_score
|
|
location_name_groups["Score"].append(str(each_score) + " Score")
|
|
|
|
for each_crystal in range(1,8):
|
|
#for each_loc, each_id in location_name_groups.items():
|
|
# if each_id == 1945 + each_crystal:
|
|
# each_loc
|
|
turn_in_name = "Ball Turn-In " + str(each_crystal)
|
|
location_name_to_id[turn_in_name] = 1945 + each_crystal
|
|
location_name_groups["Crystals"].append(turn_in_name)
|
|
return [location_name_to_id, location_name_groups] |