mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-22 15:45:04 -07:00
Merge branch 'main' into setup_more_apworld
This commit is contained in:
1
.github/labeler.yml
vendored
1
.github/labeler.yml
vendored
@@ -21,7 +21,6 @@
|
||||
- '!data/**'
|
||||
- '!.run/**'
|
||||
- '!.github/**'
|
||||
- '!worlds_disabled/**'
|
||||
- '!worlds/**'
|
||||
- '!WebHost.py'
|
||||
- '!WebHostLib/**'
|
||||
|
||||
33
Fill.py
33
Fill.py
@@ -138,32 +138,21 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
# to clean that up later, so there is a chance generation fails.
|
||||
if (not single_player_placement or location.player == item_to_place.player) \
|
||||
and location.can_fill(swap_state, item_to_place, perform_access_check):
|
||||
# Add this item to the existing placement, and
|
||||
# add the old item to the back of the queue
|
||||
spot_to_fill = placements.pop(i)
|
||||
|
||||
# Verify placing this item won't reduce available locations, which would be a useless swap.
|
||||
prev_state = swap_state.copy()
|
||||
prev_loc_count = len(
|
||||
multiworld.get_reachable_locations(prev_state))
|
||||
swap_count += 1
|
||||
swapped_items[placed_item.player, placed_item.name, unsafe] = swap_count
|
||||
|
||||
swap_state.collect(item_to_place, True)
|
||||
new_loc_count = len(
|
||||
multiworld.get_reachable_locations(swap_state))
|
||||
reachable_items[placed_item.player].appendleft(
|
||||
placed_item)
|
||||
item_pool.append(placed_item)
|
||||
|
||||
if new_loc_count >= prev_loc_count:
|
||||
# Add this item to the existing placement, and
|
||||
# add the old item to the back of the queue
|
||||
spot_to_fill = placements.pop(i)
|
||||
# cleanup at the end to hopefully get better errors
|
||||
cleanup_required = True
|
||||
|
||||
swap_count += 1
|
||||
swapped_items[placed_item.player, placed_item.name, unsafe] = swap_count
|
||||
|
||||
reachable_items[placed_item.player].appendleft(
|
||||
placed_item)
|
||||
item_pool.append(placed_item)
|
||||
|
||||
# cleanup at the end to hopefully get better errors
|
||||
cleanup_required = True
|
||||
|
||||
break
|
||||
break
|
||||
|
||||
# Item can't be placed here, restore original item
|
||||
location.item = placed_item
|
||||
|
||||
13
Options.py
13
Options.py
@@ -1676,6 +1676,7 @@ def get_option_groups(world: typing.Type[World], visibility_level: Visibility =
|
||||
|
||||
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None:
|
||||
import os
|
||||
from inspect import cleandoc
|
||||
|
||||
import yaml
|
||||
from jinja2 import Template
|
||||
@@ -1714,19 +1715,21 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
# yaml dump may add end of document marker and newlines.
|
||||
return yaml.dump(scalar).replace("...\n", "").strip()
|
||||
|
||||
with open(local_path("data", "options.yaml")) as f:
|
||||
file_data = f.read()
|
||||
template = Template(file_data)
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden or generate_hidden:
|
||||
option_groups = get_option_groups(world)
|
||||
with open(local_path("data", "options.yaml")) as f:
|
||||
file_data = f.read()
|
||||
res = Template(file_data).render(
|
||||
|
||||
res = template.render(
|
||||
option_groups=option_groups,
|
||||
__version__=__version__, game=game_name, yaml_dump=yaml_dump_scalar,
|
||||
dictify_range=dictify_range,
|
||||
cleandoc=cleandoc,
|
||||
)
|
||||
|
||||
del file_data
|
||||
|
||||
with open(os.path.join(target_folder, get_file_safe_name(game_name) + ".yaml"), "w", encoding="utf-8-sig") as f:
|
||||
f.write(res)
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ Currently, the following games are supported:
|
||||
* Inscryption
|
||||
* Civilization VI
|
||||
* The Legend of Zelda: The Wind Waker
|
||||
* Jak and Daxter: The Precursor Legacy
|
||||
* Super Mario Land 2: 6 Golden Coins
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
1
Utils.py
1
Utils.py
@@ -540,6 +540,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO,
|
||||
if add_timestamp:
|
||||
stream_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(stream_handler)
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
# Relay unhandled exceptions to logger.
|
||||
if not getattr(sys.excepthook, "_wrapped", False): # skip if already modified
|
||||
|
||||
@@ -51,10 +51,9 @@ requires:
|
||||
{%- for option_key, option in group_options.items() %}
|
||||
{{ option_key }}:
|
||||
{%- if option.__doc__ %}
|
||||
# {{ option.__doc__
|
||||
# {{ cleandoc(option.__doc__)
|
||||
| trim
|
||||
| replace('\n\n', '\n \n')
|
||||
| replace('\n ', '\n# ')
|
||||
| replace('\n', '\n# ')
|
||||
| indent(4, first=False)
|
||||
}}
|
||||
{%- endif -%}
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
# Inscryption
|
||||
/worlds/inscryption/ @DrBibop @Glowbuzz
|
||||
|
||||
# Jak and Daxter: The Precursor Legacy
|
||||
/worlds/jakanddaxter/ @massimilianodelliubaldini
|
||||
|
||||
# Kirby's Dream Land 3
|
||||
/worlds/kdl3/ @Silvris
|
||||
|
||||
@@ -175,6 +178,9 @@
|
||||
# Super Mario 64
|
||||
/worlds/sm64ex/ @N00byKing
|
||||
|
||||
# Super Mario Land 2: 6 Golden Coins
|
||||
/worlds/marioland2/ @Alchav
|
||||
|
||||
# Super Mario World
|
||||
/worlds/smw/ @PoryGone
|
||||
|
||||
@@ -232,7 +238,7 @@
|
||||
## Active Unmaintained Worlds
|
||||
|
||||
# The following worlds in this repo are currently unmaintained, but currently still work in core. If any update breaks
|
||||
# compatibility, these worlds may be moved to `worlds_disabled`. If you are interested in stepping up as maintainer for
|
||||
# compatibility, these worlds may be deleted. If you are interested in stepping up as maintainer for
|
||||
# any of these worlds, please review `/docs/world maintainer.md` documentation.
|
||||
|
||||
# Final Fantasy (1)
|
||||
@@ -241,15 +247,6 @@
|
||||
# Ocarina of Time
|
||||
# /worlds/oot/
|
||||
|
||||
## Disabled Unmaintained Worlds
|
||||
|
||||
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are
|
||||
# interested in stepping up as maintainer for any of these worlds, please review `/docs/world maintainer.md`
|
||||
# documentation.
|
||||
|
||||
# Ori and the Blind Forest
|
||||
# /worlds_disabled/oribf/
|
||||
|
||||
###################
|
||||
## Documentation ##
|
||||
###################
|
||||
|
||||
@@ -65,5 +65,5 @@ date, voting members and final result in the commit message.
|
||||
|
||||
## Handling of Unmaintained Worlds
|
||||
|
||||
As long as worlds are known to work for the most part, they can stay included. Once a world becomes broken it shall be
|
||||
moved from `worlds/` to `worlds_disabled/`.
|
||||
As long as worlds are known to work for the most part, they can stay included. Once the world becomes broken, it shall
|
||||
be deleted.
|
||||
|
||||
4
setup.py
4
setup.py
@@ -359,10 +359,6 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
||||
assert not non_apworlds - set(AutoWorldRegister.world_types), \
|
||||
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
|
||||
folders_to_remove: list[str] = []
|
||||
disabled_worlds_folder = "worlds_disabled"
|
||||
for entry in os.listdir(disabled_worlds_folder):
|
||||
if os.path.isdir(os.path.join(disabled_worlds_folder, entry)):
|
||||
folders_to_remove.append(entry)
|
||||
generate_yaml_templates(self.buildfolder / "Players" / "Templates", False)
|
||||
for worldname, worldtype in AutoWorldRegister.world_types.items():
|
||||
if worldname not in non_apworlds:
|
||||
|
||||
@@ -78,24 +78,15 @@ class InvalidDataError(Exception):
|
||||
|
||||
|
||||
class APContainer:
|
||||
"""A zipfile containing at least archipelago.json"""
|
||||
version: int = container_version
|
||||
compression_level: int = 9
|
||||
compression_method: int = zipfile.ZIP_DEFLATED
|
||||
game: Optional[str] = None
|
||||
"""A zipfile containing at least archipelago.json, which contains a manifest json payload."""
|
||||
version: ClassVar[int] = container_version
|
||||
compression_level: ClassVar[int] = 9
|
||||
compression_method: ClassVar[int] = zipfile.ZIP_DEFLATED
|
||||
|
||||
# instance attributes:
|
||||
path: Optional[str]
|
||||
player: Optional[int]
|
||||
player_name: str
|
||||
server: str
|
||||
|
||||
def __init__(self, path: Optional[str] = None, player: Optional[int] = None,
|
||||
player_name: str = "", server: str = ""):
|
||||
def __init__(self, path: Optional[str] = None):
|
||||
self.path = path
|
||||
self.player = player
|
||||
self.player_name = player_name
|
||||
self.server = server
|
||||
|
||||
def write(self, file: Optional[Union[str, BinaryIO]] = None) -> None:
|
||||
zip_file = file if file else self.path
|
||||
@@ -135,31 +126,58 @@ class APContainer:
|
||||
message = f"{arg0} - "
|
||||
raise InvalidDataError(f"{message}This might be the incorrect world version for this file") from e
|
||||
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> Dict[str, Any]:
|
||||
with opened_zipfile.open("archipelago.json", "r") as f:
|
||||
manifest = json.load(f)
|
||||
if manifest["compatible_version"] > self.version:
|
||||
raise Exception(f"File (version: {manifest['compatible_version']}) too new "
|
||||
f"for this handler (version: {self.version})")
|
||||
self.player = manifest["player"]
|
||||
self.server = manifest["server"]
|
||||
self.player_name = manifest["player_name"]
|
||||
return manifest
|
||||
|
||||
def get_manifest(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"server": self.server, # allow immediate connection to server in multiworld. Empty string otherwise
|
||||
"player": self.player,
|
||||
"player_name": self.player_name,
|
||||
"game": self.game,
|
||||
# minimum version of patch system expected for patching to be successful
|
||||
"compatible_version": 5,
|
||||
"version": container_version,
|
||||
}
|
||||
|
||||
|
||||
class APPatch(APContainer):
|
||||
class APPlayerContainer(APContainer):
|
||||
"""A zipfile containing at least archipelago.json meant for a player"""
|
||||
game: ClassVar[Optional[str]] = None
|
||||
|
||||
player: Optional[int]
|
||||
player_name: str
|
||||
server: str
|
||||
|
||||
def __init__(self, path: Optional[str] = None, player: Optional[int] = None,
|
||||
player_name: str = "", server: str = ""):
|
||||
super().__init__(path)
|
||||
self.player = player
|
||||
self.player_name = player_name
|
||||
self.server = server
|
||||
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> Dict[str, Any]:
|
||||
manifest = super().read_contents(opened_zipfile)
|
||||
self.player = manifest["player"]
|
||||
self.server = manifest["server"]
|
||||
self.player_name = manifest["player_name"]
|
||||
return manifest
|
||||
|
||||
def get_manifest(self) -> Dict[str, Any]:
|
||||
manifest = super().get_manifest()
|
||||
manifest.update({
|
||||
"server": self.server, # allow immediate connection to server in multiworld. Empty string otherwise
|
||||
"player": self.player,
|
||||
"player_name": self.player_name,
|
||||
"game": self.game,
|
||||
})
|
||||
return manifest
|
||||
|
||||
|
||||
class APPatch(APPlayerContainer):
|
||||
"""
|
||||
An `APContainer` that represents a patch file.
|
||||
An `APPlayerContainer` that represents a patch file.
|
||||
It includes the `procedure` key in the manifest to indicate that it is a patch.
|
||||
|
||||
Your implementation should inherit from this if your output file
|
||||
|
||||
@@ -182,10 +182,11 @@ class AdventureDeltaPatch(APPatch, metaclass=AutoPatchRegister):
|
||||
json.dumps(self.rom_deltas),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(AdventureDeltaPatch, self).read_contents(opened_zipfile)
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> dict[str, Any]:
|
||||
manifest = super(AdventureDeltaPatch, self).read_contents(opened_zipfile)
|
||||
self.foreign_items = AdventureDeltaPatch.read_foreign_items(opened_zipfile)
|
||||
self.autocollect_items = AdventureDeltaPatch.read_autocollect_items(opened_zipfile)
|
||||
return manifest
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
## Required Software
|
||||
|
||||
- ChecksFinder from
|
||||
the [Github releases Page for the game](https://github.com/jonloveslegos/ChecksFinder/releases) (latest version)
|
||||
the [Github releases Page for the game](https://github.com/jonloveslegos/ChecksFinder/releases) (latest version), or
|
||||
from the [itch.io Page for the game](https://suncat0.itch.io/checksfinder) (including web version)
|
||||
|
||||
## Configuring your YAML file
|
||||
|
||||
@@ -18,13 +19,13 @@ You can customize your options by visiting the [ChecksFinder Player Options Page
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Start ChecksFinder
|
||||
2. Enter the following information:
|
||||
- Enter the server url (starting from `wss://` for https connection like archipelago.gg, and starting from `ws://` for http connection and local multiserver)
|
||||
- Enter server port
|
||||
- Enter the name of the slot you wish to connect to
|
||||
- Enter the room password (optional)
|
||||
- Press `Play Online` to connect
|
||||
3. Start playing!
|
||||
|
||||
Game options and controls are described in the readme on the github repository for the game
|
||||
1. Start ChecksFinder and press `Play Online`
|
||||
2. Switch to the console window/tab
|
||||
3. Enter the following information:
|
||||
- Server url
|
||||
- Server port
|
||||
- The name of the slot you wish to connect to
|
||||
- The room password (optional)
|
||||
4. Press `Connect` to connect
|
||||
5. Switch to the game window/tab
|
||||
6. Start playing!
|
||||
|
||||
@@ -273,9 +273,7 @@ class DarkSouls3World(World):
|
||||
self.player,
|
||||
location,
|
||||
parent = new_region,
|
||||
event = True,
|
||||
)
|
||||
event_item.code = None
|
||||
new_location.place_locked_item(event_item)
|
||||
if location.name in excluded:
|
||||
excluded.remove(location.name)
|
||||
|
||||
@@ -63,7 +63,7 @@ recipe_time_ranges = {
|
||||
}
|
||||
|
||||
|
||||
class FactorioModFile(worlds.Files.APContainer):
|
||||
class FactorioModFile(worlds.Files.APPlayerContainer):
|
||||
game = "Factorio"
|
||||
compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives
|
||||
writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]]
|
||||
|
||||
504
worlds/jakanddaxter/__init__.py
Normal file
504
worlds/jakanddaxter/__init__.py
Normal file
@@ -0,0 +1,504 @@
|
||||
# Python standard libraries
|
||||
from collections import defaultdict
|
||||
from math import ceil
|
||||
from typing import Any, ClassVar, Callable, Union, cast
|
||||
|
||||
# Archipelago imports
|
||||
import settings
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from worlds.LauncherComponents import components, Component, launch_subprocess, Type, icon_paths
|
||||
from BaseClasses import (Item,
|
||||
ItemClassification as ItemClass,
|
||||
Tutorial,
|
||||
CollectionState)
|
||||
from Options import OptionGroup
|
||||
|
||||
# Jak imports
|
||||
from . import options
|
||||
from .game_id import jak1_id, jak1_name, jak1_max
|
||||
from .items import (JakAndDaxterItem,
|
||||
OrbAssoc,
|
||||
item_table,
|
||||
cell_item_table,
|
||||
scout_item_table,
|
||||
special_item_table,
|
||||
move_item_table,
|
||||
orb_item_table,
|
||||
trap_item_table)
|
||||
from .levels import level_table, level_table_with_global
|
||||
from .locations import (JakAndDaxterLocation,
|
||||
location_table,
|
||||
cell_location_table,
|
||||
scout_location_table,
|
||||
special_location_table,
|
||||
cache_location_table,
|
||||
orb_location_table)
|
||||
from .regions import create_regions
|
||||
from .rules import (enforce_multiplayer_limits,
|
||||
enforce_singleplayer_limits,
|
||||
verify_orb_trade_amounts,
|
||||
set_orb_trade_rule)
|
||||
from .locs import (cell_locations as cells,
|
||||
scout_locations as scouts,
|
||||
special_locations as specials,
|
||||
orb_cache_locations as caches,
|
||||
orb_locations as orbs)
|
||||
from .regs.region_base import JakAndDaxterRegion
|
||||
|
||||
|
||||
def launch_client():
|
||||
from . import client
|
||||
launch_subprocess(client.launch, name="JakAndDaxterClient")
|
||||
|
||||
|
||||
components.append(Component("Jak and Daxter Client",
|
||||
func=launch_client,
|
||||
component_type=Type.CLIENT,
|
||||
icon="precursor_orb"))
|
||||
|
||||
icon_paths["precursor_orb"] = f"ap:{__name__}/icons/precursor_orb.png"
|
||||
|
||||
|
||||
class JakAndDaxterSettings(settings.Group):
|
||||
class RootDirectory(settings.UserFolderPath):
|
||||
"""Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe).
|
||||
Ensure this path contains forward slashes (/) only. This setting only applies if
|
||||
Auto Detect Root Directory is set to false."""
|
||||
description = "ArchipelaGOAL Root Directory"
|
||||
|
||||
class AutoDetectRootDirectory(settings.Bool):
|
||||
"""Attempt to find the OpenGOAL installation and the mod executables (gk.exe and goalc.exe)
|
||||
automatically. If set to true, the ArchipelaGOAL Root Directory setting is ignored."""
|
||||
description = "ArchipelaGOAL Auto Detect Root Directory"
|
||||
|
||||
class EnforceFriendlyOptions(settings.Bool):
|
||||
"""Enforce friendly player options in both single and multiplayer seeds. Disabling this allows for
|
||||
more disruptive and challenging options, but may impact seed generation. Use at your own risk!"""
|
||||
description = "ArchipelaGOAL Enforce Friendly Options"
|
||||
|
||||
root_directory: RootDirectory = RootDirectory(
|
||||
"%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal")
|
||||
# Don't ever change these type hints again.
|
||||
auto_detect_root_directory: Union[AutoDetectRootDirectory, bool] = True
|
||||
enforce_friendly_options: Union[EnforceFriendlyOptions, bool] = True
|
||||
|
||||
|
||||
class JakAndDaxterWebWorld(WebWorld):
|
||||
setup_en = Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up ArchipelaGOAL (Archipelago on OpenGOAL).",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["markustulliuscicero"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en]
|
||||
bug_report_page = "https://github.com/ArchipelaGOAL/Archipelago/issues"
|
||||
|
||||
option_groups = [
|
||||
OptionGroup("Orbsanity", [
|
||||
options.EnableOrbsanity,
|
||||
options.GlobalOrbsanityBundleSize,
|
||||
options.PerLevelOrbsanityBundleSize,
|
||||
]),
|
||||
OptionGroup("Power Cell Counts", [
|
||||
options.EnableOrderedCellCounts,
|
||||
options.FireCanyonCellCount,
|
||||
options.MountainPassCellCount,
|
||||
options.LavaTubeCellCount,
|
||||
]),
|
||||
OptionGroup("Orb Trade Counts", [
|
||||
options.CitizenOrbTradeAmount,
|
||||
options.OracleOrbTradeAmount,
|
||||
]),
|
||||
OptionGroup("Traps", [
|
||||
options.FillerPowerCellsReplacedWithTraps,
|
||||
options.FillerOrbBundlesReplacedWithTraps,
|
||||
options.TrapEffectDuration,
|
||||
options.TrapWeights,
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
class JakAndDaxterWorld(World):
|
||||
"""
|
||||
Jak and Daxter: The Precursor Legacy is a 2001 action platformer developed by Naughty Dog
|
||||
for the PlayStation 2. The game follows the eponymous protagonists, a young boy named Jak
|
||||
and his friend Daxter, who has been transformed into an ottsel. With the help of Samos
|
||||
the Sage of Green Eco and his daughter Keira, the pair travel north in search of a cure for Daxter,
|
||||
discovering artifacts created by an ancient race known as the Precursors along the way. When the
|
||||
rogue sages Gol and Maia Acheron plan to flood the world with Dark Eco, they must stop their evil plan
|
||||
and save the world.
|
||||
"""
|
||||
# ID, name, version
|
||||
game = jak1_name
|
||||
required_client_version = (0, 5, 0)
|
||||
|
||||
# Options
|
||||
settings: ClassVar[JakAndDaxterSettings]
|
||||
options_dataclass = options.JakAndDaxterOptions
|
||||
options: options.JakAndDaxterOptions
|
||||
|
||||
# Web world
|
||||
web = JakAndDaxterWebWorld()
|
||||
|
||||
# Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs.
|
||||
# Remember, the game ID and various offsets for each item type have already been calculated.
|
||||
item_name_to_id = {name: k for k, name in item_table.items()}
|
||||
location_name_to_id = {name: k for k, name in location_table.items()}
|
||||
item_name_groups = {
|
||||
"Power Cells": set(cell_item_table.values()),
|
||||
"Scout Flies": set(scout_item_table.values()),
|
||||
"Specials": set(special_item_table.values()),
|
||||
"Moves": set(move_item_table.values()),
|
||||
"Precursor Orbs": set(orb_item_table.values()),
|
||||
"Traps": set(trap_item_table.values()),
|
||||
}
|
||||
location_name_groups = {
|
||||
"Power Cells": set(cell_location_table.values()),
|
||||
"Power Cells - GR": set(cells.locGR_cellTable.values()),
|
||||
"Power Cells - SV": set(cells.locSV_cellTable.values()),
|
||||
"Power Cells - FJ": set(cells.locFJ_cellTable.values()),
|
||||
"Power Cells - SB": set(cells.locSB_cellTable.values()),
|
||||
"Power Cells - MI": set(cells.locMI_cellTable.values()),
|
||||
"Power Cells - FC": set(cells.locFC_cellTable.values()),
|
||||
"Power Cells - RV": set(cells.locRV_cellTable.values()),
|
||||
"Power Cells - PB": set(cells.locPB_cellTable.values()),
|
||||
"Power Cells - LPC": set(cells.locLPC_cellTable.values()),
|
||||
"Power Cells - BS": set(cells.locBS_cellTable.values()),
|
||||
"Power Cells - MP": set(cells.locMP_cellTable.values()),
|
||||
"Power Cells - VC": set(cells.locVC_cellTable.values()),
|
||||
"Power Cells - SC": set(cells.locSC_cellTable.values()),
|
||||
"Power Cells - SM": set(cells.locSM_cellTable.values()),
|
||||
"Power Cells - LT": set(cells.locLT_cellTable.values()),
|
||||
"Power Cells - GMC": set(cells.locGMC_cellTable.values()),
|
||||
"Scout Flies": set(scout_location_table.values()),
|
||||
"Scout Flies - GR": set(scouts.locGR_scoutTable.values()),
|
||||
"Scout Flies - SV": set(scouts.locSV_scoutTable.values()),
|
||||
"Scout Flies - FJ": set(scouts.locFJ_scoutTable.values()),
|
||||
"Scout Flies - SB": set(scouts.locSB_scoutTable.values()),
|
||||
"Scout Flies - MI": set(scouts.locMI_scoutTable.values()),
|
||||
"Scout Flies - FC": set(scouts.locFC_scoutTable.values()),
|
||||
"Scout Flies - RV": set(scouts.locRV_scoutTable.values()),
|
||||
"Scout Flies - PB": set(scouts.locPB_scoutTable.values()),
|
||||
"Scout Flies - LPC": set(scouts.locLPC_scoutTable.values()),
|
||||
"Scout Flies - BS": set(scouts.locBS_scoutTable.values()),
|
||||
"Scout Flies - MP": set(scouts.locMP_scoutTable.values()),
|
||||
"Scout Flies - VC": set(scouts.locVC_scoutTable.values()),
|
||||
"Scout Flies - SC": set(scouts.locSC_scoutTable.values()),
|
||||
"Scout Flies - SM": set(scouts.locSM_scoutTable.values()),
|
||||
"Scout Flies - LT": set(scouts.locLT_scoutTable.values()),
|
||||
"Scout Flies - GMC": set(scouts.locGMC_scoutTable.values()),
|
||||
"Specials": set(special_location_table.values()),
|
||||
"Orb Caches": set(cache_location_table.values()),
|
||||
"Precursor Orbs": set(orb_location_table.values()),
|
||||
"Precursor Orbs - GR": set(orbs.locGR_orbBundleTable.values()),
|
||||
"Precursor Orbs - SV": set(orbs.locSV_orbBundleTable.values()),
|
||||
"Precursor Orbs - FJ": set(orbs.locFJ_orbBundleTable.values()),
|
||||
"Precursor Orbs - SB": set(orbs.locSB_orbBundleTable.values()),
|
||||
"Precursor Orbs - MI": set(orbs.locMI_orbBundleTable.values()),
|
||||
"Precursor Orbs - FC": set(orbs.locFC_orbBundleTable.values()),
|
||||
"Precursor Orbs - RV": set(orbs.locRV_orbBundleTable.values()),
|
||||
"Precursor Orbs - PB": set(orbs.locPB_orbBundleTable.values()),
|
||||
"Precursor Orbs - LPC": set(orbs.locLPC_orbBundleTable.values()),
|
||||
"Precursor Orbs - BS": set(orbs.locBS_orbBundleTable.values()),
|
||||
"Precursor Orbs - MP": set(orbs.locMP_orbBundleTable.values()),
|
||||
"Precursor Orbs - VC": set(orbs.locVC_orbBundleTable.values()),
|
||||
"Precursor Orbs - SC": set(orbs.locSC_orbBundleTable.values()),
|
||||
"Precursor Orbs - SM": set(orbs.locSM_orbBundleTable.values()),
|
||||
"Precursor Orbs - LT": set(orbs.locLT_orbBundleTable.values()),
|
||||
"Precursor Orbs - GMC": set(orbs.locGMC_orbBundleTable.values()),
|
||||
"Trades": {location_table[cells.to_ap_id(k)] for k in
|
||||
{11, 12, 31, 32, 33, 96, 97, 98, 99, 13, 14, 34, 35, 100, 101}},
|
||||
"'Free 7 Scout Flies' Power Cells": set(cells.loc7SF_cellTable.values()),
|
||||
}
|
||||
|
||||
# These functions and variables are Options-driven, keep them as instance variables here so that we don't clog up
|
||||
# the seed generation routines with options checking. So we set these once, and then just use them as needed.
|
||||
can_trade: Callable[[CollectionState, int, int | None], bool]
|
||||
total_orbs: int = 2000
|
||||
orb_bundle_item_name: str = ""
|
||||
orb_bundle_size: int = 0
|
||||
total_trade_orbs: int = 0
|
||||
total_prog_orb_bundles: int = 0
|
||||
total_trap_orb_bundles: int = 0
|
||||
total_filler_orb_bundles: int = 0
|
||||
total_power_cells: int = 101
|
||||
total_prog_cells: int = 0
|
||||
total_trap_cells: int = 0
|
||||
total_filler_cells: int = 0
|
||||
power_cell_thresholds: list[int]
|
||||
power_cell_thresholds_minus_one: list[int]
|
||||
trap_weights: tuple[list[str], list[int]]
|
||||
|
||||
# Store these dictionaries for speed improvements.
|
||||
level_to_regions: dict[str, list[JakAndDaxterRegion]] # Contains all levels and regions.
|
||||
level_to_orb_regions: dict[str, list[JakAndDaxterRegion]] # Contains only regions which contain orbs.
|
||||
|
||||
# Handles various options validation, rules enforcement, and caching of important information.
|
||||
def generate_early(self) -> None:
|
||||
|
||||
# Initialize the level-region dictionary.
|
||||
self.level_to_regions = defaultdict(list)
|
||||
self.level_to_orb_regions = defaultdict(list)
|
||||
|
||||
# Cache the power cell threshold values for quicker reference.
|
||||
self.power_cell_thresholds = [
|
||||
self.options.fire_canyon_cell_count.value,
|
||||
self.options.mountain_pass_cell_count.value,
|
||||
self.options.lava_tube_cell_count.value,
|
||||
100, # The 100 Power Cell Door.
|
||||
]
|
||||
|
||||
# Order the thresholds ascending and set the options values to the new order.
|
||||
if self.options.enable_ordered_cell_counts:
|
||||
self.power_cell_thresholds.sort()
|
||||
self.options.fire_canyon_cell_count.value = self.power_cell_thresholds[0]
|
||||
self.options.mountain_pass_cell_count.value = self.power_cell_thresholds[1]
|
||||
self.options.lava_tube_cell_count.value = self.power_cell_thresholds[2]
|
||||
|
||||
# Store this for remove function.
|
||||
self.power_cell_thresholds_minus_one = [x - 1 for x in self.power_cell_thresholds]
|
||||
|
||||
# For the fairness of other players in a multiworld game, enforce some friendly limitations on our options,
|
||||
# so we don't cause chaos during seed generation. These friendly limits should **guarantee** a successful gen.
|
||||
# We would have done this earlier, but we needed to sort the power cell thresholds first.
|
||||
enforce_friendly_options = self.settings.enforce_friendly_options
|
||||
if enforce_friendly_options:
|
||||
if self.multiworld.players > 1:
|
||||
enforce_multiplayer_limits(self)
|
||||
else:
|
||||
enforce_singleplayer_limits(self)
|
||||
|
||||
# Calculate the number of power cells needed for full region access, the number being replaced by traps,
|
||||
# and the number of remaining filler.
|
||||
if self.options.jak_completion_condition == options.CompletionCondition.option_open_100_cell_door:
|
||||
self.total_prog_cells = 100
|
||||
else:
|
||||
self.total_prog_cells = max(self.power_cell_thresholds[:3])
|
||||
non_prog_cells = self.total_power_cells - self.total_prog_cells
|
||||
self.total_trap_cells = min(self.options.filler_power_cells_replaced_with_traps.value, non_prog_cells)
|
||||
self.options.filler_power_cells_replaced_with_traps.value = self.total_trap_cells
|
||||
self.total_filler_cells = non_prog_cells - self.total_trap_cells
|
||||
|
||||
# Verify that we didn't overload the trade amounts with more orbs than exist in the world.
|
||||
# This is easy to do by accident even in a singleplayer world.
|
||||
self.total_trade_orbs = (9 * self.options.citizen_orb_trade_amount) + (6 * self.options.oracle_orb_trade_amount)
|
||||
verify_orb_trade_amounts(self)
|
||||
|
||||
# Cache the orb bundle size and item name for quicker reference.
|
||||
if self.options.enable_orbsanity == options.EnableOrbsanity.option_per_level:
|
||||
self.orb_bundle_size = self.options.level_orbsanity_bundle_size.value
|
||||
self.orb_bundle_item_name = orb_item_table[self.orb_bundle_size]
|
||||
elif self.options.enable_orbsanity == options.EnableOrbsanity.option_global:
|
||||
self.orb_bundle_size = self.options.global_orbsanity_bundle_size.value
|
||||
self.orb_bundle_item_name = orb_item_table[self.orb_bundle_size]
|
||||
else:
|
||||
self.orb_bundle_size = 0
|
||||
self.orb_bundle_item_name = ""
|
||||
|
||||
# Calculate the number of orb bundles needed for trades, the number being replaced by traps,
|
||||
# and the number of remaining filler. If Orbsanity is off, default values of 0 will prevail for all.
|
||||
if self.orb_bundle_size > 0:
|
||||
total_orb_bundles = self.total_orbs // self.orb_bundle_size
|
||||
self.total_prog_orb_bundles = ceil(self.total_trade_orbs / self.orb_bundle_size)
|
||||
non_prog_orb_bundles = total_orb_bundles - self.total_prog_orb_bundles
|
||||
self.total_trap_orb_bundles = min(self.options.filler_orb_bundles_replaced_with_traps.value,
|
||||
non_prog_orb_bundles)
|
||||
self.options.filler_orb_bundles_replaced_with_traps.value = self.total_trap_orb_bundles
|
||||
self.total_filler_orb_bundles = non_prog_orb_bundles - self.total_trap_orb_bundles
|
||||
else:
|
||||
self.options.filler_orb_bundles_replaced_with_traps.value = 0
|
||||
|
||||
self.trap_weights = self.options.trap_weights.weights_pair
|
||||
|
||||
# Options drive which trade rules to use, so they need to be setup before we create_regions.
|
||||
set_orb_trade_rule(self)
|
||||
|
||||
# This will also set Locations, Location access rules, Region access rules, etc.
|
||||
def create_regions(self) -> None:
|
||||
create_regions(self)
|
||||
|
||||
# Don't forget to add the created regions to the multiworld!
|
||||
for level in self.level_to_regions:
|
||||
self.multiworld.regions.extend(self.level_to_regions[level])
|
||||
|
||||
# As a lazy measure, let's also fill level_to_orb_regions here.
|
||||
# This should help speed up orbsanity calculations.
|
||||
self.level_to_orb_regions[level] = [reg for reg in self.level_to_regions[level] if reg.orb_count > 0]
|
||||
|
||||
# from Utils import visualize_regions
|
||||
# visualize_regions(self.multiworld.get_region("Menu", self.player), "jakanddaxter.puml")
|
||||
|
||||
def item_data_helper(self, item: int) -> list[tuple[int, ItemClass, OrbAssoc, int]]:
|
||||
"""
|
||||
Helper function to reuse some nasty if/else trees. This outputs a list of pairs of item count and class.
|
||||
For instance, not all 101 power cells need to be marked progression if you only need 72 to beat the game.
|
||||
So we will have 72 Progression Power Cells, and 29 Filler Power Cells.
|
||||
"""
|
||||
data: list[tuple[int, ItemClass, OrbAssoc, int]] = []
|
||||
|
||||
# Make N Power Cells. We only want AP's Progression Fill routine to handle the amount of cells we need
|
||||
# to reach the furthest possible region. Even for early completion goals, all areas in the game must be
|
||||
# reachable or generation will fail. TODO - Option-driven region creation would be an enormous refactor.
|
||||
if item in range(jak1_id, jak1_id + scouts.fly_offset):
|
||||
data.append((self.total_prog_cells, ItemClass.progression_skip_balancing, OrbAssoc.IS_POWER_CELL, 0))
|
||||
data.append((self.total_filler_cells, ItemClass.filler, OrbAssoc.IS_POWER_CELL, 0))
|
||||
|
||||
# Make 7 Scout Flies per level.
|
||||
elif item in range(jak1_id + scouts.fly_offset, jak1_id + specials.special_offset):
|
||||
data.append((7, ItemClass.progression_skip_balancing, OrbAssoc.NEVER_UNLOCKS_ORBS, 0))
|
||||
|
||||
# Make only 1 of each Special Item.
|
||||
elif item in range(jak1_id + specials.special_offset, jak1_id + caches.orb_cache_offset):
|
||||
data.append((1, ItemClass.progression | ItemClass.useful, OrbAssoc.ALWAYS_UNLOCKS_ORBS, 0))
|
||||
|
||||
# Make only 1 of each Move Item.
|
||||
elif item in range(jak1_id + caches.orb_cache_offset, jak1_id + orbs.orb_offset):
|
||||
data.append((1, ItemClass.progression | ItemClass.useful, OrbAssoc.ALWAYS_UNLOCKS_ORBS, 0))
|
||||
|
||||
# Make N Precursor Orb bundles. Like Power Cells, only a fraction of these will be marked as Progression
|
||||
# with the remainder as Filler, but they are still entirely fungible. See collect function for why these
|
||||
# are OrbAssoc.NEVER_UNLOCKS_ORBS.
|
||||
elif item in range(jak1_id + orbs.orb_offset, jak1_max - max(trap_item_table)):
|
||||
data.append((self.total_prog_orb_bundles, ItemClass.progression_skip_balancing,
|
||||
OrbAssoc.NEVER_UNLOCKS_ORBS, self.orb_bundle_size))
|
||||
data.append((self.total_filler_orb_bundles, ItemClass.filler,
|
||||
OrbAssoc.NEVER_UNLOCKS_ORBS, self.orb_bundle_size))
|
||||
|
||||
# We will manually create trap items as needed.
|
||||
elif item in range(jak1_max - max(trap_item_table), jak1_max):
|
||||
data.append((0, ItemClass.trap, OrbAssoc.NEVER_UNLOCKS_ORBS, 0))
|
||||
|
||||
# We will manually create filler items as needed.
|
||||
elif item == jak1_max:
|
||||
data.append((0, ItemClass.filler, OrbAssoc.NEVER_UNLOCKS_ORBS, 0))
|
||||
|
||||
# If we try to make items with ID's higher than we've defined, something has gone wrong.
|
||||
else:
|
||||
raise KeyError(f"Tried to fill item pool with unknown ID {item}.")
|
||||
|
||||
return data
|
||||
|
||||
def create_items(self) -> None:
|
||||
items_made: int = 0
|
||||
for item_name in self.item_name_to_id:
|
||||
item_id = self.item_name_to_id[item_name]
|
||||
|
||||
# Handle Move Randomizer option.
|
||||
# If it is OFF, put all moves in your starting inventory instead of the item pool,
|
||||
# then fill the item pool with a corresponding amount of filler items.
|
||||
if item_name in self.item_name_groups["Moves"] and not self.options.enable_move_randomizer:
|
||||
self.multiworld.push_precollected(self.create_item(item_name))
|
||||
self.multiworld.itempool.append(self.create_filler())
|
||||
items_made += 1
|
||||
continue
|
||||
|
||||
# Handle Orbsanity option.
|
||||
# If it is OFF, don't add any orb bundles to the item pool, period.
|
||||
# If it is ON, don't add any orb bundles that don't match the chosen option.
|
||||
if (item_name in self.item_name_groups["Precursor Orbs"]
|
||||
and (self.options.enable_orbsanity == options.EnableOrbsanity.option_off
|
||||
or item_name != self.orb_bundle_item_name)):
|
||||
continue
|
||||
|
||||
# Skip Traps for now.
|
||||
if item_name in self.item_name_groups["Traps"]:
|
||||
continue
|
||||
|
||||
# In almost every other scenario, do this. Not all items with the same name will have the same item class.
|
||||
data = self.item_data_helper(item_id)
|
||||
for (count, classification, orb_assoc, orb_amount) in data:
|
||||
self.multiworld.itempool += [JakAndDaxterItem(item_name, classification, item_id,
|
||||
self.player, orb_assoc, orb_amount)
|
||||
for _ in range(count)]
|
||||
items_made += count
|
||||
|
||||
# Handle Traps (for real).
|
||||
# Manually fill the item pool with a weighted assortment of trap items, equal to the sum of
|
||||
# total_trap_cells + total_trap_orb_bundles. Only do this if one or more traps have weights > 0.
|
||||
names, weights = self.trap_weights
|
||||
if sum(weights):
|
||||
total_traps = self.total_trap_cells + self.total_trap_orb_bundles
|
||||
trap_list = self.random.choices(names, weights=weights, k=total_traps)
|
||||
self.multiworld.itempool += [self.create_item(trap_name) for trap_name in trap_list]
|
||||
items_made += total_traps
|
||||
|
||||
# Handle Unfilled Locations.
|
||||
# Add an amount of filler items equal to the number of locations yet to be filled.
|
||||
# This is the final set of items we will add to the pool.
|
||||
all_regions = self.multiworld.get_regions(self.player)
|
||||
total_locations = sum(reg.location_count for reg in cast(list[JakAndDaxterRegion], all_regions))
|
||||
total_filler = total_locations - items_made
|
||||
self.multiworld.itempool += [self.create_filler() for _ in range(total_filler)]
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
item_id = self.item_name_to_id[name]
|
||||
|
||||
# Use first tuple (will likely be the most important).
|
||||
_, classification, orb_assoc, orb_amount = self.item_data_helper(item_id)[0]
|
||||
return JakAndDaxterItem(name, classification, item_id, self.player, orb_assoc, orb_amount)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return "Green Eco Pill"
|
||||
|
||||
def collect(self, state: CollectionState, item: JakAndDaxterItem) -> bool:
|
||||
change = super().collect(state, item)
|
||||
if change:
|
||||
# Orbsanity as an option is no-factor to these conditions. Matching the item name implies Orbsanity is ON,
|
||||
# so we don't need to check the option. When Orbsanity is OFF, there won't even be any orb bundle items
|
||||
# to collect.
|
||||
|
||||
# Orb items do not intrinsically unlock anything that contains more Reachable Orbs, so they do not need to
|
||||
# set the cache to stale. They just change how many orbs you have to trade with.
|
||||
if item.orb_amount > 0:
|
||||
state.prog_items[self.player]["Tradeable Orbs"] += self.orb_bundle_size # Give a bundle of Trade Orbs
|
||||
|
||||
# Power Cells DO unlock new regions that contain more Reachable Orbs - the connector levels and new
|
||||
# hub levels - BUT they only do that when you have a number of them equal to one of the threshold values.
|
||||
elif (item.orb_assoc == OrbAssoc.ALWAYS_UNLOCKS_ORBS
|
||||
or (item.orb_assoc == OrbAssoc.IS_POWER_CELL
|
||||
and state.count("Power Cell", self.player) in self.power_cell_thresholds)):
|
||||
state.prog_items[self.player]["Reachable Orbs Fresh"] = False
|
||||
|
||||
# However, every other item that does not have an appropriate OrbAssoc that changes the CollectionState
|
||||
# should NOT set the cache to stale, because they did not make it possible to reach more orb locations
|
||||
# (level unlocks, region unlocks, etc.).
|
||||
return change
|
||||
|
||||
def remove(self, state: CollectionState, item: JakAndDaxterItem) -> bool:
|
||||
change = super().remove(state, item)
|
||||
if change:
|
||||
|
||||
# Do the same thing we did in collect, except subtract trade orbs instead of add.
|
||||
if item.orb_amount > 0:
|
||||
state.prog_items[self.player]["Tradeable Orbs"] -= self.orb_bundle_size # Take a bundle of Trade Orbs
|
||||
|
||||
# Ditto Power Cells, but check thresholds - 1, because we potentially crossed the threshold in the opposite
|
||||
# direction. E.g. we've removed the 20th power cell, our count is now 19, so we should stale the cache.
|
||||
elif (item.orb_assoc == OrbAssoc.ALWAYS_UNLOCKS_ORBS
|
||||
or (item.orb_assoc == OrbAssoc.IS_POWER_CELL
|
||||
and state.count("Power Cell", self.player) in self.power_cell_thresholds_minus_one)):
|
||||
state.prog_items[self.player]["Reachable Orbs Fresh"] = False
|
||||
|
||||
return change
|
||||
|
||||
def fill_slot_data(self) -> dict[str, Any]:
|
||||
options_dict = self.options.as_dict("enable_move_randomizer",
|
||||
"enable_orbsanity",
|
||||
"global_orbsanity_bundle_size",
|
||||
"level_orbsanity_bundle_size",
|
||||
"fire_canyon_cell_count",
|
||||
"mountain_pass_cell_count",
|
||||
"lava_tube_cell_count",
|
||||
"citizen_orb_trade_amount",
|
||||
"oracle_orb_trade_amount",
|
||||
"filler_power_cells_replaced_with_traps",
|
||||
"filler_orb_bundles_replaced_with_traps",
|
||||
"trap_effect_duration",
|
||||
"trap_weights",
|
||||
"jak_completion_condition",
|
||||
"require_punch_for_klaww",
|
||||
)
|
||||
return options_dict
|
||||
0
worlds/jakanddaxter/agents/__init__.py
Normal file
0
worlds/jakanddaxter/agents/__init__.py
Normal file
489
worlds/jakanddaxter/agents/memory_reader.py
Normal file
489
worlds/jakanddaxter/agents/memory_reader.py
Normal file
@@ -0,0 +1,489 @@
|
||||
import logging
|
||||
import random
|
||||
import struct
|
||||
from typing import ByteString, Callable
|
||||
import json
|
||||
import pymem
|
||||
from pymem import pattern
|
||||
from pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinAPIError
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ..locs import (orb_locations as orbs,
|
||||
cell_locations as cells,
|
||||
scout_locations as flies,
|
||||
special_locations as specials,
|
||||
orb_cache_locations as caches)
|
||||
|
||||
|
||||
logger = logging.getLogger("MemoryReader")
|
||||
|
||||
|
||||
# Some helpful constants.
|
||||
sizeof_uint64 = 8
|
||||
sizeof_uint32 = 4
|
||||
sizeof_uint8 = 1
|
||||
sizeof_float = 4
|
||||
|
||||
|
||||
# *****************************************************************************
|
||||
# **** This number must match (-> *ap-info-jak1* version) in ap-struct.gc! ****
|
||||
# *****************************************************************************
|
||||
expected_memory_version = 5
|
||||
|
||||
|
||||
# IMPORTANT: OpenGOAL memory structures are particular about the alignment, in memory, of member elements according to
|
||||
# their size in bits. The address for an N-bit field must be divisible by N. Use this class to define the memory offsets
|
||||
# of important values in the struct. It will also do the byte alignment properly for you.
|
||||
# See https://opengoal.dev/docs/reference/type_system/#arrays
|
||||
@dataclass
|
||||
class OffsetFactory:
|
||||
current_offset: int = 0
|
||||
|
||||
def define(self, size: int, length: int = 1) -> int:
|
||||
|
||||
# If necessary, align current_offset to the current size first.
|
||||
bytes_to_alignment = self.current_offset % size
|
||||
if bytes_to_alignment != 0:
|
||||
self.current_offset += (size - bytes_to_alignment)
|
||||
|
||||
# Increment current_offset so the next definition can be made.
|
||||
offset_to_use = self.current_offset
|
||||
self.current_offset += (size * length)
|
||||
return offset_to_use
|
||||
|
||||
|
||||
# Start defining important memory address offsets here. They must be in the same order, have the same sizes, and have
|
||||
# the same lengths, as defined in `ap-info-jak1`.
|
||||
offsets = OffsetFactory()
|
||||
|
||||
# Cell, Buzzer, and Special information.
|
||||
next_cell_index_offset = offsets.define(sizeof_uint64)
|
||||
next_buzzer_index_offset = offsets.define(sizeof_uint64)
|
||||
next_special_index_offset = offsets.define(sizeof_uint64)
|
||||
|
||||
cells_checked_offset = offsets.define(sizeof_uint32, 101)
|
||||
buzzers_checked_offset = offsets.define(sizeof_uint32, 112)
|
||||
specials_checked_offset = offsets.define(sizeof_uint32, 32)
|
||||
|
||||
buzzers_received_offset = offsets.define(sizeof_uint8, 16)
|
||||
specials_received_offset = offsets.define(sizeof_uint8, 32)
|
||||
|
||||
# Deathlink information.
|
||||
death_count_offset = offsets.define(sizeof_uint32)
|
||||
death_cause_offset = offsets.define(sizeof_uint8)
|
||||
deathlink_enabled_offset = offsets.define(sizeof_uint8)
|
||||
|
||||
# Move Rando information.
|
||||
next_orb_cache_index_offset = offsets.define(sizeof_uint64)
|
||||
orb_caches_checked_offset = offsets.define(sizeof_uint32, 16)
|
||||
moves_received_offset = offsets.define(sizeof_uint8, 16)
|
||||
moverando_enabled_offset = offsets.define(sizeof_uint8)
|
||||
|
||||
# Orbsanity information.
|
||||
orbsanity_option_offset = offsets.define(sizeof_uint8)
|
||||
orbsanity_bundle_offset = offsets.define(sizeof_uint32)
|
||||
collected_bundle_offset = offsets.define(sizeof_uint32, 17)
|
||||
|
||||
# Progression and Completion information.
|
||||
fire_canyon_unlock_offset = offsets.define(sizeof_float)
|
||||
mountain_pass_unlock_offset = offsets.define(sizeof_float)
|
||||
lava_tube_unlock_offset = offsets.define(sizeof_float)
|
||||
citizen_orb_amount_offset = offsets.define(sizeof_float)
|
||||
oracle_orb_amount_offset = offsets.define(sizeof_float)
|
||||
completion_goal_offset = offsets.define(sizeof_uint8)
|
||||
completed_offset = offsets.define(sizeof_uint8)
|
||||
|
||||
# Text to display in the HUD (32 char max per string).
|
||||
their_item_name_offset = offsets.define(sizeof_uint8, 32)
|
||||
their_item_owner_offset = offsets.define(sizeof_uint8, 32)
|
||||
my_item_name_offset = offsets.define(sizeof_uint8, 32)
|
||||
my_item_finder_offset = offsets.define(sizeof_uint8, 32)
|
||||
|
||||
# Version of the memory struct, to cut down on mod/apworld version mismatches.
|
||||
memory_version_offset = offsets.define(sizeof_uint32)
|
||||
|
||||
# Connection status to AP server (not the game!)
|
||||
server_connection_offset = offsets.define(sizeof_uint8)
|
||||
slot_name_offset = offsets.define(sizeof_uint8, 16)
|
||||
slot_seed_offset = offsets.define(sizeof_uint8, 8)
|
||||
|
||||
# Trap information.
|
||||
trap_duration_offset = offsets.define(sizeof_float)
|
||||
|
||||
# The End.
|
||||
end_marker_offset = offsets.define(sizeof_uint8, 4)
|
||||
|
||||
|
||||
# Can't believe this is easier to do in GOAL than Python but that's how it be sometimes.
|
||||
def as_float(value: int) -> int:
|
||||
return int(struct.unpack('f', value.to_bytes(sizeof_float, "little"))[0])
|
||||
|
||||
|
||||
# "Jak" to be replaced by player name in the Client.
|
||||
def autopsy(cause: int) -> str:
|
||||
if cause in [1, 2, 3, 4]:
|
||||
return random.choice(["Jak said goodnight.",
|
||||
"Jak stepped into the light.",
|
||||
"Jak gave Daxter his insect collection.",
|
||||
"Jak did not follow Step 1."])
|
||||
if cause == 5:
|
||||
return "Jak fell into an endless pit."
|
||||
if cause == 6:
|
||||
return "Jak drowned in the spicy water."
|
||||
if cause == 7:
|
||||
return "Jak tried to tackle a Lurker Shark."
|
||||
if cause == 8:
|
||||
return "Jak hit 500 degrees."
|
||||
if cause == 9:
|
||||
return "Jak took a bath in a pool of dark eco."
|
||||
if cause == 10:
|
||||
return "Jak got bombarded with flaming 30-ton boulders."
|
||||
if cause == 11:
|
||||
return "Jak hit 800 degrees."
|
||||
if cause == 12:
|
||||
return "Jak ceased to be."
|
||||
if cause == 13:
|
||||
return "Jak got eaten by the dark eco plant."
|
||||
if cause == 14:
|
||||
return "Jak burned up."
|
||||
if cause == 15:
|
||||
return "Jak hit the ground hard."
|
||||
if cause == 16:
|
||||
return "Jak crashed the zoomer."
|
||||
if cause == 17:
|
||||
return "Jak got Flut Flut hurt."
|
||||
if cause == 18:
|
||||
return "Jak poisoned the whole darn catch."
|
||||
if cause == 19:
|
||||
return "Jak collided with too many obstacles."
|
||||
return "Jak died."
|
||||
|
||||
|
||||
class JakAndDaxterMemoryReader:
|
||||
marker: ByteString
|
||||
goal_address: int | None = None
|
||||
connected: bool = False
|
||||
initiated_connect: bool = False
|
||||
|
||||
# The memory reader just needs the game running.
|
||||
gk_process: pymem.process = None
|
||||
|
||||
location_outbox: list[int] = []
|
||||
outbox_index: int = 0
|
||||
finished_game: bool = False
|
||||
|
||||
# Deathlink handling
|
||||
deathlink_enabled: bool = False
|
||||
send_deathlink: bool = False
|
||||
cause_of_death: str = ""
|
||||
death_count: int = 0
|
||||
|
||||
# Orbsanity handling
|
||||
orbsanity_enabled: bool = False
|
||||
orbs_paid: int = 0
|
||||
|
||||
# Game-related callbacks (inform the AP server of changes to game state)
|
||||
inform_checked_location: Callable
|
||||
inform_finished_game: Callable
|
||||
inform_died: Callable
|
||||
inform_toggled_deathlink: Callable
|
||||
inform_traded_orbs: Callable
|
||||
|
||||
# Logging callbacks
|
||||
# These will write to the provided logger, as well as the Client GUI with color markup.
|
||||
log_error: Callable # Red
|
||||
log_warn: Callable # Orange
|
||||
log_success: Callable # Green
|
||||
log_info: Callable # White (default)
|
||||
|
||||
def __init__(self,
|
||||
location_check_callback: Callable,
|
||||
finish_game_callback: Callable,
|
||||
send_deathlink_callback: Callable,
|
||||
toggle_deathlink_callback: Callable,
|
||||
orb_trade_callback: Callable,
|
||||
log_error_callback: Callable,
|
||||
log_warn_callback: Callable,
|
||||
log_success_callback: Callable,
|
||||
log_info_callback: Callable,
|
||||
marker: ByteString = b'UnLiStEdStRaTs_JaK1\x00'):
|
||||
self.marker = marker
|
||||
|
||||
self.inform_checked_location = location_check_callback
|
||||
self.inform_finished_game = finish_game_callback
|
||||
self.inform_died = send_deathlink_callback
|
||||
self.inform_toggled_deathlink = toggle_deathlink_callback
|
||||
self.inform_traded_orbs = orb_trade_callback
|
||||
|
||||
self.log_error = log_error_callback
|
||||
self.log_warn = log_warn_callback
|
||||
self.log_success = log_success_callback
|
||||
self.log_info = log_info_callback
|
||||
|
||||
async def main_tick(self):
|
||||
if self.initiated_connect:
|
||||
await self.connect()
|
||||
self.initiated_connect = False
|
||||
|
||||
if self.connected:
|
||||
try:
|
||||
self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive.
|
||||
except (ProcessError, MemoryReadError, WinAPIError):
|
||||
msg = (f"Error reading game memory! (Did the game crash?)\n"
|
||||
f"Please close all open windows and reopen the Jak and Daxter Client "
|
||||
f"from the Archipelago Launcher.\n"
|
||||
f"If the game and compiler do not restart automatically, please follow these steps:\n"
|
||||
f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n"
|
||||
f" Then click Advanced > Play in Debug Mode.\n"
|
||||
f" Then click Advanced > Open REPL.\n"
|
||||
f" Then close and reopen the Jak and Daxter Client from the Archipelago Launcher.")
|
||||
self.log_error(logger, msg)
|
||||
self.connected = False
|
||||
else:
|
||||
return
|
||||
|
||||
if self.connected:
|
||||
|
||||
# Save some state variables temporarily.
|
||||
old_deathlink_enabled = self.deathlink_enabled
|
||||
|
||||
# Read the memory address to check the state of the game.
|
||||
self.read_memory()
|
||||
|
||||
# Checked Locations in game. Handle the entire outbox every tick until we're up to speed.
|
||||
if len(self.location_outbox) > self.outbox_index:
|
||||
self.inform_checked_location(self.location_outbox)
|
||||
self.save_data()
|
||||
self.outbox_index += 1
|
||||
|
||||
if self.finished_game:
|
||||
self.inform_finished_game()
|
||||
|
||||
if old_deathlink_enabled != self.deathlink_enabled:
|
||||
self.inform_toggled_deathlink()
|
||||
logger.debug("Toggled DeathLink " + ("ON" if self.deathlink_enabled else "OFF"))
|
||||
|
||||
if self.send_deathlink:
|
||||
self.inform_died()
|
||||
|
||||
if self.orbs_paid > 0:
|
||||
self.inform_traded_orbs(self.orbs_paid)
|
||||
self.orbs_paid = 0
|
||||
|
||||
async def connect(self):
|
||||
try:
|
||||
self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel
|
||||
logger.debug("Found the gk process: " + str(self.gk_process.process_id))
|
||||
except ProcessNotFound:
|
||||
self.log_error(logger, "Could not find the game process.")
|
||||
self.connected = False
|
||||
return
|
||||
|
||||
# If we don't find the marker in the first loaded module, we've failed.
|
||||
modules = list(self.gk_process.list_modules())
|
||||
marker_address = pattern.pattern_scan_module(self.gk_process.process_handle, modules[0], self.marker)
|
||||
if marker_address:
|
||||
# At this address is another address that contains the struct we're looking for: the game's state.
|
||||
# From here we need to add the length in bytes for the marker and 4 bytes of padding,
|
||||
# and the struct address is 8 bytes long (it's an uint64).
|
||||
goal_pointer = marker_address + len(self.marker) + 4
|
||||
self.goal_address = int.from_bytes(self.gk_process.read_bytes(goal_pointer, sizeof_uint64),
|
||||
byteorder="little",
|
||||
signed=False)
|
||||
logger.debug("Found the archipelago memory address: " + str(self.goal_address))
|
||||
await self.verify_memory_version()
|
||||
else:
|
||||
self.log_error(logger, "Could not find the Archipelago marker address!")
|
||||
self.connected = False
|
||||
|
||||
async def verify_memory_version(self):
|
||||
if self.goal_address is None:
|
||||
self.log_error(logger, "Could not find the Archipelago memory address!")
|
||||
self.connected = False
|
||||
return
|
||||
|
||||
memory_version: int | None = None
|
||||
try:
|
||||
memory_version = self.read_goal_address(memory_version_offset, sizeof_uint32)
|
||||
if memory_version == expected_memory_version:
|
||||
self.log_success(logger, "The Memory Reader is ready!")
|
||||
self.connected = True
|
||||
else:
|
||||
raise MemoryReadError(memory_version_offset, sizeof_uint32)
|
||||
except (ProcessError, MemoryReadError, WinAPIError):
|
||||
if memory_version is None:
|
||||
msg = (f"Could not find a version number in the OpenGOAL memory structure!\n"
|
||||
f" Expected Version: {str(expected_memory_version)}\n"
|
||||
f" Found Version: {str(memory_version)}\n"
|
||||
f"Please follow these steps:\n"
|
||||
f" If the game is running, try entering '/memr connect' in the client.\n"
|
||||
f" You should see 'The Memory Reader is ready!'\n"
|
||||
f" If that did not work, or the game is not running, run the OpenGOAL Launcher.\n"
|
||||
f" Click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n"
|
||||
f" Then click Advanced > Play in Debug Mode.\n"
|
||||
f" Try entering '/memr connect' in the client again.")
|
||||
else:
|
||||
msg = (f"The OpenGOAL memory structure is incompatible with the current Archipelago client!\n"
|
||||
f" Expected Version: {str(expected_memory_version)}\n"
|
||||
f" Found Version: {str(memory_version)}\n"
|
||||
f"Please follow these steps:\n"
|
||||
f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n"
|
||||
f" Click Update (if one is available).\n"
|
||||
f" Click Advanced > Compile. When this is done, click Continue.\n"
|
||||
f" Click Versions and verify the latest version is marked 'Active'.\n"
|
||||
f" Close all launchers, games, clients, and console windows, then restart Archipelago.")
|
||||
self.log_error(logger, msg)
|
||||
self.connected = False
|
||||
|
||||
async def print_status(self):
|
||||
proc_id = str(self.gk_process.process_id) if self.gk_process else "None"
|
||||
last_loc = str(self.location_outbox[self.outbox_index - 1] if self.outbox_index else "None")
|
||||
msg = (f"Memory Reader Status:\n"
|
||||
f" Game process ID: {proc_id}\n"
|
||||
f" Game state memory address: {str(self.goal_address)}\n"
|
||||
f" Last location checked: {last_loc}")
|
||||
await self.verify_memory_version()
|
||||
self.log_info(logger, msg)
|
||||
|
||||
def read_memory(self) -> list[int]:
|
||||
try:
|
||||
# Need to grab these first and convert to floats, see below.
|
||||
citizen_orb_amount = self.read_goal_address(citizen_orb_amount_offset, sizeof_float)
|
||||
oracle_orb_amount = self.read_goal_address(oracle_orb_amount_offset, sizeof_float)
|
||||
|
||||
next_cell_index = self.read_goal_address(next_cell_index_offset, sizeof_uint64)
|
||||
for k in range(0, next_cell_index):
|
||||
next_cell = self.read_goal_address(cells_checked_offset + (k * sizeof_uint32), sizeof_uint32)
|
||||
cell_ap_id = cells.to_ap_id(next_cell)
|
||||
if cell_ap_id not in self.location_outbox:
|
||||
self.location_outbox.append(cell_ap_id)
|
||||
logger.debug("Checked power cell: " + str(next_cell))
|
||||
|
||||
# If orbsanity is ON and next_cell is one of the traders or oracles, then run a callback
|
||||
# to add their amount to the DataStorage value holding our current orb trade total.
|
||||
if next_cell in {11, 12, 31, 32, 33, 96, 97, 98, 99}:
|
||||
citizen_orb_amount = as_float(citizen_orb_amount)
|
||||
self.orbs_paid += citizen_orb_amount
|
||||
logger.debug(f"Traded {citizen_orb_amount} orbs!")
|
||||
|
||||
if next_cell in {13, 14, 34, 35, 100, 101}:
|
||||
oracle_orb_amount = as_float(oracle_orb_amount)
|
||||
self.orbs_paid += oracle_orb_amount
|
||||
logger.debug(f"Traded {oracle_orb_amount} orbs!")
|
||||
|
||||
next_buzzer_index = self.read_goal_address(next_buzzer_index_offset, sizeof_uint64)
|
||||
for k in range(0, next_buzzer_index):
|
||||
next_buzzer = self.read_goal_address(buzzers_checked_offset + (k * sizeof_uint32), sizeof_uint32)
|
||||
buzzer_ap_id = flies.to_ap_id(next_buzzer)
|
||||
if buzzer_ap_id not in self.location_outbox:
|
||||
self.location_outbox.append(buzzer_ap_id)
|
||||
logger.debug("Checked scout fly: " + str(next_buzzer))
|
||||
|
||||
next_special_index = self.read_goal_address(next_special_index_offset, sizeof_uint64)
|
||||
for k in range(0, next_special_index):
|
||||
next_special = self.read_goal_address(specials_checked_offset + (k * sizeof_uint32), sizeof_uint32)
|
||||
special_ap_id = specials.to_ap_id(next_special)
|
||||
if special_ap_id not in self.location_outbox:
|
||||
self.location_outbox.append(special_ap_id)
|
||||
logger.debug("Checked special: " + str(next_special))
|
||||
|
||||
death_count = self.read_goal_address(death_count_offset, sizeof_uint32)
|
||||
death_cause = self.read_goal_address(death_cause_offset, sizeof_uint8)
|
||||
if death_count > self.death_count:
|
||||
self.cause_of_death = autopsy(death_cause) # The way he names his variables? Wack!
|
||||
self.send_deathlink = True
|
||||
self.death_count += 1
|
||||
|
||||
# Listen for any changes to this setting.
|
||||
deathlink_flag = self.read_goal_address(deathlink_enabled_offset, sizeof_uint8)
|
||||
self.deathlink_enabled = bool(deathlink_flag)
|
||||
|
||||
next_cache_index = self.read_goal_address(next_orb_cache_index_offset, sizeof_uint64)
|
||||
for k in range(0, next_cache_index):
|
||||
next_cache = self.read_goal_address(orb_caches_checked_offset + (k * sizeof_uint32), sizeof_uint32)
|
||||
cache_ap_id = caches.to_ap_id(next_cache)
|
||||
if cache_ap_id not in self.location_outbox:
|
||||
self.location_outbox.append(cache_ap_id)
|
||||
logger.debug("Checked orb cache: " + str(next_cache))
|
||||
|
||||
# Listen for any changes to this setting.
|
||||
# moverando_flag = self.read_goal_address(moverando_enabled_offset, sizeof_uint8)
|
||||
# self.moverando_enabled = bool(moverando_flag)
|
||||
|
||||
orbsanity_option = self.read_goal_address(orbsanity_option_offset, sizeof_uint8)
|
||||
bundle_size = self.read_goal_address(orbsanity_bundle_offset, sizeof_uint32)
|
||||
self.orbsanity_enabled = orbsanity_option > 0
|
||||
|
||||
# Per Level Orbsanity option. Only need to do this loop if we chose this setting.
|
||||
if orbsanity_option == 1:
|
||||
for level in range(0, 16):
|
||||
collected_bundles = self.read_goal_address(collected_bundle_offset + (level * sizeof_uint32),
|
||||
sizeof_uint32)
|
||||
|
||||
# Count up from the first bundle, by bundle size, until you reach the latest collected bundle.
|
||||
# e.g. {25, 50, 75, 100, 125...}
|
||||
if collected_bundles > 0:
|
||||
for bundle in range(bundle_size,
|
||||
bundle_size + collected_bundles, # Range max is non-inclusive.
|
||||
bundle_size):
|
||||
|
||||
bundle_ap_id = orbs.to_ap_id(orbs.find_address(level, bundle, bundle_size))
|
||||
if bundle_ap_id not in self.location_outbox:
|
||||
self.location_outbox.append(bundle_ap_id)
|
||||
logger.debug(f"Checked orb bundle: L{level} {bundle}")
|
||||
|
||||
# Global Orbsanity option. Index 16 refers to all orbs found regardless of level.
|
||||
if orbsanity_option == 2:
|
||||
collected_bundles = self.read_goal_address(collected_bundle_offset + (16 * sizeof_uint32),
|
||||
sizeof_uint32)
|
||||
if collected_bundles > 0:
|
||||
for bundle in range(bundle_size,
|
||||
bundle_size + collected_bundles, # Range max is non-inclusive.
|
||||
bundle_size):
|
||||
|
||||
bundle_ap_id = orbs.to_ap_id(orbs.find_address(16, bundle, bundle_size))
|
||||
if bundle_ap_id not in self.location_outbox:
|
||||
self.location_outbox.append(bundle_ap_id)
|
||||
logger.debug(f"Checked orb bundle: G {bundle}")
|
||||
|
||||
completed = self.read_goal_address(completed_offset, sizeof_uint8)
|
||||
if completed > 0 and not self.finished_game:
|
||||
self.finished_game = True
|
||||
self.log_success(logger, "Congratulations! You finished the game!")
|
||||
|
||||
except (ProcessError, MemoryReadError, WinAPIError):
|
||||
msg = (f"Error reading game memory! (Did the game crash?)\n"
|
||||
f"Please close all open windows and reopen the Jak and Daxter Client "
|
||||
f"from the Archipelago Launcher.\n"
|
||||
f"If the game and compiler do not restart automatically, please follow these steps:\n"
|
||||
f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n"
|
||||
f" Then click Advanced > Play in Debug Mode.\n"
|
||||
f" Then click Advanced > Open REPL.\n"
|
||||
f" Then close and reopen the Jak and Daxter Client from the Archipelago Launcher.")
|
||||
self.log_error(logger, msg)
|
||||
self.connected = False
|
||||
|
||||
return self.location_outbox
|
||||
|
||||
def read_goal_address(self, offset: int, length: int) -> int:
|
||||
return int.from_bytes(
|
||||
self.gk_process.read_bytes(self.goal_address + offset, length),
|
||||
byteorder="little",
|
||||
signed=False)
|
||||
|
||||
def save_data(self):
|
||||
with open("jakanddaxter_location_outbox.json", "w+") as f:
|
||||
dump = {
|
||||
"outbox_index": self.outbox_index,
|
||||
"location_outbox": self.location_outbox
|
||||
}
|
||||
json.dump(dump, f, indent=4)
|
||||
|
||||
def load_data(self):
|
||||
try:
|
||||
with open("jakanddaxter_location_outbox.json", "r") as f:
|
||||
load = json.load(f)
|
||||
self.outbox_index = load["outbox_index"]
|
||||
self.location_outbox = load["location_outbox"]
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
527
worlds/jakanddaxter/agents/repl_client.py
Normal file
527
worlds/jakanddaxter/agents/repl_client.py
Normal file
@@ -0,0 +1,527 @@
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
import time
|
||||
import struct
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from queue import Queue
|
||||
from typing import Callable
|
||||
|
||||
import pymem
|
||||
from pymem.exception import ProcessNotFound, ProcessError
|
||||
|
||||
import asyncio
|
||||
from asyncio import StreamReader, StreamWriter, Lock
|
||||
|
||||
from NetUtils import NetworkItem
|
||||
from ..game_id import jak1_id, jak1_max
|
||||
from ..items import item_table, trap_item_table
|
||||
from ..locs import (
|
||||
orb_locations as orbs,
|
||||
cell_locations as cells,
|
||||
scout_locations as flies,
|
||||
special_locations as specials,
|
||||
orb_cache_locations as caches)
|
||||
|
||||
|
||||
logger = logging.getLogger("ReplClient")
|
||||
|
||||
|
||||
@dataclass
|
||||
class JsonMessageData:
|
||||
my_item_name: str | None = None
|
||||
my_item_finder: str | None = None
|
||||
their_item_name: str | None = None
|
||||
their_item_owner: str | None = None
|
||||
|
||||
|
||||
ALLOWED_CHARACTERS = frozenset({
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
|
||||
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
|
||||
"U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d",
|
||||
"e", "f", "g", "h", "i", "j", "k", "l", "m", "n",
|
||||
"o", "p", "q", "r", "s", "t", "u", "v", "w", "x",
|
||||
"y", "z", " ", "!", ":", ",", ".", "/", "?", "-",
|
||||
"=", "+", "'", "(", ")", "\""
|
||||
})
|
||||
|
||||
|
||||
class JakAndDaxterReplClient:
|
||||
ip: str
|
||||
port: int
|
||||
reader: StreamReader
|
||||
writer: StreamWriter
|
||||
lock: Lock
|
||||
connected: bool = False
|
||||
initiated_connect: bool = False # Signals when user tells us to try reconnecting.
|
||||
received_deathlink: bool = False
|
||||
balanced_orbs: bool = False
|
||||
|
||||
# Variables to handle the title screen and initial game connection.
|
||||
initial_item_count = -1 # Brand new games have 0 items, so initialize this to -1.
|
||||
received_initial_items = False
|
||||
processed_initial_items = False
|
||||
|
||||
# The REPL client needs the REPL/compiler process running, but that process
|
||||
# also needs the game running. Therefore, the REPL client needs both running.
|
||||
gk_process: pymem.process = None
|
||||
goalc_process: pymem.process = None
|
||||
|
||||
item_inbox: dict[int, NetworkItem] = {}
|
||||
inbox_index = 0
|
||||
json_message_queue: Queue[JsonMessageData] = queue.Queue()
|
||||
|
||||
# Logging callbacks
|
||||
# These will write to the provided logger, as well as the Client GUI with color markup.
|
||||
log_error: Callable # Red
|
||||
log_warn: Callable # Orange
|
||||
log_success: Callable # Green
|
||||
log_info: Callable # White (default)
|
||||
|
||||
def __init__(self,
|
||||
log_error_callback: Callable,
|
||||
log_warn_callback: Callable,
|
||||
log_success_callback: Callable,
|
||||
log_info_callback: Callable,
|
||||
ip: str = "127.0.0.1",
|
||||
port: int = 8181):
|
||||
self.ip = ip
|
||||
self.port = port
|
||||
self.lock = asyncio.Lock()
|
||||
self.log_error = log_error_callback
|
||||
self.log_warn = log_warn_callback
|
||||
self.log_success = log_success_callback
|
||||
self.log_info = log_info_callback
|
||||
|
||||
async def main_tick(self):
|
||||
if self.initiated_connect:
|
||||
await self.connect()
|
||||
self.initiated_connect = False
|
||||
|
||||
if self.connected:
|
||||
try:
|
||||
self.gk_process.read_bool(self.gk_process.base_address) # Ping to see if it's alive.
|
||||
except ProcessError:
|
||||
msg = (f"Error reading game memory! (Did the game crash?)\n"
|
||||
f"Please close all open windows and reopen the Jak and Daxter Client "
|
||||
f"from the Archipelago Launcher.\n"
|
||||
f"If the game and compiler do not restart automatically, please follow these steps:\n"
|
||||
f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n"
|
||||
f" Then click Advanced > Play in Debug Mode.\n"
|
||||
f" Then click Advanced > Open REPL.\n"
|
||||
f" Then close and reopen the Jak and Daxter Client from the Archipelago Launcher.")
|
||||
self.log_error(logger, msg)
|
||||
self.connected = False
|
||||
try:
|
||||
self.goalc_process.read_bool(self.goalc_process.base_address) # Ping to see if it's alive.
|
||||
except ProcessError:
|
||||
msg = (f"Error sending data to compiler! (Did the compiler crash?)\n"
|
||||
f"Please close all open windows and reopen the Jak and Daxter Client "
|
||||
f"from the Archipelago Launcher.\n"
|
||||
f"If the game and compiler do not restart automatically, please follow these steps:\n"
|
||||
f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n"
|
||||
f" Then click Advanced > Play in Debug Mode.\n"
|
||||
f" Then click Advanced > Open REPL.\n"
|
||||
f" Then close and reopen the Jak and Daxter Client from the Archipelago Launcher.")
|
||||
self.log_error(logger, msg)
|
||||
self.connected = False
|
||||
else:
|
||||
return
|
||||
|
||||
# When connecting the game to the AP server on the title screen, we may be processing items from starting
|
||||
# inventory or items received in an async game. Once we have caught up to the initial count, tell the player
|
||||
# that we are ready to start. New items may even come in during the title screen, so if we go over the count,
|
||||
# we should still send the ready signal.
|
||||
if not self.processed_initial_items:
|
||||
if self.inbox_index >= self.initial_item_count >= 0:
|
||||
self.processed_initial_items = True
|
||||
await self.send_connection_status("ready")
|
||||
|
||||
# Receive Items from AP. Handle 1 item per tick.
|
||||
if len(self.item_inbox) > self.inbox_index:
|
||||
await self.receive_item()
|
||||
await self.save_data()
|
||||
self.inbox_index += 1
|
||||
|
||||
if self.received_deathlink:
|
||||
await self.receive_deathlink()
|
||||
self.received_deathlink = False
|
||||
|
||||
# Progressively empty the queue during each tick
|
||||
# if text messages happen to be too slow we could pool dequeuing here,
|
||||
# but it'd slow down the ItemReceived message during release
|
||||
if not self.json_message_queue.empty():
|
||||
json_txt_data = self.json_message_queue.get_nowait()
|
||||
await self.write_game_text(json_txt_data)
|
||||
|
||||
# This helper function formats and sends `form` as a command to the REPL.
|
||||
# ALL commands to the REPL should be sent using this function.
|
||||
async def send_form(self, form: str, print_ok: bool = True) -> bool:
|
||||
header = struct.pack("<II", len(form), 10)
|
||||
async with self.lock:
|
||||
self.writer.write(header + form.encode())
|
||||
await self.writer.drain()
|
||||
|
||||
response_data = await self.reader.read(1024)
|
||||
response = response_data.decode()
|
||||
|
||||
if "OK!" in response:
|
||||
if print_ok:
|
||||
logger.debug(response)
|
||||
return True
|
||||
else:
|
||||
self.log_error(logger, f"Unexpected response from REPL: {response}")
|
||||
return False
|
||||
|
||||
async def connect(self):
|
||||
try:
|
||||
self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel
|
||||
logger.debug("Found the gk process: " + str(self.gk_process.process_id))
|
||||
except ProcessNotFound:
|
||||
self.log_error(logger, "Could not find the game process.")
|
||||
return
|
||||
|
||||
try:
|
||||
self.goalc_process = pymem.Pymem("goalc.exe") # The GOAL Compiler and REPL
|
||||
logger.debug("Found the goalc process: " + str(self.goalc_process.process_id))
|
||||
except ProcessNotFound:
|
||||
self.log_error(logger, "Could not find the compiler process.")
|
||||
return
|
||||
|
||||
try:
|
||||
self.reader, self.writer = await asyncio.open_connection(self.ip, self.port)
|
||||
time.sleep(1)
|
||||
connect_data = await self.reader.read(1024)
|
||||
welcome_message = connect_data.decode()
|
||||
|
||||
# Should be the OpenGOAL welcome message (ignore version number).
|
||||
if "Connected to OpenGOAL" and "nREPL!" in welcome_message:
|
||||
logger.debug(welcome_message)
|
||||
else:
|
||||
self.log_error(logger,
|
||||
f"Unable to connect to REPL websocket: unexpected welcome message \"{welcome_message}\"")
|
||||
except ConnectionRefusedError as e:
|
||||
self.log_error(logger, f"Unable to connect to REPL websocket: {e.strerror}")
|
||||
return
|
||||
|
||||
ok_count = 0
|
||||
if self.reader and self.writer:
|
||||
|
||||
# Have the REPL listen to the game's internal websocket.
|
||||
if await self.send_form("(lt)", print_ok=False):
|
||||
ok_count += 1
|
||||
|
||||
# Show this visual cue when compilation is started.
|
||||
# It's the version number of the OpenGOAL Compiler.
|
||||
if await self.send_form("(set! *debug-segment* #t)", print_ok=False):
|
||||
ok_count += 1
|
||||
|
||||
# Start compilation. This is blocking, so nothing will happen until the REPL is done.
|
||||
if await self.send_form("(mi)", print_ok=False):
|
||||
ok_count += 1
|
||||
|
||||
# Play this audio cue when compilation is complete.
|
||||
# It's the sound you hear when you press START + START to close the Options menu.
|
||||
if await self.send_form("(dotimes (i 1) "
|
||||
"(sound-play-by-name "
|
||||
"(static-sound-name \"menu-close\") "
|
||||
"(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False):
|
||||
ok_count += 1
|
||||
|
||||
# Disable cheat-mode and debug (close the visual cues).
|
||||
if await self.send_form("(set! *debug-segment* #f)", print_ok=False):
|
||||
ok_count += 1
|
||||
|
||||
if await self.send_form("(set! *cheat-mode* #f)", print_ok=False):
|
||||
ok_count += 1
|
||||
|
||||
# Run the retail game start sequence (while still connected with REPL).
|
||||
if await self.send_form("(start \'play (get-continue-by-name *game-info* \"title-start\"))"):
|
||||
ok_count += 1
|
||||
|
||||
# Now wait until we see the success message... 7 times.
|
||||
if ok_count == 7:
|
||||
self.connected = True
|
||||
else:
|
||||
self.connected = False
|
||||
|
||||
if self.connected:
|
||||
self.log_success(logger, "The REPL is ready!")
|
||||
|
||||
async def print_status(self):
|
||||
gc_proc_id = str(self.goalc_process.process_id) if self.goalc_process else "None"
|
||||
gk_proc_id = str(self.gk_process.process_id) if self.gk_process else "None"
|
||||
msg = (f"REPL Status:\n"
|
||||
f" REPL process ID: {gc_proc_id}\n"
|
||||
f" Game process ID: {gk_proc_id}\n")
|
||||
try:
|
||||
if self.reader and self.writer:
|
||||
addr = self.writer.get_extra_info("peername")
|
||||
addr = str(addr) if addr else "None"
|
||||
msg += f" Game websocket: {addr}\n"
|
||||
await self.send_form("(dotimes (i 1) "
|
||||
"(sound-play-by-name "
|
||||
"(static-sound-name \"menu-close\") "
|
||||
"(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False)
|
||||
except ConnectionResetError:
|
||||
msg += f" Connection to the game was lost or reset!"
|
||||
last_item = str(getattr(self.item_inbox[self.inbox_index], "item")) if self.inbox_index else "None"
|
||||
msg += f" Last item received: {last_item}\n"
|
||||
msg += f" Did you hear the success audio cue?"
|
||||
self.log_info(logger, msg)
|
||||
|
||||
# To properly display in-game text:
|
||||
# - It must be a valid character from the ALLOWED_CHARACTERS list.
|
||||
# - All lowercase letters must be uppercase.
|
||||
# - It must be wrapped in double quotes (for the REPL command).
|
||||
# - Apostrophes must be handled specially - GOAL uses invisible ASCII character 0x12.
|
||||
# I also only allotted 32 bytes to each string in OpenGOAL, so we must truncate.
|
||||
@staticmethod
|
||||
def sanitize_game_text(text: str) -> str:
|
||||
result = "".join([c if c in ALLOWED_CHARACTERS else "?" for c in text[:32]]).upper()
|
||||
result = result.replace("'", "\\c12")
|
||||
return f"\"{result}\""
|
||||
|
||||
# Like sanitize_game_text, but the settings file will NOT allow any whitespace in the slot_name or slot_seed data.
|
||||
# And don't replace any chars with "?" for good measure.
|
||||
@staticmethod
|
||||
def sanitize_file_text(text: str) -> str:
|
||||
allowed_chars_no_extras = ALLOWED_CHARACTERS - {" ", "'", "(", ")", "\""}
|
||||
result = "".join([c if c in allowed_chars_no_extras else "" for c in text[:16]]).upper()
|
||||
return f"\"{result}\""
|
||||
|
||||
# Pushes a JsonMessageData object to the json message queue to be processed during the repl main_tick
|
||||
def queue_game_text(self, my_item_name, my_item_finder, their_item_name, their_item_owner):
|
||||
self.json_message_queue.put(JsonMessageData(my_item_name, my_item_finder, their_item_name, their_item_owner))
|
||||
|
||||
# OpenGOAL can handle both its own string datatype and C-like character pointers (charp).
|
||||
async def write_game_text(self, data: JsonMessageData):
|
||||
logger.debug(f"Sending info to the in-game messenger!")
|
||||
body = ""
|
||||
if data.my_item_name and data.my_item_finder:
|
||||
body += (f" (append-messages (-> *ap-messenger* 0) \'recv "
|
||||
f" {self.sanitize_game_text(data.my_item_name)} "
|
||||
f" {self.sanitize_game_text(data.my_item_finder)})")
|
||||
if data.their_item_name and data.their_item_owner:
|
||||
body += (f" (append-messages (-> *ap-messenger* 0) \'sent "
|
||||
f" {self.sanitize_game_text(data.their_item_name)} "
|
||||
f" {self.sanitize_game_text(data.their_item_owner)})")
|
||||
await self.send_form(f"(begin {body} (none))", print_ok=False)
|
||||
|
||||
async def receive_item(self):
|
||||
ap_id = getattr(self.item_inbox[self.inbox_index], "item")
|
||||
|
||||
# Determine the type of item to receive.
|
||||
if ap_id in range(jak1_id, jak1_id + flies.fly_offset):
|
||||
await self.receive_power_cell(ap_id)
|
||||
elif ap_id in range(jak1_id + flies.fly_offset, jak1_id + specials.special_offset):
|
||||
await self.receive_scout_fly(ap_id)
|
||||
elif ap_id in range(jak1_id + specials.special_offset, jak1_id + caches.orb_cache_offset):
|
||||
await self.receive_special(ap_id)
|
||||
elif ap_id in range(jak1_id + caches.orb_cache_offset, jak1_id + orbs.orb_offset):
|
||||
await self.receive_move(ap_id)
|
||||
elif ap_id in range(jak1_id + orbs.orb_offset, jak1_max - max(trap_item_table)):
|
||||
await self.receive_precursor_orb(ap_id) # Ponder the orbs.
|
||||
elif ap_id in range(jak1_max - max(trap_item_table), jak1_max):
|
||||
await self.receive_trap(ap_id)
|
||||
elif ap_id == jak1_max:
|
||||
await self.receive_green_eco() # Ponder why I chose to do ID's this way.
|
||||
else:
|
||||
self.log_error(logger, f"Tried to receive item with unknown AP ID {ap_id}!")
|
||||
|
||||
async def receive_power_cell(self, ap_id: int) -> bool:
|
||||
cell_id = cells.to_game_id(ap_id)
|
||||
ok = await self.send_form("(send-event "
|
||||
"*target* \'get-archipelago "
|
||||
"(pickup-type fuel-cell) "
|
||||
"(the float " + str(cell_id) + "))")
|
||||
if ok:
|
||||
logger.debug(f"Received a Power Cell!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive a Power Cell!")
|
||||
return ok
|
||||
|
||||
async def receive_scout_fly(self, ap_id: int) -> bool:
|
||||
fly_id = flies.to_game_id(ap_id)
|
||||
ok = await self.send_form("(send-event "
|
||||
"*target* \'get-archipelago "
|
||||
"(pickup-type buzzer) "
|
||||
"(the float " + str(fly_id) + "))")
|
||||
if ok:
|
||||
logger.debug(f"Received a {item_table[ap_id]}!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive a {item_table[ap_id]}!")
|
||||
return ok
|
||||
|
||||
async def receive_special(self, ap_id: int) -> bool:
|
||||
special_id = specials.to_game_id(ap_id)
|
||||
ok = await self.send_form("(send-event "
|
||||
"*target* \'get-archipelago "
|
||||
"(pickup-type ap-special) "
|
||||
"(the float " + str(special_id) + "))")
|
||||
if ok:
|
||||
logger.debug(f"Received special unlock {item_table[ap_id]}!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive special unlock {item_table[ap_id]}!")
|
||||
return ok
|
||||
|
||||
async def receive_move(self, ap_id: int) -> bool:
|
||||
move_id = caches.to_game_id(ap_id)
|
||||
ok = await self.send_form("(send-event "
|
||||
"*target* \'get-archipelago "
|
||||
"(pickup-type ap-move) "
|
||||
"(the float " + str(move_id) + "))")
|
||||
if ok:
|
||||
logger.debug(f"Received the ability to {item_table[ap_id]}!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive the ability to {item_table[ap_id]}!")
|
||||
return ok
|
||||
|
||||
async def receive_precursor_orb(self, ap_id: int) -> bool:
|
||||
orb_amount = orbs.to_game_id(ap_id)
|
||||
ok = await self.send_form("(send-event "
|
||||
"*target* \'get-archipelago "
|
||||
"(pickup-type money) "
|
||||
"(the float " + str(orb_amount) + "))")
|
||||
if ok:
|
||||
logger.debug(f"Received {orb_amount} Precursor orbs!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive {orb_amount} Precursor orbs!")
|
||||
return ok
|
||||
|
||||
async def receive_trap(self, ap_id: int) -> bool:
|
||||
trap_id = jak1_max - ap_id
|
||||
ok = await self.send_form("(send-event "
|
||||
"*target* \'get-archipelago "
|
||||
"(pickup-type ap-trap) "
|
||||
"(the float " + str(trap_id) + "))")
|
||||
if ok:
|
||||
logger.debug(f"Received a {item_table[ap_id]}!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive a {item_table[ap_id]}!")
|
||||
return ok
|
||||
|
||||
# Green eco pills are our filler item. Use the get-pickup event instead to handle being full health.
|
||||
async def receive_green_eco(self) -> bool:
|
||||
ok = await self.send_form("(send-event *target* \'get-pickup (pickup-type eco-pill) (the float 1))")
|
||||
if ok:
|
||||
logger.debug(f"Received a green eco pill!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive a green eco pill!")
|
||||
return ok
|
||||
|
||||
async def receive_deathlink(self) -> bool:
|
||||
|
||||
# Because it should at least be funny sometimes.
|
||||
death_types = ["\'death",
|
||||
"\'death",
|
||||
"\'death",
|
||||
"\'death",
|
||||
"\'endlessfall",
|
||||
"\'drown-death",
|
||||
"\'melt",
|
||||
"\'dark-eco-pool"]
|
||||
chosen_death = random.choice(death_types)
|
||||
|
||||
ok = await self.send_form("(ap-deathlink-received! " + chosen_death + ")")
|
||||
if ok:
|
||||
logger.debug(f"Received deathlink signal!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to receive deathlink signal!")
|
||||
return ok
|
||||
|
||||
async def subtract_traded_orbs(self, orb_count: int) -> bool:
|
||||
|
||||
# To protect against momentary server disconnects,
|
||||
# this should only be done once per client session.
|
||||
if not self.balanced_orbs:
|
||||
self.balanced_orbs = True
|
||||
|
||||
ok = await self.send_form(f"(-! (-> *game-info* money) (the float {orb_count}))")
|
||||
if ok:
|
||||
logger.debug(f"Subtracting {orb_count} traded orbs!")
|
||||
else:
|
||||
self.log_error(logger, f"Unable to subtract {orb_count} traded orbs!")
|
||||
return ok
|
||||
|
||||
return True
|
||||
|
||||
# OpenGOAL has a limit of 8 parameters per function. We've already hit this limit. So, define a new datatype
|
||||
# in OpenGOAL that holds all these options, instantiate the type here, and have ap-setup-options! function take
|
||||
# that instance as input.
|
||||
async def setup_options(self,
|
||||
os_option: int, os_bundle: int,
|
||||
fc_count: int, mp_count: int,
|
||||
lt_count: int, ct_amount: int,
|
||||
ot_amount: int, trap_time: int,
|
||||
goal_id: int, slot_name: str,
|
||||
slot_seed: str) -> bool:
|
||||
sanitized_name = self.sanitize_file_text(slot_name)
|
||||
sanitized_seed = self.sanitize_file_text(slot_seed)
|
||||
|
||||
# I didn't want to have to do this with floats but GOAL's compile-time vs runtime types leave me no choice.
|
||||
ok = await self.send_form(f"(ap-setup-options! (new 'static 'ap-seed-options "
|
||||
f":orbsanity-option {os_option} "
|
||||
f":orbsanity-bundle {os_bundle} "
|
||||
f":fire-canyon-unlock {fc_count}.0 "
|
||||
f":mountain-pass-unlock {mp_count}.0 "
|
||||
f":lava-tube-unlock {lt_count}.0 "
|
||||
f":citizen-orb-amount {ct_amount}.0 "
|
||||
f":oracle-orb-amount {ot_amount}.0 "
|
||||
f":trap-duration {trap_time}.0 "
|
||||
f":completion-goal {goal_id} "
|
||||
f":slot-name {sanitized_name} "
|
||||
f":slot-seed {sanitized_seed} ))")
|
||||
message = (f"Setting options: \n"
|
||||
f" orbsanity Option {os_option}, orbsanity Bundle {os_bundle}, \n"
|
||||
f" FC Cell Count {fc_count}, MP Cell Count {mp_count}, \n"
|
||||
f" LT Cell Count {lt_count}, Citizen Orb Amt {ct_amount}, \n"
|
||||
f" Oracle Orb Amt {ot_amount}, Trap Duration {trap_time}, \n"
|
||||
f" Completion GOAL {goal_id}, Slot Name {sanitized_name}, \n"
|
||||
f" Slot Seed {sanitized_seed}... ")
|
||||
if ok:
|
||||
logger.debug(message + "Success!")
|
||||
else:
|
||||
self.log_error(logger, message + "Failed!")
|
||||
|
||||
return ok
|
||||
|
||||
async def send_connection_status(self, status: str) -> bool:
|
||||
ok = await self.send_form(f"(ap-set-connection-status! (connection-status {status}))")
|
||||
if ok:
|
||||
logger.debug(f"Connection Status {status} set!")
|
||||
else:
|
||||
self.log_error(logger, f"Connection Status {status} failed to set!")
|
||||
|
||||
return ok
|
||||
|
||||
async def save_data(self):
|
||||
with open("jakanddaxter_item_inbox.json", "w+") as f:
|
||||
dump = {
|
||||
"inbox_index": self.inbox_index,
|
||||
"item_inbox": [{
|
||||
"item": self.item_inbox[k].item,
|
||||
"location": self.item_inbox[k].location,
|
||||
"player": self.item_inbox[k].player,
|
||||
"flags": self.item_inbox[k].flags
|
||||
} for k in self.item_inbox
|
||||
]
|
||||
}
|
||||
json.dump(dump, f, indent=4)
|
||||
|
||||
def load_data(self):
|
||||
try:
|
||||
with open("jakanddaxter_item_inbox.json", "r") as f:
|
||||
load = json.load(f)
|
||||
self.inbox_index = load["inbox_index"]
|
||||
self.item_inbox = {k: NetworkItem(
|
||||
item=load["item_inbox"][k]["item"],
|
||||
location=load["item_inbox"][k]["location"],
|
||||
player=load["item_inbox"][k]["player"],
|
||||
flags=load["item_inbox"][k]["flags"]
|
||||
) for k in range(0, len(load["item_inbox"]))
|
||||
}
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
600
worlds/jakanddaxter/client.py
Normal file
600
worlds/jakanddaxter/client.py
Normal file
@@ -0,0 +1,600 @@
|
||||
# Python standard libraries
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from asyncio import Task
|
||||
from datetime import datetime
|
||||
from logging import Logger
|
||||
from typing import Awaitable
|
||||
|
||||
# Misc imports
|
||||
import colorama
|
||||
import pymem
|
||||
|
||||
from pymem.exception import ProcessNotFound
|
||||
|
||||
# Archipelago imports
|
||||
import ModuleUpdate
|
||||
import Utils
|
||||
|
||||
from CommonClient import ClientCommandProcessor, CommonContext, server_loop, gui_enabled
|
||||
from NetUtils import ClientStatus
|
||||
|
||||
# Jak imports
|
||||
from .game_id import jak1_name
|
||||
from .options import EnableOrbsanity
|
||||
from .agents.memory_reader import JakAndDaxterMemoryReader
|
||||
from .agents.repl_client import JakAndDaxterReplClient
|
||||
from . import JakAndDaxterWorld
|
||||
|
||||
|
||||
ModuleUpdate.update()
|
||||
logger = logging.getLogger("JakClient")
|
||||
all_tasks: set[Task] = set()
|
||||
|
||||
|
||||
def create_task_log_exception(awaitable: Awaitable) -> asyncio.Task:
|
||||
async def _log_exception(a):
|
||||
try:
|
||||
return await a
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
finally:
|
||||
all_tasks.remove(task)
|
||||
task = asyncio.create_task(_log_exception(awaitable))
|
||||
all_tasks.add(task)
|
||||
return task
|
||||
|
||||
|
||||
class JakAndDaxterClientCommandProcessor(ClientCommandProcessor):
|
||||
ctx: "JakAndDaxterContext"
|
||||
|
||||
# The command processor is not async so long-running operations like the /repl connect command
|
||||
# (which takes 10-15 seconds to compile the game) have to be requested with user-initiated flags.
|
||||
# The flags are checked by the agents every main_tick.
|
||||
def _cmd_repl(self, *arguments: str):
|
||||
"""Sends a command to the OpenGOAL REPL. Arguments:
|
||||
- connect : connect the client to the REPL (goalc).
|
||||
- status : check internal status of the REPL."""
|
||||
if arguments:
|
||||
if arguments[0] == "connect":
|
||||
self.ctx.on_log_info(logger, "This may take a bit... Wait for the success audio cue before continuing!")
|
||||
self.ctx.repl.initiated_connect = True
|
||||
if arguments[0] == "status":
|
||||
create_task_log_exception(self.ctx.repl.print_status())
|
||||
|
||||
def _cmd_memr(self, *arguments: str):
|
||||
"""Sends a command to the Memory Reader. Arguments:
|
||||
- connect : connect the memory reader to the game process (gk).
|
||||
- status : check the internal status of the Memory Reader."""
|
||||
if arguments:
|
||||
if arguments[0] == "connect":
|
||||
self.ctx.memr.initiated_connect = True
|
||||
if arguments[0] == "status":
|
||||
create_task_log_exception(self.ctx.memr.print_status())
|
||||
|
||||
|
||||
class JakAndDaxterContext(CommonContext):
|
||||
game = jak1_name
|
||||
items_handling = 0b111 # Full item handling
|
||||
command_processor = JakAndDaxterClientCommandProcessor
|
||||
|
||||
# We'll need two agents working in tandem to handle two-way communication with the game.
|
||||
# The REPL Client will handle the server->game direction by issuing commands directly to the running game.
|
||||
# But the REPL cannot send information back to us, it only ingests information we send it.
|
||||
# Luckily OpenGOAL sets up memory addresses to write to, that AutoSplit can read from, for speedrunning.
|
||||
# We'll piggyback off this system with a Memory Reader, and that will handle the game->server direction.
|
||||
repl: JakAndDaxterReplClient
|
||||
memr: JakAndDaxterMemoryReader
|
||||
|
||||
# And two associated tasks, so we have handles on them.
|
||||
repl_task: asyncio.Task
|
||||
memr_task: asyncio.Task
|
||||
|
||||
# Storing some information for writing save slot identifiers.
|
||||
slot_seed: str
|
||||
|
||||
def __init__(self, server_address: str | None, password: str | None) -> None:
|
||||
self.repl = JakAndDaxterReplClient(self.on_log_error,
|
||||
self.on_log_warn,
|
||||
self.on_log_success,
|
||||
self.on_log_info)
|
||||
self.memr = JakAndDaxterMemoryReader(self.on_location_check,
|
||||
self.on_finish_check,
|
||||
self.on_deathlink_check,
|
||||
self.on_deathlink_toggle,
|
||||
self.on_orb_trade,
|
||||
self.on_log_error,
|
||||
self.on_log_warn,
|
||||
self.on_log_success,
|
||||
self.on_log_info)
|
||||
# self.repl.load_data()
|
||||
# self.memr.load_data()
|
||||
super().__init__(server_address, password)
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager
|
||||
|
||||
class JakAndDaxterManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Jak and Daxter ArchipelaGOAL Client"
|
||||
|
||||
self.ui = JakAndDaxterManager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(JakAndDaxterContext, self).server_auth(password_requested)
|
||||
await self.get_username()
|
||||
self.tags = set()
|
||||
await self.send_connect()
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
|
||||
if cmd == "RoomInfo":
|
||||
self.slot_seed = args["seed_name"]
|
||||
|
||||
if cmd == "Connected":
|
||||
slot_data = args["slot_data"]
|
||||
orbsanity_option = slot_data["enable_orbsanity"]
|
||||
if orbsanity_option == EnableOrbsanity.option_per_level:
|
||||
orbsanity_bundle = slot_data["level_orbsanity_bundle_size"]
|
||||
elif orbsanity_option == EnableOrbsanity.option_global:
|
||||
orbsanity_bundle = slot_data["global_orbsanity_bundle_size"]
|
||||
else:
|
||||
orbsanity_bundle = 1
|
||||
|
||||
# Connected packet is unaware of starting inventory or if player is returning to an existing game.
|
||||
# Set initial_item_count to 0, see below comments for more info.
|
||||
if not self.repl.received_initial_items and self.repl.initial_item_count < 0:
|
||||
self.repl.initial_item_count = 0
|
||||
|
||||
create_task_log_exception(
|
||||
self.repl.setup_options(orbsanity_option,
|
||||
orbsanity_bundle,
|
||||
slot_data["fire_canyon_cell_count"],
|
||||
slot_data["mountain_pass_cell_count"],
|
||||
slot_data["lava_tube_cell_count"],
|
||||
slot_data["citizen_orb_trade_amount"],
|
||||
slot_data["oracle_orb_trade_amount"],
|
||||
slot_data["trap_effect_duration"],
|
||||
slot_data["jak_completion_condition"],
|
||||
self.auth[:16], # The slot name
|
||||
self.slot_seed[:8]))
|
||||
|
||||
# Because Orbsanity and the orb traders in the game are intrinsically linked, we need the server
|
||||
# to track our trades at all times to support async play. "Retrieved" will tell us the orbs we lost,
|
||||
# while "ReceivedItems" will tell us the orbs we gained. This will give us the correct balance.
|
||||
if orbsanity_option in [EnableOrbsanity.option_per_level, EnableOrbsanity.option_global]:
|
||||
async def get_orb_balance():
|
||||
await self.send_msgs([{"cmd": "Get", "keys": [f"jakanddaxter_{self.auth}_orbs_paid"]}])
|
||||
|
||||
create_task_log_exception(get_orb_balance())
|
||||
|
||||
# Tell the server if Deathlink is enabled or disabled in the in-game options.
|
||||
# This allows us to "remember" the user's choice.
|
||||
self.on_deathlink_toggle()
|
||||
|
||||
if cmd == "Retrieved":
|
||||
if f"jakanddaxter_{self.auth}_orbs_paid" in args["keys"]:
|
||||
orbs_traded = args["keys"][f"jakanddaxter_{self.auth}_orbs_paid"]
|
||||
orbs_traded = orbs_traded if orbs_traded is not None else 0
|
||||
create_task_log_exception(self.repl.subtract_traded_orbs(orbs_traded))
|
||||
|
||||
if cmd == "ReceivedItems":
|
||||
|
||||
# If you have a starting inventory or are returning to a game where you have items, a ReceivedItems will be
|
||||
# in the same network packet as Connected. This guarantees it is the first of any ReceivedItems we process.
|
||||
# In this case, we should set the initial_item_count to > 0, even if already set to 0 by Connected, as well
|
||||
# as the received_initial_items flag. Finally, use send_connection_status to tell the player to wait while
|
||||
# we process the initial items. However, we will skip all this if there was no initial ReceivedItems and
|
||||
# the REPL indicates it already handled any initial items (0 or otherwise).
|
||||
if not self.repl.received_initial_items and not self.repl.processed_initial_items:
|
||||
self.repl.received_initial_items = True
|
||||
self.repl.initial_item_count = len(args["items"])
|
||||
create_task_log_exception(self.repl.send_connection_status("wait"))
|
||||
|
||||
# This enumeration should run on every ReceivedItems packet,
|
||||
# regardless of it being on initial connection or midway through a game.
|
||||
for index, item in enumerate(args["items"], start=args["index"]):
|
||||
logger.debug(f"index: {str(index)}, item: {str(item)}")
|
||||
self.repl.item_inbox[index] = item
|
||||
|
||||
async def json_to_game_text(self, args: dict):
|
||||
if "type" in args and args["type"] in {"ItemSend"}:
|
||||
my_item_name: str | None = None
|
||||
my_item_finder: str | None = None
|
||||
their_item_name: str | None = None
|
||||
their_item_owner: str | None = None
|
||||
|
||||
item = args["item"]
|
||||
recipient = args["receiving"]
|
||||
|
||||
# Receiving an item from the server.
|
||||
if self.slot_concerns_self(recipient):
|
||||
my_item_name = self.item_names.lookup_in_game(item.item)
|
||||
|
||||
# Did we find it, or did someone else?
|
||||
if self.slot_concerns_self(item.player):
|
||||
my_item_finder = "MYSELF"
|
||||
else:
|
||||
my_item_finder = self.player_names[item.player]
|
||||
|
||||
# Sending an item to the server.
|
||||
if self.slot_concerns_self(item.player):
|
||||
their_item_name = self.item_names.lookup_in_slot(item.item, recipient)
|
||||
|
||||
# Does it belong to us, or to someone else?
|
||||
if self.slot_concerns_self(recipient):
|
||||
their_item_owner = "MYSELF"
|
||||
else:
|
||||
their_item_owner = self.player_names[recipient]
|
||||
|
||||
# Write to game display.
|
||||
self.repl.queue_game_text(my_item_name, my_item_finder, their_item_name, their_item_owner)
|
||||
|
||||
# Even though N items come in as 1 ReceivedItems packet, there are still N PrintJson packets to process,
|
||||
# and they all arrive before the ReceivedItems packet does. Defer processing of these packets as
|
||||
# async tasks to speed up large releases of items.
|
||||
def on_print_json(self, args: dict) -> None:
|
||||
create_task_log_exception(self.json_to_game_text(args))
|
||||
super(JakAndDaxterContext, self).on_print_json(args)
|
||||
|
||||
# We need to do a little more than just use CommonClient's on_deathlink.
|
||||
def on_deathlink(self, data: dict):
|
||||
if self.memr.deathlink_enabled:
|
||||
self.repl.received_deathlink = True
|
||||
super().on_deathlink(data)
|
||||
|
||||
# We don't need an ap_inform function because check_locations solves that need.
|
||||
def on_location_check(self, location_ids: list[int]):
|
||||
create_task_log_exception(self.check_locations(location_ids))
|
||||
|
||||
# CommonClient has no finished_game function, so we will have to craft our own. TODO - Update if that changes.
|
||||
async def ap_inform_finished_game(self):
|
||||
if not self.finished_game and self.memr.finished_game:
|
||||
message = [{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]
|
||||
await self.send_msgs(message)
|
||||
self.finished_game = True
|
||||
|
||||
def on_finish_check(self):
|
||||
create_task_log_exception(self.ap_inform_finished_game())
|
||||
|
||||
# We need to do a little more than just use CommonClient's send_death.
|
||||
async def ap_inform_deathlink(self):
|
||||
if self.memr.deathlink_enabled:
|
||||
player = self.player_names[self.slot] if self.slot is not None else "Jak"
|
||||
death_text = self.memr.cause_of_death.replace("Jak", player)
|
||||
await self.send_death(death_text)
|
||||
self.on_log_warn(logger, death_text)
|
||||
|
||||
# Reset all flags, but leave the death count alone.
|
||||
self.memr.send_deathlink = False
|
||||
self.memr.cause_of_death = ""
|
||||
|
||||
def on_deathlink_check(self):
|
||||
create_task_log_exception(self.ap_inform_deathlink())
|
||||
|
||||
# We don't need an ap_inform function because update_death_link solves that need.
|
||||
def on_deathlink_toggle(self):
|
||||
create_task_log_exception(self.update_death_link(self.memr.deathlink_enabled))
|
||||
|
||||
# Orb trades are situations unique to Jak, so we have to craft our own function.
|
||||
async def ap_inform_orb_trade(self, orbs_changed: int):
|
||||
if self.memr.orbsanity_enabled:
|
||||
await self.send_msgs([{"cmd": "Set",
|
||||
"key": f"jakanddaxter_{self.auth}_orbs_paid",
|
||||
"default": 0,
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "add", "value": orbs_changed}]
|
||||
}])
|
||||
|
||||
def on_orb_trade(self, orbs_changed: int):
|
||||
create_task_log_exception(self.ap_inform_orb_trade(orbs_changed))
|
||||
|
||||
def _markup_panels(self, msg: str, c: str = None):
|
||||
color = self.jsontotextparser.color_codes[c] if c else None
|
||||
message = f"[color={color}]{msg}[/color]" if c else msg
|
||||
|
||||
self.ui.log_panels["Archipelago"].on_message_markup(message)
|
||||
self.ui.log_panels["All"].on_message_markup(message)
|
||||
|
||||
def on_log_error(self, lg: Logger, message: str):
|
||||
lg.error(message)
|
||||
if self.ui:
|
||||
self._markup_panels(message, "red")
|
||||
|
||||
def on_log_warn(self, lg: Logger, message: str):
|
||||
lg.warning(message)
|
||||
if self.ui:
|
||||
self._markup_panels(message, "orange")
|
||||
|
||||
def on_log_success(self, lg: Logger, message: str):
|
||||
lg.info(message)
|
||||
if self.ui:
|
||||
self._markup_panels(message, "green")
|
||||
|
||||
def on_log_info(self, lg: Logger, message: str):
|
||||
lg.info(message)
|
||||
if self.ui:
|
||||
self._markup_panels(message)
|
||||
|
||||
async def run_repl_loop(self):
|
||||
while True:
|
||||
await self.repl.main_tick()
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def run_memr_loop(self):
|
||||
while True:
|
||||
await self.memr.main_tick()
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
def find_root_directory(ctx: JakAndDaxterContext):
|
||||
|
||||
# The path to this file is platform-dependent.
|
||||
if Utils.is_windows:
|
||||
appdata = os.getenv("APPDATA")
|
||||
settings_path = os.path.normpath(f"{appdata}/OpenGOAL-Launcher/settings.json")
|
||||
elif Utils.is_linux:
|
||||
home = os.path.expanduser("~")
|
||||
settings_path = os.path.normpath(f"{home}/.config/OpenGOAL-Launcher/settings.json")
|
||||
elif Utils.is_macos:
|
||||
home = os.path.expanduser("~")
|
||||
settings_path = os.path.normpath(f"{home}/Library/Application Support/OpenGOAL-Launcher/settings.json")
|
||||
else:
|
||||
ctx.on_log_error(logger, f"Unknown operating system: {sys.platform}!")
|
||||
return
|
||||
|
||||
# Boilerplate messages that all error messages in this function should have.
|
||||
err_title = "Unable to locate the ArchipelaGOAL install directory"
|
||||
alt_instructions = (f"Please verify that OpenGOAL and ArchipelaGOAL are installed properly. "
|
||||
f"If the problem persists, follow these steps:\n"
|
||||
f" Run the OpenGOAL Launcher, click Jak and Daxter > Features > Mods > ArchipelaGOAL.\n"
|
||||
f" Then click Advanced > Open Game Data Folder.\n"
|
||||
f" Go up one folder, then copy this path.\n"
|
||||
f" Run the Archipelago Launcher, click Open host.yaml.\n"
|
||||
f" Set the value of 'jakanddaxter_options > root_directory' to this path.\n"
|
||||
f" Replace all backslashes in the path with forward slashes.\n"
|
||||
f" Set the value of 'jakanddaxter_options > auto_detect_root_directory' to false, "
|
||||
f"then save and close the host.yaml file.\n"
|
||||
f" Close all launchers, games, clients, and console windows, then restart Archipelago.")
|
||||
|
||||
if not os.path.exists(settings_path):
|
||||
msg = (f"{err_title}: the OpenGOAL settings file does not exist.\n"
|
||||
f"{alt_instructions}")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
|
||||
with open(settings_path, "r") as f:
|
||||
load = json.load(f)
|
||||
|
||||
jak1_installed = load["games"]["Jak 1"]["isInstalled"]
|
||||
if not jak1_installed:
|
||||
msg = (f"{err_title}: The OpenGOAL Launcher is missing a normal install of Jak 1!\n"
|
||||
f"{alt_instructions}")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
|
||||
mod_sources = load["games"]["Jak 1"]["modsInstalledVersion"]
|
||||
if mod_sources is None:
|
||||
msg = (f"{err_title}: No mod sources have been configured in the OpenGOAL Launcher!\n"
|
||||
f"{alt_instructions}")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
|
||||
# Mods can come from multiple user-defined sources.
|
||||
# Make no assumptions about where ArchipelaGOAL comes from, we should find it ourselves.
|
||||
archipelagoal_source = None
|
||||
for src in mod_sources:
|
||||
for mod in mod_sources[src].keys():
|
||||
if mod == "archipelagoal":
|
||||
archipelagoal_source = src
|
||||
# Using this file, we could verify the right version is installed, but we don't need to.
|
||||
if archipelagoal_source is None:
|
||||
msg = (f"{err_title}: The ArchipelaGOAL mod is not installed in the OpenGOAL Launcher!\n"
|
||||
f"{alt_instructions}")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
|
||||
# This is just the base OpenGOAL directory, we need to go deeper.
|
||||
base_path = load["installationDir"]
|
||||
mod_relative_path = f"features/jak1/mods/{archipelagoal_source}/archipelagoal"
|
||||
mod_path = os.path.normpath(
|
||||
os.path.join(
|
||||
os.path.normpath(base_path),
|
||||
os.path.normpath(mod_relative_path)))
|
||||
|
||||
return mod_path
|
||||
|
||||
|
||||
async def run_game(ctx: JakAndDaxterContext):
|
||||
|
||||
# These may already be running. If they are not running, try to start them.
|
||||
# TODO - Support other OS's. 1: Pymem is Windows-only. 2: on Linux, there's no ".exe."
|
||||
gk_running = False
|
||||
try:
|
||||
pymem.Pymem("gk.exe") # The GOAL Kernel
|
||||
gk_running = True
|
||||
except ProcessNotFound:
|
||||
ctx.on_log_warn(logger, "Game not running, attempting to start.")
|
||||
|
||||
goalc_running = False
|
||||
try:
|
||||
pymem.Pymem("goalc.exe") # The GOAL Compiler and REPL
|
||||
goalc_running = True
|
||||
except ProcessNotFound:
|
||||
ctx.on_log_warn(logger, "Compiler not running, attempting to start.")
|
||||
|
||||
try:
|
||||
auto_detect_root_directory = JakAndDaxterWorld.settings.auto_detect_root_directory
|
||||
if auto_detect_root_directory:
|
||||
root_path = find_root_directory(ctx)
|
||||
else:
|
||||
root_path = JakAndDaxterWorld.settings.root_directory
|
||||
|
||||
# Always trust your instincts... the user may not have entered their root_directory properly.
|
||||
# We don't have to do this check if the root directory was auto-detected.
|
||||
if "/" not in root_path:
|
||||
msg = (f"The ArchipelaGOAL root directory contains no path. (Are you missing forward slashes?)\n"
|
||||
f"Please check your host.yaml file.\n"
|
||||
f"Verify the value of 'jakanddaxter_options > root_directory' is a valid existing path, "
|
||||
f"and all backslashes have been replaced with forward slashes.")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
|
||||
# Start by checking the existence of the root directory provided in the host.yaml file (or found automatically).
|
||||
root_path = os.path.normpath(root_path)
|
||||
if not os.path.exists(root_path):
|
||||
msg = (f"The ArchipelaGOAL root directory does not exist, unable to locate the Game and Compiler.\n"
|
||||
f"Please check your host.yaml file.\n"
|
||||
f"If the value of 'jakanddaxter_options > auto_detect_root_directory' is true, verify that OpenGOAL "
|
||||
f"is installed properly.\n"
|
||||
f"If it is false, check the value of 'jakanddaxter_options > root_directory'. "
|
||||
f"Verify it is a valid existing path, and all backslashes have been replaced with forward slashes.")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
|
||||
# Now double-check the existence of the two executables we need.
|
||||
gk_path = os.path.join(root_path, "gk.exe")
|
||||
goalc_path = os.path.join(root_path, "goalc.exe")
|
||||
if not os.path.exists(gk_path) or not os.path.exists(goalc_path):
|
||||
msg = (f"The Game and Compiler could not be found in the ArchipelaGOAL root directory.\n"
|
||||
f"Please check your host.yaml file.\n"
|
||||
f"If the value of 'jakanddaxter_options > auto_detect_root_directory' is true, verify that OpenGOAL "
|
||||
f"is installed properly.\n"
|
||||
f"If it is false, check the value of 'jakanddaxter_options > root_directory'. "
|
||||
f"Verify it is a valid existing path, and all backslashes have been replaced with forward slashes.")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
|
||||
# Now we can FINALLY attempt to start the programs.
|
||||
if not gk_running:
|
||||
# Per-mod saves and settings are stored outside the ArchipelaGOAL root folder, so we have to traverse
|
||||
# a relative path, normalize it, and pass it in as an argument to gk. This folder will be created if
|
||||
# it does not exist.
|
||||
config_relative_path = "../_settings/archipelagoal"
|
||||
config_path = os.path.normpath(
|
||||
os.path.join(
|
||||
root_path,
|
||||
os.path.normpath(config_relative_path)))
|
||||
|
||||
# The game freezes if text is inadvertently selected in the stdout/stderr data streams. Let's pipe those
|
||||
# streams to a file, and let's not clutter the screen with another console window.
|
||||
timestamp = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
|
||||
log_path = os.path.join(Utils.user_path("logs"), f"JakAndDaxterGame_{timestamp}.txt")
|
||||
log_path = os.path.normpath(log_path)
|
||||
with open(log_path, "w") as log_file:
|
||||
gk_process = subprocess.Popen(
|
||||
[gk_path, "--game", "jak1",
|
||||
"--config-path", config_path,
|
||||
"--", "-v", "-boot", "-fakeiso", "-debug"],
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
|
||||
if not goalc_running:
|
||||
# For the OpenGOAL Compiler, the existence of the "data" subfolder indicates you are running it from
|
||||
# a built package. This subfolder is treated as its proj_path.
|
||||
proj_path = os.path.join(root_path, "data")
|
||||
if os.path.exists(proj_path):
|
||||
|
||||
# Look for "iso_data" path to automate away an oft-forgotten manual step of mod updates.
|
||||
# All relative paths should start from root_path and end with "jak1".
|
||||
goalc_args = []
|
||||
possible_relative_paths = {
|
||||
"../../../../../active/jak1/data/iso_data/jak1",
|
||||
"./data/iso_data/jak1",
|
||||
}
|
||||
|
||||
for iso_relative_path in possible_relative_paths:
|
||||
iso_path = os.path.normpath(
|
||||
os.path.join(
|
||||
root_path,
|
||||
os.path.normpath(iso_relative_path)))
|
||||
|
||||
if os.path.exists(iso_path):
|
||||
goalc_args = [goalc_path, "--game", "jak1", "--proj-path", proj_path, "--iso-path", iso_path]
|
||||
logger.debug(f"iso_data folder found: {iso_path}")
|
||||
break
|
||||
else:
|
||||
logger.debug(f"iso_data folder not found, continuing: {iso_path}")
|
||||
|
||||
if not goalc_args:
|
||||
msg = (f"The iso_data folder could not be found.\n"
|
||||
f"Please follow these steps:\n"
|
||||
f" Run the OpenGOAL Launcher, click Jak and Daxter > Advanced > Open Game Data Folder.\n"
|
||||
f" Copy the iso_data folder from this location.\n"
|
||||
f" Click Jak and Daxter > Features > Mods > ArchipelaGOAL > Advanced > "
|
||||
f"Open Game Data Folder.\n"
|
||||
f" Paste the iso_data folder in this location.\n"
|
||||
f" Click Advanced > Compile. When this is done, click Continue.\n"
|
||||
f" Close all launchers, games, clients, and console windows, then restart Archipelago.\n"
|
||||
f"(See Setup Guide for more details.)")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
|
||||
# The non-existence of the "data" subfolder indicates you are running it from source, as a developer.
|
||||
# The compiler will traverse upward to find the project path on its own. It will also assume your
|
||||
# "iso_data" folder is at the root of your repository. Therefore, we don't need any of those arguments.
|
||||
else:
|
||||
goalc_args = [goalc_path, "--game", "jak1"]
|
||||
|
||||
# This needs to be a new console. The REPL console cannot share a window with any other process.
|
||||
goalc_process = subprocess.Popen(goalc_args, creationflags=subprocess.CREATE_NEW_CONSOLE)
|
||||
|
||||
except AttributeError as e:
|
||||
if " " in e.args[0]:
|
||||
# YAML keys in Host.yaml ought to contain no spaces, which means this is a much more important error.
|
||||
ctx.on_log_error(logger, e.args[0])
|
||||
else:
|
||||
ctx.on_log_error(logger,
|
||||
f"Host.yaml does not contain {e.args[0]}, unable to locate game executables.")
|
||||
return
|
||||
except FileNotFoundError as e:
|
||||
msg = (f"The following path could not be found: {e.filename}\n"
|
||||
f"Please check your host.yaml file.\n"
|
||||
f"If the value of 'jakanddaxter_options > auto_detect_root_directory' is true, verify that OpenGOAL "
|
||||
f"is installed properly.\n"
|
||||
f"If it is false, check the value of 'jakanddaxter_options > root_directory'."
|
||||
f"Verify it is a valid existing path, and all backslashes have been replaced with forward slashes.")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
|
||||
# Auto connect the repl and memr agents. Sleep 5 because goalc takes just a little bit of time to load,
|
||||
# and it's not something we can await.
|
||||
ctx.on_log_info(logger, "This may take a bit... Wait for the game's title sequence before continuing!")
|
||||
await asyncio.sleep(5)
|
||||
ctx.repl.initiated_connect = True
|
||||
ctx.memr.initiated_connect = True
|
||||
|
||||
|
||||
async def main():
|
||||
Utils.init_logging("JakAndDaxterClient", exception_logger="Client")
|
||||
|
||||
ctx = JakAndDaxterContext(None, None)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||
ctx.repl_task = create_task_log_exception(ctx.run_repl_loop())
|
||||
ctx.memr_task = create_task_log_exception(ctx.run_memr_loop())
|
||||
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
|
||||
# Find and run the game (gk) and compiler/repl (goalc).
|
||||
create_task_log_exception(run_game(ctx))
|
||||
await ctx.exit_event.wait()
|
||||
await ctx.shutdown()
|
||||
|
||||
|
||||
def launch():
|
||||
# use colorama to display colored text highlighting
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
@@ -0,0 +1,258 @@
|
||||
# Jak And Daxter (ArchipelaGOAL)
|
||||
|
||||
## FAQ
|
||||
- [Where is the Options page?](#where-is-the-options-page)
|
||||
- [What does randomization do to this game?](#what-does-randomization-do-to-this-game)
|
||||
- [What are the Special Checks and how do I check them?](#what-are-the-special-checks-and-how-do-i-check-them)
|
||||
- [What are the Special Items and what do they unlock?](#what-are-the-special-items-and-what-do-they-unlock)
|
||||
- [How do I know which Special Items I have?](#how-do-i-know-which-special-items-i-have)
|
||||
- [What is the goal of the game once randomized?](#what-is-the-goal-of-the-game-once-randomized)
|
||||
- [What happens when I pick up or receive a Power Cell?](#what-happens-when-i-pick-up-or-receive-a-power-cell)
|
||||
- [What happens when I pick up or receive a Scout Fly?](#what-happens-when-i-pick-up-or-receive-a-scout-fly)
|
||||
- [How do I check the 'Free 7 Scout Flies' Power Cell?](#how-do-i-check-the-free-7-scout-flies-power-cell)
|
||||
- [What does Death Link do?](#what-does-death-link-do)
|
||||
- [What does Move Randomizer do?](#what-does-move-randomizer-do)
|
||||
- [What are the movement options in Move Randomizer?](#what-are-the-movement-options-in-move-randomizer)
|
||||
- [How do I know which moves I have?](#how-do-i-know-which-moves-i-have)
|
||||
- [What does Orbsanity do?](#what-does-orbsanity-do)
|
||||
- [What do Traps do?](#what-do-traps-do)
|
||||
- [What kind of Traps are there?](#what-kind-of-traps-are-there)
|
||||
- [I got soft-locked and cannot leave, how do I get out of here?](#i-got-soft-locked-and-cannot-leave-how-do-i-get-out-of-here)
|
||||
- [Why did I get an Option Error when generating a seed, and how do I fix it?](#why-did-i-get-an-option-error-when-generating-a-seed-and-how-do-i-fix-it)
|
||||
- [How do I check my player options in-game?](#how-do-i-check-my-player-options-in-game)
|
||||
- [How does the HUD work?](#how-does-the-hud-work)
|
||||
- [I think I found a bug, where should I report it?](#i-think-i-found-a-bug-where-should-i-report-it)
|
||||
|
||||
## Where is the options page
|
||||
|
||||
The [Player Options Page](../player-options) for this game contains all the options you need to configure and export
|
||||
a config file.
|
||||
|
||||
At this time, there are several caveats and restrictions:
|
||||
- Power Cells and Scout Flies are **always** randomized.
|
||||
- **All** the traders in the game become in-logic checks **if and only if** you have enough Orbs to pay all of them at once.
|
||||
- This is to prevent hard locks, where an item required for progression is locked behind a trade you can't afford because you spent the orbs elsewhere.
|
||||
- By default, that total is 1530.
|
||||
|
||||
## What does randomization do to this game
|
||||
The game now contains the following Location checks:
|
||||
- All 101 Power Cells
|
||||
- All 112 Scout Flies
|
||||
- All 14 Orb Caches (collect every orb in the cache and let it close)
|
||||
|
||||
These may contain Items for different games, as well as different Items from within Jak and Daxter.
|
||||
Additionally, several special checks and corresponding items have been added that are required to complete the game.
|
||||
|
||||
## What are the special checks and how do I check them
|
||||
| Check Name | How To Check |
|
||||
|------------------------|------------------------------------------------------------------------------|
|
||||
| Fisherman's Boat | Complete the fishing minigame in Forbidden Jungle |
|
||||
| Jungle Elevator | Collect the power cell at the top of the temple in Forbidden Jungle |
|
||||
| Blue Eco Switch | Collect the power cell on the blue vent switch in Forbidden Jungle |
|
||||
| Flut Flut | Push the egg off the cliff in Sentinel Beach and talk to the bird lady |
|
||||
| Warrior's Pontoons | Talk to the Warrior in Rock Village once (you do NOT have to trade with him) |
|
||||
| Snowy Mountain Gondola | Approach the gondola in Volcanic Crater |
|
||||
| Yellow Eco Switch | Collect the power cell on the yellow vent switch in Snowy Mountain |
|
||||
| Snowy Fort Gate | Ride the Flut Flut in Snowy Mountain and press the fort gate switch |
|
||||
| Freed The Blue Sage | Free the Blue Sage in Gol and Maia's Citadel |
|
||||
| Freed The Red Sage | Free the Red Sage in Gol and Maia's Citadel |
|
||||
| Freed The Yellow Sage | Free the Yellow Sage in Gol and Maia's Citadel |
|
||||
| Freed The Green Sage | Free the Green Sage in Gol and Maia's Citadel |
|
||||
|
||||
## What are the special items and what do they unlock
|
||||
| Item Name | What it Unlocks |
|
||||
|--------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
|
||||
| Fisherman's Boat | Misty Island |
|
||||
| Jungle Elevator | The blue vent switch inside the temple in Forbidden Jungle |
|
||||
| Blue Eco Switch | The plant boss inside the temple in Forbidden Jungle <br/> The cannon tower in Sentinel Beach |
|
||||
| Flut Flut | The upper platforms in Boggy Swamp <br/> The fort gate switch in Snowy Mountain |
|
||||
| Warrior's Pontoons | Boggy Swamp and Mountain Pass |
|
||||
| Snowy Mountain Gondola | Snowy Mountain |
|
||||
| Yellow Eco Switch | The frozen box in Snowy Mountain <br/> The shortcut in Mountain Pass |
|
||||
| Snowy Fort Gate | The fort in Snowy Mountain |
|
||||
| Freed The Blue Sage <br/> Freed The Red Sage <br/> Freed The Yellow Sage | The final staircase in Gol and Maia's Citadel |
|
||||
| Freed The Green Sage | The final elevator in Gol and Maia's Citadel |
|
||||
|
||||
## How do I know which special items I have
|
||||
Open the game's menu, navigate to `Options`, then `Archipelago Options`, then `Item Tracker`.
|
||||
This will show you a list of all the special items in the game, ones not normally tracked as power cells or scout flies.
|
||||
Gray items indicate you do not possess that item, light blue items indicate you possess that item.
|
||||
|
||||
## What is the goal of the game once randomized
|
||||
By default, to complete the game you must defeat the Gol and Maia and stop them from opening the Dark Eco silo. In order
|
||||
to reach them, you will need at least 72 Power Cells to cross the Lava Tube, as well as the four special items for
|
||||
freeing the Red, Blue, Yellow, and Green Sages.
|
||||
|
||||
Alternatively, you can choose from a handful of other completion conditions like defeating a particular boss, crossing
|
||||
a particular connector level, or opening the 100 Power Cell door after defeating the final boss. You can also customize
|
||||
the thresholds for connector levels and orb trades. These options allow you to tailor the expected length and difficulty
|
||||
of your run as you see fit.
|
||||
|
||||
## What happens when I pick up or receive a power cell
|
||||
When you pick up a power cell, Jak and Daxter will perform their victory animation. Your power cell count will
|
||||
NOT change. The pause menu will say "Task Completed" below the picked-up Power Cell. If your power cell was related
|
||||
to one of the special checks listed above, you will automatically check that location as well - a 2 for 1 deal!
|
||||
Finally, your text client will inform you what you found and who it belongs to.
|
||||
|
||||
When you receive a power cell, your power cell count will tick up by 1. Gameplay will otherwise continue as normal.
|
||||
Finally, your text client will inform you where you received the power cell from.
|
||||
|
||||
## What happens when I pick up or receive a scout fly
|
||||
When you pick up a scout fly, your scout fly count will NOT change. The pause menu will show you the number of
|
||||
scout flies you picked up per-region, and this number will have ticked up by 1 for the region that scout fly belongs to.
|
||||
Finally, your text client will inform you what you found and who it belongs to.
|
||||
|
||||
When you receive a scout fly, your total scout fly count will tick up by 1. The pause menu will show you the number of
|
||||
scout flies you received per-region, and this number will have ticked up by 1 for the region that scout fly belongs to.
|
||||
Finally, your text client will inform you where you received the scout fly from, and which one it is.
|
||||
|
||||
## How do I check the Free 7 Scout Flies power cell
|
||||
You will automatically check this power cell when you _receive_ your 7th scout fly, NOT when you _pick up_ your 7th
|
||||
scout fly. So in short:
|
||||
|
||||
- When you _pick up_ your 7th fly, the normal rules apply.
|
||||
- When you _receive_ your 7th fly, 2 things will happen in quick succession.
|
||||
- First, you will receive that scout fly, as normal.
|
||||
- Second, you will immediately complete the "Free 7 Scout Flies" check, which will send out another item.
|
||||
|
||||
## What does Death Link do
|
||||
If you enable Death Link, all the other players in your Multiworld who also have it enabled will be linked by death.
|
||||
That means when Jak dies in your game, the players in with Death Link also die. Likewise, if any of the other
|
||||
players with Death Link die, Jak will also die in a random, possibly spectacular fashion.
|
||||
|
||||
You can turn off Death Link at any time in the game by opening the game's menu and navigating to `Options`,
|
||||
then `Archipelago Options`, then `Deathlink`.
|
||||
|
||||
## What does Move Randomizer do
|
||||
If you enable Move Randomizer, most of Jak's movement set will be added to the randomized item pool, and you will need
|
||||
to receive the move in order to use it (i.e. you must find it, or another player must send it to you). Some moves have
|
||||
prerequisite moves that you must also have in order to use them (e.g. Crouch Jump is dependent on Crouch). Jak will only
|
||||
be able to run, swim (including underwater), perform single jumps, and shoot yellow eco from his goggles ("firing from
|
||||
the hip" requires Punch). Note that Flut Flut and the Zoomer will have access to their full movement sets at all times.
|
||||
|
||||
You can turn off Move Rando at any time in the game by opening the game's menu, navigate to `Options`,
|
||||
then `Archipelago Options`, then `Move Randomizer`. This will give you access to the full movement set again.
|
||||
|
||||
## What are the movement options in Move Randomizer
|
||||
| Move Name | Prerequisite Moves |
|
||||
|-----------------|--------------------|
|
||||
| Crouch | |
|
||||
| Crouch Jump | Crouch |
|
||||
| Crouch Uppercut | Crouch |
|
||||
| Roll | |
|
||||
| Roll Jump | Roll |
|
||||
| Double Jump | |
|
||||
| Jump Dive | |
|
||||
| Jump Kick | |
|
||||
| Punch | |
|
||||
| Punch Uppercut | Punch |
|
||||
| Kick | |
|
||||
|
||||
## How do I know which moves I have
|
||||
Open the game's menu, navigate to `Options`, then `Archipelago Options`, then `Move Tracker`.
|
||||
This will show you a list of all the moves in the game.
|
||||
- Gray items indicate you do not possess that move.
|
||||
- Yellow items indicate you possess that move, but you are missing its prerequisites.
|
||||
- Light blue items indicate you possess that move, as well as its prerequisites.
|
||||
|
||||
## What does Orbsanity do
|
||||
If you enable Orbsanity, bundles of Precursor Orbs will be turned into checks. Every time you collect the chosen number
|
||||
of orbs, i.e. a "bundle," you will trigger another check. Likewise, the orbs will be added to the random item pool.
|
||||
There are several options to change the difficulty of this challenge.
|
||||
|
||||
- "Per Level" Orbsanity means the bundles are for each level in the game. (Geyser Rock, Sandover Village, etc.)
|
||||
- "Global" Orbsanity means orbs collected from any level count toward the next bundle.
|
||||
- The options with "Bundle Size" in the name indicate how many orbs are in a bundle. This adds a number of Items
|
||||
and Locations to the pool inversely proportional to the size of the bundle.
|
||||
- For example, if your bundle size is 20 orbs, you will add 100 items to the pool. If your bundle size is 250 orbs,
|
||||
you will add 8 items to the pool.
|
||||
|
||||
## What do Traps do
|
||||
When creating your player YAML, you can choose to replace some of the game's extraneous Power Cells and Precursor Orbs
|
||||
with traps. You can choose which traps you want to generate in your seed and how long they last. A random assortment
|
||||
will then be chosen to populate the item pool.
|
||||
|
||||
When you receive one, you will hear a buzzer and some kind of negative effect will occur in game. These effects may be
|
||||
challenging, maddening, or entertaining. When the trap duration ends, the game should return to its previous state.
|
||||
Multiple traps can be active at the same time, and they may interact with each other in strange ways. If they become
|
||||
too frustrating, you can lower their duration by navigating to `Options`, then `Archipelago Options`, then
|
||||
`Seed Options`, then `Trap Duration`. Lowering this number to zero will disable traps entirely.
|
||||
|
||||
## What kind of Traps are there
|
||||
| Trap Name | Effect |
|
||||
|-----------------|--------------------------------------------------------------------------------|
|
||||
| Trip Trap | Jak trips and falls |
|
||||
| Slippery Trap | The world gains the physical properties of Snowy Mountain's ice lake |
|
||||
| Gravity Trap | Jak falls to the ground faster and takes fall damage more easily |
|
||||
| Camera Trap | The camera remains fixed in place no matter how far away Jak moves |
|
||||
| Darkness Trap | The world gains the lighting properties of Dark Cave |
|
||||
| Earthquake Trap | The world and camera shake |
|
||||
| Teleport Trap | Jak immediately teleports to Samos's Hut |
|
||||
| Despair Trap | The Warrior sobs profusely |
|
||||
| Pacifism Trap | Jak's attacks have no effect on enemies, crates, or buttons |
|
||||
| Ecoless Trap | Jak's eco is drained and he cannot collect new eco |
|
||||
| Health Trap | Jak's health is set to 0 - not dead yet, but he will die to any attack or bonk |
|
||||
| Ledge Trap | Jak cannot grab onto ledges |
|
||||
| Zoomer Trap | Jak mounts an invisible zoomer (model loads properly depending on level) |
|
||||
| Mirror Trap | The world is mirrored |
|
||||
|
||||
## I got soft-locked and cannot leave how do I get out of here
|
||||
Open the game's menu, navigate to `Options`, then `Archipelago Options`, then `Warp To Home`.
|
||||
Selecting this option will ask if you want to be teleported to Geyser Rock. From there, you can teleport back
|
||||
to the nearest sage's hut to continue your journey.
|
||||
|
||||
## Why did I get an Option Error when generating a seed and how do I fix it
|
||||
Depending on your player YAML, Jak and Daxter can have a lot of items, which can sometimes be overwhelming or
|
||||
disruptive to multiworld games. There are also options that are mutually incompatible with each other, even in a solo
|
||||
game. To prevent the game from disrupting multiworlds, or generating an impossible solo seed, some options have
|
||||
Singleplayer and Multiplayer Minimums and Maximums, collectively called "friendly limits."
|
||||
|
||||
If you're generating a solo game, or your multiworld host agrees to your request, you can override those limits by
|
||||
editing the `host.yaml`. In the Archipelago Launcher, click `Open host.yaml`, then search for `jakanddaxter_options`,
|
||||
then search for `enforce_friendly_options`, then change this value from `true` to `false`. Disabling this allows for
|
||||
more disruptive and challenging options, but it may cause seed generation to fail. **Use at your own risk!**
|
||||
|
||||
## How do I check my player options in-game
|
||||
When you connect your text client to the Archipelago Server, the server will tell the game what options were chosen
|
||||
for this seed, and the game will apply those settings automatically.
|
||||
|
||||
You can verify these options by navigating to `Options`, then `Archipelago Options`, then `Seed Options`. **You can open
|
||||
each option to verify them, but you should NOT alter them during a run.** This may cause you to miss important
|
||||
progression items and prevent you (and others) from completing the run.
|
||||
|
||||
## How does the HUD work
|
||||
The game's normal HUD shows you how many power cells, precursor orbs, and scout flies you currently have. But if you
|
||||
hold `L2 or R2` and press a direction on the D-Pad, the HUD will show you alternate modes. Here is how the HUD works:
|
||||
|
||||
| HUD Mode | Button Combo | What the HUD Shows | Text Messages |
|
||||
|---------------|------------------------------|-----------------------------------|---------------------------------------|
|
||||
| Per-Level | `L2 or R2` + `Down` | Locations Checked (in this level) | `SENT {Other Item} TO {Other Player}` |
|
||||
| Global | `L2 or R2` + `Up` | Locations Checked (in the game) | `GOT {Your Item} FROM {Other Player}` |
|
||||
| Normal | `L2 or R2` + `Left or Right` | Items Received | Both Sent and Got Messages |
|
||||
| | | | |
|
||||
| (In Any Mode) | | (If you sent an Item to Yourself) | `FOUND {Your Item}` |
|
||||
|
||||
In all modes, the last 3 sent/received items and the player who sent/received it will be displayed in the
|
||||
bottom left corner. This will help you quickly reference information about newly received or sent items. Items in blue
|
||||
are Progression (or non-Jak items), in green are Filler, and in red are Traps. You can turn this off by navigating
|
||||
to `Options`, then `Archipelago Options`, then set `Item Messages` to `Off`.
|
||||
|
||||
## I think I found a bug where should I report it
|
||||
Depending on the nature of the bug, there are a couple of different options.
|
||||
|
||||
* If you found a logical error in the randomizer, please create a new Issue
|
||||
[here](https://github.com/ArchipelaGOAL/Archipelago/issues). Use this page if:
|
||||
* An item required for progression is unreachable.
|
||||
* The randomizer did not respect one of the Options you chose.
|
||||
* You see a mistake, typo, etc. on this webpage.
|
||||
* You see an error or stack trace appear on the text client.
|
||||
|
||||
* If you encountered an error in OpenGOAL, please create a new Issue
|
||||
[here](https://github.com/ArchipelaGOAL/ArchipelaGOAL/issues). Use this page if:
|
||||
* You encounter a crash, freeze, reset, etc. in the game.
|
||||
* You fail to send Items you find in the game to the Archipelago server.
|
||||
* You fail to receive Items the server sends to you.
|
||||
* Your game disconnects from the server and cannot reconnect.
|
||||
* You go looking for a game item that has already disappeared before you could reach it.
|
||||
|
||||
* Please upload your config file, spoiler log file, and any other generated logs in the Issue, so we can troubleshoot the problem.
|
||||
182
worlds/jakanddaxter/docs/setup_en.md
Normal file
182
worlds/jakanddaxter/docs/setup_en.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Jak And Daxter (ArchipelaGOAL) Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- A legally purchased copy of *Jak And Daxter: The Precursor Legacy.*
|
||||
- [The OpenGOAL Launcher](https://opengoal.dev/)
|
||||
- [The Jak and Daxter .APWORLD package](https://github.com/ArchipelaGOAL/Archipelago/releases)
|
||||
|
||||
At this time, this method of setup works on Windows only, but Linux support is a strong likelihood in the near future as OpenGOAL itself supports Linux.
|
||||
|
||||
## Installation via OpenGOAL Launcher
|
||||
|
||||
**You must set up a vanilla installation of Jak and Daxter before you can install mods for it.**
|
||||
|
||||
- Follow the installation process for the official OpenGOAL Launcher. See [here](https://opengoal.dev/docs/usage/installation).
|
||||
- Follow the setup process for adding mods to the OpenGOAL Launcher. See [here](https://jakmods.dev/).
|
||||
- Run the OpenGOAL Launcher (if you had it open before, close it and reopen it).
|
||||
- Click the Jak and Daxter logo on the left sidebar.
|
||||
- Click `Features` in the bottom right corner, then click `Mods`.
|
||||
- Under `Available Mods`, click `ArchipelaGOAL`. The mod should begin installing. When it is done, click `Continue` in the bottom right corner.
|
||||
- **DO NOT PLAY AN ARCHIPELAGO GAME THROUGH THE OPENGOAL LAUNCHER.** The Archipelago Client should handle everything for you.
|
||||
|
||||
### For NTSC versions of the game, follow these steps.
|
||||
|
||||
- Run the OpenGOAL Launcher (if you had it open before, close it and reopen it).
|
||||
- Click the Jak and Daxter logo on the left sidebar.
|
||||
- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`.
|
||||
- In the bottom right corner, click `Advanced`, then click `Compile`.
|
||||
|
||||
### For PAL versions of the game, follow these steps.
|
||||
|
||||
PAL versions of the game seem to require additional troubleshooting/setup in order to work properly.
|
||||
Below are some instructions that may help.
|
||||
If you see `-- Compilation Error! --` after pressing `Compile` or Launching the ArchipelaGOAL mod, try these steps.
|
||||
|
||||
- Remove these folders if you have them:
|
||||
- `<opengoal active version directory>/iso_data`
|
||||
- `<archipelagoal directory>/iso_data`
|
||||
- `<archipelagoal directory>/data/iso_data`
|
||||
- Place your Jak1 ISO in `<archipelagoal directory>` and rename it to `JakAndDaxter.iso`
|
||||
- Type `cmd` in Windows search, right click `Command Prompt`, and pick `Run as Administrator`
|
||||
- Run `cd <archipelagoal directory>`
|
||||
- Then run `.\extractor.exe --extract --extract-path .\data\iso_data "JakAndDaxter.iso"`
|
||||
- This command should end by saying `Uses Decompiler Config Version - ntsc_v1` or `... - pal`. Take note of this message.
|
||||
- If you saw `ntsc_v1`:
|
||||
- In cmd, run `.\decompiler.exe data\decompiler\config\jak1\jak1_config.jsonc --version "ntsc_v1" data\iso_data data\decompiler_out`
|
||||
- If you saw `pal`:
|
||||
- Rename `<archipelagoal directory>\data\iso_data\jak1` to `jak1_pal`
|
||||
- Back in cmd, run `.\decompiler.exe data\decompiler\config\jak1\jak1_config.jsonc --version "pal" data\iso_data data\decompiler_out`
|
||||
- Rename `<archipelagoal directory>\data\iso_data\jak1_pal` back to `jak1`
|
||||
- Rename `<archipelagoal directory>\data\decompiler_out\jak1_pal` back to `jak1`
|
||||
- Open a **brand new** console window and launch the compiler:
|
||||
- `cd <archipelagoal directory>`
|
||||
- `.\goalc.exe --user-auto --game jak1`
|
||||
- From the compiler (in the same window): `(mi)`. This should compile the game. **Note that the parentheses are important.**
|
||||
- **Don't close this first terminal, you will need it at the end.**
|
||||
- Then, open **another brand new** console window and execute the game:
|
||||
- `cd <archipelagoal directory>`
|
||||
- `.\gk.exe -v --game jak1 -- -boot -fakeiso -debug`
|
||||
- Finally, **from the first console still in the GOALC compiler**, connect to the game: `(lt)`.
|
||||
|
||||
## Updates and New Releases via OpenGOAL Launcher
|
||||
|
||||
If you are in the middle of an async game, and you do not want to update the mod, you do not need to do this step. The mod will only update when you tell it to.
|
||||
|
||||
- Run the OpenGOAL Launcher (if you had it open before, close it and reopen it).
|
||||
- Click the Jak and Daxter logo on the left sidebar.
|
||||
- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`.
|
||||
- Click `Update` to download and install any new updates that have been released.
|
||||
- You can verify your version by clicking `Versions`. The version you are using will say `(Active)` next to it.
|
||||
- **Then you must click `Advanced`, then click `Compile` to make the update take effect.**
|
||||
|
||||
## Starting a Game
|
||||
|
||||
### New Game
|
||||
|
||||
- Run the Archipelago Launcher.
|
||||
- From the right-most list, find and click `Jak and Daxter Client`.
|
||||
- 3 new windows should appear:
|
||||
- The OpenGOAL compiler will launch and compile the game. They should take about 30 seconds to compile.
|
||||
- You should hear a musical cue to indicate the compilation was a success. If you do not, see the Troubleshooting section.
|
||||
- You can **MINIMIZE** the Compiler window, **BUT DO NOT CLOSE IT.** It is required for Archipelago and the game to communicate with each other.
|
||||
- The game window itself will launch, and Jak will be standing outside Samos's Hut.
|
||||
- Once compilation is complete, the title sequence will start.
|
||||
- Finally, the Archipelago text client will open.
|
||||
- If you see **BOTH** `The REPL is ready!` and `The Memory Reader is ready!` then that should indicate a successful startup. If you do not, see the Troubleshooting section.
|
||||
- Once you see `CONNECT TO ARCHIPELAGO NOW` on the title screen, use the text client to connect to the Archipelago server. This will communicate your current settings and slot info to the game.
|
||||
- If you see `RECEIVING ITEMS, PLEASE WAIT...`, the game is busy receiving items from your starting inventory, assuming you have some.
|
||||
- Once you see `READY! PRESS START TO CONTINUE` on the title screen, you can press Start.
|
||||
- Choose `New Game`, choose a save file, and play through the opening cutscenes.
|
||||
- Once you reach Geyser Rock, the game has begun!
|
||||
- You can leave Geyser Rock immediately if you so choose - just step on the warp gate button.
|
||||
|
||||
### Returning / Async Game
|
||||
The same steps as New Game apply, with some exceptions:
|
||||
|
||||
- Once you reach the title screen, connect to the Archipelago server **BEFORE** you load your save file.
|
||||
- This is to allow AP to give the game your current settings and all the items you had previously.
|
||||
- **THESE SETTINGS AFFECT LOADING AND SAVING OF SAVE FILES, SO IT IS IMPORTANT TO DO THIS FIRST.**
|
||||
- Once you see `READY! PRESS START TO CONTINUE` on the title screen, you can press Start.
|
||||
- Instead of choosing `New Game` in the title menu, choose `Load Game`, then choose the save file **THAT HAS YOUR CURRENT SLOT NAME.**
|
||||
- To help you find the correct save file, highlighting a save will show you that save's slot name and the first 8 digits of the multiworld seed number.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### The Text Client Says "Unable to locate the OpenGOAL install directory"
|
||||
|
||||
Normally, the Archipelago client should be able to find your OpenGOAL installation automatically.
|
||||
|
||||
If it cannot, you may have to tell it yourself. Follow these instructions.
|
||||
|
||||
- Run the OpenGOAL Launcher (if you had it open before, close it and reopen it).
|
||||
- Click the Jak and Daxter logo on the left sidebar.
|
||||
- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`.
|
||||
- Click `Advanced` in the bottom right corner, then click `Open Game Data Folder`. You should see a new File Explorer open to that directory.
|
||||
- In the File Explorer, go to the parent directory called `archipelagoal`, and you should see the `gk.exe` and `goalc.exe` executables. Copy this path.
|
||||
- Run the Archipelago Launcher, then click on `Open host.yaml`. You should see a new text editor open that file.
|
||||
- Search for `jakanddaxter_options`, and you will need to make 2 changes here.
|
||||
- First, find the `root_directory` entry. Paste the path you noted earlier (the one containing gk.exe and goalc.exe) inside the double quotes.
|
||||
- **MAKE SURE YOU CHANGE ALL BACKSLASHES `\ ` TO FORWARD SLASHES `/`.**
|
||||
|
||||
```yaml
|
||||
root_directory: "%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal"
|
||||
```
|
||||
|
||||
- Second, find the `root_directory` entry. Change this to `false`. You do not need to use double quotes.
|
||||
|
||||
```yaml
|
||||
auto_detect_root_directory: true
|
||||
```
|
||||
|
||||
- Save the file and close it.
|
||||
|
||||
### The Game Fails To Load The Title Screen
|
||||
|
||||
You may start the game via the Text Client, but it never loads in the title screen. Check the Compiler window: you may see red and yellow errors like this.
|
||||
|
||||
```
|
||||
-- Compilation Error! --
|
||||
```
|
||||
|
||||
If this happens, follow these instructions. If you are using a PAL version of the game, you should skip these instructions and follow the `Special PAL Instructions` above.
|
||||
|
||||
- Run the OpenGOAL Launcher (if you had it open before, close it and reopen it).
|
||||
- Click the Jak and Daxter logo on the left sidebar, then click `Advanced`, then click `Open Game Data Folder`. Copy the `iso_data` folder from this directory.
|
||||
- Back in the OpenGOAL Launcher, click the Jak and Daxter logo on the left sidebar.
|
||||
- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`.
|
||||
- In the bottom right corner, click `Advanced`, then click `Open Game Data Folder`.
|
||||
- Paste the `iso_data` folder you copied earlier.
|
||||
- Back in the OpenGOAL Launcher, click the Jak and Daxter logo on the left sidebar.
|
||||
- Click `Features` in the bottom right corner, then click `Mods`, then under `Installed Mods`, click `ArchipelaGOAL`.
|
||||
- In the bottom right corner, click `Advanced`, then click `Compile`.
|
||||
|
||||
### The Text Client Says "Error reading game memory!" or "Error sending data to compiler"
|
||||
|
||||
If at any point the text client says this, you will need to restart the **all** of these applications.
|
||||
|
||||
- Close all open windows: the client, the compiler, and the game.
|
||||
- Run the OpenGOAL Launcher, then click `Features`, then click `Mods`, then click `ArchipelaGOAL`.
|
||||
- Click `Advanced`, then click `Play in Debug Mode`.
|
||||
- Click `Advanced`, then click `Open REPL`.
|
||||
- Then close and reopen the Jak and Daxter Client from the Archipelago Launcher.
|
||||
- Once these are done, you can enter `/repl status` and `/memr status` in the text client to verify.
|
||||
|
||||
### The Client Cannot Open A REPL Connection
|
||||
|
||||
If the client cannot open a REPL connection to the game, you may need to check the following steps:
|
||||
|
||||
- Ensure you are not hosting anything on ports `8181` and `8112`. Those are for the REPL (goalc) and the game (gk) respectively.
|
||||
- Ensure that Windows Defender and Windows Firewall are not blocking those programs from hosting or listening on those ports.
|
||||
- You can use Windows Resource Monitor to verify those ports are open when the programs are running.
|
||||
- Ensure that you only opened those ports for your local network, not the wider internet.
|
||||
|
||||
## Known Issues
|
||||
|
||||
- The game needs to boot in debug mode in order to allow the compiler to connect to it. **Clicking "Play" on the mod page in the OpenGOAL Launcher will not work.**
|
||||
- The Compiler console window is orphaned once you close the game - you will have to kill it manually when you stop playing.
|
||||
- The console windows cannot be run as background processes due to how the REPL works, so the best we can do is minimize them.
|
||||
- Orbsanity checks may show up out of order in the text client.
|
||||
- Large item releases may take up to several minutes for the game to process them all. Item Messages will usually take longer to appear than Items themselves.
|
||||
- In Lost Precursor City, if you die in the Color Platforms room, the game may crash after you respawn. The cause is unknown.
|
||||
- Darkness Trap may cause some visual glitches on certain levels. This is temporary, and terrain and object collision are unaffected.
|
||||
8
worlds/jakanddaxter/game_id.py
Normal file
8
worlds/jakanddaxter/game_id.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# All Jak And Daxter Archipelago IDs must be offset by this number.
|
||||
jak1_id = 741000000
|
||||
|
||||
# This is maximum ID we will allow.
|
||||
jak1_max = jak1_id + 999999
|
||||
|
||||
# The name of the game.
|
||||
jak1_name = "Jak and Daxter: The Precursor Legacy"
|
||||
BIN
worlds/jakanddaxter/icons/precursor_orb.ico
Normal file
BIN
worlds/jakanddaxter/icons/precursor_orb.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
worlds/jakanddaxter/icons/precursor_orb.png
Normal file
BIN
worlds/jakanddaxter/icons/precursor_orb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
156
worlds/jakanddaxter/items.py
Normal file
156
worlds/jakanddaxter/items.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from enum import IntEnum
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .game_id import jak1_name, jak1_max
|
||||
from .locs import (orb_locations as orbs,
|
||||
cell_locations as cells,
|
||||
scout_locations as scouts,
|
||||
special_locations as specials,
|
||||
orb_cache_locations as caches)
|
||||
|
||||
|
||||
class OrbAssoc(IntEnum):
|
||||
"""
|
||||
Identifies an item's association to unlocking new sources of Precursor Orbs. For example, Double Jump will unlock
|
||||
new orbs, but Freed the Green Sage will not. Power Cells conditionally unlock new orbs if they get you across
|
||||
connector levels.
|
||||
"""
|
||||
NEVER_UNLOCKS_ORBS = 0
|
||||
ALWAYS_UNLOCKS_ORBS = 1
|
||||
IS_POWER_CELL = 2
|
||||
|
||||
|
||||
class JakAndDaxterItem(Item):
|
||||
game: str = jak1_name
|
||||
orb_assoc: OrbAssoc
|
||||
orb_amount: int # Only non-zero for Orb Bundle items.
|
||||
|
||||
def __init__(self, name: str,
|
||||
classification: ItemClassification,
|
||||
code: int | None,
|
||||
player: int,
|
||||
orb_assoc: OrbAssoc = OrbAssoc.NEVER_UNLOCKS_ORBS,
|
||||
orb_amount: int = 0):
|
||||
super().__init__(name, classification, code, player)
|
||||
self.orb_assoc = orb_assoc
|
||||
self.orb_amount = orb_amount
|
||||
|
||||
|
||||
# Power Cells are generic, fungible, interchangeable items. Every cell is indistinguishable from every other.
|
||||
cell_item_table = {
|
||||
0: "Power Cell",
|
||||
}
|
||||
|
||||
# Scout flies are interchangeable within their respective sets of 7. Notice the level name after each item.
|
||||
# Also, notice that their Item ID equals their respective Power Cell's Location ID. This is necessary for
|
||||
# game<->archipelago communication.
|
||||
scout_item_table = {
|
||||
95: "Scout Fly - Geyser Rock",
|
||||
75: "Scout Fly - Sandover Village",
|
||||
7: "Scout Fly - Forbidden Jungle",
|
||||
20: "Scout Fly - Sentinel Beach",
|
||||
28: "Scout Fly - Misty Island",
|
||||
68: "Scout Fly - Fire Canyon",
|
||||
76: "Scout Fly - Rock Village",
|
||||
57: "Scout Fly - Precursor Basin",
|
||||
49: "Scout Fly - Lost Precursor City",
|
||||
43: "Scout Fly - Boggy Swamp",
|
||||
88: "Scout Fly - Mountain Pass",
|
||||
77: "Scout Fly - Volcanic Crater",
|
||||
85: "Scout Fly - Spider Cave",
|
||||
65: "Scout Fly - Snowy Mountain",
|
||||
90: "Scout Fly - Lava Tube",
|
||||
91: "Scout Fly - Citadel", # Had to shorten, it was >32 characters.
|
||||
}
|
||||
|
||||
# Orbs are also generic and interchangeable.
|
||||
# These items are only used by Orbsanity, and only one of these
|
||||
# items will be used corresponding to the chosen bundle size.
|
||||
orb_item_table = {
|
||||
1: "1 Precursor Orb",
|
||||
2: "2 Precursor Orbs",
|
||||
4: "4 Precursor Orbs",
|
||||
5: "5 Precursor Orbs",
|
||||
8: "8 Precursor Orbs",
|
||||
10: "10 Precursor Orbs",
|
||||
16: "16 Precursor Orbs",
|
||||
20: "20 Precursor Orbs",
|
||||
25: "25 Precursor Orbs",
|
||||
40: "40 Precursor Orbs",
|
||||
50: "50 Precursor Orbs",
|
||||
80: "80 Precursor Orbs",
|
||||
100: "100 Precursor Orbs",
|
||||
125: "125 Precursor Orbs",
|
||||
200: "200 Precursor Orbs",
|
||||
250: "250 Precursor Orbs",
|
||||
400: "400 Precursor Orbs",
|
||||
500: "500 Precursor Orbs",
|
||||
1000: "1000 Precursor Orbs",
|
||||
2000: "2000 Precursor Orbs",
|
||||
}
|
||||
|
||||
# These are special items representing unique unlocks in the world. Notice that their Item ID equals their
|
||||
# respective Location ID. Like scout flies, this is necessary for game<->archipelago communication.
|
||||
special_item_table = {
|
||||
5: "Fisherman's Boat", # Unlocks Misty Island
|
||||
4: "Jungle Elevator", # Unlocks the Forbidden Jungle Temple
|
||||
2: "Blue Eco Switch", # Unlocks Blue Eco Vents
|
||||
17: "Flut Flut", # Unlocks Flut Flut sections in Boggy Swamp and Snowy Mountain
|
||||
33: "Warrior's Pontoons", # Unlocks Boggy Swamp and everything post-Rock Village
|
||||
105: "Snowy Mountain Gondola", # Unlocks Snowy Mountain
|
||||
60: "Yellow Eco Switch", # Unlocks Yellow Eco Vents
|
||||
63: "Snowy Fort Gate", # Unlocks the Snowy Mountain Fort
|
||||
71: "Freed The Blue Sage", # 1 of 3 unlocks for the final staircase in Citadel
|
||||
72: "Freed The Red Sage", # 1 of 3 unlocks for the final staircase in Citadel
|
||||
73: "Freed The Yellow Sage", # 1 of 3 unlocks for the final staircase in Citadel
|
||||
70: "Freed The Green Sage", # Unlocks the final boss elevator in Citadel
|
||||
}
|
||||
|
||||
# These are the move items for move randomizer. Notice that their Item ID equals some of the Orb Cache Location ID's.
|
||||
# This was 100% arbitrary. There's no reason to tie moves to orb caches except that I need a place to put them. ;_;
|
||||
move_item_table = {
|
||||
10344: "Crouch",
|
||||
10369: "Crouch Jump",
|
||||
11072: "Crouch Uppercut",
|
||||
12634: "Roll",
|
||||
12635: "Roll Jump",
|
||||
10945: "Double Jump",
|
||||
14507: "Jump Dive",
|
||||
14838: "Jump Kick",
|
||||
23348: "Punch",
|
||||
23349: "Punch Uppercut",
|
||||
23350: "Kick",
|
||||
# 24038: "Orb Cache at End of Blast Furnace", # Hold onto these ID's for future use.
|
||||
# 24039: "Orb Cache at End of Launch Pad Room",
|
||||
# 24040: "Orb Cache at Start of Launch Pad Room",
|
||||
}
|
||||
|
||||
# These are trap items. Their Item ID is to be subtracted from the base game ID. They do not have corresponding
|
||||
# game locations because they are intended to replace other items that have been marked as filler.
|
||||
trap_item_table = {
|
||||
1: "Trip Trap",
|
||||
2: "Slippery Trap",
|
||||
3: "Gravity Trap",
|
||||
4: "Camera Trap",
|
||||
5: "Darkness Trap",
|
||||
6: "Earthquake Trap",
|
||||
7: "Teleport Trap",
|
||||
8: "Despair Trap",
|
||||
9: "Pacifism Trap",
|
||||
10: "Ecoless Trap",
|
||||
11: "Health Trap",
|
||||
12: "Ledge Trap",
|
||||
13: "Zoomer Trap",
|
||||
14: "Mirror Trap",
|
||||
}
|
||||
|
||||
# All Items
|
||||
# While we're here, do all the ID conversions needed.
|
||||
item_table = {
|
||||
**{cells.to_ap_id(k): name for k, name in cell_item_table.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scout_item_table.items()},
|
||||
**{specials.to_ap_id(k): name for k, name in special_item_table.items()},
|
||||
**{caches.to_ap_id(k): name for k, name in move_item_table.items()},
|
||||
**{orbs.to_ap_id(k): name for k, name in orb_item_table.items()},
|
||||
**{jak1_max - k: name for k, name in trap_item_table.items()},
|
||||
jak1_max: "Green Eco Pill" # Filler item.
|
||||
}
|
||||
76
worlds/jakanddaxter/levels.py
Normal file
76
worlds/jakanddaxter/levels.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# This contains the list of levels in Jak and Daxter.
|
||||
# Not to be confused with Regions - there can be multiple Regions in every Level.
|
||||
level_table = {
|
||||
"Geyser Rock": {
|
||||
"level_index": 0,
|
||||
"orbs": 50
|
||||
},
|
||||
"Sandover Village": {
|
||||
"level_index": 1,
|
||||
"orbs": 50
|
||||
},
|
||||
"Sentinel Beach": {
|
||||
"level_index": 2,
|
||||
"orbs": 150
|
||||
},
|
||||
"Forbidden Jungle": {
|
||||
"level_index": 3,
|
||||
"orbs": 150
|
||||
},
|
||||
"Misty Island": {
|
||||
"level_index": 4,
|
||||
"orbs": 150
|
||||
},
|
||||
"Fire Canyon": {
|
||||
"level_index": 5,
|
||||
"orbs": 50
|
||||
},
|
||||
"Rock Village": {
|
||||
"level_index": 6,
|
||||
"orbs": 50
|
||||
},
|
||||
"Lost Precursor City": {
|
||||
"level_index": 7,
|
||||
"orbs": 200
|
||||
},
|
||||
"Boggy Swamp": {
|
||||
"level_index": 8,
|
||||
"orbs": 200
|
||||
},
|
||||
"Precursor Basin": {
|
||||
"level_index": 9,
|
||||
"orbs": 200
|
||||
},
|
||||
"Mountain Pass": {
|
||||
"level_index": 10,
|
||||
"orbs": 50
|
||||
},
|
||||
"Volcanic Crater": {
|
||||
"level_index": 11,
|
||||
"orbs": 50
|
||||
},
|
||||
"Snowy Mountain": {
|
||||
"level_index": 12,
|
||||
"orbs": 200
|
||||
},
|
||||
"Spider Cave": {
|
||||
"level_index": 13,
|
||||
"orbs": 200
|
||||
},
|
||||
"Lava Tube": {
|
||||
"level_index": 14,
|
||||
"orbs": 50
|
||||
},
|
||||
"Gol and Maia's Citadel": {
|
||||
"level_index": 15,
|
||||
"orbs": 200
|
||||
}
|
||||
}
|
||||
|
||||
level_table_with_global = {
|
||||
**level_table,
|
||||
"": {
|
||||
"level_index": 16, # Global
|
||||
"orbs": 2000
|
||||
}
|
||||
}
|
||||
66
worlds/jakanddaxter/locations.py
Normal file
66
worlds/jakanddaxter/locations.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from BaseClasses import Location
|
||||
from .game_id import jak1_name
|
||||
from .locs import (orb_locations as orbs,
|
||||
cell_locations as cells,
|
||||
scout_locations as scouts,
|
||||
special_locations as specials,
|
||||
orb_cache_locations as caches)
|
||||
|
||||
|
||||
class JakAndDaxterLocation(Location):
|
||||
game: str = jak1_name
|
||||
|
||||
|
||||
# Different tables for location groups.
|
||||
# Each Item ID == its corresponding Location ID. While we're here, do all the ID conversions needed.
|
||||
cell_location_table = {
|
||||
**{cells.to_ap_id(k): name for k, name in cells.loc7SF_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locGR_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locSV_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locFJ_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locSB_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locMI_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locFC_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locRV_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locPB_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locLPC_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locBS_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locMP_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locVC_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locSC_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locSM_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locLT_cellTable.items()},
|
||||
**{cells.to_ap_id(k): name for k, name in cells.locGMC_cellTable.items()},
|
||||
}
|
||||
|
||||
scout_location_table = {
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locGR_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locSV_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locFJ_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locSB_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locMI_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locFC_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locRV_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locPB_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locLPC_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locBS_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locMP_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locVC_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locSC_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locSM_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locLT_scoutTable.items()},
|
||||
**{scouts.to_ap_id(k): name for k, name in scouts.locGMC_scoutTable.items()},
|
||||
}
|
||||
|
||||
special_location_table = {specials.to_ap_id(k): name for k, name in specials.loc_specialTable.items()}
|
||||
cache_location_table = {caches.to_ap_id(k): name for k, name in caches.loc_orbCacheTable.items()}
|
||||
orb_location_table = {orbs.to_ap_id(k): name for k, name in orbs.loc_orbBundleTable.items()}
|
||||
|
||||
# All Locations
|
||||
location_table = {
|
||||
**cell_location_table,
|
||||
**scout_location_table,
|
||||
**special_location_table,
|
||||
**cache_location_table,
|
||||
**orb_location_table
|
||||
}
|
||||
0
worlds/jakanddaxter/locs/__init__.py
Normal file
0
worlds/jakanddaxter/locs/__init__.py
Normal file
194
worlds/jakanddaxter/locs/cell_locations.py
Normal file
194
worlds/jakanddaxter/locs/cell_locations.py
Normal file
@@ -0,0 +1,194 @@
|
||||
from ..game_id import jak1_id
|
||||
|
||||
# Power Cells are given ID's between 0 and 116 by the game.
|
||||
|
||||
# The game tracks all game-tasks as integers.
|
||||
# 101 of these ID's correspond directly to power cells, but they are not
|
||||
# necessarily ordered, nor are they the first 101 in the task list.
|
||||
# The remaining ones are cutscenes and other events.
|
||||
|
||||
|
||||
# These helper functions do all the math required to get information about each
|
||||
# power cell and translate its ID between AP and OpenGOAL.
|
||||
def to_ap_id(game_id: int) -> int:
|
||||
if game_id >= jak1_id:
|
||||
raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.")
|
||||
return jak1_id + game_id
|
||||
|
||||
|
||||
def to_game_id(ap_id: int) -> int:
|
||||
if ap_id < jak1_id:
|
||||
raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.")
|
||||
return ap_id - jak1_id
|
||||
|
||||
|
||||
# The ID's you see below correspond directly to that cell's game-task ID.
|
||||
|
||||
# The "Free 7 Scout Flies" Power Cells will be unlocked separately from their respective levels.
|
||||
loc7SF_cellTable = {
|
||||
95: "GR: Free 7 Scout Flies",
|
||||
75: "SV: Free 7 Scout Flies",
|
||||
7: "FJ: Free 7 Scout Flies",
|
||||
20: "SB: Free 7 Scout Flies",
|
||||
28: "MI: Free 7 Scout Flies",
|
||||
68: "FC: Free 7 Scout Flies",
|
||||
76: "RV: Free 7 Scout Flies",
|
||||
57: "PB: Free 7 Scout Flies",
|
||||
49: "LPC: Free 7 Scout Flies",
|
||||
43: "BS: Free 7 Scout Flies",
|
||||
88: "MP: Free 7 Scout Flies",
|
||||
77: "VC: Free 7 Scout Flies",
|
||||
85: "SC: Free 7 Scout Flies",
|
||||
65: "SM: Free 7 Scout Flies",
|
||||
90: "LT: Free 7 Scout Flies",
|
||||
91: "GMC: Free 7 Scout Flies",
|
||||
}
|
||||
|
||||
# Geyser Rock
|
||||
locGR_cellTable = {
|
||||
92: "GR: Find The Cell On The Path",
|
||||
93: "GR: Open The Precursor Door",
|
||||
94: "GR: Climb Up The Cliff",
|
||||
}
|
||||
|
||||
# Sandover Village
|
||||
locSV_cellTable = {
|
||||
11: "SV: Bring 90 Orbs To The Mayor",
|
||||
12: "SV: Bring 90 Orbs to Your Uncle",
|
||||
10: "SV: Herd The Yakows Into The Pen",
|
||||
13: "SV: Bring 120 Orbs To The Oracle (1)",
|
||||
14: "SV: Bring 120 Orbs To The Oracle (2)",
|
||||
}
|
||||
|
||||
# Forbidden Jungle
|
||||
locFJ_cellTable = {
|
||||
3: "FJ: Connect The Eco Beams",
|
||||
4: "FJ: Get To The Top Of The Temple",
|
||||
2: "FJ: Find The Blue Vent Switch",
|
||||
6: "FJ: Defeat The Dark Eco Plant",
|
||||
5: "FJ: Catch 200 Pounds Of Fish",
|
||||
8: "FJ: Follow The Canyon To The Sea",
|
||||
9: "FJ: Open The Locked Temple Door",
|
||||
}
|
||||
|
||||
# Sentinel Beach
|
||||
locSB_cellTable = {
|
||||
15: "SB: Unblock The Eco Harvesters",
|
||||
17: "SB: Push The Flut Flut Egg Off The Cliff",
|
||||
16: "SB: Get The Power Cell From The Pelican",
|
||||
18: "SB: Chase The Seagulls",
|
||||
19: "SB: Launch Up To The Cannon Tower",
|
||||
21: "SB: Explore The Beach",
|
||||
22: "SB: Climb The Sentinel",
|
||||
}
|
||||
|
||||
# Misty Island
|
||||
locMI_cellTable = {
|
||||
23: "MI: Catch The Sculptor's Muse",
|
||||
24: "MI: Climb The Lurker Ship",
|
||||
26: "MI: Stop The Cannon",
|
||||
25: "MI: Return To The Dark Eco Pool",
|
||||
27: "MI: Destroy the Balloon Lurkers",
|
||||
29: "MI: Use Zoomer To Reach Power Cell",
|
||||
30: "MI: Use Blue Eco To Reach Power Cell",
|
||||
}
|
||||
|
||||
# Fire Canyon
|
||||
locFC_cellTable = {
|
||||
69: "FC: Reach The End Of Fire Canyon",
|
||||
}
|
||||
|
||||
# Rock Village
|
||||
locRV_cellTable = {
|
||||
31: "RV: Bring 90 Orbs To The Gambler",
|
||||
32: "RV: Bring 90 Orbs To The Geologist",
|
||||
33: "RV: Bring 90 Orbs To The Warrior",
|
||||
34: "RV: Bring 120 Orbs To The Oracle (1)",
|
||||
35: "RV: Bring 120 Orbs To The Oracle (2)",
|
||||
}
|
||||
|
||||
# Precursor Basin
|
||||
locPB_cellTable = {
|
||||
54: "PB: Herd The Moles Into Their Hole",
|
||||
53: "PB: Catch The Flying Lurkers",
|
||||
52: "PB: Beat Record Time On The Gorge",
|
||||
56: "PB: Get The Power Cell Over The Lake",
|
||||
55: "PB: Cure Dark Eco Infected Plants",
|
||||
58: "PB: Navigate The Purple Precursor Rings",
|
||||
59: "PB: Navigate The Blue Precursor Rings",
|
||||
}
|
||||
|
||||
# Lost Precursor City
|
||||
locLPC_cellTable = {
|
||||
47: "LPC: Raise The Chamber",
|
||||
45: "LPC: Follow The Colored Pipes",
|
||||
46: "LPC: Reach The Bottom Of The City",
|
||||
48: "LPC: Quickly Cross The Dangerous Pool",
|
||||
44: "LPC: Match The Platform Colors",
|
||||
50: "LPC: Climb The Slide Tube",
|
||||
51: "LPC: Reach The Center Of The Complex",
|
||||
}
|
||||
|
||||
# Boggy Swamp
|
||||
locBS_cellTable = {
|
||||
37: "BS: Ride The Flut Flut",
|
||||
36: "BS: Protect Farthy's Snacks",
|
||||
38: "BS: Defeat The Lurker Ambush",
|
||||
39: "BS: Break The Tethers To The Zeppelin (1)",
|
||||
40: "BS: Break The Tethers To The Zeppelin (2)",
|
||||
41: "BS: Break The Tethers To The Zeppelin (3)",
|
||||
42: "BS: Break The Tethers To The Zeppelin (4)",
|
||||
}
|
||||
|
||||
# Mountain Pass
|
||||
locMP_cellTable = {
|
||||
86: "MP: Defeat Klaww",
|
||||
87: "MP: Reach The End Of The Mountain Pass",
|
||||
110: "MP: Find The Hidden Power Cell",
|
||||
}
|
||||
|
||||
# Volcanic Crater
|
||||
locVC_cellTable = {
|
||||
96: "VC: Bring 90 Orbs To The Miners (1)",
|
||||
97: "VC: Bring 90 Orbs To The Miners (2)",
|
||||
98: "VC: Bring 90 Orbs To The Miners (3)",
|
||||
99: "VC: Bring 90 Orbs To The Miners (4)",
|
||||
100: "VC: Bring 120 Orbs To The Oracle (1)",
|
||||
101: "VC: Bring 120 Orbs To The Oracle (2)",
|
||||
74: "VC: Find The Hidden Power Cell",
|
||||
}
|
||||
|
||||
# Spider Cave
|
||||
locSC_cellTable = {
|
||||
78: "SC: Use Your Goggles To Shoot The Gnawing Lurkers",
|
||||
79: "SC: Destroy The Dark Eco Crystals",
|
||||
80: "SC: Explore The Dark Cave",
|
||||
81: "SC: Climb The Giant Robot",
|
||||
82: "SC: Launch To The Poles",
|
||||
83: "SC: Navigate The Spider Tunnel",
|
||||
84: "SC: Climb the Precursor Platforms",
|
||||
}
|
||||
|
||||
# Snowy Mountain
|
||||
locSM_cellTable = {
|
||||
60: "SM: Find The Yellow Vent Switch",
|
||||
61: "SM: Stop The 3 Lurker Glacier Troops",
|
||||
66: "SM: Deactivate The Precursor Blockers",
|
||||
67: "SM: Open The Frozen Crate",
|
||||
63: "SM: Open The Lurker Fort Gate",
|
||||
62: "SM: Get Through The Lurker Fort",
|
||||
64: "SM: Survive The Lurker Infested Cave",
|
||||
}
|
||||
|
||||
# Lava Tube
|
||||
locLT_cellTable = {
|
||||
89: "LT: Cross The Lava Tube",
|
||||
}
|
||||
|
||||
# Gol and Maias Citadel
|
||||
locGMC_cellTable = {
|
||||
71: "GMC: Free The Blue Sage",
|
||||
72: "GMC: Free The Red Sage",
|
||||
73: "GMC: Free The Yellow Sage",
|
||||
70: "GMC: Free The Green Sage",
|
||||
}
|
||||
52
worlds/jakanddaxter/locs/orb_cache_locations.py
Normal file
52
worlds/jakanddaxter/locs/orb_cache_locations.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from ..game_id import jak1_id
|
||||
|
||||
# These are the locations of Orb Caches throughout the game, unlockable only with blue eco.
|
||||
# They are not game collectables and thus don't have the same kinds of game ID's. They do, however, have actor ID's.
|
||||
# There are a total of 14 in the game.
|
||||
|
||||
# When these are opened, we can execute a hook in the mod that might be able to tell us which orb cache we opened,
|
||||
# by ID, and that will allow us to map a Location object to it. We'll be using these for Move Randomizer,
|
||||
# where each move is "mapped" to an Orb Cache being unlocked. Obviously, they will then be randomized, but with moves
|
||||
# not being considered Items by the game, we need to conjure SOME kind of Location for them, and Orb Caches is the best
|
||||
# we can do.
|
||||
|
||||
# We can use 2^12 to offset these from special checks, just like we offset those from scout flies
|
||||
# by 2^11. Special checks don't exceed an ID of (jak1_id + 2153).
|
||||
orb_cache_offset = 4096
|
||||
|
||||
|
||||
# These helper functions do all the math required to get information about each
|
||||
# special check and translate its ID between AP and OpenGOAL. Similar to Scout Flies, these large numbers are not
|
||||
# necessary, and we can flatten out the range in which these numbers lie.
|
||||
def to_ap_id(game_id: int) -> int:
|
||||
if game_id >= jak1_id:
|
||||
raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.")
|
||||
uncompressed_id = jak1_id + orb_cache_offset + game_id # Add the offsets and the orb cache Actor ID.
|
||||
return uncompressed_id - 10344 # Subtract the smallest Actor ID.
|
||||
|
||||
|
||||
def to_game_id(ap_id: int) -> int:
|
||||
if ap_id < jak1_id:
|
||||
raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.")
|
||||
uncompressed_id = ap_id + 10344 # Reverse process, add back the smallest Actor ID.
|
||||
return uncompressed_id - jak1_id - orb_cache_offset # Subtract the offsets.
|
||||
|
||||
|
||||
# The ID's you see below correlate to the Actor ID of each Orb Cache.
|
||||
|
||||
loc_orbCacheTable = {
|
||||
10344: "Orb Cache in Sandover Village",
|
||||
10369: "Orb Cache in Forbidden Jungle",
|
||||
11072: "Orb Cache on Misty Island",
|
||||
12634: "Orb Cache near Flut Flut Egg",
|
||||
12635: "Orb Cache near Pelican's Nest",
|
||||
10945: "Orb Cache in Rock Village",
|
||||
14507: "Orb Cache in First Sunken Chamber",
|
||||
14838: "Orb Cache in Second Sunken Chamber",
|
||||
23348: "Orb Cache in Snowy Fort (1)",
|
||||
23349: "Orb Cache in Snowy Fort (2)",
|
||||
23350: "Orb Cache in Snowy Fort (3)",
|
||||
24038: "Orb Cache at End of Blast Furnace",
|
||||
24039: "Orb Cache at End of Launch Pad Room",
|
||||
24040: "Orb Cache at Start of Launch Pad Room",
|
||||
}
|
||||
123
worlds/jakanddaxter/locs/orb_locations.py
Normal file
123
worlds/jakanddaxter/locs/orb_locations.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from ..game_id import jak1_id
|
||||
from ..levels import level_table_with_global
|
||||
|
||||
# Precursor Orbs are not necessarily given ID's by the game.
|
||||
|
||||
# Of the 2000 orbs (or "money") you can pick up, only 1233 are standalone ones you find in the overworld.
|
||||
# We can identify them by Actor ID's, which run from 549 to 24433. Other actors reside in this range,
|
||||
# so like Power Cells these are not ordered, nor contiguous, nor exclusively orbs.
|
||||
|
||||
# In fact, other ID's in this range belong to actors that spawn orbs when they are activated or when they die,
|
||||
# like steel crates, orb caches, Spider Cave gnawers, or jumping on the Plant Boss's head. These orbs that spawn
|
||||
# from parent actors DON'T have an Actor ID themselves - the parent object keeps track of how many of its orbs
|
||||
# have been picked up.
|
||||
|
||||
# In order to deal with this mess, we're creating 2 extra functions that will create and identify Orb Locations for us.
|
||||
# These will be compatible with both Global Orbsanity and Per-Level Orbsanity, allowing us to create any
|
||||
# number of Locations depending on the bundle size chosen, while also guaranteeing that each has a unique address.
|
||||
|
||||
# We can use 2^15 to offset them from Orb Caches, because Orb Cache ID's max out at (jak1_id + 17792).
|
||||
orb_offset = 32768
|
||||
|
||||
|
||||
# These helper functions do all the math required to get information about each
|
||||
# precursor orb and translate its ID between AP and OpenGOAL.
|
||||
def to_ap_id(game_id: int) -> int:
|
||||
if game_id >= jak1_id:
|
||||
raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.")
|
||||
return jak1_id + orb_offset + game_id # Add the offsets and the orb Actor ID.
|
||||
|
||||
|
||||
def to_game_id(ap_id: int) -> int:
|
||||
if ap_id < jak1_id:
|
||||
raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.")
|
||||
return ap_id - jak1_id - orb_offset # Reverse process, subtract the offsets.
|
||||
|
||||
|
||||
# Use this when the Memory Reader learns that you checked a specific bundle.
|
||||
# Offset each level by 200 orbs (max number in any level), {200, 400, ...}
|
||||
# then divide orb count by bundle size, {201, 202, ...}
|
||||
# then subtract 1. {200, 201, ...}
|
||||
def find_address(level_index: int, orb_count: int, bundle_size: int) -> int:
|
||||
result = (level_index * 200) + (orb_count // bundle_size) - 1
|
||||
return result
|
||||
|
||||
|
||||
# Use this when assigning addresses during region generation.
|
||||
def create_address(level_index: int, bundle_index: int) -> int:
|
||||
result = (level_index * 200) + bundle_index
|
||||
return result
|
||||
|
||||
|
||||
# What follows is our methods of generating all the name/ID pairs for location_name_to_id.
|
||||
# Remember that not every bundle will be used in the actual seed, we just need a static map of strings to ints.
|
||||
locGR_orbBundleTable = {create_address(level_table_with_global["Geyser Rock"]["level_index"], index):
|
||||
f"Geyser Rock Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Geyser Rock"]["orbs"])}
|
||||
locSV_orbBundleTable = {create_address(level_table_with_global["Sandover Village"]["level_index"], index):
|
||||
f"Sandover Village Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Sandover Village"]["orbs"])}
|
||||
locFJ_orbBundleTable = {create_address(level_table_with_global["Forbidden Jungle"]["level_index"], index):
|
||||
f"Forbidden Jungle Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Forbidden Jungle"]["orbs"])}
|
||||
locSB_orbBundleTable = {create_address(level_table_with_global["Sentinel Beach"]["level_index"], index):
|
||||
f"Sentinel Beach Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Sentinel Beach"]["orbs"])}
|
||||
locMI_orbBundleTable = {create_address(level_table_with_global["Misty Island"]["level_index"], index):
|
||||
f"Misty Island Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Misty Island"]["orbs"])}
|
||||
locFC_orbBundleTable = {create_address(level_table_with_global["Fire Canyon"]["level_index"], index):
|
||||
f"Fire Canyon Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Fire Canyon"]["orbs"])}
|
||||
locRV_orbBundleTable = {create_address(level_table_with_global["Rock Village"]["level_index"], index):
|
||||
f"Rock Village Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Rock Village"]["orbs"])}
|
||||
locLPC_orbBundleTable = {create_address(level_table_with_global["Lost Precursor City"]["level_index"], index):
|
||||
f"Lost Precursor City Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Lost Precursor City"]["orbs"])}
|
||||
locBS_orbBundleTable = {create_address(level_table_with_global["Boggy Swamp"]["level_index"], index):
|
||||
f"Boggy Swamp Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Boggy Swamp"]["orbs"])}
|
||||
locPB_orbBundleTable = {create_address(level_table_with_global["Precursor Basin"]["level_index"], index):
|
||||
f"Precursor Basin Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Precursor Basin"]["orbs"])}
|
||||
locMP_orbBundleTable = {create_address(level_table_with_global["Mountain Pass"]["level_index"], index):
|
||||
f"Mountain Pass Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Mountain Pass"]["orbs"])}
|
||||
locVC_orbBundleTable = {create_address(level_table_with_global["Volcanic Crater"]["level_index"], index):
|
||||
f"Volcanic Crater Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Volcanic Crater"]["orbs"])}
|
||||
locSM_orbBundleTable = {create_address(level_table_with_global["Snowy Mountain"]["level_index"], index):
|
||||
f"Snowy Mountain Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Snowy Mountain"]["orbs"])}
|
||||
locSC_orbBundleTable = {create_address(level_table_with_global["Spider Cave"]["level_index"], index):
|
||||
f"Spider Cave Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Spider Cave"]["orbs"])}
|
||||
locLT_orbBundleTable = {create_address(level_table_with_global["Lava Tube"]["level_index"], index):
|
||||
f"Lava Tube Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Lava Tube"]["orbs"])}
|
||||
locGMC_orbBundleTable = {create_address(level_table_with_global["Gol and Maia's Citadel"]["level_index"], index):
|
||||
f"Gol and Maia's Citadel Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global["Gol and Maia's Citadel"]["orbs"])}
|
||||
locGlobal_orbBundleTable = {create_address(level_table_with_global[""]["level_index"], index):
|
||||
f"Orb Bundle {index + 1}"
|
||||
for index in range(level_table_with_global[""]["orbs"])}
|
||||
loc_orbBundleTable = {
|
||||
**locGR_orbBundleTable,
|
||||
**locSV_orbBundleTable,
|
||||
**locSB_orbBundleTable,
|
||||
**locFJ_orbBundleTable,
|
||||
**locMI_orbBundleTable,
|
||||
**locFC_orbBundleTable,
|
||||
**locRV_orbBundleTable,
|
||||
**locLPC_orbBundleTable,
|
||||
**locBS_orbBundleTable,
|
||||
**locPB_orbBundleTable,
|
||||
**locMP_orbBundleTable,
|
||||
**locVC_orbBundleTable,
|
||||
**locSM_orbBundleTable,
|
||||
**locSC_orbBundleTable,
|
||||
**locLT_orbBundleTable,
|
||||
**locGMC_orbBundleTable,
|
||||
**locGlobal_orbBundleTable
|
||||
}
|
||||
230
worlds/jakanddaxter/locs/scout_locations.py
Normal file
230
worlds/jakanddaxter/locs/scout_locations.py
Normal file
@@ -0,0 +1,230 @@
|
||||
from ..game_id import jak1_id
|
||||
|
||||
# Scout Flies are given ID's between 0 and 393311 by the game, explanation below.
|
||||
|
||||
# Each fly (or "buzzer") is given a unique 32-bit number broken into two 16-bit numbers.
|
||||
# The lower 16 bits are the game-task ID of the power cell the fly corresponds to.
|
||||
# The higher 16 bits are the index of the fly itself, from 000 (0) to 110 (6).
|
||||
|
||||
# Ex: The final scout fly on Geyser Rock
|
||||
# 0000000000000110 0000000001011111
|
||||
# ( Index: 6 ) ( Cell: 95 )
|
||||
|
||||
# Because flies are indexed from 0, each 0th fly's full ID == the power cell's ID.
|
||||
# So we need to offset all of their ID's in order for Archipelago to separate them
|
||||
# from their power cells. We can use 1024 (2^10) for this purpose, because scout flies
|
||||
# only ever need 10 bits to identify themselves (3 for the index, 7 for the cell ID).
|
||||
|
||||
# We're also going to compress the ID by bit-shifting the fly index down to lower bits,
|
||||
# keeping the scout fly ID range to a smaller set of numbers (1000 -> 2000, instead of 1 -> 400000).
|
||||
fly_offset = 1024
|
||||
|
||||
|
||||
# These helper functions do all the math required to get information about each
|
||||
# scout fly and translate its ID between AP and OpenGOAL.
|
||||
def to_ap_id(game_id: int) -> int:
|
||||
if game_id >= jak1_id:
|
||||
raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.")
|
||||
cell_id = get_cell_id(game_id) # Get the power cell ID from the lowest 7 bits.
|
||||
buzzer_index = (game_id - cell_id) >> 9 # Get the index, bit shift it down 9 places.
|
||||
compressed_id = fly_offset + buzzer_index + cell_id # Add the offset, the bit-shifted index, and the cell ID.
|
||||
return jak1_id + compressed_id # Last thing: add the game's ID.
|
||||
|
||||
|
||||
def to_game_id(ap_id: int) -> int:
|
||||
if ap_id < jak1_id:
|
||||
raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.")
|
||||
compressed_id = ap_id - jak1_id # Reverse process. First thing: subtract the game's ID.
|
||||
cell_id = get_cell_id(compressed_id) # Get the power cell ID from the lowest 7 bits.
|
||||
buzzer_index = compressed_id - fly_offset - cell_id # Get the bit-shifted index.
|
||||
return (buzzer_index << 9) + cell_id # Return the index to its normal place, re-add the cell ID.
|
||||
|
||||
|
||||
# Get the power cell ID from the lowest 7 bits.
|
||||
# Make sure to use this function ONLY when the input argument does NOT include jak1_id,
|
||||
# because that number may flip some of the bottom 7 bits, and that will throw off this bit mask.
|
||||
def get_cell_id(buzzer_id: int) -> int:
|
||||
if buzzer_id >= jak1_id:
|
||||
raise ValueError(f"Attempted to bit mask {buzzer_id}, but it is polluted by the game's ID {jak1_id}.")
|
||||
return buzzer_id & 0b1111111
|
||||
|
||||
|
||||
# The ID's you see below correspond directly to that fly's 32-bit ID in the game.
|
||||
# I used the decompiled entity JSON's and Jak's X/Y coordinates in Debug Mode
|
||||
# to determine which box ID is which location.
|
||||
|
||||
# Geyser Rock
|
||||
locGR_scoutTable = {
|
||||
95: "GR: Scout Fly On Ground, Front",
|
||||
327775: "GR: Scout Fly On Ground, Back",
|
||||
393311: "GR: Scout Fly On Left Ledge",
|
||||
65631: "GR: Scout Fly On Right Ledge",
|
||||
262239: "GR: Scout Fly On Middle Ledge, Left",
|
||||
131167: "GR: Scout Fly On Middle Ledge, Right",
|
||||
196703: "GR: Scout Fly On Top Ledge"
|
||||
}
|
||||
|
||||
# Sandover Village
|
||||
locSV_scoutTable = {
|
||||
262219: "SV: Scout Fly In Fisherman's House",
|
||||
327755: "SV: Scout Fly In Mayor's House",
|
||||
131147: "SV: Scout Fly Under Bridge",
|
||||
65611: "SV: Scout Fly Behind Sculptor's House",
|
||||
75: "SV: Scout Fly Overlooking Farmer's House",
|
||||
393291: "SV: Scout Fly Near Oracle",
|
||||
196683: "SV: Scout Fly In Farmer's House"
|
||||
}
|
||||
|
||||
# Forbidden Jungle
|
||||
locFJ_scoutTable = {
|
||||
393223: "FJ: Scout Fly At End Of Path",
|
||||
262151: "FJ: Scout Fly On Spiral Of Stumps",
|
||||
7: "FJ: Scout Fly Near Dark Eco Boxes",
|
||||
196615: "FJ: Scout Fly At End Of River",
|
||||
131079: "FJ: Scout Fly Behind Lurker Machine",
|
||||
327687: "FJ: Scout Fly Around Temple Spire",
|
||||
65543: "FJ: Scout Fly On Top Of Temple"
|
||||
}
|
||||
|
||||
# Sentinel Beach
|
||||
locSB_scoutTable = {
|
||||
327700: "SB: Scout Fly At Entrance",
|
||||
20: "SB: Scout Fly Overlooking Locked Boxes",
|
||||
65556: "SB: Scout Fly On Path To Flut Flut",
|
||||
262164: "SB: Scout Fly Under Wood Pillars",
|
||||
196628: "SB: Scout Fly Overlooking Blue Eco Vent",
|
||||
131092: "SB: Scout Fly Overlooking Green Eco Vents",
|
||||
393236: "SB: Scout Fly On Sentinel"
|
||||
}
|
||||
|
||||
# Misty Island
|
||||
locMI_scoutTable = {
|
||||
327708: "MI: Scout Fly Overlooking Entrance",
|
||||
65564: "MI: Scout Fly On Ledge Near Arena Entrance",
|
||||
262172: "MI: Scout Fly Near Arena Door",
|
||||
28: "MI: Scout Fly On Ledge Near Arena Exit",
|
||||
131100: "MI: Scout Fly On Ship",
|
||||
196636: "MI: Scout Fly On Barrel Ramps",
|
||||
393244: "MI: Scout Fly On Zoomer Ramps"
|
||||
}
|
||||
|
||||
# Fire Canyon
|
||||
locFC_scoutTable = {
|
||||
393284: "FC: Scout Fly 1",
|
||||
68: "FC: Scout Fly 2",
|
||||
65604: "FC: Scout Fly 3",
|
||||
196676: "FC: Scout Fly 4",
|
||||
131140: "FC: Scout Fly 5",
|
||||
262212: "FC: Scout Fly 6",
|
||||
327748: "FC: Scout Fly 7"
|
||||
}
|
||||
|
||||
# Rock Village
|
||||
locRV_scoutTable = {
|
||||
76: "RV: Scout Fly Behind Sage's Hut",
|
||||
131148: "RV: Scout Fly Near Waterfall",
|
||||
196684: "RV: Scout Fly Behind Geologist",
|
||||
262220: "RV: Scout Fly Behind Fiery Boulder",
|
||||
65612: "RV: Scout Fly On Dock",
|
||||
327756: "RV: Scout Fly At Pontoon Bridge",
|
||||
393292: "RV: Scout Fly At Boggy Swamp Entrance"
|
||||
}
|
||||
|
||||
# Precursor Basin
|
||||
locPB_scoutTable = {
|
||||
196665: "PB: Scout Fly Overlooking Entrance",
|
||||
393273: "PB: Scout Fly Near Mole Hole",
|
||||
131129: "PB: Scout Fly At Purple Ring Start",
|
||||
65593: "PB: Scout Fly Near Dark Eco Plant, Above",
|
||||
57: "PB: Scout Fly At Blue Ring Start",
|
||||
262201: "PB: Scout Fly Before Big Jump",
|
||||
327737: "PB: Scout Fly Near Dark Eco Plant, Below"
|
||||
}
|
||||
|
||||
# Lost Precursor City
|
||||
locLPC_scoutTable = {
|
||||
262193: "LPC: Scout Fly First Room",
|
||||
131121: "LPC: Scout Fly Before Second Room",
|
||||
393265: "LPC: Scout Fly Second Room, Near Orb Vent",
|
||||
196657: "LPC: Scout Fly Second Room, On Path To Cell",
|
||||
49: "LPC: Scout Fly Second Room, Green Pipe", # Sunken Pipe Game, special cases. See `got-buzzer?`
|
||||
65585: "LPC: Scout Fly Second Room, Blue Pipe", # Sunken Pipe Game, special cases. See `got-buzzer?`
|
||||
327729: "LPC: Scout Fly Across Steam Vents"
|
||||
}
|
||||
|
||||
# Boggy Swamp
|
||||
locBS_scoutTable = {
|
||||
43: "BS: Scout Fly Near Entrance",
|
||||
393259: "BS: Scout Fly Over First Jump Pad",
|
||||
65579: "BS: Scout Fly Over Second Jump Pad",
|
||||
262187: "BS: Scout Fly Across Black Swamp",
|
||||
327723: "BS: Scout Fly Overlooking Flut Flut",
|
||||
131115: "BS: Scout Fly On Flut Flut Platforms",
|
||||
196651: "BS: Scout Fly In Field Of Boxes"
|
||||
}
|
||||
|
||||
# Mountain Pass
|
||||
locMP_scoutTable = {
|
||||
88: "MP: Scout Fly 1",
|
||||
65624: "MP: Scout Fly 2",
|
||||
131160: "MP: Scout Fly 3",
|
||||
196696: "MP: Scout Fly 4",
|
||||
262232: "MP: Scout Fly 5",
|
||||
327768: "MP: Scout Fly 6",
|
||||
393304: "MP: Scout Fly 7"
|
||||
}
|
||||
|
||||
# Volcanic Crater
|
||||
locVC_scoutTable = {
|
||||
262221: "VC: Scout Fly In Miner's Cave",
|
||||
393293: "VC: Scout Fly Near Oracle",
|
||||
196685: "VC: Scout Fly On Stone Platforms",
|
||||
131149: "VC: Scout Fly Near Lava Tube",
|
||||
77: "VC: Scout Fly At Minecart Junction",
|
||||
65613: "VC: Scout Fly Near Spider Cave",
|
||||
327757: "VC: Scout Fly Near Mountain Pass"
|
||||
}
|
||||
|
||||
# Spider Cave
|
||||
locSC_scoutTable = {
|
||||
327765: "SC: Scout Fly Near Dark Cave Entrance",
|
||||
262229: "SC: Scout Fly In Dark Cave",
|
||||
393301: "SC: Scout Fly Main Cave, Overlooking Entrance",
|
||||
196693: "SC: Scout Fly Main Cave, Near Dark Crystal",
|
||||
131157: "SC: Scout Fly Main Cave, Near Robot Cave Entrance",
|
||||
85: "SC: Scout Fly Robot Cave, At Bottom Level",
|
||||
65621: "SC: Scout Fly Robot Cave, At Top Level",
|
||||
}
|
||||
|
||||
# Snowy Mountain
|
||||
locSM_scoutTable = {
|
||||
65: "SM: Scout Fly Near Entrance",
|
||||
327745: "SM: Scout Fly Near Frozen Box",
|
||||
65601: "SM: Scout Fly Near Yellow Eco Switch",
|
||||
131137: "SM: Scout Fly On Cliff near Flut Flut",
|
||||
393281: "SM: Scout Fly Under Bridge To Fort",
|
||||
196673: "SM: Scout Fly On Top Of Fort Tower",
|
||||
262209: "SM: Scout Fly On Top Of Fort"
|
||||
}
|
||||
|
||||
# Lava Tube
|
||||
locLT_scoutTable = {
|
||||
90: "LT: Scout Fly 1",
|
||||
65626: "LT: Scout Fly 2",
|
||||
327770: "LT: Scout Fly 3",
|
||||
262234: "LT: Scout Fly 4",
|
||||
131162: "LT: Scout Fly 5",
|
||||
196698: "LT: Scout Fly 6",
|
||||
393306: "LT: Scout Fly 7"
|
||||
}
|
||||
|
||||
# Gol and Maias Citadel
|
||||
locGMC_scoutTable = {
|
||||
91: "GMC: Scout Fly At Entrance",
|
||||
65627: "GMC: Scout Fly Main Room, Left of Robot",
|
||||
196699: "GMC: Scout Fly Main Room, Right of Robot",
|
||||
262235: "GMC: Scout Fly Before Jumping Lurkers",
|
||||
393307: "GMC: Scout Fly At Blast Furnace",
|
||||
131163: "GMC: Scout Fly At Launch Pad Room",
|
||||
327771: "GMC: Scout Fly Top Of Rotating Tower"
|
||||
}
|
||||
51
worlds/jakanddaxter/locs/special_locations.py
Normal file
51
worlds/jakanddaxter/locs/special_locations.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from ..game_id import jak1_id
|
||||
|
||||
# These are special checks that the game normally does not track. They are not game entities and thus
|
||||
# don't have game ID's.
|
||||
|
||||
# Normally, for example, completing the fishing minigame is what gives you access to the
|
||||
# fisherman's boat to get to Misty Island. The game treats completion of the fishing minigame as well as the
|
||||
# power cell you receive as one and the same. The fisherman only gives you one item, a power cell.
|
||||
|
||||
# We're significantly altering the game logic here to decouple these concepts. First, completing the fishing minigame
|
||||
# now counts as 2 Location checks. Second, the fisherman should give you a power cell (a generic item) as well as
|
||||
# the "keys" to his boat (a special item). It is the "keys" that we are defining in this file, and the respective
|
||||
# Item representing those keys will be defined in Items.py. These aren't real in the sense that
|
||||
# they have a model and texture, they are just the logical representation of the boat unlock.
|
||||
|
||||
# We can use 2^11 to offset these from scout flies, just like we offset scout flies from power cells
|
||||
# by 2^10. Even with the high-16 reminder bits, scout flies don't exceed an ID of (jak1_id + 1887).
|
||||
special_offset = 2048
|
||||
|
||||
|
||||
# These helper functions do all the math required to get information about each
|
||||
# special check and translate its ID between AP and OpenGOAL.
|
||||
def to_ap_id(game_id: int) -> int:
|
||||
if game_id >= jak1_id:
|
||||
raise ValueError(f"Attempted to convert {game_id} to an AP ID, but it already is one.")
|
||||
return jak1_id + special_offset + game_id # Add the offsets and the orb Actor ID.
|
||||
|
||||
|
||||
def to_game_id(ap_id: int) -> int:
|
||||
if ap_id < jak1_id:
|
||||
raise ValueError(f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one.")
|
||||
return ap_id - jak1_id - special_offset # Reverse process, subtract the offsets.
|
||||
|
||||
|
||||
# The ID's you see below correlate to each of their respective game-tasks, even though they are separate.
|
||||
# This makes it easier for the new game logic to know what relates to what. I hope. God I hope.
|
||||
|
||||
loc_specialTable = {
|
||||
5: "Fisherman's Boat",
|
||||
4: "Jungle Elevator",
|
||||
2: "Blue Eco Switch",
|
||||
17: "Flut Flut",
|
||||
33: "Warrior's Pontoons",
|
||||
105: "Snowy Mountain Gondola",
|
||||
60: "Yellow Eco Switch",
|
||||
63: "Snowy Fort Gate",
|
||||
71: "Freed The Blue Sage",
|
||||
72: "Freed The Red Sage",
|
||||
73: "Freed The Yellow Sage",
|
||||
70: "Freed The Green Sage",
|
||||
}
|
||||
262
worlds/jakanddaxter/options.py
Normal file
262
worlds/jakanddaxter/options.py
Normal file
@@ -0,0 +1,262 @@
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from Options import PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Range, DefaultOnToggle, OptionCounter
|
||||
from .items import trap_item_table
|
||||
|
||||
|
||||
class StaticGetter:
|
||||
def __init__(self, func):
|
||||
self.fget = func
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
return self.fget(owner)
|
||||
|
||||
|
||||
@StaticGetter
|
||||
def determine_range_end(cls) -> int:
|
||||
from . import JakAndDaxterWorld
|
||||
enforce_friendly_options = JakAndDaxterWorld.settings.enforce_friendly_options
|
||||
return cls.friendly_maximum if enforce_friendly_options else cls.absolute_maximum
|
||||
|
||||
|
||||
class EnableMoveRandomizer(Toggle):
|
||||
"""Include movement options as items in the randomizer. Until you find his other moves, Jak is limited to
|
||||
running, swimming, single-jumping, and shooting yellow eco through his goggles.
|
||||
|
||||
This adds 11 items to the pool."""
|
||||
display_name = "Enable Move Randomizer"
|
||||
|
||||
|
||||
class EnableOrbsanity(Choice):
|
||||
"""Include bundles of Precursor Orbs as checks. Every time you collect the chosen number of orbs, you will trigger
|
||||
another check.
|
||||
|
||||
Per Level: bundles are for each level in the game.
|
||||
Global: bundles carry over level to level.
|
||||
|
||||
This adds a number of Items and Locations to the pool inversely proportional to the size of the bundle.
|
||||
For example, if your bundle size is 20 orbs, you will add 100 items to the pool. If your bundle size is 250 orbs,
|
||||
you will add 8 items to the pool."""
|
||||
display_name = "Enable Orbsanity"
|
||||
option_off = 0
|
||||
option_per_level = 1
|
||||
option_global = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class GlobalOrbsanityBundleSize(Choice):
|
||||
"""The orb bundle size for Global Orbsanity. This only applies if "Enable Orbsanity" is set to "Global."
|
||||
There are 2000 orbs in the game, so your bundle size must be a factor of 2000.
|
||||
|
||||
Multiplayer Minimum: 10
|
||||
Multiplayer Maximum: 200"""
|
||||
display_name = "Global Orbsanity Bundle Size"
|
||||
option_1_orb = 1
|
||||
option_2_orbs = 2
|
||||
option_4_orbs = 4
|
||||
option_5_orbs = 5
|
||||
option_8_orbs = 8
|
||||
option_10_orbs = 10
|
||||
option_16_orbs = 16
|
||||
option_20_orbs = 20
|
||||
option_25_orbs = 25
|
||||
option_40_orbs = 40
|
||||
option_50_orbs = 50
|
||||
option_80_orbs = 80
|
||||
option_100_orbs = 100
|
||||
option_125_orbs = 125
|
||||
option_200_orbs = 200
|
||||
option_250_orbs = 250
|
||||
option_400_orbs = 400
|
||||
option_500_orbs = 500
|
||||
option_1000_orbs = 1000
|
||||
option_2000_orbs = 2000
|
||||
friendly_minimum = 10
|
||||
friendly_maximum = 200
|
||||
default = 20
|
||||
|
||||
|
||||
class PerLevelOrbsanityBundleSize(Choice):
|
||||
"""The orb bundle size for Per Level Orbsanity. This only applies if "Enable Orbsanity" is set to "Per Level."
|
||||
There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50.
|
||||
|
||||
Multiplayer Minimum: 10"""
|
||||
display_name = "Per Level Orbsanity Bundle Size"
|
||||
option_1_orb = 1
|
||||
option_2_orbs = 2
|
||||
option_5_orbs = 5
|
||||
option_10_orbs = 10
|
||||
option_25_orbs = 25
|
||||
option_50_orbs = 50
|
||||
friendly_minimum = 10
|
||||
default = 25
|
||||
|
||||
|
||||
class FireCanyonCellCount(Range):
|
||||
"""The number of power cells you need to cross Fire Canyon. This value is restricted to a safe maximum value to
|
||||
ensure valid singleplayer games and non-disruptive multiplayer games, but the host can remove this restriction by
|
||||
turning off enforce_friendly_options in host.yaml."""
|
||||
display_name = "Fire Canyon Cell Count"
|
||||
friendly_maximum = 30
|
||||
absolute_maximum = 100
|
||||
range_start = 0
|
||||
range_end = determine_range_end
|
||||
default = 20
|
||||
|
||||
|
||||
class MountainPassCellCount(Range):
|
||||
"""The number of power cells you need to reach Klaww and cross Mountain Pass. This value is restricted to a safe
|
||||
maximum value to ensure valid singleplayer games and non-disruptive multiplayer games, but the host can
|
||||
remove this restriction by turning off enforce_friendly_options in host.yaml."""
|
||||
display_name = "Mountain Pass Cell Count"
|
||||
friendly_maximum = 60
|
||||
absolute_maximum = 100
|
||||
range_start = 0
|
||||
range_end = determine_range_end
|
||||
default = 45
|
||||
|
||||
|
||||
class LavaTubeCellCount(Range):
|
||||
"""The number of power cells you need to cross Lava Tube. This value is restricted to a safe maximum value to
|
||||
ensure valid singleplayer games and non-disruptive multiplayer games, but the host can remove this restriction by
|
||||
turning off enforce_friendly_options in host.yaml."""
|
||||
display_name = "Lava Tube Cell Count"
|
||||
friendly_maximum = 90
|
||||
absolute_maximum = 100
|
||||
range_start = 0
|
||||
range_end = determine_range_end
|
||||
default = 72
|
||||
|
||||
|
||||
class EnableOrderedCellCounts(DefaultOnToggle):
|
||||
"""Reorder the Cell Count requirements for vehicle sections to be in ascending order.
|
||||
|
||||
For example, if Fire Canyon Cell Count, Mountain Pass Cell Count, and Lava Tube Cell Count are 60, 30, and 40
|
||||
respectively, they will be reordered to 30, 40, and 60."""
|
||||
display_name = "Enable Ordered Cell Counts"
|
||||
|
||||
|
||||
class RequirePunchForKlaww(DefaultOnToggle):
|
||||
"""Force the Punch move to come before Klaww. Disabling this setting may require Jak to fight Klaww
|
||||
and Gol and Maia by shooting yellow eco through his goggles. This only applies if "Enable Move Randomizer" is ON."""
|
||||
display_name = "Require Punch For Klaww"
|
||||
|
||||
|
||||
# 222 is the absolute maximum because there are 9 citizen trades and 2000 orbs to trade (2000/9 = 222).
|
||||
class CitizenOrbTradeAmount(Range):
|
||||
"""The number of orbs you need to trade to citizens for a power cell (Mayor, Uncle, etc.).
|
||||
|
||||
Along with Oracle Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000).
|
||||
The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades).
|
||||
|
||||
This value is restricted to a safe maximum value to ensure valid singleplayer games and non-disruptive
|
||||
multiplayer games, but the host can remove this restriction by turning off enforce_friendly_options in host.yaml."""
|
||||
display_name = "Citizen Orb Trade Amount"
|
||||
friendly_maximum = 120
|
||||
absolute_maximum = 222
|
||||
range_start = 0
|
||||
range_end = determine_range_end
|
||||
default = 90
|
||||
|
||||
|
||||
# 333 is the absolute maximum because there are 6 oracle trades and 2000 orbs to trade (2000/6 = 333).
|
||||
class OracleOrbTradeAmount(Range):
|
||||
"""The number of orbs you need to trade to the Oracles for a power cell.
|
||||
|
||||
Along with Citizen Orb Trade Amount, this setting cannot exceed the total number of orbs in the game (2000).
|
||||
The equation to determine the total number of trade orbs is (9 * Citizen Trades) + (6 * Oracle Trades).
|
||||
|
||||
This value is restricted to a safe maximum value to ensure valid singleplayer games and non-disruptive
|
||||
multiplayer games, but the host can remove this restriction by turning off enforce_friendly_options in host.yaml."""
|
||||
display_name = "Oracle Orb Trade Amount"
|
||||
friendly_maximum = 150
|
||||
absolute_maximum = 333
|
||||
range_start = 0
|
||||
range_end = determine_range_end
|
||||
default = 120
|
||||
|
||||
|
||||
class FillerPowerCellsReplacedWithTraps(Range):
|
||||
"""
|
||||
The number of filler power cells that will be replaced with traps. This does not affect the number of progression
|
||||
power cells.
|
||||
|
||||
If this value is greater than the number of filler power cells, then they will all be replaced with traps.
|
||||
"""
|
||||
display_name = "Filler Power Cells Replaced With Traps"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 0
|
||||
|
||||
|
||||
class FillerOrbBundlesReplacedWithTraps(Range):
|
||||
"""
|
||||
The number of filler orb bundles that will be replaced with traps. This does not affect the number of progression
|
||||
orb bundles. This only applies if "Enable Orbsanity" is set to "Per Level" or "Global."
|
||||
|
||||
If this value is greater than the number of filler orb bundles, then they will all be replaced with traps.
|
||||
"""
|
||||
display_name = "Filler Orb Bundles Replaced With Traps"
|
||||
range_start = 0
|
||||
range_end = 2000
|
||||
default = 0
|
||||
|
||||
|
||||
class TrapEffectDuration(Range):
|
||||
"""
|
||||
The length of time, in seconds, that a trap effect lasts.
|
||||
"""
|
||||
display_name = "Trap Effect Duration"
|
||||
range_start = 5
|
||||
range_end = 60
|
||||
default = 30
|
||||
|
||||
|
||||
class TrapWeights(OptionCounter):
|
||||
"""
|
||||
The list of traps and corresponding weights that will be randomly added to the item pool. A trap with weight 10 is
|
||||
twice as likely to appear as a trap with weight 5. Set a weight to 0 to prevent that trap from appearing altogether.
|
||||
If all weights are 0, no traps are created, overriding the values of "Filler * Replaced With Traps."
|
||||
"""
|
||||
display_name = "Trap Weights"
|
||||
min = 0
|
||||
default = {trap: 1 for trap in trap_item_table.values()}
|
||||
valid_keys = sorted({trap for trap in trap_item_table.values()})
|
||||
|
||||
@cached_property
|
||||
def weights_pair(self) -> tuple[list[str], list[int]]:
|
||||
return list(self.value.keys()), list(self.value.values())
|
||||
|
||||
|
||||
class CompletionCondition(Choice):
|
||||
"""Set the goal for completing the game."""
|
||||
display_name = "Completion Condition"
|
||||
option_cross_fire_canyon = 69
|
||||
option_cross_mountain_pass = 87
|
||||
option_cross_lava_tube = 89
|
||||
option_defeat_dark_eco_plant = 6
|
||||
option_defeat_klaww = 86
|
||||
option_defeat_gol_and_maia = 112
|
||||
option_open_100_cell_door = 116
|
||||
default = 112
|
||||
|
||||
|
||||
@dataclass
|
||||
class JakAndDaxterOptions(PerGameCommonOptions):
|
||||
enable_move_randomizer: EnableMoveRandomizer
|
||||
enable_orbsanity: EnableOrbsanity
|
||||
global_orbsanity_bundle_size: GlobalOrbsanityBundleSize
|
||||
level_orbsanity_bundle_size: PerLevelOrbsanityBundleSize
|
||||
fire_canyon_cell_count: FireCanyonCellCount
|
||||
mountain_pass_cell_count: MountainPassCellCount
|
||||
lava_tube_cell_count: LavaTubeCellCount
|
||||
enable_ordered_cell_counts: EnableOrderedCellCounts
|
||||
require_punch_for_klaww: RequirePunchForKlaww
|
||||
citizen_orb_trade_amount: CitizenOrbTradeAmount
|
||||
oracle_orb_trade_amount: OracleOrbTradeAmount
|
||||
filler_power_cells_replaced_with_traps: FillerPowerCellsReplacedWithTraps
|
||||
filler_orb_bundles_replaced_with_traps: FillerOrbBundlesReplacedWithTraps
|
||||
trap_effect_duration: TrapEffectDuration
|
||||
trap_weights: TrapWeights
|
||||
jak_completion_condition: CompletionCondition
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
132
worlds/jakanddaxter/regions.py
Normal file
132
worlds/jakanddaxter/regions.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import typing
|
||||
from Options import OptionError
|
||||
from .items import item_table
|
||||
from .options import EnableOrbsanity, CompletionCondition
|
||||
from .rules import can_reach_orbs_global
|
||||
from .locs import cell_locations as cells, scout_locations as scouts
|
||||
from .regs import (geyser_rock_regions as geyser_rock,
|
||||
sandover_village_regions as sandover_village,
|
||||
forbidden_jungle_regions as forbidden_jungle,
|
||||
sentinel_beach_regions as sentinel_beach,
|
||||
misty_island_regions as misty_island,
|
||||
fire_canyon_regions as fire_canyon,
|
||||
rock_village_regions as rock_village,
|
||||
precursor_basin_regions as precursor_basin,
|
||||
lost_precursor_city_regions as lost_precursor_city,
|
||||
boggy_swamp_regions as boggy_swamp,
|
||||
mountain_pass_regions as mountain_pass,
|
||||
volcanic_crater_regions as volcanic_crater,
|
||||
spider_cave_regions as spider_cave,
|
||||
snowy_mountain_regions as snowy_mountain,
|
||||
lava_tube_regions as lava_tube,
|
||||
gol_and_maias_citadel_regions as gol_and_maias_citadel)
|
||||
from .regs.region_base import JakAndDaxterRegion
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from . import JakAndDaxterWorld
|
||||
|
||||
|
||||
def create_regions(world: "JakAndDaxterWorld"):
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
# Always start with Menu.
|
||||
menu = JakAndDaxterRegion("Menu", player, multiworld)
|
||||
multiworld.regions.append(menu)
|
||||
|
||||
# Build the special "Free 7 Scout Flies" Region. This is a virtual region always accessible to Menu.
|
||||
# The Locations within are automatically checked when you receive the 7th scout fly for the corresponding cell.
|
||||
free7 = JakAndDaxterRegion("'Free 7 Scout Flies' Power Cells", player, multiworld)
|
||||
free7.add_cell_locations(cells.loc7SF_cellTable.keys())
|
||||
for scout_fly_cell in free7.locations:
|
||||
|
||||
# Translate from Cell AP ID to Scout AP ID using game ID as an intermediary.
|
||||
scout_fly_id = scouts.to_ap_id(cells.to_game_id(typing.cast(int, scout_fly_cell.address)))
|
||||
scout_fly_cell.access_rule = lambda state, flies=scout_fly_id: state.has(item_table[flies], player, 7)
|
||||
multiworld.regions.append(free7)
|
||||
menu.connect(free7)
|
||||
|
||||
# If Global Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Menu. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_global:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld)
|
||||
|
||||
bundle_count = 2000 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
|
||||
# Unlike Per-Level Orbsanity, Global Orbsanity Locations always have a level_index of 16.
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(16,
|
||||
bundle_index,
|
||||
access_rule=lambda state, orb_amount=amount:
|
||||
can_reach_orbs_global(state, player, world, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
menu.connect(orbs)
|
||||
|
||||
# Build all regions. Include their intra-connecting Rules, their Locations, and their Location access rules.
|
||||
gr = geyser_rock.build_regions("Geyser Rock", world)
|
||||
sv = sandover_village.build_regions("Sandover Village", world)
|
||||
fj, fjp = forbidden_jungle.build_regions("Forbidden Jungle", world)
|
||||
sb = sentinel_beach.build_regions("Sentinel Beach", world)
|
||||
mi = misty_island.build_regions("Misty Island", world)
|
||||
fc = fire_canyon.build_regions("Fire Canyon", world)
|
||||
rv, rvp, rvc = rock_village.build_regions("Rock Village", world)
|
||||
pb = precursor_basin.build_regions("Precursor Basin", world)
|
||||
lpc = lost_precursor_city.build_regions("Lost Precursor City", world)
|
||||
bs = boggy_swamp.build_regions("Boggy Swamp", world)
|
||||
mp, mpr = mountain_pass.build_regions("Mountain Pass", world)
|
||||
vc = volcanic_crater.build_regions("Volcanic Crater", world)
|
||||
sc = spider_cave.build_regions("Spider Cave", world)
|
||||
sm = snowy_mountain.build_regions("Snowy Mountain", world)
|
||||
lt = lava_tube.build_regions("Lava Tube", world)
|
||||
gmc, fb, fd = gol_and_maias_citadel.build_regions("Gol and Maia's Citadel", world)
|
||||
|
||||
# Configurable counts of cells for connector levels.
|
||||
fc_count = options.fire_canyon_cell_count.value
|
||||
mp_count = options.mountain_pass_cell_count.value
|
||||
lt_count = options.lava_tube_cell_count.value
|
||||
|
||||
# Define the interconnecting rules.
|
||||
menu.connect(gr)
|
||||
gr.connect(sv) # Geyser Rock modified to let you leave at any time.
|
||||
sv.connect(fj)
|
||||
sv.connect(sb)
|
||||
sv.connect(mi, rule=lambda state: state.has("Fisherman's Boat", player))
|
||||
sv.connect(fc, rule=lambda state: state.has("Power Cell", player, fc_count)) # Normally 20.
|
||||
fc.connect(rv)
|
||||
rv.connect(pb)
|
||||
rv.connect(lpc)
|
||||
rvp.connect(bs) # rv->rvp/rvc connections defined internally by RockVillageRegions.
|
||||
rvc.connect(mp, rule=lambda state: state.has("Power Cell", player, mp_count)) # Normally 45.
|
||||
mpr.connect(vc) # mp->mpr connection defined internally by MountainPassRegions.
|
||||
vc.connect(sc)
|
||||
vc.connect(sm, rule=lambda state: state.has("Snowy Mountain Gondola", player))
|
||||
vc.connect(lt, rule=lambda state: state.has("Power Cell", player, lt_count)) # Normally 72.
|
||||
lt.connect(gmc) # gmc->fb connection defined internally by GolAndMaiasCitadelRegions.
|
||||
|
||||
# Set the completion condition.
|
||||
if options.jak_completion_condition == CompletionCondition.option_cross_fire_canyon:
|
||||
multiworld.completion_condition[player] = lambda state: state.can_reach(rv, "Region", player)
|
||||
|
||||
elif options.jak_completion_condition == CompletionCondition.option_cross_mountain_pass:
|
||||
multiworld.completion_condition[player] = lambda state: state.can_reach(vc, "Region", player)
|
||||
|
||||
elif options.jak_completion_condition == CompletionCondition.option_cross_lava_tube:
|
||||
multiworld.completion_condition[player] = lambda state: state.can_reach(gmc, "Region", player)
|
||||
|
||||
elif options.jak_completion_condition == CompletionCondition.option_defeat_dark_eco_plant:
|
||||
multiworld.completion_condition[player] = lambda state: state.can_reach(fjp, "Region", player)
|
||||
|
||||
elif options.jak_completion_condition == CompletionCondition.option_defeat_klaww:
|
||||
multiworld.completion_condition[player] = lambda state: state.can_reach(mp, "Region", player)
|
||||
|
||||
elif options.jak_completion_condition == CompletionCondition.option_defeat_gol_and_maia:
|
||||
multiworld.completion_condition[player] = lambda state: state.can_reach(fb, "Region", player)
|
||||
|
||||
elif options.jak_completion_condition == CompletionCondition.option_open_100_cell_door:
|
||||
multiworld.completion_condition[player] = lambda state: state.can_reach(fd, "Region", player)
|
||||
|
||||
else:
|
||||
raise OptionError(f"{world.player_name}: Unknown completion goal ID "
|
||||
f"({options.jak_completion_condition.value}).")
|
||||
0
worlds/jakanddaxter/regs/__init__.py
Normal file
0
worlds/jakanddaxter/regs/__init__.py
Normal file
174
worlds/jakanddaxter/regs/boggy_swamp_regions.py
Normal file
174
worlds/jakanddaxter/regs/boggy_swamp_regions.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from BaseClasses import CollectionState
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_fight, can_reach_orbs_level
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> JakAndDaxterRegion:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
# This level is full of short-medium gaps that cannot be crossed by single jump alone.
|
||||
# These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...)
|
||||
def can_jump_farther(state: CollectionState, p: int) -> bool:
|
||||
return (state.has_any(("Double Jump", "Jump Kick"), p)
|
||||
or state.has_all(("Punch", "Punch Uppercut"), p))
|
||||
|
||||
def can_jump_higher(state: CollectionState, p: int) -> bool:
|
||||
return (state.has("Double Jump", p)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), p)
|
||||
or state.has_all(("Crouch", "Crouch Uppercut"), p)
|
||||
or state.has_all(("Punch", "Punch Uppercut"), p))
|
||||
|
||||
# Orb crates and fly box in this area can be gotten with yellow eco and goggles.
|
||||
# Start with the first yellow eco cluster near first_bats and work your way backward toward the entrance.
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23)
|
||||
main_area.add_fly_locations([43])
|
||||
|
||||
# Includes 4 orbs collectable with the blue eco vent.
|
||||
first_bats = JakAndDaxterRegion("First Bats Area", player, multiworld, level_name, 4)
|
||||
|
||||
first_jump_pad = JakAndDaxterRegion("First Jump Pad", player, multiworld, level_name, 0)
|
||||
first_jump_pad.add_fly_locations([393259])
|
||||
|
||||
# The tethers in this level are all out of order... a casual playthrough has the following order for the cell ID's:
|
||||
# 42, 39, 40, 41. So that is the order we're calling "first, second, third, fourth".
|
||||
|
||||
# First tether cell is collectable with yellow eco and goggles.
|
||||
first_tether = JakAndDaxterRegion("First Tether", player, multiworld, level_name, 7)
|
||||
first_tether.add_cell_locations([42])
|
||||
|
||||
# This rat colony has 3 orbs on top of it, requires special movement.
|
||||
first_tether_rat_colony = JakAndDaxterRegion("First Tether Rat Colony", player, multiworld, level_name, 3)
|
||||
|
||||
# If quick enough, combat not required.
|
||||
second_jump_pad = JakAndDaxterRegion("Second Jump Pad", player, multiworld, level_name, 0)
|
||||
second_jump_pad.add_fly_locations([65579])
|
||||
|
||||
first_pole_course = JakAndDaxterRegion("First Pole Course", player, multiworld, level_name, 28)
|
||||
|
||||
# You can break this tether with a yellow eco vent and goggles,
|
||||
# but you can't reach the platform unless you can jump high.
|
||||
second_tether = JakAndDaxterRegion("Second Tether", player, multiworld, level_name, 0)
|
||||
second_tether.add_cell_locations([39], access_rule=lambda state: can_jump_higher(state, player))
|
||||
|
||||
# Fly and orbs are collectable with nearby blue eco cluster.
|
||||
second_bats = JakAndDaxterRegion("Second Bats Area", player, multiworld, level_name, 27)
|
||||
second_bats.add_fly_locations([262187], access_rule=lambda state: can_jump_farther(state, player))
|
||||
|
||||
third_jump_pad = JakAndDaxterRegion("Third Jump Pad (Arena)", player, multiworld, level_name, 0)
|
||||
third_jump_pad.add_cell_locations([38], access_rule=lambda state: can_fight(state, player))
|
||||
|
||||
# The platform for the third tether might look high, but you can get a boost from the yellow eco vent.
|
||||
fourth_jump_pad = JakAndDaxterRegion("Fourth Jump Pad (Third Tether)", player, multiworld, level_name, 9)
|
||||
fourth_jump_pad.add_cell_locations([40])
|
||||
|
||||
# Orbs collectable here with yellow eco and goggles.
|
||||
flut_flut_pad = JakAndDaxterRegion("Flut Flut Pad", player, multiworld, level_name, 36)
|
||||
|
||||
flut_flut_course = JakAndDaxterRegion("Flut Flut Course", player, multiworld, level_name, 23)
|
||||
flut_flut_course.add_cell_locations([37])
|
||||
flut_flut_course.add_fly_locations([327723, 131115])
|
||||
|
||||
# Includes some orbs on the way to the cabin, blue+yellow eco to collect.
|
||||
farthy_snacks = JakAndDaxterRegion("Farthy's Snacks", player, multiworld, level_name, 7)
|
||||
farthy_snacks.add_cell_locations([36])
|
||||
|
||||
# Scout fly in this field can be broken with yellow eco.
|
||||
box_field = JakAndDaxterRegion("Field of Boxes", player, multiworld, level_name, 10)
|
||||
box_field.add_fly_locations([196651])
|
||||
|
||||
last_tar_pit = JakAndDaxterRegion("Last Tar Pit", player, multiworld, level_name, 12)
|
||||
|
||||
fourth_tether = JakAndDaxterRegion("Fourth Tether", player, multiworld, level_name, 11)
|
||||
fourth_tether.add_cell_locations([41], access_rule=lambda state: can_jump_higher(state, player))
|
||||
|
||||
main_area.connect(first_bats, rule=lambda state: can_jump_farther(state, player))
|
||||
|
||||
first_bats.connect(main_area)
|
||||
first_bats.connect(first_jump_pad)
|
||||
first_bats.connect(first_tether)
|
||||
|
||||
first_jump_pad.connect(first_bats)
|
||||
|
||||
first_tether.connect(first_bats)
|
||||
first_tether.connect(first_tether_rat_colony, rule=lambda state:
|
||||
(state.has_all(("Roll", "Roll Jump"), player)
|
||||
or state.has_all(("Double Jump", "Jump Kick"), player)))
|
||||
first_tether.connect(second_jump_pad)
|
||||
first_tether.connect(first_pole_course)
|
||||
|
||||
first_tether_rat_colony.connect(first_tether)
|
||||
|
||||
second_jump_pad.connect(first_tether)
|
||||
|
||||
first_pole_course.connect(first_tether)
|
||||
first_pole_course.connect(second_tether)
|
||||
|
||||
second_tether.connect(first_pole_course, rule=lambda state: can_jump_higher(state, player))
|
||||
second_tether.connect(second_bats)
|
||||
|
||||
second_bats.connect(second_tether)
|
||||
second_bats.connect(third_jump_pad)
|
||||
second_bats.connect(fourth_jump_pad)
|
||||
second_bats.connect(flut_flut_pad)
|
||||
|
||||
third_jump_pad.connect(second_bats)
|
||||
fourth_jump_pad.connect(second_bats)
|
||||
|
||||
flut_flut_pad.connect(second_bats)
|
||||
flut_flut_pad.connect(flut_flut_course, rule=lambda state: state.has("Flut Flut", player)) # Naturally.
|
||||
flut_flut_pad.connect(farthy_snacks)
|
||||
|
||||
flut_flut_course.connect(flut_flut_pad)
|
||||
|
||||
farthy_snacks.connect(flut_flut_pad)
|
||||
farthy_snacks.connect(box_field, rule=lambda state: can_jump_higher(state, player))
|
||||
|
||||
box_field.connect(farthy_snacks, rule=lambda state: can_jump_higher(state, player))
|
||||
box_field.connect(last_tar_pit, rule=lambda state: can_jump_farther(state, player))
|
||||
|
||||
last_tar_pit.connect(box_field, rule=lambda state: can_jump_farther(state, player))
|
||||
last_tar_pit.connect(fourth_tether, rule=lambda state: can_jump_farther(state, player))
|
||||
|
||||
fourth_tether.connect(last_tar_pit, rule=lambda state: can_jump_farther(state, player))
|
||||
fourth_tether.connect(main_area) # Fall down.
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
world.level_to_regions[level_name].append(first_bats)
|
||||
world.level_to_regions[level_name].append(first_jump_pad)
|
||||
world.level_to_regions[level_name].append(first_tether)
|
||||
world.level_to_regions[level_name].append(first_tether_rat_colony)
|
||||
world.level_to_regions[level_name].append(second_jump_pad)
|
||||
world.level_to_regions[level_name].append(first_pole_course)
|
||||
world.level_to_regions[level_name].append(second_tether)
|
||||
world.level_to_regions[level_name].append(second_bats)
|
||||
world.level_to_regions[level_name].append(third_jump_pad)
|
||||
world.level_to_regions[level_name].append(fourth_jump_pad)
|
||||
world.level_to_regions[level_name].append(flut_flut_pad)
|
||||
world.level_to_regions[level_name].append(flut_flut_course)
|
||||
world.level_to_regions[level_name].append(farthy_snacks)
|
||||
world.level_to_regions[level_name].append(box_field)
|
||||
world.level_to_regions[level_name].append(last_tar_pit)
|
||||
world.level_to_regions[level_name].append(fourth_tether)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 200 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(8,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
return main_area
|
||||
38
worlds/jakanddaxter/regs/fire_canyon_regions.py
Normal file
38
worlds/jakanddaxter/regs/fire_canyon_regions.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_reach_orbs_level
|
||||
from ..locs import cell_locations as cells, scout_locations as scouts
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> JakAndDaxterRegion:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50)
|
||||
|
||||
# Everything is accessible by making contact with the zoomer.
|
||||
main_area.add_cell_locations(cells.locFC_cellTable.keys())
|
||||
main_area.add_fly_locations(scouts.locFC_scoutTable.keys())
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 50 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(5,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
return main_area
|
||||
103
worlds/jakanddaxter/regs/forbidden_jungle_regions.py
Normal file
103
worlds/jakanddaxter/regs/forbidden_jungle_regions.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_free_scout_flies, can_fight, can_reach_orbs_level
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> tuple[JakAndDaxterRegion, ...]:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 25)
|
||||
|
||||
# You can get this scout fly by running from the blue eco vent across the temple bridge,
|
||||
# falling onto the river, collecting the 3 blue clusters, using the jump pad, and running straight to the box.
|
||||
main_area.add_fly_locations([393223])
|
||||
|
||||
lurker_machine = JakAndDaxterRegion("Lurker Machine", player, multiworld, level_name, 5)
|
||||
lurker_machine.add_cell_locations([3], access_rule=lambda state: can_fight(state, player))
|
||||
|
||||
# This cell and this scout fly can both be gotten with the blue eco clusters near the jump pad.
|
||||
lurker_machine.add_cell_locations([9])
|
||||
lurker_machine.add_fly_locations([131079])
|
||||
|
||||
river = JakAndDaxterRegion("River", player, multiworld, level_name, 42)
|
||||
|
||||
# All of these can be gotten with blue eco, hitting the dark eco boxes, or by running.
|
||||
river.add_cell_locations([5, 8])
|
||||
river.add_fly_locations([7, 196615])
|
||||
river.add_special_locations([5])
|
||||
river.add_cache_locations([10369])
|
||||
|
||||
temple_exit = JakAndDaxterRegion("Temple Exit", player, multiworld, level_name, 12)
|
||||
|
||||
# This fly is too far from accessible blue eco sources.
|
||||
temple_exit.add_fly_locations([262151], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
temple_exterior = JakAndDaxterRegion("Temple Exterior", player, multiworld, level_name, 10)
|
||||
|
||||
# All of these can be gotten with blue eco and running.
|
||||
temple_exterior.add_cell_locations([4])
|
||||
temple_exterior.add_fly_locations([327687, 65543])
|
||||
temple_exterior.add_special_locations([4])
|
||||
|
||||
temple_int_pre_blue = JakAndDaxterRegion("Temple Interior (Pre Blue Eco)", player, multiworld, level_name, 17)
|
||||
temple_int_pre_blue.add_cell_locations([2])
|
||||
temple_int_pre_blue.add_special_locations([2])
|
||||
|
||||
temple_int_post_blue = JakAndDaxterRegion("Temple Interior (Post Blue Eco)", player, multiworld, level_name, 39)
|
||||
temple_int_post_blue.add_cell_locations([6], access_rule=lambda state: can_fight(state, player))
|
||||
|
||||
main_area.connect(lurker_machine) # Run and jump (tree stump platforms).
|
||||
main_area.connect(river) # Jump down.
|
||||
main_area.connect(temple_exit) # Run and jump (bridges).
|
||||
|
||||
lurker_machine.connect(main_area) # Jump down.
|
||||
lurker_machine.connect(river) # Jump down.
|
||||
lurker_machine.connect(temple_exterior) # Jump down (ledge).
|
||||
|
||||
river.connect(main_area) # Jump up (ledges near fisherman).
|
||||
river.connect(lurker_machine) # Jump pad (aim toward machine).
|
||||
river.connect(temple_exit) # Run and jump (trampolines).
|
||||
river.connect(temple_exterior) # Jump pad (aim toward temple door).
|
||||
|
||||
temple_exit.connect(main_area) # Run and jump (bridges).
|
||||
temple_exit.connect(river) # Jump down.
|
||||
temple_exit.connect(temple_exterior) # Run and jump (bridges, dodge spikes).
|
||||
|
||||
# Requires Jungle Elevator.
|
||||
temple_exterior.connect(temple_int_pre_blue, rule=lambda state: state.has("Jungle Elevator", player))
|
||||
|
||||
# Requires Blue Eco Switch.
|
||||
temple_int_pre_blue.connect(temple_int_post_blue, rule=lambda state: state.has("Blue Eco Switch", player))
|
||||
|
||||
# Requires defeating the plant boss (combat).
|
||||
temple_int_post_blue.connect(temple_exit, rule=lambda state: can_fight(state, player))
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
world.level_to_regions[level_name].append(lurker_machine)
|
||||
world.level_to_regions[level_name].append(river)
|
||||
world.level_to_regions[level_name].append(temple_exit)
|
||||
world.level_to_regions[level_name].append(temple_exterior)
|
||||
world.level_to_regions[level_name].append(temple_int_pre_blue)
|
||||
world.level_to_regions[level_name].append(temple_int_post_blue)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 150 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(3,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
return main_area, temple_int_post_blue
|
||||
48
worlds/jakanddaxter/regs/geyser_rock_regions.py
Normal file
48
worlds/jakanddaxter/regs/geyser_rock_regions.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_reach_orbs_level
|
||||
from ..locs import scout_locations as scouts
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> JakAndDaxterRegion:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 48)
|
||||
main_area.add_cell_locations([92, 93])
|
||||
main_area.add_fly_locations(scouts.locGR_scoutTable.keys()) # All Flies here are accessible with blue eco.
|
||||
|
||||
# The last 2 orbs are barely gettable with the blue eco vent, but it's pushing accessibility. So I moved them here.
|
||||
cliff = JakAndDaxterRegion("Cliff", player, multiworld, level_name, 2)
|
||||
cliff.add_cell_locations([94])
|
||||
|
||||
main_area.connect(cliff, rule=lambda state:
|
||||
state.has("Double Jump", player)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), player)
|
||||
or state.has_all(("Crouch", "Crouch Uppercut"), player))
|
||||
|
||||
cliff.connect(main_area) # Jump down or ride blue eco elevator.
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
world.level_to_regions[level_name].append(cliff)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 50 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(0,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
return main_area
|
||||
137
worlds/jakanddaxter/regs/gol_and_maias_citadel_regions.py
Normal file
137
worlds/jakanddaxter/regs/gol_and_maias_citadel_regions.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from BaseClasses import CollectionState
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity, CompletionCondition
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_free_scout_flies, can_fight, can_reach_orbs_level
|
||||
|
||||
|
||||
# God help me... here we go.
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> tuple[JakAndDaxterRegion | None, ...]:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
# This level is full of short-medium gaps that cannot be crossed by single jump alone.
|
||||
# These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...)
|
||||
def can_jump_farther(state: CollectionState, p: int) -> bool:
|
||||
return (state.has_any(("Double Jump", "Jump Kick"), p)
|
||||
or state.has_all(("Punch", "Punch Uppercut"), p))
|
||||
|
||||
def can_triple_jump(state: CollectionState, p: int) -> bool:
|
||||
return state.has_all(("Double Jump", "Jump Kick"), p)
|
||||
|
||||
def can_jump_stairs(state: CollectionState, p: int) -> bool:
|
||||
return (state.has("Double Jump", p)
|
||||
or state.has("Jump Dive", p)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), p)
|
||||
or state.has_all(("Crouch", "Crouch Uppercut"), p))
|
||||
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0)
|
||||
main_area.add_fly_locations([91], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
robot_scaffolding = JakAndDaxterRegion("Scaffolding Around Robot", player, multiworld, level_name, 8)
|
||||
robot_scaffolding.add_fly_locations([196699], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
jump_pad_room = JakAndDaxterRegion("Jump Pad Chamber", player, multiworld, level_name, 88)
|
||||
jump_pad_room.add_cell_locations([73], access_rule=lambda state: can_fight(state, player))
|
||||
jump_pad_room.add_special_locations([73], access_rule=lambda state: can_fight(state, player))
|
||||
jump_pad_room.add_fly_locations([131163]) # Blue eco vent is right next to it.
|
||||
jump_pad_room.add_fly_locations([65627], access_rule=lambda state:
|
||||
can_free_scout_flies(state, player) and can_jump_farther(state, player))
|
||||
jump_pad_room.add_cache_locations([24039, 24040]) # First, blue eco vent, second, blue eco cluster near sage.
|
||||
|
||||
blast_furnace = JakAndDaxterRegion("Blast Furnace", player, multiworld, level_name, 39)
|
||||
blast_furnace.add_cell_locations([71], access_rule=lambda state: can_fight(state, player))
|
||||
blast_furnace.add_special_locations([71], access_rule=lambda state: can_fight(state, player))
|
||||
blast_furnace.add_fly_locations([393307]) # Blue eco vent nearby.
|
||||
blast_furnace.add_cache_locations([24038]) # Blue eco cluster near sage.
|
||||
|
||||
bunny_room = JakAndDaxterRegion("Bunny Chamber", player, multiworld, level_name, 45)
|
||||
bunny_room.add_cell_locations([72], access_rule=lambda state: can_fight(state, player))
|
||||
bunny_room.add_special_locations([72], access_rule=lambda state: can_fight(state, player))
|
||||
bunny_room.add_fly_locations([262235], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
rotating_tower = JakAndDaxterRegion("Rotating Tower", player, multiworld, level_name, 20)
|
||||
rotating_tower.add_cell_locations([70], access_rule=lambda state: can_fight(state, player))
|
||||
rotating_tower.add_special_locations([70], access_rule=lambda state: can_fight(state, player))
|
||||
rotating_tower.add_fly_locations([327771], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
final_boss = JakAndDaxterRegion("Final Boss", player, multiworld, level_name, 0)
|
||||
|
||||
# Jump Dive required for a lot of buttons, prepare yourself.
|
||||
main_area.connect(robot_scaffolding, rule=lambda state:
|
||||
state.has("Jump Dive", player) or state.has_all(("Roll", "Roll Jump"), player))
|
||||
main_area.connect(jump_pad_room)
|
||||
|
||||
robot_scaffolding.connect(main_area, rule=lambda state: state.has("Jump Dive", player))
|
||||
robot_scaffolding.connect(blast_furnace, rule=lambda state:
|
||||
state.has("Jump Dive", player)
|
||||
and can_jump_farther(state, player)
|
||||
and (can_triple_jump(state, player) or state.has_all(("Roll", "Roll Jump"), player)))
|
||||
robot_scaffolding.connect(bunny_room, rule=lambda state:
|
||||
state.has("Jump Dive", player)
|
||||
and can_jump_farther(state, player)
|
||||
and (can_triple_jump(state, player) or state.has_all(("Roll", "Roll Jump"), player)))
|
||||
|
||||
jump_pad_room.connect(main_area)
|
||||
jump_pad_room.connect(robot_scaffolding, rule=lambda state:
|
||||
state.has("Jump Dive", player)
|
||||
and (can_triple_jump(state, player) or state.has_all(("Roll", "Roll Jump"), player)))
|
||||
|
||||
blast_furnace.connect(robot_scaffolding) # Blue eco elevator takes you right back.
|
||||
|
||||
bunny_room.connect(robot_scaffolding, rule=lambda state:
|
||||
state.has("Jump Dive", player)
|
||||
and (can_jump_farther(state, player) or state.has_all(("Roll", "Roll Jump"), player)))
|
||||
|
||||
# Final climb.
|
||||
robot_scaffolding.connect(rotating_tower, rule=lambda state:
|
||||
can_jump_stairs(state, player)
|
||||
and state.has_all(("Freed The Blue Sage",
|
||||
"Freed The Red Sage",
|
||||
"Freed The Yellow Sage"), player))
|
||||
|
||||
rotating_tower.connect(main_area) # Take stairs back down.
|
||||
|
||||
# Final elevator. Need to break boxes at summit to get blue eco for platform.
|
||||
rotating_tower.connect(final_boss, rule=lambda state:
|
||||
can_fight(state, player)
|
||||
and state.has("Freed The Green Sage", player))
|
||||
|
||||
final_boss.connect(rotating_tower) # Take elevator back down.
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
world.level_to_regions[level_name].append(robot_scaffolding)
|
||||
world.level_to_regions[level_name].append(jump_pad_room)
|
||||
world.level_to_regions[level_name].append(blast_furnace)
|
||||
world.level_to_regions[level_name].append(bunny_room)
|
||||
world.level_to_regions[level_name].append(rotating_tower)
|
||||
world.level_to_regions[level_name].append(final_boss)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 200 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(15,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
# Final door. Need 100 power cells.
|
||||
if options.jak_completion_condition == CompletionCondition.option_open_100_cell_door:
|
||||
final_door = JakAndDaxterRegion("Final Door", player, multiworld, level_name, 0)
|
||||
final_boss.connect(final_door, rule=lambda state: state.has("Power Cell", player, 100))
|
||||
|
||||
world.level_to_regions[level_name].append(final_door)
|
||||
|
||||
return main_area, final_boss, final_door
|
||||
else:
|
||||
return main_area, final_boss, None
|
||||
38
worlds/jakanddaxter/regs/lava_tube_regions.py
Normal file
38
worlds/jakanddaxter/regs/lava_tube_regions.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_reach_orbs_level
|
||||
from ..locs import cell_locations as cells, scout_locations as scouts
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> JakAndDaxterRegion:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50)
|
||||
|
||||
# Everything is accessible by making contact with the zoomer.
|
||||
main_area.add_cell_locations(cells.locLT_cellTable.keys())
|
||||
main_area.add_fly_locations(scouts.locLT_scoutTable.keys())
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 50 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(14,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
return main_area
|
||||
155
worlds/jakanddaxter/regs/lost_precursor_city_regions.py
Normal file
155
worlds/jakanddaxter/regs/lost_precursor_city_regions.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_free_scout_flies, can_fight, can_reach_orbs_level
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> JakAndDaxterRegion:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
# Just the starting area.
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 4)
|
||||
|
||||
first_room_upper = JakAndDaxterRegion("First Chamber (Upper)", player, multiworld, level_name, 21)
|
||||
|
||||
first_room_lower = JakAndDaxterRegion("First Chamber (Lower)", player, multiworld, level_name, 0)
|
||||
first_room_lower.add_fly_locations([262193], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
first_room_orb_cache = JakAndDaxterRegion("First Chamber Orb Cache", player, multiworld, level_name, 22)
|
||||
|
||||
# Need jump dive to activate button, double jump to reach blue eco to unlock cache.
|
||||
first_room_orb_cache.add_cache_locations([14507], access_rule=lambda state:
|
||||
state.has_all(("Jump Dive", "Double Jump"), player))
|
||||
|
||||
first_hallway = JakAndDaxterRegion("First Hallway", player, multiworld, level_name, 10)
|
||||
first_hallway.add_fly_locations([131121], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
# This entire room is accessible with floating platforms and single jump.
|
||||
second_room = JakAndDaxterRegion("Second Chamber", player, multiworld, level_name, 28)
|
||||
|
||||
# These items can only be gotten with jump dive to activate a button.
|
||||
second_room.add_cell_locations([45], access_rule=lambda state: state.has("Jump Dive", player))
|
||||
second_room.add_fly_locations([49, 65585], access_rule=lambda state: state.has("Jump Dive", player))
|
||||
|
||||
# This is the scout fly on the way to the pipe cell, requires normal breaking moves.
|
||||
second_room.add_fly_locations([196657], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
# This orb vent and scout fly are right next to each other, can be gotten with blue eco and the floating platforms.
|
||||
second_room.add_fly_locations([393265])
|
||||
second_room.add_cache_locations([14838])
|
||||
|
||||
# Named after the cell, includes the armored lurker room.
|
||||
center_complex = JakAndDaxterRegion("Center of the Complex", player, multiworld, level_name, 17)
|
||||
center_complex.add_cell_locations([51])
|
||||
|
||||
color_platforms = JakAndDaxterRegion("Color Platforms", player, multiworld, level_name, 6)
|
||||
color_platforms.add_cell_locations([44], access_rule=lambda state: can_fight(state, player))
|
||||
|
||||
quick_platforms = JakAndDaxterRegion("Quick Platforms", player, multiworld, level_name, 3)
|
||||
|
||||
# Jump dive to activate button.
|
||||
quick_platforms.add_cell_locations([48], access_rule=lambda state: state.has("Jump Dive", player))
|
||||
|
||||
first_slide = JakAndDaxterRegion("First Slide", player, multiworld, level_name, 22)
|
||||
|
||||
# Raised chamber room, includes vent room with scout fly prior to second slide.
|
||||
capsule_room = JakAndDaxterRegion("Capsule Chamber", player, multiworld, level_name, 6)
|
||||
|
||||
# Use jump dive to activate button inside the capsule. Blue eco vent can ready the chamber and get the scout fly.
|
||||
capsule_room.add_cell_locations([47], access_rule=lambda state:
|
||||
state.has("Jump Dive", player)
|
||||
and (state.has_any(("Double Jump", "Jump Kick"), player)
|
||||
or state.has_all(("Punch", "Punch Uppercut"), player)))
|
||||
capsule_room.add_fly_locations([327729])
|
||||
|
||||
# You can slide to the bottom of the city, but if you spawn down there, you have no momentum from the slide.
|
||||
# So you need some kind of jump to reach this cell.
|
||||
second_slide = JakAndDaxterRegion("Second Slide", player, multiworld, level_name, 31)
|
||||
second_slide.add_cell_locations([46], access_rule=lambda state:
|
||||
state.has_any(("Double Jump", "Jump Kick"), player)
|
||||
or state.has_all(("Punch", "Punch Uppercut"), player))
|
||||
|
||||
# If you can enter the helix room, you can jump or fight your way to the top. But you need some kind of movement
|
||||
# to enter it in the first place.
|
||||
helix_room = JakAndDaxterRegion("Helix Chamber", player, multiworld, level_name, 30)
|
||||
helix_room.add_cell_locations([50], access_rule=lambda state:
|
||||
state.has("Double Jump", player)
|
||||
or can_fight(state, player))
|
||||
|
||||
main_area.connect(first_room_upper) # Run.
|
||||
|
||||
first_room_upper.connect(main_area) # Run.
|
||||
first_room_upper.connect(first_hallway) # Run and jump (floating platforms).
|
||||
first_room_upper.connect(first_room_lower) # Run and jump down.
|
||||
|
||||
first_room_lower.connect(first_room_upper) # Run and jump (floating platforms).
|
||||
|
||||
# Needs some movement to reach these orbs and orb cache.
|
||||
first_room_lower.connect(first_room_orb_cache, rule=lambda state:
|
||||
state.has_all(("Jump Dive", "Double Jump"), player))
|
||||
first_room_orb_cache.connect(first_room_lower, rule=lambda state:
|
||||
state.has_all(("Jump Dive", "Double Jump"), player))
|
||||
|
||||
first_hallway.connect(first_room_upper) # Run and jump down.
|
||||
first_hallway.connect(second_room) # Run and jump (floating platforms).
|
||||
|
||||
second_room.connect(first_hallway) # Run and jump.
|
||||
second_room.connect(center_complex) # Run and jump down.
|
||||
|
||||
center_complex.connect(second_room) # Run and jump (swim).
|
||||
center_complex.connect(color_platforms) # Run and jump (swim).
|
||||
center_complex.connect(quick_platforms) # Run and jump (swim).
|
||||
|
||||
color_platforms.connect(center_complex) # Run and jump (swim).
|
||||
|
||||
quick_platforms.connect(center_complex) # Run and jump (swim).
|
||||
quick_platforms.connect(first_slide) # Slide.
|
||||
|
||||
first_slide.connect(capsule_room) # Slide.
|
||||
|
||||
capsule_room.connect(second_slide) # Slide.
|
||||
capsule_room.connect(main_area, rule=lambda state: # Chamber goes back to surface.
|
||||
state.has("Jump Dive", player)) # (Assume one-way for sanity.)
|
||||
|
||||
second_slide.connect(helix_room, rule=lambda state: # As stated above, you need to jump
|
||||
state.has_any(("Double Jump", "Jump Kick"), player) # across the dark eco pool before
|
||||
or state.has_all(("Punch", "Punch Uppercut"), player)) # you can climb the helix room.
|
||||
|
||||
helix_room.connect(quick_platforms, rule=lambda state: # Escape to get back to here.
|
||||
state.has("Double Jump", player) # Capsule is a convenient exit to the level.
|
||||
or can_fight(state, player))
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
world.level_to_regions[level_name].append(first_room_upper)
|
||||
world.level_to_regions[level_name].append(first_room_lower)
|
||||
world.level_to_regions[level_name].append(first_room_orb_cache)
|
||||
world.level_to_regions[level_name].append(first_hallway)
|
||||
world.level_to_regions[level_name].append(second_room)
|
||||
world.level_to_regions[level_name].append(center_complex)
|
||||
world.level_to_regions[level_name].append(color_platforms)
|
||||
world.level_to_regions[level_name].append(quick_platforms)
|
||||
world.level_to_regions[level_name].append(first_slide)
|
||||
world.level_to_regions[level_name].append(capsule_room)
|
||||
world.level_to_regions[level_name].append(second_slide)
|
||||
world.level_to_regions[level_name].append(helix_room)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 200 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(7,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
return main_area
|
||||
131
worlds/jakanddaxter/regs/misty_island_regions.py
Normal file
131
worlds/jakanddaxter/regs/misty_island_regions.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_free_scout_flies, can_fight, can_reach_orbs_level
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> JakAndDaxterRegion:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 9)
|
||||
|
||||
muse_course = JakAndDaxterRegion("Muse Course", player, multiworld, level_name, 21)
|
||||
muse_course.add_cell_locations([23])
|
||||
muse_course.add_fly_locations([327708], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
zoomer = JakAndDaxterRegion("Zoomer", player, multiworld, level_name, 32)
|
||||
zoomer.add_cell_locations([27, 29])
|
||||
zoomer.add_fly_locations([393244])
|
||||
|
||||
ship = JakAndDaxterRegion("Ship", player, multiworld, level_name, 10)
|
||||
ship.add_cell_locations([24])
|
||||
ship.add_fly_locations([131100], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
far_side = JakAndDaxterRegion("Far Side", player, multiworld, level_name, 16)
|
||||
|
||||
# In order to even reach this fly, you must use the seesaw or crouch jump.
|
||||
far_side_cliff = JakAndDaxterRegion("Far Side Cliff", player, multiworld, level_name, 5)
|
||||
far_side_cliff.add_fly_locations([28], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
# To carry the blue eco fast enough to open this cache, you need to break the bone bridges along the way.
|
||||
far_side_cache = JakAndDaxterRegion("Far Side Orb Cache", player, multiworld, level_name, 15)
|
||||
far_side_cache.add_cache_locations([11072], access_rule=lambda state: can_fight(state, player))
|
||||
|
||||
barrel_course = JakAndDaxterRegion("Barrel Course", player, multiworld, level_name, 10)
|
||||
barrel_course.add_fly_locations([196636], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
# 14 orbs for the boxes you can only break with the cannon.
|
||||
cannon = JakAndDaxterRegion("Cannon", player, multiworld, level_name, 14)
|
||||
cannon.add_cell_locations([26], access_rule=lambda state: can_fight(state, player))
|
||||
|
||||
upper_approach = JakAndDaxterRegion("Upper Arena Approach", player, multiworld, level_name, 6)
|
||||
upper_approach.add_fly_locations([65564, 262172], access_rule=lambda state:
|
||||
can_free_scout_flies(state, player))
|
||||
|
||||
lower_approach = JakAndDaxterRegion("Lower Arena Approach", player, multiworld, level_name, 7)
|
||||
lower_approach.add_cell_locations([30])
|
||||
|
||||
arena = JakAndDaxterRegion("Arena", player, multiworld, level_name, 5)
|
||||
arena.add_cell_locations([25], access_rule=lambda state: can_fight(state, player))
|
||||
|
||||
main_area.connect(muse_course) # TODO - What do you need to chase the muse the whole way around?
|
||||
main_area.connect(zoomer) # Run and jump down.
|
||||
main_area.connect(ship) # Run and jump.
|
||||
main_area.connect(lower_approach) # Run and jump.
|
||||
|
||||
# Need to break the bone bridge to access.
|
||||
main_area.connect(upper_approach, rule=lambda state: can_fight(state, player))
|
||||
|
||||
muse_course.connect(main_area) # Run and jump down.
|
||||
|
||||
# The zoomer pad is low enough that it requires Crouch Jump specifically.
|
||||
zoomer.connect(main_area, rule=lambda state: state.has_all(("Crouch", "Crouch Jump"), player))
|
||||
|
||||
ship.connect(main_area) # Run and jump down.
|
||||
ship.connect(far_side) # Run and jump down.
|
||||
ship.connect(barrel_course) # Run and jump (dodge barrels).
|
||||
|
||||
far_side.connect(ship) # Run and jump.
|
||||
far_side.connect(arena) # Run and jump.
|
||||
|
||||
# Only if you can use the seesaw or Crouch Jump from the seesaw's edge.
|
||||
far_side.connect(far_side_cliff, rule=lambda state:
|
||||
state.has("Jump Dive", player)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), player))
|
||||
|
||||
# Only if you can break the bone bridges to carry blue eco over the mud pit.
|
||||
far_side.connect(far_side_cache, rule=lambda state: can_fight(state, player))
|
||||
|
||||
far_side_cliff.connect(far_side) # Run and jump down.
|
||||
|
||||
barrel_course.connect(cannon) # Run and jump (dodge barrels).
|
||||
|
||||
cannon.connect(barrel_course) # Run and jump (dodge barrels).
|
||||
cannon.connect(arena) # Run and jump down.
|
||||
cannon.connect(upper_approach) # Run and jump down.
|
||||
|
||||
upper_approach.connect(lower_approach) # Jump down.
|
||||
upper_approach.connect(arena) # Jump down.
|
||||
|
||||
# One cliff is accessible, but only via Crouch Jump.
|
||||
lower_approach.connect(upper_approach, rule=lambda state: state.has_all(("Crouch", "Crouch Jump"), player))
|
||||
|
||||
# Requires breaking bone bridges.
|
||||
lower_approach.connect(arena, rule=lambda state: can_fight(state, player))
|
||||
|
||||
arena.connect(lower_approach) # Run.
|
||||
arena.connect(far_side) # Run.
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
world.level_to_regions[level_name].append(muse_course)
|
||||
world.level_to_regions[level_name].append(zoomer)
|
||||
world.level_to_regions[level_name].append(ship)
|
||||
world.level_to_regions[level_name].append(far_side)
|
||||
world.level_to_regions[level_name].append(far_side_cliff)
|
||||
world.level_to_regions[level_name].append(far_side_cache)
|
||||
world.level_to_regions[level_name].append(barrel_course)
|
||||
world.level_to_regions[level_name].append(cannon)
|
||||
world.level_to_regions[level_name].append(upper_approach)
|
||||
world.level_to_regions[level_name].append(lower_approach)
|
||||
world.level_to_regions[level_name].append(arena)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 150 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(4,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
return main_area
|
||||
67
worlds/jakanddaxter/regs/mountain_pass_regions.py
Normal file
67
worlds/jakanddaxter/regs/mountain_pass_regions.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_reach_orbs_level
|
||||
from ..locs import scout_locations as scouts
|
||||
from worlds.generic.Rules import add_rule
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> tuple[JakAndDaxterRegion, ...]:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
# This is basically just Klaww.
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0)
|
||||
main_area.add_cell_locations([86])
|
||||
|
||||
# Some folks prefer firing Yellow Eco from the hip, so optionally put this rule before Klaww. Klaww is the only
|
||||
# location in main_area, so he's at index 0.
|
||||
if world.options.require_punch_for_klaww:
|
||||
add_rule(main_area.locations[0], lambda state: state.has("Punch", player))
|
||||
|
||||
race = JakAndDaxterRegion("Race", player, multiworld, level_name, 50)
|
||||
race.add_cell_locations([87])
|
||||
|
||||
# All scout flies can be broken with the zoomer.
|
||||
race.add_fly_locations(scouts.locMP_scoutTable.keys())
|
||||
|
||||
shortcut = JakAndDaxterRegion("Shortcut", player, multiworld, level_name, 0)
|
||||
shortcut.add_cell_locations([110])
|
||||
|
||||
# Of course, in order to make it to the race region, you must defeat Klaww. He's not optional.
|
||||
# So we need to set up this inter-region rule as well (or make it free if the setting is off).
|
||||
if world.options.require_punch_for_klaww:
|
||||
main_area.connect(race, rule=lambda state: state.has("Punch", player))
|
||||
else:
|
||||
main_area.connect(race)
|
||||
|
||||
# You actually can go backwards from the race back to Klaww's area.
|
||||
race.connect(main_area)
|
||||
race.connect(shortcut, rule=lambda state: state.has("Yellow Eco Switch", player))
|
||||
|
||||
shortcut.connect(race)
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
world.level_to_regions[level_name].append(race)
|
||||
world.level_to_regions[level_name].append(shortcut)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 50 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(10,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
# Return race required for inter-level connections.
|
||||
return main_area, race
|
||||
38
worlds/jakanddaxter/regs/precursor_basin_regions.py
Normal file
38
worlds/jakanddaxter/regs/precursor_basin_regions.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_reach_orbs_level
|
||||
from ..locs import cell_locations as cells, scout_locations as scouts
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> JakAndDaxterRegion:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 200)
|
||||
|
||||
# Everything is accessible by making contact with the zoomer.
|
||||
main_area.add_cell_locations(cells.locPB_cellTable.keys())
|
||||
main_area.add_fly_locations(scouts.locPB_scoutTable.keys())
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 200 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(9,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
return main_area
|
||||
91
worlds/jakanddaxter/regs/region_base.py
Normal file
91
worlds/jakanddaxter/regs/region_base.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from typing import Iterable
|
||||
from BaseClasses import MultiWorld, Region
|
||||
from ..game_id import jak1_name
|
||||
from ..locations import JakAndDaxterLocation, location_table
|
||||
from ..locs import (orb_locations as orbs,
|
||||
cell_locations as cells,
|
||||
scout_locations as scouts,
|
||||
special_locations as specials,
|
||||
orb_cache_locations as caches)
|
||||
from worlds.generic.Rules import CollectionRule
|
||||
|
||||
|
||||
class JakAndDaxterRegion(Region):
|
||||
"""
|
||||
Holds region information such as name, level name, number of orbs available, etc.
|
||||
We especially need orb counts to be tracked because we need to know when you can
|
||||
afford the Citizen and Oracle orb payments for more checks.
|
||||
"""
|
||||
game: str = jak1_name
|
||||
level_name: str
|
||||
orb_count: int
|
||||
location_count: int = 0
|
||||
|
||||
def __init__(self, name: str, player: int, multiworld: MultiWorld, level_name: str = "", orb_count: int = 0):
|
||||
formatted_name = f"{level_name} {name}".strip()
|
||||
super().__init__(formatted_name, player, multiworld)
|
||||
self.level_name = level_name
|
||||
self.orb_count = orb_count
|
||||
|
||||
def add_cell_locations(self, locations: Iterable[int], access_rule: CollectionRule | None = None) -> None:
|
||||
"""
|
||||
Adds a Power Cell Location to this region with the given access rule.
|
||||
Converts Game ID's to AP ID's for you.
|
||||
"""
|
||||
for loc in locations:
|
||||
ap_id = cells.to_ap_id(loc)
|
||||
self.add_jak_location(ap_id, location_table[ap_id], access_rule)
|
||||
|
||||
def add_fly_locations(self, locations: Iterable[int], access_rule: CollectionRule | None = None) -> None:
|
||||
"""
|
||||
Adds a Scout Fly Location to this region with the given access rule.
|
||||
Converts Game ID's to AP ID's for you.
|
||||
"""
|
||||
for loc in locations:
|
||||
ap_id = scouts.to_ap_id(loc)
|
||||
self.add_jak_location(ap_id, location_table[ap_id], access_rule)
|
||||
|
||||
def add_special_locations(self, locations: Iterable[int], access_rule: CollectionRule | None = None) -> None:
|
||||
"""
|
||||
Adds a Special Location to this region with the given access rule.
|
||||
Converts Game ID's to AP ID's for you.
|
||||
Special Locations should be matched alongside their respective
|
||||
Power Cell Locations, so you get 2 unlocks for these rather than 1.
|
||||
"""
|
||||
for loc in locations:
|
||||
ap_id = specials.to_ap_id(loc)
|
||||
self.add_jak_location(ap_id, location_table[ap_id], access_rule)
|
||||
|
||||
def add_cache_locations(self, locations: Iterable[int], access_rule: CollectionRule | None = None) -> None:
|
||||
"""
|
||||
Adds an Orb Cache Location to this region with the given access rule.
|
||||
Converts Game ID's to AP ID's for you.
|
||||
"""
|
||||
for loc in locations:
|
||||
ap_id = caches.to_ap_id(loc)
|
||||
self.add_jak_location(ap_id, location_table[ap_id], access_rule)
|
||||
|
||||
def add_orb_locations(self, level_index: int, bundle_index: int, access_rule: CollectionRule | None = None) -> None:
|
||||
"""
|
||||
Adds Orb Bundle Locations to this region equal to `bundle_count`. Used only when Per-Level Orbsanity is enabled.
|
||||
The orb factory class will handle AP ID enumeration.
|
||||
"""
|
||||
bundle_address = orbs.create_address(level_index, bundle_index)
|
||||
location = JakAndDaxterLocation(self.player,
|
||||
f"{self.level_name} Orb Bundle {bundle_index + 1}".strip(),
|
||||
orbs.to_ap_id(bundle_address),
|
||||
self)
|
||||
if access_rule:
|
||||
location.access_rule = access_rule
|
||||
self.locations.append(location)
|
||||
self.location_count += 1
|
||||
|
||||
def add_jak_location(self, ap_id: int, name: str, access_rule: CollectionRule | None = None) -> None:
|
||||
"""
|
||||
Helper function to add Locations. Not to be used directly.
|
||||
"""
|
||||
location = JakAndDaxterLocation(self.player, name, ap_id, self)
|
||||
if access_rule:
|
||||
location.access_rule = access_rule
|
||||
self.locations.append(location)
|
||||
self.location_count += 1
|
||||
75
worlds/jakanddaxter/regs/rock_village_regions.py
Normal file
75
worlds/jakanddaxter/regs/rock_village_regions.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_free_scout_flies, can_reach_orbs_level
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> tuple[JakAndDaxterRegion, ...]:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
# This includes most of the area surrounding LPC as well, for orb_count purposes. You can swim and single jump.
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23)
|
||||
main_area.add_cell_locations([31], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None))
|
||||
main_area.add_cell_locations([32], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None))
|
||||
main_area.add_cell_locations([33], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None))
|
||||
main_area.add_cell_locations([34], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None))
|
||||
main_area.add_cell_locations([35], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, 34))
|
||||
|
||||
# These 2 scout fly boxes can be broken by running with nearby blue eco.
|
||||
main_area.add_fly_locations([196684, 262220])
|
||||
main_area.add_fly_locations([76, 131148, 65612, 327756], access_rule=lambda state:
|
||||
can_free_scout_flies(state, player))
|
||||
|
||||
# Warrior Pontoon check. You just talk to him and get his introduction.
|
||||
main_area.add_special_locations([33])
|
||||
|
||||
orb_cache = JakAndDaxterRegion("Orb Cache", player, multiworld, level_name, 20)
|
||||
|
||||
# You need roll jump to be able to reach this before the blue eco runs out.
|
||||
orb_cache.add_cache_locations([10945], access_rule=lambda state: state.has_all(("Roll", "Roll Jump"), player))
|
||||
|
||||
# Fly here can be gotten with Yellow Eco from Boggy, goggles, and no extra movement options (see fly ID 43).
|
||||
pontoon_bridge = JakAndDaxterRegion("Pontoon Bridge", player, multiworld, level_name, 7)
|
||||
pontoon_bridge.add_fly_locations([393292])
|
||||
|
||||
klaww_cliff = JakAndDaxterRegion("Klaww's Cliff", player, multiworld, level_name, 0)
|
||||
|
||||
main_area.connect(orb_cache, rule=lambda state: state.has_all(("Roll", "Roll Jump"), player))
|
||||
main_area.connect(pontoon_bridge, rule=lambda state: state.has("Warrior's Pontoons", player))
|
||||
|
||||
orb_cache.connect(main_area)
|
||||
|
||||
pontoon_bridge.connect(main_area, rule=lambda state: state.has("Warrior's Pontoons", player))
|
||||
pontoon_bridge.connect(klaww_cliff, rule=lambda state:
|
||||
state.has("Double Jump", player)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), player)
|
||||
or state.has_all(("Crouch", "Crouch Uppercut", "Jump Kick"), player))
|
||||
|
||||
klaww_cliff.connect(pontoon_bridge) # Just jump back down.
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
world.level_to_regions[level_name].append(orb_cache)
|
||||
world.level_to_regions[level_name].append(pontoon_bridge)
|
||||
world.level_to_regions[level_name].append(klaww_cliff)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 50 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(6,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
# Return klaww_cliff required for inter-level connections.
|
||||
return main_area, pontoon_bridge, klaww_cliff
|
||||
83
worlds/jakanddaxter/regs/sandover_village_regions.py
Normal file
83
worlds/jakanddaxter/regs/sandover_village_regions.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_free_scout_flies, can_reach_orbs_level
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> JakAndDaxterRegion:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 26)
|
||||
|
||||
# Yakows requires no combat.
|
||||
main_area.add_cell_locations([10])
|
||||
main_area.add_cell_locations([11], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None))
|
||||
main_area.add_cell_locations([12], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None))
|
||||
|
||||
# These 4 scout fly boxes can be broken by running with all the blue eco from Sentinel Beach.
|
||||
main_area.add_fly_locations([262219, 327755, 131147, 65611])
|
||||
|
||||
# The farmer's scout fly. You can either get the Orb Cache Cliff blue eco, or break it normally.
|
||||
main_area.add_fly_locations([196683], access_rule=lambda state:
|
||||
state.has("Double Jump", player)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), player)
|
||||
or can_free_scout_flies(state, player))
|
||||
|
||||
orb_cache_cliff = JakAndDaxterRegion("Orb Cache Cliff", player, multiworld, level_name, 15)
|
||||
orb_cache_cliff.add_cache_locations([10344])
|
||||
|
||||
yakow_cliff = JakAndDaxterRegion("Yakow Cliff", player, multiworld, level_name, 3)
|
||||
yakow_cliff.add_fly_locations([75], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
oracle_platforms = JakAndDaxterRegion("Oracle Platforms", player, multiworld, level_name, 6)
|
||||
oracle_platforms.add_cell_locations([13], access_rule=lambda state:
|
||||
world.can_trade(state, world.total_trade_orbs, None))
|
||||
oracle_platforms.add_cell_locations([14], access_rule=lambda state:
|
||||
world.can_trade(state, world.total_trade_orbs, 13))
|
||||
oracle_platforms.add_fly_locations([393291], access_rule=lambda state:
|
||||
can_free_scout_flies(state, player))
|
||||
|
||||
main_area.connect(orb_cache_cliff, rule=lambda state:
|
||||
state.has("Double Jump", player)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), player)
|
||||
or state.has_all(("Crouch", "Crouch Uppercut", "Jump Kick"), player))
|
||||
|
||||
main_area.connect(yakow_cliff, rule=lambda state:
|
||||
state.has("Double Jump", player)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), player)
|
||||
or state.has_all(("Crouch", "Crouch Uppercut", "Jump Kick"), player))
|
||||
|
||||
main_area.connect(oracle_platforms, rule=lambda state:
|
||||
state.has_all(("Roll", "Roll Jump"), player)
|
||||
or state.has_all(("Double Jump", "Jump Kick"), player))
|
||||
|
||||
# All these can go back to main_area immediately.
|
||||
orb_cache_cliff.connect(main_area)
|
||||
yakow_cliff.connect(main_area)
|
||||
oracle_platforms.connect(main_area)
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
world.level_to_regions[level_name].append(orb_cache_cliff)
|
||||
world.level_to_regions[level_name].append(yakow_cliff)
|
||||
world.level_to_regions[level_name].append(oracle_platforms)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 50 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(1,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
return main_area
|
||||
108
worlds/jakanddaxter/regs/sentinel_beach_regions.py
Normal file
108
worlds/jakanddaxter/regs/sentinel_beach_regions.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from BaseClasses import CollectionState
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_free_scout_flies, can_fight, can_reach_orbs_level
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> JakAndDaxterRegion:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 128)
|
||||
main_area.add_cell_locations([18, 21, 22])
|
||||
|
||||
# These scout fly boxes can be broken by running with freely accessible blue eco.
|
||||
# The 3 clusters by the Flut Flut egg can go surprisingly far.
|
||||
main_area.add_fly_locations([327700, 20, 65556, 262164])
|
||||
|
||||
# This scout fly box can be broken with the locked blue eco vent, or by normal combat tricks.
|
||||
main_area.add_fly_locations([393236], access_rule=lambda state:
|
||||
state.has("Blue Eco Switch", player)
|
||||
or can_free_scout_flies(state, player))
|
||||
|
||||
# No need for the blue eco vent for either of the orb caches.
|
||||
main_area.add_cache_locations([12634, 12635])
|
||||
|
||||
pelican = JakAndDaxterRegion("Pelican", player, multiworld, level_name, 0)
|
||||
pelican.add_cell_locations([16], access_rule=lambda state: can_fight(state, player))
|
||||
|
||||
# Only these specific attacks can push the flut flut egg off the cliff.
|
||||
flut_flut_egg = JakAndDaxterRegion("Flut Flut Egg", player, multiworld, level_name, 0)
|
||||
flut_flut_egg.add_cell_locations([17], access_rule=lambda state:
|
||||
state.has_any(("Punch", "Kick", "Jump Kick"), player))
|
||||
flut_flut_egg.add_special_locations([17], access_rule=lambda state:
|
||||
state.has_any(("Punch", "Kick", "Jump Kick"), player))
|
||||
|
||||
eco_harvesters = JakAndDaxterRegion("Eco Harvesters", player, multiworld, level_name, 0)
|
||||
eco_harvesters.add_cell_locations([15], access_rule=lambda state: can_fight(state, player))
|
||||
|
||||
green_ridge = JakAndDaxterRegion("Ridge Near Green Vents", player, multiworld, level_name, 5)
|
||||
green_ridge.add_fly_locations([131092], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
blue_ridge = JakAndDaxterRegion("Ridge Near Blue Vent", player, multiworld, level_name, 5)
|
||||
blue_ridge.add_fly_locations([196628], access_rule=lambda state:
|
||||
state.has("Blue Eco Switch", player)
|
||||
or can_free_scout_flies(state, player))
|
||||
|
||||
cannon_tower = JakAndDaxterRegion("Cannon Tower", player, multiworld, level_name, 12)
|
||||
cannon_tower.add_cell_locations([19], access_rule=lambda state: can_fight(state, player))
|
||||
|
||||
main_area.connect(pelican) # Swim and jump.
|
||||
main_area.connect(flut_flut_egg) # Run and jump.
|
||||
main_area.connect(eco_harvesters) # Run.
|
||||
|
||||
# We need a helper function for the uppercut logs.
|
||||
def can_uppercut_and_jump_logs(state: CollectionState, p: int) -> bool:
|
||||
return (state.has_any(("Double Jump", "Jump Kick"), p)
|
||||
and (state.has_all(("Crouch", "Crouch Uppercut"), p)
|
||||
or state.has_all(("Punch", "Punch Uppercut"), p)))
|
||||
|
||||
# If you have double jump or crouch jump, you don't need the logs to reach this place.
|
||||
main_area.connect(green_ridge, rule=lambda state:
|
||||
state.has("Double Jump", player)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), player)
|
||||
or can_uppercut_and_jump_logs(state, player))
|
||||
|
||||
# If you have the blue eco jump pad, you don't need the logs to reach this place.
|
||||
main_area.connect(blue_ridge, rule=lambda state:
|
||||
state.has("Blue Eco Switch", player)
|
||||
or can_uppercut_and_jump_logs(state, player))
|
||||
|
||||
main_area.connect(cannon_tower, rule=lambda state: state.has("Blue Eco Switch", player))
|
||||
|
||||
# All these can go back to main_area immediately.
|
||||
pelican.connect(main_area)
|
||||
flut_flut_egg.connect(main_area)
|
||||
eco_harvesters.connect(main_area)
|
||||
green_ridge.connect(main_area)
|
||||
blue_ridge.connect(main_area)
|
||||
cannon_tower.connect(main_area)
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
world.level_to_regions[level_name].append(pelican)
|
||||
world.level_to_regions[level_name].append(flut_flut_egg)
|
||||
world.level_to_regions[level_name].append(eco_harvesters)
|
||||
world.level_to_regions[level_name].append(green_ridge)
|
||||
world.level_to_regions[level_name].append(blue_ridge)
|
||||
world.level_to_regions[level_name].append(cannon_tower)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 150 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(2,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
return main_area
|
||||
203
worlds/jakanddaxter/regs/snowy_mountain_regions.py
Normal file
203
worlds/jakanddaxter/regs/snowy_mountain_regions.py
Normal file
@@ -0,0 +1,203 @@
|
||||
from BaseClasses import CollectionState
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_free_scout_flies, can_fight, can_reach_orbs_level
|
||||
|
||||
|
||||
# God help me... here we go.
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> JakAndDaxterRegion:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
# We need a few helper functions.
|
||||
def can_cross_long_gap(state: CollectionState, p: int) -> bool:
|
||||
return (state.has_all(("Roll", "Roll Jump"), p)
|
||||
or state.has_all(("Double Jump", "Jump Kick"), p))
|
||||
|
||||
def can_jump_blockers(state: CollectionState, p: int) -> bool:
|
||||
return (state.has_any(("Double Jump", "Jump Kick"), p)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), p)
|
||||
or state.has_all(("Punch", "Punch Uppercut"), p))
|
||||
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0)
|
||||
main_area.add_fly_locations([65], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
# We need a few virtual regions like we had for Dark Crystals in Spider Cave.
|
||||
# First, a virtual region for the glacier lurkers.
|
||||
glacier_lurkers = JakAndDaxterRegion("Glacier Lurkers", player, multiworld, level_name, 0)
|
||||
|
||||
# Need to fight all the troops.
|
||||
# Troop in snowball_canyon: cross main_area.
|
||||
# Troop in ice_skating_rink: cross main_area and fort_exterior.
|
||||
# Troop in fort_exterior: cross main_area and fort_exterior.
|
||||
glacier_lurkers.add_cell_locations([61], access_rule=lambda state:
|
||||
can_fight(state, player)
|
||||
and can_cross_long_gap(state, player))
|
||||
|
||||
# Second, a virtual region for the precursor blockers. Unlike the others, this contains orbs:
|
||||
# the total number of orbs that sit on top of the blockers. Yes, there are only 8.
|
||||
blockers = JakAndDaxterRegion("Precursor Blockers", player, multiworld, level_name, 8)
|
||||
|
||||
# 1 in main_area
|
||||
# 2 in snowball_canyon
|
||||
# 4 in ice_skating_rink
|
||||
# 3 in fort_exterior
|
||||
# 3 in bunny_cave_start
|
||||
blockers.add_cell_locations([66], access_rule=lambda state:
|
||||
can_fight(state, player)
|
||||
and can_cross_long_gap(state, player))
|
||||
|
||||
snowball_canyon = JakAndDaxterRegion("Snowball Canyon", player, multiworld, level_name, 28)
|
||||
|
||||
# The scout fly box *can* be broken without YES, so leave it in this region.
|
||||
frozen_box_cave = JakAndDaxterRegion("Frozen Box Cave", player, multiworld, level_name, 12)
|
||||
frozen_box_cave.add_fly_locations([327745], access_rule=lambda state:
|
||||
state.has("Yellow Eco Switch", player)
|
||||
or can_free_scout_flies(state, player))
|
||||
|
||||
# This region has crates that can *only* be broken with YES.
|
||||
frozen_box_cave_crates = JakAndDaxterRegion("Frozen Box Cave Orb Crates", player, multiworld, level_name, 8)
|
||||
frozen_box_cave_crates.add_cell_locations([67], access_rule=lambda state:
|
||||
state.has("Yellow Eco Switch", player))
|
||||
|
||||
# Include 6 orbs on the twin elevator ice ramp.
|
||||
ice_skating_rink = JakAndDaxterRegion("Ice Skating Rink", player, multiworld, level_name, 20)
|
||||
ice_skating_rink.add_fly_locations([131137], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
flut_flut_course = JakAndDaxterRegion("Flut Flut Course", player, multiworld, level_name, 15)
|
||||
flut_flut_course.add_cell_locations([63], access_rule=lambda state: state.has("Flut Flut", player))
|
||||
flut_flut_course.add_special_locations([63], access_rule=lambda state: state.has("Flut Flut", player))
|
||||
|
||||
# Includes the bridge from snowball_canyon, the area beneath that bridge, and the areas around the fort.
|
||||
fort_exterior = JakAndDaxterRegion("Fort Exterior", player, multiworld, level_name, 20)
|
||||
fort_exterior.add_fly_locations([65601, 393281], access_rule=lambda state:
|
||||
can_free_scout_flies(state, player))
|
||||
|
||||
# Includes the icy island and bridge outside the cave entrance.
|
||||
bunny_cave_start = JakAndDaxterRegion("Bunny Cave (Start)", player, multiworld, level_name, 10)
|
||||
|
||||
# Includes the cell and 3 orbs at the exit.
|
||||
bunny_cave_end = JakAndDaxterRegion("Bunny Cave (End)", player, multiworld, level_name, 3)
|
||||
bunny_cave_end.add_cell_locations([64])
|
||||
|
||||
switch_cave = JakAndDaxterRegion("Yellow Eco Switch Cave", player, multiworld, level_name, 4)
|
||||
switch_cave.add_cell_locations([60])
|
||||
switch_cave.add_special_locations([60])
|
||||
|
||||
# Only what can be covered by single jump.
|
||||
fort_interior = JakAndDaxterRegion("Fort Interior (Main)", player, multiworld, level_name, 19)
|
||||
|
||||
# Reaching the top of the watch tower, getting the fly with the blue eco, and falling down to get the caches.
|
||||
fort_interior_caches = JakAndDaxterRegion("Fort Interior (Caches)", player, multiworld, level_name, 51)
|
||||
fort_interior_caches.add_fly_locations([196673])
|
||||
fort_interior_caches.add_cache_locations([23348, 23349, 23350])
|
||||
|
||||
# Need higher jump.
|
||||
fort_interior_base = JakAndDaxterRegion("Fort Interior (Base)", player, multiworld, level_name, 0)
|
||||
fort_interior_base.add_fly_locations([262209], access_rule=lambda state:
|
||||
can_free_scout_flies(state, player))
|
||||
|
||||
# Need farther jump.
|
||||
fort_interior_course_end = JakAndDaxterRegion("Fort Interior (Course End)", player, multiworld, level_name, 2)
|
||||
fort_interior_course_end.add_cell_locations([62])
|
||||
|
||||
# Wire up the virtual regions first.
|
||||
main_area.connect(blockers, rule=lambda state: can_jump_blockers(state, player))
|
||||
main_area.connect(glacier_lurkers, rule=lambda state: can_fight(state, player))
|
||||
|
||||
# Yes, the only way into the rest of the level requires advanced movement.
|
||||
main_area.connect(snowball_canyon, rule=lambda state: can_cross_long_gap(state, player))
|
||||
|
||||
snowball_canyon.connect(main_area) # But you can just jump down and run up the ramp.
|
||||
snowball_canyon.connect(bunny_cave_start) # Jump down from the glacier troop cliff.
|
||||
snowball_canyon.connect(fort_exterior) # Jump down, to the left of frozen box cave.
|
||||
snowball_canyon.connect(frozen_box_cave, rule=lambda state: # More advanced movement.
|
||||
can_cross_long_gap(state, player))
|
||||
|
||||
frozen_box_cave.connect(snowball_canyon, rule=lambda state: # Same movement to go back.
|
||||
can_cross_long_gap(state, player))
|
||||
frozen_box_cave.connect(frozen_box_cave_crates, rule=lambda state: # YES to get these crates.
|
||||
state.has("Yellow Eco Switch", player))
|
||||
frozen_box_cave.connect(ice_skating_rink, rule=lambda state: # Same movement to go forward.
|
||||
can_cross_long_gap(state, player))
|
||||
|
||||
frozen_box_cave_crates.connect(frozen_box_cave) # Semi-virtual region, no moves req'd.
|
||||
|
||||
ice_skating_rink.connect(frozen_box_cave, rule=lambda state: # Same movement to go back.
|
||||
can_cross_long_gap(state, player))
|
||||
ice_skating_rink.connect(flut_flut_course, rule=lambda state: # Duh.
|
||||
state.has("Flut Flut", player))
|
||||
ice_skating_rink.connect(fort_exterior) # Just slide down the elevator ramp.
|
||||
|
||||
fort_exterior.connect(ice_skating_rink, rule=lambda state: # Twin elevators OR scout fly ledge.
|
||||
can_cross_long_gap(state, player)) # Both doable with main_gap logic.
|
||||
fort_exterior.connect(snowball_canyon) # Run across bridge.
|
||||
fort_exterior.connect(fort_interior, rule=lambda state: # Duh.
|
||||
state.has("Snowy Fort Gate", player))
|
||||
fort_exterior.connect(bunny_cave_start) # Run across bridge.
|
||||
fort_exterior.connect(switch_cave, rule=lambda state: # Yes, blocker jumps work here.
|
||||
can_jump_blockers(state, player))
|
||||
|
||||
fort_interior.connect(fort_interior_caches, rule=lambda state: # Just need a little height.
|
||||
state.has("Double Jump", player)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), player))
|
||||
fort_interior.connect(fort_interior_base, rule=lambda state: # Just need a little height.
|
||||
state.has("Double Jump", player)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), player))
|
||||
fort_interior.connect(fort_interior_course_end, rule=lambda state: # Just need a little distance.
|
||||
state.has_any(("Double Jump", "Jump Kick"), player)
|
||||
or state.has_all(("Punch", "Punch Uppercut"), player))
|
||||
|
||||
flut_flut_course.connect(fort_exterior) # Ride the elevator.
|
||||
|
||||
# Must fight way through cave, but there is also a grab-less ledge we must jump over.
|
||||
bunny_cave_start.connect(bunny_cave_end, rule=lambda state:
|
||||
can_fight(state, player)
|
||||
and (state.has("Double Jump", player)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), player)))
|
||||
|
||||
# All jump down.
|
||||
fort_interior_caches.connect(fort_interior)
|
||||
fort_interior_base.connect(fort_interior)
|
||||
fort_interior_course_end.connect(fort_interior)
|
||||
switch_cave.connect(fort_exterior)
|
||||
bunny_cave_end.connect(fort_exterior)
|
||||
|
||||
# I really hope that is everything.
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
world.level_to_regions[level_name].append(glacier_lurkers)
|
||||
world.level_to_regions[level_name].append(blockers)
|
||||
world.level_to_regions[level_name].append(snowball_canyon)
|
||||
world.level_to_regions[level_name].append(frozen_box_cave)
|
||||
world.level_to_regions[level_name].append(frozen_box_cave_crates)
|
||||
world.level_to_regions[level_name].append(ice_skating_rink)
|
||||
world.level_to_regions[level_name].append(flut_flut_course)
|
||||
world.level_to_regions[level_name].append(fort_exterior)
|
||||
world.level_to_regions[level_name].append(bunny_cave_start)
|
||||
world.level_to_regions[level_name].append(bunny_cave_end)
|
||||
world.level_to_regions[level_name].append(switch_cave)
|
||||
world.level_to_regions[level_name].append(fort_interior)
|
||||
world.level_to_regions[level_name].append(fort_interior_caches)
|
||||
world.level_to_regions[level_name].append(fort_interior_base)
|
||||
world.level_to_regions[level_name].append(fort_interior_course_end)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 200 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(12,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
return main_area
|
||||
127
worlds/jakanddaxter/regs/spider_cave_regions.py
Normal file
127
worlds/jakanddaxter/regs/spider_cave_regions.py
Normal file
@@ -0,0 +1,127 @@
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_free_scout_flies, can_fight, can_reach_orbs_level
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> JakAndDaxterRegion:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
# A large amount of this area can be covered by single jump, floating platforms, web trampolines, and goggles.
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 63)
|
||||
main_area.add_cell_locations([78, 84])
|
||||
main_area.add_fly_locations([327765, 393301, 196693, 131157])
|
||||
|
||||
# This is a virtual region describing what you need to DO to get the Dark Crystal power cell,
|
||||
# rather than describing where each of the crystals ARE, because you can destroy them in any order,
|
||||
# and you need to destroy ALL of them to get the cell.
|
||||
dark_crystals = JakAndDaxterRegion("Dark Crystals", player, multiworld, level_name, 0)
|
||||
|
||||
# can_fight = The underwater crystal in dark cave.
|
||||
# Roll Jump = The underwater crystal across a long dark eco pool.
|
||||
# The rest of the crystals can be destroyed with yellow eco in main_area.
|
||||
dark_crystals.add_cell_locations([79], access_rule=lambda state:
|
||||
can_fight(state, player)
|
||||
and state.has_all(("Roll", "Roll Jump"), player))
|
||||
|
||||
dark_cave = JakAndDaxterRegion("Dark Cave", player, multiworld, level_name, 5)
|
||||
dark_cave.add_cell_locations([80])
|
||||
dark_cave.add_fly_locations([262229], access_rule=lambda state: can_free_scout_flies(state, player))
|
||||
|
||||
robot_cave = JakAndDaxterRegion("Robot Cave", player, multiworld, level_name, 0)
|
||||
|
||||
# Need double jump for orbs.
|
||||
scaffolding_level_zero = JakAndDaxterRegion("Robot Scaffolding Level 0", player, multiworld, level_name, 12)
|
||||
|
||||
scaffolding_level_one = JakAndDaxterRegion("Robot Scaffolding Level 1", player, multiworld, level_name, 53)
|
||||
scaffolding_level_one.add_fly_locations([85]) # Shootable.
|
||||
|
||||
scaffolding_level_two = JakAndDaxterRegion("Robot Scaffolding Level 2", player, multiworld, level_name, 4)
|
||||
|
||||
# Using the blue eco from the pole course, you can single jump to the scout fly up here.
|
||||
scaffolding_level_three = JakAndDaxterRegion("Robot Scaffolding Level 3", player, multiworld, level_name, 29)
|
||||
scaffolding_level_three.add_cell_locations([81])
|
||||
scaffolding_level_three.add_fly_locations([65621])
|
||||
|
||||
pole_course = JakAndDaxterRegion("Pole Course", player, multiworld, level_name, 18)
|
||||
pole_course.add_cell_locations([82])
|
||||
|
||||
# You only need combat to fight through the spiders, but to collect the orb crates,
|
||||
# you will need the yellow eco vent unlocked.
|
||||
spider_tunnel = JakAndDaxterRegion("Spider Tunnel", player, multiworld, level_name, 4)
|
||||
spider_tunnel.add_cell_locations([83])
|
||||
|
||||
spider_tunnel_crates = JakAndDaxterRegion("Spider Tunnel Orb Crates", player, multiworld, level_name, 12)
|
||||
|
||||
main_area.connect(dark_crystals)
|
||||
main_area.connect(robot_cave)
|
||||
main_area.connect(dark_cave, rule=lambda state:
|
||||
can_fight(state, player)
|
||||
and (state.has("Double Jump", player)
|
||||
or state.has_all(("Crouch", "Crouch Jump"), player)))
|
||||
|
||||
robot_cave.connect(main_area)
|
||||
robot_cave.connect(pole_course) # Nothing special required.
|
||||
robot_cave.connect(scaffolding_level_one) # Ramps lead to level 1.
|
||||
robot_cave.connect(spider_tunnel) # Web trampolines (bounce twice on each to gain momentum).
|
||||
|
||||
pole_course.connect(robot_cave) # Blue eco platform down.
|
||||
|
||||
scaffolding_level_one.connect(robot_cave) # All scaffolding (level 1+) connects back by jumping down.
|
||||
|
||||
# Elevator, but the orbs need double jump or jump kick.
|
||||
scaffolding_level_one.connect(scaffolding_level_zero, rule=lambda state:
|
||||
state.has_any(("Double Jump", "Jump Kick"), player))
|
||||
|
||||
# Narrow enough that enemies are unavoidable.
|
||||
scaffolding_level_one.connect(scaffolding_level_two, rule=lambda state: can_fight(state, player))
|
||||
|
||||
scaffolding_level_zero.connect(scaffolding_level_one) # Elevator.
|
||||
|
||||
scaffolding_level_two.connect(robot_cave) # Jump down.
|
||||
scaffolding_level_two.connect(scaffolding_level_one) # Elevator.
|
||||
|
||||
# Elevator, but narrow enough that enemies are unavoidable.
|
||||
scaffolding_level_two.connect(scaffolding_level_three, rule=lambda state: can_fight(state, player))
|
||||
|
||||
scaffolding_level_three.connect(robot_cave) # Jump down.
|
||||
scaffolding_level_three.connect(scaffolding_level_two) # Elevator.
|
||||
|
||||
spider_tunnel.connect(robot_cave) # Back to web trampolines.
|
||||
spider_tunnel.connect(main_area) # Escape with jump pad.
|
||||
|
||||
# Requires yellow eco switch.
|
||||
spider_tunnel.connect(spider_tunnel_crates, rule=lambda state: state.has("Yellow Eco Switch", player))
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
world.level_to_regions[level_name].append(dark_crystals)
|
||||
world.level_to_regions[level_name].append(dark_cave)
|
||||
world.level_to_regions[level_name].append(robot_cave)
|
||||
world.level_to_regions[level_name].append(scaffolding_level_zero)
|
||||
world.level_to_regions[level_name].append(scaffolding_level_one)
|
||||
world.level_to_regions[level_name].append(scaffolding_level_two)
|
||||
world.level_to_regions[level_name].append(scaffolding_level_three)
|
||||
world.level_to_regions[level_name].append(pole_course)
|
||||
world.level_to_regions[level_name].append(spider_tunnel)
|
||||
world.level_to_regions[level_name].append(spider_tunnel_crates)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 200 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(13,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
return main_area
|
||||
52
worlds/jakanddaxter/regs/volcanic_crater_regions.py
Normal file
52
worlds/jakanddaxter/regs/volcanic_crater_regions.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from .region_base import JakAndDaxterRegion
|
||||
from ..options import EnableOrbsanity
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .. import JakAndDaxterWorld
|
||||
from ..rules import can_free_scout_flies, can_reach_orbs_level
|
||||
from ..locs import scout_locations as scouts
|
||||
|
||||
|
||||
def build_regions(level_name: str, world: "JakAndDaxterWorld") -> JakAndDaxterRegion:
|
||||
multiworld = world.multiworld
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
# No area is inaccessible in VC even with only running and jumping.
|
||||
main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50)
|
||||
main_area.add_cell_locations([96], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None))
|
||||
main_area.add_cell_locations([97], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, 96))
|
||||
main_area.add_cell_locations([98], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, 97))
|
||||
main_area.add_cell_locations([99], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, 98))
|
||||
main_area.add_cell_locations([100], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, None))
|
||||
main_area.add_cell_locations([101], access_rule=lambda state: world.can_trade(state, world.total_trade_orbs, 100))
|
||||
|
||||
# Hidden Power Cell: you can carry yellow eco from Spider Cave just by running and jumping
|
||||
# and using your Goggles to shoot the box (you do not need Punch to shoot from FP mode).
|
||||
main_area.add_cell_locations([74])
|
||||
|
||||
# No blue eco sources in this area, all boxes must be broken by hand (yellow eco can't be carried far enough).
|
||||
main_area.add_fly_locations(scouts.locVC_scoutTable.keys(), access_rule=lambda state:
|
||||
can_free_scout_flies(state, player))
|
||||
|
||||
# Approach the gondola to get this check.
|
||||
main_area.add_special_locations([105])
|
||||
|
||||
world.level_to_regions[level_name].append(main_area)
|
||||
|
||||
# If Per-Level Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
|
||||
# accessible to Main Area. The Locations within are automatically checked when you collect enough orbs.
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
|
||||
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld, level_name)
|
||||
|
||||
bundle_count = 50 // world.orb_bundle_size
|
||||
for bundle_index in range(bundle_count):
|
||||
amount = world.orb_bundle_size * (bundle_index + 1)
|
||||
orbs.add_orb_locations(11,
|
||||
bundle_index,
|
||||
access_rule=lambda state, level=level_name, orb_amount=amount:
|
||||
can_reach_orbs_level(state, player, world, level, orb_amount))
|
||||
multiworld.regions.append(orbs)
|
||||
main_area.connect(orbs)
|
||||
|
||||
return main_area
|
||||
1
worlds/jakanddaxter/requirements.txt
Normal file
1
worlds/jakanddaxter/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
Pymem>=1.13.0
|
||||
230
worlds/jakanddaxter/rules.py
Normal file
230
worlds/jakanddaxter/rules.py
Normal file
@@ -0,0 +1,230 @@
|
||||
import typing
|
||||
from BaseClasses import CollectionState
|
||||
from Options import OptionError
|
||||
from .options import (EnableOrbsanity,
|
||||
GlobalOrbsanityBundleSize,
|
||||
PerLevelOrbsanityBundleSize,
|
||||
FireCanyonCellCount,
|
||||
MountainPassCellCount,
|
||||
LavaTubeCellCount,
|
||||
CitizenOrbTradeAmount,
|
||||
OracleOrbTradeAmount)
|
||||
from .locs import cell_locations as cells
|
||||
from .locations import location_table
|
||||
from .levels import level_table
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from . import JakAndDaxterWorld
|
||||
|
||||
|
||||
def set_orb_trade_rule(world: "JakAndDaxterWorld"):
|
||||
options = world.options
|
||||
player = world.player
|
||||
|
||||
if options.enable_orbsanity == EnableOrbsanity.option_off:
|
||||
world.can_trade = lambda state, required_orbs, required_previous_trade: (
|
||||
can_trade_vanilla(state, player, world, required_orbs, required_previous_trade))
|
||||
else:
|
||||
world.can_trade = lambda state, required_orbs, required_previous_trade: (
|
||||
can_trade_orbsanity(state, player, world, required_orbs, required_previous_trade))
|
||||
|
||||
|
||||
def recalculate_reachable_orbs(state: CollectionState, player: int, world: "JakAndDaxterWorld") -> None:
|
||||
|
||||
# Recalculate every level, every time the cache is stale, because you don't know
|
||||
# when a specific bundle of orbs in one level may unlock access to another.
|
||||
accessible_total_orbs = 0
|
||||
for level in level_table:
|
||||
accessible_level_orbs = count_reachable_orbs_level(state, world, level)
|
||||
accessible_total_orbs += accessible_level_orbs
|
||||
state.prog_items[player][f"{level} Reachable Orbs".lstrip()] = accessible_level_orbs
|
||||
|
||||
# Also recalculate the global count, still used even when Orbsanity is Off.
|
||||
state.prog_items[player]["Reachable Orbs"] = accessible_total_orbs
|
||||
state.prog_items[player]["Reachable Orbs Fresh"] = True
|
||||
|
||||
|
||||
def count_reachable_orbs_global(state: CollectionState,
|
||||
world: "JakAndDaxterWorld") -> int:
|
||||
|
||||
accessible_orbs = 0
|
||||
for level_regions in world.level_to_orb_regions.values():
|
||||
for region in level_regions:
|
||||
if region.can_reach(state):
|
||||
accessible_orbs += region.orb_count
|
||||
return accessible_orbs
|
||||
|
||||
|
||||
def count_reachable_orbs_level(state: CollectionState,
|
||||
world: "JakAndDaxterWorld",
|
||||
level_name: str = "") -> int:
|
||||
|
||||
accessible_orbs = 0
|
||||
for region in world.level_to_orb_regions[level_name]:
|
||||
if region.can_reach(state):
|
||||
accessible_orbs += region.orb_count
|
||||
return accessible_orbs
|
||||
|
||||
|
||||
def can_reach_orbs_global(state: CollectionState,
|
||||
player: int,
|
||||
world: "JakAndDaxterWorld",
|
||||
orb_amount: int) -> bool:
|
||||
|
||||
if not state.prog_items[player]["Reachable Orbs Fresh"]:
|
||||
recalculate_reachable_orbs(state, player, world)
|
||||
|
||||
return state.has("Reachable Orbs", player, orb_amount)
|
||||
|
||||
|
||||
def can_reach_orbs_level(state: CollectionState,
|
||||
player: int,
|
||||
world: "JakAndDaxterWorld",
|
||||
level_name: str,
|
||||
orb_amount: int) -> bool:
|
||||
|
||||
if not state.prog_items[player]["Reachable Orbs Fresh"]:
|
||||
recalculate_reachable_orbs(state, player, world)
|
||||
|
||||
return state.has(f"{level_name} Reachable Orbs", player, orb_amount)
|
||||
|
||||
|
||||
def can_trade_vanilla(state: CollectionState,
|
||||
player: int,
|
||||
world: "JakAndDaxterWorld",
|
||||
required_orbs: int,
|
||||
required_previous_trade: typing.Optional[int] = None) -> bool:
|
||||
|
||||
# With Orbsanity Off, Reachable Orbs are in fact Tradeable Orbs.
|
||||
if not state.prog_items[player]["Reachable Orbs Fresh"]:
|
||||
recalculate_reachable_orbs(state, player, world)
|
||||
|
||||
if required_previous_trade:
|
||||
name_of_previous_trade = location_table[cells.to_ap_id(required_previous_trade)]
|
||||
return (state.has("Reachable Orbs", player, required_orbs)
|
||||
and state.can_reach_location(name_of_previous_trade, player=player))
|
||||
return state.has("Reachable Orbs", player, required_orbs)
|
||||
|
||||
|
||||
def can_trade_orbsanity(state: CollectionState,
|
||||
player: int,
|
||||
world: "JakAndDaxterWorld",
|
||||
required_orbs: int,
|
||||
required_previous_trade: typing.Optional[int] = None) -> bool:
|
||||
|
||||
# Yes, even Orbsanity trades may unlock access to new Reachable Orbs.
|
||||
if not state.prog_items[player]["Reachable Orbs Fresh"]:
|
||||
recalculate_reachable_orbs(state, player, world)
|
||||
|
||||
if required_previous_trade:
|
||||
name_of_previous_trade = location_table[cells.to_ap_id(required_previous_trade)]
|
||||
return (state.has("Tradeable Orbs", player, required_orbs)
|
||||
and state.can_reach_location(name_of_previous_trade, player=player))
|
||||
return state.has("Tradeable Orbs", player, required_orbs)
|
||||
|
||||
|
||||
def can_free_scout_flies(state: CollectionState, player: int) -> bool:
|
||||
return state.has("Jump Dive", player) or state.has_all({"Crouch", "Crouch Uppercut"}, player)
|
||||
|
||||
|
||||
def can_fight(state: CollectionState, player: int) -> bool:
|
||||
return state.has_any(("Jump Dive", "Jump Kick", "Punch", "Kick"), player)
|
||||
|
||||
|
||||
def enforce_multiplayer_limits(world: "JakAndDaxterWorld"):
|
||||
options = world.options
|
||||
friendly_message = ""
|
||||
|
||||
if (options.enable_orbsanity == EnableOrbsanity.option_global
|
||||
and (options.global_orbsanity_bundle_size.value < GlobalOrbsanityBundleSize.friendly_minimum
|
||||
or options.global_orbsanity_bundle_size.value > GlobalOrbsanityBundleSize.friendly_maximum)):
|
||||
friendly_message += (f" "
|
||||
f"{options.global_orbsanity_bundle_size.display_name} must be no less than "
|
||||
f"{GlobalOrbsanityBundleSize.friendly_minimum} and no greater than "
|
||||
f"{GlobalOrbsanityBundleSize.friendly_maximum} (currently "
|
||||
f"{options.global_orbsanity_bundle_size.value}).\n")
|
||||
|
||||
if (options.enable_orbsanity == EnableOrbsanity.option_per_level
|
||||
and options.level_orbsanity_bundle_size.value < PerLevelOrbsanityBundleSize.friendly_minimum):
|
||||
friendly_message += (f" "
|
||||
f"{options.level_orbsanity_bundle_size.display_name} must be no less than "
|
||||
f"{PerLevelOrbsanityBundleSize.friendly_minimum} (currently "
|
||||
f"{options.level_orbsanity_bundle_size.value}).\n")
|
||||
|
||||
if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum:
|
||||
friendly_message += (f" "
|
||||
f"{options.fire_canyon_cell_count.display_name} must be no greater than "
|
||||
f"{FireCanyonCellCount.friendly_maximum} (currently "
|
||||
f"{options.fire_canyon_cell_count.value}).\n")
|
||||
|
||||
if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum:
|
||||
friendly_message += (f" "
|
||||
f"{options.mountain_pass_cell_count.display_name} must be no greater than "
|
||||
f"{MountainPassCellCount.friendly_maximum} (currently "
|
||||
f"{options.mountain_pass_cell_count.value}).\n")
|
||||
|
||||
if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum:
|
||||
friendly_message += (f" "
|
||||
f"{options.lava_tube_cell_count.display_name} must be no greater than "
|
||||
f"{LavaTubeCellCount.friendly_maximum} (currently "
|
||||
f"{options.lava_tube_cell_count.value}).\n")
|
||||
|
||||
if options.citizen_orb_trade_amount.value > CitizenOrbTradeAmount.friendly_maximum:
|
||||
friendly_message += (f" "
|
||||
f"{options.citizen_orb_trade_amount.display_name} must be no greater than "
|
||||
f"{CitizenOrbTradeAmount.friendly_maximum} (currently "
|
||||
f"{options.citizen_orb_trade_amount.value}).\n")
|
||||
|
||||
if options.oracle_orb_trade_amount.value > OracleOrbTradeAmount.friendly_maximum:
|
||||
friendly_message += (f" "
|
||||
f"{options.oracle_orb_trade_amount.display_name} must be no greater than "
|
||||
f"{OracleOrbTradeAmount.friendly_maximum} (currently "
|
||||
f"{options.oracle_orb_trade_amount.value}).\n")
|
||||
|
||||
if friendly_message != "":
|
||||
raise OptionError(f"{world.player_name}: The options you have chosen may disrupt the multiworld. \n"
|
||||
f"Please adjust the following Options for a multiplayer game. \n"
|
||||
f"{friendly_message}"
|
||||
f"Or use 'random-range-x-y' instead of 'random' in your player yaml.\n"
|
||||
f"Or set 'enforce_friendly_options' in the seed generator's host.yaml to false. "
|
||||
f"(Use at your own risk!)")
|
||||
|
||||
|
||||
def enforce_singleplayer_limits(world: "JakAndDaxterWorld"):
|
||||
options = world.options
|
||||
friendly_message = ""
|
||||
|
||||
if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum:
|
||||
friendly_message += (f" "
|
||||
f"{options.fire_canyon_cell_count.display_name} must be no greater than "
|
||||
f"{FireCanyonCellCount.friendly_maximum} (currently "
|
||||
f"{options.fire_canyon_cell_count.value}).\n")
|
||||
|
||||
if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum:
|
||||
friendly_message += (f" "
|
||||
f"{options.mountain_pass_cell_count.display_name} must be no greater than "
|
||||
f"{MountainPassCellCount.friendly_maximum} (currently "
|
||||
f"{options.mountain_pass_cell_count.value}).\n")
|
||||
|
||||
if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum:
|
||||
friendly_message += (f" "
|
||||
f"{options.lava_tube_cell_count.display_name} must be no greater than "
|
||||
f"{LavaTubeCellCount.friendly_maximum} (currently "
|
||||
f"{options.lava_tube_cell_count.value}).\n")
|
||||
|
||||
if friendly_message != "":
|
||||
raise OptionError(f"The options you have chosen may result in seed generation failures. \n"
|
||||
f"Please adjust the following Options for a singleplayer game. \n"
|
||||
f"{friendly_message}"
|
||||
f"Or use 'random-range-x-y' instead of 'random' in your player yaml.\n"
|
||||
f"Or set 'enforce_friendly_options' in your host.yaml to false. "
|
||||
f"(Use at your own risk!)")
|
||||
|
||||
|
||||
def verify_orb_trade_amounts(world: "JakAndDaxterWorld"):
|
||||
|
||||
if world.total_trade_orbs > 2000:
|
||||
raise OptionError(f"{world.player_name}: Required number of orbs for all trades ({world.total_trade_orbs}) "
|
||||
f"is more than all the orbs in the game (2000). Reduce the value of either "
|
||||
f"{world.options.citizen_orb_trade_amount.display_name} "
|
||||
f"or {world.options.oracle_orb_trade_amount.display_name}.")
|
||||
0
worlds/jakanddaxter/test/__init__.py
Normal file
0
worlds/jakanddaxter/test/__init__.py
Normal file
107
worlds/jakanddaxter/test/bases.py
Normal file
107
worlds/jakanddaxter/test/bases.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from worlds.jakanddaxter import JakAndDaxterWorld
|
||||
from ..game_id import jak1_name
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class JakAndDaxterTestBase(WorldTestBase):
|
||||
game = jak1_name
|
||||
world: JakAndDaxterWorld
|
||||
|
||||
level_info = {
|
||||
"Geyser Rock": {
|
||||
"cells": 4,
|
||||
"flies": 7,
|
||||
"orbs": 50,
|
||||
"caches": 0,
|
||||
},
|
||||
"Sandover Village": {
|
||||
"cells": 6,
|
||||
"flies": 7,
|
||||
"orbs": 50,
|
||||
"caches": 1,
|
||||
},
|
||||
"Forbidden Jungle": {
|
||||
"cells": 8,
|
||||
"flies": 7,
|
||||
"orbs": 150,
|
||||
"caches": 1,
|
||||
},
|
||||
"Sentinel Beach": {
|
||||
"cells": 8,
|
||||
"flies": 7,
|
||||
"orbs": 150,
|
||||
"caches": 2,
|
||||
},
|
||||
"Misty Island": {
|
||||
"cells": 8,
|
||||
"flies": 7,
|
||||
"orbs": 150,
|
||||
"caches": 1,
|
||||
},
|
||||
"Fire Canyon": {
|
||||
"cells": 2,
|
||||
"flies": 7,
|
||||
"orbs": 50,
|
||||
"caches": 0,
|
||||
},
|
||||
"Rock Village": {
|
||||
"cells": 6,
|
||||
"flies": 7,
|
||||
"orbs": 50,
|
||||
"caches": 1,
|
||||
},
|
||||
"Precursor Basin": {
|
||||
"cells": 8,
|
||||
"flies": 7,
|
||||
"orbs": 200,
|
||||
"caches": 0,
|
||||
},
|
||||
"Lost Precursor City": {
|
||||
"cells": 8,
|
||||
"flies": 7,
|
||||
"orbs": 200,
|
||||
"caches": 2,
|
||||
},
|
||||
"Boggy Swamp": {
|
||||
"cells": 8,
|
||||
"flies": 7,
|
||||
"orbs": 200,
|
||||
"caches": 0,
|
||||
},
|
||||
"Mountain Pass": {
|
||||
"cells": 4,
|
||||
"flies": 7,
|
||||
"orbs": 50,
|
||||
"caches": 0,
|
||||
},
|
||||
"Volcanic Crater": {
|
||||
"cells": 8,
|
||||
"flies": 7,
|
||||
"orbs": 50,
|
||||
"caches": 0,
|
||||
},
|
||||
"Spider Cave": {
|
||||
"cells": 8,
|
||||
"flies": 7,
|
||||
"orbs": 200,
|
||||
"caches": 0,
|
||||
},
|
||||
"Snowy Mountain": {
|
||||
"cells": 8,
|
||||
"flies": 7,
|
||||
"orbs": 200,
|
||||
"caches": 3,
|
||||
},
|
||||
"Lava Tube": {
|
||||
"cells": 2,
|
||||
"flies": 7,
|
||||
"orbs": 50,
|
||||
"caches": 0,
|
||||
},
|
||||
"Gol and Maia's Citadel": {
|
||||
"cells": 5,
|
||||
"flies": 7,
|
||||
"orbs": 200,
|
||||
"caches": 3,
|
||||
},
|
||||
}
|
||||
52
worlds/jakanddaxter/test/test_locations.py
Normal file
52
worlds/jakanddaxter/test/test_locations.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import typing
|
||||
|
||||
from .bases import JakAndDaxterTestBase
|
||||
from ..game_id import jak1_id
|
||||
from ..regs.region_base import JakAndDaxterRegion
|
||||
from ..locs import (scout_locations as scouts,
|
||||
special_locations as specials,
|
||||
orb_cache_locations as caches,
|
||||
orb_locations as orbs)
|
||||
|
||||
|
||||
class LocationsTest(JakAndDaxterTestBase):
|
||||
|
||||
def get_regions(self):
|
||||
return [typing.cast(JakAndDaxterRegion, reg) for reg in self.multiworld.get_regions(self.player)]
|
||||
|
||||
def test_count_cells(self):
|
||||
|
||||
for level in self.level_info:
|
||||
cell_count = 0
|
||||
sublevels = [reg for reg in self.get_regions() if reg.level_name == level]
|
||||
for sl in sublevels:
|
||||
for loc in sl.locations:
|
||||
if loc.address in range(jak1_id, jak1_id + scouts.fly_offset):
|
||||
cell_count += 1
|
||||
self.assertEqual(self.level_info[level]["cells"] - 1, cell_count, level) # Don't count the Free 7 Cells.
|
||||
|
||||
def test_count_flies(self):
|
||||
for level in self.level_info:
|
||||
fly_count = 0
|
||||
sublevels = [reg for reg in self.get_regions() if reg.level_name == level]
|
||||
for sl in sublevels:
|
||||
for loc in sl.locations:
|
||||
if loc.address in range(jak1_id + scouts.fly_offset, jak1_id + specials.special_offset):
|
||||
fly_count += 1
|
||||
self.assertEqual(self.level_info[level]["flies"], fly_count, level)
|
||||
|
||||
def test_count_orbs(self):
|
||||
for level in self.level_info:
|
||||
sublevels = [reg for reg in self.get_regions() if reg.level_name == level]
|
||||
orb_count = sum([reg.orb_count for reg in sublevels])
|
||||
self.assertEqual(self.level_info[level]["orbs"], orb_count, level)
|
||||
|
||||
def test_count_caches(self):
|
||||
for level in self.level_info:
|
||||
cache_count = 0
|
||||
sublevels = [reg for reg in self.get_regions() if reg.level_name == level]
|
||||
for sl in sublevels:
|
||||
for loc in sl.locations:
|
||||
if loc.address in range(jak1_id + caches.orb_cache_offset, jak1_id + orbs.orb_offset):
|
||||
cache_count += 1
|
||||
self.assertEqual(self.level_info[level]["caches"], cache_count, level)
|
||||
32
worlds/jakanddaxter/test/test_moverando.py
Normal file
32
worlds/jakanddaxter/test/test_moverando.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from .bases import JakAndDaxterTestBase
|
||||
from ..items import move_item_table
|
||||
|
||||
|
||||
class MoveRandoTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"enable_move_randomizer": True
|
||||
}
|
||||
|
||||
def test_move_items_in_pool(self):
|
||||
for move in move_item_table:
|
||||
self.assertIn(move_item_table[move], {item.name for item in self.multiworld.itempool})
|
||||
self.assertNotIn(move_item_table[move],
|
||||
{item.name for item in self.multiworld.precollected_items[self.player]})
|
||||
|
||||
def test_cannot_reach_without_move(self):
|
||||
self.assertAccessDependency(
|
||||
["GR: Climb Up The Cliff"],
|
||||
[["Double Jump"], ["Crouch"]],
|
||||
only_check_listed=True)
|
||||
|
||||
|
||||
class NoMoveRandoTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"enable_move_randomizer": False
|
||||
}
|
||||
|
||||
def test_move_items_in_inventory(self):
|
||||
for move in move_item_table:
|
||||
self.assertNotIn(move_item_table[move], {item.name for item in self.multiworld.itempool})
|
||||
self.assertIn(move_item_table[move],
|
||||
{item.name for item in self.multiworld.precollected_items[self.player]})
|
||||
61
worlds/jakanddaxter/test/test_orbsanity.py
Normal file
61
worlds/jakanddaxter/test/test_orbsanity.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from .bases import JakAndDaxterTestBase
|
||||
from ..items import orb_item_table
|
||||
|
||||
|
||||
class NoOrbsanityTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"enable_orbsanity": 0, # Off
|
||||
"level_orbsanity_bundle_size": 25,
|
||||
"global_orbsanity_bundle_size": 16
|
||||
}
|
||||
|
||||
def test_orb_bundles_not_exist_in_pool(self):
|
||||
for bundle in orb_item_table:
|
||||
self.assertNotIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool})
|
||||
|
||||
def test_orb_bundle_count(self):
|
||||
bundle_name = orb_item_table[self.options["level_orbsanity_bundle_size"]]
|
||||
count = len([item.name for item in self.multiworld.itempool if item.name == bundle_name])
|
||||
self.assertEqual(0, count)
|
||||
|
||||
bundle_name = orb_item_table[self.options["global_orbsanity_bundle_size"]]
|
||||
count = len([item.name for item in self.multiworld.itempool if item.name == bundle_name])
|
||||
self.assertEqual(0, count)
|
||||
|
||||
|
||||
class PerLevelOrbsanityTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"enable_orbsanity": 1, # Per Level
|
||||
"level_orbsanity_bundle_size": 25
|
||||
}
|
||||
|
||||
def test_orb_bundles_exist_in_pool(self):
|
||||
for bundle in orb_item_table:
|
||||
if bundle == self.options["level_orbsanity_bundle_size"]:
|
||||
self.assertIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool})
|
||||
else:
|
||||
self.assertNotIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool})
|
||||
|
||||
def test_orb_bundle_count(self):
|
||||
bundle_name = orb_item_table[self.options["level_orbsanity_bundle_size"]]
|
||||
count = len([item.name for item in self.multiworld.itempool if item.name == bundle_name])
|
||||
self.assertEqual(80, count)
|
||||
|
||||
|
||||
class GlobalOrbsanityTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"enable_orbsanity": 2, # Global
|
||||
"global_orbsanity_bundle_size": 16
|
||||
}
|
||||
|
||||
def test_orb_bundles_exist_in_pool(self):
|
||||
for bundle in orb_item_table:
|
||||
if bundle == self.options["global_orbsanity_bundle_size"]:
|
||||
self.assertIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool})
|
||||
else:
|
||||
self.assertNotIn(orb_item_table[bundle], {item.name for item in self.multiworld.itempool})
|
||||
|
||||
def test_orb_bundle_count(self):
|
||||
bundle_name = orb_item_table[self.options["global_orbsanity_bundle_size"]]
|
||||
count = len([item.name for item in self.multiworld.itempool if item.name == bundle_name])
|
||||
self.assertEqual(125, count)
|
||||
29
worlds/jakanddaxter/test/test_orderedcellcounts.py
Normal file
29
worlds/jakanddaxter/test/test_orderedcellcounts.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from .bases import JakAndDaxterTestBase
|
||||
|
||||
|
||||
class ReorderedCellCountsTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"enable_ordered_cell_counts": True,
|
||||
"fire_canyon_cell_count": 20,
|
||||
"mountain_pass_cell_count": 15,
|
||||
"lava_tube_cell_count": 10,
|
||||
}
|
||||
|
||||
def test_reordered_cell_counts(self):
|
||||
self.world.generate_early()
|
||||
self.assertLessEqual(self.world.options.fire_canyon_cell_count, self.world.options.mountain_pass_cell_count)
|
||||
self.assertLessEqual(self.world.options.mountain_pass_cell_count, self.world.options.lava_tube_cell_count)
|
||||
|
||||
|
||||
class UnorderedCellCountsTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"enable_ordered_cell_counts": False,
|
||||
"fire_canyon_cell_count": 20,
|
||||
"mountain_pass_cell_count": 15,
|
||||
"lava_tube_cell_count": 10,
|
||||
}
|
||||
|
||||
def test_unordered_cell_counts(self):
|
||||
self.world.generate_early()
|
||||
self.assertGreaterEqual(self.world.options.fire_canyon_cell_count, self.world.options.mountain_pass_cell_count)
|
||||
self.assertGreaterEqual(self.world.options.mountain_pass_cell_count, self.world.options.lava_tube_cell_count)
|
||||
39
worlds/jakanddaxter/test/test_trades.py
Normal file
39
worlds/jakanddaxter/test/test_trades.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from .bases import JakAndDaxterTestBase
|
||||
|
||||
|
||||
class TradesCostNothingTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"enable_orbsanity": 2,
|
||||
"global_orbsanity_bundle_size": 5,
|
||||
"citizen_orb_trade_amount": 0,
|
||||
"oracle_orb_trade_amount": 0
|
||||
}
|
||||
|
||||
def test_orb_items_are_filler(self):
|
||||
self.collect_all_but("")
|
||||
self.assertNotIn("5 Precursor Orbs", self.multiworld.state.prog_items)
|
||||
|
||||
def test_trades_are_accessible(self):
|
||||
self.assertTrue(self.multiworld
|
||||
.get_location("SV: Bring 90 Orbs To The Mayor", self.player)
|
||||
.can_reach(self.multiworld.state))
|
||||
|
||||
|
||||
class TradesCostEverythingTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"enable_orbsanity": 2,
|
||||
"global_orbsanity_bundle_size": 5,
|
||||
"citizen_orb_trade_amount": 120,
|
||||
"oracle_orb_trade_amount": 150
|
||||
}
|
||||
|
||||
def test_orb_items_are_progression(self):
|
||||
self.collect_all_but("")
|
||||
self.assertIn("5 Precursor Orbs", self.multiworld.state.prog_items[self.player])
|
||||
self.assertEqual(396, self.multiworld.state.prog_items[self.player]["5 Precursor Orbs"])
|
||||
|
||||
def test_trades_are_accessible(self):
|
||||
self.collect_all_but("")
|
||||
self.assertTrue(self.multiworld
|
||||
.get_location("SV: Bring 90 Orbs To The Mayor", self.player)
|
||||
.can_reach(self.multiworld.state))
|
||||
80
worlds/jakanddaxter/test/test_traps.py
Normal file
80
worlds/jakanddaxter/test/test_traps.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from BaseClasses import ItemClassification
|
||||
from .bases import JakAndDaxterTestBase
|
||||
|
||||
|
||||
class NoTrapsTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"filler_power_cells_replaced_with_traps": 0,
|
||||
"filler_orb_bundles_replaced_with_traps": 0,
|
||||
"trap_weights": {"Trip Trap": 1},
|
||||
}
|
||||
|
||||
def test_trap_count(self):
|
||||
count = len([item.name for item in self.multiworld.itempool
|
||||
if item.name == "Trip Trap"
|
||||
and item.classification == ItemClassification.trap])
|
||||
self.assertEqual(0, count)
|
||||
|
||||
def test_prog_power_cells_count(self):
|
||||
count = len([item.name for item in self.multiworld.itempool
|
||||
if item.name == "Power Cell"
|
||||
and item.classification == ItemClassification.progression_skip_balancing])
|
||||
self.assertEqual(72, count)
|
||||
|
||||
def test_fill_power_cells_count(self):
|
||||
count = len([item.name for item in self.multiworld.itempool
|
||||
if item.name == "Power Cell"
|
||||
and item.classification == ItemClassification.filler])
|
||||
self.assertEqual(29, count)
|
||||
|
||||
|
||||
class SomeTrapsTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"filler_power_cells_replaced_with_traps": 10,
|
||||
"filler_orb_bundles_replaced_with_traps": 10,
|
||||
"trap_weights": {"Trip Trap": 1},
|
||||
}
|
||||
|
||||
def test_trap_count(self):
|
||||
count = len([item.name for item in self.multiworld.itempool
|
||||
if item.name == "Trip Trap"
|
||||
and item.classification == ItemClassification.trap])
|
||||
self.assertEqual(10, count)
|
||||
|
||||
def test_prog_power_cells_count(self):
|
||||
count = len([item.name for item in self.multiworld.itempool
|
||||
if item.name == "Power Cell"
|
||||
and item.classification == ItemClassification.progression_skip_balancing])
|
||||
self.assertEqual(72, count)
|
||||
|
||||
def test_fill_power_cells_count(self):
|
||||
count = len([item.name for item in self.multiworld.itempool
|
||||
if item.name == "Power Cell"
|
||||
and item.classification == ItemClassification.filler])
|
||||
self.assertEqual(19, count)
|
||||
|
||||
|
||||
class MaximumTrapsTest(JakAndDaxterTestBase):
|
||||
options = {
|
||||
"filler_power_cells_replaced_with_traps": 100,
|
||||
"filler_orb_bundles_replaced_with_traps": 100,
|
||||
"trap_weights": {"Trip Trap": 1},
|
||||
}
|
||||
|
||||
def test_trap_count(self):
|
||||
count = len([item.name for item in self.multiworld.itempool
|
||||
if item.name == "Trip Trap"
|
||||
and item.classification == ItemClassification.trap])
|
||||
self.assertEqual(29, count)
|
||||
|
||||
def test_prog_power_cells_count(self):
|
||||
count = len([item.name for item in self.multiworld.itempool
|
||||
if item.name == "Power Cell"
|
||||
and item.classification == ItemClassification.progression_skip_balancing])
|
||||
self.assertEqual(72, count)
|
||||
|
||||
def test_fill_power_cells_count(self):
|
||||
count = len([item.name for item in self.multiworld.itempool
|
||||
if item.name == "Power Cell"
|
||||
and item.classification == ItemClassification.filler])
|
||||
self.assertEqual(0, count)
|
||||
@@ -8,10 +8,10 @@ import zipfile
|
||||
from .Items import item_dictionary_table
|
||||
from .Locations import all_locations, SoraLevels, exclusion_table
|
||||
from .XPValues import lvlStats, formExp, soraExp
|
||||
from worlds.Files import APContainer
|
||||
from worlds.Files import APPlayerContainer
|
||||
|
||||
|
||||
class KH2Container(APContainer):
|
||||
class KH2Container(APPlayerContainer):
|
||||
game: str = 'Kingdom Hearts 2'
|
||||
|
||||
def __init__(self, patch_data: dict, base_path: str, output_directory: str,
|
||||
|
||||
21
worlds/marioland2/LICENSE
Normal file
21
worlds/marioland2/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2023 Alex "Alchav" Avery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
449
worlds/marioland2/__init__.py
Normal file
449
worlds/marioland2/__init__.py
Normal file
@@ -0,0 +1,449 @@
|
||||
import base64
|
||||
import Utils
|
||||
import settings
|
||||
from copy import deepcopy
|
||||
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from BaseClasses import Region, Location, Item, ItemClassification, Tutorial
|
||||
|
||||
from . import client
|
||||
from .rom import generate_output, SuperMarioLand2ProcedurePatch
|
||||
from .options import SML2Options
|
||||
from .locations import (locations, location_name_to_id, level_name_to_id, level_id_to_name, START_IDS, coins_coords,
|
||||
auto_scroll_max)
|
||||
from .items import items
|
||||
from .sprites import level_sprites
|
||||
from .sprite_randomizer import randomize_enemies, randomize_platforms
|
||||
from .logic import has_pipe_up, has_pipe_down, has_pipe_left, has_pipe_right, has_level_progression, is_auto_scroll
|
||||
from . import logic
|
||||
|
||||
|
||||
class MarioLand2Settings(settings.Group):
|
||||
class SML2RomFile(settings.UserFilePath):
|
||||
"""File name of the Super Mario Land 2 1.0 ROM"""
|
||||
description = "Super Mario Land 2 - 6 Golden Coins (USA, Europe) 1.0 ROM File"
|
||||
copy_to = "Super Mario Land 2 - 6 Golden Coins (USA, Europe).gb"
|
||||
md5s = [SuperMarioLand2ProcedurePatch.hash]
|
||||
|
||||
rom_file: SML2RomFile = SML2RomFile(SML2RomFile.copy_to)
|
||||
|
||||
|
||||
class MarioLand2WebWorld(WebWorld):
|
||||
setup_en = Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to playing Super Mario Land 2 with Archipelago.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Alchav"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en]
|
||||
|
||||
|
||||
class MarioLand2World(World):
|
||||
"""Super Mario Land 2 is a classic platformer that follows Mario on a quest to reclaim his castle from the
|
||||
villainous Wario. This iconic game features 32 levels, unique power-ups, and introduces Wario as Mario's
|
||||
arch-rival.""" # -ChatGPT
|
||||
|
||||
game = "Super Mario Land 2"
|
||||
|
||||
settings_key = "sml2_options"
|
||||
settings: MarioLand2Settings
|
||||
|
||||
location_name_to_id = location_name_to_id
|
||||
item_name_to_id = {item_name: ID for ID, item_name in enumerate(items, START_IDS)}
|
||||
|
||||
web = MarioLand2WebWorld()
|
||||
|
||||
item_name_groups = {
|
||||
"Level Progression": {
|
||||
item_name for item_name in items if item_name.endswith(("Progression", "Secret", "Secret 1", "Secret 2"))
|
||||
and "Auto Scroll" not in item_name
|
||||
},
|
||||
"Bells": {item_name for item_name in items if "Bell" in item_name},
|
||||
"Golden Coins": {"Mario Coin", "Macro Coin", "Space Coin", "Tree Coin", "Turtle Coin", "Pumpkin Coin"},
|
||||
"Coins": {"1 Coin", *{f"{i} Coins" for i in range(2, 169)}},
|
||||
"Powerups": {"Mushroom", "Fire Flower", "Carrot"},
|
||||
"Difficulties": {"Easy Mode", "Normal Mode"},
|
||||
"Auto Scroll Traps": {item_name for item_name in items
|
||||
if "Auto Scroll" in item_name and "Cancel" not in item_name},
|
||||
"Cancel Auto Scrolls": {item_name for item_name in items if "Cancel Auto Scroll" in item_name},
|
||||
}
|
||||
|
||||
location_name_groups = {
|
||||
"Bosses": {
|
||||
"Tree Zone 5 - Boss", "Space Zone 2 - Boss", "Macro Zone 4 - Boss",
|
||||
"Pumpkin Zone 4 - Boss", "Mario Zone 4 - Boss", "Turtle Zone 3 - Boss"
|
||||
},
|
||||
"Normal Exits": {location for location in locations if locations[location]["type"] == "level"},
|
||||
"Secret Exits": {location for location in locations if locations[location]["type"] == "secret"},
|
||||
"Bells": {location for location in locations if locations[location]["type"] == "bell"},
|
||||
"Coins": {location for location in location_name_to_id if "Coin" in location}
|
||||
}
|
||||
|
||||
options_dataclass = SML2Options
|
||||
options: SML2Options
|
||||
|
||||
generate_output = generate_output
|
||||
|
||||
def __init__(self, world, player: int):
|
||||
super().__init__(world, player)
|
||||
self.auto_scroll_levels = []
|
||||
self.num_coin_locations = []
|
||||
self.max_coin_locations = {}
|
||||
self.sprite_data = {}
|
||||
self.coin_fragments_required = 0
|
||||
|
||||
def generate_early(self):
|
||||
self.sprite_data = deepcopy(level_sprites)
|
||||
if self.options.randomize_enemies:
|
||||
randomize_enemies(self.sprite_data, self.random)
|
||||
if self.options.randomize_platforms:
|
||||
randomize_platforms(self.sprite_data, self.random)
|
||||
|
||||
if self.options.marios_castle_midway_bell:
|
||||
self.sprite_data["Mario's Castle"][35]["sprite"] = "Midway Bell"
|
||||
|
||||
if self.options.auto_scroll_chances == "vanilla":
|
||||
self.auto_scroll_levels = [int(i in [19, 25, 30]) for i in range(32)]
|
||||
else:
|
||||
self.auto_scroll_levels = [int(self.random.randint(1, 100) <= self.options.auto_scroll_chances)
|
||||
for _ in range(32)]
|
||||
|
||||
self.auto_scroll_levels[level_name_to_id["Mario's Castle"]] = 0
|
||||
unbeatable_scroll_levels = ["Tree Zone 3", "Macro Zone 2", "Space Zone 1", "Turtle Zone 2", "Pumpkin Zone 2"]
|
||||
if not self.options.shuffle_midway_bells:
|
||||
unbeatable_scroll_levels.append("Pumpkin Zone 1")
|
||||
for level, i in enumerate(self.auto_scroll_levels):
|
||||
if i == 1:
|
||||
if self.options.auto_scroll_mode in ("global_cancel_item", "level_cancel_items"):
|
||||
self.auto_scroll_levels[level] = 2
|
||||
elif self.options.auto_scroll_mode == "chaos":
|
||||
if (self.options.accessibility == "full"
|
||||
and level_id_to_name[level] in unbeatable_scroll_levels):
|
||||
self.auto_scroll_levels[level] = 2
|
||||
else:
|
||||
self.auto_scroll_levels[level] = self.random.randint(1, 3)
|
||||
elif (self.options.accessibility == "full"
|
||||
and level_id_to_name[level] in unbeatable_scroll_levels):
|
||||
self.auto_scroll_levels[level] = 0
|
||||
if self.auto_scroll_levels[level] == 1 and "trap" in self.options.auto_scroll_mode.current_key:
|
||||
self.auto_scroll_levels[level] = 3
|
||||
|
||||
def create_regions(self):
|
||||
menu_region = Region("Menu", self.player, self.multiworld)
|
||||
self.multiworld.regions.append(menu_region)
|
||||
created_regions = []
|
||||
for location_name, data in locations.items():
|
||||
region_name = location_name.split(" -")[0]
|
||||
if region_name in created_regions:
|
||||
region = self.multiworld.get_region(region_name, self.player)
|
||||
else:
|
||||
region = Region(region_name, self.player, self.multiworld)
|
||||
if region_name == "Tree Zone Secret Course":
|
||||
region_to_connect = self.multiworld.get_region("Tree Zone 2", self.player)
|
||||
elif region_name == "Space Zone Secret Course":
|
||||
region_to_connect = self.multiworld.get_region("Space Zone 1", self.player)
|
||||
elif region_name == "Macro Zone Secret Course":
|
||||
region_to_connect = self.multiworld.get_region("Macro Zone 1", self.player)
|
||||
elif region_name == "Pumpkin Zone Secret Course 1":
|
||||
region_to_connect = self.multiworld.get_region("Pumpkin Zone 2", self.player)
|
||||
elif region_name == "Pumpkin Zone Secret Course 2":
|
||||
region_to_connect = self.multiworld.get_region("Pumpkin Zone 3", self.player)
|
||||
elif region_name == "Turtle Zone Secret Course":
|
||||
region_to_connect = self.multiworld.get_region("Turtle Zone 2", self.player)
|
||||
elif region_name.split(" ")[-1].isdigit() and int(region_name.split(" ")[-1]) > 1:
|
||||
region_to_connect = self.multiworld.get_region(" ".join(region_name.split(" ")[:2])
|
||||
+ f" {int(region_name.split(' ')[2]) - 1}",
|
||||
self.player)
|
||||
else:
|
||||
region_to_connect = menu_region
|
||||
region_to_connect.connect(region)
|
||||
self.multiworld.regions.append(region)
|
||||
created_regions.append(region_name)
|
||||
|
||||
if location_name == "Mario's Castle - Midway Bell" and not self.options.marios_castle_midway_bell:
|
||||
continue
|
||||
region.locations.append(MarioLand2Location(self.player, location_name,
|
||||
self.location_name_to_id[location_name], region))
|
||||
self.multiworld.get_region("Macro Zone Secret Course", self.player).connect(
|
||||
self.multiworld.get_region("Macro Zone 4", self.player))
|
||||
self.multiworld.get_region("Macro Zone 4", self.player).connect(
|
||||
self.multiworld.get_region("Macro Zone Secret Course", self.player))
|
||||
|
||||
castle = self.multiworld.get_region("Mario's Castle", self.player)
|
||||
wario = MarioLand2Location(self.player, "Mario's Castle - Wario", parent=castle)
|
||||
castle.locations.append(wario)
|
||||
wario.place_locked_item(MarioLand2Item("Wario Defeated", ItemClassification.progression, None, self.player))
|
||||
|
||||
if self.options.coinsanity:
|
||||
coinsanity_checks = self.options.coinsanity_checks.value
|
||||
self.num_coin_locations = [[region, 1] for region in created_regions if region != "Mario's Castle"]
|
||||
self.max_coin_locations = {region: len(coins_coords[region]) for region in created_regions
|
||||
if region != "Mario's Castle"}
|
||||
if self.options.accessibility == "full" or self.options.auto_scroll_mode == "always":
|
||||
for level in self.max_coin_locations:
|
||||
if level in auto_scroll_max and self.auto_scroll_levels[level_name_to_id[level]] in (1, 3):
|
||||
if isinstance(auto_scroll_max[level], tuple):
|
||||
self.max_coin_locations[level] = min(
|
||||
auto_scroll_max[level][int(self.options.shuffle_midway_bells.value)],
|
||||
self.max_coin_locations[level])
|
||||
else:
|
||||
self.max_coin_locations[level] = min(auto_scroll_max[level], self.max_coin_locations[level])
|
||||
coinsanity_checks = min(sum(self.max_coin_locations.values()), coinsanity_checks)
|
||||
for i in range(coinsanity_checks - 31):
|
||||
self.num_coin_locations.sort(key=lambda region: self.max_coin_locations[region[0]] / region[1])
|
||||
self.num_coin_locations[-1][1] += 1
|
||||
coin_locations = []
|
||||
for level, coins in self.num_coin_locations:
|
||||
if self.max_coin_locations[level]:
|
||||
coin_thresholds = self.random.sample(range(1, self.max_coin_locations[level] + 1), coins)
|
||||
coin_locations += [f"{level} - {i} Coin{'s' if i > 1 else ''}" for i in coin_thresholds]
|
||||
for location_name in coin_locations:
|
||||
region = self.multiworld.get_region(location_name.split(" -")[0], self.player)
|
||||
region.locations.append(MarioLand2Location(self.player, location_name,
|
||||
self.location_name_to_id[location_name], parent=region))
|
||||
|
||||
def set_rules(self):
|
||||
entrance_rules = {
|
||||
"Menu -> Space Zone 1": lambda state: state.has("Hippo Bubble", self.player)
|
||||
or (state.has("Carrot", self.player)
|
||||
and not is_auto_scroll(state, self.player, "Hippo Zone")),
|
||||
"Space Zone 1 -> Space Zone Secret Course": lambda state: state.has("Space Zone Secret", self.player),
|
||||
"Space Zone 1 -> Space Zone 2": lambda state: has_level_progression(state, "Space Zone Progression", self.player),
|
||||
"Tree Zone 1 -> Tree Zone 2": lambda state: has_level_progression(state, "Tree Zone Progression", self.player),
|
||||
"Tree Zone 2 -> Tree Zone Secret Course": lambda state: state.has("Tree Zone Secret", self.player),
|
||||
"Tree Zone 2 -> Tree Zone 3": lambda state: has_level_progression(state, "Tree Zone Progression", self.player, 2),
|
||||
"Tree Zone 4 -> Tree Zone 5": lambda state: has_level_progression(state, "Tree Zone Progression", self.player, 3),
|
||||
"Macro Zone 1 -> Macro Zone Secret Course": lambda state: state.has("Macro Zone Secret 1", self.player),
|
||||
"Macro Zone Secret Course -> Macro Zone 4": lambda state: state.has("Macro Zone Secret 2", self.player),
|
||||
"Macro Zone 1 -> Macro Zone 2": lambda state: has_level_progression(state, "Macro Zone Progression", self.player),
|
||||
"Macro Zone 2 -> Macro Zone 3": lambda state: has_level_progression(state, "Macro Zone Progression", self.player, 2),
|
||||
"Macro Zone 3 -> Macro Zone 4": lambda state: has_level_progression(state, "Macro Zone Progression", self.player, 3),
|
||||
"Macro Zone 4 -> Macro Zone Secret Course": lambda state: state.has("Macro Zone Secret 2", self.player),
|
||||
"Pumpkin Zone 1 -> Pumpkin Zone 2": lambda state: has_level_progression(state, "Pumpkin Zone Progression", self.player),
|
||||
"Pumpkin Zone 2 -> Pumpkin Zone Secret Course 1": lambda state: state.has("Pumpkin Zone Secret 1", self.player),
|
||||
"Pumpkin Zone 2 -> Pumpkin Zone 3": lambda state: has_level_progression(state, "Pumpkin Zone Progression", self.player, 2),
|
||||
"Pumpkin Zone 3 -> Pumpkin Zone Secret Course 2": lambda state: state.has("Pumpkin Zone Secret 2", self.player),
|
||||
"Pumpkin Zone 3 -> Pumpkin Zone 4": lambda state: has_level_progression(state, "Pumpkin Zone Progression", self.player, 3),
|
||||
"Mario Zone 1 -> Mario Zone 2": lambda state: has_level_progression(state, "Mario Zone Progression", self.player),
|
||||
"Mario Zone 2 -> Mario Zone 3": lambda state: has_level_progression(state, "Mario Zone Progression", self.player, 2),
|
||||
"Mario Zone 3 -> Mario Zone 4": lambda state: has_level_progression(state, "Mario Zone Progression", self.player, 3),
|
||||
"Turtle Zone 1 -> Turtle Zone 2": lambda state: has_level_progression(state, "Turtle Zone Progression", self.player),
|
||||
"Turtle Zone 2 -> Turtle Zone Secret Course": lambda state: state.has("Turtle Zone Secret", self.player),
|
||||
"Turtle Zone 2 -> Turtle Zone 3": lambda state: has_level_progression(state, "Turtle Zone Progression", self.player, 2),
|
||||
}
|
||||
|
||||
if self.options.shuffle_golden_coins == "mario_coin_fragment_hunt":
|
||||
# Require the other coins just to ensure they are being added to start inventory properly,
|
||||
# and so they show up in Playthrough as required
|
||||
entrance_rules["Menu -> Mario's Castle"] = lambda state: (state.has_all(
|
||||
["Tree Coin", "Space Coin", "Macro Coin", "Pumpkin Coin", "Turtle Coin"], self.player)
|
||||
and state.has("Mario Coin Fragment", self.player, self.coin_fragments_required))
|
||||
else:
|
||||
entrance_rules["Menu -> Mario's Castle"] = lambda state: state.has_from_list_unique([
|
||||
"Tree Coin", "Space Coin", "Macro Coin", "Pumpkin Coin", "Mario Coin", "Turtle Coin"
|
||||
], self.player, self.options.required_golden_coins)
|
||||
|
||||
|
||||
for entrance, rule in entrance_rules.items():
|
||||
self.multiworld.get_entrance(entrance, self.player).access_rule = rule
|
||||
|
||||
for location in self.multiworld.get_locations(self.player):
|
||||
if location.name.endswith(("Coins", "Coin")):
|
||||
rule = getattr(logic, location.parent_region.name.lower().replace(" ", "_") + "_coins", None)
|
||||
if rule:
|
||||
coins = int(location.name.split(" ")[-2])
|
||||
location.access_rule = lambda state, coin_rule=rule, num_coins=coins: \
|
||||
coin_rule(state, self.player, num_coins)
|
||||
else:
|
||||
rule = getattr(logic, location.name.lower().replace(
|
||||
" - ", "_").replace(" ", "_").replace("'", ""), None)
|
||||
if rule:
|
||||
location.access_rule = lambda state, loc_rule=rule: loc_rule(state, self.player)
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Wario Defeated", self.player)
|
||||
|
||||
def create_items(self):
|
||||
item_counts = {
|
||||
"Space Zone Progression": 1,
|
||||
"Space Zone Secret": 1,
|
||||
"Tree Zone Progression": 3,
|
||||
"Tree Zone Secret": 1,
|
||||
"Macro Zone Progression": 3,
|
||||
"Macro Zone Secret 1": 1,
|
||||
"Macro Zone Secret 2": 1,
|
||||
"Pumpkin Zone Progression": 3,
|
||||
"Pumpkin Zone Secret 1": 1,
|
||||
"Pumpkin Zone Secret 2": 1,
|
||||
"Mario Zone Progression": 3,
|
||||
"Turtle Zone Progression": 2,
|
||||
"Turtle Zone Secret": 1,
|
||||
"Mushroom": 1,
|
||||
"Fire Flower": 1,
|
||||
"Carrot": 1,
|
||||
"Space Physics": 1,
|
||||
"Hippo Bubble": 1,
|
||||
"Water Physics": 1,
|
||||
"Super Star Duration Increase": 2,
|
||||
"Mario Coin Fragment": 0,
|
||||
}
|
||||
|
||||
if self.options.shuffle_golden_coins == "mario_coin_fragment_hunt":
|
||||
# There are 5 Zone Progression items that can be condensed.
|
||||
item_counts["Mario Coin Fragment"] = 1 + ((5 * self.options.mario_coin_fragment_percentage) // 100)
|
||||
|
||||
if self.options.coinsanity:
|
||||
coin_count = sum([level[1] for level in self.num_coin_locations])
|
||||
max_coins = sum(self.max_coin_locations.values())
|
||||
if self.options.shuffle_golden_coins == "mario_coin_fragment_hunt":
|
||||
removed_coins = (coin_count * self.options.mario_coin_fragment_percentage) // 100
|
||||
coin_count -= removed_coins
|
||||
item_counts["Mario Coin Fragment"] += removed_coins
|
||||
# Randomly remove some coin items for variety
|
||||
coin_count -= (coin_count // self.random.randint(100, max(100, coin_count)))
|
||||
|
||||
if coin_count:
|
||||
coin_bundle_sizes = [max_coins // coin_count] * coin_count
|
||||
remainder = max_coins - sum(coin_bundle_sizes)
|
||||
for i in range(remainder):
|
||||
coin_bundle_sizes[i] += 1
|
||||
for a, b in zip(range(1, len(coin_bundle_sizes), 2), range(2, len(coin_bundle_sizes), 2)):
|
||||
split = self.random.randint(1, coin_bundle_sizes[a] + coin_bundle_sizes[b] - 1)
|
||||
coin_bundle_sizes[a], coin_bundle_sizes[b] = split, coin_bundle_sizes[a] + coin_bundle_sizes[b] - split
|
||||
for coin_bundle_size in coin_bundle_sizes:
|
||||
item_name = f"{coin_bundle_size} Coin{'s' if coin_bundle_size > 1 else ''}"
|
||||
if item_name in item_counts:
|
||||
item_counts[item_name] += 1
|
||||
else:
|
||||
item_counts[item_name] = 1
|
||||
|
||||
if self.options.shuffle_golden_coins == "shuffle":
|
||||
for item in self.item_name_groups["Golden Coins"]:
|
||||
item_counts[item] = 1
|
||||
elif self.options.shuffle_golden_coins == "mario_coin_fragment_hunt":
|
||||
for item in ("Tree Coin", "Space Coin", "Macro Coin", "Pumpkin Coin", "Turtle Coin"):
|
||||
self.multiworld.push_precollected(self.create_item(item))
|
||||
else:
|
||||
for item, location_name in (
|
||||
("Mario Coin", "Mario Zone 4 - Boss"),
|
||||
("Tree Coin", "Tree Zone 5 - Boss"),
|
||||
("Space Coin", "Space Zone 2 - Boss"),
|
||||
("Macro Coin", "Macro Zone 4 - Boss"),
|
||||
("Pumpkin Coin", "Pumpkin Zone 4 - Boss"),
|
||||
("Turtle Coin", "Turtle Zone 3 - Boss")
|
||||
):
|
||||
location = self.multiworld.get_location(location_name, self.player)
|
||||
location.place_locked_item(self.create_item(item))
|
||||
location.address = None
|
||||
location.item.code = None
|
||||
|
||||
if self.options.shuffle_midway_bells:
|
||||
for item in [item for item in items if "Midway Bell" in item]:
|
||||
if item != "Mario's Castle Midway Bell" or self.options.marios_castle_midway_bell:
|
||||
item_counts[item] = 1
|
||||
|
||||
if self.options.difficulty_mode == "easy_to_normal":
|
||||
item_counts["Normal Mode"] = 1
|
||||
elif self.options.difficulty_mode == "normal_to_easy":
|
||||
item_counts["Easy Mode"] = 1
|
||||
|
||||
if self.options.shuffle_pipe_traversal == "single":
|
||||
item_counts["Pipe Traversal"] = 1
|
||||
elif self.options.shuffle_pipe_traversal == "split":
|
||||
item_counts["Pipe Traversal - Right"] = 1
|
||||
item_counts["Pipe Traversal - Left"] = 1
|
||||
item_counts["Pipe Traversal - Up"] = 1
|
||||
item_counts["Pipe Traversal - Down"] = 1
|
||||
else:
|
||||
self.multiworld.push_precollected(self.create_item("Pipe Traversal"))
|
||||
|
||||
if any(self.auto_scroll_levels):
|
||||
if self.options.auto_scroll_mode == "global_trap_item":
|
||||
item_counts["Auto Scroll"] = 1
|
||||
elif self.options.auto_scroll_mode == "global_cancel_item":
|
||||
item_counts["Cancel Auto Scroll"] = 1
|
||||
else:
|
||||
for level, i in enumerate(self.auto_scroll_levels):
|
||||
if i == 3:
|
||||
item_counts[f"Auto Scroll - {level_id_to_name[level]}"] = 1
|
||||
elif i == 2:
|
||||
item_counts[f"Cancel Auto Scroll - {level_id_to_name[level]}"] = 1
|
||||
|
||||
for item in self.multiworld.precollected_items[self.player]:
|
||||
if item.name in item_counts and item_counts[item.name] > 0:
|
||||
item_counts[item.name] -= 1
|
||||
|
||||
location_count = len(self.multiworld.get_unfilled_locations(self.player))
|
||||
items_to_add = location_count - sum(item_counts.values())
|
||||
if items_to_add > 0:
|
||||
mario_coin_frags = 0
|
||||
if self.options.shuffle_golden_coins == "mario_coin_fragment_hunt":
|
||||
mario_coin_frags = (items_to_add * self.options.mario_coin_fragment_percentage) // 100
|
||||
item_counts["Mario Coin Fragment"] += mario_coin_frags
|
||||
item_counts["Super Star Duration Increase"] += items_to_add - mario_coin_frags
|
||||
elif items_to_add < 0:
|
||||
if self.options.coinsanity:
|
||||
for i in range(1, 168):
|
||||
coin_name = f"{i} Coin{'s' if i > 1 else ''}"
|
||||
if coin_name in item_counts:
|
||||
amount_to_remove = min(-items_to_add, item_counts[coin_name])
|
||||
item_counts[coin_name] -= amount_to_remove
|
||||
items_to_add += amount_to_remove
|
||||
if items_to_add >= 0:
|
||||
break
|
||||
|
||||
double_progression_items = ["Tree Zone Progression", "Macro Zone Progression", "Pumpkin Zone Progression",
|
||||
"Mario Zone Progression", "Turtle Zone Progression"]
|
||||
self.random.shuffle(double_progression_items)
|
||||
while sum(item_counts.values()) > location_count:
|
||||
if double_progression_items:
|
||||
double_progression_item = double_progression_items.pop()
|
||||
item_counts[double_progression_item] -= 2
|
||||
item_counts[double_progression_item + " x2"] = 1
|
||||
continue
|
||||
if self.options.auto_scroll_mode in ("level_trap_items", "level_cancel_items",
|
||||
"chaos"):
|
||||
auto_scroll_item = self.random.choice([item for item in item_counts if "Auto Scroll" in item])
|
||||
level = auto_scroll_item.split("- ")[1]
|
||||
self.auto_scroll_levels[level_name_to_id[level]] = 0
|
||||
del item_counts[auto_scroll_item]
|
||||
continue
|
||||
raise Exception(f"Too many items in the item pool for Super Mario Land 2 player {self.player_name}")
|
||||
# item = self.random.choice(list(item_counts))
|
||||
# item_counts[item] -= 1
|
||||
# if item_counts[item] == 0:
|
||||
# del item_counts[item]
|
||||
# self.multiworld.push_precollected(self.create_item(item))
|
||||
|
||||
self.coin_fragments_required = max((item_counts["Mario Coin Fragment"]
|
||||
* self.options.mario_coin_fragments_required_percentage) // 100, 1)
|
||||
|
||||
for item_name, count in item_counts.items():
|
||||
self.multiworld.itempool += [self.create_item(item_name) for _ in range(count)]
|
||||
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"energy_link": self.options.energy_link.value
|
||||
}
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
return MarioLand2Item(name, items[name], self.item_name_to_id[name], self.player)
|
||||
|
||||
def get_filler_item_name(self):
|
||||
return "1 Coin"
|
||||
|
||||
def modify_multidata(self, multidata: dict):
|
||||
rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0',
|
||||
'utf8')[:21]
|
||||
rom_name.extend([0] * (21 - len(rom_name)))
|
||||
new_name = base64.b64encode(bytes(rom_name)).decode()
|
||||
multidata["connect_names"][new_name] = multidata["connect_names"][self.player_name]
|
||||
|
||||
|
||||
class MarioLand2Location(Location):
|
||||
game = "Super Mario Land 2"
|
||||
|
||||
|
||||
class MarioLand2Item(Item):
|
||||
game = "Super Mario Land 2"
|
||||
BIN
worlds/marioland2/basepatch.bsdiff4
Normal file
BIN
worlds/marioland2/basepatch.bsdiff4
Normal file
Binary file not shown.
250
worlds/marioland2/client.py
Normal file
250
worlds/marioland2/client.py
Normal file
@@ -0,0 +1,250 @@
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from NetUtils import ClientStatus
|
||||
from worlds._bizhawk.client import BizHawkClient
|
||||
from worlds._bizhawk import read, write, guarded_write
|
||||
|
||||
from .rom_addresses import rom_addresses
|
||||
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
BANK_EXCHANGE_RATE = 20000000000
|
||||
|
||||
overworld_music = (0x05, 0x06, 0x0D, 0x0E, 0x10, 0x12, 0x1B, 0x1C, 0x1E)
|
||||
|
||||
class MarioLand2Client(BizHawkClient):
|
||||
system = ("GB", "SGB")
|
||||
patch_suffix = ".apsml2"
|
||||
game = "Super Mario Land 2"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.locations_array = []
|
||||
self.previous_level = None
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
game_name = await read(ctx.bizhawk_ctx, [(0x134, 10, "ROM")])
|
||||
game_name = game_name[0].decode("ascii")
|
||||
if game_name == "MARIOLAND2":
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b111
|
||||
return True
|
||||
return False
|
||||
|
||||
async def set_auth(self, ctx):
|
||||
auth_name = await read(ctx.bizhawk_ctx, [(0x77777, 21, "ROM")])
|
||||
auth_name = base64.b64encode(auth_name[0]).decode()
|
||||
ctx.auth = auth_name
|
||||
|
||||
async def game_watcher(self, ctx):
|
||||
from . import START_IDS
|
||||
from .items import items
|
||||
from .locations import locations, level_id_to_name, coins_coords, location_name_to_id
|
||||
|
||||
(game_loaded_check, level_data, music, auto_scroll_levels, current_level,
|
||||
midway_point, bcd_lives, num_items_received, coins, options) = \
|
||||
await read(ctx.bizhawk_ctx, [(0x0046, 10, "CartRAM"), (0x0848, 42, "CartRAM"), (0x0469, 1, "CartRAM"),
|
||||
(rom_addresses["Auto_Scroll_Levels_B"], 32, "ROM"),
|
||||
(0x0269, 1, "CartRAM"), (0x02A0, 1, "CartRAM"), (0x022C, 1, "CartRAM"),
|
||||
(0x00F0, 2, "CartRAM"), (0x0262, 2, "CartRAM"),
|
||||
(rom_addresses["Coins_Required"], 8, "ROM")])
|
||||
|
||||
coins_required = int.from_bytes(options[:2], "big")
|
||||
difficulty_mode = options[2]
|
||||
star_count = int.from_bytes(options[3:5], "big")
|
||||
midway_bells = options[5]
|
||||
energy_link = options[6]
|
||||
coin_mode = options[7]
|
||||
|
||||
current_level = int.from_bytes(current_level, "big")
|
||||
auto_scroll_levels = list(auto_scroll_levels)
|
||||
midway_point = int.from_bytes(midway_point, "big")
|
||||
music = int.from_bytes(music, "big")
|
||||
level_data = list(level_data)
|
||||
lives = bcd_lives.hex()
|
||||
num_items_received = int.from_bytes(num_items_received, "big")
|
||||
if num_items_received == 0xFFFF:
|
||||
num_items_received = 0
|
||||
|
||||
items_received = [list(items.keys())[item.item - START_IDS] for item in ctx.items_received]
|
||||
write_num_items_received = len(items_received).to_bytes(2, "big")
|
||||
|
||||
level_progression = {
|
||||
"Space Zone Progression",
|
||||
"Tree Zone Progression",
|
||||
"Macro Zone Progression",
|
||||
"Pumpkin Zone Progression",
|
||||
"Mario Zone Progression",
|
||||
"Turtle Zone Progression",
|
||||
}
|
||||
for level_item in level_progression:
|
||||
for _ in range(items_received.count(level_item + " x2")):
|
||||
items_received += ([level_item] * 2)
|
||||
|
||||
if "Pipe Traversal" in items_received:
|
||||
items_received += ["Pipe Traversal - Left", "Pipe Traversal - Right",
|
||||
"Pipe Traversal - Up", "Pipe Traversal - Down"]
|
||||
|
||||
if coin_mode == 2 and items_received.count("Mario Coin Fragment") >= coins_required:
|
||||
items_received.append("Mario Coin")
|
||||
|
||||
if current_level == 255 and self.previous_level != 255:
|
||||
if coin_mode < 2:
|
||||
logger.info(f"Golden Coins required: {coins_required}")
|
||||
else:
|
||||
logger.info(f"Mario Coin Fragments required: {coins_required}. "
|
||||
f"You have {items_received.count('Mario Coin Fragment')}")
|
||||
self.previous_level = current_level
|
||||
|
||||
# There is no music in the title screen demos, this is how we guard against anything in the demos registering.
|
||||
# There is also no music at the door to Mario's Castle, which is why the above is before this check.
|
||||
if game_loaded_check != b'\x124Vx\xff\xff\xff\xff\xff\xff' or music == 0:
|
||||
return
|
||||
|
||||
locations_checked = []
|
||||
if current_level in level_id_to_name:
|
||||
level_name = level_id_to_name[current_level]
|
||||
coin_tile_data = await read(ctx.bizhawk_ctx, [(0xB000 + ((coords[1] * 256) + coords[0]), 1, "System Bus")
|
||||
for coords in coins_coords[level_name]])
|
||||
num_coins = len([tile[0] for tile in coin_tile_data if tile[0] in (0x7f, 0x60, 0x07)])
|
||||
locations_checked = [location_name_to_id[f"{level_name} - {i} Coin{'s' if i > 1 else ''}"]
|
||||
for i in range(1, num_coins + 1)]
|
||||
|
||||
new_lives = int(lives)
|
||||
energy_link_add = None
|
||||
if energy_link:
|
||||
if new_lives == 0:
|
||||
if (f"EnergyLink{ctx.team}" in ctx.stored_data
|
||||
and ctx.stored_data[f"EnergyLink{ctx.team}"]
|
||||
and ctx.stored_data[f"EnergyLink{ctx.team}"] >= BANK_EXCHANGE_RATE):
|
||||
new_lives = 1
|
||||
energy_link_add = -BANK_EXCHANGE_RATE
|
||||
elif new_lives > 1:
|
||||
energy_link_add = BANK_EXCHANGE_RATE * (new_lives - 1)
|
||||
new_lives = 1
|
||||
# Convert back to binary-coded-decimal
|
||||
new_lives = int(str(new_lives), 16)
|
||||
|
||||
new_coins = coins.hex()
|
||||
new_coins = int(new_coins[2:] + new_coins[:2])
|
||||
for item in items_received[num_items_received:]:
|
||||
if item.endswith("Coins") or item == "1 Coin":
|
||||
new_coins += int(item.split(" ")[0])
|
||||
# Limit to 999 and convert back to binary-coded-decimal
|
||||
new_coins = int(str(min(new_coins, 999)), 16).to_bytes(2, "little")
|
||||
|
||||
modified_level_data = level_data.copy()
|
||||
for ID, (location, data) in enumerate(locations.items(), START_IDS):
|
||||
if "clear_condition" in data:
|
||||
if items_received.count(data["clear_condition"][0]) >= data["clear_condition"][1]:
|
||||
modified_level_data[data["ram_index"]] |= (0x08 if data["type"] == "bell"
|
||||
else 0x01 if data["type"] == "secret" else 0x80)
|
||||
|
||||
if data["type"] == "level" and level_data[data["ram_index"]] & 0x40:
|
||||
locations_checked.append(ID)
|
||||
if data["type"] == "secret" and level_data[data["ram_index"]] & 0x02:
|
||||
locations_checked.append(ID)
|
||||
elif data["type"] == "bell" and data["id"] == current_level and midway_point == 0xFF:
|
||||
locations_checked.append(ID)
|
||||
|
||||
invincibility_length = int((832.0 / (star_count + 1))
|
||||
* (items_received.count("Super Star Duration Increase") + 1))
|
||||
|
||||
if "Easy Mode" in items_received:
|
||||
difficulty_mode = 1
|
||||
elif "Normal Mode" in items_received:
|
||||
difficulty_mode = 0
|
||||
|
||||
data_writes = [
|
||||
(rom_addresses["Space_Physics"], [0x7e] if "Space Physics" in items_received else [0xaf], "ROM"),
|
||||
(rom_addresses["Get_Hurt_To_Big_Mario"], [1] if "Mushroom" in items_received else [0], "ROM"),
|
||||
(rom_addresses["Get_Mushroom_A"], [0xea, 0x16, 0xa2] if "Mushroom" in items_received else [0, 0, 0], "ROM"),
|
||||
(rom_addresses["Get_Mushroom_B"], [0xea, 0x16, 0xa2] if "Mushroom" in items_received else [0, 0, 0], "ROM"),
|
||||
(rom_addresses["Get_Mushroom_C"], [00] if "Mushroom" in items_received else [0xd8], "ROM"),
|
||||
(rom_addresses["Get_Carrot_A"], [0xea, 0x16, 0xa2] if "Carrot" in items_received else [0, 0, 0], "ROM"),
|
||||
(rom_addresses["Get_Carrot_B"], [0xea, 0x16, 0xa2] if "Carrot" in items_received else [0, 0, 0], "ROM"),
|
||||
(rom_addresses["Get_Carrot_C"], [00] if "Carrot" in items_received else [0xc8], "ROM"),
|
||||
(rom_addresses["Get_Fire_Flower_A"], [0xea, 0x16, 0xa2] if "Fire Flower" in items_received else [0, 0, 0], "ROM"),
|
||||
(rom_addresses["Get_Fire_Flower_B"], [0xea, 0x16, 0xa2] if "Fire Flower" in items_received else [0, 0, 0], "ROM"),
|
||||
(rom_addresses["Get_Fire_Flower_C"], [00] if "Fire Flower" in items_received else [0xc8], "ROM"),
|
||||
(rom_addresses["Invincibility_Star_A"], [(invincibility_length >> 8) + 1], "ROM"),
|
||||
(rom_addresses["Invincibility_Star_B"], [invincibility_length & 0xFF], "ROM"),
|
||||
(rom_addresses["Enable_Bubble"], [0xcb, 0xd7] if "Hippo Bubble" in items_received else [0, 0], "ROM"),
|
||||
(rom_addresses["Enable_Swim"], [0xcb, 0xcf] if "Water Physics" in items_received else [0, 0], "ROM"),
|
||||
(rom_addresses["Pipe_Traversal_A"], [16] if "Pipe Traversal - Down" in items_received else [0], "ROM"),
|
||||
(rom_addresses["Pipe_Traversal_B"], [32] if "Pipe Traversal - Up" in items_received else [10], "ROM"),
|
||||
(rom_addresses["Pipe_Traversal_C"], [48] if "Pipe Traversal - Right" in items_received else [0], "ROM"),
|
||||
(rom_addresses["Pipe_Traversal_D"], [64] if "Pipe Traversal - Left" in items_received else [0], "ROM"),
|
||||
(rom_addresses["Pipe_Traversal_SFX_A"], [5] if "Pipe Traversal - Down" in items_received else [0], "ROM"),
|
||||
(rom_addresses["Pipe_Traversal_SFX_B"], [5] if "Pipe Traversal - Up" in items_received else [0], "ROM"),
|
||||
(rom_addresses["Pipe_Traversal_SFX_C"], [5] if "Pipe Traversal - Right" in items_received else [0], "ROM"),
|
||||
(rom_addresses["Pipe_Traversal_SFX_D"], [5] if "Pipe Traversal - Left" in items_received else [0], "ROM"),
|
||||
(0x022c, [new_lives], "CartRAM"),
|
||||
(0x02E4, [difficulty_mode], "CartRAM"),
|
||||
(0x0848, modified_level_data, "CartRAM"),
|
||||
(0x0262, new_coins, "CartRAM"),
|
||||
]
|
||||
|
||||
if items_received:
|
||||
data_writes.append((0x00F0, write_num_items_received, "CartRAM"))
|
||||
|
||||
if midway_point == 0xFF and (midway_bells or music in overworld_music):
|
||||
# after registering the check for the midway bell, clear the value just for safety.
|
||||
data_writes.append((0x02A0, [0], "CartRAM"))
|
||||
|
||||
for i in range(32):
|
||||
if auto_scroll_levels[i] == 3:
|
||||
if "Auto Scroll" in items_received or f"Auto Scroll - {level_id_to_name[i]}" in items_received:
|
||||
auto_scroll_levels[i] = 1
|
||||
if i == current_level:
|
||||
data_writes.append((0x02C8, [0x01], "CartRAM"))
|
||||
else:
|
||||
auto_scroll_levels[i] = 0
|
||||
elif auto_scroll_levels[i] == 2:
|
||||
if ("Cancel Auto Scroll" in items_received
|
||||
or f"Cancel Auto Scroll - {level_id_to_name[i]}" in items_received):
|
||||
auto_scroll_levels[i] = 0
|
||||
if i == current_level:
|
||||
data_writes.append((0x02C8, [0x00], "CartRAM"))
|
||||
else:
|
||||
auto_scroll_levels[i] = 1
|
||||
data_writes.append((rom_addresses["Auto_Scroll_Levels"], auto_scroll_levels, "ROM"))
|
||||
|
||||
success = await guarded_write(ctx.bizhawk_ctx, data_writes, [(0x0848, level_data, "CartRAM"),
|
||||
(0x022C, [int.from_bytes(bcd_lives, "big")],
|
||||
"CartRAM"),
|
||||
[0x0262, coins, "CartRAM"]])
|
||||
|
||||
if success and energy_link_add is not None:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations":
|
||||
[{"operation": "add", "value": energy_link_add},
|
||||
{"operation": "max", "value": 0}],
|
||||
}])
|
||||
|
||||
if not ctx.server or not ctx.server.socket.open or ctx.server.socket.closed:
|
||||
return
|
||||
|
||||
if locations_checked and locations_checked != self.locations_array:
|
||||
self.locations_array = locations_checked
|
||||
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations_checked}])
|
||||
|
||||
if music == 0x18:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
|
||||
def on_package(self, ctx, cmd: str, args: dict):
|
||||
super().on_package(ctx, cmd, args)
|
||||
if cmd == 'Connected':
|
||||
if ctx.slot_data["energy_link"]:
|
||||
ctx.set_notify(f"EnergyLink{ctx.team}")
|
||||
if ctx.ui:
|
||||
ctx.ui.enable_energy_link()
|
||||
ctx.ui.energy_link_label.text = "Lives: Standby"
|
||||
elif cmd == "SetReply" and args["key"].startswith("EnergyLink"):
|
||||
if ctx.ui:
|
||||
ctx.ui.energy_link_label.text = f"Lives: {int(args['value'] / BANK_EXCHANGE_RATE)}"
|
||||
elif cmd == "Retrieved":
|
||||
if f"EnergyLink{ctx.team}" in args["keys"] and args['keys'][f'EnergyLink{ctx.team}'] and ctx.ui:
|
||||
ctx.ui.energy_link_label.text = f"Lives: {int(args['keys'][f'EnergyLink{ctx.team}'] / BANK_EXCHANGE_RATE)}"
|
||||
64
worlds/marioland2/docs/en_Super Mario Land 2.md
Normal file
64
worlds/marioland2/docs/en_Super Mario Land 2.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Super Mario Land 2: 6 Golden Coins
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
|
||||
Completing a level's exits results in a location check instead of automatically bringing you to the next level.
|
||||
Where there are secret exits, the secret exit will be a separate location check. There is one exception, Hippo Zone,
|
||||
that does not have a separate check for its secret exit. The Hippo Zone secret exit will still bring you to the Space
|
||||
Zone.
|
||||
|
||||
Ringing the Midway Bells in each level that has one will register a location check. If the "Shuffle Midway Bells" option
|
||||
is turned on, then ringing the bell will not grant the checkpoint, and instead you must obtain the Midway Bell item from
|
||||
the item pool to gain the checkpoint for that level. Holding SELECT while loading into a level where you have unlocked
|
||||
the Midway Bell checkpoint will start you at the beginning of the level.
|
||||
|
||||
Unlocking paths to new levels requires finding or receiving Zone Progression items. For example, receiving the first
|
||||
"Turtle Zone Progression" will unlock the path from Turtle Zone 1 to Turtle Zone 2. Paths to secret levels are separate
|
||||
items, so Turtle Zone Secret will open the path from Turtle Zone 2 to the Turtle Zone Secret Course.
|
||||
|
||||
Depending on settings, there may be some "Zone Progression x2" items that open two paths at once.
|
||||
|
||||
The path from Tree Zone 2 to the branch to Tree Zone 3 and 4 is one unlock, so both levels will open at this point.
|
||||
|
||||
Besides the zone progression unlocks, the following items are always shuffled:
|
||||
- Mushroom: required to become Big Mario. If you are Fire or Bunny Mario and take damage, and have not obtained the
|
||||
Mushroom, you will drop straight down to Small Mario.
|
||||
- Fire Flower: required to become Fire Mario.
|
||||
- Carrot: required to become Bunny Mario.
|
||||
- Hippo Bubble: required to use the bubbles in Hippo Zone to fly.
|
||||
- Water Physics: Mario will fall through water as though it is air until this is obtained.
|
||||
- Space Physics: the Space Zone levels will have normal gravity until this is obtained.
|
||||
- Super Star Duration Increase: you begin with a drastically lowered invincibility star duration, and these items will
|
||||
increase it.
|
||||
|
||||
Additionally, the following items can be shuffled depending on your YAML options:
|
||||
- The 6 Golden Coins: note that the game will still show you the coin being sent to the castle when defeating a boss
|
||||
regardless of whether the coin is actually obtained in that location.
|
||||
- Mario Coin Fragments: As an alternative to shuffling the 6 Golden Coins, you can shuffle Mario Coin Fragments,
|
||||
a chosen percentage of which are needed to assemble the Mario Coin. You will start with the other 5 coins.
|
||||
- Normal Mode/Easy Mode: you can start the game in Normal Mode with an Easy Mode "upgrade" in the item pool, or start in
|
||||
Easy Mode with a Normal Mode "trap" item, swapping the difficulty.
|
||||
- Auto Scroll: auto-scrolling levels can be set to not auto scroll until this trap item is received.
|
||||
- Pipe Traversal: required to enter pipes. Can also be split into 4 items, each enabling pipe entry from a different
|
||||
direction.
|
||||
- Coins: if Coinsanity is enabled, coins will be shuffled into the item pool. A number of checks will be added to each
|
||||
level for obtaining a specific number of coins within a single playthrough of the level.
|
||||
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
There is no in-game indication that an item has been received. You will need to watch the client or web tracker to be
|
||||
sure you're aware of the items you've received.
|
||||
|
||||
## Special Thanks to:
|
||||
|
||||
- [froggestspirit](https://github.com/froggestspirit) for his Super Mario Land 2 disassembly. While very incomplete, it
|
||||
had enough memory values mapped out to make my work significantly easier.
|
||||
- [slashinfty](https://github.com/slashinfty), the author of the
|
||||
[Super Mario Land 2 Randomizer](https://sml2r.download/) for permitting me to port features such as Randomize Enemies
|
||||
and Randomize Platforms directly from it.
|
||||
75
worlds/marioland2/docs/setup_en.md
Normal file
75
worlds/marioland2/docs/setup_en.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Setup Guide for Super Mario Land 2: 6 Golden Coins
|
||||
|
||||
## Important
|
||||
|
||||
As we are using BizHawk, this guide is only applicable to Windows and Linux systems.
|
||||
|
||||
## Required Software
|
||||
|
||||
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Version 2.9.1 is recommended.
|
||||
- Detailed installation instructions for BizHawk can be found at the above link.
|
||||
- Windows users must run the prereq installer first, which can also be found at the above link.
|
||||
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- A Super Mario Land 2: 6 Golden Coins version 1.0 ROM file. The Archipelago community cannot provide this.
|
||||
|
||||
## Configuring BizHawk
|
||||
|
||||
Once BizHawk has been installed, open EmuHawk and change the following settings:
|
||||
|
||||
- Under Config > Customize > Advanced, make sure the box for AutoSaveRAM is checked, and click the 5s button.
|
||||
This reduces the possibility of losing save data in emulator crashes.
|
||||
- Under Config > Customize, check the "Run in background" box. This will prevent disconnecting from the client while
|
||||
EmuHawk is running in the background.
|
||||
|
||||
It is strongly recommended to associate Game Boy ROM extensions (\*.gb) to the EmuHawk we've just installed.
|
||||
To do so, we simply have to search any Game Boy ROM we happened to own, right click and select "Open with...", unfold
|
||||
the list that appears and select the bottom option "Look for another application", then browse to the BizHawk folder
|
||||
and select EmuHawk.exe.
|
||||
|
||||
## Configuring your YAML file
|
||||
|
||||
### What is a YAML file and why do I need one?
|
||||
|
||||
Your YAML file contains a set of configuration options which provide the generator with information about how it should
|
||||
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
|
||||
an experience customized for their taste, and different players in the same multiworld can all have different options.
|
||||
|
||||
### Where do I get a YAML file?
|
||||
|
||||
You can generate a yaml or download a template by visiting the [Super Mario Land 2 Player Options Page](/games/Super%20Mario%20Land%202/player-options)
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Generating and Patching a Game
|
||||
|
||||
1. Create your options file (YAML).
|
||||
2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
|
||||
This will generate an output file for you. Your patch file will have a `.apsml2` file extension.
|
||||
3. Open `ArchipelagoLauncher.exe`
|
||||
4. Select "Open Patch" on the left side and select your patch file.
|
||||
5. If this is your first time patching, you will be prompted to locate your vanilla ROM.
|
||||
6. A patched `.gb` file will be created in the same place as the patch file.
|
||||
7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
|
||||
BizHawk install.
|
||||
|
||||
You must connect Super Mario Land 2 to a server, even for a single player game, or progress cannot be made.
|
||||
|
||||
### Connect to the Multiserver
|
||||
|
||||
By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
|
||||
in case you have to close and reopen a window mid-game for some reason.
|
||||
|
||||
1. Super Mario Land 2 uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your
|
||||
game, you can re-open it from the launcher.
|
||||
2. Ensure EmuHawk is running the patched ROM.
|
||||
3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
|
||||
4. In the Lua Console window, go to `Script > Open Script…`.
|
||||
5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
|
||||
6. The emulator may freeze every few seconds until it manages to connect to the client. This is expected. The BizHawk
|
||||
Client window should indicate that it connected and recognized Super Mario Land 2.
|
||||
7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
|
||||
top text field of the client and click Connect.
|
||||
|
||||
To connect the client to the multiserver simply put `<address>:<port>` on the textfield on top and press enter (if the
|
||||
server uses password, type in the bottom textfield `/connect <address>:<port> [password]`)
|
||||
79
worlds/marioland2/items.py
Normal file
79
worlds/marioland2/items.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from BaseClasses import ItemClassification
|
||||
from .locations import level_name_to_id
|
||||
from .options import CoinsanityChecks
|
||||
|
||||
items = {
|
||||
"Space Zone Progression": ItemClassification.progression,
|
||||
"Space Zone Secret": ItemClassification.progression,
|
||||
"Tree Zone Progression": ItemClassification.progression,
|
||||
"Tree Zone Progression x2": ItemClassification.progression,
|
||||
"Tree Zone Secret": ItemClassification.progression,
|
||||
"Macro Zone Progression": ItemClassification.progression,
|
||||
"Macro Zone Progression x2": ItemClassification.progression,
|
||||
"Macro Zone Secret 1": ItemClassification.progression,
|
||||
"Macro Zone Secret 2": ItemClassification.progression_skip_balancing,
|
||||
"Pumpkin Zone Progression": ItemClassification.progression,
|
||||
"Pumpkin Zone Progression x2": ItemClassification.progression,
|
||||
"Pumpkin Zone Secret 1": ItemClassification.progression,
|
||||
"Pumpkin Zone Secret 2": ItemClassification.progression,
|
||||
"Mario Zone Progression": ItemClassification.progression,
|
||||
"Mario Zone Progression x2": ItemClassification.progression,
|
||||
"Turtle Zone Progression": ItemClassification.progression,
|
||||
"Turtle Zone Progression x2": ItemClassification.progression,
|
||||
"Turtle Zone Secret": ItemClassification.progression,
|
||||
"Tree Coin": ItemClassification.progression_skip_balancing,
|
||||
"Space Coin": ItemClassification.progression_skip_balancing,
|
||||
"Macro Coin": ItemClassification.progression_skip_balancing,
|
||||
"Pumpkin Coin": ItemClassification.progression_skip_balancing,
|
||||
"Mario Coin": ItemClassification.progression_skip_balancing,
|
||||
"Turtle Coin": ItemClassification.progression_skip_balancing,
|
||||
"Mario Coin Fragment": ItemClassification.progression_skip_balancing,
|
||||
"Mushroom": ItemClassification.progression,
|
||||
"Fire Flower": ItemClassification.progression,
|
||||
"Carrot": ItemClassification.progression,
|
||||
"Space Physics": ItemClassification.progression_skip_balancing,
|
||||
"Hippo Bubble": ItemClassification.progression_skip_balancing,
|
||||
"Water Physics": ItemClassification.progression,
|
||||
"Pipe Traversal": ItemClassification.progression,
|
||||
"Pipe Traversal - Down": ItemClassification.progression,
|
||||
"Pipe Traversal - Up": ItemClassification.progression,
|
||||
"Pipe Traversal - Right": ItemClassification.progression,
|
||||
"Pipe Traversal - Left": ItemClassification.progression_skip_balancing,
|
||||
"Super Star Duration Increase": ItemClassification.filler,
|
||||
"Easy Mode": ItemClassification.useful,
|
||||
"Normal Mode": ItemClassification.trap,
|
||||
"Auto Scroll": ItemClassification.trap,
|
||||
**{f"Auto Scroll - {level}": ItemClassification.trap for level in level_name_to_id if level != "Wario's Castle"},
|
||||
"Cancel Auto Scroll": ItemClassification.progression,
|
||||
**{f"Cancel Auto Scroll - {level}": ItemClassification.progression for level in level_name_to_id
|
||||
if level != "Wario's Castle"},
|
||||
"Mushroom Zone Midway Bell": ItemClassification.filler,
|
||||
"Tree Zone 1 Midway Bell": ItemClassification.filler,
|
||||
"Tree Zone 2 Midway Bell": ItemClassification.progression_skip_balancing,
|
||||
"Tree Zone 4 Midway Bell": ItemClassification.progression_skip_balancing,
|
||||
"Tree Zone 5 Midway Bell": ItemClassification.filler,
|
||||
"Space Zone 1 Midway Bell": ItemClassification.filler,
|
||||
"Space Zone 2 Midway Bell": ItemClassification.progression_skip_balancing,
|
||||
"Macro Zone 1 Midway Bell": ItemClassification.progression_skip_balancing,
|
||||
"Macro Zone 2 Midway Bell": ItemClassification.progression_skip_balancing,
|
||||
"Macro Zone 3 Midway Bell": ItemClassification.progression_skip_balancing,
|
||||
"Macro Zone 4 Midway Bell": ItemClassification.filler,
|
||||
"Pumpkin Zone 1 Midway Bell": ItemClassification.progression_skip_balancing,
|
||||
"Pumpkin Zone 2 Midway Bell": ItemClassification.filler,
|
||||
"Pumpkin Zone 3 Midway Bell": ItemClassification.filler,
|
||||
"Pumpkin Zone 4 Midway Bell": ItemClassification.filler,
|
||||
"Mario Zone 1 Midway Bell": ItemClassification.progression_skip_balancing,
|
||||
"Mario Zone 2 Midway Bell": ItemClassification.filler,
|
||||
"Mario Zone 3 Midway Bell": ItemClassification.filler,
|
||||
"Mario Zone 4 Midway Bell": ItemClassification.filler,
|
||||
"Turtle Zone 1 Midway Bell": ItemClassification.filler,
|
||||
"Turtle Zone 2 Midway Bell": ItemClassification.progression_skip_balancing,
|
||||
"Turtle Zone 3 Midway Bell": ItemClassification.filler,
|
||||
"Mario's Castle Midway Bell": ItemClassification.progression_skip_balancing,
|
||||
"1 Coin": ItemClassification.filler,
|
||||
**{f"{i} Coins": ItemClassification.filler for i in range(2, CoinsanityChecks.range_end + 1)}
|
||||
}
|
||||
|
||||
for level in {"Turtle Zone Secret Course", "Macro Zone Secret Course", "Turtle Zone 3", "Scenic Course",
|
||||
"Mario Zone 2"}:
|
||||
items[f"Cancel Auto Scroll - {level}"] = ItemClassification.useful
|
||||
498
worlds/marioland2/locations.py
Normal file
498
worlds/marioland2/locations.py
Normal file
@@ -0,0 +1,498 @@
|
||||
START_IDS = 1
|
||||
|
||||
locations = {
|
||||
"Mushroom Zone - Normal Exit": {"id": 0x00, "ram_index": 0, "type": "level"},
|
||||
"Mushroom Zone - Midway Bell": {"id": 0x00, "ram_index": 0, "clear_condition": ("Mushroom Zone Midway Bell", 1), "type": "bell"},
|
||||
"Scenic Course - Normal Exit": {"id": 0x19, "ram_index": 40, "type": "level"},
|
||||
"Tree Zone 1 - Normal Exit": {"id": 0x01, "ram_index": 1, "clear_condition": ("Tree Zone Progression", 1), "type": "level"},
|
||||
"Tree Zone 1 - Midway Bell": {"id": 0x01, "ram_index": 1, "clear_condition": ("Tree Zone 1 Midway Bell", 1), "type": "bell"},
|
||||
"Tree Zone 2 - Normal Exit": {"id": 0x02, "ram_index": 2, "clear_condition": ("Tree Zone Progression", 2), "type": "level"},
|
||||
"Tree Zone 2 - Secret Exit": {"id": 0x02, "ram_index": 2, "clear_condition": ("Tree Zone Secret", 1), "type": "secret"},
|
||||
"Tree Zone 2 - Midway Bell": {"id": 0x02, "ram_index": 2, "clear_condition": ("Tree Zone 2 Midway Bell", 1), "type": "bell"},
|
||||
"Tree Zone 3 - Normal Exit": {"id": 0x04, "ram_index": 4, "clear_condition": ("Tree Zone Progression", 3), "type": "level"},
|
||||
"Tree Zone 4 - Normal Exit": {"id": 0x03, "ram_index": 3, "clear_condition": ("Tree Zone Progression", 3), "type": "level"},
|
||||
"Tree Zone 4 - Midway Bell": {"id": 0x03, "ram_index": 3, "clear_condition": ("Tree Zone 4 Midway Bell", 1), "type": "bell"},
|
||||
"Tree Zone 5 - Boss": {"id": 0x05, "ram_index": 5, "clear_condition": ("Tree Coin", 1), "type": "level"},
|
||||
"Tree Zone 5 - Midway Bell": {"id": 0x05, "ram_index": 5, "clear_condition": ("Tree Zone 5 Midway Bell", 1), "type": "bell"},
|
||||
"Tree Zone Secret Course - Normal Exit": {"id": 0x1D, "ram_index": 36, "type": "level"},
|
||||
"Hippo Zone - Normal or Secret Exit": {"id": 0x11, "ram_index": 31, "type": "level"},
|
||||
"Space Zone 1 - Normal Exit": {"id": 0x12, "ram_index": 16, "clear_condition": ("Space Zone Progression", 1), "type": "level"},
|
||||
"Space Zone 1 - Secret Exit": {"id": 0x12, "ram_index": 16, "clear_condition": ("Space Zone Secret", 1), "type": "secret"},
|
||||
"Space Zone 1 - Midway Bell": {"id": 0x12, "ram_index": 16, "clear_condition": ("Space Zone 1 Midway Bell", 1), "type": "bell"},
|
||||
"Space Zone Secret Course - Normal Exit": {"id": 0x1C, "ram_index": 41, "type": "level"},
|
||||
"Space Zone 2 - Boss": {"id": 0x13, "ram_index": 17, "clear_condition": ("Space Coin", 1), "type": "level"},
|
||||
"Space Zone 2 - Midway Bell": {"id": 0x13, "ram_index": 17, "clear_condition": ("Space Zone 2 Midway Bell", 1), "type": "bell"},
|
||||
"Macro Zone 1 - Normal Exit": {"id": 0x14, "ram_index": 11, "clear_condition": ("Macro Zone Progression", 1), "type": "level"},
|
||||
"Macro Zone 1 - Secret Exit": {"id": 0x14, "ram_index": 11, "clear_condition": ("Macro Zone Secret 1", 1), "type": "secret"},
|
||||
"Macro Zone 1 - Midway Bell": {"id": 0x14, "ram_index": 11, "clear_condition": ("Macro Zone 1 Midway Bell", 1), "type": "bell"},
|
||||
"Macro Zone 2 - Normal Exit": {"id": 0x15, "ram_index": 12, "clear_condition": ("Macro Zone Progression", 2), "type": "level"},
|
||||
"Macro Zone 2 - Midway Bell": {"id": 0x15, "ram_index": 12, "clear_condition": ("Macro Zone 2 Midway Bell", 1), "type": "bell"},
|
||||
"Macro Zone 3 - Normal Exit": {"id": 0x16, "ram_index": 13, "clear_condition": ("Macro Zone Progression", 3), "type": "level"},
|
||||
"Macro Zone 3 - Midway Bell": {"id": 0x16, "ram_index": 13, "clear_condition": ("Macro Zone 3 Midway Bell", 1), "type": "bell"},
|
||||
"Macro Zone 4 - Boss": {"id": 0x17, "ram_index": 14, "clear_condition": ("Macro Coin", 1), "type": "level"},
|
||||
"Macro Zone 4 - Midway Bell": {"id": 0x17, "ram_index": 14, "clear_condition": ("Macro Zone 4 Midway Bell", 1), "type": "bell"},
|
||||
"Macro Zone Secret Course - Normal Exit": {"id": 0x1E, "ram_index": 35, "clear_condition": ("Macro Zone Secret 2", 1), "type": "level"},
|
||||
"Pumpkin Zone 1 - Normal Exit": {"id": 0x06, "ram_index": 6, "clear_condition": ("Pumpkin Zone Progression", 1), "type": "level"},
|
||||
"Pumpkin Zone 1 - Midway Bell": {"id": 0x06, "ram_index": 6, "clear_condition": ("Pumpkin Zone 1 Midway Bell", 1), "type": "bell"},
|
||||
"Pumpkin Zone 2 - Normal Exit": {"id": 0x07, "ram_index": 7, "clear_condition": ("Pumpkin Zone Progression", 2), "type": "level"},
|
||||
"Pumpkin Zone 2 - Secret Exit": {"id": 0x07, "ram_index": 7, "clear_condition": ("Pumpkin Zone Secret 1", 1), "type": "secret"},
|
||||
"Pumpkin Zone 2 - Midway Bell": {"id": 0x07, "ram_index": 7, "clear_condition": ("Pumpkin Zone 2 Midway Bell", 2), "type": "bell"},
|
||||
"Pumpkin Zone 3 - Normal Exit": {"id": 0x08, "ram_index": 8, "clear_condition": ("Pumpkin Zone Progression", 3), "type": "level"},
|
||||
"Pumpkin Zone 3 - Secret Exit": {"id": 0x08, "ram_index": 8, "clear_condition": ("Pumpkin Zone Secret 2", 1), "type": "secret"},
|
||||
"Pumpkin Zone 3 - Midway Bell": {"id": 0x08, "ram_index": 8, "clear_condition": ("Pumpkin Zone 3 Midway Bell", 3), "type": "bell"},
|
||||
"Pumpkin Zone 4 - Boss": {"id": 0x09, "ram_index": 9, "clear_condition": ("Pumpkin Coin", 1), "type": "level"},
|
||||
"Pumpkin Zone 4 - Midway Bell": {"id": 0x09, "ram_index": 9, "clear_condition": ("Pumpkin Zone 4 Midway Bell", 1), "type": "bell"},
|
||||
"Pumpkin Zone Secret Course 1 - Normal Exit": {"id": 0x1B, "ram_index": 38, "type": "level"},
|
||||
"Pumpkin Zone Secret Course 2 - Normal Exit": {"id": 0x1F, "ram_index": 39, "type": "level"},
|
||||
"Mario Zone 1 - Normal Exit": {"id": 0x0A, "ram_index": 26, "clear_condition": ("Mario Zone Progression", 1), "type": "level"},
|
||||
"Mario Zone 1 - Midway Bell": {"id": 0x0A, "ram_index": 26, "clear_condition": ("Mario Zone 1 Midway Bell", 1), "type": "bell"},
|
||||
"Mario Zone 2 - Normal Exit": {"id": 0x0B, "ram_index": 27, "clear_condition": ("Mario Zone Progression", 2), "type": "level"},
|
||||
"Mario Zone 2 - Midway Bell": {"id": 0x0B, "ram_index": 27, "clear_condition": ("Mario Zone 2 Midway Bell", 1), "type": "bell"},
|
||||
"Mario Zone 3 - Normal Exit": {"id": 0x0C, "ram_index": 28, "clear_condition": ("Mario Zone Progression", 3), "type": "level"},
|
||||
"Mario Zone 3 - Midway Bell": {"id": 0x0C, "ram_index": 28, "clear_condition": ("Mario Zone 3 Midway Bell", 1), "type": "bell"},
|
||||
"Mario Zone 4 - Boss": {"id": 0x0D, "ram_index": 29, "clear_condition": ("Mario Coin", 1), "type": "level"},
|
||||
"Mario Zone 4 - Midway Bell": {"id": 0x0D, "ram_index": 29, "clear_condition": ("Mario Zone 4 Midway Bell", 1), "type": "bell"},
|
||||
"Turtle Zone 1 - Normal Exit": {"id": 0x0E, "ram_index": 21, "clear_condition": ("Turtle Zone Progression", 1), "type": "level"},
|
||||
"Turtle Zone 1 - Midway Bell": {"id": 0x0E, "ram_index": 21, "clear_condition": ("Turtle Zone 1 Midway Bell", 1), "type": "bell"},
|
||||
"Turtle Zone 2 - Normal Exit": {"id": 0x0F, "ram_index": 22, "clear_condition": ("Turtle Zone Progression", 2), "type": "level"},
|
||||
"Turtle Zone 2 - Secret Exit": {"id": 0x0F, "ram_index": 22, "clear_condition": ("Turtle Zone Secret", 1), "type": "secret"},
|
||||
"Turtle Zone 2 - Midway Bell": {"id": 0x0F, "ram_index": 22, "clear_condition": ("Turtle Zone 2 Midway Bell", 1), "type": "bell"},
|
||||
"Turtle Zone 3 - Boss": {"id": 0x10, "ram_index": 23, "clear_condition": ("Turtle Coin", 1), "type": "level"},
|
||||
"Turtle Zone 3 - Midway Bell": {"id": 0x10, "ram_index": 23, "clear_condition": ("Turtle Zone 3 Midway Bell", 1), "type": "bell"},
|
||||
"Turtle Zone Secret Course - Normal Exit": {"id": 0x1A, "ram_index": 37, "type": "level"},
|
||||
"Mario's Castle - Midway Bell": {"id": 24, "ram_index": 24, "clear_condition": ("Mario's Castle Midway Bell", 1), "type": "bell"},
|
||||
}
|
||||
|
||||
|
||||
coins_coords = {
|
||||
"Mushroom Zone":
|
||||
[(22, 28), (24, 28), (42, 28), (43, 28), (74, 36), (74, 37), (74, 38), (76, 36), (76, 37),
|
||||
(76, 38), (78, 36), (78, 37), (78, 38), (80, 36), (80, 37), (80, 38), (82, 36), (82, 37),
|
||||
(82, 38), (83, 25), (84, 25), (84, 36), (84, 37), (84, 38), (85, 25), (86, 25), (86, 36),
|
||||
(86, 37), (86, 38), (87, 25), (88, 36), (88, 37), (88, 38), (116, 24), (117, 24), (118, 24),
|
||||
(151, 28), (152, 28), (180, 28), (181, 24), (181, 28), (182, 24), (182, 28), (183, 24), (183, 28),
|
||||
(184, 24), (184, 28), (185, 24), (185, 28), (186, 24), (186, 28), (187, 24), (187, 28), (188, 24),
|
||||
(188, 28), (189, 28), (211, 25), (212, 25), (212, 36), (212, 37), (212, 38), (212, 39), (213, 25),
|
||||
(213, 36), (213, 37), (213, 38), (213, 39), (214, 25), (214, 36), (214, 37), (214, 38), (214, 39),
|
||||
(215, 25), (216, 25), (217, 25), (217, 36), (217, 37), (217, 38), (217, 39), (218, 25), (218, 36),
|
||||
(218, 37), (218, 38), (218, 39), (219, 25), (219, 36), (219, 37), (219, 38), (219, 39), (220, 25),
|
||||
(231, 24), (232, 24)],
|
||||
"Tree Zone 1":
|
||||
[(27, 30), (28, 30), (29, 30), (33, 27), (34, 27), (35, 27), (40, 30), (41, 30), (42, 30), (47, 27),
|
||||
(48, 27), (49, 27), (56, 30), (57, 30), (58, 30), (64, 30), (65, 30), (66, 30), (88, 30), (89, 30),
|
||||
(90, 30), (94, 30), (95, 30), (96, 30), (100, 30), (101, 30), (102, 30), (106, 27), (107, 27),
|
||||
(108, 27), (112, 30), (113, 30), (114, 30), (119, 28), (138, 30), (139, 30), (140, 30), (150, 28),
|
||||
(151, 20), (151, 28), (152, 20), (152, 26), (152, 28), (153, 26), (153, 28), (154, 26), (154, 28),
|
||||
(155, 26), (155, 28), (156, 26), (156, 28), (157, 20), (157, 26), (157, 28), (158, 20), (158, 26),
|
||||
(158, 28), (159, 26), (159, 28), (160, 28), (161, 28), (176, 13), (177, 13), (177, 29), (178, 13),
|
||||
(178, 29), (179, 13), (179, 29), (180, 13), (181, 13), (182, 13), (183, 13), (184, 13), (185, 13),
|
||||
(186, 13), (187, 13), (187, 29), (188, 13), (188, 29), (189, 13), (189, 29), (190, 13), (191, 13),
|
||||
(192, 13), (193, 13), (194, 13), (195, 13), (196, 13), (197, 13), (197, 29), (198, 13), (198, 29),
|
||||
(199, 13), (199, 29), (200, 13), (201, 13), (202, 13), (203, 13), (204, 13), (205, 13), (206, 13),
|
||||
(207, 27), (208, 13), (208, 27), (209, 14), (209, 27), (210, 10), (210, 11), (210, 12), (210, 13),
|
||||
(210, 14), (210, 15), (211, 14), (212, 13), (219, 30), (220, 30), (221, 30), (229, 27), (230, 27),
|
||||
(231, 27)],
|
||||
"Tree Zone 2":
|
||||
[(27, 11), (28, 11), (42, 10), (43, 10), (44, 10), (51, 28), (61, 9), (65, 26), (66, 26), (67, 26),
|
||||
(70, 24), (71, 24), (72, 10), (72, 24), (73, 10), (73, 24), (75, 10), (76, 10), (76, 26), (77, 26),
|
||||
(78, 10), (78, 26), (79, 10), (80, 24), (81, 10), (81, 24), (82, 10), (82, 24), (83, 24), (127, 7),
|
||||
(128, 7), (129, 7), (130, 7), (136, 43), (138, 9), (138, 10), (138, 11), (139, 41), (140, 41),
|
||||
(141, 41), (142, 9), (142, 10), (142, 11), (144, 41), (145, 41), (146, 9), (146, 10), (146, 11),
|
||||
(146, 41), (149, 41), (150, 41), (151, 41), (154, 41), (155, 41), (156, 41), (159, 41), (160, 41),
|
||||
(161, 41), (164, 41), (165, 41), (166, 41), (169, 41), (170, 41), (171, 41), (174, 41), (175, 41),
|
||||
(176, 41), (182, 3), (188, 42), (188, 43), (188, 44), (189, 42), (189, 43), (189, 44), (190, 42),
|
||||
(190, 43), (190, 44), (191, 42), (191, 43), (191, 44), (192, 42), (192, 43), (192, 44), (193, 42),
|
||||
(193, 43), (193, 44), (213, 8), (213, 9), (213, 10), (213, 11), (213, 12), (213, 13), (213, 14),
|
||||
(213, 15), (213, 16), (213, 17), (213, 18), (213, 19), (213, 20), (213, 21), (213, 22), (213, 23),
|
||||
(213, 24), (213, 25)],
|
||||
"Tree Zone Secret Course":
|
||||
[(10, 24), (11, 24), (12, 24), (17, 23), (39, 24), (40, 24), (41, 24), (42, 24), (45, 24),
|
||||
(46, 24), (47, 24), (48, 24), (51, 25), (52, 25), (53, 25), (54, 25), (58, 26), (59, 26),
|
||||
(60, 26), (61, 24), (61, 25), (62, 24), (62, 25), (63, 24), (63, 25), (64, 24), (64, 25),
|
||||
(67, 25), (68, 26), (69, 27), (70, 27), (73, 26), (74, 27), (75, 27), (76, 27), (80, 23),
|
||||
(80, 24), (81, 23), (81, 24), (82, 23), (82, 24), (83, 23), (83, 24), (87, 25), (88, 24),
|
||||
(89, 24), (90, 25), (91, 26), (100, 23), (114, 27), (114, 28), (115, 27), (115, 28), (116, 27),
|
||||
(116, 28), (117, 27), (117, 28), (118, 27), (118, 28), (119, 27), (119, 28), (120, 27),
|
||||
(120, 28), (121, 27), (121, 28), (128, 27), (128, 28), (131, 27), (131, 28), (134, 27),
|
||||
(134, 28), (137, 27), (137, 28), (138, 27), (138, 28), (143, 27), (143, 28), (159, 23)],
|
||||
"Tree Zone 4":
|
||||
[(22, 10), (24, 12), (26, 10), (28, 27), (29, 11), (30, 11), (31, 11), (32, 11), (33, 11), (34, 11),
|
||||
(35, 11), (37, 10), (38, 12), (41, 11), (43, 12), (61, 11), (70, 11), (79, 11), (89, 11), (103, 22),
|
||||
(103, 25), (103, 28), (105, 22), (105, 25), (105, 28), (107, 22), (107, 25), (107, 28), (109, 22),
|
||||
(109, 25), (109, 28), (111, 22), (111, 25), (111, 28), (113, 22), (113, 25), (113, 28), (115, 22),
|
||||
(115, 25), (115, 28), (117, 22), (117, 25), (117, 28), (122, 22), (122, 25), (122, 28), (124, 22),
|
||||
(124, 25), (124, 28), (126, 22), (126, 25), (126, 28), (128, 22), (128, 25), (128, 28), (130, 22),
|
||||
(130, 25), (130, 28), (132, 22), (132, 25), (132, 28), (134, 22), (134, 25), (134, 28), (136, 22),
|
||||
(136, 25), (136, 28), (171, 10), (196, 26), (196, 29), (197, 26), (197, 29), (198, 26), (198, 29),
|
||||
(199, 26), (199, 29), (200, 26), (200, 29)],
|
||||
"Tree Zone 3":
|
||||
[(18, 11), (18, 12), (19, 11), (19, 12), (20, 11), (20, 12), (21, 11), (21, 12), (22, 11), (22, 12),
|
||||
(26, 40), (27, 11), (27, 12), (28, 11), (28, 12), (29, 11), (29, 12), (30, 11), (30, 12), (31, 11),
|
||||
(31, 12), (48, 41), (49, 41), (50, 41), (51, 41), (61, 25), (77, 24)],
|
||||
"Tree Zone 5":
|
||||
[(23, 41), (84, 39), (85, 39), (116, 42), (123, 39), (132, 39), (134, 36), (134, 39), (134, 43),
|
||||
(134, 44), (135, 43), (135, 44), (136, 36), (136, 39), (136, 43), (136, 44), (137, 43), (137, 44),
|
||||
(138, 36), (138, 39), (138, 43), (138, 44), (139, 43), (139, 44), (140, 36), (140, 39), (140, 43),
|
||||
(140, 44), (141, 43), (141, 44), (142, 36), (142, 39), (142, 43), (142, 44), (144, 36), (144, 39),
|
||||
(146, 36), (146, 39)],
|
||||
"Scenic Course":
|
||||
[(24, 28), (39, 28), (54, 28), (72, 28), (87, 28), (103, 28), (117, 28)],
|
||||
"Hippo Zone":
|
||||
[(2, 20), (3, 3), (15, 26), (16, 26), (17, 26), (28, 4), (28, 7), (28, 10), (28, 13), (29, 4),
|
||||
(29, 7), (29, 10), (29, 13), (29, 21), (30, 4), (30, 7), (30, 10), (30, 13), (32, 15), (33, 15),
|
||||
(34, 15), (35, 15), (36, 15), (37, 15), (41, 12), (41, 13), (42, 11), (43, 10), (44, 10), (45, 10),
|
||||
(46, 11), (47, 12), (47, 13), (48, 14), (49, 15), (50, 15), (51, 15), (52, 14), (53, 12), (53, 13),
|
||||
(54, 11), (55, 10), (56, 10), (57, 10), (58, 11), (59, 12), (59, 13), (60, 14), (61, 15), (62, 15),
|
||||
(63, 15), (64, 14), (65, 12), (65, 13), (66, 11), (67, 10), (68, 10), (69, 10), (70, 11), (71, 12),
|
||||
(71, 13), (72, 14), (73, 15), (74, 15), (75, 15), (76, 14), (77, 12), (77, 13), (84, 11), (85, 11),
|
||||
(85, 22), (86, 22), (91, 6), (92, 6), (92, 11), (93, 6), (93, 11), (94, 11), (95, 16), (96, 12),
|
||||
(96, 16), (97, 8), (97, 12), (97, 16), (98, 8), (98, 12), (99, 8), (112, 6), (112, 7), (112, 12),
|
||||
(112, 13), (113, 2), (113, 5), (113, 8), (113, 11), (113, 14), (113, 17), (114, 2), (114, 5),
|
||||
(114, 8), (114, 11), (114, 14), (114, 17), (115, 3), (115, 4), (115, 9), (115, 10), (115, 15),
|
||||
(115, 16), (124, 3), (124, 4), (124, 9), (124, 10), (124, 15), (124, 16), (125, 2), (125, 5),
|
||||
(125, 8), (125, 11), (125, 14), (125, 17), (126, 2), (126, 5), (126, 8), (126, 11), (126, 14),
|
||||
(126, 17), (127, 6), (127, 7), (127, 12), (127, 13), (129, 13), (130, 13), (131, 13), (132, 13),
|
||||
(132, 22), (133, 13), (134, 13), (135, 13), (136, 13), (136, 21), (137, 13), (138, 13), (139, 13),
|
||||
(139, 22), (140, 13), (141, 13), (142, 13), (154, 7), (155, 7), (156, 7), (157, 10), (158, 10),
|
||||
(159, 10), (162, 15), (162, 16), (162, 17), (164, 15), (164, 16), (164, 17), (166, 15), (166, 16),
|
||||
(166, 17), (168, 15), (168, 16), (168, 17), (170, 15), (170, 16), (170, 17), (172, 15), (172, 16),
|
||||
(172, 17), (174, 15), (174, 16), (174, 17), (176, 15), (176, 16), (176, 17)],
|
||||
"Space Zone 1":
|
||||
[(38, 26), (45, 25), (46, 25), (47, 25), (57, 24), (58, 24), (59, 24), (60, 19), (60, 23), (61, 23),
|
||||
(62, 23), (63, 23), (75, 24), (89, 16), (89, 17), (89, 18), (89, 19), (90, 16), (90, 17), (90, 18),
|
||||
(90, 19), (91, 16), (91, 17), (91, 18), (91, 19), (92, 16), (92, 17), (92, 18), (92, 19), (93, 16),
|
||||
(93, 17), (93, 18), (93, 19), (104, 22), (105, 22), (114, 22), (115, 22), (125, 10), (126, 9),
|
||||
(127, 8), (128, 8), (129, 8), (130, 8), (131, 8), (132, 9), (133, 10), (136, 10), (137, 9),
|
||||
(138, 8), (139, 8), (140, 8), (141, 8), (142, 8), (143, 9), (144, 10), (147, 10), (148, 9),
|
||||
(149, 8), (150, 8), (151, 8), (152, 8), (153, 8), (154, 9), (155, 10), (155, 18), (155, 19),
|
||||
(155, 20), (156, 17), (156, 18), (156, 19), (156, 20), (156, 21), (157, 16), (157, 17), (157, 18),
|
||||
(157, 19), (157, 20), (157, 21), (157, 22), (158, 10), (158, 16), (158, 17), (158, 18), (158, 19),
|
||||
(158, 20), (158, 21), (158, 22), (159, 9), (159, 16), (159, 17), (159, 18), (159, 19), (159, 20),
|
||||
(159, 21), (159, 22), (160, 8), (160, 16), (160, 17), (160, 18), (160, 19), (160, 20), (160, 21),
|
||||
(160, 22), (161, 8), (161, 16), (161, 17), (161, 18), (161, 19), (161, 20), (161, 21), (161, 22),
|
||||
(162, 8), (162, 17), (162, 18), (162, 19), (162, 20), (162, 21), (163, 8), (163, 18), (163, 19),
|
||||
(163, 20), (164, 8), (165, 9), (166, 10), (168, 10), (169, 9), (170, 8), (171, 8), (172, 8),
|
||||
(173, 8), (174, 8), (175, 9), (176, 10)],
|
||||
"Space Zone Secret Course":
|
||||
[(16, 22), (16, 23), (16, 24), (18, 21), (18, 22), (18, 23), (20, 21), (20, 22), (20, 23),
|
||||
(22, 20), (22, 21), (22, 22), (24, 19), (24, 20), (24, 21), (26, 18), (26, 19), (26, 20),
|
||||
(28, 18), (28, 19), (28, 20), (30, 17), (30, 18), (30, 19), (36, 15), (36, 16), (36, 17),
|
||||
(38, 14), (38, 15), (38, 16), (40, 13), (40, 14), (40, 15), (40, 24), (41, 24), (42, 13),
|
||||
(42, 14), (42, 15), (44, 12), (44, 13), (44, 14), (46, 12), (46, 13), (46, 14), (48, 12),
|
||||
(48, 13), (48, 14), (50, 11), (50, 12), (50, 13), (50, 27), (51, 27), (52, 10), (52, 11),
|
||||
(52, 12), (52, 27), (53, 27), (54, 27), (58, 11), (58, 12), (58, 13), (60, 12), (60, 13),
|
||||
(60, 14), (62, 12), (62, 13), (62, 14), (64, 12), (64, 13), (64, 14), (66, 13), (66, 14),
|
||||
(66, 15), (68, 13), (68, 14), (68, 15), (70, 14), (70, 15), (70, 16), (72, 15), (72, 16),
|
||||
(72, 17), (74, 16), (74, 17), (74, 18), (80, 18), (80, 19), (80, 20), (82, 19), (82, 20),
|
||||
(82, 21), (84, 19), (84, 20), (84, 21), (86, 20), (86, 21), (86, 22), (88, 21), (88, 22),
|
||||
(88, 23)],
|
||||
"Space Zone 2":
|
||||
[(11, 13), (12, 13), (13, 13), (20, 8), (21, 8), (22, 8), (25, 5), (26, 5), (27, 5), (33, 6),
|
||||
(34, 6), (35, 6), (36, 10), (37, 10), (38, 10), (45, 7), (46, 7), (47, 7), (59, 5), (60, 5),
|
||||
(61, 5), (64, 3), (93, 8), (94, 8), (95, 8), (96, 11), (97, 11), (98, 11), (100, 6), (101, 6),
|
||||
(102, 6), (102, 8), (120, 5), (124, 12), (124, 13), (125, 12), (125, 13), (126, 12), (126, 13),
|
||||
(127, 3), (127, 12), (127, 13), (128, 3), (128, 7), (129, 3), (129, 7), (130, 7), (148, 6),
|
||||
(148, 7), (148, 8), (149, 5), (149, 6), (149, 7), (149, 8), (149, 9), (150, 5), (150, 6), (150, 7),
|
||||
(150, 8), (150, 9), (151, 5), (151, 6), (151, 7), (151, 8), (151, 9), (152, 5), (152, 6), (152, 7),
|
||||
(152, 8), (152, 9), (153, 6), (153, 7), (153, 8), (165, 7), (165, 8), (166, 7), (166, 8), (167, 7),
|
||||
(167, 8), (168, 7), (168, 8), (169, 7), (169, 8), (170, 7), (170, 8), (171, 7), (171, 8), (181, 9),
|
||||
(185, 4), (200, 3), (200, 6), (200, 9), (201, 3), (201, 6), (201, 9), (202, 3), (202, 6), (202, 9),
|
||||
(203, 3), (203, 6), (203, 9), (204, 3), (204, 6), (204, 9), (205, 3), (205, 6), (205, 9), (206, 3),
|
||||
(206, 6), (206, 9), (207, 3), (207, 6), (207, 9), (208, 3), (208, 6), (208, 9), (209, 3), (209, 6),
|
||||
(209, 9), (210, 3), (210, 6), (210, 9), (230, 12), (231, 12), (232, 12), (236, 2), (236, 3),
|
||||
(236, 4), (236, 5), (237, 2), (237, 3), (237, 4), (237, 5), (238, 2), (238, 3), (238, 4), (238, 5),
|
||||
(248, 10)],
|
||||
"Turtle Zone 1":
|
||||
[(22, 34), (27, 37), (28, 37), (29, 37), (30, 37), (31, 37), (32, 37), (33, 37), (34, 37),
|
||||
(35, 37), (36, 37), (46, 32), (46, 33), (47, 32), (47, 33), (50, 32), (50, 33), (51, 32),
|
||||
(51, 33), (54, 32), (54, 33), (55, 32), (55, 33), (56, 33), (57, 33), (58, 32), (58, 33),
|
||||
(59, 32), (59, 33), (62, 32), (62, 33), (63, 32), (63, 33), (66, 32), (66, 33), (67, 32),
|
||||
(67, 33), (73, 43), (74, 43), (75, 43), (77, 41), (78, 41), (79, 41), (81, 40), (82, 40),
|
||||
(83, 40), (85, 41), (86, 41), (87, 41), (122, 36), (123, 36), (124, 36), (125, 36), (126, 36),
|
||||
(127, 36), (130, 36), (131, 36), (132, 36), (133, 36), (134, 36), (135, 36), (136, 36), (137, 36),
|
||||
(138, 36), (139, 36), (140, 36), (141, 36), (143, 34), (163, 36), (164, 36), (166, 36), (167, 36),
|
||||
(169, 36), (170, 36), (180, 37), (181, 37), (182, 37), (183, 37), (184, 37), (185, 37), (188, 44),
|
||||
(189, 44)],
|
||||
"Turtle Zone 2":
|
||||
[(6, 34), (11, 34), (15, 43), (48, 36), (51, 28), (56, 35), (57, 35), (59, 42), (61, 20), (62, 20),
|
||||
(62, 35), (63, 20), (63, 35), (64, 20), (65, 20), (67, 35), (68, 35), (72, 39), (79, 34),
|
||||
(82, 35), (87, 42), (96, 43), (105, 43), (107, 43), (109, 43), (118, 28), (121, 28), (139, 39),
|
||||
(142, 39)],
|
||||
"Turtle Zone Secret Course":
|
||||
[(19, 27), (39, 27), (39, 28), (40, 26), (40, 27), (41, 25), (41, 27), (42, 25), (42, 27),
|
||||
(43, 26), (43, 27), (44, 27), (44, 28), (48, 25), (48, 26), (48, 27), (48, 28), (49, 25),
|
||||
(49, 27), (50, 25), (50, 27), (51, 25), (51, 27), (52, 26), (52, 28), (53, 27), (61, 25),
|
||||
(61, 28), (62, 25), (62, 26), (62, 27), (62, 28), (63, 25), (63, 28), (64, 26), (73, 26),
|
||||
(73, 27), (74, 25), (74, 28), (75, 25), (75, 28), (76, 25), (76, 28), (77, 26), (77, 27),
|
||||
(82, 25), (82, 26), (82, 27), (82, 28), (83, 28), (84, 28), (85, 28), (87, 27), (89, 27),
|
||||
(89, 28), (90, 26), (90, 27), (91, 25), (91, 27), (92, 25), (92, 27), (93, 26), (93, 27),
|
||||
(94, 27), (94, 28), (98, 24), (98, 25), (98, 26), (98, 27), (99, 25), (100, 26), (101, 27),
|
||||
(102, 24), (102, 25), (102, 26), (102, 27), (108, 24), (108, 25), (108, 26), (108, 27),
|
||||
(109, 24), (109, 27), (110, 24), (110, 27), (111, 24), (111, 27), (112, 25), (112, 26),
|
||||
(116, 24), (116, 27), (117, 23), (117, 26), (117, 27), (118, 23), (118, 25), (118, 27),
|
||||
(119, 23), (119, 25), (119, 27), (120, 24), (120, 27), (121, 28), (122, 28), (123, 28)],
|
||||
"Turtle Zone 3":
|
||||
[(16, 25), (17, 25), (18, 25), (19, 25), (20, 25), (21, 25), (22, 25), (23, 25), (24, 25),
|
||||
(35, 24), (36, 24), (37, 24), (38, 24), (39, 24), (40, 24), (41, 24), (42, 24), (43, 24),
|
||||
(75, 28), (75, 29), (76, 28), (76, 29), (81, 28), (81, 29), (82, 28), (82, 29), (92, 26),
|
||||
(93, 26), (94, 26), (98, 26), (99, 26), (100, 26), (123, 26), (124, 26), (126, 26), (127, 26),
|
||||
(129, 26), (130, 26), (146, 22), (146, 29), (147, 22), (147, 29), (148, 22), (150, 22), (151, 22),
|
||||
(152, 23), (152, 29), (153, 29), (154, 22), (155, 22), (156, 22), (158, 29), (159, 29), (161, 22),
|
||||
(162, 22), (163, 22), (165, 22), (166, 22), (167, 22), (169, 22), (170, 22), (171, 22)],
|
||||
"Mario Zone 1":
|
||||
[(18, 44), (47, 36), (47, 37), (47, 38), (47, 39), (49, 36), (49, 37), (49, 38), (50, 36), (50, 37),
|
||||
(50, 38), (52, 36), (52, 37), (52, 38), (53, 36), (53, 37), (53, 38), (60, 35), (60, 36), (60, 37),
|
||||
(61, 35), (61, 36), (61, 37), (64, 35), (64, 36), (64, 37), (65, 35), (65, 36), (65, 37), (71, 36),
|
||||
(78, 36), (78, 37), (78, 38), (98, 38), (145, 42), (146, 22), (146, 23), (146, 25), (146, 26),
|
||||
(146, 42), (147, 22), (147, 23), (147, 25), (147, 26), (147, 30), (147, 31), (147, 32), (147, 42),
|
||||
(148, 22), (148, 23), (148, 25), (148, 30), (148, 31), (148, 32), (148, 42), (149, 21), (149, 22),
|
||||
(149, 23), (149, 24), (149, 30), (149, 31), (149, 42), (150, 21), (150, 22), (150, 23), (150, 24),
|
||||
(150, 26), (150, 27), (150, 28), (150, 30), (150, 31), (150, 42), (151, 27), (151, 28), (151, 30),
|
||||
(151, 31), (151, 42), (152, 27), (152, 29), (152, 30), (152, 31), (152, 42), (153, 27), (153, 29),
|
||||
(153, 30), (153, 42), (154, 27), (154, 29), (154, 30), (164, 20), (167, 21), (167, 26), (167, 34),
|
||||
(168, 21), (168, 25), (168, 27), (168, 33), (168, 35), (169, 20), (169, 24), (169, 28), (169, 33),
|
||||
(169, 35), (170, 20), (170, 23), (170, 29), (170, 32), (170, 35), (171, 20), (171, 23), (171, 29),
|
||||
(171, 32), (171, 36), (171, 37), (172, 21), (172, 22), (172, 30), (172, 31)],
|
||||
"Mario Zone 2":
|
||||
[(25, 24), (25, 27), (26, 24), (26, 27), (27, 24), (27, 27), (81, 27), (112, 24), (113, 24),
|
||||
(114, 24), (115, 24), (116, 24), (117, 24), (118, 24), (121, 24), (122, 24), (123, 24), (124, 24),
|
||||
(125, 24), (126, 24), (127, 24), (138, 26), (139, 26), (140, 24), (140, 28), (141, 24), (141, 28),
|
||||
(144, 26), (145, 26), (146, 24), (146, 28), (147, 28), (151, 26), (152, 24), (152, 28), (153, 24),
|
||||
(153, 28), (156, 26), (157, 26), (158, 24), (158, 28), (159, 24), (159, 28), (162, 26), (163, 26),
|
||||
(164, 28), (165, 28)],
|
||||
"Mario Zone 3":
|
||||
[(8, 28), (11, 28), (14, 28), (17, 28), (20, 28), (23, 28), (54, 25), (100, 27), (109, 18),
|
||||
(109, 19), (110, 18), (110, 19), (111, 17), (111, 18), (111, 19), (112, 17), (112, 18), (112, 19),
|
||||
(127, 17), (127, 18), (127, 19), (128, 17), (128, 18), (128, 19), (129, 18), (129, 19), (130, 18),
|
||||
(130, 19), (130, 20), (131, 18), (131, 19), (131, 20), (132, 18), (132, 19), (132, 20), (133, 18),
|
||||
(133, 19), (133, 20), (133, 27), (134, 18), (134, 19), (134, 20), (157, 28), (158, 28), (159, 28),
|
||||
(160, 28), (161, 28), (168, 28), (169, 28), (170, 28), (171, 28), (172, 28), (189, 44), (199, 28),
|
||||
(200, 28), (201, 28), (202, 28), (203, 28), (214, 27), (217, 27), (220, 27), (223, 27)],
|
||||
"Mario Zone 4":
|
||||
[(20, 25), (114, 24), (114, 25), (114, 26), (115, 24), (115, 25), (115, 26), (115, 27), (115, 28),
|
||||
(115, 29), (116, 24), (116, 25), (116, 26), (116, 27), (116, 29), (117, 24), (117, 25), (117, 26),
|
||||
(117, 27), (117, 28), (117, 29), (118, 24), (118, 25), (118, 26), (118, 27), (118, 28), (118, 29),
|
||||
(119, 24), (119, 25), (119, 27), (119, 28), (119, 29), (120, 24), (120, 25), (120, 26), (120, 27),
|
||||
(120, 28), (120, 29), (121, 24), (121, 25), (121, 26), (121, 27), (121, 28), (121, 29), (122, 12),
|
||||
(122, 24), (122, 25), (122, 26), (122, 27), (122, 29), (123, 12), (123, 24), (123, 25), (123, 26),
|
||||
(123, 27), (123, 28), (123, 29), (124, 12), (124, 24), (124, 25), (124, 26), (124, 27), (124, 28),
|
||||
(124, 29), (125, 12), (179, 12)],
|
||||
"Pumpkin Zone 1":
|
||||
[(23, 12), (55, 24), (55, 26), (56, 27), (57, 24), (57, 26), (63, 24), (63, 26), (64, 27),
|
||||
(65, 24), (65, 26), (71, 24), (71, 26), (72, 27), (73, 24), (73, 26), (79, 24), (79, 26),
|
||||
(80, 27), (81, 24), (81, 26), (86, 25), (86, 27), (92, 4), (93, 27), (95, 25), (95, 27), (98, 4),
|
||||
(102, 26), (102, 28), (104, 4), (104, 26), (104, 28), (165, 14), (166, 15), (171, 20), (172, 21),
|
||||
(173, 22), (175, 24), (176, 25), (177, 26), (179, 28), (180, 29), (189, 6)],
|
||||
"Pumpkin Zone 2":
|
||||
[(34, 26), (40, 21), (41, 21), (42, 21), (43, 21), (48, 20), (49, 20), (50, 20), (50, 41),
|
||||
(51, 20), (51, 41), (52, 41), (53, 41), (54, 41), (56, 21), (57, 21), (58, 21), (59, 21),
|
||||
(61, 41), (62, 41), (64, 20), (65, 20), (66, 20), (67, 20), (114, 36), (115, 35), (115, 36),
|
||||
(116, 34), (116, 35), (116, 36), (132, 20), (132, 21), (132, 22), (132, 23), (132, 24),
|
||||
(144, 23), (169, 27)],
|
||||
"Pumpkin Zone 3":
|
||||
[(18, 26), (20, 26), (22, 26), (24, 26), (26, 26), (31, 18), (32, 18), (33, 18), (34, 18),
|
||||
(35, 18), (36, 18), (37, 18), (38, 18), (39, 18), (40, 18), (41, 18), (42, 18), (48, 24),
|
||||
(52, 20), (52, 24), (56, 24), (87, 27), (88, 27), (89, 27), (90, 27), (94, 27), (95, 27),
|
||||
(96, 27), (97, 27), (101, 27), (102, 27), (103, 27), (104, 27), (104, 42), (108, 27), (109, 27),
|
||||
(110, 27), (111, 27), (115, 27), (116, 27), (117, 27), (118, 27), (134, 35), (134, 41),
|
||||
(135, 35), (135, 41), (136, 35), (136, 41), (137, 35), (137, 41), (138, 35), (138, 41),
|
||||
(139, 35), (139, 41), (140, 41), (225, 38), (226, 37), (227, 36), (227, 37), (227, 38),
|
||||
(227, 39), (227, 40), (227, 41), (228, 37), (229, 38)],
|
||||
"Pumpkin Zone Secret Course 1":
|
||||
[(14, 15), (16, 9), (16, 10), (16, 11), (16, 12), (16, 13), (16, 14), (16, 15), (16, 16),
|
||||
(17, 9), (17, 10), (17, 11), (17, 12), (17, 13), (17, 14), (17, 15), (17, 16), (18, 9),
|
||||
(18, 10), (18, 11), (18, 12), (18, 13), (18, 14), (18, 15), (18, 16), (19, 9), (19, 10),
|
||||
(19, 11), (19, 12), (19, 13), (19, 14), (19, 15), (19, 16), (20, 9), (20, 10), (20, 11),
|
||||
(20, 12), (20, 13), (20, 14), (20, 15), (20, 16), (21, 9), (21, 10), (21, 11), (21, 12),
|
||||
(21, 13), (21, 14), (21, 15), (21, 16), (22, 9), (22, 10), (22, 11), (22, 12), (22, 13),
|
||||
(22, 14), (22, 15), (22, 16), (23, 9), (23, 10), (23, 11), (23, 12), (23, 13), (23, 14),
|
||||
(23, 15), (23, 16), (24, 9), (24, 10), (24, 11), (24, 12), (24, 13), (24, 14), (24, 15),
|
||||
(24, 16), (25, 9), (25, 10), (25, 11), (25, 12), (25, 13), (25, 14), (25, 15), (25, 16),
|
||||
(26, 9), (26, 10), (26, 11), (26, 12), (26, 13), (26, 14), (26, 15), (27, 16), (28, 9),
|
||||
(28, 10), (28, 11), (28, 12), (28, 13), (28, 14), (28, 15), (28, 16), (29, 9), (29, 10),
|
||||
(29, 11), (29, 12), (29, 13), (29, 14), (29, 15), (29, 16), (30, 9), (30, 10), (30, 11),
|
||||
(30, 12), (30, 13), (30, 14), (30, 15), (30, 16), (31, 9), (31, 10), (31, 11), (31, 12),
|
||||
(31, 13), (31, 14), (31, 15), (31, 16), (32, 9), (32, 10), (32, 11), (32, 12), (32, 13),
|
||||
(32, 14), (32, 15), (32, 16), (33, 9), (33, 10), (33, 11), (33, 12), (33, 13), (33, 14),
|
||||
(33, 15), (33, 16), (34, 9), (34, 10), (34, 11), (34, 12), (34, 13), (34, 14), (34, 15),
|
||||
(34, 16), (35, 9), (35, 10), (35, 11), (35, 12), (35, 13), (35, 14), (35, 15), (35, 16),
|
||||
(36, 9), (36, 10), (36, 11), (36, 12), (36, 13), (36, 14), (36, 15), (36, 16), (37, 9),
|
||||
(37, 10), (37, 11), (37, 12), (37, 13), (37, 14), (37, 15), (37, 16), (39, 16), (40, 9),
|
||||
(40, 10), (40, 11), (40, 12), (40, 13), (40, 14), (40, 15), (40, 16), (41, 9), (41, 10),
|
||||
(41, 11), (41, 12), (41, 13), (41, 14), (41, 15), (41, 16), (42, 9), (42, 10), (42, 11),
|
||||
(42, 12), (42, 13), (42, 14), (42, 15), (42, 16), (43, 9), (43, 10), (43, 11), (43, 12),
|
||||
(43, 13), (43, 14), (43, 15), (43, 16), (44, 9), (44, 10), (44, 11), (44, 12), (44, 13),
|
||||
(44, 14), (44, 15), (44, 16), (45, 9), (45, 10), (45, 11), (45, 12), (45, 13), (45, 14),
|
||||
(45, 15), (45, 16), (46, 9), (46, 10), (46, 11), (46, 12), (46, 13), (46, 14), (46, 15),
|
||||
(46, 16), (47, 9), (47, 10), (47, 11), (47, 12), (47, 13), (47, 14), (47, 15), (47, 16),
|
||||
(48, 9), (48, 10), (48, 11), (48, 12), (48, 13), (48, 14), (48, 15), (48, 16), (49, 9),
|
||||
(49, 10), (49, 11), (49, 12), (49, 13), (49, 14), (49, 15), (49, 16), (52, 9), (52, 10),
|
||||
(52, 11), (52, 12), (52, 13), (52, 14), (52, 15), (52, 16), (53, 9), (53, 10), (53, 11),
|
||||
(53, 12), (53, 13), (53, 14), (53, 15), (53, 16), (54, 9), (54, 10), (54, 11), (54, 12),
|
||||
(54, 13), (54, 14), (54, 15), (54, 16), (55, 9), (55, 10), (55, 11), (55, 12), (55, 13),
|
||||
(55, 14), (55, 15), (55, 16), (56, 9), (56, 10), (56, 11), (56, 12), (56, 13), (56, 14),
|
||||
(56, 15), (56, 16), (57, 9), (57, 10), (57, 11), (57, 12), (57, 13), (57, 14), (57, 15),
|
||||
(57, 16), (58, 9), (58, 10), (58, 11), (58, 12), (58, 13), (58, 14), (58, 15), (58, 16),
|
||||
(59, 9), (59, 10), (59, 11), (59, 12), (59, 13), (59, 14), (59, 15), (59, 16), (60, 9),
|
||||
(60, 10), (60, 11), (60, 12), (60, 13), (60, 14), (60, 15), (60, 16), (61, 9), (61, 10),
|
||||
(61, 11), (61, 12), (61, 13), (61, 14), (61, 15), (61, 16), (64, 9), (64, 10), (64, 11),
|
||||
(64, 12), (64, 13), (64, 14), (64, 15), (64, 16), (65, 9), (65, 10), (65, 11), (65, 12),
|
||||
(65, 13), (65, 14), (65, 15), (65, 16), (66, 9), (66, 10), (66, 11), (66, 12), (66, 13),
|
||||
(66, 14), (66, 15), (66, 16), (67, 9), (67, 10), (67, 11), (67, 12), (67, 13), (67, 14),
|
||||
(67, 15), (67, 16), (68, 9), (68, 10), (68, 11), (68, 12), (68, 13), (68, 14), (68, 15),
|
||||
(68, 16), (69, 9), (69, 10), (69, 11), (69, 12), (69, 13), (69, 14), (69, 15), (69, 16),
|
||||
(70, 9), (70, 10), (70, 11), (70, 12), (70, 13), (70, 14), (70, 15), (70, 16), (71, 9),
|
||||
(71, 10), (71, 11), (71, 12), (71, 13), (71, 14), (71, 15), (71, 16), (72, 9), (72, 10),
|
||||
(72, 11), (72, 12), (72, 13), (72, 14), (72, 15), (72, 16), (73, 9), (73, 10), (73, 11),
|
||||
(73, 12), (73, 13), (73, 14), (73, 15), (73, 16)],
|
||||
"Pumpkin Zone Secret Course 2":
|
||||
[(12, 6), (72, 7), (73, 7), (80, 7), (81, 7), (85, 8), (90, 10), (91, 10), (94, 10),
|
||||
(95, 10), (98, 9), (99, 9), (102, 9), (103, 9)],
|
||||
"Pumpkin Zone 4":
|
||||
[(18, 28), (19, 28), (20, 28), (21, 28), (83, 37), (83, 38), (84, 37), (84, 38), (85, 37),
|
||||
(85, 38), (85, 39), (86, 37), (87, 37), (87, 40), (88, 37), (88, 38), (88, 39), (88, 40),
|
||||
(89, 37), (89, 38), (89, 39), (89, 40), (90, 37), (90, 38), (91, 37), (91, 38), (92, 37),
|
||||
(92, 38), (92, 39), (92, 40), (93, 23), (93, 37), (93, 40), (94, 23), (94, 37), (94, 40),
|
||||
(103, 23), (104, 23), (113, 23), (114, 23), (169, 30), (170, 28), (170, 30), (171, 26),
|
||||
(171, 28), (171, 30), (172, 24), (172, 26), (172, 28), (172, 30), (173, 24), (173, 26),
|
||||
(173, 28), (173, 30), (174, 26), (174, 28), (174, 30), (175, 28), (175, 30), (176, 30),
|
||||
(198, 37), (198, 38), (199, 37), (199, 38), (204, 37), (204, 38), (205, 37), (205, 38),
|
||||
(210, 37), (210, 38), (211, 37), (211, 38), (216, 37), (216, 38), (217, 37), (217, 38)],
|
||||
"Macro Zone 1":
|
||||
[(22, 32), (22, 33), (22, 34), (23, 20), (23, 21), (23, 22), (23, 38), (23, 39), (23, 40), (24, 26),
|
||||
(24, 27), (24, 28), (39, 42), (40, 41), (41, 40), (42, 39), (49, 43), (62, 42), (62, 43), (62, 44),
|
||||
(68, 42), (68, 43), (68, 44), (75, 42), (75, 43), (75, 44), (84, 40), (84, 41), (84, 42), (87, 39),
|
||||
(89, 42), (89, 43), (89, 44), (107, 42), (108, 42), (109, 42), (118, 42), (119, 42), (120, 42),
|
||||
(121, 42), (122, 42), (128, 42), (128, 43), (130, 42), (130, 43), (134, 42), (134, 43), (140, 42),
|
||||
(141, 42), (142, 42), (143, 42), (144, 42), (154, 42), (155, 42), (156, 42), (163, 22), (163, 23),
|
||||
(164, 21), (164, 22), (164, 23), (165, 22), (165, 23), (166, 21), (166, 22), (166, 23), (167, 22),
|
||||
(167, 23), (168, 21), (168, 22), (168, 23), (169, 22), (169, 23), (170, 21), (170, 22), (170, 23),
|
||||
(171, 22), (171, 23), (177, 40), (182, 40), (189, 41), (189, 42), (189, 43), (189, 44), (189, 45),
|
||||
(205, 46), (206, 46), (208, 37), (209, 37), (212, 41), (213, 41), (214, 41), (215, 41), (216, 41),
|
||||
(220, 37), (221, 37), (224, 46), (225, 46), (234, 43), (246, 38), (246, 39), (246, 40), (246, 41),
|
||||
(246, 42), (246, 43)],
|
||||
"Macro Zone 2":
|
||||
[(18, 28), (19, 27), (22, 28), (23, 27), (25, 26), (27, 27), (28, 27), (31, 27), (32, 27), (41, 29),
|
||||
(42, 29), (43, 29), (55, 29), (57, 29), (60, 29), (62, 29), (69, 27), (70, 27), (71, 27), (74, 27),
|
||||
(75, 27), (76, 27), (79, 27), (80, 27), (81, 27), (84, 27), (85, 27), (86, 27), (99, 40),
|
||||
(137, 37), (180, 40), (181, 8), (182, 8), (183, 8), (184, 8), (185, 8), (186, 8), (187, 8),
|
||||
(188, 8), (189, 8), (190, 8), (191, 8), (192, 8), (193, 8), (194, 8), (195, 8), (196, 8), (197, 8),
|
||||
(198, 8), (199, 8), (200, 8), (201, 8), (202, 8), (204, 8), (205, 8), (206, 8), (207, 8), (208, 8),
|
||||
(209, 8), (210, 8), (211, 8), (212, 8), (213, 8), (215, 8), (216, 8), (217, 8), (217, 11),
|
||||
(218, 8), (218, 9), (218, 10), (218, 11), (218, 12), (219, 11)],
|
||||
"Macro Zone 3":
|
||||
[(24, 23), (37, 28), (38, 28), (39, 28), (40, 28), (57, 30), (58, 30), (59, 30), (65, 17), (65, 18),
|
||||
(65, 19), (66, 17), (66, 18), (66, 19), (67, 17), (67, 18), (67, 19), (68, 17), (68, 18), (68, 19),
|
||||
(69, 17), (69, 18), (69, 19), (70, 17), (70, 18), (70, 19), (73, 17), (73, 18), (73, 19), (74, 17),
|
||||
(74, 18), (74, 19), (75, 17), (75, 18), (75, 19), (76, 17), (76, 18), (76, 19), (77, 17), (77, 18),
|
||||
(77, 19), (78, 17), (78, 18), (78, 19), (85, 43), (95, 17), (95, 18), (95, 19), (95, 20), (96, 17),
|
||||
(96, 18), (96, 19), (96, 20), (97, 17), (97, 18), (97, 19), (97, 20), (98, 17), (98, 18), (98, 19),
|
||||
(98, 20), (100, 17), (100, 18), (100, 19), (100, 20), (101, 17), (101, 18), (101, 19), (101, 20),
|
||||
(103, 42), (104, 42), (105, 42), (106, 42), (107, 42), (116, 42), (117, 42), (118, 42), (119, 42),
|
||||
(120, 42), (121, 42), (122, 42), (129, 19), (131, 42), (132, 42), (133, 42), (134, 42), (135, 42),
|
||||
(136, 42), (165, 21), (165, 22), (166, 21), (166, 22), (171, 21), (171, 22), (172, 21), (172, 22),
|
||||
(176, 20), (176, 21), (176, 22), (179, 20), (179, 21), (179, 22), (183, 21), (183, 22), (184, 21),
|
||||
(184, 22), (194, 27), (194, 28), (197, 27), (197, 28), (200, 27), (200, 28), (203, 25), (203, 27),
|
||||
(203, 28), (205, 18), (205, 19), (206, 18), (206, 19)],
|
||||
"Macro Zone 4":
|
||||
[(16, 28), (34, 28), (39, 28), (39, 29), (39, 30), (40, 28), (40, 29), (40, 30), (41, 28), (41, 29),
|
||||
(41, 30), (62, 29), (63, 29), (64, 29), (65, 29), (66, 29), (67, 29), (68, 29), (69, 29), (81, 17),
|
||||
(81, 18), (82, 17), (82, 18), (83, 17), (83, 18), (84, 17), (84, 18), (85, 17), (85, 18), (86, 17),
|
||||
(86, 18), (87, 17), (87, 18), (87, 28), (88, 17), (88, 18), (114, 28), (144, 22), (146, 27),
|
||||
(146, 28), (147, 27), (147, 28), (148, 27), (148, 28), (149, 27), (149, 28), (150, 27), (150, 28),
|
||||
(151, 27), (151, 28), (152, 27), (152, 28), (153, 27), (153, 28), (154, 27), (154, 28), (155, 27),
|
||||
(155, 28), (156, 27), (156, 28), (157, 27), (157, 28), (158, 27), (158, 28)],
|
||||
"Macro Zone Secret Course":
|
||||
[(21, 24), (70, 27), (70, 28), (71, 27), (71, 28), (72, 27), (72, 28), (73, 27), (73, 28),
|
||||
(74, 27), (74, 28), (75, 27), (75, 28), (76, 24), (76, 25), (76, 26), (76, 27), (76, 28),
|
||||
(77, 24), (77, 25), (77, 26), (77, 27), (77, 28), (78, 24), (78, 25), (78, 26), (78, 27),
|
||||
(78, 28), (79, 24), (79, 25), (79, 26), (79, 27), (80, 25), (80, 26), (81, 25), (81, 26),
|
||||
(88, 25), (108, 30)],
|
||||
"Mario's Castle":
|
||||
[(7, 25), (160, 44), (167, 28), (247, 26)],
|
||||
}
|
||||
powerup_coords = {
|
||||
"Mushroom Zone": [(42, 28), (151, 28), (152, 28), (188, 28)],
|
||||
"Scenic Course": [(39, 28), (72, 28), (117, 28)],
|
||||
"Tree Zone 1": [(119, 28), (152, 28)],
|
||||
"Tree Zone 2": [(43, 10), (61, 9), (51, 28), (130, 7), (182, 3), (136, 43)],
|
||||
"Tree Zone Secret Course": [(17, 23), (100, 23), (159, 23)],
|
||||
"Tree Zone 3": [(26, 40), (77, 24)],
|
||||
"Tree Zone 4": [(28, 27), (105, 25), (136, 22), (171, 10)],
|
||||
"Tree Zone 5": [(123, 39), (138, 39), (146, 36)],
|
||||
"Pumpkin Zone 1": [(23, 12), (72, 27), (98, 4), (189, 6)],
|
||||
"Pumpkin Zone 2": [(144, 23)],
|
||||
"Pumpkin Zone Secret Course 1": [(14, 15)],
|
||||
"Pumpkin Zone 3": [(52, 20), (104, 42), (139, 35), (140, 41)],
|
||||
"Pumpkin Zone Secret Course 2": [(12, 6), (85, 8)],
|
||||
"Pumpkin Zone 4": [(83, 38), (94, 40), (104, 23)],
|
||||
"Mario Zone 1": [(18, 44), (98, 38), (145, 42), (164, 20)],
|
||||
"Mario Zone 2": [(81, 27)],
|
||||
"Mario Zone 3": [(54, 25), (100, 27), (134, 18), (189, 44), (214, 27)],
|
||||
"Mario Zone 4": [(20, 25), (124, 12), (179, 12)],
|
||||
"Turtle Zone 1": [(22, 34), (56, 33), (57, 33), (143, 34), (189, 44)],
|
||||
"Turtle Zone 2": [(82, 35), (139, 39), ],
|
||||
"Turtle Zone Secret Course": [(19, 27), (53, 27), (64, 26), (87, 27), (121, 28), (122, 28), (123, 28)],
|
||||
"Turtle Zone 3": [(39, 24), (94, 26), (152, 23)],
|
||||
"Hippo Zone": [(3, 3), (2, 20), (15, 26), (16, 26), (29, 21), (86, 22), (137, 13)],
|
||||
"Space Zone 1": [(75, 24), (114, 22)],
|
||||
# "Space Zone Secret Course": [],
|
||||
"Space Zone 2": [(64, 3), (102, 8), (120, 5), (207, 6), (210, 9), (248, 10)],
|
||||
"Macro Zone 1": [(49, 43), (87, 39), (177, 40), (164, 21), (166, 21), (170, 21), (234, 43)],
|
||||
"Macro Zone 2": [(25, 26), (99, 40), (137, 37), (180, 40)],
|
||||
"Macro Zone 3": [(24, 23), (85, 43), (129, 19), (203, 25)],
|
||||
"Macro Zone 4": [(16, 28), (87, 28), (144, 22)],
|
||||
"Macro Zone Secret Course": [(21, 24), (88, 25), (108, 30)],
|
||||
"Mario's Castle": [(7, 25), (160, 44), (167, 28), (247, 26)]
|
||||
}
|
||||
for zone, coords_list in powerup_coords.items():
|
||||
for coords in coords_list:
|
||||
coins_coords[zone].remove(coords)
|
||||
|
||||
location_name_to_id = {location_name: ID for ID, location_name in enumerate(locations, START_IDS)}
|
||||
loc_id = START_IDS + len(locations)
|
||||
for level, coin_coords in coins_coords.items():
|
||||
for i in range(1, len(coin_coords) + 1):
|
||||
location_name_to_id[f"{level} - {i} Coin{'s' if i > 1 else ''}"] = loc_id
|
||||
loc_id += 1
|
||||
|
||||
# eligible_levels = [0, 1, 2, 3, 5, 8, 9, 11, 13, 14, 16, 19, 20, 22, 23, 25, 30, 31]
|
||||
|
||||
level_id_to_name = {
|
||||
0: "Mushroom Zone", 25: "Scenic Course", 1: "Tree Zone 1", 2: "Tree Zone 2", 4: "Tree Zone 3", 3: "Tree Zone 4",
|
||||
5: "Tree Zone 5", 29: "Tree Zone Secret Course", 17: "Hippo Zone", 18: "Space Zone 1",
|
||||
28: "Space Zone Secret Course", 19: "Space Zone 2", 20: "Macro Zone 1", 21: "Macro Zone 2", 22: "Macro Zone 3",
|
||||
23: "Macro Zone 4", 30: "Macro Zone Secret Course", 6: "Pumpkin Zone 1", 7: "Pumpkin Zone 2",
|
||||
8: "Pumpkin Zone 3", 9: "Pumpkin Zone 4", 27: "Pumpkin Zone Secret Course 1", 31: "Pumpkin Zone Secret Course 2",
|
||||
10: "Mario Zone 1", 11: "Mario Zone 2", 12: "Mario Zone 3", 13: "Mario Zone 4", 14: "Turtle Zone 1",
|
||||
15: "Turtle Zone 2", 16: "Turtle Zone 3", 26: "Turtle Zone Secret Course", 24: "Mario's Castle"
|
||||
}
|
||||
|
||||
level_name_to_id = {name: level_id for level_id, name in level_id_to_name.items()}
|
||||
|
||||
auto_scroll_max = {
|
||||
"Mushroom Zone": 84,
|
||||
"Hippo Zone": 160,
|
||||
"Tree Zone 1": 87,
|
||||
"Tree Zone 2": 68,
|
||||
"Tree Zone 3": 4,
|
||||
"Tree Zone 4": 28,
|
||||
"Tree Zone 5": 22,
|
||||
"Space Zone 1": 72,
|
||||
"Space Zone 2": 113,
|
||||
"Space Zone Secret Course": 96,
|
||||
"Macro Zone 1": 74,
|
||||
"Macro Zone 2": 27,
|
||||
"Macro Zone 3": 63,
|
||||
"Macro Zone 4": 59,
|
||||
"Pumpkin Zone 1": (0, 12),
|
||||
"Pumpkin Zone 2": 23,
|
||||
"Pumpkin Zone 3": 50,
|
||||
"Pumpkin Zone 4": 45,
|
||||
"Pumpkin Zone Secret Course 1": 172,
|
||||
"Mario Zone 1": 68,
|
||||
"Mario Zone 3": 29,
|
||||
"Mario Zone 4": 60,
|
||||
"Turtle Zone 1": 66,
|
||||
"Turtle Zone 2": 8,
|
||||
}
|
||||
608
worlds/marioland2/logic.py
Normal file
608
worlds/marioland2/logic.py
Normal file
@@ -0,0 +1,608 @@
|
||||
from .locations import level_name_to_id
|
||||
|
||||
|
||||
def is_auto_scroll(state, player, level):
|
||||
level_id = level_name_to_id[level]
|
||||
if state.has_any(["Cancel Auto Scroll", f"Cancel Auto Scroll - {level}"], player):
|
||||
return False
|
||||
return state.multiworld.worlds[player].auto_scroll_levels[level_id] > 0
|
||||
|
||||
|
||||
def has_pipe_right(state, player):
|
||||
return state.has_any(["Pipe Traversal - Right", "Pipe Traversal"], player)
|
||||
|
||||
|
||||
def has_pipe_left(state, player):
|
||||
return state.has_any(["Pipe Traversal - Left", "Pipe Traversal"], player)
|
||||
|
||||
|
||||
def has_pipe_down(state, player):
|
||||
return state.has_any(["Pipe Traversal - Down", "Pipe Traversal"], player)
|
||||
|
||||
|
||||
def has_pipe_up(state, player):
|
||||
return state.has_any(["Pipe Traversal - Up", "Pipe Traversal"], player)
|
||||
|
||||
|
||||
def has_level_progression(state, item, player, count=1):
|
||||
return state.count(item, player) + (state.count(item + " x2", player) * 2) >= count
|
||||
|
||||
|
||||
def mushroom_zone_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Mushroom Zone")
|
||||
reachable_coins = 38
|
||||
if state.has_any(["Mushroom", "Fire Flower"], player) or not auto_scroll:
|
||||
# Was able to get all but 1, being lenient.
|
||||
reachable_coins += 2
|
||||
if has_pipe_down(state, player):
|
||||
# There's 24 in each of the underground sections.
|
||||
# The first one requires missing some question mark blocks if auto scrolling (the last +4).
|
||||
# If you go in the second without pipe up, you can get everything except the last 5 plus the ones in the first
|
||||
# underground section.
|
||||
reachable_coins += 19
|
||||
if has_pipe_up(state, player) or not auto_scroll:
|
||||
reachable_coins += 5
|
||||
if has_pipe_up(state, player):
|
||||
reachable_coins += 20
|
||||
if not auto_scroll:
|
||||
reachable_coins += 4
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def tree_zone_1_coins(state, player, coins):
|
||||
return coins <= 87 or not is_auto_scroll(state, player, "Tree Zone 1")
|
||||
|
||||
|
||||
def tree_zone_2_normal_exit(state, player):
|
||||
return has_pipe_right(state, player) or state.has("Tree Zone 2 Midway Bell", player)
|
||||
|
||||
|
||||
def tree_zone_2_secret_exit(state, player):
|
||||
return has_pipe_right(state, player) and state.has("Carrot", player)
|
||||
|
||||
|
||||
def tree_zone_2_midway_bell(state, player):
|
||||
return has_pipe_right(state, player) or state.has("Tree Zone 2 Midway Bell", player)
|
||||
|
||||
|
||||
def tree_zone_2_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Tree Zone 2")
|
||||
reachable_coins = 18
|
||||
if has_pipe_right(state, player):
|
||||
reachable_coins += 38
|
||||
if state.has("Carrot", player):
|
||||
reachable_coins += 12
|
||||
if not auto_scroll:
|
||||
reachable_coins += 30
|
||||
elif state.has("Tree Zone 2 Midway Bell", player):
|
||||
reachable_coins = 30
|
||||
if not auto_scroll:
|
||||
reachable_coins += 8
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def tree_zone_3_normal_exit(state, player):
|
||||
return not is_auto_scroll(state, player, "Tree Zone 3")
|
||||
|
||||
|
||||
def tree_zone_3_coins(state, player, coins):
|
||||
if is_auto_scroll(state, player, "Tree Zone 3"):
|
||||
return coins <= 4
|
||||
if coins <= 19:
|
||||
return True
|
||||
elif state.has_any(["Mushroom", "Fire Flower"], player) and coins <= 21:
|
||||
return True
|
||||
return state.has("Carrot", player)
|
||||
|
||||
|
||||
def tree_zone_4_normal_exit(state, player):
|
||||
return has_pipe_down(state, player) and tree_zone_4_midway_bell(state, player)
|
||||
|
||||
|
||||
def tree_zone_4_midway_bell(state, player):
|
||||
return ((has_pipe_right(state, player) and has_pipe_up(state, player))
|
||||
or state.has("Tree Zone 4 Midway Bell", player))
|
||||
|
||||
|
||||
def tree_zone_4_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Tree Zone 4")
|
||||
reachable_coins = 0
|
||||
if has_pipe_up(state, player):
|
||||
reachable_coins += 14
|
||||
if has_pipe_right(state, player):
|
||||
reachable_coins += 4
|
||||
if has_pipe_down(state, player):
|
||||
reachable_coins += 10
|
||||
if not auto_scroll:
|
||||
reachable_coins += 46
|
||||
elif state.has("Tree Zone 4 Midway Bell", player):
|
||||
if not auto_scroll:
|
||||
if has_pipe_left(state, player):
|
||||
reachable_coins += 18
|
||||
if has_pipe_down(state, player):
|
||||
reachable_coins += 10
|
||||
if has_pipe_up(state, player):
|
||||
reachable_coins += 46
|
||||
elif has_pipe_down(state, player):
|
||||
reachable_coins += 10
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def tree_zone_5_boss(state, player):
|
||||
return has_pipe_right(state, player) and (has_pipe_up(state, player) or state.has("Carrot", player))
|
||||
|
||||
|
||||
def tree_zone_5_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Tree Zone 5")
|
||||
reachable_coins = 0
|
||||
# Not actually sure if these platforms can be randomized / can make the coin blocks unreachable from below
|
||||
if ((not state.multiworld.worlds[player].options.randomize_platforms)
|
||||
or state.has_any(["Mushroom", "Fire Flower"], player)):
|
||||
reachable_coins += 2
|
||||
if state.has_any(["Mushroom", "Fire Flower"], player):
|
||||
reachable_coins += 2
|
||||
if state.has("Carrot", player):
|
||||
reachable_coins += 18
|
||||
if has_pipe_up(state, player) and not auto_scroll:
|
||||
reachable_coins += 13
|
||||
elif has_pipe_up(state, player):
|
||||
reachable_coins += 13
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def pumpkin_zone_1_normal_exit(state, player):
|
||||
return pumpkin_zone_1_midway_bell(state, player)
|
||||
|
||||
|
||||
def pumpkin_zone_1_midway_bell(state, player):
|
||||
return ((has_pipe_down(state, player) and not is_auto_scroll(state, player, "Pumpkin Zone 1"))
|
||||
or state.has("Pumpkin Zone 1 Midway Bell", player))
|
||||
|
||||
|
||||
def pumpkin_zone_1_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Pumpkin Zone 1")
|
||||
if auto_scroll:
|
||||
return coins <= 12 and state.has("Pumpkin Zone 1 Midway Bell", player)
|
||||
reachable_coins = 0
|
||||
if state.has("Pumpkin Zone 1 Midway Bell", player) or has_pipe_down(state, player):
|
||||
reachable_coins += 38
|
||||
if has_pipe_up(state, player):
|
||||
reachable_coins += 2
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def pumpkin_zone_2_normal_exit(state, player):
|
||||
return has_pipe_down(state, player) and has_pipe_up(state, player) and has_pipe_right(state, player) and state.has(
|
||||
"Water Physics", player) and not is_auto_scroll(state, player, "Pumpkin Zone 2")
|
||||
|
||||
|
||||
def pumpkin_zone_2_secret_exit(state, player):
|
||||
return pumpkin_zone_2_normal_exit(state, player) and state.has_any(["Mushroom", "Fire Flower"], player)
|
||||
|
||||
|
||||
def pumpkin_zone_2_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Pumpkin Zone 2")
|
||||
reachable_coins = 17
|
||||
if has_pipe_down(state, player):
|
||||
if not auto_scroll:
|
||||
reachable_coins += 7
|
||||
if (has_pipe_up(state, player) or auto_scroll) and state.has("Water Physics", player):
|
||||
reachable_coins += 6
|
||||
if has_pipe_right(state, player) and not auto_scroll:
|
||||
reachable_coins += 1
|
||||
if state.has_any(["Mushroom", "Fire Flower"], player):
|
||||
reachable_coins += 5
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def pumpkin_zone_secret_course_1_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Pumpkin Zone Secret Course 1")
|
||||
# We'll be a bit forgiving. I was able to reach 43 while small.
|
||||
if coins <= 40:
|
||||
return True
|
||||
if state.has("Carrot", player):
|
||||
if auto_scroll:
|
||||
return coins <= 172
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def pumpkin_zone_3_secret_exit(state, player):
|
||||
return state.has("Carrot", player)
|
||||
|
||||
|
||||
def pumpkin_zone_3_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Pumpkin Zone 3")
|
||||
reachable_coins = 38
|
||||
if has_pipe_up(state, player) and ((not auto_scroll) or has_pipe_down(state, player)):
|
||||
reachable_coins += 12
|
||||
if has_pipe_down(state, player) and not auto_scroll:
|
||||
reachable_coins += 11
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def pumpkin_zone_4_boss(state, player):
|
||||
return has_pipe_right(state, player)
|
||||
|
||||
|
||||
def pumpkin_zone_4_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Pumpkin Zone 4")
|
||||
reachable_coins = 29
|
||||
if has_pipe_down(state, player):
|
||||
if auto_scroll:
|
||||
if has_pipe_up(state, player):
|
||||
reachable_coins += 16
|
||||
else:
|
||||
reachable_coins += 4
|
||||
else:
|
||||
reachable_coins += 28
|
||||
# both sets of coins are down, but you need pipe up to return to go down to the next set in one playthrough
|
||||
if has_pipe_up(state, player):
|
||||
reachable_coins += 16
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def mario_zone_1_normal_exit(state, player):
|
||||
if has_pipe_right(state, player):
|
||||
if state.has_any(["Mushroom", "Fire Flower", "Carrot", "Mario Zone 1 Midway Bell"], player):
|
||||
return True
|
||||
if is_auto_scroll(state, player, "Mario Zone 1"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def mario_zone_1_midway_bell(state, player):
|
||||
# It is possible to get as small mario, but it is a very precise jump and you will die afterward.
|
||||
return ((state.has_any(["Mushroom", "Fire Flower", "Carrot"], player) and has_pipe_right(state, player))
|
||||
or state.has("Mario Zone 1 Midway Bell", player))
|
||||
|
||||
|
||||
def mario_zone_1_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Mario Zone 1")
|
||||
reachable_coins = 0
|
||||
if has_pipe_right(state, player) or (has_pipe_left(state, player)
|
||||
and state.has("Mario Zone 1 Midway Bell", player) and not auto_scroll):
|
||||
reachable_coins += 32
|
||||
if has_pipe_right(state, player) and (state.has_any(["Mushroom", "Fire Flower", "Carrot"], player)
|
||||
or not auto_scroll):
|
||||
reachable_coins += 8
|
||||
# coins from end section. I was able to get 13 as small mario, giving some leniency
|
||||
if state.has("Carrot", player):
|
||||
reachable_coins += 28
|
||||
else:
|
||||
reachable_coins += 12
|
||||
if state.has("Fire Flower", player) and not auto_scroll:
|
||||
reachable_coins += 46
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def mario_zone_3_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Mario Zone 3")
|
||||
reachable_coins = 10
|
||||
if state.has("Carrot", player):
|
||||
reachable_spike_coins = 15
|
||||
else:
|
||||
sprites = state.multiworld.worlds[player].sprite_data["Mario Zone 3"]
|
||||
reachable_spike_coins = min(3, len({sprites[i]["sprite"] == "Claw Grabber" for i in (17, 18, 25)})
|
||||
+ state.has("Mushroom", player) + state.has("Fire Flower", player)) * 5
|
||||
reachable_coins += reachable_spike_coins
|
||||
if not auto_scroll:
|
||||
reachable_coins += 10
|
||||
if state.has("Fire Flower", player):
|
||||
reachable_coins += 22
|
||||
if auto_scroll:
|
||||
reachable_coins -= 3 + reachable_spike_coins
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def mario_zone_4_boss(state, player):
|
||||
return has_pipe_right(state, player)
|
||||
|
||||
|
||||
def mario_zone_4_coins(state, player, coins):
|
||||
return coins <= 60 or not is_auto_scroll(state, player, "Mario Zone 4")
|
||||
|
||||
|
||||
def not_blocked_by_sharks(state, player):
|
||||
sharks = [state.multiworld.worlds[player].sprite_data["Turtle Zone 1"][i]["sprite"]
|
||||
for i in (27, 28)].count("Shark")
|
||||
if state.has("Carrot", player) or not sharks:
|
||||
return True
|
||||
if sharks == 2:
|
||||
return state.has_all(["Mushroom", "Fire Flower"], player)
|
||||
if sharks == 1:
|
||||
return state.has_any(["Mushroom", "Fire Flower"], player)
|
||||
return False
|
||||
|
||||
|
||||
def turtle_zone_1_normal_exit(state, player):
|
||||
return not_blocked_by_sharks(state, player)
|
||||
|
||||
|
||||
def turtle_zone_1_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Turtle Zone 1")
|
||||
reachable_coins = 30
|
||||
if not_blocked_by_sharks(state, player):
|
||||
reachable_coins += 13
|
||||
if auto_scroll:
|
||||
reachable_coins -= 1
|
||||
if state.has("Water Physics", player) or state.has("Carrot", player):
|
||||
reachable_coins += 10
|
||||
if state.has("Carrot", player):
|
||||
reachable_coins += 24
|
||||
if auto_scroll:
|
||||
reachable_coins -= 10
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def turtle_zone_2_normal_exit(state, player):
|
||||
return (has_pipe_up(state, player) and has_pipe_down(state, player) and has_pipe_right(state, player) and
|
||||
has_pipe_left(state, player) and state.has("Water Physics", player)
|
||||
and not is_auto_scroll(state, player, "Turtle Zone 2"))
|
||||
|
||||
|
||||
def turtle_zone_2_secret_exit(state, player):
|
||||
return (has_pipe_up(state, player) and state.has("Water Physics", player)
|
||||
and not is_auto_scroll(state, player, "Turtle Zone 2"))
|
||||
|
||||
|
||||
def turtle_zone_2_midway_bell(state, player):
|
||||
return ((state.has("Water Physics", player) and not is_auto_scroll(state, player, "Turtle Zone 2"))
|
||||
or state.has("Turtle Zone 2 Midway Bell", player))
|
||||
|
||||
|
||||
def turtle_zone_2_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Turtle Zone 2")
|
||||
reachable_coins = 2
|
||||
if auto_scroll:
|
||||
if state.has("Water Physics", player):
|
||||
reachable_coins += 6
|
||||
else:
|
||||
reachable_coins += 2
|
||||
if state.has("Water Physics", player):
|
||||
reachable_coins += 20
|
||||
elif state.has("Turtle Zone 2 Midway Bell", player):
|
||||
reachable_coins += 4
|
||||
if (has_pipe_right(state, player) and has_pipe_down(state, player)
|
||||
and state.has_any(["Water Physics", "Turtle Zone 2 Midway Bell"], player)):
|
||||
reachable_coins += 1
|
||||
if has_pipe_left(state, player) and has_pipe_up(state, player):
|
||||
reachable_coins += 1
|
||||
if state.has("Water Physics", player):
|
||||
reachable_coins += 1
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def turtle_zone_secret_course_normal_exit(state, player):
|
||||
return state.has_any(["Fire Flower", "Carrot"], player)
|
||||
|
||||
|
||||
def turtle_zone_secret_course_coins(state, player, coins):
|
||||
reachable_coins = 53
|
||||
if state.has("Carrot", player):
|
||||
reachable_coins += 44
|
||||
elif state.has("Fire Flower", player):
|
||||
reachable_coins += 36 # was able to get 38, some leniency
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def turtle_zone_3_boss(state, player):
|
||||
return has_pipe_right(state, player)
|
||||
|
||||
|
||||
def turtle_zone_3_coins(state, player, coins):
|
||||
return state.has_any(["Water Physics", "Mushroom", "Fire Flower", "Carrot"], player) or coins <= 51
|
||||
|
||||
|
||||
def hippo_zone_normal_or_secret_exit(state, player):
|
||||
return (state.has_any(["Hippo Bubble", "Water Physics"], player)
|
||||
or (state.has("Carrot", player)
|
||||
and not is_auto_scroll(state, player, "Hippo Zone")))
|
||||
|
||||
|
||||
def hippo_zone_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Hippo Zone")
|
||||
# This is all somewhat forgiving.
|
||||
reachable_coins = 4
|
||||
if auto_scroll:
|
||||
if state.has("Hippo Bubble", player):
|
||||
reachable_coins = 160
|
||||
elif state.has("Carrot", player):
|
||||
reachable_coins = 90
|
||||
elif state.has("Water Physics", player):
|
||||
reachable_coins = 28
|
||||
else:
|
||||
if state.has_any(["Water Physics", "Hippo Bubble", "Carrot"], player):
|
||||
reachable_coins += 108
|
||||
if state.has_any(["Mushroom", "Fire Flower", "Hippo Bubble"], player):
|
||||
reachable_coins += 6
|
||||
if state.has_all(["Fire Flower", "Water Physics"], player):
|
||||
reachable_coins += 1
|
||||
if state.has("Hippo Bubble", player):
|
||||
reachable_coins += 52
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def space_zone_1_normal_exit(state, player):
|
||||
# It is possible, however tricky, to beat the Moon Stage without Carrot or Space Physics.
|
||||
# However, it requires somewhat precisely jumping off enemies. Enemy shuffle may make this impossible.
|
||||
# Instead, I will just always make one or the other required, since it is difficult without them anyway.
|
||||
return state.has_any(["Space Physics", "Carrot"], player)
|
||||
|
||||
|
||||
def space_zone_1_secret_exit(state, player):
|
||||
# One or the other is actually necessary for the secret exit.
|
||||
return state.has_any(["Space Physics", "Carrot"], player) and not is_auto_scroll(state, player, "Space Zone 1")
|
||||
|
||||
|
||||
def space_zone_1_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Space Zone 1")
|
||||
if auto_scroll:
|
||||
reachable_coins = 12
|
||||
if state.has_any(["Carrot", "Space Physics"], player):
|
||||
reachable_coins += 20
|
||||
# If you have Space Physics, you can't make it up to the upper section. We have to assume you might have it,
|
||||
# so the coins up there must be out of logic if there is auto scrolling.
|
||||
if state.has("Space Physics", player):
|
||||
reachable_coins += 40
|
||||
return coins <= reachable_coins
|
||||
return (coins <= 21 or (coins <= 50 and state.has_any(["Mushroom", "Fire Flower"], player))
|
||||
or state.has_any(["Carrot", "Space Physics"], player))
|
||||
|
||||
|
||||
def space_zone_2_midway_bell(state, player):
|
||||
return state.has_any(["Space Physics", "Space Zone 2 Midway Bell", "Mushroom", "Fire Flower", "Carrot"], player)
|
||||
|
||||
|
||||
def space_zone_2_boss(state, player):
|
||||
if has_pipe_right(state, player):
|
||||
if state.has("Space Physics", player):
|
||||
return True
|
||||
if (state.has("Space Zone 2 Midway Bell", player)
|
||||
or not state.multiworld.worlds[player].options.shuffle_midway_bells):
|
||||
# Reaching the midway bell without space physics requires taking damage once. Reaching the end pipe from the
|
||||
# midway bell also requires taking damage once.
|
||||
if state.has_any(["Mushroom", "Fire Flower", "Carrot"], player):
|
||||
return True
|
||||
else:
|
||||
# With no midway bell, you'll have to be able to take damage twice.
|
||||
if state.has("Mushroom", player) and state.has_any(["Fire Flower", "Carrot"], player):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def space_zone_2_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Space Zone 2")
|
||||
reachable_coins = 12
|
||||
if state.has_any(["Mushroom", "Fire Flower", "Carrot", "Space Physics"], player):
|
||||
reachable_coins += 15
|
||||
if state.has("Space Physics", player) or not auto_scroll:
|
||||
reachable_coins += 4 # last few bottom row question mark blocks that are hard to get when auto scrolling.
|
||||
if (state.has("Space Physics", player) or (
|
||||
state.has("Mushroom", player) and state.has_any(["Fire Flower", "Carrot"], player))):
|
||||
reachable_coins += 3
|
||||
if state.has("Space Physics", player):
|
||||
reachable_coins += 79
|
||||
if not auto_scroll:
|
||||
reachable_coins += 21
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def space_zone_secret_course_coins(state, player, coins):
|
||||
return coins <= 96 or not is_auto_scroll(state, player, "Space Zone Secret Course")
|
||||
|
||||
|
||||
def macro_zone_1_normal_exit(state, player):
|
||||
return has_pipe_down(state, player) or state.has("Macro Zone 1 Midway Bell", player)
|
||||
|
||||
|
||||
def macro_zone_1_secret_exit(state, player):
|
||||
return state.has("Fire Flower", player) and has_pipe_up(state, player) and macro_zone_1_midway_bell(state, player)
|
||||
|
||||
|
||||
def macro_zone_1_midway_bell(state, player):
|
||||
return has_pipe_down(state, player) or state.has("Macro Zone 1 Midway Bell", player)
|
||||
|
||||
|
||||
def macro_zone_1_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Macro Zone 1")
|
||||
reachable_coins = 0
|
||||
if has_pipe_down(state, player):
|
||||
reachable_coins += 69
|
||||
if auto_scroll:
|
||||
if state.has_any(["Mushroom", "Fire Flower"], player):
|
||||
reachable_coins += 5
|
||||
else:
|
||||
reachable_coins += 9
|
||||
if state.has("Fire Flower", player):
|
||||
reachable_coins += 19
|
||||
elif state.has("Macro Zone 1 Midway Bell", player):
|
||||
if auto_scroll:
|
||||
reachable_coins += 16
|
||||
if state.has_any(["Mushroom", "Fire Flower"], player):
|
||||
reachable_coins += 5
|
||||
else:
|
||||
reachable_coins += 67
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def macro_zone_secret_course_coins(state, player, coins):
|
||||
return state.has_any(["Mushroom", "Fire Flower"], player)
|
||||
|
||||
|
||||
def macro_zone_2_normal_exit(state, player):
|
||||
return (has_pipe_down(state, player) or state.has("Macro Zone 2 Midway Bell", player)) and state.has(
|
||||
"Water Physics", player) and has_pipe_up(state, player) and not is_auto_scroll(state, player, "Macro Zone 2")
|
||||
|
||||
|
||||
def macro_zone_2_midway_bell(state, player):
|
||||
return ((has_pipe_down(state, player) and state.has("Water Physics", player))
|
||||
or state.has("Macro Zone 2 Midway Bell", player))
|
||||
|
||||
|
||||
def macro_zone_2_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Macro Zone 2")
|
||||
if coins <= 27:
|
||||
return True
|
||||
if has_pipe_up(state, player) and state.has("Water Physics", player) and not auto_scroll:
|
||||
if has_pipe_down(state, player):
|
||||
return True
|
||||
if state.has("Macro Zone 2 Midway Bell", player):
|
||||
# Cannot return to the first section from the bell
|
||||
return coins <= 42
|
||||
return False
|
||||
|
||||
|
||||
def macro_zone_3_normal_exit(state, player):
|
||||
return ((has_pipe_down(state, player) and has_pipe_up(state, player))
|
||||
or state.has("Macro Zone 3 Midway Bell", player))
|
||||
|
||||
|
||||
def macro_zone_3_midway_bell(state, player):
|
||||
return macro_zone_3_normal_exit(state, player)
|
||||
|
||||
|
||||
def macro_zone_3_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Macro Zone 3")
|
||||
reachable_coins = 7
|
||||
if not auto_scroll:
|
||||
reachable_coins += 17
|
||||
if has_pipe_up(state, player) and has_pipe_down(state, player):
|
||||
if auto_scroll:
|
||||
reachable_coins += 56
|
||||
else:
|
||||
return True
|
||||
elif has_pipe_up(state, player):
|
||||
if auto_scroll:
|
||||
reachable_coins += 12
|
||||
else:
|
||||
reachable_coins += 36
|
||||
elif has_pipe_down(state, player):
|
||||
reachable_coins += 18
|
||||
if state.has("Macro Zone 3 - Midway Bell", player):
|
||||
reachable_coins = max(reachable_coins, 30)
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def macro_zone_4_boss(state, player):
|
||||
return has_pipe_right(state, player)
|
||||
|
||||
|
||||
def macro_zone_4_coins(state, player, coins):
|
||||
auto_scroll = is_auto_scroll(state, player, "Macro Zone 4")
|
||||
reachable_coins = 61
|
||||
if auto_scroll:
|
||||
reachable_coins -= 8
|
||||
if state.has("Carrot", player):
|
||||
reachable_coins += 6
|
||||
return coins <= reachable_coins
|
||||
|
||||
|
||||
def marios_castle_wario(state, player):
|
||||
return ((has_pipe_right(state, player) and has_pipe_left(state, player))
|
||||
or state.has("Mario's Castle Midway Bell", player))
|
||||
|
||||
|
||||
def marios_castle_midway_bell(state, player):
|
||||
return ((has_pipe_right(state, player) and has_pipe_left(state, player))
|
||||
or state.has("Mario's Castle Midway Bell", player))
|
||||
198
worlds/marioland2/options.py
Normal file
198
worlds/marioland2/options.py
Normal file
@@ -0,0 +1,198 @@
|
||||
from Options import Toggle, Choice, NamedRange, Range, PerGameCommonOptions, ItemsAccessibility
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class ShuffleGoldenCoins(Choice):
|
||||
"""
|
||||
Vanilla: Golden Coins are received when defeating bosses.
|
||||
Shuffle: Shuffle the Golden Coins into the item pool and make bosses location checks.
|
||||
Mario Coin Fragment Hunt: You start with all Golden Coins except the Mario Coin, which has been fragmented into many pieces.
|
||||
You will see a Golden Coin being received when defeating bosses regardless of whether you are actually getting a coin.
|
||||
"""
|
||||
display_name = "Shuffle Golden Coins"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_shuffle = 1
|
||||
option_mario_coin_fragment_hunt = 2
|
||||
|
||||
|
||||
class GoldenCoinsRequired(Range):
|
||||
"""
|
||||
Number of Golden Coins required to enter Mario's Castle. Ignored on Mario Coin Fragment Hunt.
|
||||
"""
|
||||
display_name = "Golden Coins Required"
|
||||
range_start = 0
|
||||
range_end = 6
|
||||
default = 6
|
||||
|
||||
|
||||
class MarioCoinFragmentPercentage(Range):
|
||||
"""
|
||||
Percentage of filler items to be replaced with Mario Coin Fragments. Note that the Coinsanity and Coinsanity
|
||||
Checks options will greatly impact the number of replaceable filler items.
|
||||
"""
|
||||
display_name = "Mario Coin Fragment Percentage"
|
||||
range_start = 1
|
||||
range_end = 50
|
||||
default = 20
|
||||
|
||||
|
||||
class MarioCoinFragmentsRequiredPercentage(Range):
|
||||
"""
|
||||
Percentage of the Mario Coins in the item pool that are required to put the Mario Coin together.
|
||||
"""
|
||||
display_name = "Mario Coin Fragments Required Percentage"
|
||||
range_start = 1
|
||||
range_end = 100
|
||||
default = 75
|
||||
|
||||
|
||||
class ShuffleMidwayBells(Toggle):
|
||||
"""
|
||||
Shuffle Midway Bells into the item pool. You can always start at the beginning of a level after obtaining the
|
||||
Midway Bell by holding SELECT while entering the level (until you load into the level).
|
||||
The Midway Bells in levels will trigger location checks whether this option is on or not, but they will only
|
||||
set the checkpoint if this is off, otherwise you must obtain the Midway Bell item from the item pool.
|
||||
"""
|
||||
display_name = "Shuffle Midway Bells"
|
||||
|
||||
|
||||
class MariosCastleMidwayBell(Toggle):
|
||||
"""
|
||||
Adds a Midway Bell to the final stage, just before the Wario fight.
|
||||
"""
|
||||
display_name = "Mario's Castle Midway Bell"
|
||||
|
||||
|
||||
class Coinsanity(Toggle):
|
||||
"""
|
||||
Shuffles the singular coins found freestanding and in question mark blocks into the item pool, and adds location
|
||||
checks made by obtaining a sufficient number of coins in particular levels within a single playthrough.
|
||||
"""
|
||||
display_name = "Coinsanity"
|
||||
|
||||
|
||||
class CoinsanityChecks(Range):
|
||||
"""
|
||||
Number of Coinsanity checks.
|
||||
A higher number means more checks, and smaller coin amounts per coin item in the item pool.
|
||||
If Accessibility is set to Full, auto-scroll levels may have a lower maximum count, which may lead to this
|
||||
value being limited.
|
||||
"""
|
||||
display_name = "Coinsanity Checks"
|
||||
range_start = 31
|
||||
range_end = 2599
|
||||
default = 150
|
||||
|
||||
|
||||
class DifficultyMode(Choice):
|
||||
"""
|
||||
Play in normal or easy mode. You can also start in Normal Mode with an "upgrade" to Easy Mode in the item pool,
|
||||
or start in Easy Mode with a Normal Mode "trap" in the item pool.
|
||||
"""
|
||||
display_name = "Difficulty Mode"
|
||||
option_normal = 0
|
||||
option_easy = 1
|
||||
option_normal_to_easy = 2
|
||||
option_easy_to_normal = 3
|
||||
default = 0
|
||||
|
||||
|
||||
class ShufflePipeTraversal(Choice):
|
||||
"""
|
||||
Single: Shuffle a Pipe Traversal item into the item pool, which is required to enter any pipes.
|
||||
Split: Shuffle 4 Pipe Traversal items, one required for entering pipes from each direction.
|
||||
Note that being unable to enter pipes is very limiting and affects nearly half of all levels.
|
||||
"""
|
||||
display_name = "Shuffle Pipe Traversal"
|
||||
option_off = 0
|
||||
option_single = 1
|
||||
option_split = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class RandomizeEnemies(Toggle):
|
||||
"""
|
||||
Randomize enemies throughout levels.
|
||||
"""
|
||||
display_name = "Randomize Enemies"
|
||||
|
||||
|
||||
class RandomizePlatforms(Toggle):
|
||||
"""
|
||||
Randomize platforms throughout levels.
|
||||
"""
|
||||
display_name = "Randomize Platforms"
|
||||
|
||||
|
||||
class AutoScrollChances(NamedRange):
|
||||
"""
|
||||
Chance per eligible level to be made into an auto scroll level. Can also set to Vanilla to leave them unchanged.
|
||||
"""
|
||||
display_name = "Auto Scroll Chance"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
special_range_names = {"vanilla": -1, "none": 0, "all": 100}
|
||||
default = -1
|
||||
|
||||
|
||||
class AutoScrollMode(Choice):
|
||||
"""
|
||||
Always: Any auto scroll levels will always auto-scroll.
|
||||
Global Trap Item: Auto scroll levels will only auto-scroll after obtaining the Auto Scroll trap item.
|
||||
Level Trap Items: As with Trap Item, but there is a separate trap item for each auto scroll level.
|
||||
Global Cancel Item: Auto Scroll levels will stop auto-scrolling after obtaining the Auto Scroll Cancel item.
|
||||
Level Cancel Items: As with Cancel Item, but there is a separate cancel item for each auto scroll level.
|
||||
Chaos: Each level will randomly always auto scroll, have an Auto Scroll Trap, or have an Auto Scroll Cancel item.
|
||||
The effects of Trap and Cancel items are permanent! If Accessibility is not set to Full,
|
||||
Traps may cause locations to become permanently unreachable.
|
||||
With individual level items, the number of auto scroll levels may be limited by the available space in the item
|
||||
pool.
|
||||
"""
|
||||
display_name = "Auto Scroll Mode"
|
||||
option_always = 0
|
||||
option_global_trap_item = 1
|
||||
option_level_trap_items = 2
|
||||
option_global_cancel_item = 3
|
||||
option_level_cancel_items = 4
|
||||
option_chaos = 5
|
||||
default = 0
|
||||
|
||||
|
||||
class RandomizeMusic(Toggle):
|
||||
"""
|
||||
Randomize the music that plays in levels and overworld areas.
|
||||
"""
|
||||
display_name = "Randomize Music"
|
||||
|
||||
|
||||
class EnergyLink(Toggle):
|
||||
"""
|
||||
All extra lives beyond 1 are transferred into the server's shared EnergyLink storage. If you drop to 0,
|
||||
1 will be replenished if there is sufficient energy stored.
|
||||
"""
|
||||
display_name = "Energy Link"
|
||||
default = 1
|
||||
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class SML2Options(PerGameCommonOptions):
|
||||
accessibility: ItemsAccessibility
|
||||
shuffle_golden_coins: ShuffleGoldenCoins
|
||||
required_golden_coins: GoldenCoinsRequired
|
||||
mario_coin_fragment_percentage: MarioCoinFragmentPercentage
|
||||
mario_coin_fragments_required_percentage: MarioCoinFragmentsRequiredPercentage
|
||||
coinsanity: Coinsanity
|
||||
coinsanity_checks: CoinsanityChecks
|
||||
shuffle_midway_bells: ShuffleMidwayBells
|
||||
marios_castle_midway_bell: MariosCastleMidwayBell
|
||||
shuffle_pipe_traversal: ShufflePipeTraversal
|
||||
auto_scroll_mode: AutoScrollMode
|
||||
auto_scroll_chances: AutoScrollChances
|
||||
difficulty_mode: DifficultyMode
|
||||
randomize_enemies: RandomizeEnemies
|
||||
randomize_platforms: RandomizePlatforms
|
||||
randomize_music: RandomizeMusic
|
||||
energy_link: EnergyLink
|
||||
146
worlds/marioland2/rom.py
Normal file
146
worlds/marioland2/rom.py
Normal file
@@ -0,0 +1,146 @@
|
||||
import hashlib
|
||||
import os
|
||||
import pkgutil
|
||||
|
||||
import Utils
|
||||
|
||||
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
|
||||
from settings import get_settings
|
||||
|
||||
from .rom_addresses import rom_addresses
|
||||
from .sprites import sprite_name_to_id
|
||||
|
||||
|
||||
def randomize_music(patch, random):
|
||||
# overworld
|
||||
overworld_music_tracks = [0x05, 0x06, 0x0D, 0x0E, 0x10, 0x12, 0x1B, 0x1C, 0x1E]
|
||||
random.shuffle(overworld_music_tracks)
|
||||
for i, track in zip([0x3004F, 0x3EA9B, 0x3D186, 0x3D52B, 0x3D401, 0x3D297, 0x3D840, 0x3D694, 0x3D758],
|
||||
overworld_music_tracks):
|
||||
patch.write_bytes(i, track)
|
||||
# levels
|
||||
for i in range(0x5619, 0x5899, 0x14):
|
||||
patch.write_bytes(i, random.choice([0x01, 0x0B, 0x11, 0x13, 0x14, 0x17, 0x1D, 0x1F, 0x28]))
|
||||
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
|
||||
patch = SuperMarioLand2ProcedurePatch(player=self.player, player_name=self.player_name)
|
||||
|
||||
patch.write_file("basepatch.bsdiff4", pkgutil.get_data(__name__, "basepatch.bsdiff4"))
|
||||
random = self.random
|
||||
|
||||
if self.options.marios_castle_midway_bell:
|
||||
# Remove Question Mark Block
|
||||
patch.write_bytes(0x4F012, 0x5D)
|
||||
# Fix level pointer to read midway bell flag
|
||||
patch.write_bytes(0x3E569, 0x18)
|
||||
patch.write_bytes(0x3E56A, 0x18)
|
||||
# Position and screen coordinates
|
||||
patch.write_bytes(0x383B, [0xD4, 0x01, 0x4D, 0x0A, 0xC0, 0x01, 0x50, 0x0A])
|
||||
|
||||
if self.options.coinsanity:
|
||||
# Add platform to return to start of Pumpkin Zone Secret Course 1
|
||||
patch.write_bytes(0x258B6, 0x3B)
|
||||
patch.write_bytes(0x258F8, 0x7a)
|
||||
patch.write_bytes(0x2594D, 0x67)
|
||||
patch.write_bytes(0x259A8, 0x68)
|
||||
patch.write_bytes(0x259A9, 0x60)
|
||||
|
||||
i = 0xe077
|
||||
for level, sprites in self.sprite_data.items():
|
||||
for sprite_data in sprites:
|
||||
sprite_id = sprite_name_to_id[sprite_data["sprite"]]
|
||||
data = [((sprite_id & 0b01000000) >> 2) | ((sprite_id & 0b00111000) << 2) | sprite_data["screen"],
|
||||
((sprite_id & 0b00000111) << 5) | sprite_data["x"],
|
||||
sprite_data["misc"] | sprite_data["y"]]
|
||||
patch.write_bytes(i, data)
|
||||
i += 3
|
||||
patch.write_bytes(i, 255)
|
||||
i += 1
|
||||
|
||||
if self.options.randomize_music:
|
||||
randomize_music(patch, random)
|
||||
|
||||
if self.options.shuffle_golden_coins:
|
||||
patch.write_bytes(rom_addresses["Coin_Shuffle"], 0x40)
|
||||
if self.options.shuffle_midway_bells:
|
||||
patch.write_bytes(rom_addresses["Disable_Midway_Bell"], 0xC9)
|
||||
|
||||
if self.options.coinsanity:
|
||||
for section in ("A", "B"):
|
||||
for i in range(0, 30):
|
||||
patch.write_bytes(rom_addresses[f"Coinsanity_{section}"] + i, 0x00)
|
||||
|
||||
star_count = max(len([loc for loc in self.multiworld.get_filled_locations() if loc.item.player == self.player
|
||||
and loc.item.name == "Super Star Duration Increase"]), 1)
|
||||
patch.write_bytes(rom_addresses["Star_Count"], star_count // 256)
|
||||
patch.write_bytes(rom_addresses["Star_Count"] + 1, star_count - (star_count // 256))
|
||||
if self.options.shuffle_golden_coins == "mario_coin_fragment_hunt":
|
||||
patch.write_bytes(rom_addresses["Coins_Required"], self.coin_fragments_required // 256)
|
||||
patch.write_bytes(rom_addresses["Coins_Required"] + 1, self.coin_fragments_required % 256)
|
||||
patch.write_bytes(rom_addresses["Required_Golden_Coins"], 6)
|
||||
else:
|
||||
patch.write_bytes(rom_addresses["Coins_Required"] + 1, self.options.required_golden_coins.value)
|
||||
patch.write_bytes(rom_addresses["Required_Golden_Coins"], self.options.required_golden_coins.value)
|
||||
patch.write_bytes(rom_addresses["Midway_Bells"], self.options.shuffle_midway_bells.value)
|
||||
patch.write_bytes(rom_addresses["Energy_Link"], self.options.energy_link.value)
|
||||
patch.write_bytes(rom_addresses["Difficulty_Mode"], self.options.difficulty_mode.value)
|
||||
patch.write_bytes(rom_addresses["Coin_Mode"], self.options.shuffle_golden_coins.value)
|
||||
|
||||
for level, i in enumerate(self.auto_scroll_levels):
|
||||
# We set 0 if no auto scroll or auto scroll trap, so it defaults to no auto scroll. 1 if always or cancel items.
|
||||
patch.write_bytes(rom_addresses["Auto_Scroll_Levels"] + level, max(0, i - 1))
|
||||
patch.write_bytes(rom_addresses["Auto_Scroll_Levels_B"] + level, i)
|
||||
|
||||
if self.options.energy_link:
|
||||
# start with 1 life if Energy Link is on so that you don't deposit lives at the start of the game.
|
||||
patch.write_bytes(rom_addresses["Starting_Lives"], 1)
|
||||
|
||||
rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0',
|
||||
'utf8')[:21]
|
||||
rom_name.extend([0] * (21 - len(rom_name)))
|
||||
patch.write_bytes(0x77777, rom_name)
|
||||
patch.write_file("tokens.bin", patch.get_token_binary())
|
||||
patch.write(os.path.join(output_directory,
|
||||
f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}"))
|
||||
|
||||
|
||||
class SuperMarioLand2ProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
hash = "a8413347d5df8c9d14f97f0330d67bce"
|
||||
patch_file_ending = ".apsml2"
|
||||
game = "Super Mario Land 2"
|
||||
result_file_ending = ".gb"
|
||||
procedure = [
|
||||
("apply_bsdiff4", ["basepatch.bsdiff4"]),
|
||||
("apply_tokens", ["tokens.bin"]),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
def write_bytes(self, offset, value):
|
||||
if isinstance(value, int):
|
||||
value = [value]
|
||||
self.write_token(APTokenTypes.WRITE, offset, bytes(value))
|
||||
|
||||
|
||||
def get_base_rom_bytes():
|
||||
file_name = get_base_rom_path()
|
||||
with open(file_name, "rb") as file:
|
||||
base_rom_bytes = bytes(file.read())
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if SuperMarioLand2ProcedurePatch.hash != basemd5.hexdigest():
|
||||
raise Exception("Supplied Base Rom does not match known MD5 for Super Mario Land 1.0. "
|
||||
"Get the correct game and version, then dump it")
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def get_base_rom_path():
|
||||
file_name = get_settings()["sml2_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
39
worlds/marioland2/rom_addresses.py
Normal file
39
worlds/marioland2/rom_addresses.py
Normal file
@@ -0,0 +1,39 @@
|
||||
rom_addresses = {
|
||||
"Space_Physics": 0x4e7,
|
||||
"Pipe_Traversal_A": 0x11a4,
|
||||
"Pipe_Traversal_SFX_A": 0x11a9,
|
||||
"Pipe_Traversal_B": 0x11d6,
|
||||
"Pipe_Traversal_SFX_B": 0x11e7,
|
||||
"Pipe_Traversal_C": 0x1226,
|
||||
"Pipe_Traversal_SFX_C": 0x123f,
|
||||
"Pipe_Traversal_D": 0x1256,
|
||||
"Pipe_Traversal_SFX_D": 0x125b,
|
||||
"Enable_Swim": 0x1d17,
|
||||
"Coinsanity_B": 0x1d86,
|
||||
"Auto_Scroll_Levels": 0x1f71,
|
||||
"Starting_Lives": 0x2920,
|
||||
"Get_Hurt_To_Big_Mario": 0x31c7,
|
||||
"Get_Mushroom_A": 0x345c,
|
||||
"Get_Fire_Flower_A": 0x346d,
|
||||
"Get_Carrot_A": 0x347e,
|
||||
"Invincibility_Star_A": 0x349e,
|
||||
"Invincibility_Star_B": 0x34a3,
|
||||
"Enable_Bubble": 0x34e5,
|
||||
"Coinsanity_A": 0x591f,
|
||||
"Coin_Shuffle": 0x304ce,
|
||||
"Required_Golden_Coins": 0x306e9,
|
||||
"Disable_Midway_Bell": 0x3ef1e,
|
||||
"Get_Carrot_C": 0x6092f,
|
||||
"Get_Mushroom_C": 0x60930,
|
||||
"Get_Fire_Flower_C": 0x60933,
|
||||
"Get_Mushroom_B": 0x60ddb,
|
||||
"Get_Carrot_B": 0x60de7,
|
||||
"Get_Fire_Flower_B": 0x60df3,
|
||||
"Coins_Required": 0x80139,
|
||||
"Difficulty_Mode": 0x8013b,
|
||||
"Star_Count": 0x8013c,
|
||||
"Midway_Bells": 0x8013e,
|
||||
"Energy_Link": 0x8013f,
|
||||
"Coin_Mode": 0x80140,
|
||||
"Auto_Scroll_Levels_B": 0x80141,
|
||||
}
|
||||
131
worlds/marioland2/sprite_randomizer.py
Normal file
131
worlds/marioland2/sprite_randomizer.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# Based on SML2R enemy and platform randomizer
|
||||
# # https://github.com/slashinfty/sml2r-node/blob/862128c73d336d6cbfbf6290c09f3eff103688e8/src/index.ts#L284
|
||||
|
||||
def randomize_enemies(sprite_data, random):
|
||||
for level, level_sprite_data in sprite_data.items():
|
||||
shuffle = ()
|
||||
if level in ("Mushroom Zone", "Macro Zone 4"):
|
||||
shuffle = ("Koopa Troopa", "Goomba", "Paragoomba (Vertical)", "Paragoomba (Diagonal)")
|
||||
elif level in ("Scenic Course", "Pumpkin Zone Secret Course 1"):
|
||||
shuffle = ("Goomba", "Paragoomba (Vertical)", "Paragoomba (Diagonal)")
|
||||
elif level == "Tree Zone 1":
|
||||
shuffle = ("Money Bag/Bopping Toady", "Ragumo/Aqua Kuribo", "Pencil/Spikey", "Kyotonbo")
|
||||
elif level == "Tree Zone 2":
|
||||
shuffle = ("Noko Bombette/Bear", "No 48/Mogyo")
|
||||
elif level == "Tree Zone 3":
|
||||
shuffle = ("Battle Beetle", "Be", "Ant")
|
||||
elif level == "Tree Zone 5":
|
||||
shuffle = ("Paragoomba (Diagonal)", "Dondon", "Paragoomba (Vertical)")
|
||||
elif level == "Pumpkin Zone 2":
|
||||
shuffle = ("Boo/Bomubomu", "Kyororo", "Honebon/F Boy", "Karakara", "Star (Vertical)/Blurp (Horizontal)",
|
||||
"Star (Horizontal)/Blurp (Vertical)")
|
||||
elif level == "Pumpkin Zone 3":
|
||||
shuffle = ("Boo/Bomubomu", "Unibo/Terekuribo")
|
||||
elif level == "Mario Zone 1":
|
||||
shuffle = ("Koopa Troopa", "Neiji/Buichi", "Tatenoko")
|
||||
elif level == "Mario Zone 2":
|
||||
shuffle = ("Paragoomba (Diagonal)", "Goomba", "Paragoomba (Vertical)", "Noko Bombette/Bear",
|
||||
"Boo/Bomubomu")
|
||||
elif level == "Turtle Zone 1":
|
||||
shuffle = ("Horizontal Blurp", "Shark", "Cheep Cheep (Vertical)", "Paragoomba (Diagonal)", "Goomba",
|
||||
"Spiny Cheep Cheep", "Paragoomba (Vertical)",
|
||||
"Owl Platform (Horizontal)/Cheep Cheep (Horizontal)")
|
||||
elif level == "Hippo Zone":
|
||||
shuffle = ("Horizontal Blurp", "Dondon", "Unibo/Terekuribo", "Toriuo")
|
||||
elif level == "Space Zone 2":
|
||||
shuffle = ("Tosenbo/Pikku", "Star (Vertical)/Blurp (Horizontal)", "Star (Horizontal)/Blurp (Vertical)")
|
||||
elif level == "Macro Zone 1":
|
||||
shuffle = ("Kyotonbo", "Goronto", "Dokanto", "Chikunto")
|
||||
elif level == "Macro Zone 2":
|
||||
shuffle = ("Cheep Cheep (Vertical)", "Battle Beetle", "Be",
|
||||
"Owl Platform (Horizontal)/Cheep Cheep (Horizontal)", "Ant")
|
||||
elif level == "Macro Zone 3":
|
||||
shuffle = ("Koopa Troopa", "Paragoomba (Diagonal)", "Goomba", "Be", "Paragoomba (Vertical)",
|
||||
"Honebon/F Boy")
|
||||
elif level == "Pumpkin Zone Secret Course 2":
|
||||
shuffle = ("Koopa Troopa", "Goomba")
|
||||
for sprite in level_sprite_data:
|
||||
if level == "Pumpkin Zone 1":
|
||||
if sprite["sprite"] == "Falling Spike":
|
||||
shuffle = ("Boo/Bomubomu", "Falling Spike", "Kurokyura/Jack-in-the-Box", "Masked Ghoul/Bullet Bill")
|
||||
elif sprite["sprite"] == "Falling Spike on Chain":
|
||||
shuffle = ("Boo/Bomubomu", "Falling Spike on Chain", "Kurokyura/Jack-in-the-Box",
|
||||
"Masked Ghoul/Bullet Bill")
|
||||
else:
|
||||
shuffle = ("Boo/Bomubomu", "Kurokyura/Jack-in-the-Box", "Masked Ghoul/Bullet Bill")
|
||||
elif level == "Pumpkin Zone 4":
|
||||
if sprite["sprite"] == "Falling Spike on Chain":
|
||||
shuffle = ("Boo/Bomubomu", "Falling Spike on Chain", "Masked Ghoul/Bullet Bill", "Rerere/Poro",
|
||||
"Tosenbo/Pikku")
|
||||
else:
|
||||
shuffle = ("Boo/Bomubomu", "Masked Ghoul/Bullet Bill", "Rerere/Poro", "Tosenbo/Pikku")
|
||||
elif level == "Mario Zone 3":
|
||||
if sprite["sprite"] == "Claw Grabber":
|
||||
shuffle = ("Koopa Troopa", "Diagonal Ball on Chain", "Kiddokatto", "Claw Grabber",
|
||||
"Masked Ghoul/Bullet Bill")
|
||||
elif sprite["sprite"] in ("Koopa Troopa", "Diagonal Ball on Chain", "Kiddokatto"):
|
||||
shuffle = ("Koopa Troopa", "Diagonal Ball on Chain", "Kiddokatto", "Masked Ghoul/Bullet Bill")
|
||||
else:
|
||||
shuffle = ()
|
||||
elif level == "Mario Zone 4":
|
||||
if sprite["sprite"] == "Spinning Spike/Tamara":
|
||||
shuffle = ("Goomba", "Spinning Spike/Tamara", "Boo/Bomubomu", "Masked Ghoul/Bullet Bill")
|
||||
elif sprite["sprite"] == "Moving Saw (Floor)":
|
||||
shuffle = ("Goomba", "Moving Saw (Floor)", "Boo/Bomubomu", "Masked Ghoul/Bullet Bill")
|
||||
else:
|
||||
shuffle = ("Goomba", "Boo/Bomubomu", "Masked Ghoul/Bullet Bill")
|
||||
elif level == "Turtle Zone 3":
|
||||
if sprite["sprite"] == "Pencil/Spikey":
|
||||
shuffle = ("Koopa Troopa", "Paragoomba (Diagonal)", "Ragumo/Aqua Kuribo", "Pencil/Spikey",
|
||||
"Paragoomba (Vertical)", "Honebon/F Boy")
|
||||
else:
|
||||
shuffle = ("Koopa Troopa", "Paragoomba (Diagonal)", "Ragumo/Aqua Kuribo",
|
||||
"Paragoomba (Vertical)", "Honebon/F Boy")
|
||||
elif level == "Space Zone 1":
|
||||
if sprite["sprite"] == "Boo/Bomubomu":
|
||||
shuffle = ("Boo/Bomubomu", "No 48/Mogyo")
|
||||
else:
|
||||
shuffle = ("Boo/Bomubomu", "No 48/Mogyo", "Rerere/Poro")
|
||||
elif level == "Mario's Castle":
|
||||
if sprite["sprite"] in ("Fire Pakkun Zo (Large)", "Fire Pakkun Zo (Left)"):
|
||||
shuffle = ("Fire Pakkun Zo (Large)", "Fire Pakkun Zo (Left)")
|
||||
else:
|
||||
shuffle = ("Spike Ball (Large)", "Spike Ball (Small)")
|
||||
elif level == "Tree Zone 4":
|
||||
# Deviation from SML2R: No Buichis placed into non-Buichi locations, as they can place under the
|
||||
# underground question mark blocks. Potentially could make a list of which ones are allowed to become
|
||||
# Buichis?
|
||||
if sprite["sprite"] in ("Runaway Heart Block/Bibi", "Piranha Plant (Downward)/Grubby",
|
||||
"Spinning Platform (Horizontal)/Skeleton Bee",
|
||||
"Spinning Spike (Horizontal)/Unera"):
|
||||
shuffle = ("Runaway Heart Block/Bibi", "Piranha Plant (Downward)/Grubby",
|
||||
"Spinning Platform (Horizontal)/Skeleton Bee", "Spinning Spike (Horizontal)/Unera")
|
||||
elif sprite["sprite"] == "Neiji/Buichi":
|
||||
shuffle = ("Runaway Heart Block/Bibi", "Neiji/Buichi", "Piranha Plant (Downward)/Grubby",
|
||||
"Spinning Platform (Horizontal)/Skeleton Bee", "Spinning Spike (Horizontal)/Unera")
|
||||
else:
|
||||
shuffle = ()
|
||||
if sprite["sprite"] in ("Piranha Plant", "Fire Piranha Plant"):
|
||||
if level not in ("Pumpkin Zone 2", "Pumpkin Zone 4", "Macro Zone 3"):
|
||||
shuffle = ("Piranha Plant", "Fire Piranha Plant")
|
||||
if sprite["sprite"] in shuffle:
|
||||
sprite["sprite"] = random.choice(shuffle)
|
||||
elif level == "Mario's Castle" and sprite["sprite"] == "Karamenbo" and not random.randint(0, 9):
|
||||
sprite["y"] += 1
|
||||
|
||||
|
||||
def randomize_platforms(sprite_data, random):
|
||||
shuffle = ("Moving Platform (Small, Vertical)", "Moving Platform (Large, Vertical)",
|
||||
"Moving Platform (Small, Horizontal)", "Moving Platform (Large, Horizontal)",
|
||||
"Moving Platform (Large, Diagonal)", "Falling Platform")
|
||||
for sprite in sprite_data["Tree Zone 3"]:
|
||||
if sprite["sprite"] in shuffle:
|
||||
sprite["sprite"] = random.choice(shuffle)
|
||||
shuffle = ("Cloud Platform (Horizontal)", "Owl Platform (Horizontal)/Cheep Cheep (Horizontal)")
|
||||
for sprite in sprite_data["Tree Zone 5"]:
|
||||
if sprite["sprite"] in shuffle:
|
||||
sprite["sprite"] = random.choice(shuffle)
|
||||
shuffle = ("Falling Bone Platform", "Rising Bone Platform", "Skull Platform")
|
||||
for sprite in sprite_data["Mario's Castle"]:
|
||||
if sprite["sprite"] in shuffle:
|
||||
sprite["sprite"] = random.choice(shuffle)
|
||||
1016
worlds/marioland2/sprites.py
Normal file
1016
worlds/marioland2/sprites.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,8 @@
|
||||
## Multiplayer Raft
|
||||
|
||||
You're able to have multiple Raft players on a single Raftipelago world. This will work, with a few notes:
|
||||
- Only the player that creates/loads the world can connect to Archipelago (this is the "host" of the Raft world). Other players do not need to connect; everything will be routed through the the host.
|
||||
- Every player that joins the Raft world must have the Raftipelago mod loaded.
|
||||
- Only the player that creates/loads the world can connect to Archipelago (this is the "host" of the Raft world). Other players do not need to run */connect*; everything will be routed through the the host.
|
||||
- Players other than the host will be labeled as a "Raft Player (Steam name)" when using ingame chat, which will be routed through Archipelago chat.
|
||||
- Ingame chat will only work when the host is connected to the Archipelago server.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"Steering Wheel": "progressive-engine",
|
||||
"Engine controls": "progressive-engine",
|
||||
"Scarecrow": "progressive-scarecrow",
|
||||
"Advanced scarecrow": "progressive-scarecrow",
|
||||
"Advanced Scarecrow": "progressive-scarecrow",
|
||||
"Simple collection net": "progressive-net",
|
||||
"Advanced collection net": "progressive-net",
|
||||
"Storage": "progressive-storage",
|
||||
|
||||
@@ -241,7 +241,7 @@ def get_mission_count_table(multiworld: MultiWorld, world: World, player: int):
|
||||
sonic_active_missions = min(sonic_active_missions, world.options.sonic_mission_count.value)
|
||||
tails_active_missions = min(tails_active_missions, world.options.tails_mission_count.value)
|
||||
knuckles_active_missions = min(knuckles_active_missions, world.options.knuckles_mission_count.value)
|
||||
shadow_active_missions = min(shadow_active_missions, world.options.sonic_mission_count.value)
|
||||
shadow_active_missions = min(shadow_active_missions, world.options.shadow_mission_count.value)
|
||||
eggman_active_missions = min(eggman_active_missions, world.options.eggman_mission_count.value)
|
||||
rouge_active_missions = min(rouge_active_missions, world.options.rouge_mission_count.value)
|
||||
kart_active_missions = min(kart_active_missions, world.options.kart_mission_count.value)
|
||||
|
||||
@@ -2257,7 +2257,7 @@ def set_mission_upgrade_rules_hard(multiworld: MultiWorld, world: World, player:
|
||||
add_rule_safe(multiworld, LocationName.weapons_bed_5, player,
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
add_rule_safe(multiworld, LocationName.security_hall_5, player,
|
||||
lambda state: state.has(ItemName.rouge_treasure_scope, player))
|
||||
lambda state: state.has(ItemName.rouge_pick_nails, player))
|
||||
add_rule_safe(multiworld, LocationName.cosmic_wall_5, player,
|
||||
lambda state: state.has(ItemName.eggman_jet_engine, player))
|
||||
|
||||
@@ -2971,7 +2971,7 @@ def set_mission_upgrade_rules_hard(multiworld: MultiWorld, world: World, player:
|
||||
|
||||
add_rule(multiworld.get_location(LocationName.mission_street_lifebox_2, player),
|
||||
lambda state: (state.has(ItemName.tails_booster, player) and
|
||||
- state.has(ItemName.tails_mystic_melody, player)))
|
||||
state.has(ItemName.tails_mystic_melody, player)))
|
||||
add_rule(multiworld.get_location(LocationName.eternal_engine_lifebox_2, player),
|
||||
lambda state: state.has(ItemName.tails_booster, player))
|
||||
|
||||
|
||||
417
worlds/shapez/__init__.py
Normal file
417
worlds/shapez/__init__.py
Normal file
@@ -0,0 +1,417 @@
|
||||
import math
|
||||
from typing import Any, List, Dict, Tuple, Mapping
|
||||
|
||||
from Options import OptionError
|
||||
from .data.strings import OTHER, ITEMS, CATEGORY, LOCATIONS, SLOTDATA, GOALS, OPTIONS
|
||||
from .items import item_descriptions, item_table, ShapezItem, \
|
||||
buildings_routing, buildings_processing, buildings_other, \
|
||||
buildings_top_row, buildings_wires, gameplay_unlocks, upgrades, \
|
||||
big_upgrades, filler, trap, bundles, belt_and_extractor, standard_traps, random_draining_trap, split_draining_traps, \
|
||||
whacky_upgrade_traps
|
||||
from .locations import ShapezLocation, addlevels, addupgrades, addachievements, location_description, \
|
||||
addshapesanity, addshapesanity_ut, shapesanity_simple, init_shapesanity_pool, achievement_locations, \
|
||||
level_locations, upgrade_locations, shapesanity_locations, categories
|
||||
from .presets import options_presets
|
||||
from .options import ShapezOptions
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from BaseClasses import Item, Tutorial, LocationProgressType, MultiWorld
|
||||
from .regions import create_shapez_regions, has_x_belt_multiplier
|
||||
from ..generic.Rules import add_rule
|
||||
|
||||
|
||||
class ShapezWeb(WebWorld):
|
||||
options_presets = options_presets
|
||||
rich_text_options_doc = True
|
||||
theme = "stone"
|
||||
game_info_languages = ['en', 'de']
|
||||
setup_en = Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to playing shapez with Archipelago:",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["BlastSlimey"]
|
||||
)
|
||||
setup_de = Tutorial(
|
||||
setup_en.tutorial_name,
|
||||
setup_en.description,
|
||||
"Deutsch",
|
||||
"setup_de.md",
|
||||
"setup/de",
|
||||
["BlastSlimey"]
|
||||
)
|
||||
datapackage_settings_en = Tutorial(
|
||||
"Changing datapackage settings",
|
||||
"3000 locations are too many or not enough? Here's how you can change that:",
|
||||
"English",
|
||||
"datapackage_settings_en.md",
|
||||
"datapackage_settings/en",
|
||||
["BlastSlimey"]
|
||||
)
|
||||
datapackage_settings_de = Tutorial(
|
||||
datapackage_settings_en.tutorial_name,
|
||||
datapackage_settings_en.description,
|
||||
"Deutsch",
|
||||
"datapackage_settings_de.md",
|
||||
"datapackage_settings/de",
|
||||
["BlastSlimey"]
|
||||
)
|
||||
tutorials = [setup_en, setup_de, datapackage_settings_en, datapackage_settings_de]
|
||||
item_descriptions = item_descriptions
|
||||
location_descriptions = location_description
|
||||
|
||||
|
||||
class ShapezWorld(World):
|
||||
"""
|
||||
shapez is an automation game about cutting, rotating, stacking, and painting shapes, that you extract from randomly
|
||||
generated patches on an infinite canvas, without the need to manage your infinite resources or to pay for building
|
||||
your factories.
|
||||
"""
|
||||
game = OTHER.game_name
|
||||
options_dataclass = ShapezOptions
|
||||
options: ShapezOptions
|
||||
topology_present = True
|
||||
web = ShapezWeb()
|
||||
base_id = 20010707
|
||||
item_name_to_id = {name: id for id, name in enumerate(item_table.keys(), base_id)}
|
||||
location_name_to_id = {name: id for id, name in enumerate(level_locations + upgrade_locations
|
||||
+ achievement_locations + shapesanity_locations, base_id)}
|
||||
item_name_groups = {
|
||||
"Main Buildings": {ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker},
|
||||
"Processing Buildings": {*buildings_processing},
|
||||
"Goal Buildings": {ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.rotator_ccw, ITEMS.color_mixer,
|
||||
ITEMS.stacker, ITEMS.cutter_quad, ITEMS.painter_double, ITEMS.painter_quad, ITEMS.wires,
|
||||
ITEMS.switch, ITEMS.const_signal},
|
||||
"Most Useful Buildings": {ITEMS.balancer, ITEMS.tunnel, ITEMS.tunnel_tier_ii, ITEMS.comp_merger,
|
||||
ITEMS.comp_splitter, ITEMS.trash, ITEMS.extractor_chain},
|
||||
"Most Important Buildings": {*belt_and_extractor},
|
||||
"Top Row Buildings": {*buildings_top_row},
|
||||
"Wires Layer Buildings": {*buildings_wires},
|
||||
"Gameplay Mechanics": {ITEMS.blueprints, ITEMS.wires},
|
||||
"Upgrades": {*{ITEMS.upgrade(size, cat)
|
||||
for size in {CATEGORY.big, CATEGORY.small, CATEGORY.gigantic, CATEGORY.rising}
|
||||
for cat in {CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting}},
|
||||
*{ITEMS.trap_upgrade(cat, size)
|
||||
for cat in {CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting}
|
||||
for size in {"", CATEGORY.demonic}},
|
||||
*{ITEMS.upgrade(size, CATEGORY.random)
|
||||
for size in {CATEGORY.big, CATEGORY.small}}},
|
||||
**{f"{cat} Upgrades": {*{ITEMS.upgrade(size, cat)
|
||||
for size in {CATEGORY.big, CATEGORY.small, CATEGORY.gigantic, CATEGORY.rising}},
|
||||
*{ITEMS.trap_upgrade(cat, size)
|
||||
for size in {"", CATEGORY.demonic}}}
|
||||
for cat in {CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting}},
|
||||
"Bundles": {*bundles},
|
||||
"Traps": {*standard_traps, *random_draining_trap, *split_draining_traps, *whacky_upgrade_traps},
|
||||
}
|
||||
location_name_groups = {
|
||||
"Levels": {*level_locations},
|
||||
"Upgrades": {*upgrade_locations},
|
||||
"Achievements": {*achievement_locations},
|
||||
"Shapesanity": {*shapesanity_locations},
|
||||
**{f"{cat} Upgrades": {loc for loc in upgrade_locations if loc.startswith(cat)} for cat in categories},
|
||||
"Only Belt and Extractor": {LOCATIONS.level(1), LOCATIONS.level(1, 1),
|
||||
LOCATIONS.my_eyes, LOCATIONS.its_a_mess, LOCATIONS.getting_into_it,
|
||||
LOCATIONS.perfectionist, LOCATIONS.oops, LOCATIONS.i_need_trains, LOCATIONS.gps,
|
||||
LOCATIONS.a_long_time, LOCATIONS.addicted,
|
||||
LOCATIONS.shapesanity(1), LOCATIONS.shapesanity(2), LOCATIONS.shapesanity(3)},
|
||||
}
|
||||
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
super().__init__(multiworld, player)
|
||||
|
||||
# Defining instance attributes for each shapez world
|
||||
# These are set to default values that should fail unit tests if not replaced with correct values
|
||||
self.location_count: int = 0
|
||||
self.level_logic: List[str] = []
|
||||
self.upgrade_logic: List[str] = []
|
||||
self.level_logic_type: str = ""
|
||||
self.upgrade_logic_type: str = ""
|
||||
self.random_logic_phase_length: List[int] = []
|
||||
self.category_random_logic_amounts: Dict[str, int] = {}
|
||||
self.maxlevel: int = 0
|
||||
self.finaltier: int = 0
|
||||
self.included_locations: Dict[str, Tuple[str, LocationProgressType]] = {}
|
||||
self.client_seed: int = 0
|
||||
self.shapesanity_names: List[str] = []
|
||||
self.upgrade_traps_allowed: bool = False
|
||||
|
||||
# Universal Tracker support
|
||||
self.ut_active: bool = False
|
||||
self.passthrough: Dict[str, any] = {}
|
||||
self.location_id_to_alias: Dict[int, str] = {}
|
||||
|
||||
@classmethod
|
||||
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
|
||||
# Import the 75800 entries long shapesanity pool only once and only if it's actually needed
|
||||
if len(shapesanity_simple) == 0:
|
||||
init_shapesanity_pool()
|
||||
|
||||
def generate_early(self) -> None:
|
||||
# Calculate all the important values used for generating a shapez world, with some of them being random
|
||||
self.upgrade_traps_allowed: bool = (self.options.include_whacky_upgrades and
|
||||
(not self.options.goal == GOALS.efficiency_iii) and
|
||||
self.options.throughput_levels_ratio == 0)
|
||||
|
||||
# Load values from UT if this is a regenerated world
|
||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||
if OTHER.game_name in self.multiworld.re_gen_passthrough:
|
||||
self.ut_active = True
|
||||
self.passthrough = self.multiworld.re_gen_passthrough[OTHER.game_name]
|
||||
self.maxlevel = self.passthrough[SLOTDATA.maxlevel]
|
||||
self.finaltier = self.passthrough[SLOTDATA.finaltier]
|
||||
self.client_seed = self.passthrough[SLOTDATA.seed]
|
||||
self.level_logic = [self.passthrough[SLOTDATA.level_building(i+1)] for i in range(5)]
|
||||
self.upgrade_logic = [self.passthrough[SLOTDATA.upgrade_building(i+1)] for i in range(5)]
|
||||
self.level_logic_type = self.passthrough[SLOTDATA.rand_level_logic]
|
||||
self.upgrade_logic_type = self.passthrough[SLOTDATA.rand_upgrade_logic]
|
||||
self.random_logic_phase_length = [self.passthrough[SLOTDATA.phase_length(i)] for i in range(5)]
|
||||
self.category_random_logic_amounts = {cat: self.passthrough[SLOTDATA.cat_buildings_amount(cat)]
|
||||
for cat in [CATEGORY.belt_low, CATEGORY.miner_low,
|
||||
CATEGORY.processors_low, CATEGORY.painting_low]}
|
||||
# Forces balancers, tunnel, and trash to not appear in regen to make UT more accurate
|
||||
self.options.early_balancer_tunnel_and_trash.value = 0
|
||||
return
|
||||
|
||||
# "MAM" goal is supposed to be longer than vanilla, but to not have more options than necessary,
|
||||
# both goal amounts for "MAM" and "Even fasterer" are set in a single option.
|
||||
if self.options.goal == GOALS.mam and self.options.goal_amount < 27:
|
||||
raise OptionError(self.player_name
|
||||
+ ": When setting goal to 1 ('mam'), goal_amount must be at least 27 and not "
|
||||
+ str(self.options.goal_amount.value))
|
||||
|
||||
# If lock_belt_and_extractor is true, the only sphere 1 locations will be achievements
|
||||
if self.options.lock_belt_and_extractor and not self.options.include_achievements:
|
||||
raise OptionError(self.player_name + ": Achievements must be included when belt and extractor are locked")
|
||||
|
||||
# Determines maxlevel and finaltier, which are needed for location and item generation
|
||||
if self.options.goal == GOALS.vanilla:
|
||||
self.maxlevel = 25
|
||||
self.finaltier = 8
|
||||
elif self.options.goal == GOALS.mam:
|
||||
self.maxlevel = self.options.goal_amount - 1
|
||||
self.finaltier = 8
|
||||
elif self.options.goal == GOALS.even_fasterer:
|
||||
self.maxlevel = 26
|
||||
self.finaltier = self.options.goal_amount.value
|
||||
else: # goal == efficiency_iii
|
||||
self.maxlevel = 26
|
||||
self.finaltier = 8
|
||||
|
||||
# Setting the seed for the game before any other randomization call is done
|
||||
self.client_seed = self.random.randint(0, 100000)
|
||||
|
||||
# Determines the order of buildings for levels logic
|
||||
if self.options.randomize_level_requirements:
|
||||
self.level_logic_type = self.options.randomize_level_logic.current_key
|
||||
if self.level_logic_type.endswith(OPTIONS.logic_shuffled) or self.level_logic_type == OPTIONS.logic_dopamine:
|
||||
vanilla_list = [ITEMS.cutter, ITEMS.painter, ITEMS.stacker]
|
||||
while len(vanilla_list) > 0:
|
||||
index = self.random.randint(0, len(vanilla_list)-1)
|
||||
next_building = vanilla_list.pop(index)
|
||||
if next_building == ITEMS.cutter:
|
||||
vanilla_list.append(ITEMS.rotator)
|
||||
if next_building == ITEMS.painter:
|
||||
vanilla_list.append(ITEMS.color_mixer)
|
||||
self.level_logic.append(next_building)
|
||||
else:
|
||||
self.level_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker]
|
||||
else:
|
||||
self.level_logic_type = OPTIONS.logic_vanilla
|
||||
self.level_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker]
|
||||
|
||||
# Determines the order of buildings for upgrades logic
|
||||
if self.options.randomize_upgrade_requirements:
|
||||
self.upgrade_logic_type = self.options.randomize_upgrade_logic.current_key
|
||||
if self.upgrade_logic_type == OPTIONS.logic_hardcore:
|
||||
self.upgrade_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker]
|
||||
elif self.upgrade_logic_type == OPTIONS.logic_category:
|
||||
self.upgrade_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.stacker, ITEMS.painter, ITEMS.color_mixer]
|
||||
else:
|
||||
vanilla_list = [ITEMS.cutter, ITEMS.painter, ITEMS.stacker]
|
||||
while len(vanilla_list) > 0:
|
||||
index = self.random.randint(0, len(vanilla_list)-1)
|
||||
next_building = vanilla_list.pop(index)
|
||||
if next_building == ITEMS.cutter:
|
||||
vanilla_list.append(ITEMS.rotator)
|
||||
if next_building == ITEMS.painter:
|
||||
vanilla_list.append(ITEMS.color_mixer)
|
||||
self.upgrade_logic.append(next_building)
|
||||
else:
|
||||
self.upgrade_logic_type = OPTIONS.logic_vanilla_like
|
||||
self.upgrade_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker]
|
||||
|
||||
# Determine lenghts of phases in level logic type "random"
|
||||
self.random_logic_phase_length = [1, 1, 1, 1, 1]
|
||||
if self.level_logic_type.startswith(OPTIONS.logic_random_steps):
|
||||
remaininglength = self.maxlevel - 1
|
||||
for phase in range(0, 5):
|
||||
if self.random.random() < 0.1: # Make sure that longer phases are less frequent
|
||||
self.random_logic_phase_length[phase] = self.random.randint(0, remaininglength)
|
||||
else:
|
||||
self.random_logic_phase_length[phase] = self.random.randint(0, remaininglength // (6 - phase))
|
||||
remaininglength -= self.random_logic_phase_length[phase]
|
||||
|
||||
# Determine amount of needed buildings for each category in upgrade logic type "category_random"
|
||||
self.category_random_logic_amounts = {CATEGORY.belt_low: 0, CATEGORY.miner_low: 1,
|
||||
CATEGORY.processors_low: 2, CATEGORY.painting_low: 3}
|
||||
if self.upgrade_logic_type == OPTIONS.logic_category_random:
|
||||
cats = [CATEGORY.belt_low, CATEGORY.miner_low, CATEGORY.processors_low, CATEGORY.painting_low]
|
||||
nextcat = self.random.choice(cats)
|
||||
self.category_random_logic_amounts[nextcat] = 0
|
||||
cats.remove(nextcat)
|
||||
for cat in cats:
|
||||
self.category_random_logic_amounts[cat] = self.random.randint(0, 5)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
return ShapezItem(name, item_table[name](self.options), self.item_name_to_id[name], self.player)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return filler(self.random.random(), bool(self.options.include_whacky_upgrades))
|
||||
|
||||
def append_shapesanity(self, name: str) -> None:
|
||||
"""This method is given as a parameter when creating the locations for shapesanity."""
|
||||
self.shapesanity_names.append(name)
|
||||
|
||||
def add_alias(self, location_name: str, alias: str):
|
||||
"""This method is given as a parameter when locations with helpful aliases for UT are created."""
|
||||
if self.ut_active:
|
||||
self.location_id_to_alias[self.location_name_to_id[location_name]] = alias
|
||||
|
||||
def create_regions(self) -> None:
|
||||
# Create list of all included level and upgrade locations based on player options
|
||||
# This already includes the region to be placed in and the LocationProgressType
|
||||
self.included_locations = {**addlevels(self.maxlevel, self.level_logic_type,
|
||||
self.random_logic_phase_length),
|
||||
**addupgrades(self.finaltier, self.upgrade_logic_type,
|
||||
self.category_random_logic_amounts)}
|
||||
|
||||
# Add shapesanity to included location and creates the corresponding list based on player options
|
||||
if self.ut_active:
|
||||
self.shapesanity_names = self.passthrough[SLOTDATA.shapesanity]
|
||||
self.included_locations.update(addshapesanity_ut(self.shapesanity_names, self.add_alias))
|
||||
else:
|
||||
self.included_locations.update(addshapesanity(self.options.shapesanity_amount.value, self.random,
|
||||
self.append_shapesanity, self.add_alias))
|
||||
|
||||
# Add achievements to included locations based on player options
|
||||
if self.options.include_achievements:
|
||||
self.included_locations.update(addachievements(
|
||||
bool(self.options.exclude_softlock_achievements), bool(self.options.exclude_long_playtime_achievements),
|
||||
bool(self.options.exclude_progression_unreasonable), self.maxlevel, self.upgrade_logic_type,
|
||||
self.category_random_logic_amounts, self.options.goal.current_key, self.included_locations,
|
||||
self.add_alias, self.upgrade_traps_allowed))
|
||||
|
||||
# Save the final amount of to-be-filled locations
|
||||
self.location_count = len(self.included_locations)
|
||||
|
||||
# Create regions and entrances based on included locations and player options
|
||||
self.multiworld.regions.extend(create_shapez_regions(self.player, self.multiworld,
|
||||
bool(self.options.allow_floating_layers.value),
|
||||
self.included_locations, self.location_name_to_id,
|
||||
self.level_logic, self.upgrade_logic,
|
||||
self.options.early_balancer_tunnel_and_trash.current_key,
|
||||
self.options.goal.current_key))
|
||||
|
||||
def create_items(self) -> None:
|
||||
# Include guaranteed items (game mechanic unlocks and 7x4 big upgrades)
|
||||
included_items: List[Item] = ([self.create_item(name) for name in buildings_processing.keys()]
|
||||
+ [self.create_item(name) for name in buildings_routing.keys()]
|
||||
+ [self.create_item(name) for name in buildings_other.keys()]
|
||||
+ [self.create_item(name) for name in buildings_top_row.keys()]
|
||||
+ [self.create_item(name) for name in buildings_wires.keys()]
|
||||
+ [self.create_item(name) for name in gameplay_unlocks.keys()]
|
||||
+ [self.create_item(name) for name in big_upgrades for _ in range(7)])
|
||||
|
||||
if not self.options.lock_belt_and_extractor:
|
||||
for name in belt_and_extractor:
|
||||
self.multiworld.push_precollected(self.create_item(name))
|
||||
else: # This also requires self.options.include_achievements to be true
|
||||
included_items.extend([self.create_item(name) for name in belt_and_extractor.keys()])
|
||||
|
||||
# Give a detailed error message if there are already more items than available locations.
|
||||
# At the moment, this won't happen, but it's better for debugging in case a future update breaks things.
|
||||
if len(included_items) > self.location_count:
|
||||
raise RuntimeError(self.player_name + ": There are more guaranteed items than available locations")
|
||||
|
||||
# Get value from traps probability option and convert to float
|
||||
traps_probability = self.options.traps_percentage/100
|
||||
split_draining = bool(self.options.split_inventory_draining_trap)
|
||||
# Fill remaining locations with fillers
|
||||
for x in range(self.location_count - len(included_items)):
|
||||
if self.random.random() < traps_probability:
|
||||
# Fill with trap
|
||||
included_items.append(self.create_item(trap(self.random.random(), split_draining,
|
||||
self.upgrade_traps_allowed)))
|
||||
else:
|
||||
# Fil with random filler item
|
||||
included_items.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
# Add correct number of items to itempool
|
||||
self.multiworld.itempool += included_items
|
||||
|
||||
# Add balancer, tunnel, and trash to early items if player options say so
|
||||
if self.options.early_balancer_tunnel_and_trash == OPTIONS.sphere_1:
|
||||
self.multiworld.early_items[self.player][ITEMS.balancer] = 1
|
||||
self.multiworld.early_items[self.player][ITEMS.tunnel] = 1
|
||||
self.multiworld.early_items[self.player][ITEMS.trash] = 1
|
||||
|
||||
def set_rules(self) -> None:
|
||||
# Levels might need more belt speed if they require throughput per second. As the randomization of what levels
|
||||
# need throughput happens in the client mod, this logic needs to be applied to all levels. This is applied to
|
||||
# every individual level instead of regions, because they would need a much more complex calculation to prevent
|
||||
# softlocks.
|
||||
|
||||
def f(x: int, name: str):
|
||||
# These calculations are taken from the client mod
|
||||
if x < 26:
|
||||
throughput = math.ceil((2.999+x*0.333)*self.options.required_shapes_multiplier/10)
|
||||
else:
|
||||
throughput = min((4+(x-26)*0.25)*self.options.required_shapes_multiplier/10, 200)
|
||||
if throughput/32 >= 1:
|
||||
add_rule(self.get_location(name),
|
||||
lambda state: has_x_belt_multiplier(state, self.player, throughput/32))
|
||||
|
||||
if not self.options.throughput_levels_ratio == 0:
|
||||
f(0, LOCATIONS.level(1, 1))
|
||||
f(19, LOCATIONS.level(20, 1))
|
||||
f(19, LOCATIONS.level(20, 2))
|
||||
for _x in range(self.maxlevel):
|
||||
f(_x, LOCATIONS.level(_x+1))
|
||||
if self.options.goal.current_key in [GOALS.vanilla, GOALS.mam]:
|
||||
f(self.maxlevel, LOCATIONS.goal)
|
||||
|
||||
def fill_slot_data(self) -> Mapping[str, Any]:
|
||||
# Buildings logic; all buildings as individual parameters
|
||||
level_logic_data = {SLOTDATA.level_building(x+1): self.level_logic[x] for x in range(5)}
|
||||
upgrade_logic_data = {SLOTDATA.upgrade_building(x+1): self.upgrade_logic[x] for x in range(5)}
|
||||
# Randomized values for certain logic types
|
||||
logic_type_random_data = {SLOTDATA.phase_length(x): self.random_logic_phase_length[x] for x in range(0, 5)}
|
||||
logic_type_cat_random_data = {SLOTDATA.cat_buildings_amount(cat): self.category_random_logic_amounts[cat]
|
||||
for cat in [CATEGORY.belt_low, CATEGORY.miner_low,
|
||||
CATEGORY.processors_low, CATEGORY.painting_low]}
|
||||
|
||||
# Options that are relevant to the mod
|
||||
option_data = {
|
||||
SLOTDATA.goal: self.options.goal.current_key,
|
||||
SLOTDATA.maxlevel: self.maxlevel,
|
||||
SLOTDATA.finaltier: self.finaltier,
|
||||
SLOTDATA.req_shapes_mult: self.options.required_shapes_multiplier.value,
|
||||
SLOTDATA.allow_float_layers: bool(self.options.allow_floating_layers),
|
||||
SLOTDATA.rand_level_req: bool(self.options.randomize_level_requirements),
|
||||
SLOTDATA.rand_upgrade_req: bool(self.options.randomize_upgrade_requirements),
|
||||
SLOTDATA.rand_level_logic: self.level_logic_type,
|
||||
SLOTDATA.rand_upgrade_logic: self.upgrade_logic_type,
|
||||
SLOTDATA.throughput_levels_ratio: self.options.throughput_levels_ratio.value,
|
||||
SLOTDATA.comp_growth_gradient: self.options.complexity_growth_gradient.value,
|
||||
SLOTDATA.same_late: bool(self.options.same_late_upgrade_requirements),
|
||||
SLOTDATA.toolbar_shuffling: bool(self.options.toolbar_shuffling),
|
||||
}
|
||||
|
||||
return {**level_logic_data, **upgrade_logic_data, **option_data, **logic_type_random_data,
|
||||
**logic_type_cat_random_data, SLOTDATA.seed: self.client_seed,
|
||||
SLOTDATA.shapesanity: self.shapesanity_names}
|
||||
|
||||
def interpret_slot_data(self, slot_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Helper function for Universal Tracker"""
|
||||
return slot_data
|
||||
0
worlds/shapez/common/__init__.py
Normal file
0
worlds/shapez/common/__init__.py
Normal file
190
worlds/shapez/common/options.py
Normal file
190
worlds/shapez/common/options.py
Normal file
@@ -0,0 +1,190 @@
|
||||
import random
|
||||
import typing
|
||||
|
||||
from Options import FreeText, NumericOption
|
||||
|
||||
|
||||
class FloatRangeText(FreeText, NumericOption):
|
||||
"""FreeText option optimized for entering float numbers.
|
||||
Supports everything that Range supports.
|
||||
range_start and range_end have to be floats, while default has to be a string."""
|
||||
|
||||
default = "0.0"
|
||||
value: float
|
||||
range_start: float = 0.0
|
||||
range_end: float = 1.0
|
||||
|
||||
def __init__(self, value: str):
|
||||
super().__init__(value)
|
||||
value = value.lower()
|
||||
if value.startswith("random"):
|
||||
self.value = self.weighted_range(value)
|
||||
elif value == "default" and hasattr(self, "default"):
|
||||
self.value = float(self.default)
|
||||
elif value == "high":
|
||||
self.value = self.range_end
|
||||
elif value == "low":
|
||||
self.value = self.range_start
|
||||
elif self.range_start == 0.0 \
|
||||
and hasattr(self, "default") \
|
||||
and self.default != "0.0" \
|
||||
and value in ("true", "false"):
|
||||
# these are the conditions where "true" and "false" make sense
|
||||
if value == "true":
|
||||
self.value = float(self.default)
|
||||
else: # "false"
|
||||
self.value = 0.0
|
||||
else:
|
||||
try:
|
||||
self.value = float(value)
|
||||
except ValueError:
|
||||
raise Exception(f"Invalid value for option {self.__class__.__name__}: {value}")
|
||||
except OverflowError:
|
||||
raise Exception(f"Out of range floating value for option {self.__class__.__name__}: {value}")
|
||||
if self.value < self.range_start:
|
||||
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}")
|
||||
if self.value > self.range_end:
|
||||
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__}")
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> typing.Any:
|
||||
return cls(text)
|
||||
|
||||
@classmethod
|
||||
def weighted_range(cls, text: str) -> float:
|
||||
if text == "random-low":
|
||||
return random.triangular(cls.range_start, cls.range_end, cls.range_start)
|
||||
elif text == "random-high":
|
||||
return random.triangular(cls.range_start, cls.range_end, cls.range_end)
|
||||
elif text == "random-middle":
|
||||
return random.triangular(cls.range_start, cls.range_end)
|
||||
elif text.startswith("random-range-"):
|
||||
return cls.custom_range(text)
|
||||
elif text == "random":
|
||||
return random.uniform(cls.range_start, cls.range_end)
|
||||
else:
|
||||
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
|
||||
f"Acceptable values are: random, random-high, random-middle, random-low, "
|
||||
f"random-range-low-<min>-<max>, random-range-middle-<min>-<max>, "
|
||||
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
|
||||
|
||||
@classmethod
|
||||
def custom_range(cls, text: str) -> float:
|
||||
textsplit = text.split("-")
|
||||
try:
|
||||
random_range = [float(textsplit[len(textsplit) - 2]), float(textsplit[len(textsplit) - 1])]
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid random range {text} for option {cls.__name__}")
|
||||
except OverflowError:
|
||||
raise Exception(f"Out of range floating value for option {cls.__name__}: {text}")
|
||||
random_range.sort()
|
||||
if random_range[0] < cls.range_start or random_range[1] > cls.range_end:
|
||||
raise Exception(
|
||||
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
||||
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
|
||||
if text.startswith("random-range-low"):
|
||||
return random.triangular(random_range[0], random_range[1], random_range[0])
|
||||
elif text.startswith("random-range-middle"):
|
||||
return random.triangular(random_range[0], random_range[1])
|
||||
elif text.startswith("random-range-high"):
|
||||
return random.triangular(random_range[0], random_range[1], random_range[1])
|
||||
else:
|
||||
return random.uniform(random_range[0], random_range[1])
|
||||
|
||||
@property
|
||||
def current_key(self) -> str:
|
||||
return str(self.value)
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: float) -> str:
|
||||
return str(value)
|
||||
|
||||
def __eq__(self, other: typing.Any):
|
||||
if isinstance(other, NumericOption):
|
||||
return self.value == other.value
|
||||
else:
|
||||
return typing.cast(bool, self.value == other)
|
||||
|
||||
def __lt__(self, other: typing.Union[int, float, NumericOption]) -> bool:
|
||||
if isinstance(other, NumericOption):
|
||||
return self.value < other.value
|
||||
else:
|
||||
return self.value < other
|
||||
|
||||
def __le__(self, other: typing.Union[int, float, NumericOption]) -> bool:
|
||||
if isinstance(other, NumericOption):
|
||||
return self.value <= other.value
|
||||
else:
|
||||
return self.value <= other
|
||||
|
||||
def __gt__(self, other: typing.Union[int, float, NumericOption]) -> bool:
|
||||
if isinstance(other, NumericOption):
|
||||
return self.value > other.value
|
||||
else:
|
||||
return self.value > other
|
||||
|
||||
def __ge__(self, other: typing.Union[int, float, NumericOption]) -> bool:
|
||||
if isinstance(other, NumericOption):
|
||||
return self.value >= other.value
|
||||
else:
|
||||
return self.value >= other
|
||||
|
||||
def __int__(self) -> int:
|
||||
return int(self.value)
|
||||
|
||||
def __and__(self, other: typing.Any) -> int:
|
||||
raise TypeError("& operator not supported for float values")
|
||||
|
||||
def __floordiv__(self, other: typing.Any) -> int:
|
||||
return int(self.value // float(other))
|
||||
|
||||
def __invert__(self) -> int:
|
||||
raise TypeError("~ operator not supported for float values")
|
||||
|
||||
def __lshift__(self, other: typing.Any) -> int:
|
||||
raise TypeError("<< operator not supported for float values")
|
||||
|
||||
def __mod__(self, other: typing.Any) -> float:
|
||||
return self.value % float(other)
|
||||
|
||||
def __neg__(self) -> float:
|
||||
return -self.value
|
||||
|
||||
def __or__(self, other: typing.Any) -> int:
|
||||
raise TypeError("| operator not supported for float values")
|
||||
|
||||
def __pos__(self) -> float:
|
||||
return +self.value
|
||||
|
||||
def __rand__(self, other: typing.Any) -> int:
|
||||
raise TypeError("& operator not supported for float values")
|
||||
|
||||
def __rfloordiv__(self, other: typing.Any) -> int:
|
||||
return int(float(other) // self.value)
|
||||
|
||||
def __rlshift__(self, other: typing.Any) -> int:
|
||||
raise TypeError("<< operator not supported for float values")
|
||||
|
||||
def __rmod__(self, other: typing.Any) -> float:
|
||||
return float(other) % self.value
|
||||
|
||||
def __ror__(self, other: typing.Any) -> int:
|
||||
raise TypeError("| operator not supported for float values")
|
||||
|
||||
def __round__(self, ndigits: typing.Optional[int] = None) -> float:
|
||||
return round(self.value, ndigits)
|
||||
|
||||
def __rpow__(self, base: typing.Any) -> typing.Any:
|
||||
return base ** self.value
|
||||
|
||||
def __rrshift__(self, other: typing.Any) -> int:
|
||||
raise TypeError(">> operator not supported for float values")
|
||||
|
||||
def __rshift__(self, other: typing.Any) -> int:
|
||||
raise TypeError(">> operator not supported for float values")
|
||||
|
||||
def __rxor__(self, other: typing.Any) -> int:
|
||||
raise TypeError("^ operator not supported for float values")
|
||||
|
||||
def __xor__(self, other: typing.Any) -> int:
|
||||
raise TypeError("^ operator not supported for float values")
|
||||
0
worlds/shapez/data/__init__.py
Normal file
0
worlds/shapez/data/__init__.py
Normal file
134
worlds/shapez/data/generate.py
Normal file
134
worlds/shapez/data/generate.py
Normal file
@@ -0,0 +1,134 @@
|
||||
import itertools
|
||||
import time
|
||||
from typing import Dict, List
|
||||
|
||||
from worlds.shapez.data.strings import SHAPESANITY, REGIONS
|
||||
|
||||
shapesanity_simple: Dict[str, str] = {}
|
||||
shapesanity_1_4: Dict[str, str] = {}
|
||||
shapesanity_two_sided: Dict[str, str] = {}
|
||||
shapesanity_three_parts: Dict[str, str] = {}
|
||||
shapesanity_four_parts: Dict[str, str] = {}
|
||||
subshape_names = [SHAPESANITY.circle, SHAPESANITY.square, SHAPESANITY.star, SHAPESANITY.windmill]
|
||||
color_names = [SHAPESANITY.red, SHAPESANITY.blue, SHAPESANITY.green, SHAPESANITY.yellow, SHAPESANITY.purple,
|
||||
SHAPESANITY.cyan, SHAPESANITY.white, SHAPESANITY.uncolored]
|
||||
short_subshapes = ["C", "R", "S", "W"]
|
||||
short_colors = ["b", "c", "g", "p", "r", "u", "w", "y"]
|
||||
|
||||
|
||||
def color_to_needed_building(color_list: List[str]) -> str:
|
||||
for next_color in color_list:
|
||||
if next_color in [SHAPESANITY.yellow, SHAPESANITY.purple, SHAPESANITY.cyan, SHAPESANITY.white,
|
||||
"y", "p", "c", "w"]:
|
||||
return REGIONS.mixed
|
||||
for next_color in color_list:
|
||||
if next_color not in [SHAPESANITY.uncolored, "u"]:
|
||||
return REGIONS.painted
|
||||
return REGIONS.uncol
|
||||
|
||||
|
||||
def generate_shapesanity_pool() -> None:
|
||||
# same shapes && same color
|
||||
for color in color_names:
|
||||
color_region = color_to_needed_building([color])
|
||||
shapesanity_simple[SHAPESANITY.full(color, SHAPESANITY.circle)] = REGIONS.sanity(REGIONS.full, color_region)
|
||||
shapesanity_simple[SHAPESANITY.full(color, SHAPESANITY.square)] = REGIONS.sanity(REGIONS.full, color_region)
|
||||
shapesanity_simple[SHAPESANITY.full(color, SHAPESANITY.star)] = REGIONS.sanity(REGIONS.full, color_region)
|
||||
shapesanity_simple[SHAPESANITY.full(color, SHAPESANITY.windmill)] = REGIONS.sanity(REGIONS.east_wind, color_region)
|
||||
for shape in subshape_names:
|
||||
for color in color_names:
|
||||
color_region = color_to_needed_building([color])
|
||||
shapesanity_simple[SHAPESANITY.half(color, shape)] = REGIONS.sanity(REGIONS.half, color_region)
|
||||
shapesanity_simple[SHAPESANITY.piece(color, shape)] = REGIONS.sanity(REGIONS.piece, color_region)
|
||||
shapesanity_simple[SHAPESANITY.cutout(color, shape)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||
shapesanity_simple[SHAPESANITY.cornered(color, shape)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||
|
||||
# one color && 4 shapes (including empty)
|
||||
for first_color, second_color, third_color, fourth_color in itertools.combinations(short_colors+["-"], 4):
|
||||
colors = [first_color, second_color, third_color, fourth_color]
|
||||
color_region = color_to_needed_building(colors)
|
||||
shape_regions = [REGIONS.stitched, REGIONS.stitched] if fourth_color == "-" else [REGIONS.col_full, REGIONS.col_east_wind]
|
||||
color_code = ''.join(colors)
|
||||
shapesanity_1_4[SHAPESANITY.full(color_code, SHAPESANITY.circle)] = REGIONS.sanity(shape_regions[0], color_region)
|
||||
shapesanity_1_4[SHAPESANITY.full(color_code, SHAPESANITY.square)] = REGIONS.sanity(shape_regions[0], color_region)
|
||||
shapesanity_1_4[SHAPESANITY.full(color_code, SHAPESANITY.star)] = REGIONS.sanity(shape_regions[0], color_region)
|
||||
shapesanity_1_4[SHAPESANITY.full(color_code, SHAPESANITY.windmill)] = REGIONS.sanity(shape_regions[1], color_region)
|
||||
|
||||
# one shape && 4 colors (including empty)
|
||||
for first_shape, second_shape, third_shape, fourth_shape in itertools.combinations(short_subshapes+["-"], 4):
|
||||
for color in color_names:
|
||||
shapesanity_1_4[SHAPESANITY.full(color, ''.join([first_shape, second_shape, third_shape, fourth_shape]))] \
|
||||
= REGIONS.sanity(REGIONS.stitched, color_to_needed_building([color]))
|
||||
|
||||
combos = [shape + color for shape in short_subshapes for color in short_colors]
|
||||
for first_combo, second_combo in itertools.permutations(combos, 2):
|
||||
# 2-sided shapes
|
||||
color_region = color_to_needed_building([first_combo[1], second_combo[1]])
|
||||
ordered_combo = " ".join(sorted([first_combo, second_combo]))
|
||||
shape_regions = (([REGIONS.east_wind, REGIONS.east_wind, REGIONS.col_half]
|
||||
if first_combo[0] == "W" else [REGIONS.col_full, REGIONS.col_full, REGIONS.col_half])
|
||||
if first_combo[0] == second_combo[0] else [REGIONS.stitched, REGIONS.half_half, REGIONS.stitched])
|
||||
shapesanity_two_sided[SHAPESANITY.three_one(first_combo, second_combo)] = REGIONS.sanity(shape_regions[0], color_region)
|
||||
shapesanity_two_sided[SHAPESANITY.halfhalf(ordered_combo)] = REGIONS.sanity(shape_regions[1], color_region)
|
||||
shapesanity_two_sided[SHAPESANITY.checkered(ordered_combo)] = REGIONS.sanity(shape_regions[0], color_region)
|
||||
shapesanity_two_sided[SHAPESANITY.singles(ordered_combo, SHAPESANITY.adjacent_pos)] = REGIONS.sanity(shape_regions[2], color_region)
|
||||
shapesanity_two_sided[SHAPESANITY.singles(ordered_combo, SHAPESANITY.cornered_pos)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||
shapesanity_two_sided[SHAPESANITY.two_one(first_combo, second_combo, SHAPESANITY.adjacent_pos)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||
shapesanity_two_sided[SHAPESANITY.two_one(first_combo, second_combo, SHAPESANITY.cornered_pos)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||
for third_combo in combos:
|
||||
if third_combo in [first_combo, second_combo]:
|
||||
continue
|
||||
# 3-part shapes
|
||||
colors = [first_combo[1], second_combo[1], third_combo[1]]
|
||||
color_region = color_to_needed_building(colors)
|
||||
ordered_two = " ".join(sorted([second_combo, third_combo]))
|
||||
if not (first_combo[1] == second_combo[1] == third_combo[1] or
|
||||
first_combo[0] == second_combo[0] == third_combo[0]):
|
||||
ordered_all = " ".join(sorted([first_combo, second_combo, third_combo]))
|
||||
shapesanity_three_parts[SHAPESANITY.singles(ordered_all)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||
shape_regions = ([REGIONS.stitched, REGIONS.stitched] if not second_combo[0] == third_combo[0]
|
||||
else (([REGIONS.east_wind, REGIONS.east_wind] if first_combo[0] == "W"
|
||||
else [REGIONS.col_full, REGIONS.col_full])
|
||||
if first_combo[0] == second_combo[0] else [REGIONS.col_half_half, REGIONS.stitched]))
|
||||
shapesanity_three_parts[SHAPESANITY.two_one_one(first_combo, ordered_two, SHAPESANITY.adjacent_pos)] \
|
||||
= REGIONS.sanity(shape_regions[0], color_region)
|
||||
shapesanity_three_parts[SHAPESANITY.two_one_one(first_combo, ordered_two, SHAPESANITY.cornered_pos)] \
|
||||
= REGIONS.sanity(shape_regions[1], color_region)
|
||||
for fourth_combo in combos:
|
||||
if fourth_combo in [first_combo, second_combo, third_combo]:
|
||||
continue
|
||||
if (first_combo[1] == second_combo[1] == third_combo[1] == fourth_combo[1] or
|
||||
first_combo[0] == second_combo[0] == third_combo[0] == fourth_combo[0]):
|
||||
continue
|
||||
colors = [first_combo[1], second_combo[1], third_combo[1], fourth_combo[1]]
|
||||
color_region = color_to_needed_building(colors)
|
||||
ordered_all = " ".join(sorted([first_combo, second_combo, third_combo, fourth_combo]))
|
||||
if ((first_combo[0] == second_combo[0] and third_combo[0] == fourth_combo[0]) or
|
||||
(first_combo[0] == third_combo[0] and second_combo[0] == fourth_combo[0]) or
|
||||
(first_combo[0] == fourth_combo[0] and third_combo[0] == second_combo[0])):
|
||||
shapesanity_four_parts[SHAPESANITY.singles(ordered_all)] = REGIONS.sanity(REGIONS.col_half_half, color_region)
|
||||
else:
|
||||
shapesanity_four_parts[SHAPESANITY.singles(ordered_all)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
start = time.time()
|
||||
generate_shapesanity_pool()
|
||||
print(time.time() - start)
|
||||
with open("shapesanity_pool.py", "w") as outfile:
|
||||
outfile.writelines(["shapesanity_simple = {\n"]
|
||||
+ [f" \"{name}\": \"{shapesanity_simple[name]}\",\n"
|
||||
for name in shapesanity_simple]
|
||||
+ ["}\n\nshapesanity_1_4 = {\n"]
|
||||
+ [f" \"{name}\": \"{shapesanity_1_4[name]}\",\n"
|
||||
for name in shapesanity_1_4]
|
||||
+ ["}\n\nshapesanity_two_sided = {\n"]
|
||||
+ [f" \"{name}\": \"{shapesanity_two_sided[name]}\",\n"
|
||||
for name in shapesanity_two_sided]
|
||||
+ ["}\n\nshapesanity_three_parts = {\n"]
|
||||
+ [f" \"{name}\": \"{shapesanity_three_parts[name]}\",\n"
|
||||
for name in shapesanity_three_parts]
|
||||
+ ["}\n\nshapesanity_four_parts = {\n"]
|
||||
+ [f" \"{name}\": \"{shapesanity_four_parts[name]}\",\n"
|
||||
for name in shapesanity_four_parts]
|
||||
+ ["}\n"])
|
||||
4
worlds/shapez/data/options.json
Normal file
4
worlds/shapez/data/options.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"max_levels_and_upgrades": 500,
|
||||
"max_shapesanity": 1000
|
||||
}
|
||||
75814
worlds/shapez/data/shapesanity_pool.py
Normal file
75814
worlds/shapez/data/shapesanity_pool.py
Normal file
File diff suppressed because it is too large
Load Diff
337
worlds/shapez/data/strings.py
Normal file
337
worlds/shapez/data/strings.py
Normal file
@@ -0,0 +1,337 @@
|
||||
|
||||
class OTHER:
|
||||
game_name = "shapez"
|
||||
|
||||
|
||||
class SLOTDATA:
|
||||
goal = "goal"
|
||||
maxlevel = "maxlevel"
|
||||
finaltier = "finaltier"
|
||||
req_shapes_mult = "required_shapes_multiplier"
|
||||
allow_float_layers = "allow_floating_layers"
|
||||
rand_level_req = "randomize_level_requirements"
|
||||
rand_upgrade_req = "randomize_upgrade_requirements"
|
||||
rand_level_logic = "randomize_level_logic"
|
||||
rand_upgrade_logic = "randomize_upgrade_logic"
|
||||
throughput_levels_ratio = "throughput_levels_ratio"
|
||||
comp_growth_gradient = "complexity_growth_gradient"
|
||||
same_late = "same_late_upgrade_requirements"
|
||||
toolbar_shuffling = "toolbar_shuffling"
|
||||
seed = "seed"
|
||||
shapesanity = "shapesanity"
|
||||
|
||||
@staticmethod
|
||||
def level_building(number: int) -> str:
|
||||
return f"Level building {number}"
|
||||
|
||||
@staticmethod
|
||||
def upgrade_building(number: int) -> str:
|
||||
return f"Upgrade building {number}"
|
||||
|
||||
@staticmethod
|
||||
def phase_length(number: int) -> str:
|
||||
return f"Phase {number} length"
|
||||
|
||||
@staticmethod
|
||||
def cat_buildings_amount(category: str) -> str:
|
||||
return f"{category} category buildings amount"
|
||||
|
||||
|
||||
class GOALS:
|
||||
vanilla = "vanilla"
|
||||
mam = "mam"
|
||||
even_fasterer = "even_fasterer"
|
||||
efficiency_iii = "efficiency_iii"
|
||||
|
||||
|
||||
class CATEGORY:
|
||||
belt = "Belt"
|
||||
miner = "Miner"
|
||||
processors = "Processors"
|
||||
painting = "Painting"
|
||||
random = "Random"
|
||||
belt_low = "belt"
|
||||
miner_low = "miner"
|
||||
processors_low = "processors"
|
||||
painting_low = "painting"
|
||||
big = "Big"
|
||||
small = "Small"
|
||||
gigantic = "Gigantic"
|
||||
rising = "Rising"
|
||||
demonic = "Demonic"
|
||||
|
||||
|
||||
class OPTIONS:
|
||||
logic_vanilla = "vanilla"
|
||||
logic_stretched = "stretched"
|
||||
logic_quick = "quick"
|
||||
logic_random_steps = "random_steps"
|
||||
logic_hardcore = "hardcore"
|
||||
logic_dopamine = "dopamine"
|
||||
logic_dopamine_overflow = "dopamine_overflow"
|
||||
logic_vanilla_like = "vanilla_like"
|
||||
logic_linear = "linear"
|
||||
logic_category = "category"
|
||||
logic_category_random = "category_random"
|
||||
logic_shuffled = "shuffled"
|
||||
sphere_1 = "sphere_1"
|
||||
buildings_3 = "3_buildings"
|
||||
buildings_5 = "5_buildings"
|
||||
|
||||
|
||||
class REGIONS:
|
||||
menu = "Menu"
|
||||
belt = "Shape transportation"
|
||||
extract = "Shape extraction"
|
||||
main = "Main"
|
||||
levels_1 = "Levels with 1 building"
|
||||
levels_2 = "Levels with 2 buildings"
|
||||
levels_3 = "Levels with 3 buildings"
|
||||
levels_4 = "Levels with 4 buildings"
|
||||
levels_5 = "Levels with 5 buildings"
|
||||
upgrades_1 = "Upgrades with 1 building"
|
||||
upgrades_2 = "Upgrades with 2 buildings"
|
||||
upgrades_3 = "Upgrades with 3 buildings"
|
||||
upgrades_4 = "Upgrades with 4 buildings"
|
||||
upgrades_5 = "Upgrades with 5 buildings"
|
||||
paint_not_quad = "Achievements with (double) painter"
|
||||
cut_not_quad = "Achievements with half cutter"
|
||||
rotate_cw = "Achievements with clockwise rotator"
|
||||
stack_shape = "Achievements with stacker"
|
||||
store_shape = "Achievements with storage"
|
||||
trash_shape = "Achievements with trash"
|
||||
blueprint = "Achievements with blueprints"
|
||||
wiring = "Achievements with wires"
|
||||
mam = "Achievements needing a MAM"
|
||||
any_building = "Achievements with any placeable building"
|
||||
all_buildings = "Achievements with all main buildings"
|
||||
all_buildings_x1_6_belt = "Achievements with x1.6 belt speed"
|
||||
full = "Full"
|
||||
half = "Half"
|
||||
piece = "Piece"
|
||||
stitched = "Stitched"
|
||||
east_wind = "East Windmill"
|
||||
half_half = "Half-Half"
|
||||
col_east_wind = "Colorful East Windmill"
|
||||
col_half_half = "Colorful Half-Half"
|
||||
col_full = "Colorful Full"
|
||||
col_half = "Colorful Half"
|
||||
uncol = "Uncolored"
|
||||
painted = "Painted"
|
||||
mixed = "Mixed"
|
||||
|
||||
@staticmethod
|
||||
def sanity(processing: str, coloring: str):
|
||||
return f"Shapesanity {processing} {coloring}"
|
||||
|
||||
|
||||
class LOCATIONS:
|
||||
my_eyes = "My eyes no longer hurt"
|
||||
painter = "Painter"
|
||||
cutter = "Cutter"
|
||||
rotater = "Rotater"
|
||||
wait_they_stack = "Wait, they stack?"
|
||||
wires = "Wires"
|
||||
storage = "Storage"
|
||||
freedom = "Freedom"
|
||||
the_logo = "The logo!"
|
||||
to_the_moon = "To the moon"
|
||||
its_piling_up = "It's piling up"
|
||||
use_it_later = "I'll use it later"
|
||||
efficiency_1 = "Efficiency 1"
|
||||
preparing_to_launch = "Preparing to launch"
|
||||
spacey = "SpaceY"
|
||||
stack_overflow = "Stack overflow"
|
||||
its_a_mess = "It's a mess"
|
||||
faster = "Faster"
|
||||
even_faster = "Even faster"
|
||||
get_rid_of_them = "Get rid of them"
|
||||
a_long_time = "It's been a long time"
|
||||
addicted = "Addicted"
|
||||
cant_stop = "Can't stop"
|
||||
is_this_the_end = "Is this the end?"
|
||||
getting_into_it = "Getting into it"
|
||||
now_its_easy = "Now it's easy"
|
||||
computer_guy = "Computer Guy"
|
||||
speedrun_master = "Speedrun Master"
|
||||
speedrun_novice = "Speedrun Novice"
|
||||
not_idle_game = "Not an idle game"
|
||||
efficiency_2 = "Efficiency 2"
|
||||
branding_1 = "Branding specialist 1"
|
||||
branding_2 = "Branding specialist 2"
|
||||
king_of_inefficiency = "King of Inefficiency"
|
||||
its_so_slow = "It's so slow"
|
||||
mam = "MAM (Make Anything Machine)"
|
||||
perfectionist = "Perfectionist"
|
||||
next_dimension = "The next dimension"
|
||||
oops = "Oops"
|
||||
copy_pasta = "Copy-Pasta"
|
||||
ive_seen_that_before = "I've seen that before ..."
|
||||
memories = "Memories from the past"
|
||||
i_need_trains = "I need trains"
|
||||
a_bit_early = "A bit early?"
|
||||
gps = "GPS"
|
||||
goal = "Goal"
|
||||
|
||||
@staticmethod
|
||||
def level(number: int, additional: int = 0) -> str:
|
||||
if not additional:
|
||||
return f"Level {number}"
|
||||
elif additional == 1:
|
||||
return f"Level {number} Additional"
|
||||
else:
|
||||
return f"Level {number} Additional {additional}"
|
||||
|
||||
@staticmethod
|
||||
def upgrade(category: str, tier: str) -> str:
|
||||
return f"{category} Upgrade Tier {tier}"
|
||||
|
||||
@staticmethod
|
||||
def shapesanity(number: int) -> str:
|
||||
return f"Shapesanity {number}"
|
||||
|
||||
|
||||
class ITEMS:
|
||||
cutter = "Cutter"
|
||||
cutter_quad = "Quad Cutter"
|
||||
rotator = "Rotator"
|
||||
rotator_ccw = "Rotator (CCW)"
|
||||
rotator_180 = "Rotator (180°)"
|
||||
stacker = "Stacker"
|
||||
painter = "Painter"
|
||||
painter_double = "Double Painter"
|
||||
painter_quad = "Quad Painter"
|
||||
color_mixer = "Color Mixer"
|
||||
|
||||
belt = "Belt"
|
||||
extractor = "Extractor"
|
||||
extractor_chain = "Chaining Extractor"
|
||||
balancer = "Balancer"
|
||||
comp_merger = "Compact Merger"
|
||||
comp_splitter = "Compact Splitter"
|
||||
tunnel = "Tunnel"
|
||||
tunnel_tier_ii = "Tunnel Tier II"
|
||||
trash = "Trash"
|
||||
|
||||
belt_reader = "Belt Reader"
|
||||
storage = "Storage"
|
||||
switch = "Switch"
|
||||
item_filter = "Item Filter"
|
||||
display = "Display"
|
||||
wires = "Wires"
|
||||
const_signal = "Constant Signal"
|
||||
logic_gates = "Logic Gates"
|
||||
virtual_proc = "Virtual Processing"
|
||||
blueprints = "Blueprints"
|
||||
|
||||
upgrade_big_belt = "Big Belt Upgrade"
|
||||
upgrade_big_miner = "Big Miner Upgrade"
|
||||
upgrade_big_proc = "Big Processors Upgrade"
|
||||
upgrade_big_paint = "Big Painting Upgrade"
|
||||
upgrade_small_belt = "Small Belt Upgrade"
|
||||
upgrade_small_miner = "Small Miner Upgrade"
|
||||
upgrade_small_proc = "Small Processors Upgrade"
|
||||
upgrade_small_paint = "Small Painting Upgrade"
|
||||
upgrade_gigantic_belt = "Gigantic Belt Upgrade"
|
||||
upgrade_gigantic_miner = "Gigantic Miner Upgrade"
|
||||
upgrade_gigantic_proc = "Gigantic Processors Upgrade"
|
||||
upgrade_gigantic_paint = "Gigantic Painting Upgrade"
|
||||
upgrade_rising_belt = "Rising Belt Upgrade"
|
||||
upgrade_rising_miner = "Rising Miner Upgrade"
|
||||
upgrade_rising_proc = "Rising Processors Upgrade"
|
||||
upgrade_rising_paint = "Rising Painting Upgrade"
|
||||
trap_upgrade_belt = "Belt Upgrade Trap"
|
||||
trap_upgrade_miner = "Miner Upgrade Trap"
|
||||
trap_upgrade_proc = "Processors Upgrade Trap"
|
||||
trap_upgrade_paint = "Painting Upgrade Trap"
|
||||
trap_upgrade_demonic_belt = "Demonic Belt Upgrade Trap"
|
||||
trap_upgrade_demonic_miner = "Demonic Miner Upgrade Trap"
|
||||
trap_upgrade_demonic_proc = "Demonic Processors Upgrade Trap"
|
||||
trap_upgrade_demonic_paint = "Demonic Painting Upgrade Trap"
|
||||
upgrade_big_random = "Big Random Upgrade"
|
||||
upgrade_small_random = "Small Random Upgrade"
|
||||
|
||||
@staticmethod
|
||||
def upgrade(size: str, category: str) -> str:
|
||||
return f"{size} {category} Upgrade"
|
||||
|
||||
@staticmethod
|
||||
def trap_upgrade(category: str, size: str = "") -> str:
|
||||
return f"{size} {category} Upgrade Trap".strip()
|
||||
|
||||
bundle_blueprint = "Blueprint Shapes Bundle"
|
||||
bundle_level = "Level Shapes Bundle"
|
||||
bundle_upgrade = "Upgrade Shapes Bundle"
|
||||
|
||||
trap_locked = "Locked Building Trap"
|
||||
trap_throttled = "Throttled Building Trap"
|
||||
trap_malfunction = "Malfunctioning Trap"
|
||||
trap_inflation = "Inflation Trap"
|
||||
trap_draining_inv = "Inventory Draining Trap"
|
||||
trap_draining_blueprint = "Blueprint Shapes Draining Trap"
|
||||
trap_draining_level = "Level Shapes Draining Trap"
|
||||
trap_draining_upgrade = "Upgrade Shapes Draining Trap"
|
||||
trap_clear_belts = "Belts Clearing Trap"
|
||||
|
||||
goal = "Goal"
|
||||
|
||||
|
||||
class SHAPESANITY:
|
||||
circle = "Circle"
|
||||
square = "Square"
|
||||
star = "Star"
|
||||
windmill = "Windmill"
|
||||
red = "Red"
|
||||
blue = "Blue"
|
||||
green = "Green"
|
||||
yellow = "Yellow"
|
||||
purple = "Purple"
|
||||
cyan = "Cyan"
|
||||
white = "White"
|
||||
uncolored = "Uncolored"
|
||||
adjacent_pos = "Adjacent"
|
||||
cornered_pos = "Cornered"
|
||||
|
||||
@staticmethod
|
||||
def full(color: str, subshape: str):
|
||||
return f"{color} {subshape}"
|
||||
|
||||
@staticmethod
|
||||
def half(color: str, subshape: str):
|
||||
return f"Half {color} {subshape}"
|
||||
|
||||
@staticmethod
|
||||
def piece(color: str, subshape: str):
|
||||
return f"{color} {subshape} Piece"
|
||||
|
||||
@staticmethod
|
||||
def cutout(color: str, subshape: str):
|
||||
return f"Cut Out {color} {subshape}"
|
||||
|
||||
@staticmethod
|
||||
def cornered(color: str, subshape: str):
|
||||
return f"Cornered {color} {subshape}"
|
||||
|
||||
@staticmethod
|
||||
def three_one(first: str, second: str):
|
||||
return f"3-1 {first} {second}"
|
||||
|
||||
@staticmethod
|
||||
def halfhalf(combo: str):
|
||||
return f"Half-Half {combo}"
|
||||
|
||||
@staticmethod
|
||||
def checkered(combo: str):
|
||||
return f"Checkered {combo}"
|
||||
|
||||
@staticmethod
|
||||
def singles(combo: str, position: str = ""):
|
||||
return f"{position} Singles {combo}".strip()
|
||||
|
||||
@staticmethod
|
||||
def two_one(first: str, second: str, position: str):
|
||||
return f"{position} 2-1 {first} {second}"
|
||||
|
||||
@staticmethod
|
||||
def two_one_one(first: str, second: str, position: str):
|
||||
return f"{position} 2-1-1 {first} {second}"
|
||||
35
worlds/shapez/docs/datapackage_settings_de.md
Normal file
35
worlds/shapez/docs/datapackage_settings_de.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Anleitung zum Ändern der maximalen Anzahl an Locations in shapez
|
||||
|
||||
## Wo finde ich die Einstellungen zum Erhöhen/Verringern der maximalen Anzahl an Locations?
|
||||
|
||||
Die Maximalwerte von `goal_amount` und `shapesanity_amount` sind fest eingebaute Einstellungen, die das Datenpaket des
|
||||
Spiels beeinflussen. Sie sind in einer Datei names `options.json` innerhalb der APWorld festgelegt. Durch das Ändern
|
||||
dieser Werte erschaffst du eine custom APWorld, die nur auf deinem PC existiert.
|
||||
|
||||
## Wie du die Datenpaket-Einstellungen änderst
|
||||
|
||||
Diese Anleitung ist für erfahrene Nutzer und kann in nicht richtig funktionierender Software resultieren, wenn sie nicht
|
||||
ordnungsgemäß befolgt wird. Anwendung auf eigene Gefahr.
|
||||
|
||||
1. Navigiere zu `<AP-Installation>/lib/worlds`.
|
||||
2. Benenne `shapez.apworld` zu `shapez.zip` um.
|
||||
3. Öffne die Zip-Datei und navigiere zu `shapez/data/options.json`.
|
||||
4. Ändere die Werte in dieser Datei nach Belieben und speichere die Datei.
|
||||
- `max_shapesanity` kann nicht weniger als `4` sein, da dies die benötigte Mindestanzahl zum Verhindern von
|
||||
FillErrors ist.
|
||||
- `max_shapesanity` kann auch nicht mehr als `75800` sein, da dies die maximale Anzahl an möglichen Shapesanity-Namen
|
||||
ist. Ansonsten könnte die Generierung der Multiworld fehlschlagen.
|
||||
- `max_levels_and_upgrades` kann nicht weniger als `27` sein, da dies die Mindestanzahl für das `mam`-Ziel ist.
|
||||
5. Schließe die Zip-Datei und benenne sie zurück zu `shapez.apworld`.
|
||||
|
||||
## Warum muss ich das ganze selbst machen?
|
||||
|
||||
Alle Spiele in Archipelago müssen eine Liste aller möglichen Locations **unabhängig der Spieler-Optionen**
|
||||
bereitstellen. Diese Listen aller in einer Multiworld inkludierten Spiele werden in den Daten der Multiworld gespeichert
|
||||
und an alle verbundenen Clients gesendet. Je mehr mögliche Locations, desto größer das Datenpaket. Und mit ~80000
|
||||
möglichen Locations hatte shapez zu einem gewissen Zeitpunkt ein (von der Datenmenge her) größeres Datenpaket als alle
|
||||
supporteten Spiele zusammen. Um also diese Datenmenge zu reduzieren wurden die ausgeschriebenen
|
||||
Shapesanity-Locations-Namen (`Shapesanity Uncolored Circle`, `Shapesanity Blue Rectangle`, ...) durch standardisierte
|
||||
Namen (`Shapesanity 1`, `Shapesanity 2`, ...) ersetzt. Durch das Ändern dieser Maximalwerte, und damit das Erstellen
|
||||
einer custom APWorld, kannst du die Anzahl der möglichen Locations erhöhen, wirst aber auch gleichzeitig das Datenpaket
|
||||
vergrößern.
|
||||
33
worlds/shapez/docs/datapackage_settings_en.md
Normal file
33
worlds/shapez/docs/datapackage_settings_en.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Guide to change maximum locations in shapez
|
||||
|
||||
## Where do I find the settings to increase/decrease the amount of possible locations?
|
||||
|
||||
The maximum values of the `goal_amount` and `shapesanity_amount` are hardcoded settings that affect the datapackage.
|
||||
They are stored in a file called `options.json` inside the apworld. By changing them, you will create a custom apworld
|
||||
on your local machine.
|
||||
|
||||
## How to change datapackage options
|
||||
|
||||
This tutorial is for advanced users and can result in the software not working properly, if not read carefully.
|
||||
Proceed at your own risk.
|
||||
|
||||
1. Go to `<AP installation>/lib/worlds`.
|
||||
2. Rename `shapez.apworld` to `shapez.zip`.
|
||||
3. Open the zip file and go to `shapez/data/options.json`.
|
||||
4. Edit the values in this file to your desire and save the file.
|
||||
- `max_shapesanity` cannot be lower than `4`, as this is the minimum amount to prevent FillErrors.
|
||||
- `max_shapesanity` also cannot be higher than `75800`, as this is the maximum amount of possible shapesanity names.
|
||||
Else the multiworld generation might fail.
|
||||
- `max_levels_and_upgrades` cannot be lower than `27`, as this is the minimum amount for the `mam` goal to properly
|
||||
work.
|
||||
5. Close the zip and rename it back to `shapez.apworld`.
|
||||
|
||||
## Why do I have to do this manually?
|
||||
|
||||
For every game in Archipelago, there must be a list of all possible locations, **regardless of player options**. When
|
||||
generating a multiworld, a list of all locations of all included games will be saved in the multiworld data and sent to
|
||||
all clients. The higher the amount of possible locations, the bigger the datapackage. And having ~80000 possible
|
||||
locations at one point made the datapackage for shapez bigger than all other supported games combined. So to reduce the
|
||||
datapackage of shapez, the locations for shapesanity are named `Shapesanity 1`, `Shapesanity 2` etc. instead of their
|
||||
actual names. By creating a custom apworld, you can increase the amount of possible locations, but you will also
|
||||
increase the size of the datapackage at the same time.
|
||||
71
worlds/shapez/docs/de_shapez.md
Normal file
71
worlds/shapez/docs/de_shapez.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# shapez
|
||||
|
||||
## Was für ein Spiel ist das?
|
||||
|
||||
shapez ist ein Automatisierungsspiel, in dem du Formen aus zufällig generierten Vorkommen in einer endlosen Welt
|
||||
extrahierst, zerschneidest, rotierst, stapelst, anmalst und schließlich zum Zentrum beförderst, um Level abzuschließen
|
||||
und Upgrades zu kaufen. Das Tutorial beinhaltet 26 Level, in denen du (fast) immer ein neues Gebäude oder eine neue
|
||||
Spielmechanik freischaltest. Danach folgen endlos weitere Level mit zufällig generierten Vorgaben. Um das Spiel bzw.
|
||||
deine Gebäude schneller zu machen, kannst du bis zu 1000 Upgrades (pro Kategorie) kaufen.
|
||||
|
||||
## Wo ist die Optionen-Seite?
|
||||
|
||||
Die [Spieler-Optionen-Seite für dieses Spiel](../player-options) enthält alle Optionen zum Erstellen und exportieren
|
||||
einer YAML-Datei.
|
||||
Zusätzlich gibt es zu diesem Spiel "Datenpaket-Einstellungen", die du nach
|
||||
[dieser Anleitung](/tutorial/shapez/datapackage_settings/de) einstellen kannst.
|
||||
|
||||
## Inwiefern wird das Spiel randomisiert?
|
||||
|
||||
Alle Belohnungen aus den Tutorial-Level (das Freischalten von Gebäuden und Spielmechaniken) und Verbesserungen durch
|
||||
Upgrades werden dem Itempool der Multiworld hinzugefügt. Außerdem werden, wenn so in den Spieler-Optionen festgelegt,
|
||||
die Bedingungen zum Abschließen eines Levels und zum Kaufen der Upgrades randomisiert.
|
||||
|
||||
## Was ist das Ziel von shapez in Archipelago?
|
||||
|
||||
Da das Spiel eigentlich kein konkretes Ziel (nach dem Tutorial) hat, kann man sich zwischen (momentan) 4 verschiedenen
|
||||
Zielen entscheiden:
|
||||
1. Vanilla: Schließe Level 26 ab (eigentlich das Ende des Tutorials).
|
||||
2. MAM: Schließe ein bestimmtes Level nach Level 26 ab, das zuvor in den Spieler-Optionen festgelegt wurde. Es ist
|
||||
empfohlen, eine Maschine zu bauen, die alles automatisch herstellt ("Make-Anything-Machine", kurz MAM).
|
||||
3. Even Fasterer: Kaufe alle Upgrades bis zu einer in den Spieler-Optionen festgelegten Stufe (nach Stufe 8).
|
||||
4. Efficiency III: Liefere 256 Blaupausen-Formen pro Sekunde ins Zentrum.
|
||||
|
||||
## Welche Items können in den Welten anderer Spieler erscheinen?
|
||||
|
||||
- Freischalten verschiedener Gebäude
|
||||
- Blaupausen freischalten
|
||||
- Große Upgrades (addiert 1 zum Geschwindigkeitsmultiplikator)
|
||||
- Kleine Upgrades (addiert 0.1 zum Geschwindigkeitsmultiplikator)
|
||||
- Andere ungewöhnliche Upgrades (optional)
|
||||
- Verschiedene Bündel, die bestimmte Formen enthalten
|
||||
- Fallen, die bestimmte Formen aus dem Zentrum dränieren (ja, das Wort gibt es)
|
||||
- Fallen, die zufällige Gebäude oder andere Spielmechaniken betreffen
|
||||
|
||||
## Was ist eine Location / ein Check?
|
||||
|
||||
- Level (minimum 1-25, bis zu 499 je nach Spieler-Optionen, mit zusätzlichen Checks für Level 1 und 20)
|
||||
- Upgrades (minimum Stufen II-VIII (2-8), bis zu D (500) je nach Spieler-Optionen)
|
||||
- Bestimmte Formen mindestens einmal ins Zentrum liefern ("Shapesanity", bis zu 1000 zufällig gewählte Definitionen)
|
||||
- Errungenschaften (bis zu 45)
|
||||
|
||||
## Was passiert, wenn der Spieler ein Item erhält?
|
||||
|
||||
Ein Pop-Up erscheint, das das/die erhaltene(n) Item(s) und eventuell weitere Informationen auflistet.
|
||||
|
||||
## Was bedeuten die Namen dieser ganzen Shapesanity Dinger?
|
||||
|
||||
Hier ist ein Spicker für die Englischarbeit (bloß nicht dem Lehrer zeigen):
|
||||
|
||||

|
||||
|
||||
## Kann ich auch weitere Mods neben dem AP Client installieren?
|
||||
|
||||
Zurzeit wird Kompatibilität mit anderen Mods nicht unterstützt, aber niemand kann dich davon abhalten, es trotzdem zu
|
||||
versuchen. Mods, die das Gameplay verändern, werden wahrscheinlich nicht funktionieren, indem sie das Laden der
|
||||
jeweiligen Mods verhindern oder das Spiel zum Abstürzen bringen, während einfache QoL-Mods vielleicht problemlos
|
||||
funktionieren könnten. Wenn du es versuchst, dann also auf eigene Gefahr.
|
||||
|
||||
## Hast du wirklich eine deutschsprachige Infoseite geschrieben, obwohl man sie aktuell nur über Umwege erreichen kann und du eigentlich an dem Praktikumsportfolio arbeiten solltest?
|
||||
|
||||
Ja
|
||||
65
worlds/shapez/docs/en_shapez.md
Normal file
65
worlds/shapez/docs/en_shapez.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# shapez
|
||||
|
||||
## What is this game?
|
||||
|
||||
shapez is an automation game about cutting, rotating, stacking, and painting shapes, that you extract from randomly
|
||||
generated patches on an infinite canvas, and sending them to the hub to complete levels. The "tutorial", where you
|
||||
unlock a new building or game mechanic (almost) each level, lasts until level 26, where you unlock freeplay with
|
||||
infinitely more levels, that require a new, randomly generated shape. Alongside the levels, you can unlock upgrades,
|
||||
that make your buildings work faster.
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure
|
||||
and export a config file.
|
||||
There are also some advanced "datapackage settings" that can be changed by following
|
||||
[this guide](/tutorial/shapez/datapackage_settings/en).
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
Buildings and gameplay mechanics, that you normally unlock by completing a level, and upgrade improvements are put
|
||||
into the item pool of the multiworld. Also, if enabled, the requirements for completing a level or buying an upgrade are
|
||||
randomized.
|
||||
|
||||
## What is the goal of shapez in Archipelago?
|
||||
|
||||
As the game has no actual goal where the game ends, there are (currently) 4 different goals you can choose from in the
|
||||
player options:
|
||||
1. Vanilla: Complete level 26 (the end of the tutorial).
|
||||
2. MAM: Complete a player-specified level after level 26. It's recommended to build a Make-Anything-Machine (MAM).
|
||||
3. Even Fasterer: Upgrade everything to a player-specified tier after tier 8.
|
||||
4. Efficiency III: Deliver 256 blueprint shapes per second to the hub.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
|
||||
- Unlock different buildings
|
||||
- Unlock blueprints
|
||||
- Big upgrade improvements (adds 1 to the multiplier)
|
||||
- Small upgrade improvements (adds .1 to the multiplier)
|
||||
- Other unusual upgrade improvements (optional)
|
||||
- Different shapes bundles
|
||||
- Inventory draining traps
|
||||
- Different traps afflicting random buildings and game mechanics
|
||||
|
||||
## What is considered a location check?
|
||||
|
||||
- Levels (minimum 1-25, up to 499 depending on player options, with additional checks for levels 1 and 20)
|
||||
- Upgrades (minimum tiers II-VIII (2-8), up to D (500) depending on player options)
|
||||
- Delivering certain shapes at least once to the hub ("shapesanity", up to 1000 from a 75800 names pool)
|
||||
- Achievements (up to 45)
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
A pop-up will show, which item(s) were received, with additional information on some of them.
|
||||
|
||||
## What do the names of all these shapesanity locations mean?
|
||||
|
||||
Here's a cheat sheet:
|
||||
|
||||

|
||||
|
||||
## Can I use other mods alongside the AP client?
|
||||
|
||||
At the moment, compatibility with other mods is not supported, but not forbidden. Gameplay altering mods will most
|
||||
likely crash the game or disable loading the afflicted mods, while QoL mods might work without problems. Try at your own
|
||||
risk.
|
||||
62
worlds/shapez/docs/setup_de.md
Normal file
62
worlds/shapez/docs/setup_de.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Setup-Anleitung für shapez: Archipelago
|
||||
|
||||
## Schnelle Links
|
||||
|
||||
- Info-Seite zum Spiel
|
||||
* [English](/games/shapez/info/en)
|
||||
* [Deutsch](/games/shapez/info/de)
|
||||
- [Spieler-Optionen-Seite](/games/shapez/player-options)
|
||||
|
||||
## Benötigte Software
|
||||
|
||||
- Eine installierbare und aktuelle PC-Version von shapez ([Steam](https://store.steampowered.com/app/1318690/shapez/)).
|
||||
- Die shapezipelago Mod von der [mod.io-Seite](https://mod.io/g/shapez/m/shapezipelago).
|
||||
|
||||
## Optionale Software
|
||||
|
||||
- Archipelago von der [Archipelago-Release-Seite](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
* (Für den Text-Client)
|
||||
* (Alternativ kannst du auch die eingebaute Konsole (nur lesbar) nutzen, indem du beim Starten des Spiels den
|
||||
`-dev`-Parameter verwendest)
|
||||
- Universal Tracker (schau im `#future-game-design`-Thread für UT auf dem Discord-Server nach der aktuellen Anleitung)
|
||||
|
||||
## Installation
|
||||
|
||||
Da das Spiel einen eingebauten Mod-Loader hat, musst du nur die "shapezipelago@X.X.X.js"-Datei in den dafür vorgesehenen
|
||||
Ordner kopieren. Wenn du nicht weißt, wo dieser ist, dann öffne das Spiel, drücke auf "MODS" und schließlich auf
|
||||
"MODORDNER ÖFFNEN".
|
||||
|
||||
Du solltest (egal ob vor oder nach der Installation) die Einstellungen des Spiels öffnen und `HINWEISE & TUTORIALS` im
|
||||
Reiter `BENUTZEROBERFLÄCHE` ausschalten, da sie sonst den Upgrade-Shop verstecken wird, bis du ein paar Level
|
||||
abgeschlossen hast.
|
||||
|
||||
## Erstellen deiner YAML-Datei
|
||||
|
||||
### Was ist eine YAML-Datei und wofür brauche ich die?
|
||||
|
||||
Deine persönliche YAML-Datei beinhaltet eine Reihe von Optionen, die der Zufallsgenerator zum Erstellen von deinem
|
||||
Spiel benötigt. Jeder Spieler einer Multiworld stellt seine eigene YAML-Datei zur Verfügung. Dadurch kann jeder Spieler
|
||||
sein Spiel nach seinem eigenen Geschmack gestalten, während andere Spieler unabhängig davon ihre eigenen Optionen
|
||||
wählen können!
|
||||
|
||||
### Wo bekomme ich so eine YAML-Datei her?
|
||||
|
||||
Du kannst auf der [shapez-Spieler-Optionen-Seite](/games/shapez/player-options) eine YAML-Datei generieren oder ein
|
||||
Template herunterladen.
|
||||
|
||||
## Einer MultiWorld beitreten
|
||||
|
||||
1. Öffne das Spiel.
|
||||
2. Gib im Hauptmenü den Slot-Namen, die Adresse, den Port und das Passwort (optional) in die dafür vorgesehene Box ein.
|
||||
3. Drücke auf "Connect".
|
||||
- Erneutes Drücken trennt die Verbindung zum Server.
|
||||
- Ob du verbunden bist, steht direkt daneben.
|
||||
4. Starte ein neues Spiel.
|
||||
|
||||
Nachdem der Speicherstand erstellt wurde und du zum Hauptmenü zurückkehrst, wird das erneute Öffnen des Speicherstandes
|
||||
erneut verbinden.
|
||||
|
||||
### Der Port/Die Adresse der MultiWorld hat sich geändert, wie trete ich mit meinem existierenden Speicherstand bei?
|
||||
|
||||
Wiederhole die Schritte 1-3 und öffne den existierenden Speicherstand. Dies wird außerdem die gespeicherten Login-Daten
|
||||
überschreiben, sodass du dies nur einmal machen musst.
|
||||
58
worlds/shapez/docs/setup_en.md
Normal file
58
worlds/shapez/docs/setup_en.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Setup Guide for shapez: Archipelago
|
||||
|
||||
## Quick Links
|
||||
|
||||
- Game Info Page
|
||||
* [English](/games/shapez/info/en)
|
||||
* [Deutsch](/games/shapez/info/de)
|
||||
- [Player Options Page](/games/shapez/player-options)
|
||||
|
||||
## Required Software
|
||||
|
||||
- An installable, up-to-date PC version of shapez ([Steam](https://store.steampowered.com/app/1318690/shapez/)).
|
||||
- The shapezipelago mod from the [mod.io page](https://mod.io/g/shapez/m/shapezipelago).
|
||||
|
||||
## Optional Software
|
||||
|
||||
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
* (Only for the TextClient)
|
||||
* (If you want, you can use the built-in console as a read-only text client by launching the game
|
||||
with the `-dev` parameter)
|
||||
- Universal Tracker (check UT's `#future-game-design` thread in the discord server for instructions)
|
||||
|
||||
## Installation
|
||||
|
||||
As the game has a built-in mod loader, all you need to do is copy the `shapezipelago@X.X.X.js` mod file into the mods
|
||||
folder. If you don't know where that is, open the game, click on `MODS`, and then `OPEN MODS FOLDER`.
|
||||
|
||||
It is recommended to go into the settings of the game and disable `HINTS & TUTORIALS` in the `USER INTERFACE` tab, as
|
||||
this setting will disable the upgrade shop until you complete a few levels.
|
||||
|
||||
## Configuring your YAML file
|
||||
|
||||
### What is a YAML file and why do I need one?
|
||||
|
||||
Your YAML file contains a set of configuration options which provide the generator with information about how it should
|
||||
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
|
||||
an experience customized for their taste, and different players in the same multiworld can all have different options.
|
||||
|
||||
### Where do I get a YAML file?
|
||||
|
||||
You can generate a yaml or download a template by visiting the
|
||||
[shapez Player Options Page](/games/shapez/player-options)
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Open the game.
|
||||
2. In the main menu, type the slot name, address, port, and password (optional) into the input box.
|
||||
3. Click "Connect".
|
||||
- To disconnect, just press this button again.
|
||||
- The status of your connection is shown right next to the button.
|
||||
4. Create a new game.
|
||||
|
||||
After creating the save file and returning to the main menu, opening the save file again will automatically reconnect.
|
||||
|
||||
### The MultiWorld changed its port/address, how do I reconnect correctly with my existing save file?
|
||||
|
||||
Repeat steps 1-3 and open the existing save file. This will also overwrite the saved connection details, so you will
|
||||
only have to do this once.
|
||||
BIN
worlds/shapez/docs/shapesanity_full.png
Normal file
BIN
worlds/shapez/docs/shapesanity_full.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
279
worlds/shapez/items.py
Normal file
279
worlds/shapez/items.py
Normal file
@@ -0,0 +1,279 @@
|
||||
from typing import Dict, Callable, Any, List
|
||||
|
||||
from BaseClasses import Item, ItemClassification as IClass
|
||||
from .options import ShapezOptions
|
||||
from .data.strings import GOALS, ITEMS, OTHER
|
||||
|
||||
|
||||
def is_mam_achievement_included(options: ShapezOptions) -> IClass:
|
||||
return IClass.progression if options.include_achievements and (not options.goal == GOALS.vanilla) else IClass.useful
|
||||
|
||||
|
||||
def is_achievements_included(options: ShapezOptions) -> IClass:
|
||||
return IClass.progression if options.include_achievements else IClass.useful
|
||||
|
||||
|
||||
def is_goal_efficiency_iii(options: ShapezOptions) -> IClass:
|
||||
return IClass.progression if options.goal == GOALS.efficiency_iii else IClass.useful
|
||||
|
||||
|
||||
def always_progression(options: ShapezOptions) -> IClass:
|
||||
return IClass.progression
|
||||
|
||||
|
||||
def always_useful(options: ShapezOptions) -> IClass:
|
||||
return IClass.useful
|
||||
|
||||
|
||||
def always_filler(options: ShapezOptions) -> IClass:
|
||||
return IClass.filler
|
||||
|
||||
|
||||
def always_trap(options: ShapezOptions) -> IClass:
|
||||
return IClass.trap
|
||||
|
||||
|
||||
# Routing buildings are not needed to complete the game, but building factories without balancers and tunnels
|
||||
# would be unreasonably complicated and time-consuming.
|
||||
# Some buildings are not needed to complete the game, but are "logically needed" for the "MAM" achievement.
|
||||
|
||||
buildings_processing: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.cutter: always_progression,
|
||||
ITEMS.cutter_quad: always_progression,
|
||||
ITEMS.rotator: always_progression,
|
||||
ITEMS.rotator_ccw: always_progression,
|
||||
ITEMS.rotator_180: always_progression,
|
||||
ITEMS.stacker: always_progression,
|
||||
ITEMS.painter: always_progression,
|
||||
ITEMS.painter_double: always_progression,
|
||||
ITEMS.painter_quad: always_progression,
|
||||
ITEMS.color_mixer: always_progression,
|
||||
}
|
||||
|
||||
buildings_routing: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.balancer: always_progression,
|
||||
ITEMS.comp_merger: always_progression,
|
||||
ITEMS.comp_splitter: always_progression,
|
||||
ITEMS.tunnel: always_progression,
|
||||
ITEMS.tunnel_tier_ii: is_mam_achievement_included,
|
||||
}
|
||||
|
||||
buildings_other: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.trash: always_progression,
|
||||
ITEMS.extractor_chain: always_useful
|
||||
}
|
||||
|
||||
buildings_top_row: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.belt_reader: is_mam_achievement_included,
|
||||
ITEMS.storage: is_achievements_included,
|
||||
ITEMS.switch: always_progression,
|
||||
ITEMS.item_filter: is_mam_achievement_included,
|
||||
ITEMS.display: always_useful
|
||||
}
|
||||
|
||||
buildings_wires: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.wires: always_progression,
|
||||
ITEMS.const_signal: always_progression,
|
||||
ITEMS.logic_gates: is_mam_achievement_included,
|
||||
ITEMS.virtual_proc: is_mam_achievement_included
|
||||
}
|
||||
|
||||
gameplay_unlocks: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.blueprints: is_achievements_included
|
||||
}
|
||||
|
||||
upgrades: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.upgrade_big_belt: always_progression,
|
||||
ITEMS.upgrade_big_miner: always_useful,
|
||||
ITEMS.upgrade_big_proc: always_useful,
|
||||
ITEMS.upgrade_big_paint: always_useful,
|
||||
ITEMS.upgrade_small_belt: always_filler,
|
||||
ITEMS.upgrade_small_miner: always_filler,
|
||||
ITEMS.upgrade_small_proc: always_filler,
|
||||
ITEMS.upgrade_small_paint: always_filler
|
||||
}
|
||||
|
||||
whacky_upgrades: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.upgrade_gigantic_belt: always_progression,
|
||||
ITEMS.upgrade_gigantic_miner: always_useful,
|
||||
ITEMS.upgrade_gigantic_proc: always_useful,
|
||||
ITEMS.upgrade_gigantic_paint: always_useful,
|
||||
ITEMS.upgrade_rising_belt: always_progression,
|
||||
ITEMS.upgrade_rising_miner: always_useful,
|
||||
ITEMS.upgrade_rising_proc: always_useful,
|
||||
ITEMS.upgrade_rising_paint: always_useful,
|
||||
ITEMS.upgrade_big_random: always_useful,
|
||||
ITEMS.upgrade_small_random: always_filler,
|
||||
}
|
||||
|
||||
whacky_upgrade_traps: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.trap_upgrade_belt: always_trap,
|
||||
ITEMS.trap_upgrade_miner: always_trap,
|
||||
ITEMS.trap_upgrade_proc: always_trap,
|
||||
ITEMS.trap_upgrade_paint: always_trap,
|
||||
ITEMS.trap_upgrade_demonic_belt: always_trap,
|
||||
ITEMS.trap_upgrade_demonic_miner: always_trap,
|
||||
ITEMS.trap_upgrade_demonic_proc: always_trap,
|
||||
ITEMS.trap_upgrade_demonic_paint: always_trap,
|
||||
}
|
||||
|
||||
bundles: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.bundle_blueprint: always_filler,
|
||||
ITEMS.bundle_level: always_filler,
|
||||
ITEMS.bundle_upgrade: always_filler
|
||||
}
|
||||
|
||||
standard_traps: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.trap_locked: always_trap,
|
||||
ITEMS.trap_throttled: always_trap,
|
||||
ITEMS.trap_malfunction: always_trap,
|
||||
ITEMS.trap_inflation: always_trap,
|
||||
ITEMS.trap_clear_belts: always_trap,
|
||||
}
|
||||
|
||||
random_draining_trap: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.trap_draining_inv: always_trap
|
||||
}
|
||||
|
||||
split_draining_traps: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.trap_draining_blueprint: always_trap,
|
||||
ITEMS.trap_draining_level: always_trap,
|
||||
ITEMS.trap_draining_upgrade: always_trap
|
||||
}
|
||||
|
||||
belt_and_extractor: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
ITEMS.belt: always_progression,
|
||||
ITEMS.extractor: always_progression
|
||||
}
|
||||
|
||||
item_table: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||
**buildings_processing,
|
||||
**buildings_routing,
|
||||
**buildings_other,
|
||||
**buildings_top_row,
|
||||
**buildings_wires,
|
||||
**gameplay_unlocks,
|
||||
**upgrades,
|
||||
**whacky_upgrades,
|
||||
**whacky_upgrade_traps,
|
||||
**bundles,
|
||||
**standard_traps,
|
||||
**random_draining_trap,
|
||||
**split_draining_traps,
|
||||
**belt_and_extractor
|
||||
}
|
||||
|
||||
big_upgrades = [
|
||||
ITEMS.upgrade_big_belt,
|
||||
ITEMS.upgrade_big_miner,
|
||||
ITEMS.upgrade_big_proc,
|
||||
ITEMS.upgrade_big_paint
|
||||
]
|
||||
|
||||
small_upgrades = [
|
||||
ITEMS.upgrade_small_belt,
|
||||
ITEMS.upgrade_small_miner,
|
||||
ITEMS.upgrade_small_proc,
|
||||
ITEMS.upgrade_small_paint
|
||||
]
|
||||
|
||||
|
||||
def filler(random: float, whacky_allowed: bool) -> str:
|
||||
"""Returns a random filler item."""
|
||||
bundles_list = [*bundles]
|
||||
return random_choice_nested(random, [
|
||||
small_upgrades,
|
||||
[
|
||||
bundles_list,
|
||||
bundles_list,
|
||||
[
|
||||
big_upgrades,
|
||||
[*whacky_upgrades] if whacky_allowed else big_upgrades,
|
||||
],
|
||||
],
|
||||
])
|
||||
|
||||
|
||||
def trap(random: float, split_draining: bool, whacky_allowed: bool) -> str:
|
||||
"""Returns a random trap item."""
|
||||
pool = [
|
||||
*standard_traps,
|
||||
ITEMS.trap_draining_inv if not split_draining else [*split_draining_traps],
|
||||
]
|
||||
if whacky_allowed:
|
||||
pool.append([*whacky_upgrade_traps])
|
||||
return random_choice_nested(random, pool)
|
||||
|
||||
|
||||
def random_choice_nested(random: float, nested: List[Any]) -> Any:
|
||||
"""Helper function for getting a random element from a nested list."""
|
||||
current: Any = nested
|
||||
while isinstance(current, List):
|
||||
index_float = random*len(current)
|
||||
current = current[int(index_float)]
|
||||
random = index_float-int(index_float)
|
||||
return current
|
||||
|
||||
|
||||
item_descriptions = { # TODO replace keys with global strings and update with whacky upgrades
|
||||
"Balancer": "A routing building, that can merge two belts into one, split a belt in two, " +
|
||||
"or balance the items of two belts",
|
||||
"Tunnel": "A routing building consisting of two parts, that allows for gaps in belts",
|
||||
"Compact Merger": "A small routing building, that merges two belts into one",
|
||||
"Tunnel Tier II": "A routing building consisting of two parts, that allows for even longer gaps in belts",
|
||||
"Compact Splitter": "A small routing building, that splits a belt in two",
|
||||
"Cutter": "A processing building, that cuts shapes vertically in two halves",
|
||||
"Rotator": "A processing building, that rotates shapes 90 degrees clockwise",
|
||||
"Painter": "A processing building, that paints shapes in a given color",
|
||||
"Rotator (CCW)": "A processing building, that rotates shapes 90 degrees counter-clockwise",
|
||||
"Color Mixer": "A processing building, that mixes two colors together to create a new one",
|
||||
"Stacker": "A processing building, that combines two shapes with missing parts or puts one on top of the other",
|
||||
"Quad Cutter": "A processing building, that cuts shapes in four quarter parts",
|
||||
"Double Painter": "A processing building, that paints two shapes in a given color",
|
||||
"Rotator (180°)": "A processing building, that rotates shapes 180 degrees",
|
||||
"Quad Painter": "A processing building, that paint each quarter of a shape in another given color and requires " +
|
||||
"wire inputs for each color to work",
|
||||
"Trash": "A building, that destroys unused shapes",
|
||||
"Chaining Extractor": "An upgrade to extractors, that can increase the output without balancers or mergers",
|
||||
"Belt Reader": "A wired building, that shows the average amount of items passing through per second",
|
||||
"Storage": "A building, that stores up to 5000 of a certain shape",
|
||||
"Switch": "A building, that sends a constant boolean signal",
|
||||
"Item Filter": "A wired building, that filters items based on wire input",
|
||||
"Display": "A wired building, that displays a shape or color based on wire input",
|
||||
"Wires": "The main building of the wires layer, that carries signals between other buildings",
|
||||
"Constant Signal": "A building on the wires layer, that sends a constant shape, color, or boolean signal",
|
||||
"Logic Gates": "Multiple buildings on the wires layer, that perform logical operations on wire signals",
|
||||
"Virtual Processing": "Multiple buildings on the wires layer, that process wire signals like processor buildings",
|
||||
"Blueprints": "A game mechanic, that allows copy-pasting multiple buildings at once",
|
||||
"Big Belt Upgrade": "An upgrade, that adds 1 to the speed multiplier of belts, distributors, and tunnels",
|
||||
"Big Miner Upgrade": "An upgrade, that adds 1 to the speed multiplier of extractors",
|
||||
"Big Processors Upgrade": "An upgrade, that adds 1 to the speed multiplier of cutters, rotators, and stackers",
|
||||
"Big Painting Upgrade": "An upgrade, that adds 1 to the speed multiplier of painters and color mixers",
|
||||
"Small Belt Upgrade": "An upgrade, that adds 0.1 to the speed multiplier of belts, distributors, and tunnels",
|
||||
"Small Miner Upgrade": "An upgrade, that adds 0.1 to the speed multiplier of extractors",
|
||||
"Small Processors Upgrade": "An upgrade, that adds 0.1 to the speed multiplier of cutters, rotators, and stackers",
|
||||
"Small Painting Upgrade": "An upgrade, that adds 0.1 to the speed multiplier of painters and color mixers",
|
||||
"Blueprint Shapes Bundle": "A bundle with 1000 blueprint shapes, instantly delivered to the hub",
|
||||
"Level Shapes Bundle": "A bundle with some shapes needed for the current level, " +
|
||||
"instantly delivered to the hub",
|
||||
"Upgrade Shapes Bundle": "A bundle with some shapes needed for a random upgrade, " +
|
||||
"instantly delivered to the hub",
|
||||
"Inventory Draining Trap": "Randomly drains either blueprint shapes, current level requirement shapes, " +
|
||||
"or random upgrade requirement shapes, by half",
|
||||
"Blueprint Shapes Draining Trap": "Drains the stored blueprint shapes by half",
|
||||
"Level Shapes Draining Trap": "Drains the current level requirement shapes by half",
|
||||
"Upgrade Shapes Draining Trap": "Drains a random upgrade requirement shape by half",
|
||||
"Locked Building Trap": "Locks a random building from being placed for 15-60 seconds",
|
||||
"Throttled Building Trap": "Halves the speed of a random building for 15-60 seconds",
|
||||
"Malfunctioning Trap": "Makes a random building process items incorrectly for 15-60 seconds",
|
||||
"Inflation Trap": "Permanently increases the required shapes multiplier by 1. "
|
||||
"In other words: Permanently increases required shapes by 10% of the standard amount.",
|
||||
"Belt": "One of the most important buildings in the game, that transports your shapes and colors from one " +
|
||||
"place to another",
|
||||
"Extractor": "One of the most important buildings in the game, that extracts shapes from those randomly " +
|
||||
"generated patches"
|
||||
}
|
||||
|
||||
|
||||
class ShapezItem(Item):
|
||||
game = OTHER.game_name
|
||||
546
worlds/shapez/locations.py
Normal file
546
worlds/shapez/locations.py
Normal file
@@ -0,0 +1,546 @@
|
||||
from random import Random
|
||||
from typing import List, Tuple, Dict, Optional, Callable
|
||||
|
||||
from BaseClasses import Location, LocationProgressType, Region
|
||||
from .data.strings import CATEGORY, LOCATIONS, REGIONS, OPTIONS, GOALS, OTHER, SHAPESANITY
|
||||
from .options import max_shapesanity, max_levels_and_upgrades
|
||||
|
||||
categories = [CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting]
|
||||
|
||||
translate: List[Tuple[int, str]] = [
|
||||
(1000, "M"),
|
||||
(900, "CM"),
|
||||
(500, "D"),
|
||||
(400, "CD"),
|
||||
(100, "C"),
|
||||
(90, "XC"),
|
||||
(50, "L"),
|
||||
(40, "XL"),
|
||||
(10, "X"),
|
||||
(9, "IX"),
|
||||
(5, "V"),
|
||||
(4, "IV"),
|
||||
(1, "I")
|
||||
]
|
||||
|
||||
|
||||
def roman(num: int) -> str:
|
||||
"""Converts positive non-zero integers into roman numbers."""
|
||||
rom: str = ""
|
||||
for key, val in translate:
|
||||
while num >= key:
|
||||
rom += val
|
||||
num -= key
|
||||
return rom
|
||||
|
||||
|
||||
location_description = { # TODO change keys to global strings
|
||||
"Level 1": "Levels are completed by delivering certain shapes in certain amounts to the hub. The required shape "
|
||||
"and amount for the current level are always displayed on the hub.",
|
||||
"Level 1 Additional": "In the vanilla game, levels 1 and 20 have unlock more than one building.",
|
||||
"Level 20 Additional": "In the vanilla game, levels 1 and 20 have unlock more than one building.",
|
||||
"Level 20 Additional 2": "In the vanilla game, levels 1 and 20 have unlock more than one building.",
|
||||
"Level 26": "In the vanilla game, level 26 is the final level of the tutorial, unlocking freeplay.",
|
||||
f"Level {max_levels_and_upgrades-1}": "This is the highest possible level that can contains an item, if your goal "
|
||||
"is set to \"mam\"",
|
||||
"Belt Upgrade Tier II": "Upgrades can be purchased by having certain shapes in certain amounts stored in your hub. "
|
||||
"This is the first upgrade in the belt, balancers, and tunnel category.",
|
||||
"Miner Upgrade Tier II": "Upgrades can be purchased by having certain shapes in certain amounts stored in your "
|
||||
"hub. This is the first upgrade in the extractor category.",
|
||||
"Processors Upgrade Tier II": "Upgrades can be purchased by having certain shapes in certain amounts stored in "
|
||||
"your hub. This is the first upgrade in the cutter, rotators, and stacker category.",
|
||||
"Painting Upgrade Tier II": "Upgrades can be purchased by having certain shapes in certain amounts stored in your "
|
||||
"hub. This is the first upgrade in the painters and color mixer category.",
|
||||
"Belt Upgrade Tier VIII": "This is the final upgrade in the belt, balancers, and tunnel category, if your goal is "
|
||||
"**not** set to \"even_fasterer\".",
|
||||
"Miner Upgrade Tier VIII": "This is the final upgrade in the extractor category, if your goal is **not** set to "
|
||||
"\"even_fasterer\".",
|
||||
"Processors Upgrade Tier VIII": "This is the final upgrade in the cutter, rotators, and stacker category, if your "
|
||||
"goal is **not** set to \"even_fasterer\".",
|
||||
"Painting Upgrade Tier VIII": "This is the final upgrade in the painters and color mixer category, if your goal is "
|
||||
"**not** set to \"even_fasterer\".",
|
||||
f"Belt Upgrade Tier {roman(max_levels_and_upgrades)}": "This is the highest possible upgrade in the belt, "
|
||||
"balancers, and tunnel category, if your goal is set to "
|
||||
"\"even_fasterer\".",
|
||||
f"Miner Upgrade Tier {roman(max_levels_and_upgrades)}": "This is the highest possible upgrade in the extractor "
|
||||
"category, if your goal is set to \"even_fasterer\".",
|
||||
f"Processors Upgrade Tier {roman(max_levels_and_upgrades)}": "This is the highest possible upgrade in the cutter, "
|
||||
"rotators, and stacker category, if your goal is set "
|
||||
"to \"even_fasterer\".",
|
||||
f"Painting Upgrade Tier {roman(max_levels_and_upgrades)}": "This is the highest possible upgrade in the painters "
|
||||
"and color mixer category, if your goal is set to "
|
||||
"\"even_fasterer\".",
|
||||
"My eyes no longer hurt": "This is an achievement, that is unlocked by activating dark mode.",
|
||||
"Painter": "This is an achievement, that is unlocked by painting a shape using the painter or double painter.",
|
||||
"Cutter": "This is an achievement, that is unlocked by cutting a shape in half using the cutter.",
|
||||
"Rotater": "This is an achievement, that is unlocked by rotating a shape clock wise.",
|
||||
"Wait, they stack?": "This is an achievement, that is unlocked by stacking two shapes on top of each other.",
|
||||
"Wires": "This is an achievement, that is unlocked by completing level 20.",
|
||||
"Storage": "This is an achievement, that is unlocked by storing a shape in a storage.",
|
||||
"Freedom": "This is an achievement, that is unlocked by completing level 20. It is only included if the goal is "
|
||||
"**not** set to vanilla.",
|
||||
"The logo!": "This is an achievement, that is unlocked by producing the logo of the game.",
|
||||
"To the moon": "This is an achievement, that is unlocked by producing the rocket shape.",
|
||||
"It's piling up": "This is an achievement, that is unlocked by having 100.000 blueprint shapes stored in the hub.",
|
||||
"I'll use it later": "This is an achievement, that is unlocked by having one million blueprint shapes stored in "
|
||||
"the hub.",
|
||||
"Efficiency 1": "This is an achievement, that is unlocked by delivering 25 blueprint shapes per second to the hub.",
|
||||
"Preparing to launch": "This is an achievement, that is unlocked by delivering 10 rocket shapes per second to the "
|
||||
"hub.",
|
||||
"SpaceY": "This is an achievement, that is unlocked by 20 rocket shapes per second to the hub.",
|
||||
"Stack overflow": "This is an achievement, that is unlocked by stacking 4 layers on top of each other.",
|
||||
"It's a mess": "This is an achievement, that is unlocked by having 100 different shapes stored in the hub.",
|
||||
"Faster": "This is an achievement, that is unlocked by upgrading everything to at least tier V.",
|
||||
"Even faster": "This is an achievement, that is unlocked by upgrading everything to at least tier VIII.",
|
||||
"Get rid of them": "This is an achievement, that is unlocked by transporting 1000 shapes into a trash can.",
|
||||
"It's been a long time": "This is an achievement, that is unlocked by playing your save file for 10 hours "
|
||||
"(combined playtime).",
|
||||
"Addicted": "This is an achievement, that is unlocked by playing your save file for 20 hours (combined playtime).",
|
||||
"Can't stop": "This is an achievement, that is unlocked by reaching level 50.",
|
||||
"Is this the end?": "This is an achievement, that is unlocked by reaching level 100.",
|
||||
"Getting into it": "This is an achievement, that is unlocked by playing your save file for 1 hour (combined "
|
||||
"playtime).",
|
||||
"Now it's easy": "This is an achievement, that is unlocked by placing a blueprint.",
|
||||
"Computer Guy": "This is an achievement, that is unlocked by placing 5000 wires.",
|
||||
"Speedrun Master": "This is an achievement, that is unlocked by completing level 12 in under 30 Minutes. This "
|
||||
"location is excluded by default, as it can become inaccessible in a save file after that time.",
|
||||
"Speedrun Novice": "This is an achievement, that is unlocked by completing level 12 in under 60 Minutes. This "
|
||||
"location is excluded by default, as it can become inaccessible in a save file after that time.",
|
||||
"Not an idle game": "This is an achievement, that is unlocked by completing level 12 in under 120 Minutes. This "
|
||||
"location is excluded by default, as it can become inaccessible in a save file after that time.",
|
||||
"Efficiency 2": "This is an achievement, that is unlocked by delivering 50 blueprint shapes per second to the hub.",
|
||||
"Branding specialist 1": "This is an achievement, that is unlocked by delivering 25 logo shapes per second to the "
|
||||
"hub.",
|
||||
"Branding specialist 2": "This is an achievement, that is unlocked by delivering 50 logo shapes per second to the "
|
||||
"hub.",
|
||||
"King of Inefficiency": "This is an achievement, that is unlocked by **not** placing a counter clock wise rotator "
|
||||
"until level 14. This location is excluded by default, as it can become inaccessible in a "
|
||||
"save file after placing that building.",
|
||||
"It's so slow": "This is an achievement, that is unlocked by completing level 12 **without** buying any belt "
|
||||
"upgrade. This location is excluded by default, as it can become inaccessible in a save file after "
|
||||
"buying that upgrade.",
|
||||
"MAM (Make Anything Machine)": "This is an achievement, that is unlocked by completing any level after level 26 "
|
||||
"**without** modifying your factory. It is recommended to build a Make Anything "
|
||||
"Machine.",
|
||||
"Perfectionist": "This is an achievement, that is unlocked by destroying more than 1000 buildings at once.",
|
||||
"The next dimension": "This is an achievement, that is unlocked by opening the wires layer.",
|
||||
"Oops": "This is an achievement, that is unlocked by delivering a shape, that neither a level requirement nor an "
|
||||
"upgrade requirement.",
|
||||
"Copy-Pasta": "This is an achievement, that is unlocked by placing a blueprint with at least 1000 buildings.",
|
||||
"I've seen that before ...": "This is an achievement, that is unlocked by producing RgRyRbRr.",
|
||||
"Memories from the past": "This is an achievement, that is unlocked by producing WrRgWrRg:CwCrCwCr:SgSgSgSg.",
|
||||
"I need trains": "This is an achievement, that is unlocked by placing a 500 tiles long belt.",
|
||||
"A bit early?": "This is an achievement, that is unlocked by producing the logo shape before reaching level 18. "
|
||||
"This location is excluded by default, as it can become inaccessible in a save file after reaching "
|
||||
"that level.",
|
||||
"GPS": "This is an achievement, that is unlocked by placing 15 or more map markers.",
|
||||
"Shapesanity 1": "Shapesanity locations can be checked by delivering a described shape to the hub, without "
|
||||
"requiring a certain roation, orientation, or ordering. Shapesanity 1 is always an uncolored "
|
||||
"circle.",
|
||||
"Shapesanity 2": "Shapesanity locations can be checked by delivering a described shape to the hub, without "
|
||||
"requiring a certain roation, orientation, or ordering. Shapesanity 2 is always an uncolored "
|
||||
"square.",
|
||||
"Shapesanity 3": "Shapesanity locations can be checked by delivering a described shape to the hub, without "
|
||||
"requiring a certain roation, orientation, or ordering. Shapesanity 3 is always an uncolored "
|
||||
"star.",
|
||||
"Shapesanity 4": "Shapesanity locations can be checked by delivering a described shape to the hub, without "
|
||||
"requiring a certain roation, orientation, or ordering. Shapesanity 4 is always an uncolored "
|
||||
"windmill.",
|
||||
}
|
||||
|
||||
shapesanity_simple: Dict[str, str] = {}
|
||||
shapesanity_1_4: Dict[str, str] = {}
|
||||
shapesanity_two_sided: Dict[str, str] = {}
|
||||
shapesanity_three_parts: Dict[str, str] = {}
|
||||
shapesanity_four_parts: Dict[str, str] = {}
|
||||
|
||||
level_locations: List[str] = ([LOCATIONS.level(1, 1), LOCATIONS.level(20, 1), LOCATIONS.level(20, 2)]
|
||||
+ [LOCATIONS.level(x) for x in range(1, max_levels_and_upgrades)])
|
||||
upgrade_locations: List[str] = [LOCATIONS.upgrade(cat, roman(x))
|
||||
for cat in categories for x in range(2, max_levels_and_upgrades+1)]
|
||||
achievement_locations: List[str] = [LOCATIONS.my_eyes, LOCATIONS.painter, LOCATIONS.cutter, LOCATIONS.rotater,
|
||||
LOCATIONS.wait_they_stack, LOCATIONS.wires, LOCATIONS.storage, LOCATIONS.freedom,
|
||||
LOCATIONS.the_logo, LOCATIONS.to_the_moon, LOCATIONS.its_piling_up,
|
||||
LOCATIONS.use_it_later, LOCATIONS.efficiency_1, LOCATIONS.preparing_to_launch,
|
||||
LOCATIONS.spacey, LOCATIONS.stack_overflow, LOCATIONS.its_a_mess, LOCATIONS.faster,
|
||||
LOCATIONS.even_faster, LOCATIONS.get_rid_of_them, LOCATIONS.a_long_time,
|
||||
LOCATIONS.addicted, LOCATIONS.cant_stop, LOCATIONS.is_this_the_end,
|
||||
LOCATIONS.getting_into_it, LOCATIONS.now_its_easy, LOCATIONS.computer_guy,
|
||||
LOCATIONS.speedrun_master, LOCATIONS.speedrun_novice, LOCATIONS.not_idle_game,
|
||||
LOCATIONS.efficiency_2, LOCATIONS.branding_1,
|
||||
LOCATIONS.branding_2, LOCATIONS.king_of_inefficiency, LOCATIONS.its_so_slow,
|
||||
LOCATIONS.mam, LOCATIONS.perfectionist, LOCATIONS.next_dimension, LOCATIONS.oops,
|
||||
LOCATIONS.copy_pasta, LOCATIONS.ive_seen_that_before, LOCATIONS.memories,
|
||||
LOCATIONS.i_need_trains, LOCATIONS.a_bit_early, LOCATIONS.gps]
|
||||
shapesanity_locations: List[str] = [LOCATIONS.shapesanity(x) for x in range(1, max_shapesanity+1)]
|
||||
|
||||
|
||||
def init_shapesanity_pool() -> None:
|
||||
"""Imports the pregenerated shapesanity pool."""
|
||||
from .data import shapesanity_pool
|
||||
shapesanity_simple.update(shapesanity_pool.shapesanity_simple)
|
||||
shapesanity_1_4.update(shapesanity_pool.shapesanity_1_4)
|
||||
shapesanity_two_sided.update(shapesanity_pool.shapesanity_two_sided)
|
||||
shapesanity_three_parts.update(shapesanity_pool.shapesanity_three_parts)
|
||||
shapesanity_four_parts.update(shapesanity_pool.shapesanity_four_parts)
|
||||
|
||||
|
||||
def addlevels(maxlevel: int, logictype: str,
|
||||
random_logic_phase_length: List[int]) -> Dict[str, Tuple[str, LocationProgressType]]:
|
||||
"""Returns a dictionary with all level locations based on player options (maxlevel INCLUDED).
|
||||
If shape requirements are not randomized, the logic type is expected to be vanilla."""
|
||||
|
||||
# Level 1 is always directly accessible
|
||||
locations: Dict[str, Tuple[str, LocationProgressType]] \
|
||||
= {LOCATIONS.level(1): (REGIONS.main, LocationProgressType.PRIORITY),
|
||||
LOCATIONS.level(1, 1): (REGIONS.main, LocationProgressType.PRIORITY)}
|
||||
level_regions = [REGIONS.main, REGIONS.levels_1, REGIONS.levels_2, REGIONS.levels_3,
|
||||
REGIONS.levels_4, REGIONS.levels_5]
|
||||
|
||||
def f(name: str, region: str, progress: LocationProgressType = LocationProgressType.DEFAULT) -> None:
|
||||
locations[name] = (region, progress)
|
||||
|
||||
if logictype.startswith(OPTIONS.logic_vanilla):
|
||||
f(LOCATIONS.level(20, 1), REGIONS.levels_5)
|
||||
f(LOCATIONS.level(20, 2), REGIONS.levels_5)
|
||||
f(LOCATIONS.level(2), REGIONS.levels_1)
|
||||
f(LOCATIONS.level(3), REGIONS.levels_1)
|
||||
f(LOCATIONS.level(4), REGIONS.levels_1)
|
||||
f(LOCATIONS.level(5), REGIONS.levels_2)
|
||||
f(LOCATIONS.level(6), REGIONS.levels_2)
|
||||
f(LOCATIONS.level(7), REGIONS.levels_3)
|
||||
f(LOCATIONS.level(8), REGIONS.levels_3)
|
||||
f(LOCATIONS.level(9), REGIONS.levels_4)
|
||||
f(LOCATIONS.level(10), REGIONS.levels_4)
|
||||
for x in range(11, maxlevel+1):
|
||||
f(LOCATIONS.level(x), REGIONS.levels_5)
|
||||
|
||||
elif logictype.startswith(OPTIONS.logic_stretched):
|
||||
phaselength = maxlevel//6
|
||||
f(LOCATIONS.level(20, 1), level_regions[20//phaselength])
|
||||
f(LOCATIONS.level(20, 2), level_regions[20//phaselength])
|
||||
for x in range(2, phaselength):
|
||||
f(LOCATIONS.level(x), REGIONS.main)
|
||||
for x in range(phaselength, phaselength*2):
|
||||
f(LOCATIONS.level(x), REGIONS.levels_1)
|
||||
for x in range(phaselength*2, phaselength*3):
|
||||
f(LOCATIONS.level(x), REGIONS.levels_2)
|
||||
for x in range(phaselength*3, phaselength*4):
|
||||
f(LOCATIONS.level(x), REGIONS.levels_3)
|
||||
for x in range(phaselength*4, phaselength*5):
|
||||
f(LOCATIONS.level(x), REGIONS.levels_4)
|
||||
for x in range(phaselength*5, maxlevel+1):
|
||||
f(LOCATIONS.level(x), REGIONS.levels_5)
|
||||
|
||||
elif logictype.startswith(OPTIONS.logic_quick):
|
||||
f(LOCATIONS.level(20, 1), REGIONS.levels_5)
|
||||
f(LOCATIONS.level(20, 2), REGIONS.levels_5)
|
||||
f(LOCATIONS.level(2), REGIONS.levels_1)
|
||||
f(LOCATIONS.level(3), REGIONS.levels_2)
|
||||
f(LOCATIONS.level(4), REGIONS.levels_3)
|
||||
f(LOCATIONS.level(5), REGIONS.levels_4)
|
||||
for x in range(6, maxlevel+1):
|
||||
f(LOCATIONS.level(x), REGIONS.levels_5)
|
||||
|
||||
elif logictype.startswith(OPTIONS.logic_random_steps):
|
||||
next_level = 2
|
||||
for phase in range(5):
|
||||
for x in range(random_logic_phase_length[phase]):
|
||||
f(LOCATIONS.level(next_level+x), level_regions[phase])
|
||||
next_level += random_logic_phase_length[phase]
|
||||
if next_level > 20:
|
||||
f(LOCATIONS.level(20, 1), level_regions[phase])
|
||||
f(LOCATIONS.level(20, 2), level_regions[phase])
|
||||
for x in range(next_level, maxlevel+1):
|
||||
f(LOCATIONS.level(x), REGIONS.levels_5)
|
||||
if next_level <= 20:
|
||||
f(LOCATIONS.level(20, 1), REGIONS.levels_5)
|
||||
f(LOCATIONS.level(20, 2), REGIONS.levels_5)
|
||||
|
||||
elif logictype == OPTIONS.logic_hardcore:
|
||||
f(LOCATIONS.level(20, 1), REGIONS.levels_5)
|
||||
f(LOCATIONS.level(20, 2), REGIONS.levels_5)
|
||||
for x in range(2, maxlevel+1):
|
||||
f(LOCATIONS.level(x), REGIONS.levels_5)
|
||||
|
||||
elif logictype == OPTIONS.logic_dopamine:
|
||||
f(LOCATIONS.level(20, 1), REGIONS.levels_2)
|
||||
f(LOCATIONS.level(20, 2), REGIONS.levels_2)
|
||||
for x in range(2, maxlevel+1):
|
||||
f(LOCATIONS.level(x), REGIONS.levels_2)
|
||||
|
||||
elif logictype == OPTIONS.logic_dopamine_overflow:
|
||||
f(LOCATIONS.level(20, 1), REGIONS.main)
|
||||
f(LOCATIONS.level(20, 2), REGIONS.main)
|
||||
for x in range(2, maxlevel+1):
|
||||
f(LOCATIONS.level(x), REGIONS.main)
|
||||
|
||||
else:
|
||||
raise Exception(f"Illegal level logic type {logictype}")
|
||||
|
||||
return locations
|
||||
|
||||
|
||||
def addupgrades(finaltier: int, logictype: str,
|
||||
category_random_logic_amounts: Dict[str, int]) -> Dict[str, Tuple[str, LocationProgressType]]:
|
||||
"""Returns a dictionary with all upgrade locations based on player options (finaltier INCLUDED).
|
||||
If shape requirements are not randomized, give logic type 0."""
|
||||
|
||||
locations: Dict[str, Tuple[str, LocationProgressType]] = {}
|
||||
upgrade_regions = [REGIONS.main, REGIONS.upgrades_1, REGIONS.upgrades_2, REGIONS.upgrades_3,
|
||||
REGIONS.upgrades_4, REGIONS.upgrades_5]
|
||||
|
||||
def f(name: str, region: str, progress: LocationProgressType = LocationProgressType.DEFAULT) -> None:
|
||||
locations[name] = (region, progress)
|
||||
|
||||
if logictype == OPTIONS.logic_vanilla_like:
|
||||
f(LOCATIONS.upgrade(CATEGORY.belt, "II"), REGIONS.main)
|
||||
f(LOCATIONS.upgrade(CATEGORY.miner, "II"), REGIONS.main)
|
||||
f(LOCATIONS.upgrade(CATEGORY.processors, "II"), REGIONS.main)
|
||||
f(LOCATIONS.upgrade(CATEGORY.painting, "II"), REGIONS.upgrades_3)
|
||||
f(LOCATIONS.upgrade(CATEGORY.belt, "III"), REGIONS.upgrades_2)
|
||||
f(LOCATIONS.upgrade(CATEGORY.miner, "III"), REGIONS.upgrades_2)
|
||||
f(LOCATIONS.upgrade(CATEGORY.processors, "III"), REGIONS.upgrades_1)
|
||||
f(LOCATIONS.upgrade(CATEGORY.painting, "III"), REGIONS.upgrades_3)
|
||||
for x in range(4, finaltier+1):
|
||||
tier = roman(x)
|
||||
for cat in categories:
|
||||
f(LOCATIONS.upgrade(cat, tier), REGIONS.upgrades_5)
|
||||
|
||||
elif logictype == OPTIONS.logic_linear:
|
||||
for x in range(2, 7):
|
||||
tier = roman(x)
|
||||
for cat in categories:
|
||||
f(LOCATIONS.upgrade(cat, tier), upgrade_regions[x-2])
|
||||
for x in range(7, finaltier+1):
|
||||
tier = roman(x)
|
||||
for cat in categories:
|
||||
f(LOCATIONS.upgrade(cat, tier), REGIONS.upgrades_5)
|
||||
|
||||
elif logictype == OPTIONS.logic_category:
|
||||
for x in range(2, 7):
|
||||
tier = roman(x)
|
||||
f(LOCATIONS.upgrade(CATEGORY.belt, tier), REGIONS.main)
|
||||
f(LOCATIONS.upgrade(CATEGORY.miner, tier), REGIONS.main)
|
||||
for x in range(7, finaltier + 1):
|
||||
tier = roman(x)
|
||||
f(LOCATIONS.upgrade(CATEGORY.belt, tier), REGIONS.upgrades_5)
|
||||
f(LOCATIONS.upgrade(CATEGORY.miner, tier), REGIONS.upgrades_5)
|
||||
f(LOCATIONS.upgrade(CATEGORY.processors, "II"), REGIONS.upgrades_1)
|
||||
f(LOCATIONS.upgrade(CATEGORY.processors, "III"), REGIONS.upgrades_2)
|
||||
f(LOCATIONS.upgrade(CATEGORY.processors, "IV"), REGIONS.upgrades_2)
|
||||
f(LOCATIONS.upgrade(CATEGORY.processors, "V"), REGIONS.upgrades_3)
|
||||
f(LOCATIONS.upgrade(CATEGORY.processors, "VI"), REGIONS.upgrades_3)
|
||||
for x in range(7, finaltier+1):
|
||||
f(LOCATIONS.upgrade(CATEGORY.processors, roman(x)), REGIONS.upgrades_5)
|
||||
for x in range(2, 4):
|
||||
f(LOCATIONS.upgrade(CATEGORY.painting, roman(x)), REGIONS.upgrades_4)
|
||||
for x in range(4, finaltier+1):
|
||||
f(LOCATIONS.upgrade(CATEGORY.painting, roman(x)), REGIONS.upgrades_5)
|
||||
|
||||
elif logictype == OPTIONS.logic_category_random:
|
||||
for x in range(2, 7):
|
||||
tier = roman(x)
|
||||
f(LOCATIONS.upgrade(CATEGORY.belt, tier),
|
||||
upgrade_regions[category_random_logic_amounts[CATEGORY.belt_low]])
|
||||
f(LOCATIONS.upgrade(CATEGORY.miner, tier),
|
||||
upgrade_regions[category_random_logic_amounts[CATEGORY.miner_low]])
|
||||
f(LOCATIONS.upgrade(CATEGORY.processors, tier),
|
||||
upgrade_regions[category_random_logic_amounts[CATEGORY.processors_low]])
|
||||
f(LOCATIONS.upgrade(CATEGORY.painting, tier),
|
||||
upgrade_regions[category_random_logic_amounts[CATEGORY.painting_low]])
|
||||
for x in range(7, finaltier+1):
|
||||
tier = roman(x)
|
||||
for cat in categories:
|
||||
f(LOCATIONS.upgrade(cat, tier), REGIONS.upgrades_5)
|
||||
|
||||
else: # logictype == hardcore
|
||||
for cat in categories:
|
||||
f(LOCATIONS.upgrade(cat, "II"), REGIONS.main)
|
||||
for x in range(3, finaltier+1):
|
||||
tier = roman(x)
|
||||
for cat in categories:
|
||||
f(LOCATIONS.upgrade(cat, tier), REGIONS.upgrades_5)
|
||||
|
||||
return locations
|
||||
|
||||
|
||||
def addachievements(excludesoftlock: bool, excludelong: bool, excludeprogressive: bool,
|
||||
maxlevel: int, upgradelogictype: str, category_random_logic_amounts: Dict[str, int],
|
||||
goal: str, presentlocations: Dict[str, Tuple[str, LocationProgressType]],
|
||||
add_alias: Callable[[str, str], None], has_upgrade_traps: bool
|
||||
) -> Dict[str, Tuple[str, LocationProgressType]]:
|
||||
"""Returns a dictionary with all achievement locations based on player options."""
|
||||
|
||||
locations: Dict[str, Tuple[str, LocationProgressType]] = dict()
|
||||
upgrade_regions = [REGIONS.main, REGIONS.upgrades_1, REGIONS.upgrades_2, REGIONS.upgrades_3,
|
||||
REGIONS.upgrades_4, REGIONS.upgrades_5]
|
||||
|
||||
def f(name: str, region: str, alias: str, progress: LocationProgressType = LocationProgressType.DEFAULT):
|
||||
locations[name] = (region, progress)
|
||||
add_alias(name, alias)
|
||||
|
||||
f(LOCATIONS.my_eyes, REGIONS.menu, "Activate dark mode")
|
||||
f(LOCATIONS.painter, REGIONS.paint_not_quad, "Paint a shape (no Quad Painter)")
|
||||
f(LOCATIONS.cutter, REGIONS.cut_not_quad, "Cut a shape (no Quad Cutter)")
|
||||
f(LOCATIONS.rotater, REGIONS.rotate_cw, "Rotate a shape clock wise")
|
||||
f(LOCATIONS.wait_they_stack, REGIONS.stack_shape, "Stack a shape")
|
||||
f(LOCATIONS.storage, REGIONS.store_shape, "Store a shape in the storage")
|
||||
f(LOCATIONS.the_logo, REGIONS.all_buildings, "Produce the shapez logo")
|
||||
f(LOCATIONS.to_the_moon, REGIONS.all_buildings, "Produce the rocket shape")
|
||||
f(LOCATIONS.its_piling_up, REGIONS.all_buildings, "100k blueprint shapes")
|
||||
f(LOCATIONS.use_it_later, REGIONS.all_buildings, "1 million blueprint shapes")
|
||||
|
||||
f(LOCATIONS.stack_overflow, REGIONS.stack_shape, "4 layers shape")
|
||||
f(LOCATIONS.its_a_mess, REGIONS.main, "100 different shapes in hub")
|
||||
f(LOCATIONS.get_rid_of_them, REGIONS.trash_shape, "1000 shapes trashed")
|
||||
f(LOCATIONS.getting_into_it, REGIONS.menu, "1 hour")
|
||||
f(LOCATIONS.now_its_easy, REGIONS.blueprint, "Place a blueprint")
|
||||
f(LOCATIONS.computer_guy, REGIONS.wiring, "Place 5000 wires")
|
||||
f(LOCATIONS.perfectionist, REGIONS.any_building, "Destroy more than 1000 objects at once")
|
||||
f(LOCATIONS.next_dimension, REGIONS.wiring, "Open the wires layer")
|
||||
f(LOCATIONS.copy_pasta, REGIONS.blueprint, "Place a 1000 buildings blueprint")
|
||||
f(LOCATIONS.ive_seen_that_before, REGIONS.all_buildings, "Produce RgRyRbRr")
|
||||
f(LOCATIONS.memories, REGIONS.all_buildings, "Produce WrRgWrRg:CwCrCwCr:SgSgSgSg")
|
||||
f(LOCATIONS.i_need_trains, REGIONS.belt, "Have a 500 tiles belt")
|
||||
f(LOCATIONS.gps, REGIONS.menu, "15 map markers")
|
||||
|
||||
# Per second delivery achievements
|
||||
f(LOCATIONS.preparing_to_launch, REGIONS.all_buildings, "10 rocket shapes / second")
|
||||
if not has_upgrade_traps:
|
||||
f(LOCATIONS.spacey, REGIONS.all_buildings, "20 rocket shapes / second")
|
||||
f(LOCATIONS.efficiency_1, REGIONS.all_buildings, "25 blueprints shapes / second")
|
||||
f(LOCATIONS.efficiency_2, REGIONS.all_buildings_x1_6_belt, "50 blueprints shapes / second")
|
||||
f(LOCATIONS.branding_1, REGIONS.all_buildings, "25 logo shapes / second")
|
||||
f(LOCATIONS.branding_2, REGIONS.all_buildings_x1_6_belt, "50 logo shapes / second")
|
||||
|
||||
# Achievements that depend on upgrades
|
||||
f(LOCATIONS.even_faster, REGIONS.upgrades_5, "All upgrades on tier VIII")
|
||||
if upgradelogictype == OPTIONS.logic_linear:
|
||||
f(LOCATIONS.faster, REGIONS.upgrades_3, "All upgrades on tier V")
|
||||
elif upgradelogictype == OPTIONS.logic_category_random:
|
||||
f(LOCATIONS.faster, upgrade_regions[
|
||||
max(category_random_logic_amounts[CATEGORY.belt_low],
|
||||
category_random_logic_amounts[CATEGORY.miner_low],
|
||||
category_random_logic_amounts[CATEGORY.processors_low],
|
||||
category_random_logic_amounts[CATEGORY.painting_low])
|
||||
], "All upgrades on tier V")
|
||||
else:
|
||||
f(LOCATIONS.faster, REGIONS.upgrades_5, "All upgrades on tier V")
|
||||
|
||||
# Achievements that depend on the level
|
||||
f(LOCATIONS.wires, presentlocations[LOCATIONS.level(20)][0], "Complete level 20")
|
||||
if not goal == GOALS.vanilla:
|
||||
f(LOCATIONS.freedom, presentlocations[LOCATIONS.level(26)][0], "Complete level 26")
|
||||
f(LOCATIONS.mam, REGIONS.mam, "Complete any level > 26 without modifications")
|
||||
if maxlevel >= 50:
|
||||
f(LOCATIONS.cant_stop, presentlocations[LOCATIONS.level(50)][0], "Reach level 50")
|
||||
elif goal not in [GOALS.vanilla, GOALS.mam]:
|
||||
f(LOCATIONS.cant_stop, REGIONS.levels_5, "Reach level 50")
|
||||
if maxlevel >= 100:
|
||||
f(LOCATIONS.is_this_the_end, presentlocations[LOCATIONS.level(100)][0], "Reach level 100")
|
||||
elif goal not in [GOALS.vanilla, GOALS.mam]:
|
||||
f(LOCATIONS.is_this_the_end, REGIONS.levels_5, "Reach level 100")
|
||||
|
||||
# Achievements that depend on player preferences
|
||||
if excludeprogressive:
|
||||
unreasonable_type = LocationProgressType.EXCLUDED
|
||||
else:
|
||||
unreasonable_type = LocationProgressType.DEFAULT
|
||||
if not excludesoftlock:
|
||||
f(LOCATIONS.speedrun_master, presentlocations[LOCATIONS.level(12)][0],
|
||||
"Complete level 12 in under 30 min", unreasonable_type)
|
||||
f(LOCATIONS.speedrun_novice, presentlocations[LOCATIONS.level(12)][0],
|
||||
"Complete level 12 in under 60 min", unreasonable_type)
|
||||
f(LOCATIONS.not_idle_game, presentlocations[LOCATIONS.level(12)][0],
|
||||
"Complete level 12 in under 120 min", unreasonable_type)
|
||||
f(LOCATIONS.its_so_slow, presentlocations[LOCATIONS.level(12)][0],
|
||||
"Complete level 12 without upgrading belts", unreasonable_type)
|
||||
f(LOCATIONS.king_of_inefficiency, presentlocations[LOCATIONS.level(14)][0],
|
||||
"No ccw rotator until level 14", unreasonable_type)
|
||||
f(LOCATIONS.a_bit_early, REGIONS.all_buildings,
|
||||
"Produce logo shape before level 18", unreasonable_type)
|
||||
if not excludelong:
|
||||
f(LOCATIONS.a_long_time, REGIONS.menu, "10 hours")
|
||||
f(LOCATIONS.addicted, REGIONS.menu, "20 hours")
|
||||
|
||||
# Achievements with a softlock chance of less than
|
||||
# 1 divided by 2 to the power of the number of all atoms in the universe
|
||||
f(LOCATIONS.oops, REGIONS.main, "Deliver an irrelevant shape")
|
||||
|
||||
return locations
|
||||
|
||||
|
||||
def addshapesanity(amount: int, random: Random, append_shapesanity: Callable[[str], None],
|
||||
add_alias: Callable[[str, str], None]) -> Dict[str, Tuple[str, LocationProgressType]]:
|
||||
"""Returns a dictionary with a given number of random shapesanity locations."""
|
||||
|
||||
included_shapes: Dict[str, Tuple[str, LocationProgressType]] = {}
|
||||
|
||||
def f(name: str, region: str, alias: str, progress: LocationProgressType = LocationProgressType.DEFAULT) -> None:
|
||||
included_shapes[name] = (region, progress)
|
||||
append_shapesanity(alias)
|
||||
shapes_list.remove((alias, region))
|
||||
add_alias(name, alias)
|
||||
|
||||
# Always have at least 4 shapesanity checks because of sphere 1 usefulls + both hardcore logic
|
||||
shapes_list = list(shapesanity_simple.items())
|
||||
f(LOCATIONS.shapesanity(1), REGIONS.sanity(REGIONS.full, REGIONS.uncol),
|
||||
SHAPESANITY.full(SHAPESANITY.uncolored, SHAPESANITY.circle))
|
||||
f(LOCATIONS.shapesanity(2), REGIONS.sanity(REGIONS.full, REGIONS.uncol),
|
||||
SHAPESANITY.full(SHAPESANITY.uncolored, SHAPESANITY.square))
|
||||
f(LOCATIONS.shapesanity(3), REGIONS.sanity(REGIONS.full, REGIONS.uncol),
|
||||
SHAPESANITY.full(SHAPESANITY.uncolored, SHAPESANITY.star))
|
||||
f(LOCATIONS.shapesanity(4), REGIONS.sanity(REGIONS.east_wind, REGIONS.uncol),
|
||||
SHAPESANITY.full(SHAPESANITY.uncolored, SHAPESANITY.windmill))
|
||||
|
||||
# The pool switches dynamically depending on if either it's ratio or limit is reached
|
||||
switched = 0
|
||||
for counting in range(4, amount):
|
||||
if switched == 0 and (len(shapes_list) == 0 or counting == amount//2):
|
||||
shapes_list = list(shapesanity_1_4.items())
|
||||
switched = 1
|
||||
elif switched == 1 and (len(shapes_list) == 0 or counting == amount*7//12):
|
||||
shapes_list = list(shapesanity_two_sided.items())
|
||||
switched = 2
|
||||
elif switched == 2 and (len(shapes_list) == 0 or counting == amount*5//6):
|
||||
shapes_list = list(shapesanity_three_parts.items())
|
||||
switched = 3
|
||||
elif switched == 3 and (len(shapes_list) == 0 or counting == amount*11//12):
|
||||
shapes_list = list(shapesanity_four_parts.items())
|
||||
switched = 4
|
||||
x = random.randint(0, len(shapes_list)-1)
|
||||
next_shape = shapes_list.pop(x)
|
||||
included_shapes[LOCATIONS.shapesanity(counting+1)] = (next_shape[1], LocationProgressType.DEFAULT)
|
||||
append_shapesanity(next_shape[0])
|
||||
add_alias(LOCATIONS.shapesanity(counting+1), next_shape[0])
|
||||
|
||||
return included_shapes
|
||||
|
||||
|
||||
def addshapesanity_ut(shapesanity_names: List[str], add_alias: Callable[[str, str], None]
|
||||
) -> Dict[str, Tuple[str, LocationProgressType]]:
|
||||
"""Returns the same information as addshapesanity but will add specific values based on a UT rebuild."""
|
||||
|
||||
included_shapes: Dict[str, Tuple[str, LocationProgressType]] = {}
|
||||
|
||||
for name in shapesanity_names:
|
||||
for options in [shapesanity_simple, shapesanity_1_4, shapesanity_two_sided, shapesanity_three_parts,
|
||||
shapesanity_four_parts]:
|
||||
if name in options:
|
||||
next_shape = options[name]
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"Could not find shapesanity name {name}")
|
||||
included_shapes[LOCATIONS.shapesanity(len(included_shapes)+1)] = (next_shape, LocationProgressType.DEFAULT)
|
||||
add_alias(LOCATIONS.shapesanity(len(included_shapes)), name)
|
||||
return included_shapes
|
||||
|
||||
|
||||
class ShapezLocation(Location):
|
||||
game = OTHER.game_name
|
||||
|
||||
def __init__(self, player: int, name: str, address: Optional[int], region: Region,
|
||||
progress_type: LocationProgressType):
|
||||
super(ShapezLocation, self).__init__(player, name, address, region)
|
||||
self.progress_type = progress_type
|
||||
310
worlds/shapez/options.py
Normal file
310
worlds/shapez/options.py
Normal file
@@ -0,0 +1,310 @@
|
||||
import pkgutil
|
||||
from dataclasses import dataclass
|
||||
|
||||
import orjson
|
||||
|
||||
from Options import Toggle, Choice, PerGameCommonOptions, NamedRange, Range
|
||||
from .common.options import FloatRangeText
|
||||
|
||||
datapackage_options = orjson.loads(pkgutil.get_data(__name__, "data/options.json"))
|
||||
max_levels_and_upgrades = datapackage_options["max_levels_and_upgrades"]
|
||||
max_shapesanity = datapackage_options["max_shapesanity"]
|
||||
del datapackage_options
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
"""Sets the goal of your world.
|
||||
|
||||
- **Vanilla:** Complete level 26.
|
||||
- **MAM:** Complete a specified level after level 26. Every level before that will be a location. It's recommended
|
||||
to build a Make-Anything-Machine (MAM).
|
||||
- **Even fasterer:** Upgrade everything to a specified tier after tier 8. Every upgrade before that will be a
|
||||
location.
|
||||
- **Efficiency III:** Deliver 256 blueprint shapes per second to the hub."""
|
||||
display_name = "Goal"
|
||||
rich_text_doc = True
|
||||
option_vanilla = 0
|
||||
option_mam = 1
|
||||
option_even_fasterer = 2
|
||||
option_efficiency_iii = 3
|
||||
default = 0
|
||||
|
||||
|
||||
class GoalAmount(NamedRange):
|
||||
"""Specify, what level or tier (when either MAM or Even Fasterer is chosen as goal) is required to reach the goal.
|
||||
|
||||
If MAM is set as the goal, this has to be set to 27 or more. Else it will raise an error."""
|
||||
display_name = "Goal amount"
|
||||
rich_text_doc = True
|
||||
range_start = 9
|
||||
range_end = max_levels_and_upgrades
|
||||
default = 27
|
||||
special_range_names = {
|
||||
"minimum_mam": 27,
|
||||
"recommended_mam": 50,
|
||||
"long_game_mam": 120,
|
||||
"minimum_even_fasterer": 9,
|
||||
"recommended_even_fasterer": 16,
|
||||
"long_play_even_fasterer": 35,
|
||||
}
|
||||
|
||||
|
||||
class RequiredShapesMultiplier(Range):
|
||||
"""Multiplies the amount of required shapes for levels and upgrades by value/10.
|
||||
|
||||
For level 1, the amount of shapes ranges from 3 to 300.
|
||||
|
||||
For level 26, it ranges from 5k to 500k."""
|
||||
display_name = "Required shapes multiplier"
|
||||
rich_text_doc = True
|
||||
range_start = 1
|
||||
range_end = 100
|
||||
default = 10
|
||||
|
||||
|
||||
class AllowFloatingLayers(Toggle):
|
||||
"""Toggle whether shape requirements are allowed to have floating layers (like the logo or the rocket shape).
|
||||
|
||||
However, be aware that floating shapes make MAMs much more complex."""
|
||||
display_name = "Allow floating layers"
|
||||
rich_text_doc = True
|
||||
default = False
|
||||
|
||||
|
||||
class RandomizeLevelRequirements(Toggle):
|
||||
"""Randomize the required shapes to complete levels."""
|
||||
display_name = "Randomize level requirements"
|
||||
rich_text_doc = True
|
||||
default = True
|
||||
|
||||
|
||||
class RandomizeUpgradeRequirements(Toggle):
|
||||
"""Randomize the required shapes to buy upgrades."""
|
||||
display_name = "Randomize upgrade requirements"
|
||||
rich_text_doc = True
|
||||
default = True
|
||||
|
||||
|
||||
class RandomizeLevelLogic(Choice):
|
||||
"""If level requirements are randomized, this sets how those random shapes are generated and how logic works for
|
||||
levels. The shuffled variants shuffle the order of progression buildings obtained in the multiworld. The standard
|
||||
order is: **cutter -> rotator -> painter -> color mixer -> stacker**
|
||||
|
||||
- **Vanilla:** Level 1 requires nothing, 2-4 require the first building, 5-6 require also the second, 7-8 the
|
||||
third, 9-10 the fourth, and 11 and onwards the fifth and thereby all buildings.
|
||||
- **Stretched:** After every floor(maxlevel/6) levels, another building is required.
|
||||
- **Quick:** Every Level, except level 1, requires another building, with level 6 and onwards requiring all
|
||||
buildings.
|
||||
- **Random steps:** After a random amount of levels, another building is required, with level 1 always requiring
|
||||
none. This can potentially generate like any other option.
|
||||
- **Hardcore:** All levels (except level 1) have completely random shape requirements and thus require all
|
||||
buildings. Expect early BKs.
|
||||
- **Dopamine (overflow):** All levels (except level 1 and the goal) require 2 random buildings (or none in case of
|
||||
overflow)."""
|
||||
display_name = "Randomize level logic"
|
||||
rich_text_doc = True
|
||||
option_vanilla = 0
|
||||
option_vanilla_shuffled = 1
|
||||
option_stretched = 2
|
||||
option_stretched_shuffled = 3
|
||||
option_quick = 4
|
||||
option_quick_shuffled = 5
|
||||
option_random_steps = 6
|
||||
option_random_steps_shuffled = 7
|
||||
option_hardcore = 8
|
||||
option_dopamine = 9
|
||||
option_dopamine_overflow = 10
|
||||
default = 2
|
||||
|
||||
|
||||
class RandomizeUpgradeLogic(Choice):
|
||||
"""If upgrade requirements are randomized, this sets how those random shapes are generated
|
||||
and how logic works for upgrades.
|
||||
|
||||
- **Vanilla-like:** Tier II requires up to two random buildings, III requires up to three random buildings,
|
||||
and IV and onwards require all processing buildings.
|
||||
- **Linear:** Tier II requires nothing, III-VI require another random building each,
|
||||
and VII and onwards require all buildings.
|
||||
- **Category:** Belt and miner upgrades require no building up to tier V, but onwards all buildings, processors
|
||||
upgrades require the cutter (all tiers), rotator (tier III and onwards), and stacker (tier V and onwards), and
|
||||
painting upgrades require the cutter, rotator, stacker, painter (all tiers) and color mixer (tiers V and onwards).
|
||||
Tier VII and onwards will always require all buildings.
|
||||
- **Category random:** Each upgrades category (up to tier VI) requires a random amount of buildings (in order),
|
||||
with one category always requiring no buildings. Tier VII and onwards will always require all buildings.
|
||||
- **Hardcore:** All tiers (except each tier II) have completely random shape requirements and thus require all
|
||||
buildings. Expect early BKs."""
|
||||
display_name = "Randomize upgrade logic"
|
||||
rich_text_doc = True
|
||||
option_vanilla_like = 0
|
||||
option_linear = 1
|
||||
option_category = 2
|
||||
option_category_random = 3
|
||||
option_hardcore = 4
|
||||
default = 1
|
||||
|
||||
|
||||
class ThroughputLevelsRatio(NamedRange):
|
||||
"""If level requirements are randomized, this sets the ratio of how many levels (approximately) will require either
|
||||
a total amount or per second amount (throughput) of shapes delivered.
|
||||
|
||||
0 means only total, 100 means only throughput, and vanilla (-1) means only levels 14, 27 and beyond have throughput.
|
||||
"""
|
||||
display_name = "Throughput levels ratio"
|
||||
rich_text_doc = True
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 0
|
||||
special_range_names = {
|
||||
"vanilla": -1,
|
||||
"only_total": 0,
|
||||
"half_half": 50,
|
||||
"only_throughput": 100,
|
||||
}
|
||||
|
||||
|
||||
class ComplexityGrowthGradient(FloatRangeText):
|
||||
"""If level requirements are randomized, this determines how fast complexity will grow each level. In other words:
|
||||
The higher you set this value, the more difficult lategame shapes will be.
|
||||
|
||||
Allowed values are floating numbers ranging from 0.0 to 10.0."""
|
||||
display_name = "Complexity growth gradient"
|
||||
rich_text_doc = True
|
||||
range_start = 0.0
|
||||
range_end = 10.0
|
||||
default = "0.5"
|
||||
|
||||
|
||||
class SameLateUpgradeRequirements(Toggle):
|
||||
"""If upgrade requirements are randomized, should the last 3 shapes for each category be the same,
|
||||
as in vanilla?"""
|
||||
display_name = "Same late upgrade requirements"
|
||||
rich_text_doc = True
|
||||
default = True
|
||||
|
||||
|
||||
class EarlyBalancerTunnelAndTrash(Choice):
|
||||
"""Makes the balancer, tunnel, and trash appear in earlier spheres.
|
||||
|
||||
- **None:** Complete randomization.
|
||||
- **5 buildings:** Should be accessible before getting all 5 main buildings.
|
||||
- **3 buildings:** Should be accessible before getting the first 3 main buildings for levels and upgrades.
|
||||
- **Sphere 1:** Always accessible from start. **Beware of generation failures.**"""
|
||||
display_name = "Early balancer, tunnel, and trash"
|
||||
rich_text_doc = True
|
||||
option_none = 0
|
||||
option_5_buildings = 1
|
||||
option_3_buildings = 2
|
||||
option_sphere_1 = 3
|
||||
default = 2
|
||||
|
||||
|
||||
class LockBeltAndExtractor(Toggle):
|
||||
"""Locks Belts and Extractors and adds them to the item pool.
|
||||
|
||||
**If you set this to true, achievements must also be included.**"""
|
||||
display_name = "Lock Belt and Extractor"
|
||||
rich_text_doc = True
|
||||
default = False
|
||||
|
||||
|
||||
class IncludeAchievements(Toggle):
|
||||
"""Include up to 45 achievements (depending on other options) as additional locations."""
|
||||
display_name = "Include Achievements"
|
||||
rich_text_doc = True
|
||||
default = True
|
||||
|
||||
|
||||
class ExcludeSoftlockAchievements(Toggle):
|
||||
"""Exclude 6 achievements, that can become unreachable in a save file, if not achieved until a certain level."""
|
||||
display_name = "Exclude softlock achievements"
|
||||
rich_text_doc = True
|
||||
default = True
|
||||
|
||||
|
||||
class ExcludeLongPlaytimeAchievements(Toggle):
|
||||
"""Exclude 2 achievements, that require actively playing for a really long time."""
|
||||
display_name = "Exclude long playtime achievements"
|
||||
rich_text_doc = True
|
||||
default = True
|
||||
|
||||
|
||||
class ExcludeProgressionUnreasonable(Toggle):
|
||||
"""Exclude progression and useful items from being placed into softlock and long playtime achievements."""
|
||||
display_name = "Exclude progression items in softlock and long playtime achievements"
|
||||
rich_text_doc = True
|
||||
default = True
|
||||
|
||||
|
||||
class ShapesanityAmount(Range):
|
||||
"""Amount of single-layer shapes that will be included as locations."""
|
||||
display_name = "Shapesanity amount"
|
||||
rich_text_doc = True
|
||||
range_start = 4
|
||||
range_end = max_shapesanity
|
||||
default = 50
|
||||
|
||||
|
||||
class TrapsProbability(NamedRange):
|
||||
"""The probability of any filler item (in percent) being replaced by a trap."""
|
||||
display_name = "Traps Percentage"
|
||||
rich_text_doc = True
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 0
|
||||
special_range_names = {
|
||||
"none": 0,
|
||||
"rare": 4,
|
||||
"occasionally": 10,
|
||||
"maximum_suffering": 100,
|
||||
}
|
||||
|
||||
|
||||
class IncludeWhackyUpgrades(Toggle):
|
||||
"""Includes some very unusual upgrade items in generation (and logic), that greatly increase or decrease building
|
||||
speeds. If the goal is set to Efficiency III or throughput levels ratio is not 0, decreasing upgrades (aka traps)
|
||||
will always be disabled."""
|
||||
display_name = "Include Whacky Upgrades"
|
||||
rich_text_doc = True
|
||||
default = False
|
||||
|
||||
|
||||
class SplitInventoryDrainingTrap(Toggle):
|
||||
"""If set to true, the inventory draining trap will be split into level, upgrade, and blueprint draining traps
|
||||
instead of executing as one of those 3 randomly."""
|
||||
display_name = "Split Inventory Draining Trap"
|
||||
rich_text_doc = True
|
||||
default = False
|
||||
|
||||
|
||||
class ToolbarShuffling(Toggle):
|
||||
"""If set to true, the toolbars (main and wires layer) will be shuffled (including bottom and top row).
|
||||
However, keybindings will still select the same building to place."""
|
||||
display_name = "Toolbar Shuffling"
|
||||
rich_text_doc = True
|
||||
default = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShapezOptions(PerGameCommonOptions):
|
||||
goal: Goal
|
||||
goal_amount: GoalAmount
|
||||
required_shapes_multiplier: RequiredShapesMultiplier
|
||||
allow_floating_layers: AllowFloatingLayers
|
||||
randomize_level_requirements: RandomizeLevelRequirements
|
||||
randomize_upgrade_requirements: RandomizeUpgradeRequirements
|
||||
randomize_level_logic: RandomizeLevelLogic
|
||||
randomize_upgrade_logic: RandomizeUpgradeLogic
|
||||
throughput_levels_ratio: ThroughputLevelsRatio
|
||||
complexity_growth_gradient: ComplexityGrowthGradient
|
||||
same_late_upgrade_requirements: SameLateUpgradeRequirements
|
||||
early_balancer_tunnel_and_trash: EarlyBalancerTunnelAndTrash
|
||||
lock_belt_and_extractor: LockBeltAndExtractor
|
||||
include_achievements: IncludeAchievements
|
||||
exclude_softlock_achievements: ExcludeSoftlockAchievements
|
||||
exclude_long_playtime_achievements: ExcludeLongPlaytimeAchievements
|
||||
exclude_progression_unreasonable: ExcludeProgressionUnreasonable
|
||||
shapesanity_amount: ShapesanityAmount
|
||||
traps_percentage: TrapsProbability
|
||||
include_whacky_upgrades: IncludeWhackyUpgrades
|
||||
split_inventory_draining_trap: SplitInventoryDrainingTrap
|
||||
toolbar_shuffling: ToolbarShuffling
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user