forked from mirror/Archipelago
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7971961166 | |||
| 9246bd9541 | |||
| 30fa0658b0 | |||
| 44a0c44036 |
@@ -51,7 +51,6 @@ EnemizerCLI/
|
|||||||
/SNI/
|
/SNI/
|
||||||
/sni-*/
|
/sni-*/
|
||||||
/appimagetool*
|
/appimagetool*
|
||||||
/host.yaml
|
|
||||||
/options.yaml
|
/options.yaml
|
||||||
/config.yaml
|
/config.yaml
|
||||||
/logs/
|
/logs/
|
||||||
|
|||||||
143
.github/workflows/docker.yml
vendored
143
.github/workflows/docker.yml
vendored
@@ -11,144 +11,43 @@ on:
|
|||||||
- "!.github/workflows/**"
|
- "!.github/workflows/**"
|
||||||
- ".github/workflows/docker.yml"
|
- ".github/workflows/docker.yml"
|
||||||
branches:
|
branches:
|
||||||
- "main"
|
- "dock-dev"
|
||||||
tags:
|
tags:
|
||||||
- "v?[0-9]+.[0-9]+.[0-9]*"
|
- "v?[0-9]+.[0-9]+.[0-9]*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
push_to_registry:
|
||||||
|
name: Push Docker image to Docker Hub
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
|
||||||
image-name: ${{ steps.image.outputs.name }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
package-name: ${{ steps.package.outputs.name }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set lowercase image name
|
|
||||||
id: image
|
|
||||||
run: |
|
|
||||||
echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Set package name
|
|
||||||
id: package
|
|
||||||
run: |
|
|
||||||
echo "name=$(basename ${GITHUB_REPOSITORY,,})" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Extract metadata
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch,enable={{is_not_default_branch}}
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=raw,value=nightly,enable={{is_default_branch}}
|
|
||||||
|
|
||||||
- name: Compute final tags
|
|
||||||
id: final-tags
|
|
||||||
run: |
|
|
||||||
readarray -t tags <<< "${{ steps.meta.outputs.tags }}"
|
|
||||||
|
|
||||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
|
||||||
tag="${{ github.ref_name }}"
|
|
||||||
if [[ "$tag" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
||||||
full_latest="${{ env.REGISTRY }}/${{ steps.image.outputs.name }}:latest"
|
|
||||||
# Check if latest is already in tags to avoid duplicates
|
|
||||||
if ! printf '%s\n' "${tags[@]}" | grep -q "^$full_latest$"; then
|
|
||||||
tags+=("$full_latest")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set multiline output
|
|
||||||
echo "tags<<EOF" >> $GITHUB_OUTPUT
|
|
||||||
printf '%s\n' "${tags[@]}" >> $GITHUB_OUTPUT
|
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
build:
|
|
||||||
needs: prepare
|
|
||||||
runs-on: ${{ matrix.runner }}
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
|
||||||
packages: write
|
packages: write
|
||||||
strategy:
|
contents: read
|
||||||
matrix:
|
attestations: write
|
||||||
include:
|
id-token: write
|
||||||
- platform: amd64
|
|
||||||
runner: ubuntu-latest
|
|
||||||
suffix: amd64
|
|
||||||
cache-scope: amd64
|
|
||||||
- platform: arm64
|
|
||||||
runner: ubuntu-24.04-arm
|
|
||||||
suffix: arm64
|
|
||||||
cache-scope: arm64
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Log in to Docker Hub
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
username: ${{ secrets.DOCKERHUB_USER }}
|
||||||
username: ${{ github.actor }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Compute suffixed tags
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: tags
|
id: meta
|
||||||
run: |
|
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||||
readarray -t tags <<< "${{ needs.prepare.outputs.tags }}"
|
with:
|
||||||
suffixed=()
|
images: ubufugu/dockipelago
|
||||||
for t in "${tags[@]}"; do
|
|
||||||
suffixed+=("$t-${{ matrix.suffix }}")
|
|
||||||
done
|
|
||||||
echo "tags=$(IFS=','; echo "${suffixed[*]}")" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5
|
id: push
|
||||||
|
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/${{ matrix.platform }}
|
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.tags.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ needs.prepare.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha,scope=${{ matrix.cache-scope }}
|
|
||||||
cache-to: type=gha,mode=max,scope=${{ matrix.cache-scope }}
|
|
||||||
provenance: false
|
|
||||||
|
|
||||||
manifest:
|
|
||||||
needs: [prepare, build]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Create and push multi-arch manifest
|
|
||||||
run: |
|
|
||||||
readarray -t tag_array <<< "${{ needs.prepare.outputs.tags }}"
|
|
||||||
|
|
||||||
for tag in "${tag_array[@]}"; do
|
|
||||||
docker manifest create "$tag" \
|
|
||||||
"$tag-amd64" \
|
|
||||||
"$tag-arm64"
|
|
||||||
|
|
||||||
docker manifest push "$tag"
|
|
||||||
done
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,8 +45,11 @@ EnemizerCLI/
|
|||||||
/SNI/
|
/SNI/
|
||||||
/sni-*/
|
/sni-*/
|
||||||
/appimagetool*
|
/appimagetool*
|
||||||
|
<<<<<<< Updated upstream
|
||||||
/VC_redist.x64.exe
|
/VC_redist.x64.exe
|
||||||
/host.yaml
|
/host.yaml
|
||||||
|
=======
|
||||||
|
>>>>>>> Stashed changes
|
||||||
/options.yaml
|
/options.yaml
|
||||||
/config.yaml
|
/config.yaml
|
||||||
/logs/
|
/logs/
|
||||||
|
|||||||
@@ -97,4 +97,10 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
|||||||
# Ensure no runtime ModuleUpdate.
|
# Ensure no runtime ModuleUpdate.
|
||||||
ENV SKIP_REQUIREMENTS_UPDATE=true
|
ENV SKIP_REQUIREMENTS_UPDATE=true
|
||||||
|
|
||||||
|
# Port range for Archipelago rooms. I choose only ports 49152-49162
|
||||||
|
ARG MAX_PORT=49162
|
||||||
|
|
||||||
|
RUN sed -i "s/65535/${MAX_PORT}/" WebHostLib/customserver.py
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
ENTRYPOINT [ "python", "WebHost.py" ]
|
ENTRYPOINT [ "python", "WebHost.py" ]
|
||||||
|
|||||||
586
host.yaml
Normal file
586
host.yaml
Normal file
@@ -0,0 +1,586 @@
|
|||||||
|
general_options:
|
||||||
|
# Where to place output files
|
||||||
|
output_path: "output"
|
||||||
|
# Options for MultiServer
|
||||||
|
# Null means nothing, for the server this means to default the value
|
||||||
|
# These overwrite command line arguments!
|
||||||
|
server_options:
|
||||||
|
host: null
|
||||||
|
port: 38281
|
||||||
|
password: null
|
||||||
|
multidata: null
|
||||||
|
savefile: null
|
||||||
|
disable_save: false
|
||||||
|
loglevel: "info"
|
||||||
|
logtime: false
|
||||||
|
# Allows for clients to log on and manage the server. If this is null, no remote administration is possible.
|
||||||
|
server_password: null
|
||||||
|
# Disallow !getitem
|
||||||
|
disable_item_cheat: false
|
||||||
|
# Client hint system
|
||||||
|
# Points given to a player for each acquired item in their world
|
||||||
|
location_check_points: 1
|
||||||
|
# Relative point cost to receive a hint via !hint for players
|
||||||
|
# so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint,
|
||||||
|
# for a total of 5
|
||||||
|
hint_cost: 10
|
||||||
|
# Release modes
|
||||||
|
# A Release sends out the remaining items *from* a world that releases
|
||||||
|
# "disabled" -> clients can't release,
|
||||||
|
# "enabled" -> clients can always release
|
||||||
|
# "auto" -> automatic release on goal completion
|
||||||
|
# "auto-enabled" -> automatic release on goal completion and manual release is also enabled
|
||||||
|
# "goal" -> release is allowed after goal completion
|
||||||
|
release_mode: "auto"
|
||||||
|
# Collect modes
|
||||||
|
# A Collect sends the remaining items *to* a world that collects
|
||||||
|
# "disabled" -> clients can't collect,
|
||||||
|
# "enabled" -> clients can always collect
|
||||||
|
# "auto" -> automatic collect on goal completion
|
||||||
|
# "auto-enabled" -> automatic collect on goal completion and manual collect is also enabled
|
||||||
|
# "goal" -> collect is allowed after goal completion
|
||||||
|
collect_mode: "auto"
|
||||||
|
# Remaining modes
|
||||||
|
# !remaining handling, that tells a client which items remain in their pool
|
||||||
|
# "enabled" -> Client can always ask for remaining items
|
||||||
|
# "disabled" -> Client can never ask for remaining items
|
||||||
|
# "goal" -> Client can ask for remaining items after goal completion
|
||||||
|
remaining_mode: "goal"
|
||||||
|
# Countdown modes
|
||||||
|
# Determines whether or not a player can initiate a countdown with !countdown
|
||||||
|
# Note that /countdown is always available to the host.
|
||||||
|
# "enabled" -> Client can always initiate a countdown with !countdown.
|
||||||
|
# "disabled" -> Client can never initiate a countdown with !countdown.
|
||||||
|
# "auto" -> !countdown will be available for any room with less than 30 slots.
|
||||||
|
countdown_mode: "auto"
|
||||||
|
# Automatically shut down the server after this many seconds without new location checks, 0 to keep running
|
||||||
|
auto_shutdown: 0
|
||||||
|
# Compatibility handling
|
||||||
|
# 2 -> Recommended for casual/cooperative play, attempt to be compatible with everything across all versions
|
||||||
|
# 1 -> No longer in use, kept reserved in case of future use
|
||||||
|
# 0 -> Recommended for tournaments to force a level playing field, only allow an exact version match
|
||||||
|
compatibility: 2
|
||||||
|
# log all server traffic, mostly for dev use
|
||||||
|
log_network: 0
|
||||||
|
# Options for Generation
|
||||||
|
generator:
|
||||||
|
# Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases
|
||||||
|
enemizer_path: "EnemizerCLI/EnemizerCLI.Core"
|
||||||
|
# Folder from which the player yaml files are pulled from
|
||||||
|
player_files_path: "Players"
|
||||||
|
# amount of players, 0 to infer from player files
|
||||||
|
players: 0
|
||||||
|
# general weights file, within the stated player_files_path location
|
||||||
|
# gets used if players is higher than the amount of per-player files found to fill remaining slots
|
||||||
|
weights_file_path: "weights.yaml"
|
||||||
|
# Meta file name, within the stated player_files_path location
|
||||||
|
meta_file_path: "meta.yaml"
|
||||||
|
# Create a spoiler file
|
||||||
|
# 0 -> None
|
||||||
|
# 1 -> Spoiler without playthrough or paths to playthrough required items
|
||||||
|
# 2 -> Spoiler with playthrough (viable solution to goals)
|
||||||
|
# 3 -> Spoiler with playthrough and traversal paths towards items
|
||||||
|
spoiler: 3
|
||||||
|
# Create encrypted race roms and flag games as race mode
|
||||||
|
race: 0
|
||||||
|
# List of options that can be plando'd. Can be combined, for example "bosses, items"
|
||||||
|
# Available options: bosses, items, texts, connections
|
||||||
|
plando_options: "bosses, connections, texts"
|
||||||
|
# What to do if the current item placements appear unsolvable.
|
||||||
|
# raise -> Raise an exception and abort.
|
||||||
|
# swap -> Attempt to fix it by swapping prior placements around. (Default)
|
||||||
|
# start_inventory -> Move remaining items to start_inventory, generate additional filler items to fill locations.
|
||||||
|
panic_method: "swap"
|
||||||
|
loglevel: "info"
|
||||||
|
logtime: false
|
||||||
|
sni_options:
|
||||||
|
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
|
||||||
|
sni_path: "SNI"
|
||||||
|
# Set this to false to never autostart a rom (such as after patching)
|
||||||
|
# True for operating system default program
|
||||||
|
# Alternatively, a path to a program to open the .sfc file with
|
||||||
|
snes_rom_start: true
|
||||||
|
bizhawkclient_options:
|
||||||
|
# The location of the EmuHawk you want to auto launch patched ROMs with
|
||||||
|
emuhawk_path: "None"
|
||||||
|
# Set this to true to autostart a patched ROM in BizHawk with the connector script,
|
||||||
|
# to false to never open the patched rom automatically,
|
||||||
|
# or to a path to an external program to open the ROM file with that instead.
|
||||||
|
rom_start: true
|
||||||
|
adventure_options:
|
||||||
|
# File name of the standard NTSC Adventure rom.
|
||||||
|
# The licensed "The 80 Classic Games" CD-ROM contains this.
|
||||||
|
# It may also have a .a26 extension
|
||||||
|
rom_file: "roms/ADVNTURE.BIN"
|
||||||
|
# Set this to false to never autostart a rom (such as after patching)
|
||||||
|
# True for operating system default program for '.a26'
|
||||||
|
# Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld)
|
||||||
|
rom_start: true
|
||||||
|
# Optional, additional args passed into rom_start before the .bin file
|
||||||
|
# For example, this can be used to autoload the connector script in BizHawk
|
||||||
|
# (see BizHawk --lua= option)
|
||||||
|
# Windows example:
|
||||||
|
# rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/connector_adventure.lua"
|
||||||
|
rom_args: " "
|
||||||
|
# Set this to true to display item received messages in EmuHawk
|
||||||
|
display_msgs: true
|
||||||
|
ape_escape_3_options:
|
||||||
|
# Preferences for game session management.
|
||||||
|
# > save_state_on_room_transition: Automatically create a save state when transitioning between rooms.
|
||||||
|
# > save_state_on_item_received: Automatically create a save state when receiving a new progressive item.
|
||||||
|
# > save_state_on_location_check: Automatically create a save state when checking a new location.
|
||||||
|
# > load_state_on_connect: Load a state automatically after connecting to the multiworld if the client
|
||||||
|
# is already connected to the game and that the last save is from a save state and not a normal game save.
|
||||||
|
save_state_on_room_transition: false
|
||||||
|
save_state_on_item_received: true
|
||||||
|
save_state_on_location_check: false
|
||||||
|
load_state_on_connect: false
|
||||||
|
# Preferences for game/client-enforcement behavior
|
||||||
|
# > auto-equip : Automatically assign received gadgets to a face button
|
||||||
|
auto_equip: true
|
||||||
|
# Preferences for game generation. Only relevant for world generation and not the setup of or during play.
|
||||||
|
# > whitelist_pgc_bypass: Allow Ape Escape 3 players to enable "PGC Bypass" as a possible outcome for
|
||||||
|
# Lucky Ticket Consolation Prize.
|
||||||
|
# > whitelist_instant_goal: Allow Ape Escape 3 players to enable "Instant Goal" as a possible outcome for
|
||||||
|
# Lucky Ticket Consolation Prize.
|
||||||
|
whitelist_pgc_bypass: false
|
||||||
|
whitelist_instant_goal: false
|
||||||
|
banjo_tooie_options:
|
||||||
|
# File path of the Banjo-Tooie (USA) ROM.
|
||||||
|
rom_path: ""
|
||||||
|
# Folder path of where to save the patched ROM.
|
||||||
|
patch_path: ""
|
||||||
|
# File path of the program to automatically run.
|
||||||
|
# Leave blank to disable.
|
||||||
|
program_path: ""
|
||||||
|
# Arguments to pass to the automatically run program.
|
||||||
|
# Leave blank to disable.
|
||||||
|
# Set to "--lua=" to automatically use the correct path for the lua connector.
|
||||||
|
program_args: "--lua="
|
||||||
|
# No idea
|
||||||
|
clair_obscur_options:
|
||||||
|
{}
|
||||||
|
cv64_options:
|
||||||
|
# File name of the CV64 US 1.0 rom
|
||||||
|
rom_file: "roms/Castlevania (USA).z64"
|
||||||
|
cv_dos_options:
|
||||||
|
# File name of the Castlevania: Dawn of Sorrow ROM file.
|
||||||
|
rom_file: "roms/CASTLEVANIA1_ACVEA4_00.nds"
|
||||||
|
cvcotm_options:
|
||||||
|
# File name of the Castlevania CotM US rom
|
||||||
|
rom_file: "roms/Castlevania - Circle of the Moon (USA).gba"
|
||||||
|
cvhodis_options:
|
||||||
|
# File name of the Castlevania HoD US rom
|
||||||
|
rom_file: "roms/Castlevania - Harmony of Dissonance (USA).gba"
|
||||||
|
cvlod_options:
|
||||||
|
# File name of the CVLoD US rom
|
||||||
|
rom_file: "Castlevania - Legacy of Darkness (USA).z64"
|
||||||
|
# Settings for the DK64 randomizer.
|
||||||
|
dk64_options:
|
||||||
|
# Choose the release version of the DK64 randomizer to use.
|
||||||
|
# By setting it to master (Default) you will always pull the latest stable version.
|
||||||
|
# By setting it to dev you will pull the latest development version.
|
||||||
|
# If you want a specific version, you can set it to a AP version number eg: v1.0.45
|
||||||
|
release_branch: "master"
|
||||||
|
dkc2_options:
|
||||||
|
# File name of the Donkey Kong Country 2 US v1.1 ROM
|
||||||
|
rom_file: "roms/Donkey Kong Country 2 - Diddy's Kong Quest (USA).sfc"
|
||||||
|
# Path to the user's Donkey Kong Country 2 Poptracker Pack.
|
||||||
|
ut_poptracker_path: ""
|
||||||
|
# Folder path of the trivia database
|
||||||
|
# Preferably point it to /data/trivia/dkc2/
|
||||||
|
trivia_path: "data/trivia/dkc2"
|
||||||
|
dkc3_options:
|
||||||
|
# File name of the DKC3 US rom
|
||||||
|
rom_file: "roms/Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
|
||||||
|
earthbound_options:
|
||||||
|
# File name of the EarthBound US ROM
|
||||||
|
rom_file: "roms/EarthBound.sfc"
|
||||||
|
factorio_options:
|
||||||
|
executable: "factorio/bin/x64/factorio"
|
||||||
|
# by default, no settings are loaded if this file does not exist. If this file does exist, then it will be used.
|
||||||
|
# server_settings: "factorio\\data\\server-settings.json"
|
||||||
|
server_settings: null
|
||||||
|
# Whether to filter item send messages displayed in-game to only those that involve you.
|
||||||
|
filter_item_sends: false
|
||||||
|
# Whether to filter connection changes displayed in-game.
|
||||||
|
filter_connection_changes: false
|
||||||
|
# Whether to send chat messages from players on the Factorio server to Archipelago.
|
||||||
|
bridge_chat_out: true
|
||||||
|
fe8_settings:
|
||||||
|
# File name of your Fire Emblem: The Sacred Stones (U) ROM
|
||||||
|
rom_file: "roms/Fire Emblem The Sacred Stones (U).gba"
|
||||||
|
ffr_options:
|
||||||
|
display_msgs: true
|
||||||
|
gauntletlegends_options:
|
||||||
|
# The location of your Retroarch folder
|
||||||
|
retroarch_path: "None"
|
||||||
|
# File name of the GL US rom
|
||||||
|
rom_file: "roms/Gauntlet Legends (U) [!].z64"
|
||||||
|
rom_start: true
|
||||||
|
glover_options:
|
||||||
|
# File path of the Glover (USA) ROM.
|
||||||
|
rom_path: ""
|
||||||
|
# Folder path of where to save the patched ROM.
|
||||||
|
patch_path: ""
|
||||||
|
# File path of the program to automatically run.
|
||||||
|
# Leave blank to disable.
|
||||||
|
program_path: ""
|
||||||
|
# Arguments to pass to the automatically run program.
|
||||||
|
# Leave blank to disable.
|
||||||
|
# Set to "--lua=" to automatically use the correct path for the lua connector.
|
||||||
|
program_args: "--lua="
|
||||||
|
gstla_options:
|
||||||
|
# File name of the GS TLA UE Rom
|
||||||
|
rom_file: "roms/Golden Sun - The Lost Age (UE) [!].gba"
|
||||||
|
hades_options:
|
||||||
|
# Path to the StyxScribe install
|
||||||
|
styx_scribe_path: "C:/Program Files/Steam/steamapps/common/Hades/StyxScribe.py"
|
||||||
|
hk_options:
|
||||||
|
# Disallows the APMapMod from showing spoiler placements.
|
||||||
|
disable_spoilers: false
|
||||||
|
jakanddaxter_options:
|
||||||
|
# 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.
|
||||||
|
root_directory: "%programfiles%/OpenGOAL-Launcher/features/jak1/mods/JakMods/archipelagoal"
|
||||||
|
# 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.
|
||||||
|
auto_detect_root_directory: true
|
||||||
|
# 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!
|
||||||
|
enforce_friendly_options: true
|
||||||
|
k64_options:
|
||||||
|
# File name of the K64 EN rom
|
||||||
|
rom_file: "roms/Kirby 64 - The Crystal Shards (USA).z64"
|
||||||
|
kdl3_options:
|
||||||
|
# File name of the KDL3 JP or EN rom
|
||||||
|
rom_file: "roms/Kirby's Dream Land 3.sfc"
|
||||||
|
ladx_options:
|
||||||
|
# File name of the Link's Awakening DX rom
|
||||||
|
rom_file: "roms/Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"
|
||||||
|
# Set this to false to never autostart a rom (such as after patching)
|
||||||
|
# true for operating system default program
|
||||||
|
# Alternatively, a path to a program to open the .gbc file with
|
||||||
|
# Examples:
|
||||||
|
# Retroarch:
|
||||||
|
# rom_start: "C:/RetroArch-Win64/retroarch.exe -L sameboy"
|
||||||
|
# BizHawk:
|
||||||
|
# rom_start: "C:/BizHawk-2.9-win-x64/EmuHawk.exe --lua=data/lua/connector_ladx_bizhawk.lua"
|
||||||
|
rom_start: true
|
||||||
|
# Gfxmod file, get it from upstream: https://github.com/daid/LADXR/tree/master/gfx
|
||||||
|
# Only .bin or .bdiff files
|
||||||
|
# The same directory will be checked for a matching text modification file
|
||||||
|
gfx_mod_file: ""
|
||||||
|
lttp_options:
|
||||||
|
# File name of the v1.0 J rom
|
||||||
|
rom_file: "roms/Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
||||||
|
lufia2ac_options:
|
||||||
|
# File name of the US rom
|
||||||
|
rom_file: "roms/Lufia II - Rise of the Sinistrals (USA).sfc"
|
||||||
|
messenger_settings:
|
||||||
|
game_path: "TheMessenger.exe"
|
||||||
|
metroidzeromission_options:
|
||||||
|
# File name of the Metroid: Zero Mission ROM.
|
||||||
|
rom_file: "roms/Metroid - Zero Mission (USA).gba"
|
||||||
|
# Set this to false to never autostart a rom (such as after patching),
|
||||||
|
# Set it to true to have the operating system default program open the rom
|
||||||
|
# Alternatively, set it to a path to a program to open the .gba file with
|
||||||
|
rom_start: true
|
||||||
|
mk64_options:
|
||||||
|
# File name of the MK64 ROM
|
||||||
|
rom_file: "roms/Mario Kart 64 (U) [!].z64"
|
||||||
|
metroidfusion_options:
|
||||||
|
# File name of the Metroid Fusion ROM
|
||||||
|
rom_file: "roms/Metroid Fusion (USA).gba"
|
||||||
|
rom_start: true
|
||||||
|
display_location_found_messages: true
|
||||||
|
mlss_options:
|
||||||
|
# File name of the MLSS US rom
|
||||||
|
rom_file: "roms/Mario & Luigi - Superstar Saga (U).gba"
|
||||||
|
rom_start: true
|
||||||
|
mm2_options:
|
||||||
|
# File name of the MM2 EN rom
|
||||||
|
rom_file: "roms/Mega Man 2 (USA).nes"
|
||||||
|
mmbn3_options:
|
||||||
|
# File name of the MMBN3 Blue US rom
|
||||||
|
rom_file: "roms/Mega Man Battle Network 3 - Blue Version (USA).gba"
|
||||||
|
# Set this to false to never autostart a rom (such as after patching),
|
||||||
|
# true for operating system default program
|
||||||
|
# Alternatively, a path to a program to open the .gba file with
|
||||||
|
rom_start: true
|
||||||
|
mzm_options:
|
||||||
|
rom_file: "roms/Metroid - Zero Mission (USA).gba"
|
||||||
|
rom_start: true
|
||||||
|
oot_options:
|
||||||
|
# File name of the OoT v1.0 ROM
|
||||||
|
rom_file: "roms/The Legend of Zelda - Ocarina of Time.z64"
|
||||||
|
# Set this to false to never autostart a rom (such as after patching),
|
||||||
|
# true for operating system default program
|
||||||
|
# Alternatively, a path to a program to open the .z64 file with
|
||||||
|
rom_start: true
|
||||||
|
paper_mario_settings:
|
||||||
|
# File name of the Paper Mario USA ROM
|
||||||
|
rom_file: "roms/Paper Mario (USA).z64"
|
||||||
|
# Set this to false to never autostart a rom (such as after patching),
|
||||||
|
# true for operating system default program
|
||||||
|
# Alternatively, a path to a program to open the .z64 file with
|
||||||
|
rom_start: true
|
||||||
|
papermariottyd_options:
|
||||||
|
# The location of the Dolphin you want to auto launch patched ROMs with
|
||||||
|
dolphin_path: "None"
|
||||||
|
# File name of the TTYD US iso
|
||||||
|
rom_file: "roms/Paper Mario - The Thousand-Year Door (USA).iso"
|
||||||
|
rom_start: true
|
||||||
|
pmd_eos_options:
|
||||||
|
# File name of the EoS EU rom
|
||||||
|
rom_file: "roms/POKEDUN_SORA_C2SP01_00.nds"
|
||||||
|
rom_start: true
|
||||||
|
pokemon_bw_settings:
|
||||||
|
# File name of your Pokémon Black Version ROM
|
||||||
|
black_rom: "PokemonBlack.nds"
|
||||||
|
# File name of your Pokémon White Version ROM
|
||||||
|
white_rom: "PokemonWhite.nds"
|
||||||
|
# Toggles whether Encounter Plando is enabled for players in generation.
|
||||||
|
# If disabled, yamls that use Encounter Plando do not raise OptionErrors, but display a warning.
|
||||||
|
enable_encounter_plando: true
|
||||||
|
# If enabled, files inside the rom that are changed as part of the patching process (except for base patches)
|
||||||
|
# will be dumped into a zip file next to the patched rom (for debug purposes).
|
||||||
|
dump_patched_files: false
|
||||||
|
pokemon_crystal_settings:
|
||||||
|
rom_file: "roms/Pokemon - Crystal Version (UE) [C][!].gbc"
|
||||||
|
pokemon_emerald_settings:
|
||||||
|
# File name of your English Pokemon Emerald ROM
|
||||||
|
rom_file: "roms/Pokemon - Emerald Version (USA, Europe).gba"
|
||||||
|
pokemon_frlg_settings:
|
||||||
|
# File name of your English Pokémon FireRed ROM
|
||||||
|
firered_rom_file: "roms/Pokemon - FireRed Version (USA, Europe).gba"
|
||||||
|
# File name of your English Pokémon LeafGreen ROM
|
||||||
|
leafgreen_rom_file: "roms/Pokemon - LeafGreen Version (USA, Europe).gba"
|
||||||
|
ut_poptracker_path: ""
|
||||||
|
pokemon_platinum_settings:
|
||||||
|
rom_file: "roms/pokeplatinum.nds"
|
||||||
|
pokemon_rb_options:
|
||||||
|
# File names of the Pokemon Red and Blue roms
|
||||||
|
red_rom_file: "roms/Pokemon Red (UE) [S][!].gb"
|
||||||
|
blue_rom_file: "roms/Pokemon Blue (UE) [S][!].gb"
|
||||||
|
pokepinball_settings:
|
||||||
|
# File name of the Pokemon Pinball Color US rom
|
||||||
|
rom_file: "roms/PokemonPinball.gbc"
|
||||||
|
portal2_options:
|
||||||
|
# The file path of the extras.txt file (used to generate the menu in game)
|
||||||
|
menu_file: "C:\\Program Files (x86)\\Steam\\steamapps\\sourcemods\\Portal2Archipelago\\scripts\\extras.txt"
|
||||||
|
# The port set in the portal 2 launch options e.g. 3000
|
||||||
|
default_portal2_port: 3000
|
||||||
|
saving_princess_settings:
|
||||||
|
# Path to the game executable from which files are extracted
|
||||||
|
exe_path: "Saving Princess.exe"
|
||||||
|
# Path to the mod installation folder
|
||||||
|
install_folder: "Saving Princess"
|
||||||
|
# Set this to false to never autostart the game
|
||||||
|
launch_game: true
|
||||||
|
# The console command that will be used to launch the game
|
||||||
|
# The command will be executed with the installation folder as the current directory
|
||||||
|
launch_command: "wine \"Saving Princess v0_8.exe\""
|
||||||
|
sc2_options:
|
||||||
|
# The starting width the client window in pixels
|
||||||
|
window_width: 1080
|
||||||
|
# The starting height the client window in pixels
|
||||||
|
window_height: 720
|
||||||
|
# Controls whether the game should start in windowed mode
|
||||||
|
game_windowed_mode: false
|
||||||
|
# If set to true, in-client scouting will show traps as distinct from filler
|
||||||
|
show_traps: false
|
||||||
|
# Overrides the disable forced-camera slot option. Possible values: `true`, `false`, `default`. Default uses slot value
|
||||||
|
disable_forced_camera: "default"
|
||||||
|
# Overrides the skip cutscenes slot option. Possible values: `true`, `false`, `default`. Default uses slot value
|
||||||
|
skip_cutscenes: "default"
|
||||||
|
# Overrides the slot's difficulty setting. Possible values: `casual`, `normal`, `hard`, `brutal`, `default`. Default uses slot value
|
||||||
|
game_difficulty: "default"
|
||||||
|
# Overrides the slot's gamespeed setting. Possible values: `slower`, `slow`, `normal`, `fast`, `faster`, `default`. Default uses slot value
|
||||||
|
game_speed: "default"
|
||||||
|
# Defines the colour of terran mission buttons in the launcher in rgb format (3 elements ranging from 0 to 1)
|
||||||
|
terran_button_color:
|
||||||
|
- 0.0838
|
||||||
|
- 0.2898
|
||||||
|
- 0.2346
|
||||||
|
# Defines the colour of zerg mission buttons in the launcher in rgb format (3 elements ranging from 0 to 1)
|
||||||
|
zerg_button_color:
|
||||||
|
- 0.345
|
||||||
|
- 0.22425
|
||||||
|
- 0.12765
|
||||||
|
# Defines the colour of protoss mission buttons in the launcher in rgb format (3 elements ranging from 0 to 1)
|
||||||
|
protoss_button_color:
|
||||||
|
- 0.18975
|
||||||
|
- 0.2415
|
||||||
|
- 0.345
|
||||||
|
sf64_options:
|
||||||
|
# File path of the Star Fox 64 v1.1 ROM.
|
||||||
|
rom_path: ""
|
||||||
|
# Folder path of where to save the patched ROM.
|
||||||
|
patch_path: ""
|
||||||
|
# File path of the program to automatically run.
|
||||||
|
# Leave blank to disable.
|
||||||
|
program_path: ""
|
||||||
|
# Arguments to pass to the automatically run program.
|
||||||
|
# Leave blank to disable.
|
||||||
|
program_args: "--lua=\\\\wsl.localhost\\Ubuntu\\home\\ubufu\\ap-cm-1dd91ec\\Archipelago-main\\data\\lua\\connector_sf64_bizhawk.lua"
|
||||||
|
# Whether to enable the built in logic Tracker.
|
||||||
|
# If enabled, the 'Tracker' tab will show all unchecked locations in logic.
|
||||||
|
enable_tracker: true
|
||||||
|
sm_options:
|
||||||
|
# File name of the v1.0 J rom
|
||||||
|
rom_file: "roms/Super Metroid (JU).sfc"
|
||||||
|
sml2_options:
|
||||||
|
# File name of the Super Mario Land 2 1.0 ROM
|
||||||
|
rom_file: "roms/Super Mario Land 2 - 6 Golden Coins (USA, Europe).gb"
|
||||||
|
sms_options:
|
||||||
|
iso_file: "roms/sms_us_2002.iso"
|
||||||
|
smw_options:
|
||||||
|
# File name of the SMW US rom
|
||||||
|
rom_file: "roms/Super Mario World (USA).sfc"
|
||||||
|
soe_options:
|
||||||
|
# File name of the SoE US ROM
|
||||||
|
rom_file: "roms/Secret of Evermore (USA).sfc"
|
||||||
|
spyro2_options:
|
||||||
|
# Permits full gemsanity options for multiplayer games.
|
||||||
|
# Full gemsanity adds 2546 locations and an equal number of progression items.
|
||||||
|
# These items may be local-only or spread across the multiworld.
|
||||||
|
allow_full_gemsanity: false
|
||||||
|
stadium_options:
|
||||||
|
# File name of the Pokemon Stadium (US, 1.0) ROM
|
||||||
|
rom_file: "roms/Pokemon Stadium (US, 1.0).z64"
|
||||||
|
stardew_valley_options:
|
||||||
|
# Allow players to pick the goal 'Allsanity'. If disallowed, generation will fail.
|
||||||
|
allow_allsanity: true
|
||||||
|
# Allow players to pick the goal 'Perfection'. If disallowed, generation will fail.
|
||||||
|
allow_perfection: true
|
||||||
|
# Allow players to pick the option 'Bundle Price: Maximum'. If disallowed, it will be replaced with 'Very Expensive'
|
||||||
|
allow_max_bundles: true
|
||||||
|
# Allow players to pick the option 'Entrance Randomization: Chaos'. If disallowed, it will be replaced with 'Buildings'
|
||||||
|
allow_chaos_er: false
|
||||||
|
# Allow players to pick the option 'Shipsanity: Everything'. If disallowed, it will be replaced with 'Full Shipment With Fish'
|
||||||
|
allow_shipsanity_everything: true
|
||||||
|
# Allow players to pick the option 'Hatsanity: Near Perfection OR Post Perfection'. If disallowed, it will be replaced with 'Difficult'
|
||||||
|
allow_hatsanity_perfection: true
|
||||||
|
# Allow players to toggle on Custom logic flags. If disallowed, it will be disabled
|
||||||
|
allow_custom_logic: true
|
||||||
|
# Allow players to enable Jojapocalypse. If disallowed, it will be disabled
|
||||||
|
allow_jojapocalypse: false
|
||||||
|
tcg_card_shop_simulator_options:
|
||||||
|
# This limits goals to a reasonable number and sets all excessive settings to local_fill or Excluded for better sync experiences.
|
||||||
|
limit_checks_for_syncs: false
|
||||||
|
# Card Sanity adds pure randomness to card checks. This option disables this sanity in your multiworlds
|
||||||
|
allow_card_sanity: true
|
||||||
|
tloz_ooa_options:
|
||||||
|
# File path of the OOA US rom
|
||||||
|
rom_file: "roms/Legend of Zelda, The - Oracle of Ages (USA).gbc"
|
||||||
|
# A factor applied to the infamous heart beep sound interval.
|
||||||
|
# Valid values are: "vanilla", "half", "quarter", "disabled"
|
||||||
|
heart_beep_interval: "vanilla"
|
||||||
|
# The name of the sprite file to use (from "data/sprites/oos_ooa/").
|
||||||
|
# Putting "link" as a value uses the default game sprite.
|
||||||
|
# Putting "random" as a value randomly picks a sprite from your sprites directory for each generated ROM.
|
||||||
|
character_sprite: "link"
|
||||||
|
# The color palette used for character sprite throughout the game.
|
||||||
|
# Valid values are: "green", "red", "blue", "orange", and "random"
|
||||||
|
character_palette: "green"
|
||||||
|
# Defines if you don't want to spam the buttons to swim with the mermaid suit.
|
||||||
|
qol_mermaid_suit: true
|
||||||
|
# When enabled, playing the flute and the harp will immobilize you during a very small amount of time compared to vanilla game.
|
||||||
|
qol_quick_flute: true
|
||||||
|
# Defines if you want to skip the small dance that tokkay does
|
||||||
|
skip_tokkey_dance: false
|
||||||
|
# Defines if you want to skip the joke you tell to the sad boi
|
||||||
|
skip_boi_joke: false
|
||||||
|
tloz_oos_options:
|
||||||
|
# File name of the Oracle of Seasons US ROM
|
||||||
|
rom_file: "roms/Legend of Zelda, The - Oracle of Seasons (USA).gbc"
|
||||||
|
# File name of the Oracle of Ages US ROM (only needed for cross items)
|
||||||
|
ages_rom_file: "roms/Legend of Zelda, The - Oracle of Ages (USA).gbc"
|
||||||
|
rom_start: true
|
||||||
|
# The name of the sprite file to use (from "data/sprites/oos_ooa/").
|
||||||
|
# Putting "link" as a value uses the default game sprite.
|
||||||
|
# Putting "random" as a value randomly picks a sprite from your sprites directory for each generated ROM.
|
||||||
|
# If you want some weighted result, you can arrange the options like in your option yaml.
|
||||||
|
character_sprite: "link"
|
||||||
|
# The color palette used for character sprite throughout the game.
|
||||||
|
# Valid values are: "green", "red", "blue", "orange", and "random"
|
||||||
|
# If you want some weighted result, you can arrange the options like in your option yaml.
|
||||||
|
# If you want a color weight to only apply to a specific sprite, you can write color|sprite: weight.
|
||||||
|
# For example, red|link: 1 would add red in the possible palettes with a weight of 1 only if link is the selected sprite
|
||||||
|
character_palette: "green"
|
||||||
|
# If enabled, hidden digging spots in Subrosia are revealed as diggable tiles.
|
||||||
|
reveal_hidden_subrosia_digging_spots: true
|
||||||
|
# A factor applied to the infamous heart beep sound interval.
|
||||||
|
# Valid values are: "vanilla", "half", "quarter", "disabled"
|
||||||
|
heart_beep_interval: "vanilla"
|
||||||
|
# If true, no music will be played in the game while sound effects remain untouched
|
||||||
|
remove_music: false
|
||||||
|
tloz_options:
|
||||||
|
# File name of the Zelda 1
|
||||||
|
rom_file: "roms/Legend of Zelda, The (U) (PRG0) [!].nes"
|
||||||
|
# Set this to false to never autostart a rom (such as after patching)
|
||||||
|
# true for operating system default program
|
||||||
|
# Alternatively, a path to a program to open the .nes file with
|
||||||
|
rom_start: true
|
||||||
|
# Display message inside of Bizhawk
|
||||||
|
display_msgs: true
|
||||||
|
tloz_ph_options:
|
||||||
|
# For use with universal tracker.
|
||||||
|
# Toggles if universal tracker can use unlocked shortcuts and map warps to find shorter paths for /get_logical_path.
|
||||||
|
ut_get_logical_path_shortcuts: true
|
||||||
|
tloz_st_options:
|
||||||
|
# Train speed for each of the 4 gears, from lowest (reverse) to highest.
|
||||||
|
# defaults are -143, 0, 115, 193
|
||||||
|
train_speed:
|
||||||
|
- -143
|
||||||
|
- 0
|
||||||
|
- 115
|
||||||
|
- 193
|
||||||
|
# The train will instantly switch to the new speed when changing gears, no acceleration required.
|
||||||
|
# Does not apply to your stop gear.
|
||||||
|
train_snap_speed: true
|
||||||
|
# Allows entering stations immediately on the stop gear, no matter your speed.
|
||||||
|
train_quick_station: true
|
||||||
|
ttyd_options:
|
||||||
|
# The location of the Dolphin you want to auto launch patched ROMs with
|
||||||
|
dolphin_path: "None"
|
||||||
|
# File name of the TTYD US iso
|
||||||
|
rom_file: "roms/Paper Mario - The Thousand-Year Door (USA).iso"
|
||||||
|
rom_start: true
|
||||||
|
tunic_options:
|
||||||
|
# Disallows the TUNIC client from creating a local spoiler log.
|
||||||
|
disable_local_spoiler: false
|
||||||
|
# Limits the impact of Grass Randomizer on the multiworld by disallowing local_fill percentages below 95.
|
||||||
|
limit_grass_rando: true
|
||||||
|
# Path to the user's TUNIC Poptracker Pack.
|
||||||
|
ut_poptracker_path: ""
|
||||||
|
vampire_survivors_options:
|
||||||
|
# Allow the use of unfair characters
|
||||||
|
allow_unfair_characters: false
|
||||||
|
voltorb_flip_settings:
|
||||||
|
# Allows the **experimental** choice in the **Artificial Logic** option.
|
||||||
|
allow_experimental_logic: false
|
||||||
|
wargroove_options:
|
||||||
|
# Locates the Wargroove root directory on your system.
|
||||||
|
# This is used by the Wargroove client, so it knows where to send communication files to.
|
||||||
|
root_directory: "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
||||||
|
# Locates the Wargroove save file directory on your system.
|
||||||
|
# This is used by the Wargroove client, so it knows where to send mod and save files to.
|
||||||
|
save_directory: "%APPDATA%"
|
||||||
|
yoshisisland_options:
|
||||||
|
# File name of the Yoshi's Island 1.0 US rom
|
||||||
|
rom_file: "roms/Super Mario World 2 - Yoshi's Island (U).sfc"
|
||||||
|
yugioh06_settings:
|
||||||
|
# File name of your Yu-Gi-Oh 2006 ROM
|
||||||
|
rom_file: "roms/YuGiOh06.gba"
|
||||||
|
zillion_options:
|
||||||
|
# File name of the Zillion US rom
|
||||||
|
rom_file: "roms/Zillion (UE) [!].sms"
|
||||||
|
# Set this to false to never autostart a rom (such as after patching)
|
||||||
|
# True for operating system default program
|
||||||
|
# Alternatively, a path to a program to open the .sfc file with
|
||||||
|
# RetroArch doesn't make it easy to launch a game from the command line.
|
||||||
|
# You have to know the path to the emulator core library on the user's computer.
|
||||||
|
rom_start: "retroarch"
|
||||||
429
worlds/PokemonStadium/Client.py
Normal file
429
worlds/PokemonStadium/Client.py
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from .Items import pokemon_stadium_items, gym_badge_codes, box_upgrade_items, cup_tier_upgrade_items
|
||||||
|
from .Locations import pokemon_stadium_locations, event_locations
|
||||||
|
from NetUtils import ClientStatus
|
||||||
|
from .Types import LocData
|
||||||
|
import Utils
|
||||||
|
import worlds._bizhawk as bizhawk
|
||||||
|
from worlds._bizhawk.client import BizHawkClient
|
||||||
|
|
||||||
|
logger = logging.getLogger('Client')
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from worlds._bizhawk.context import BizHawkClientContext
|
||||||
|
|
||||||
|
class PokemonStadiumClient(BizHawkClient):
|
||||||
|
game = 'Pokemon Stadium'
|
||||||
|
system = 'N64'
|
||||||
|
patch_suffix = '.apstadium'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.local_checked_locations = set()
|
||||||
|
self.glc_loaded = False
|
||||||
|
self.cups_loaded = False
|
||||||
|
self.minigame_index = None
|
||||||
|
self.minigame_done = False
|
||||||
|
self.minigame_check_sent = False
|
||||||
|
|
||||||
|
async def validate_rom(self, ctx: 'BizHawkClientContext') -> bool:
|
||||||
|
try:
|
||||||
|
# Check ROM name
|
||||||
|
rom_name = ((await bizhawk.read(ctx.bizhawk_ctx, [(0x20, 15, 'ROM')]))[0]).decode('ascii')
|
||||||
|
if rom_name != 'POKEMON STADIUM':
|
||||||
|
logger.info('Invalid ROM for Pokemon Stadium AP World')
|
||||||
|
return False
|
||||||
|
except bizhawk.RequestFailedError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ctx.game = self.game
|
||||||
|
ctx.items_handling = 0b111
|
||||||
|
ctx.want_slot_data = True
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def game_watcher(self, ctx: 'BizHawkClientContext') -> None:
|
||||||
|
item_codes = {net_item.item for net_item in ctx.items_received}
|
||||||
|
|
||||||
|
flags = await bizhawk.read(ctx.bizhawk_ctx, [
|
||||||
|
(0x420000, 4, 'RDRAM'), # GLC Flag
|
||||||
|
(0x420010, 4, 'RDRAM'), # Entered Battle Flag
|
||||||
|
(0x148AC8, 12, 'RDRAM'), # Beat Rival Flag
|
||||||
|
(0x12FC1C, 4, 'RDRAM'), # Minigame being played
|
||||||
|
(0x124860, 4, 'RDRAM'), # Minigame results
|
||||||
|
(0xAE77F, 1, 'RDRAM'), # Enemy team HP slot 1
|
||||||
|
(0xAE7D3, 1, 'RDRAM'), # Enemy team HP slot 2
|
||||||
|
(0xAE827, 1, 'RDRAM'), # Enemy team HP slot 3
|
||||||
|
(0x220C19, 3, 'RDRAM'), # GLC Rentals address
|
||||||
|
(0x221D99, 3, 'RDRAM'), # GLC Registration table address
|
||||||
|
(0x218CE9, 3, 'RDRAM'), # Poke Cup Rentals address
|
||||||
|
(0x219E69, 3, 'RDRAM'), # Poke Cup Registration table address
|
||||||
|
(0x218CB9, 3, 'RDRAM'), # Prime Cup Rentals address
|
||||||
|
(0x219E39, 3, 'RDRAM'), # Prime Cup Registration table address
|
||||||
|
(0x218C99, 3, 'RDRAM'), # Petit Cup Rentals address
|
||||||
|
(0x219E19, 3, 'RDRAM'), # Petit Cup Registration table address
|
||||||
|
(0x218CA9, 3, 'RDRAM'), # Pika Cup Rentals address
|
||||||
|
(0x219E29, 3, 'RDRAM'), # Pika Cup Registration table address
|
||||||
|
(0x420020, 4, 'RDRAM'), # Picking a Cup tier
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
player_has_battled = flags[1] != b'\x00\x00\x00\x00'
|
||||||
|
battle_info = await bizhawk.read(ctx.bizhawk_ctx, [(0x0AE540, 4, 'RDRAM')])
|
||||||
|
mode = int(battle_info[0].hex()[:2])
|
||||||
|
gym_info = battle_info[0].hex()[4:]
|
||||||
|
gym_number = int(battle_info[0].hex()[4:6])
|
||||||
|
trainer_index = int(battle_info[0].hex()[6:])
|
||||||
|
|
||||||
|
if player_has_battled:
|
||||||
|
player_won = all(x == b'\x00' for x in flags[5:8])
|
||||||
|
|
||||||
|
if player_won:
|
||||||
|
ap_code = 20000000 + (mode * 100) + (gym_number * 10) + trainer_index
|
||||||
|
|
||||||
|
# If a Gym Leader was beaten or the last trainer for a Cup was beaten an additional check must be sent
|
||||||
|
if mode == 7 and trainer_index == 4:
|
||||||
|
locations_to_check = set([ap_code, ap_code + 1])
|
||||||
|
elif trainer_index == 8:
|
||||||
|
locations_to_check = set([ap_code, ap_code - trainer_index, ap_code + 1])
|
||||||
|
else:
|
||||||
|
locations_to_check = set([ap_code])
|
||||||
|
|
||||||
|
try:
|
||||||
|
await ctx.check_locations(locations_to_check)
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(0x420010, [0x00, 0x00, 0x00, 0x00], 'RDRAM')])
|
||||||
|
self.glc_loaded = False
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
glc_flag = int.from_bytes(flags[0], byteorder='big')
|
||||||
|
if glc_flag == 2 and not self.glc_loaded:
|
||||||
|
self.glc_loaded = True
|
||||||
|
|
||||||
|
self.GLC_UNLOCK_FLAGS = [
|
||||||
|
0x147B70, # Pewter
|
||||||
|
0x147B98, # Cerulean
|
||||||
|
0x147BC0, # Vermilion
|
||||||
|
0x147BE8, # Celadon
|
||||||
|
0x147C10, # Fuchsia
|
||||||
|
0x147C38, # Saffron
|
||||||
|
0x147C60, # Cinnabar
|
||||||
|
0x147C88, # Viridian
|
||||||
|
0x147CB1, # E4 entrance
|
||||||
|
0x147CD9, # E4 exit
|
||||||
|
0x147D01, # E4
|
||||||
|
]
|
||||||
|
|
||||||
|
# UUDDLLRR
|
||||||
|
self.GLC_CURSOR_TARGETS = [
|
||||||
|
0x147B84, # Brock, 00000002
|
||||||
|
0x147BAC, # Misty, 03000103
|
||||||
|
0x147BD4, # Surge, 04020200
|
||||||
|
0x147BFC, # Erika, 05030500
|
||||||
|
0x147C24, # Koga, 06040604
|
||||||
|
0x147C4C, # Sabrina, 07050007
|
||||||
|
0x147C74, # Blaine, 00080608
|
||||||
|
0x147C9C, # Giovanni, 07000709
|
||||||
|
]
|
||||||
|
|
||||||
|
gym_codes = [
|
||||||
|
pokemon_stadium_items['Pewter City Key'].ap_code,
|
||||||
|
pokemon_stadium_items['Cerulean City Key'].ap_code,
|
||||||
|
pokemon_stadium_items['Vermillion City Key'].ap_code,
|
||||||
|
pokemon_stadium_items['Celadon City Key'].ap_code,
|
||||||
|
pokemon_stadium_items['Fuchsia City Key'].ap_code,
|
||||||
|
pokemon_stadium_items['Saffron City Key'].ap_code,
|
||||||
|
pokemon_stadium_items['Cinnabar Island Key'].ap_code,
|
||||||
|
pokemon_stadium_items['Viridian City Key'].ap_code,
|
||||||
|
]
|
||||||
|
|
||||||
|
self.unlocked_gyms = [i + 1 for i, code in enumerate(gym_codes) if code in item_codes]
|
||||||
|
victory_road_open = set(gym_badge_codes).issubset(item_codes)
|
||||||
|
if victory_road_open:
|
||||||
|
self.unlocked_gyms.append(9)
|
||||||
|
|
||||||
|
if gym_codes[0] in item_codes:
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[0], [0x00, 0x01], 'RDRAM')])
|
||||||
|
await self.update_brock_cursor(ctx)
|
||||||
|
|
||||||
|
if gym_codes[1] in item_codes:
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[1], [0x00, 0x01], 'RDRAM')])
|
||||||
|
await self.update_misty_cursor(ctx)
|
||||||
|
|
||||||
|
if gym_codes[2] in item_codes:
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[2], [0x00, 0x01], 'RDRAM')])
|
||||||
|
await self.update_surge_cursor(ctx)
|
||||||
|
|
||||||
|
if gym_codes[3] in item_codes:
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[3], [0x00, 0x01], 'RDRAM')])
|
||||||
|
await self.update_erika_cursor(ctx)
|
||||||
|
|
||||||
|
if gym_codes[4] in item_codes:
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[4], [0x00, 0x01], 'RDRAM')])
|
||||||
|
await self.update_koga_cursor(ctx)
|
||||||
|
|
||||||
|
if gym_codes[5] in item_codes:
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[5], [0x00, 0x01], 'RDRAM')])
|
||||||
|
await self.update_sabrina_cursor(ctx)
|
||||||
|
|
||||||
|
if gym_codes[6] in item_codes:
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[6], [0x00, 0x01], 'RDRAM')])
|
||||||
|
await self.update_blaine_cursor(ctx)
|
||||||
|
|
||||||
|
if gym_codes[7] in item_codes:
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[7], [0x00, 0x01], 'RDRAM')])
|
||||||
|
await self.update_giovanni_cursor(ctx, item_codes)
|
||||||
|
|
||||||
|
if victory_road_open:
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[8], [0x01], 'RDRAM')])
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[9], [0x01], 'RDRAM')])
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[10], [0x01], 'RDRAM')])
|
||||||
|
|
||||||
|
if len(self.unlocked_gyms) > 0 and gym_info != '0804':
|
||||||
|
first_gym = self.unlocked_gyms[0] - 1
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(0x147D50, [0x00, first_gym], 'RDRAM')])
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(0x146F38, [0x52, 0x61, 0xFF, 0x82], 'RDRAM')])
|
||||||
|
elif glc_flag != 2 and self.glc_loaded:
|
||||||
|
self.glc_loaded = False
|
||||||
|
|
||||||
|
text = flags[2].decode("ascii", errors="ignore")
|
||||||
|
if text == 'Magnificent!':
|
||||||
|
await ctx.check_locations(set([event_locations['Beat Rival'].ap_code]))
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(0x420010, [0x00, 0x00, 0x00, 0x00], 'RDRAM')])
|
||||||
|
|
||||||
|
cups_flag = int.from_bytes(flags[18], byteorder='big')
|
||||||
|
if cups_flag != 0 and not self.cups_loaded:
|
||||||
|
self.cups_loaded = True
|
||||||
|
|
||||||
|
if mode == 3:
|
||||||
|
cup_tier_item = cup_tier_upgrade_items['Poké Cup - Tier Upgrade'].ap_code
|
||||||
|
else:
|
||||||
|
cup_tier_item = cup_tier_upgrade_items['Prime Cup - Tier Upgrade'].ap_code
|
||||||
|
|
||||||
|
cup_tier = sum(1 for net_item in ctx.items_received if net_item.item == cup_tier_item)
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(0x147018, [0x00, 0x00, 0x00, cup_tier], 'RDRAM')])
|
||||||
|
elif cups_flag == 0:
|
||||||
|
self.cups_loaded = False
|
||||||
|
|
||||||
|
# GLC Boxes
|
||||||
|
selecting_team = flags[8] == b'\x22\x0E\x20'
|
||||||
|
registering_team = flags[9] == b'\x22\x1F\xA0'
|
||||||
|
if selecting_team or registering_team:
|
||||||
|
address = 0x220E23 if selecting_team else 0x221FA3
|
||||||
|
item = box_upgrade_items['GLC PC Box Upgrade'].ap_code
|
||||||
|
box_count = sum(1 for net_item in ctx.items_received if net_item.item == item)
|
||||||
|
table_size = 29 + 20 * box_count
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(address, [table_size], 'RDRAM')])
|
||||||
|
|
||||||
|
# Poke Boxes
|
||||||
|
selecting_team = flags[10] == b'\x21\x8F\x10'
|
||||||
|
registering_team = flags[11] == b'\x21\xA0\x90'
|
||||||
|
if selecting_team or registering_team:
|
||||||
|
address = 0x218F13 if selecting_team else 0x21A093
|
||||||
|
item = box_upgrade_items['Poke Cup PC Box Upgrade'].ap_code
|
||||||
|
box_count = sum(1 for net_item in ctx.items_received if net_item.item == item)
|
||||||
|
table_size = 29 + 20 * box_count
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(address, [table_size], 'RDRAM')])
|
||||||
|
|
||||||
|
# Prime Boxes
|
||||||
|
selecting_team = flags[12] == b'\x21\x8F\x10'
|
||||||
|
registering_team = flags[13] == b'\x21\xA0\x90'
|
||||||
|
if selecting_team or registering_team:
|
||||||
|
address = 0x218F13 if selecting_team else 0x21A093
|
||||||
|
item = box_upgrade_items['Prime Cup PC Box Upgrade'].ap_code
|
||||||
|
box_count = sum(1 for net_item in ctx.items_received if net_item.item == item)
|
||||||
|
table_size = 29 + 20 * box_count
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(address, [table_size], 'RDRAM')])
|
||||||
|
|
||||||
|
# Minigames
|
||||||
|
if flags[3].startswith(b'\x00\x03\x00') and flags[3][3] in range(9):
|
||||||
|
self.minigame_index = flags[3][3]
|
||||||
|
|
||||||
|
if self.minigame_index != None and flags[4] == b'\x00\x00\x00\x00':
|
||||||
|
self.minigame_done = False
|
||||||
|
|
||||||
|
if self.minigame_index != None and not self.minigame_done and flags[4] == b'\x01\x00\x00\x00':
|
||||||
|
self.minigame_done = True
|
||||||
|
self.minigame_check_sent = False
|
||||||
|
|
||||||
|
if self.minigame_done and self.minigame_index != None and not self.minigame_check_sent:
|
||||||
|
minigame_ap_acode = 20000100 + self.minigame_index
|
||||||
|
await ctx.check_locations([minigame_ap_acode])
|
||||||
|
|
||||||
|
self.minigame_check_sent = True
|
||||||
|
|
||||||
|
# Send game clear
|
||||||
|
if not ctx.finished_game and pokemon_stadium_items['Victory'].ap_code in item_codes:
|
||||||
|
ctx.finished_game = True
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "StatusUpdate",
|
||||||
|
"status": ClientStatus.CLIENT_GOAL,
|
||||||
|
}])
|
||||||
|
|
||||||
|
def lowest_unlocked_from(self, lower_bound):
|
||||||
|
for i in range(lower_bound, 9):
|
||||||
|
if i in self.unlocked_gyms:
|
||||||
|
return i
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def highest_unlocked_from(self, upper_bound):
|
||||||
|
for i in range(upper_bound, 0, -1):
|
||||||
|
if i in self.unlocked_gyms:
|
||||||
|
return i
|
||||||
|
return 0
|
||||||
|
|
||||||
|
async def update_brock_cursor(self, ctx):
|
||||||
|
# Determine UP: lowest unlocked gym from 4 to 9
|
||||||
|
up = self.lowest_unlocked_from(4)
|
||||||
|
|
||||||
|
# Determine RIGHT: lowest of 2 or 3 or 4 if any are unlocked
|
||||||
|
right = 0
|
||||||
|
misty_unlocked = 2 in self.unlocked_gyms
|
||||||
|
surge_unlocked = 3 in self.unlocked_gyms
|
||||||
|
erika_unlocked = 4 in self.unlocked_gyms
|
||||||
|
|
||||||
|
if misty_unlocked:
|
||||||
|
right = 2
|
||||||
|
elif surge_unlocked:
|
||||||
|
right = 3
|
||||||
|
elif erika_unlocked:
|
||||||
|
right = 4
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[0], [up, 0x00, 0x00, right], 'RDRAM')])
|
||||||
|
|
||||||
|
async def update_misty_cursor(self, ctx):
|
||||||
|
# Determine UP: lowest unlocked gym from 4 to 9
|
||||||
|
up = self.lowest_unlocked_from(4)
|
||||||
|
|
||||||
|
# Determine LEFT: is Brock unlocked
|
||||||
|
left = 1 if 1 in self.unlocked_gyms else 0
|
||||||
|
|
||||||
|
# Determine RIGHT: is Surge unlocked
|
||||||
|
right = 3 if 3 in self.unlocked_gyms else 0
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[1], [up, 0x00, left, right], 'RDRAM')])
|
||||||
|
|
||||||
|
async def update_surge_cursor(self, ctx):
|
||||||
|
# Determine UP: lowest unlocked gym from 4 to 9
|
||||||
|
up = self.lowest_unlocked_from(4)
|
||||||
|
|
||||||
|
# Determine DOWN: is Misty unlocked
|
||||||
|
down = 2 if 2 in self.unlocked_gyms else 0
|
||||||
|
|
||||||
|
# Determine LEFT: is Misty or Brock unlocked
|
||||||
|
left = 0
|
||||||
|
misty_unlocked = 2 if 2 in self.unlocked_gyms else 0
|
||||||
|
brock_unlocked = 1 if 1 in self.unlocked_gyms else 0
|
||||||
|
|
||||||
|
if misty_unlocked:
|
||||||
|
left = 2
|
||||||
|
elif brock_unlocked:
|
||||||
|
left = 1
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[2], [up, down, left, 0x00], 'RDRAM')])
|
||||||
|
|
||||||
|
async def update_erika_cursor(self, ctx):
|
||||||
|
# Determine UP: lowest unlocked gym from 5 to 9
|
||||||
|
up = self.lowest_unlocked_from(5)
|
||||||
|
|
||||||
|
# Determine DOWN: highest unlocked gym from 3 to 1
|
||||||
|
down = self.highest_unlocked_from(3)
|
||||||
|
|
||||||
|
# Determine LEFT: is Koga or Sabrina unlocked
|
||||||
|
left = 0
|
||||||
|
koga_unlocked = 5 if 5 in self.unlocked_gyms else 0
|
||||||
|
sabrina_unlocked = 6 if 6 in self.unlocked_gyms else 0
|
||||||
|
|
||||||
|
if koga_unlocked:
|
||||||
|
left = 5
|
||||||
|
elif sabrina_unlocked:
|
||||||
|
left = 6
|
||||||
|
|
||||||
|
# Determine RIGHT: is Surge unlocked
|
||||||
|
right = 3 if 3 in self.unlocked_gyms else 0
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[3], [up, down, left, right], 'RDRAM')])
|
||||||
|
|
||||||
|
async def update_koga_cursor(self, ctx):
|
||||||
|
# Determine UP: lowest unlocked gym from 6 to 9
|
||||||
|
up = self.lowest_unlocked_from(6)
|
||||||
|
|
||||||
|
# Determine DOWN: highest unlocked gym from 2 to 1
|
||||||
|
down = self.highest_unlocked_from(2)
|
||||||
|
|
||||||
|
# Determine LEFT: is Sabrina unlocked
|
||||||
|
left = 6 if 6 in self.unlocked_gyms else 0
|
||||||
|
|
||||||
|
# Determine RIGHT: is Erika or Surge unlocked
|
||||||
|
right = 0
|
||||||
|
erika_unlocked = 4 if 4 in self.unlocked_gyms else 0
|
||||||
|
surge_unlocked = 3 if 3 in self.unlocked_gyms else 0
|
||||||
|
|
||||||
|
if erika_unlocked:
|
||||||
|
right = 4
|
||||||
|
elif surge_unlocked:
|
||||||
|
right = 3
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[4], [up, down, left, right], 'RDRAM')])
|
||||||
|
|
||||||
|
async def update_sabrina_cursor(self, ctx):
|
||||||
|
# Determine DOWN: highest unlocked gym from 5 to 1
|
||||||
|
down = self.highest_unlocked_from(5)
|
||||||
|
|
||||||
|
# Determine RIGHT: is Blaine or Giovanni unlocked
|
||||||
|
right = 0
|
||||||
|
blaine_unlocked = 7 if 7 in self.unlocked_gyms else 0
|
||||||
|
giovanni_unlocked = 8 if 8 in self.unlocked_gyms else 0
|
||||||
|
|
||||||
|
if blaine_unlocked:
|
||||||
|
right = 7
|
||||||
|
elif giovanni_unlocked:
|
||||||
|
right = 8
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[5], [0x00, down, 0x00, right], 'RDRAM')])
|
||||||
|
|
||||||
|
async def update_blaine_cursor(self, ctx):
|
||||||
|
# Determine DOWN: highest unlocked gym from 5 to 1
|
||||||
|
down = self.highest_unlocked_from(5)
|
||||||
|
|
||||||
|
# Determine LEFT: is Sabrina unlocked
|
||||||
|
left = 6 if 6 in self.unlocked_gyms else 0
|
||||||
|
|
||||||
|
# Determine RIGHT: is Giovanni unlocked or do you have all badges needed
|
||||||
|
if 8 in self.unlocked_gyms:
|
||||||
|
right = 8
|
||||||
|
elif 9 in self.unlocked_gyms:
|
||||||
|
right = 9
|
||||||
|
else:
|
||||||
|
right = 0
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[6], [0x00, down, left, right], 'RDRAM')])
|
||||||
|
|
||||||
|
async def update_giovanni_cursor(self, ctx, item_codes):
|
||||||
|
# Determine UP: All badges obtained?
|
||||||
|
up = 9 if set(gym_badge_codes).issubset(item_codes) else 0
|
||||||
|
|
||||||
|
# Determine DOWN: highest unlocked gym from 5 to 1
|
||||||
|
down = self.highest_unlocked_from(5)
|
||||||
|
|
||||||
|
# Determine LEFT: is Blaine or Sabrina unlocked
|
||||||
|
left = 0
|
||||||
|
blaine_unlocked = 7 if 7 in self.unlocked_gyms else 0
|
||||||
|
sabrina_unlocked = 6 if 6 in self.unlocked_gyms else 0
|
||||||
|
|
||||||
|
if blaine_unlocked:
|
||||||
|
left = 7
|
||||||
|
elif sabrina_unlocked:
|
||||||
|
left = 6
|
||||||
|
|
||||||
|
# Determine RIGHT: All badges obtained?
|
||||||
|
right = up
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[7], [up, down, left, right], 'RDRAM')])
|
||||||
132
worlds/PokemonStadium/Items.py
Normal file
132
worlds/PokemonStadium/Items.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import logging
|
||||||
|
import random
|
||||||
|
|
||||||
|
from BaseClasses import Item, ItemClassification
|
||||||
|
|
||||||
|
from .Types import ItemData, PokemonStadiumItem
|
||||||
|
from .Locations import get_total_locations
|
||||||
|
from typing import List, Dict, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import PokemonStadiumWorld
|
||||||
|
|
||||||
|
def create_itempool(world: 'PokemonStadiumWorld') -> List[Item]:
|
||||||
|
item_pool: List[Item] = []
|
||||||
|
|
||||||
|
# This is a good place to grab anything you need from options
|
||||||
|
|
||||||
|
for name in pokemon_stadium_items:
|
||||||
|
if name != 'Victory' and name not in world.starting_gym_keys:
|
||||||
|
item_pool.append(create_item(world, name))
|
||||||
|
|
||||||
|
victory = create_item(world, 'Victory')
|
||||||
|
world.multiworld.get_location('Beat Rival', world.player).place_locked_item(victory)
|
||||||
|
|
||||||
|
item_pool += create_multiple_items(world, 'Poké Cup - Tier Upgrade', 3, ItemClassification.progression)
|
||||||
|
item_pool += create_multiple_items(world, 'Prime Cup - Tier Upgrade', 3, ItemClassification.progression)
|
||||||
|
|
||||||
|
item_pool += create_multiple_items(world, 'GLC PC Box Upgrade', 6, ItemClassification.useful)
|
||||||
|
item_pool += create_multiple_items(world, 'Poke Cup PC Box Upgrade', 6, ItemClassification.useful)
|
||||||
|
item_pool += create_multiple_items(world, 'Prime Cup PC Box Upgrade', 6, ItemClassification.useful)
|
||||||
|
|
||||||
|
item_pool += create_junk_items(world, get_total_locations(world) - len(item_pool) - 1)
|
||||||
|
|
||||||
|
return item_pool
|
||||||
|
|
||||||
|
def create_item(world: 'PokemonStadiumWorld', name: str) -> Item:
|
||||||
|
data = item_table[name]
|
||||||
|
return PokemonStadiumItem(name, data.classification, data.ap_code, world.player)
|
||||||
|
|
||||||
|
def create_multiple_items(world: "PokemonStadiumWorld", name: str, count: int, item_type: ItemClassification = ItemClassification.progression) -> List[Item]:
|
||||||
|
data = item_table[name]
|
||||||
|
itemlist: List[Item] = []
|
||||||
|
|
||||||
|
for _ in range(count):
|
||||||
|
itemlist += [PokemonStadiumItem(name, item_type, data.ap_code, world.player)]
|
||||||
|
|
||||||
|
return itemlist
|
||||||
|
|
||||||
|
def create_junk_items(world: 'PokemonStadiumWorld', count: int) -> List[Item]:
|
||||||
|
junk_pool: List[Item] = []
|
||||||
|
junk_list: Dict[str, int] = {}
|
||||||
|
|
||||||
|
for name in item_table.keys():
|
||||||
|
ic = item_table[name].classification
|
||||||
|
if ic == ItemClassification.filler:
|
||||||
|
junk_list[name] = junk_weights.get(name)
|
||||||
|
|
||||||
|
for _ in range(count):
|
||||||
|
junk_pool.append(world.create_item(world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0]))
|
||||||
|
|
||||||
|
return junk_pool
|
||||||
|
|
||||||
|
pokemon_stadium_items = {
|
||||||
|
# Progression items
|
||||||
|
'Pewter City Key': ItemData(10000001, ItemClassification.progression),
|
||||||
|
'Boulder Badge': ItemData(10000002, ItemClassification.progression),
|
||||||
|
'Cerulean City Key': ItemData(10000003, ItemClassification.progression),
|
||||||
|
'Cascade Badge': ItemData(10000004, ItemClassification.progression),
|
||||||
|
'Vermillion City Key': ItemData(10000005, ItemClassification.progression),
|
||||||
|
'Thunder Badge': ItemData(10000006, ItemClassification.progression),
|
||||||
|
'Celadon City Key': ItemData(10000007, ItemClassification.progression),
|
||||||
|
'Rainbow Badge': ItemData(10000008, ItemClassification.progression),
|
||||||
|
'Fuchsia City Key': ItemData(10000009, ItemClassification.progression),
|
||||||
|
'Soul Badge': ItemData(10000010, ItemClassification.progression),
|
||||||
|
'Saffron City Key': ItemData(10000011, ItemClassification.progression),
|
||||||
|
'Marsh Badge': ItemData(10000012, ItemClassification.progression),
|
||||||
|
'Cinnabar Island Key': ItemData(10000013, ItemClassification.progression),
|
||||||
|
'Volcano Badge': ItemData(10000014, ItemClassification.progression),
|
||||||
|
'Viridian City Key': ItemData(10000015, ItemClassification.progression),
|
||||||
|
'Earth Badge': ItemData(10000016, ItemClassification.progression),
|
||||||
|
|
||||||
|
# Victory is added here since in this organization it needs to be in the default item pool
|
||||||
|
'Victory': ItemData(10000000, ItemClassification.progression)
|
||||||
|
}
|
||||||
|
|
||||||
|
gym_keys = [
|
||||||
|
'Pewter City Key',
|
||||||
|
'Cerulean City Key',
|
||||||
|
'Vermillion City Key',
|
||||||
|
'Celadon City Key',
|
||||||
|
'Fuchsia City Key',
|
||||||
|
'Saffron City Key',
|
||||||
|
'Cinnabar Island Key',
|
||||||
|
'Viridian City Key',
|
||||||
|
]
|
||||||
|
|
||||||
|
gym_badge_codes = [
|
||||||
|
10000002,
|
||||||
|
10000004,
|
||||||
|
10000006,
|
||||||
|
10000008,
|
||||||
|
10000010,
|
||||||
|
10000012,
|
||||||
|
10000014,
|
||||||
|
10000016,
|
||||||
|
]
|
||||||
|
|
||||||
|
cup_tier_upgrade_items = {
|
||||||
|
'Poké Cup - Tier Upgrade': ItemData(10000017, ItemClassification.progression),
|
||||||
|
'Prime Cup - Tier Upgrade': ItemData(10000018, ItemClassification.progression),
|
||||||
|
}
|
||||||
|
|
||||||
|
box_upgrade_items = {
|
||||||
|
'GLC PC Box Upgrade': ItemData(10000101, ItemClassification.useful),
|
||||||
|
'Poke Cup PC Box Upgrade' : ItemData(10000102, ItemClassification.useful),
|
||||||
|
'Prime Cup PC Box Upgrade' : ItemData(10000103, ItemClassification.useful),
|
||||||
|
}
|
||||||
|
|
||||||
|
junk_items = {
|
||||||
|
"Pokedoll": ItemData(10000200, ItemClassification.filler, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
junk_weights = {
|
||||||
|
"Pokedoll": 40,
|
||||||
|
}
|
||||||
|
|
||||||
|
item_table = {
|
||||||
|
**pokemon_stadium_items,
|
||||||
|
**cup_tier_upgrade_items,
|
||||||
|
**box_upgrade_items,
|
||||||
|
**junk_items,
|
||||||
|
}
|
||||||
193
worlds/PokemonStadium/Locations.py
Normal file
193
worlds/PokemonStadium/Locations.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
from typing import Dict, TYPE_CHECKING
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .Types import LocData
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import PokemonStadiumWorld
|
||||||
|
|
||||||
|
def get_total_locations(world: 'PokemonStadiumWorld') -> int:
|
||||||
|
if world.options.Trainersanity.value == 1:
|
||||||
|
location_table.update(trainersanity_locations)
|
||||||
|
|
||||||
|
return len(location_table)
|
||||||
|
|
||||||
|
def get_location_names() -> Dict[str, int]:
|
||||||
|
temp_loc_table = location_table.copy()
|
||||||
|
temp_loc_table.update(trainersanity_locations)
|
||||||
|
|
||||||
|
names = {name: data.ap_code for name, data in temp_loc_table.items()}
|
||||||
|
|
||||||
|
return names
|
||||||
|
|
||||||
|
def is_valid_location(world: 'PokemonStadiumWorld', name) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
pokemon_stadium_locations = {
|
||||||
|
'Magikarp\'s Splash': LocData(20000100, 'Kids Club'),
|
||||||
|
'Clefairy Says': LocData(20000101, 'Kids Club'),
|
||||||
|
'Run, Rattata, Run': LocData(20000102, 'Kids Club'),
|
||||||
|
'Snore War': LocData(20000103, 'Kids Club'),
|
||||||
|
'Thundering Dynamo': LocData(20000104, 'Kids Club'),
|
||||||
|
'Sushi-Go-Round': LocData(20000105, 'Kids Club'),
|
||||||
|
'Ekans\'s Hoop Hurl': LocData(20000106, 'Kids Club'),
|
||||||
|
'Rock Harden': LocData(20000107, 'Kids Club'),
|
||||||
|
'Dig! Dig! Dig!': LocData(20000108, 'Kids Club'),
|
||||||
|
|
||||||
|
'Poké Cup - Poké Ball - Prize': LocData(20000300, 'Poké Cup'),
|
||||||
|
'Poké Cup - Poké Ball - Tier Upgrade': LocData(20000309, 'Poké Cup'),
|
||||||
|
'Poké Cup - Great Ball - Prize': LocData(20000310, 'Poké Cup'),
|
||||||
|
'Poké Cup - Great Ball - Tier Upgrade': LocData(20000319, 'Poké Cup'),
|
||||||
|
'Poké Cup - Ultra Ball - Prize': LocData(20000320, 'Poké Cup'),
|
||||||
|
'Poké Cup - Ultra Ball - Tier Upgrade': LocData(20000329, 'Poké Cup'),
|
||||||
|
'Poké Cup - Master Ball - Prize': LocData(20000330, 'Poké Cup'),
|
||||||
|
|
||||||
|
'Petit Cup Prize': LocData(20000400, 'Petit Cup'),
|
||||||
|
|
||||||
|
'Pika Cup Prize': LocData(20000500, 'Pika Cup'),
|
||||||
|
|
||||||
|
'Prime Cup - Poké Ball - Prize': LocData(20000600, 'Prime Cup'),
|
||||||
|
'Prime Cup - Poké Ball - Tier Upgrade': LocData(20000609, 'Prime Cup'),
|
||||||
|
'Prime Cup - Great Ball - Prize': LocData(20000610, 'Prime Cup'),
|
||||||
|
'Prime Cup - Great Ball - Tier Upgrade': LocData(20000619, 'Prime Cup'),
|
||||||
|
'Prime Cup - Ultra Ball - Prize': LocData(20000620, 'Prime Cup'),
|
||||||
|
'Prime Cup - Ultra Ball - Tier Upgrade': LocData(20000629, 'Prime Cup'),
|
||||||
|
'Prime Cup - Master Ball - Prize': LocData(20000630, 'Prime Cup'),
|
||||||
|
|
||||||
|
'BROCK': LocData(20000704, 'Gym Leader Castle'),
|
||||||
|
'Pewter Gym': LocData(20000705, 'Gym Leader Castle'),
|
||||||
|
'MISTY': LocData(20000714, 'Gym Leader Castle'),
|
||||||
|
'Cerulean Gym': LocData(20000715, 'Gym Leader Castle'),
|
||||||
|
'SURGE': LocData(20000724, 'Gym Leader Castle'),
|
||||||
|
'Vermillion Gym': LocData(20000725, 'Gym Leader Castle'),
|
||||||
|
'ERIKA': LocData(20000734, 'Gym Leader Castle'),
|
||||||
|
'Celadon Gym': LocData(20000735, 'Gym Leader Castle'),
|
||||||
|
'KOGA': LocData(20000744, 'Gym Leader Castle'),
|
||||||
|
'Fuchsia Gym': LocData(20000745, 'Gym Leader Castle'),
|
||||||
|
'SABRINA': LocData(20000754, 'Gym Leader Castle'),
|
||||||
|
'Saffron Gym': LocData(20000755, 'Gym Leader Castle'),
|
||||||
|
'BLAINE': LocData(20000764, 'Gym Leader Castle'),
|
||||||
|
'Cinnabar Gym': LocData(20000765, 'Gym Leader Castle'),
|
||||||
|
'GIOVANNI': LocData(20000774, 'Gym Leader Castle'),
|
||||||
|
'Viridian Gym': LocData(20000775, 'Gym Leader Castle'),
|
||||||
|
}
|
||||||
|
|
||||||
|
event_locations = {
|
||||||
|
'Beat Rival': LocData(20000000, 'Hall of Fame')
|
||||||
|
}
|
||||||
|
|
||||||
|
trainersanity_locations = {
|
||||||
|
'Poké Cup - Poké Ball - Bug Boy': LocData(20000301, 'Poké Cup'),
|
||||||
|
'Poké Cup - Poké Ball - Lad': LocData(20000302, 'Poké Cup'),
|
||||||
|
'Poké Cup - Poké Ball - Nerd': LocData(20000303, 'Poké Cup'),
|
||||||
|
'Poké Cup - Poké Ball - Sailor': LocData(20000304, 'Poké Cup'),
|
||||||
|
'Poké Cup - Poké Ball - Jr(F)': LocData(20000305, 'Poké Cup'),
|
||||||
|
'Poké Cup - Poké Ball - Jr(M)': LocData(20000306, 'Poké Cup'),
|
||||||
|
'Poké Cup - Poké Ball - Lass': LocData(20000307, 'Poké Cup'),
|
||||||
|
'Poké Cup - Poké Ball - Pokémaniac': LocData(20000308, 'Poké Cup'),
|
||||||
|
'Poké Cup - Great Ball - Bug Boy': LocData(20000311, 'Poké Cup'),
|
||||||
|
'Poké Cup - Great Ball - Lad': LocData(20000312, 'Poké Cup'),
|
||||||
|
'Poké Cup - Great Ball - Nerd': LocData(20000313, 'Poké Cup'),
|
||||||
|
'Poké Cup - Great Ball - Sailor': LocData(20000314, 'Poké Cup'),
|
||||||
|
'Poké Cup - Great Ball - Jr(F)': LocData(20000315, 'Poké Cup'),
|
||||||
|
'Poké Cup - Great Ball - Jr(M)': LocData(20000316, 'Poké Cup'),
|
||||||
|
'Poké Cup - Great Ball - Lass': LocData(20000317, 'Poké Cup'),
|
||||||
|
'Poké Cup - Great Ball - Pokémaniac': LocData(20000318, 'Poké Cup'),
|
||||||
|
'Poké Cup - Ultra Ball - Bug Boy': LocData(20000321, 'Poké Cup'),
|
||||||
|
'Poké Cup - Ultra Ball - Lad': LocData(20000322, 'Poké Cup'),
|
||||||
|
'Poké Cup - Ultra Ball - Nerd': LocData(20000323, 'Poké Cup'),
|
||||||
|
'Poké Cup - Ultra Ball - Sailor': LocData(20000324, 'Poké Cup'),
|
||||||
|
'Poké Cup - Ultra Ball - Jr(F)': LocData(20000325, 'Poké Cup'),
|
||||||
|
'Poké Cup - Ultra Ball - Jr(M)': LocData(20000326, 'Poké Cup'),
|
||||||
|
'Poké Cup - Ultra Ball - Lass': LocData(20000327, 'Poké Cup'),
|
||||||
|
'Poké Cup - Ultra Ball - Pokémaniac': LocData(20000328, 'Poké Cup'),
|
||||||
|
'Poké Cup - Master Ball - Bug Boy': LocData(20000331, 'Poké Cup'),
|
||||||
|
'Poké Cup - Master Ball - Lad': LocData(20000332, 'Poké Cup'),
|
||||||
|
'Poké Cup - Master Ball - Nerd': LocData(20000333, 'Poké Cup'),
|
||||||
|
'Poké Cup - Master Ball - Sailor': LocData(20000334, 'Poké Cup'),
|
||||||
|
'Poké Cup - Master Ball - Jr(F)': LocData(20000335, 'Poké Cup'),
|
||||||
|
'Poké Cup - Master Ball - Jr(M)': LocData(20000336, 'Poké Cup'),
|
||||||
|
'Poké Cup - Master Ball - Lass': LocData(20000337, 'Poké Cup'),
|
||||||
|
'Poké Cup - Master Ball - Pokémaniac': LocData(20000338, 'Poké Cup'),
|
||||||
|
|
||||||
|
'Petit Cup - Bug Boy': LocData(20000401, 'Petit Cup'),
|
||||||
|
'Petit Cup - Lad': LocData(20000402, 'Petit Cup'),
|
||||||
|
'Petit Cup - Nerd': LocData(20000403, 'Petit Cup'),
|
||||||
|
'Petit Cup - Sailor': LocData(20000404, 'Petit Cup'),
|
||||||
|
'Petit Cup - Jr(F)': LocData(20000405, 'Petit Cup'),
|
||||||
|
'Petit Cup - Jr(M)': LocData(20000406, 'Petit Cup'),
|
||||||
|
'Petit Cup - Lass': LocData(20000407, 'Petit Cup'),
|
||||||
|
'Petit Cup - Pokémaniac': LocData(20000408, 'Petit Cup'),
|
||||||
|
|
||||||
|
'Pika Cup - Bug Boy': LocData(20000501, 'Pika Cup'),
|
||||||
|
'Pika Cup - Lad': LocData(20000502, 'Pika Cup'),
|
||||||
|
'Pika Cup - Swimmer': LocData(20000503, 'Pika Cup'),
|
||||||
|
'Pika Cup - Burglar': LocData(20000504, 'Pika Cup'),
|
||||||
|
'Pika Cup - Mr. Fix': LocData(20000505, 'Pika Cup'),
|
||||||
|
'Pika Cup - Hiker': LocData(20000506, 'Pika Cup'),
|
||||||
|
'Pika Cup - Lass': LocData(20000507, 'Pika Cup'),
|
||||||
|
'Pika Cup - Fisher': LocData(20000508, 'Pika Cup'),
|
||||||
|
|
||||||
|
'Prime Cup - Poké Ball - Cue Ball': LocData(20000601, 'Prime Cup'),
|
||||||
|
'Prime Cup - Poké Ball - Rocket': LocData(20000602, 'Prime Cup'),
|
||||||
|
'Prime Cup - Poké Ball - Judoboy': LocData(20000603, 'Prime Cup'),
|
||||||
|
'Prime Cup - Poké Ball - Gambler': LocData(20000604, 'Prime Cup'),
|
||||||
|
'Prime Cup - Poké Ball - Cool(F)': LocData(20000605, 'Prime Cup'),
|
||||||
|
'Prime Cup - Poké Ball - Bird Boy': LocData(20000606, 'Prime Cup'),
|
||||||
|
'Prime Cup - Poké Ball - Lab Man': LocData(20000607, 'Prime Cup'),
|
||||||
|
'Prime Cup - Poké Ball - Cool(M)': LocData(20000608, 'Prime Cup'),
|
||||||
|
'Prime Cup - Great Ball - Cue Ball': LocData(20000611, 'Prime Cup'),
|
||||||
|
'Prime Cup - Great Ball - Rocket': LocData(20000612, 'Prime Cup'),
|
||||||
|
'Prime Cup - Great Ball - Judoboy': LocData(20000613, 'Prime Cup'),
|
||||||
|
'Prime Cup - Great Ball - Gambler': LocData(20000614, 'Prime Cup'),
|
||||||
|
'Prime Cup - Great Ball - Cool(F)': LocData(20000615, 'Prime Cup'),
|
||||||
|
'Prime Cup - Great Ball - Bird Boy': LocData(20000616, 'Prime Cup'),
|
||||||
|
'Prime Cup - Great Ball - Lab Man': LocData(20000617, 'Prime Cup'),
|
||||||
|
'Prime Cup - Great Ball - Cool(M)': LocData(20000618, 'Prime Cup'),
|
||||||
|
'Prime Cup - Ultra Ball - Cue Ball': LocData(20000621, 'Prime Cup'),
|
||||||
|
'Prime Cup - Ultra Ball - Rocket': LocData(20000622, 'Prime Cup'),
|
||||||
|
'Prime Cup - Ultra Ball - Judoboy': LocData(20000623, 'Prime Cup'),
|
||||||
|
'Prime Cup - Ultra Ball - Gambler': LocData(20000624, 'Prime Cup'),
|
||||||
|
'Prime Cup - Ultra Ball - Cool(F)': LocData(20000625, 'Prime Cup'),
|
||||||
|
'Prime Cup - Ultra Ball - Bird Boy': LocData(20000626, 'Prime Cup'),
|
||||||
|
'Prime Cup - Ultra Ball - Lab Man': LocData(20000627, 'Prime Cup'),
|
||||||
|
'Prime Cup - Ultra Ball - Cool(M)': LocData(20000628, 'Prime Cup'),
|
||||||
|
'Prime Cup - Master Ball - Cue Ball': LocData(20000631, 'Prime Cup'),
|
||||||
|
'Prime Cup - Master Ball - Rocket': LocData(20000632, 'Prime Cup'),
|
||||||
|
'Prime Cup - Master Ball - Judoboy': LocData(20000633, 'Prime Cup'),
|
||||||
|
'Prime Cup - Master Ball - Gambler': LocData(20000634, 'Prime Cup'),
|
||||||
|
'Prime Cup - Master Ball - Cool(F)': LocData(20000635, 'Prime Cup'),
|
||||||
|
'Prime Cup - Master Ball - Bird Boy': LocData(20000636, 'Prime Cup'),
|
||||||
|
'Prime Cup - Master Ball - Lab Man': LocData(20000637, 'Prime Cup'),
|
||||||
|
'Prime Cup - Master Ball - Cool(M)': LocData(20000638, 'Prime Cup'),
|
||||||
|
|
||||||
|
'Pewter Gym - Bug Boy': LocData(20000701, 'Gym Leader Castle'),
|
||||||
|
'Pewter Gym - Lad': LocData(20000702, 'Gym Leader Castle'),
|
||||||
|
'Pewter Gym - Jr(M)': LocData(20000703, 'Gym Leader Castle'),
|
||||||
|
'Cerulean Gym - Fisher': LocData(20000711, 'Gym Leader Castle'),
|
||||||
|
'Cerulean Gym - Jr(F)': LocData(20000712, 'Gym Leader Castle'),
|
||||||
|
'Cerulean Gym - Swimmer': LocData(20000713, 'Gym Leader Castle'),
|
||||||
|
'Vermillion Gym - Sailor': LocData(20000721, 'Gym Leader Castle'),
|
||||||
|
'Vermillion Gym - Rocker': LocData(20000722, 'Gym Leader Castle'),
|
||||||
|
'Vermillion Gym - Old Man': LocData(20000723, 'Gym Leader Castle'),
|
||||||
|
'Celadon Gym - Lass': LocData(20000731, 'Gym Leader Castle'),
|
||||||
|
'Celadon Gym - Beauty': LocData(20000732, 'Gym Leader Castle'),
|
||||||
|
'Celadon Gym - Cool(F)': LocData(20000733, 'Gym Leader Castle'),
|
||||||
|
'Fuchsia Gym - Biker': LocData(20000741, 'Gym Leader Castle'),
|
||||||
|
'Fuchsia Gym - Tamer': LocData(20000742, 'Gym Leader Castle'),
|
||||||
|
'Fuchsia Gym - Juggler': LocData(20000743, 'Gym Leader Castle'),
|
||||||
|
'Saffron Gym - Cue Ball': LocData(20000751, 'Gym Leader Castle'),
|
||||||
|
'Saffron Gym - Burglar': LocData(20000752, 'Gym Leader Castle'),
|
||||||
|
'Saffron Gym - Medium': LocData(20000753, 'Gym Leader Castle'),
|
||||||
|
'Cinnabar Gym - Judoboy': LocData(20000761, 'Gym Leader Castle'),
|
||||||
|
'Cinnabar Gym - Psychic': LocData(20000762, 'Gym Leader Castle'),
|
||||||
|
'Cinnabar Gym - Nerd': LocData(20000763, 'Gym Leader Castle'),
|
||||||
|
'Viridian Gym - Rocket': LocData(20000771, 'Gym Leader Castle'),
|
||||||
|
'Viridian Gym - Lab Man': LocData(20000772, 'Gym Leader Castle'),
|
||||||
|
'Viridian Gym - Cool(M)': LocData(20000773, 'Gym Leader Castle'),
|
||||||
|
}
|
||||||
|
|
||||||
|
location_table = {
|
||||||
|
**pokemon_stadium_locations,
|
||||||
|
**event_locations
|
||||||
|
}
|
||||||
342
worlds/PokemonStadium/Options.py
Normal file
342
worlds/PokemonStadium/Options.py
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
from typing import List, Dict, Any
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from worlds.AutoWorld import PerGameCommonOptions
|
||||||
|
from Options import Choice, OptionGroup, Toggle, Range
|
||||||
|
|
||||||
|
def create_option_groups() -> List[OptionGroup]:
|
||||||
|
option_group_list: List[OptionGroup] = []
|
||||||
|
for name, options in pokemon_stadium_option_groups.items():
|
||||||
|
option_group_list.append(OptionGroup(name=name, options=options))
|
||||||
|
|
||||||
|
return option_group_list
|
||||||
|
|
||||||
|
class VictoryCondition(Choice):
|
||||||
|
"""
|
||||||
|
Choose victory condition
|
||||||
|
"""
|
||||||
|
display_name = "Victory Condition"
|
||||||
|
option_defeat_rival = 1
|
||||||
|
option_clear_master_ball_cup = 2
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
class BaseStatTotalRandomness(Choice):
|
||||||
|
"""
|
||||||
|
Controls the level of randomness for Pokemon BST. Stat distribution per Pokemon will follow a randomly selected distribution curve.
|
||||||
|
The higher the selection, the more extreme a curve you may see used.
|
||||||
|
Stat changes are universal. Rental Pokemon and enemy trainer team Pokemon use the same BSTs.
|
||||||
|
Vanilla - No change
|
||||||
|
Low - 3 distribution types
|
||||||
|
Medium - 4 distribution types
|
||||||
|
High - 5 distribution types
|
||||||
|
"""
|
||||||
|
display_name = "BST Randomness"
|
||||||
|
option_vanilla = 1
|
||||||
|
option_low = 2
|
||||||
|
option_medium = 3
|
||||||
|
option_high = 4
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
class Trainersanity(Toggle):
|
||||||
|
"""
|
||||||
|
Toggle on to make all Trainers into checks. This option is off by default.
|
||||||
|
"""
|
||||||
|
display_name = 'Trainersanity'
|
||||||
|
option_off = 0
|
||||||
|
option_on = 1
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
class GymCastleTrainerRandomness(Choice):
|
||||||
|
"""
|
||||||
|
Controls the level of randomness for the enemy team and movesets in Gym Leader Castle.
|
||||||
|
Vanilla - No change
|
||||||
|
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
|
||||||
|
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
|
||||||
|
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
|
||||||
|
"""
|
||||||
|
display_name = "Gym Castle Trainer Randomness"
|
||||||
|
option_vanilla = 1
|
||||||
|
option_low = 2
|
||||||
|
option_medium = 3
|
||||||
|
option_high = 4
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
class PokeCupTrainerRandomness(Choice):
|
||||||
|
"""
|
||||||
|
Controls the level of randomness for the enemy team and movesets in Poke Cup.
|
||||||
|
Vanilla - No change
|
||||||
|
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
|
||||||
|
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
|
||||||
|
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
|
||||||
|
"""
|
||||||
|
display_name = "Poke Cup Trainer Randomness"
|
||||||
|
option_vanilla = 1
|
||||||
|
option_low = 2
|
||||||
|
option_medium = 3
|
||||||
|
option_high = 4
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
class PrimeCupTrainerRandomness(Choice):
|
||||||
|
"""
|
||||||
|
Controls the level of randomness for the enemy team and movesets in Prime Cup.
|
||||||
|
Vanilla - No change
|
||||||
|
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
|
||||||
|
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
|
||||||
|
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
|
||||||
|
"""
|
||||||
|
display_name = "Prime Cup Trainer Randomness"
|
||||||
|
option_vanilla = 1
|
||||||
|
option_low = 2
|
||||||
|
option_medium = 3
|
||||||
|
option_high = 4
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
class PetitCupTrainerRandomness(Choice):
|
||||||
|
"""
|
||||||
|
Controls the level of randomness for the enemy team and movesets in Petit Cup.
|
||||||
|
Vanilla - No change
|
||||||
|
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
|
||||||
|
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
|
||||||
|
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
|
||||||
|
"""
|
||||||
|
display_name = "Petit Cup Trainer Randomness"
|
||||||
|
option_vanilla = 1
|
||||||
|
option_low = 2
|
||||||
|
option_medium = 3
|
||||||
|
option_high = 4
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
class PikaCupTrainerRandomness(Choice):
|
||||||
|
"""
|
||||||
|
Controls the level of randomness for the enemy team and movesets in Pika Cup.
|
||||||
|
Vanilla - No change
|
||||||
|
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
|
||||||
|
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
|
||||||
|
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
|
||||||
|
"""
|
||||||
|
display_name = "Pika Cup Trainer Randomness"
|
||||||
|
option_vanilla = 1
|
||||||
|
option_low = 2
|
||||||
|
option_medium = 3
|
||||||
|
option_high = 4
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
class GymCastleRentalRandomness(Choice):
|
||||||
|
"""
|
||||||
|
Controls the level of randomness for the rental Pokemon moves in Gym Leader Castle.
|
||||||
|
Vanilla - No change
|
||||||
|
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
|
||||||
|
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
|
||||||
|
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
|
||||||
|
"""
|
||||||
|
display_name = "Gym Castle Rental Randomness"
|
||||||
|
option_vanilla = 1
|
||||||
|
option_low = 2
|
||||||
|
option_medium = 3
|
||||||
|
option_high = 4
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
class PokeCupRentalRandomness(Choice):
|
||||||
|
"""
|
||||||
|
Controls the level of randomness for the rental Pokemon moves in the Poke Cup.
|
||||||
|
Vanilla - No change
|
||||||
|
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
|
||||||
|
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
|
||||||
|
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
|
||||||
|
"""
|
||||||
|
display_name = "Poke Cup Rental Randomness"
|
||||||
|
option_vanilla = 1
|
||||||
|
option_low = 2
|
||||||
|
option_medium = 3
|
||||||
|
option_high = 4
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
class PrimeCupRentalRandomness(Choice):
|
||||||
|
"""
|
||||||
|
Controls the level of randomness for the rental Pokemon moves in the Prime Cup.
|
||||||
|
Vanilla - No change
|
||||||
|
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
|
||||||
|
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
|
||||||
|
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
|
||||||
|
"""
|
||||||
|
display_name = "Prime Cup Rental Randomness"
|
||||||
|
option_vanilla = 1
|
||||||
|
option_low = 2
|
||||||
|
option_medium = 3
|
||||||
|
option_high = 4
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
class PetitCupRentalRandomness(Choice):
|
||||||
|
"""
|
||||||
|
Controls the level of randomness for the rental Pokemon moves in the Petit Cup.
|
||||||
|
Vanilla - No change
|
||||||
|
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
|
||||||
|
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
|
||||||
|
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
|
||||||
|
"""
|
||||||
|
display_name = "Petit Cup Rental Randomness"
|
||||||
|
option_vanilla = 1
|
||||||
|
option_low = 2
|
||||||
|
option_medium = 3
|
||||||
|
option_high = 4
|
||||||
|
default = 1
|
||||||
|
class PikaCupRentalRandomness(Choice):
|
||||||
|
"""
|
||||||
|
Controls the level of randomness for the rental Pokemon moves in the Pika Cup.
|
||||||
|
Vanilla - No change
|
||||||
|
Low - Movesets have a status, STAB, and higher attack stat aligned move. (4th move is fully random)
|
||||||
|
Medium - Movesets have a STAB, and higher attack stat aligned move. (3rd and 4th moves are fully random)
|
||||||
|
High - Movesets have a higher attack stat aligned move. (all other moves are fully random)
|
||||||
|
"""
|
||||||
|
display_name = "Pika Cup Rental Randomness"
|
||||||
|
option_vanilla = 1
|
||||||
|
option_low = 2
|
||||||
|
option_medium = 3
|
||||||
|
option_high = 4
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
class RentalListShuffle(Choice):
|
||||||
|
"""
|
||||||
|
Controls whether the rental pokemon list is randomized or not
|
||||||
|
Instead of going in dex order, the rental tables will be shuffled
|
||||||
|
|
||||||
|
Off - No change
|
||||||
|
On - All tables shuffled
|
||||||
|
Manual: Select which tables are shuffled
|
||||||
|
"""
|
||||||
|
display_name = "Rental List Shuffle"
|
||||||
|
option_off = 1
|
||||||
|
option_on = 2
|
||||||
|
option_manual = 3
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
class RentalListShuffleGLC(Choice):
|
||||||
|
"""
|
||||||
|
Controls whether the rental pokemon list for the Gym Leader Castle is randomized or not
|
||||||
|
Instead of going in dex order, the rental tables will be shuffled
|
||||||
|
This option only matters if RentalListShuffle is set to Manual mode.
|
||||||
|
Default is set to On
|
||||||
|
|
||||||
|
Off - No change
|
||||||
|
On - All tables shuffled
|
||||||
|
"""
|
||||||
|
display_name = "RLS Manual: Gym Leader Castle"
|
||||||
|
option_off = 1
|
||||||
|
option_on = 2
|
||||||
|
default = 2
|
||||||
|
|
||||||
|
class RentalListShufflePokeCup(Choice):
|
||||||
|
"""
|
||||||
|
Controls whether the rental pokemon list for the Poke Cup is randomized or not
|
||||||
|
Instead of going in dex order, the rental tables will be shuffled
|
||||||
|
This option only matters if RentalListShuffle is set to Manual mode.
|
||||||
|
Default is set to On
|
||||||
|
|
||||||
|
Off - No change
|
||||||
|
On - All tables shuffled
|
||||||
|
"""
|
||||||
|
display_name = "RLS Manual: Poke Cup"
|
||||||
|
option_off = 1
|
||||||
|
option_on = 2
|
||||||
|
default = 2
|
||||||
|
|
||||||
|
class RentalListShufflePrimeCup(Choice):
|
||||||
|
"""
|
||||||
|
Controls whether the rental pokemon list for the Prime Cup is randomized or not
|
||||||
|
Instead of going in dex order, the rental tables will be shuffled
|
||||||
|
This option only matters if RentalListShuffle is set to Manual mode.
|
||||||
|
Default is set to On
|
||||||
|
|
||||||
|
Off - No change
|
||||||
|
On - All tables shuffled
|
||||||
|
"""
|
||||||
|
display_name = "RLS Manual: Prime Cup"
|
||||||
|
option_off = 1
|
||||||
|
option_on = 2
|
||||||
|
default = 2
|
||||||
|
|
||||||
|
class RentalListShufflePetitCup(Choice):
|
||||||
|
"""
|
||||||
|
Controls whether the rental pokemon list for the Petit Cup is randomized or not
|
||||||
|
Instead of going in dex order, the rental tables will be shuffled
|
||||||
|
This option only matters if RentalListShuffle is set to Manual mode.
|
||||||
|
Default is set to On
|
||||||
|
|
||||||
|
Off - No change
|
||||||
|
On - All tables shuffled
|
||||||
|
"""
|
||||||
|
display_name = "RLS Manual: Petit Cup"
|
||||||
|
option_off = 1
|
||||||
|
option_on = 2
|
||||||
|
default = 2
|
||||||
|
|
||||||
|
class RentalListShufflePikaCup(Choice):
|
||||||
|
"""
|
||||||
|
Controls whether the rental pokemon list for the Pika Cup is randomized or not
|
||||||
|
Instead of going in dex order, the rental tables will be shuffled
|
||||||
|
This option only matters if RentalListShuffle is set to Manual mode.
|
||||||
|
Default is set to On
|
||||||
|
|
||||||
|
Off - No change
|
||||||
|
On - All tables shuffled
|
||||||
|
"""
|
||||||
|
display_name = "RLS Manual: Pika Cup"
|
||||||
|
option_off = 1
|
||||||
|
option_on = 2
|
||||||
|
default = 2
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PokemonStadiumOptions(PerGameCommonOptions):
|
||||||
|
VictoryCondition: VictoryCondition
|
||||||
|
BaseStatTotalRandomness: BaseStatTotalRandomness
|
||||||
|
Trainersanity: Trainersanity
|
||||||
|
GymCastleTrainerRandomness: GymCastleTrainerRandomness
|
||||||
|
PokeCupTrainerRandomness: PokeCupTrainerRandomness
|
||||||
|
PrimeCupTrainerRandomness: PrimeCupTrainerRandomness
|
||||||
|
PetitCupTrainerRandomness: PetitCupTrainerRandomness
|
||||||
|
PikaCupTrainerRandomness: PikaCupTrainerRandomness
|
||||||
|
GymCastleRentalRandomness: GymCastleRentalRandomness
|
||||||
|
PokeCupRentalRandomness: PokeCupRentalRandomness
|
||||||
|
PrimeCupRentalRandomness: PrimeCupRentalRandomness
|
||||||
|
PetitCupRentalRandomness: PetitCupRentalRandomness
|
||||||
|
PikaCupRentalRandomness: PikaCupRentalRandomness
|
||||||
|
RentalListShuffle: RentalListShuffle
|
||||||
|
RentalListShuffleGLC: RentalListShuffleGLC
|
||||||
|
RentalListShufflePokeCup: RentalListShufflePokeCup
|
||||||
|
RentalListShufflePrimeCup: RentalListShufflePrimeCup
|
||||||
|
RentalListShufflePetitCup: RentalListShufflePetitCup
|
||||||
|
RentalListShufflePikaCup: RentalListShufflePikaCup
|
||||||
|
|
||||||
|
|
||||||
|
# This is where you organize your options
|
||||||
|
# Its entirely up to you how you want to organize it
|
||||||
|
pokemon_stadium_option_groups: Dict[str, List[Any]] = {
|
||||||
|
"General Options": [
|
||||||
|
VictoryCondition,
|
||||||
|
BaseStatTotalRandomness,
|
||||||
|
Trainersanity,
|
||||||
|
],
|
||||||
|
|
||||||
|
"Enemy Trainer Pokemon Options": [
|
||||||
|
GymCastleTrainerRandomness,
|
||||||
|
PokeCupTrainerRandomness,
|
||||||
|
PrimeCupTrainerRandomness,
|
||||||
|
PetitCupTrainerRandomness,
|
||||||
|
PikaCupTrainerRandomness,
|
||||||
|
],
|
||||||
|
"Rental Pokemon Options":
|
||||||
|
[
|
||||||
|
GymCastleRentalRandomness,
|
||||||
|
PokeCupRentalRandomness,
|
||||||
|
PrimeCupRentalRandomness,
|
||||||
|
PetitCupRentalRandomness,
|
||||||
|
PikaCupRentalRandomness,
|
||||||
|
],
|
||||||
|
"Shuffling Options":
|
||||||
|
[ RentalListShuffle,
|
||||||
|
RentalListShuffleGLC,
|
||||||
|
RentalListShufflePokeCup,
|
||||||
|
RentalListShufflePrimeCup,
|
||||||
|
RentalListShufflePetitCup,
|
||||||
|
RentalListShufflePikaCup],
|
||||||
|
}
|
||||||
49
worlds/PokemonStadium/Regions.py
Normal file
49
worlds/PokemonStadium/Regions.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from BaseClasses import Region
|
||||||
|
from .Types import PokemonStadiumLocation
|
||||||
|
from .Locations import location_table, trainersanity_locations, is_valid_location
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import PokemonStadiumWorld
|
||||||
|
|
||||||
|
def create_regions(world: "PokemonStadiumWorld"):
|
||||||
|
menu = create_region(world, "Menu")
|
||||||
|
|
||||||
|
# ---------------------------------- Gym Leader Castle ----------------------------------
|
||||||
|
gym_leader_castle = create_region_and_connect(world, "Gym Leader Castle", "Menu -> Gym Leader Castle", menu)
|
||||||
|
|
||||||
|
create_region_and_connect(world, "Elite Four", "Gym Leader Castle -> Elite Four", gym_leader_castle)
|
||||||
|
create_region_and_connect(world, "Rival", "Elite Four -> Rival", gym_leader_castle)
|
||||||
|
create_region_and_connect(world, "Hall of Fame", "Rival -> Hall of Fame", gym_leader_castle)
|
||||||
|
create_region_and_connect(world, "Beat Rival", "Hall of Fame -> Beat Rival", gym_leader_castle)
|
||||||
|
|
||||||
|
# -------------------------------------- Kids Club --------------------------------------
|
||||||
|
create_region_and_connect(world, "Kids Club", "Menu -> Kids Club", menu)
|
||||||
|
|
||||||
|
# --------------------------------------- Stadium ---------------------------------------
|
||||||
|
stadium = create_region_and_connect(world, "Stadium", "Menu -> Stadium", menu)
|
||||||
|
create_region_and_connect(world, "Poké Cup", "Stadium -> Poké Cup", stadium)
|
||||||
|
create_region_and_connect(world, "Petit Cup", "Stadium -> Petit Cup", stadium)
|
||||||
|
create_region_and_connect(world, "Pika Cup", "Stadium -> Pika Cup", stadium)
|
||||||
|
create_region_and_connect(world, "Prime Cup", "Stadium -> Prime Cup", stadium)
|
||||||
|
|
||||||
|
def create_region(world: "PokemonStadiumWorld", name: str) -> Region:
|
||||||
|
reg = Region(name, world.player, world.multiworld)
|
||||||
|
|
||||||
|
if world.options.Trainersanity.value == 1:
|
||||||
|
location_table.update(trainersanity_locations)
|
||||||
|
|
||||||
|
for (key, data) in location_table.items():
|
||||||
|
if data.region == name:
|
||||||
|
if not is_valid_location(world, key):
|
||||||
|
continue
|
||||||
|
location = PokemonStadiumLocation(world.player, key, data.ap_code, reg)
|
||||||
|
reg.locations.append(location)
|
||||||
|
|
||||||
|
world.multiworld.regions.append(reg)
|
||||||
|
return reg
|
||||||
|
|
||||||
|
def create_region_and_connect(world: "PokemonStadiumWorld", name: str, entrancename: str, connected_region: Region) -> Region:
|
||||||
|
reg: Region = create_region(world, name)
|
||||||
|
connected_region.connect(reg, entrancename)
|
||||||
|
return reg
|
||||||
136
worlds/PokemonStadium/Rom.py
Normal file
136
worlds/PokemonStadium/Rom.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
from settings import get_settings
|
||||||
|
import Utils
|
||||||
|
from worlds.AutoWorld import World
|
||||||
|
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
|
||||||
|
|
||||||
|
from .randomizer import stadium_randomizer
|
||||||
|
|
||||||
|
NOP = bytes([0x00,0x00,0x00,0x00])
|
||||||
|
MD5Hash = "ed1378bc12115f71209a77844965ba50"
|
||||||
|
|
||||||
|
class PokemonStadiumProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||||
|
game = "Pokemon Stadium"
|
||||||
|
hash = MD5Hash
|
||||||
|
patch_file_ending = ".apstadium"
|
||||||
|
result_file_ending = ".z64"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_source_data(cls) -> bytes:
|
||||||
|
return get_base_rom_bytes()
|
||||||
|
|
||||||
|
def get_base_rom_bytes() -> bytes:
|
||||||
|
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
|
||||||
|
if not base_rom_bytes:
|
||||||
|
file_name = get_base_rom_path()
|
||||||
|
base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb")))
|
||||||
|
|
||||||
|
basemd5 = hashlib.md5()
|
||||||
|
basemd5.update(base_rom_bytes)
|
||||||
|
md5hash = basemd5.hexdigest()
|
||||||
|
if MD5Hash !=md5hash:
|
||||||
|
raise Exception("Supplied Rom does not match known MD5 for Pokemon Stadium")
|
||||||
|
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
|
||||||
|
return base_rom_bytes
|
||||||
|
|
||||||
|
def get_base_rom_path():
|
||||||
|
file_name = get_settings()["stadium_options"]["rom_file"]
|
||||||
|
if not os.path.exists(file_name):
|
||||||
|
file_name = Utils.user_path(file_name)
|
||||||
|
return file_name
|
||||||
|
|
||||||
|
def write_tokens(world:World, patch:PokemonStadiumProcedurePatch):
|
||||||
|
# version = settings['ROMVersion']
|
||||||
|
bst_factor = world.options.BaseStatTotalRandomness.value
|
||||||
|
glc_trainer_factor = world.options.GymCastleTrainerRandomness.value
|
||||||
|
pokecup_trainer_factor = world.options.PokeCupTrainerRandomness.value
|
||||||
|
primecup_trainer_factor = world.options.PrimeCupTrainerRandomness.value
|
||||||
|
petitcup_trainer_factor = world.options.PetitCupTrainerRandomness.value
|
||||||
|
pikacup_trainer_factor = world.options.PikaCupTrainerRandomness.value
|
||||||
|
glc_rental_factor = world.options.GymCastleRentalRandomness.value
|
||||||
|
pokecup_rental_factor = world.options.PokeCupRentalRandomness.value
|
||||||
|
primecup_rental_factor = world.options.PrimeCupRentalRandomness.value
|
||||||
|
petitcup_rental_factor = world.options.PetitCupRentalRandomness.value
|
||||||
|
pikacup_rental_factor = world.options.PikaCupRentalRandomness.value
|
||||||
|
rental_list_shuffle_factor = world.options.RentalListShuffle.value
|
||||||
|
rental_list_shuffle_glc_factor = world.options.RentalListShuffleGLC.value
|
||||||
|
rental_list_shuffle_poke_cup_factor = world.options.RentalListShufflePokeCup.value
|
||||||
|
rental_list_shuffle_prime_cup_factor = world.options.RentalListShufflePrimeCup.value
|
||||||
|
rental_list_shuffle_petit_cup_factor = world.options.RentalListShufflePetitCup.value
|
||||||
|
rental_list_shuffle_pika_cup_factor = world.options.RentalListShufflePikaCup.value
|
||||||
|
randomizer = stadium_randomizer.Randomizer('US_1.0', bst_factor, glc_trainer_factor, pokecup_trainer_factor, primecup_trainer_factor, petitcup_trainer_factor,
|
||||||
|
pikacup_trainer_factor, glc_rental_factor, pokecup_rental_factor, primecup_rental_factor,petitcup_rental_factor, pikacup_rental_factor,
|
||||||
|
rental_list_shuffle_factor, rental_list_shuffle_glc_factor, rental_list_shuffle_poke_cup_factor, rental_list_shuffle_prime_cup_factor,
|
||||||
|
rental_list_shuffle_petit_cup_factor, rental_list_shuffle_pika_cup_factor)
|
||||||
|
|
||||||
|
# Bypass CIC
|
||||||
|
randomizer.disable_checksum(patch)
|
||||||
|
if bst_factor > 1:
|
||||||
|
randomizer.randomize_base_stats(patch)
|
||||||
|
if glc_trainer_factor > 1:
|
||||||
|
randomizer.randomize_glc_trainer_pokemon_round1(patch)
|
||||||
|
if pokecup_trainer_factor > 1:
|
||||||
|
randomizer.randomize_pokecup_trainer_pokemon_round1(patch)
|
||||||
|
if primecup_trainer_factor > 1:
|
||||||
|
randomizer.randomize_primecup_trainer_pokemon_round1(patch)
|
||||||
|
if petitcup_trainer_factor > 1:
|
||||||
|
randomizer.randomize_petitcup_trainer_pokemon_round1(patch)
|
||||||
|
if pikacup_trainer_factor > 1:
|
||||||
|
randomizer.randomize_pikacup_trainer_pokemon_round1(patch)
|
||||||
|
|
||||||
|
if glc_rental_factor > 1:
|
||||||
|
randomizer.randomize_glc_rentals_round1(patch)
|
||||||
|
if pokecup_rental_factor > 1:
|
||||||
|
randomizer.randomize_pokecup_rentals(patch)
|
||||||
|
if primecup_rental_factor > 1:
|
||||||
|
randomizer.randomize_primecup_rentals_round1(patch)
|
||||||
|
if petitcup_rental_factor > 1:
|
||||||
|
randomizer.randomize_petitcup_rentals(patch)
|
||||||
|
if pikacup_rental_factor > 1:
|
||||||
|
randomizer.randomize_pikacup_rentals(patch)
|
||||||
|
if rental_list_shuffle_factor > 1:
|
||||||
|
if rental_list_shuffle_factor != 3: #Not in manual mode
|
||||||
|
randomizer.shuffle_rentals(patch)
|
||||||
|
else:
|
||||||
|
if rental_list_shuffle_glc_factor > 1:
|
||||||
|
randomizer.shuffle_glc(patch)
|
||||||
|
if rental_list_shuffle_poke_cup_factor > 1:
|
||||||
|
randomizer.shuffle_poke(patch)
|
||||||
|
if rental_list_shuffle_prime_cup_factor > 1:
|
||||||
|
randomizer.shuffle_prime(patch)
|
||||||
|
if rental_list_shuffle_petit_cup_factor > 1:
|
||||||
|
randomizer.shuffle_petit(patch)
|
||||||
|
if rental_list_shuffle_pika_cup_factor > 1:
|
||||||
|
randomizer.shuffle_pika(patch)
|
||||||
|
|
||||||
|
# Set GP Register to 80420000
|
||||||
|
patch.write_token(APTokenTypes.WRITE, 0x202B8, bytes([0x3C, 0x1C, 0x80, 0x42]))
|
||||||
|
|
||||||
|
# Set 'Starting Battle' flag
|
||||||
|
patch.write_token(APTokenTypes.WRITE, 0x855C, bytes([0xAF, 0x81, 0x00, 0x10]))
|
||||||
|
|
||||||
|
# Clear 'Starting Battle' flag
|
||||||
|
patch.write_token(APTokenTypes.WRITE, 0x396D08, bytes([0xAF, 0x80, 0x00, 0x10]))
|
||||||
|
|
||||||
|
# Turn off A and B button on GLC select screen
|
||||||
|
patch.write_token(APTokenTypes.WRITE, 0x3B4DA8, bytes([0x50, 0x21, 0xFF, 0x82]))
|
||||||
|
|
||||||
|
# First instruction to set flag for GLC selection screen
|
||||||
|
patch.write_token(APTokenTypes.WRITE, 0x3B5548, bytes([0xAF, 0x84, 0x00, 0x00]))
|
||||||
|
|
||||||
|
# Second instruction to set flag for GLC selection screen
|
||||||
|
patch.write_token(APTokenTypes.WRITE, 0x3B55F4, bytes([0xAF, 0x82, 0x00, 0x00]))
|
||||||
|
|
||||||
|
# Set selecting Poke Cup tier flag
|
||||||
|
patch.write_token(APTokenTypes.WRITE, 0x2D6A20, bytes([0xAF, 0x93, 0x00, 0x20]))
|
||||||
|
|
||||||
|
# Clear selecting Poke Cup tier flag
|
||||||
|
patch.write_token(APTokenTypes.WRITE, 0x2D6DB0, bytes([0xAF, 0x80, 0x00, 0x20]))
|
||||||
|
|
||||||
|
# Stop game from activating unlocked gyms
|
||||||
|
patch.write_token(APTokenTypes.WRITE, 0x3B5728, bytes([0xA3, 0x20, 0x00, 0x01]))
|
||||||
|
|
||||||
|
# Write patch file
|
||||||
|
patch.write_file("token_data.bin", patch.get_token_binary())
|
||||||
132
worlds/PokemonStadium/Rules.py
Normal file
132
worlds/PokemonStadium/Rules.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
from worlds.generic.Rules import set_rule, add_item_rule
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import PokemonStadiumWorld
|
||||||
|
|
||||||
|
def set_rules(world: "PokemonStadiumWorld"):
|
||||||
|
player = world.player
|
||||||
|
options = world.options
|
||||||
|
|
||||||
|
# Gym Access
|
||||||
|
set_rule(world.multiworld.get_location("Pewter Gym", player), lambda state: state.has("Pewter City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Cerulean Gym", player), lambda state: state.has("Cerulean City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Vermillion Gym", player), lambda state: state.has("Vermillion City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Celadon Gym", player), lambda state: state.has("Celadon City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Fuchsia Gym", player), lambda state: state.has("Fuchsia City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Saffron Gym", player), lambda state: state.has("Saffron City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Cinnabar Gym", player), lambda state: state.has("Cinnabar Island Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Viridian Gym", player), lambda state: state.has("Viridian City Key", player))
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("BROCK", player), lambda state: state.has("Pewter City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("MISTY", player), lambda state: state.has("Cerulean City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("SURGE", player), lambda state: state.has("Vermillion City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("ERIKA", player), lambda state: state.has("Celadon City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("KOGA", player), lambda state: state.has("Fuchsia City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("SABRINA", player), lambda state: state.has("Saffron City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("BLAINE", player), lambda state: state.has("Cinnabar Island Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("GIOVANNI", player), lambda state: state.has("Viridian City Key", player))
|
||||||
|
|
||||||
|
# Cup Access
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Prize", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Prize", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Prize", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Prize", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Prize", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Prize", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
|
||||||
|
|
||||||
|
#Trainersanity All
|
||||||
|
if world.options.Trainersanity.value == 1:
|
||||||
|
set_rule(world.multiworld.get_location("Pewter Gym - Bug Boy", player), lambda state: state.has("Pewter City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Pewter Gym - Lad", player), lambda state: state.has("Pewter City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Pewter Gym - Jr(M)", player), lambda state: state.has("Pewter City Key", player))
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Cerulean Gym - Fisher", player), lambda state: state.has("Cerulean City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Cerulean Gym - Jr(F)", player), lambda state: state.has("Cerulean City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Cerulean Gym - Swimmer", player), lambda state: state.has("Cerulean City Key", player))
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Vermillion Gym - Sailor", player), lambda state: state.has("Vermillion City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Vermillion Gym - Rocker", player), lambda state: state.has("Vermillion City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Vermillion Gym - Old Man", player), lambda state: state.has("Vermillion City Key", player))
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Celadon Gym - Lass", player), lambda state: state.has("Celadon City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Celadon Gym - Beauty", player), lambda state: state.has("Celadon City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Celadon Gym - Cool(F)", player), lambda state: state.has("Celadon City Key", player))
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Fuchsia Gym - Biker", player), lambda state: state.has("Fuchsia City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Fuchsia Gym - Tamer", player), lambda state: state.has("Fuchsia City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Fuchsia Gym - Juggler", player), lambda state: state.has("Fuchsia City Key", player))
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Saffron Gym - Cue Ball", player), lambda state: state.has("Saffron City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Saffron Gym - Burglar", player), lambda state: state.has("Saffron City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Saffron Gym - Medium", player), lambda state: state.has("Saffron City Key", player))
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Cinnabar Gym - Judoboy", player), lambda state: state.has("Cinnabar Island Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Cinnabar Gym - Psychic", player), lambda state: state.has("Cinnabar Island Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Cinnabar Gym - Nerd", player), lambda state: state.has("Cinnabar Island Key", player))
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Viridian Gym - Rocket", player), lambda state: state.has("Viridian City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Viridian Gym - Lab Man", player), lambda state: state.has("Viridian City Key", player))
|
||||||
|
set_rule(world.multiworld.get_location("Viridian Gym - Cool(M)", player), lambda state: state.has("Viridian City Key", player))
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Bug Boy", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Lad", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Nerd", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Sailor", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Jr(F)", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Jr(M)", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Lass", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Great Ball - Pokémaniac", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 0)
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Bug Boy", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Lad", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Nerd", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Sailor", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Jr(F)", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Jr(M)", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Lass", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Ultra Ball - Pokémaniac", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 1)
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Bug Boy", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Lad", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Nerd", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Sailor", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Jr(F)", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Jr(M)", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Lass", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Poké Cup - Master Ball - Pokémaniac", player), lambda state: state.count('Poké Cup - Tier Upgrade', player) > 2)
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Cue Ball", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Rocket", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Judoboy", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Gambler", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Cool(F)", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Bird Boy", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Lab Man", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Great Ball - Cool(M)", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 0)
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Cue Ball", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Rocket", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Judoboy", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Gambler", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Cool(F)", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Bird Boy", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Lab Man", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Ultra Ball - Cool(M)", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 1)
|
||||||
|
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Cue Ball", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Rocket", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Judoboy", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Gambler", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Cool(F)", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Bird Boy", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Lab Man", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
|
||||||
|
set_rule(world.multiworld.get_location("Prime Cup - Master Ball - Cool(M)", player), lambda state: state.count('Prime Cup - Tier Upgrade', player) > 2)
|
||||||
|
|
||||||
|
# Beat Rival Rule
|
||||||
|
badges = ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge", "Marsh Badge", "Volcano Badge", "Earth Badge"]
|
||||||
|
set_rule(world.multiworld.get_location("Beat Rival", player), lambda state: state.has_all(badges, player))
|
||||||
|
|
||||||
|
# Victory condition rule!
|
||||||
|
world.multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
|
||||||
18
worlds/PokemonStadium/Types.py
Normal file
18
worlds/PokemonStadium/Types.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from enum import IntEnum
|
||||||
|
from typing import NamedTuple, Optional
|
||||||
|
from BaseClasses import Location, Item, ItemClassification
|
||||||
|
|
||||||
|
class PokemonStadiumLocation(Location):
|
||||||
|
game = 'PokemonStadium'
|
||||||
|
|
||||||
|
class PokemonStadiumItem(Item):
|
||||||
|
game = 'PokemonStadium'
|
||||||
|
|
||||||
|
class ItemData(NamedTuple):
|
||||||
|
ap_code: Optional[int]
|
||||||
|
classification: ItemClassification
|
||||||
|
count: Optional[int] = 1
|
||||||
|
|
||||||
|
class LocData(NamedTuple):
|
||||||
|
ap_code: Optional[int]
|
||||||
|
region: Optional[str]
|
||||||
148
worlds/PokemonStadium/__init__.py
Normal file
148
worlds/PokemonStadium/__init__.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pkgutil
|
||||||
|
import random
|
||||||
|
|
||||||
|
from BaseClasses import MultiWorld, Item, Tutorial
|
||||||
|
import settings
|
||||||
|
from typing import Dict
|
||||||
|
import Utils
|
||||||
|
from worlds.AutoWorld import World, CollectionState, WebWorld
|
||||||
|
|
||||||
|
from .Client import PokemonStadiumClient # Unused, but required to register with BizHawkClient
|
||||||
|
from .Items import create_item, create_itempool, gym_keys, item_table
|
||||||
|
from .Locations import get_location_names, get_total_locations
|
||||||
|
from .Options import PokemonStadiumOptions
|
||||||
|
from .Regions import create_regions
|
||||||
|
from .Rom import MD5Hash, PokemonStadiumProcedurePatch, write_tokens
|
||||||
|
from .Rom import get_base_rom_path as get_base_rom_path
|
||||||
|
from .Rules import set_rules
|
||||||
|
|
||||||
|
class PokemonStadiumSettings(settings.Group):
|
||||||
|
class PokemonStadiumRomFile(settings.UserFilePath):
|
||||||
|
"""File name of the Pokemon Stadium (US, 1.0) ROM"""
|
||||||
|
description = "Pokemon Stadium (US, 1.0) ROM File"
|
||||||
|
copy_to = "Pokemon Stadium (US, 1.0).z64"
|
||||||
|
md5s = [PokemonStadiumProcedurePatch.hash]
|
||||||
|
|
||||||
|
rom_file: PokemonStadiumRomFile = PokemonStadiumRomFile(PokemonStadiumRomFile.copy_to)
|
||||||
|
|
||||||
|
class PokemonStadiumWeb(WebWorld):
|
||||||
|
theme = "Party"
|
||||||
|
|
||||||
|
tutorials = [Tutorial(
|
||||||
|
"Multiworld Setup Guide",
|
||||||
|
"A guide to setting up (the game you are randomizing) for Archipelago. "
|
||||||
|
"This guide covers single-player, multiworld, and related software.",
|
||||||
|
"English",
|
||||||
|
"setup_en.md",
|
||||||
|
"setup/en",
|
||||||
|
["JCIII"]
|
||||||
|
)]
|
||||||
|
|
||||||
|
class PokemonStadiumWorld(World):
|
||||||
|
game = "Pokemon Stadium"
|
||||||
|
|
||||||
|
settings_key = "stadium_options"
|
||||||
|
settings: PokemonStadiumSettings
|
||||||
|
|
||||||
|
item_name_to_id = {name: data.ap_code for name, data in item_table.items()}
|
||||||
|
|
||||||
|
location_name_to_id = get_location_names()
|
||||||
|
|
||||||
|
options_dataclass = PokemonStadiumOptions
|
||||||
|
options = PokemonStadiumOptions
|
||||||
|
|
||||||
|
web = PokemonStadiumWeb()
|
||||||
|
|
||||||
|
starting_gym_keys = random.sample(gym_keys, 3)
|
||||||
|
|
||||||
|
def __init__(self, multiworld: "MultiWorld", player: int):
|
||||||
|
super().__init__(multiworld, player)
|
||||||
|
|
||||||
|
def generate_early(self):
|
||||||
|
for key in self.starting_gym_keys:
|
||||||
|
self.multiworld.push_precollected(self.create_item(key))
|
||||||
|
|
||||||
|
def create_regions(self):
|
||||||
|
create_regions(self)
|
||||||
|
|
||||||
|
def create_items(self):
|
||||||
|
self.multiworld.itempool += create_itempool(self)
|
||||||
|
|
||||||
|
def create_item(self, name: str) -> Item:
|
||||||
|
return create_item(self, name)
|
||||||
|
|
||||||
|
def set_rules(self):
|
||||||
|
set_rules(self)
|
||||||
|
|
||||||
|
def fill_slot_data(self) -> Dict[str, object]:
|
||||||
|
slot_data: Dict[str, object] = {
|
||||||
|
"options": {
|
||||||
|
"VictoryCondition": self.options.VictoryCondition.value,
|
||||||
|
"BaseStatTotalRandomness": self.options.BaseStatTotalRandomness.value,
|
||||||
|
"Trainersanity": self.options.Trainersanity.value,
|
||||||
|
"GymCastleTrainerRandomness": self.options.GymCastleTrainerRandomness.value,
|
||||||
|
"PokeCupTrainerRandomness": self.options.PokeCupTrainerRandomness.value,
|
||||||
|
"PrimeCupTrainerRandomness": self.options.PrimeCupTrainerRandomness.value,
|
||||||
|
"PetitupTrainerRandomness": self.options.PetitCupTrainerRandomness.value,
|
||||||
|
"PikaCupTrainerRandomness": self.options.PikaCupTrainerRandomness.value,
|
||||||
|
"GymCastleRentalRandomness": self.options.GymCastleRentalRandomness.value,
|
||||||
|
"PokeCupRentalRandomness": self.options.PokeCupRentalRandomness.value,
|
||||||
|
"PrimeCupRentalRandomness": self.options.PrimeCupRentalRandomness.value,
|
||||||
|
"PetitCupRentalRandomness": self.options.PetitCupRentalRandomness.value,
|
||||||
|
"RentalListShuffle": self.options.RentalListShuffle.value,
|
||||||
|
"RentalListShuffleGLC": self.options.RentalListShuffleGLC.value,
|
||||||
|
"RentalListShufflePokeCup": self.options.RentalListShufflePokeCup.value,
|
||||||
|
"RentalListShufflePrimeCup": self.options.RentalListShufflePrimeCup.value,
|
||||||
|
"RentalListShufflePetitCup": self.options.RentalListShufflePetitCup.value,
|
||||||
|
"RentalListShufflePikaCup": self.options.RentalListShufflePikaCup.value,
|
||||||
|
},
|
||||||
|
"Seed": self.multiworld.seed_name, # to verify the server's multiworld
|
||||||
|
"Slot": self.multiworld.player_name[self.player], # to connect to server
|
||||||
|
"TotalLocations": get_total_locations(self) # get_total_locations(self) comes from Locations.py
|
||||||
|
}
|
||||||
|
|
||||||
|
return slot_data
|
||||||
|
|
||||||
|
def generate_output(self, output_directory: str) -> None:
|
||||||
|
# === Step 1: Build ROM and player metadata ===
|
||||||
|
outfilepname = f"_P{self.player}_"
|
||||||
|
outfilepname += f"{self.multiworld.get_file_safe_player_name(self.player).replace(' ', '_')}"
|
||||||
|
|
||||||
|
# ROM name metadata (embedded in ROM for client/UI)
|
||||||
|
self.rom_name_text = f'PokemonStadium{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:011}\0'
|
||||||
|
self.romName = bytearray(self.rom_name_text, "utf8")[:0x20]
|
||||||
|
self.romName.extend([0] * (0x20 - len(self.romName))) # pad to 0x20
|
||||||
|
self.rom_name = self.romName
|
||||||
|
|
||||||
|
# Player name metadata
|
||||||
|
self.playerName = bytearray(self.multiworld.player_name[self.player], "utf8")[:0x20]
|
||||||
|
self.playerName.extend([0] * (0x20 - len(self.playerName)))
|
||||||
|
|
||||||
|
# === Step 3: Create procedure patch object ===
|
||||||
|
patch = PokemonStadiumProcedurePatch(
|
||||||
|
player=self.player,
|
||||||
|
player_name=self.multiworld.player_name[self.player]
|
||||||
|
)
|
||||||
|
|
||||||
|
# === Step 4: Apply token modifications directly ===
|
||||||
|
write_tokens(self, patch)
|
||||||
|
procedure = [("apply_tokens", ["token_data.bin"])]
|
||||||
|
|
||||||
|
# === Step 6: Finalize procedure ===
|
||||||
|
patch.procedure = procedure
|
||||||
|
|
||||||
|
# Generate output file path
|
||||||
|
out_file_name = self.multiworld.get_out_file_name_base(self.player)
|
||||||
|
patch_file_path = os.path.join(output_directory, f"{out_file_name}{patch.patch_file_ending}")
|
||||||
|
|
||||||
|
# Write the final patch file (.bps)
|
||||||
|
patch.write(patch_file_path)
|
||||||
|
|
||||||
|
def collect(self, state: "CollectionState", item: "Item") -> bool:
|
||||||
|
return super().collect(state, item)
|
||||||
|
|
||||||
|
def remove(self, state: "CollectionState", item: "Item") -> bool:
|
||||||
|
return super().remove(state, item)
|
||||||
1
worlds/PokemonStadium/docs/setup_en.md
Normal file
1
worlds/PokemonStadium/docs/setup_en.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
stop making me do this
|
||||||
674
worlds/PokemonStadium/randomizer/LICENSE
Normal file
674
worlds/PokemonStadium/randomizer/LICENSE
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
18
worlds/PokemonStadium/randomizer/README.md
Normal file
18
worlds/PokemonStadium/randomizer/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Pokemon Stadium Randomizer
|
||||||
|
|
||||||
|
https://stadiumrando.com
|
||||||
|
|
||||||
|
## Built-in Rental Rando
|
||||||
|
- Press START on the rental team selection screen to fill your team with random Pokemon
|
||||||
|
|
||||||
|
## Currently Randomizing
|
||||||
|
- Gym Leader Castle
|
||||||
|
- Player Rentals
|
||||||
|
- Base stats
|
||||||
|
- EVs and IVs
|
||||||
|
- Moves
|
||||||
|
- Enemies
|
||||||
|
- Pokemon
|
||||||
|
- Base stats
|
||||||
|
- EVs and IVs
|
||||||
|
- Moves
|
||||||
898
worlds/PokemonStadium/randomizer/constants.py
Normal file
898
worlds/PokemonStadium/randomizer/constants.py
Normal file
@@ -0,0 +1,898 @@
|
|||||||
|
rom_offsets = {
|
||||||
|
"US_1.0" : {
|
||||||
|
"CheckSum1" : 0x63C,
|
||||||
|
"CheckSum2" : 0x648,
|
||||||
|
"SetBattleStartFlag": 34140,
|
||||||
|
"BaseStats" : 465825,
|
||||||
|
"SetGPRegister": 131768,
|
||||||
|
"SetPokeCupFlag": 2976288,
|
||||||
|
"ClearPokeCupFlag": 2977200,
|
||||||
|
"Rental_Table_Input_Routine" : 3023512,
|
||||||
|
"DefeatedNonLeaderFlag": 3761116,
|
||||||
|
"LostToTrainerFlag": 3763336,
|
||||||
|
"SetGLCFlag1": 3888456,
|
||||||
|
"SetGLCFlag2": 3888628,
|
||||||
|
"GymCastle_Round1": 9057228,
|
||||||
|
"PokeCup_Round1": 9039244, #This starts at pokeball cup
|
||||||
|
"PokeCup_Round2": 9159120, #Needs adjustment
|
||||||
|
"PrimeCup_Round1": 9021260, #This starts at pokeball cup
|
||||||
|
"PrimeCup_Round2": 9141136, #Needs adjustment
|
||||||
|
"PetitCup_Round1": 9012268, #Starts at first pokemon first trainer
|
||||||
|
"PetitCup_Round2": 9132144, #Needs adjustment
|
||||||
|
"PikaCup_Round1": 9016764, #Starts at first pokemon first trainer
|
||||||
|
"PikaCup_Round2": 9136640, #Needs adjustment
|
||||||
|
"Mewtwo_Round1": 9081408, #Needs adjustment
|
||||||
|
"Mewtwo_Round2": 9201344, #Needs adjustment
|
||||||
|
|
||||||
|
|
||||||
|
"Rentals_GymCastle_Round1" : 9119616,
|
||||||
|
"Rentals_PokeCup" : 9105952,
|
||||||
|
"Rentals_PrimeCup_Round1" : 9093424,
|
||||||
|
"Rentals_PrimeCup_Round2" : 9201920,
|
||||||
|
"Rentals_PetitCup" : 9081984,
|
||||||
|
"Rentals_PikaCup" : 9085776,
|
||||||
|
},
|
||||||
|
"PAL_1.1" : {
|
||||||
|
"CheckSum1" : 1596,
|
||||||
|
"CheckSum2" : 1608,
|
||||||
|
"BaseStats" : 466337,
|
||||||
|
"Rental_Table_Input_Routine" : 2967864,
|
||||||
|
"Rental_Table_Header" : 7882439,
|
||||||
|
"Rental_GymCastle_Round1_Pointer" : 8872432,
|
||||||
|
"GymCastle_Round1": 8917964,
|
||||||
|
"EmptyRomSpace" : 33301456,
|
||||||
|
"EmptyRomSpaceForTables" : 33302224,
|
||||||
|
"OffsetToNewTable" : "0174C6D000003200"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kanto_dex_names = [
|
||||||
|
{"name": "BULBASAUR", "type": "1603", "exp": "117360", 'bst': [45, 49, 49, 45, 65 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "IVYSAUR", "type": "1603", "exp": "117360", 'bst': [60, 62, 63, 60, 80 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "VENUSAUR", "type": "1603", "exp": "117360", 'bst': [80, 82, 83, 80, 100], "gr" : "mediumslow"},
|
||||||
|
{"name": "CHARMANDER", "type": "1414", "exp": "117360", 'bst': [39, 52, 43, 65, 50 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "CHARMELEON", "type": "1414", "exp": "117360", 'bst': [58, 64, 58, 80, 65 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "CHARIZARD", "type": "1402", "exp": "117360", 'bst': [78, 84, 78, 100, 85 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "SQUIRTLE", "type": "1515", "exp": "117360", 'bst': [44, 48, 65, 43, 50 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "WARTORTLE", "type": "1515", "exp": "117360", 'bst': [59, 63, 80, 58, 65 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "BLASTOISE", "type": "1515", "exp": "117360", 'bst': [79, 83, 100, 78, 85 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "CATERPIE", "type": "0707", "exp": "125000", 'bst': [45, 30, 35, 45, 20 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "METAPOD", "type": "0707", "exp": "125000", 'bst': [50, 20, 55, 30, 25 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "BUTTERFREE", "type": "0702", "exp": "125000", 'bst': [60, 45, 50, 70, 80 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "WEEDLE", "type": "0703", "exp": "125000", 'bst': [40, 35, 30, 50, 20 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "KAKUNA", "type": "0703", "exp": "125000", 'bst': [45, 25, 50, 35, 25 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "BEEDRILL", "type": "0703", "exp": "125000", 'bst': [65, 80, 40, 75, 45 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "PIDGEY", "type": "0002", "exp": "117360", 'bst': [40, 45, 40, 56, 35 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "PIDGEOTTO", "type": "0002", "exp": "117360", 'bst': [63, 60, 55, 71, 50 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "PIDGEOT", "type": "0002", "exp": "117360", 'bst': [83, 80, 75, 91, 70 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "RATTATA", "type": "0000", "exp": "125000", 'bst': [30, 56, 35, 72, 25 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "RATICATE", "type": "0000", "exp": "125000", 'bst': [55, 81, 60, 97, 50 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "SPEAROW", "type": "0002", "exp": "125000", 'bst': [40, 60, 30, 70, 31 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "FEAROW", "type": "0002", "exp": "125000", 'bst': [65, 90, 65, 100, 61 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "EKANS", "type": "0303", "exp": "125000", 'bst': [35, 60, 44, 55, 40 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "ARBOK", "type": "0303", "exp": "125000", 'bst': [60, 85, 69, 80, 65 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "PIKACHU", "type": "1717", "exp": "125000", 'bst': [35, 55, 30, 90, 50 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "RAICHU", "type": "1717", "exp": "125000", 'bst': [60, 90, 55, 100, 90 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "SANDSHREW", "type": "0404", "exp": "125000", 'bst': [50, 75, 85, 40, 30 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "SANDSLASH", "type": "0404", "exp": "125000", 'bst': [75, 100, 110, 65, 55 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "NIDORAN", "type": "0303", "exp": "117360", 'bst': [55, 47, 52, 41, 40 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "NIDORINA", "type": "0303", "exp": "117360", 'bst': [70, 62, 67, 56, 55 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "NIDOQUEEN", "type": "0304", "exp": "117360", 'bst': [90, 82, 87, 76, 75 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "NIDORAN", "type": "0303", "exp": "117360", 'bst': [46, 57, 40, 50, 40 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "NIDORINO", "type": "0303", "exp": "117360", 'bst': [61, 72, 57, 65, 55 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "NIDOKING", "type": "0304", "exp": "117360", 'bst': [81, 92, 77, 85, 75 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "CLEFAIRY", "type": "0000", "exp": "100000", 'bst': [70, 45, 48, 35, 60 ], "gr" : "fast"},
|
||||||
|
{"name": "CLEFABLE", "type": "0000", "exp": "100000", 'bst': [95, 70, 73, 60, 85 ], "gr" : "fast"},
|
||||||
|
{"name": "VULPIX", "type": "1414", "exp": "125000", 'bst': [38, 41, 40, 65, 65 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "NINETALES", "type": "1414", "exp": "125000", 'bst': [73, 76, 75, 100, 100], "gr" : "mediumfast"},
|
||||||
|
{"name": "JIGGLYPUFF", "type": "0000", "exp": "100000", 'bst': [115, 45, 20, 20, 25 ], "gr" : "fast"},
|
||||||
|
{"name": "WIGGLYTUFF", "type": "0000", "exp": "100000", 'bst': [140, 70, 45, 45, 50 ], "gr" : "fast"},
|
||||||
|
{"name": "ZUBAT", "type": "0302", "exp": "125000", 'bst': [40, 45, 35, 55, 40 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "GOLBAT", "type": "0302", "exp": "125000", 'bst': [75, 80, 70, 90, 75 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "ODDISH", "type": "1603", "exp": "117360", 'bst': [45, 50, 55, 30, 75 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "GLOOM", "type": "1603", "exp": "117360", 'bst': [60, 65, 70, 40, 85 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "VILEPLUME", "type": "1603", "exp": "117360", 'bst': [75, 80, 85, 50, 100], "gr" : "mediumslow"},
|
||||||
|
{"name": "PARAS", "type": "0716", "exp": "125000", 'bst': [35, 70, 55, 25, 55 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "PARASECT", "type": "0716", "exp": "125000", 'bst': [60, 95, 80, 30, 80 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "VENONAT", "type": "0703", "exp": "125000", 'bst': [60, 55, 50, 45, 40 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "VENOMOTH", "type": "0703", "exp": "125000", 'bst': [70, 65, 60, 90, 90 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "DIGLETT", "type": "0404", "exp": "125000", 'bst': [10, 55, 25, 95, 45 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "DUGTRIO", "type": "0404", "exp": "125000", 'bst': [35, 80, 50, 120, 70 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "MEOWTH", "type": "0000", "exp": "125000", 'bst': [40, 45, 35, 90, 40 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "PERSIAN", "type": "0000", "exp": "125000", 'bst': [65, 70, 60, 115, 65 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "PSYDUCK", "type": "1515", "exp": "125000", 'bst': [50, 52, 48, 55, 50 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "GOLDUCK", "type": "1515", "exp": "125000", 'bst': [80, 82, 78, 85, 80 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "MANKEY", "type": "0101", "exp": "125000", 'bst': [40, 80, 35, 70, 35 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "PRIMEAPE", "type": "0101", "exp": "125000", 'bst': [65, 105, 60, 95, 60 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "GROWLITHE", "type": "1414", "exp": "156250", 'bst': [55, 70, 45, 60, 50 ], "gr" : "slow"},
|
||||||
|
{"name": "ARCANINE", "type": "1414", "exp": "156250", 'bst': [90, 110, 80, 95, 80 ], "gr" : "slow"},
|
||||||
|
{"name": "POLIWAG", "type": "1515", "exp": "117360", 'bst': [40, 50, 40, 90, 40 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "POLIWHIRL", "type": "1515", "exp": "117360", 'bst': [65, 65, 65, 90, 50 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "POLIWRATH", "type": "1501", "exp": "117360", 'bst': [90, 85, 95, 70, 70 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "ABRA", "type": "1818", "exp": "117360", 'bst': [25, 20, 15, 90, 105], "gr" : "mediumslow"},
|
||||||
|
{"name": "KADABRA", "type": "1818", "exp": "117360", 'bst': [40, 35, 30, 105, 120], "gr" : "mediumslow"},
|
||||||
|
{"name": "ALAKAZAM", "type": "1818", "exp": "117360", 'bst': [55, 50, 45, 120, 135], "gr" : "mediumslow"},
|
||||||
|
{"name": "MACHOP", "type": "0101", "exp": "117360", 'bst': [70, 80, 50, 35, 35 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "MACHOKE", "type": "0101", "exp": "117360", 'bst': [80, 100, 70, 45, 50 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "MACHAMP", "type": "0101", "exp": "117360", 'bst': [90, 130, 80, 55, 65 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "BELLSPROUT", "type": "1603", "exp": "117360", 'bst': [50, 75, 35, 40, 70 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "WEEPINBELL", "type": "1603", "exp": "117360", 'bst': [65, 90, 50, 55, 85 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "VICTREEBEL", "type": "1603", "exp": "117360", 'bst': [80, 105, 65, 70, 100], "gr" : "mediumslow"},
|
||||||
|
{"name": "TENTACOOL", "type": "1503", "exp": "156250", 'bst': [40, 40, 35, 70, 100], "gr" : "slow"},
|
||||||
|
{"name": "TENTACRUEL", "type": "1503", "exp": "156250", 'bst': [80, 70, 65, 100, 120], "gr" : "slow"},
|
||||||
|
{"name": "GEODUDE", "type": "0504", "exp": "117360", 'bst': [40, 80, 100, 20, 30 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "GRAVELER", "type": "0504", "exp": "117360", 'bst': [55, 95, 115, 35, 45 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "GOLEM", "type": "0504", "exp": "117360", 'bst': [80, 110, 130, 45, 55 ], "gr" : "mediumslow"},
|
||||||
|
{"name": "PONYTA", "type": "1414", "exp": "125000", 'bst': [50, 85, 55, 90, 65 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "RAPIDASH", "type": "1414", "exp": "125000", 'bst': [65, 100, 70, 105, 80 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "SLOWPOKE", "type": "1518", "exp": "125000", 'bst': [90, 65, 65, 15, 40 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "SLOWBRO", "type": "1518", "exp": "125000", 'bst': [95, 75, 110, 30, 80 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "MAGNEMITE", "type": "1717", "exp": "125000", 'bst': [25, 35, 70, 45, 95 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "MAGNETON", "type": "1717", "exp": "125000", 'bst': [50, 60, 95, 70, 120], "gr" : "mediumfast"},
|
||||||
|
{"name": "FARFETCH'D", "type": "0002", "exp": "125000", 'bst': [52, 65, 55, 60, 58 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "DODUO", "type": "0002", "exp": "125000", 'bst': [35, 85, 45, 75, 35 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "DODRIO", "type": "0002", "exp": "125000", 'bst': [60, 110, 70, 100, 60 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "SEEL", "type": "1515", "exp": "125000", 'bst': [65, 45, 55, 45, 70 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "DEWGONG", "type": "1519", "exp": "125000", 'bst': [90, 70, 80, 70, 95 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "GRIMER", "type": "0303", "exp": "125000", 'bst': [80, 80, 50, 25, 40 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "MUK", "type": "0303", "exp": "125000", 'bst': [105, 105, 75, 50, 65 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "SHELLDER", "type": "1515", "exp": "156250", 'bst': [30, 65, 100, 40, 45 ], "gr" : "slow"},
|
||||||
|
{"name": "CLOYSTER", "type": "1519", "exp": "156250", 'bst': [50, 95, 180, 70, 85 ], "gr" : "slow"},
|
||||||
|
{"name": "GASTLY", "type": "0803", "exp": "117360", 'bst': [30, 35, 30, 80, 100], "gr" : "mediumslow"},
|
||||||
|
{"name": "HAUNTER", "type": "0803", "exp": "117360", 'bst': [45, 50, 45, 95, 115], "gr" : "mediumslow"},
|
||||||
|
{"name": "GENGAR", "type": "0803", "exp": "117360", 'bst': [60, 65, 60, 110, 130], "gr" : "mediumslow"},
|
||||||
|
{"name": "ONIX", "type": "0504", "exp": "125000", 'bst': [35, 45, 160, 70, 30 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "DROWZEE", "type": "1818", "exp": "125000", 'bst': [60, 48, 45, 42, 90 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "HYPNO", "type": "1818", "exp": "125000", 'bst': [85, 73, 70, 67, 115], "gr" : "mediumfast"},
|
||||||
|
{"name": "KRABBY", "type": "1515", "exp": "125000", 'bst': [30, 105, 90, 50, 25 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "KINGLER", "type": "1515", "exp": "125000", 'bst': [55, 130, 115, 75, 50 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "VOLTORB", "type": "1717", "exp": "125000", 'bst': [40, 30, 50, 100, 55 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "ELECTRODE", "type": "1717", "exp": "125000", 'bst': [60, 50, 70, 140, 80 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "EXEGGCUTE", "type": "1618", "exp": "156250", 'bst': [60, 40, 80, 40, 60 ], "gr" : "slow"},
|
||||||
|
{"name": "EXEGGUTOR", "type": "1618", "exp": "156250", 'bst': [95, 95, 85, 55, 125], "gr" : "slow"},
|
||||||
|
{"name": "CUBONE", "type": "0404", "exp": "125000", 'bst': [50, 50, 95, 35, 40 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "MAROWAK", "type": "0404", "exp": "125000", 'bst': [60, 80, 110, 45, 50 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "HITMONLEE", "type": "0101", "exp": "125000", 'bst': [50, 120, 53, 87, 35 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "HITMONCHAN", "type": "0101", "exp": "125000", 'bst': [50, 105, 79, 76, 35 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "LICKITUNG", "type": "0000", "exp": "125000", 'bst': [90, 55, 75, 30, 60 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "KOFFING", "type": "0303", "exp": "125000", 'bst': [40, 65, 95, 35, 60 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "WEEZING", "type": "0303", "exp": "125000", 'bst': [65, 90, 120, 60, 85 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "RHYHORN", "type": "0405", "exp": "156250", 'bst': [80, 85, 95, 25, 30 ], "gr" : "slow"},
|
||||||
|
{"name": "RHYDON", "type": "0405", "exp": "156250", 'bst': [105, 130, 120, 40, 45 ], "gr" : "slow"},
|
||||||
|
{"name": "CHANSEY", "type": "0000", "exp": "100000", 'bst': [250, 5, 5, 50, 105], "gr" : "fast"},
|
||||||
|
{"name": "TANGELA", "type": "1616", "exp": "125000", 'bst': [65, 55, 115, 60, 100], "gr" : "mediumfast"},
|
||||||
|
{"name": "KANGASKHAN", "type": "0000", "exp": "125000", 'bst': [105, 95, 80, 90, 40 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "HORSEA", "type": "1515", "exp": "125000", 'bst': [30, 40, 70, 60, 70 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "SEADRA", "type": "1515", "exp": "125000", 'bst': [55, 65, 95, 85, 95 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "GOLDEEN", "type": "1515", "exp": "125000", 'bst': [45, 67, 60, 63, 50 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "SEAKING", "type": "1515", "exp": "125000", 'bst': [80, 92, 65, 68, 80 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "STARYU", "type": "1515", "exp": "156250", 'bst': [30, 45, 55, 85, 70 ], "gr" : "slow"},
|
||||||
|
{"name": "STARMIE", "type": "1518", "exp": "156250", 'bst': [60, 75, 85, 115, 100], "gr" : "slow"},
|
||||||
|
{"name": "MR. MIME", "type": "1818", "exp": "125000", 'bst': [40, 45, 65, 90, 100], "gr" : "mediumfast"},
|
||||||
|
{"name": "SCYTHER", "type": "0702", "exp": "125000", 'bst': [70, 110, 80, 105, 55 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "JYNX", "type": "1918", "exp": "125000", 'bst': [65, 50, 35, 95, 95 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "ELECTABUZZ", "type": "1717", "exp": "125000", 'bst': [65, 83, 57, 105, 85 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "MAGMAR", "type": "1414", "exp": "125000", 'bst': [65, 95, 57, 93, 85 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "PINSIR", "type": "0707", "exp": "156250", 'bst': [65, 125, 100, 85, 55 ], "gr" : "slow"},
|
||||||
|
{"name": "TAUROS", "type": "0000", "exp": "156250", 'bst': [75, 100, 95, 110, 70 ], "gr" : "slow"},
|
||||||
|
{"name": "MAGIKARP", "type": "1515", "exp": "156250", 'bst': [20, 10, 55, 80, 20 ], "gr" : "slow"},
|
||||||
|
{"name": "GYARADOS", "type": "1502", "exp": "156250", 'bst': [95, 125, 79, 81, 100], "gr" : "slow"},
|
||||||
|
{"name": "LAPRAS", "type": "1519", "exp": "156250", 'bst': [130, 85, 80, 60, 95 ], "gr" : "slow"},
|
||||||
|
{"name": "DITTO", "type": "0000", "exp": "125000", 'bst': [48, 48, 48, 48, 48 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "EEVEE", "type": "0000", "exp": "125000", 'bst': [55, 55, 50, 55, 65 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "VAPOREON", "type": "1515", "exp": "125000", 'bst': [130, 65, 60, 65, 110], "gr" : "mediumfast"},
|
||||||
|
{"name": "JOLTEON", "type": "1717", "exp": "125000", 'bst': [65, 65, 60, 130, 110], "gr" : "mediumfast"},
|
||||||
|
{"name": "FLAREON", "type": "1414", "exp": "125000", 'bst': [65, 130, 60, 65, 110], "gr" : "mediumfast"},
|
||||||
|
{"name": "PORYGON", "type": "0000", "exp": "125000", 'bst': [65, 60, 70, 40, 75 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "OMANYTE", "type": "0515", "exp": "125000", 'bst': [35, 40, 100, 35, 90 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "OMASTAR", "type": "0515", "exp": "125000", 'bst': [70, 60, 125, 55, 115], "gr" : "mediumfast"},
|
||||||
|
{"name": "KABUTO", "type": "0515", "exp": "125000", 'bst': [30, 80, 90, 55, 45 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "KABUTOPS", "type": "0515", "exp": "125000", 'bst': [60, 115, 105, 80, 70 ], "gr" : "mediumfast"},
|
||||||
|
{"name": "AERODACTYL", "type": "0502", "exp": "156250", 'bst': [80, 105, 65, 130, 60 ], "gr" : "slow"},
|
||||||
|
{"name": "SNORLAX", "type": "0000", "exp": "156250", 'bst': [160, 110, 65, 30, 65 ], "gr" : "slow"},
|
||||||
|
{"name": "ARTICUNO", "type": "1902", "exp": "156250", 'bst': [90, 85, 100, 85, 125], "gr" : "slow"},
|
||||||
|
{"name": "ZAPDOS", "type": "1702", "exp": "156250", 'bst': [90, 90, 85, 100, 125], "gr" : "slow"},
|
||||||
|
{"name": "MOLTRES", "type": "1402", "exp": "156250", 'bst': [90, 100, 90, 90, 125], "gr" : "slow"},
|
||||||
|
{"name": "DRATINI", "type": "1A1A", "exp": "156250", 'bst': [41, 64, 45, 50, 50 ], "gr" : "slow"},
|
||||||
|
{"name": "DRAGONAIR", "type": "1A1A", "exp": "156250", 'bst': [61, 84, 65, 70, 70 ], "gr" : "slow"},
|
||||||
|
{"name": "DRAGONITE", "type": "1A02", "exp": "156250", 'bst': [91, 134, 95, 80, 100], "gr" : "slow"},
|
||||||
|
{"name": "MEWTWO", "type": "1818", "exp": "156250", 'bst': [106, 110, 90, 130, 154], "gr" : "slow"},
|
||||||
|
{"name": "MEW", "type": "1818", "exp": "117360", 'bst': [100, 100, 100, 100, 100], "gr" : "slow"}
|
||||||
|
]
|
||||||
|
|
||||||
|
GLC_list = [
|
||||||
|
{"name": "BULBASAUR", "type": "1603", "exp": "117360", 'bst': [45, 49, 49, 45, 65 ], "Moveset": [73, 92, 34, 75]},
|
||||||
|
{"name": "IVYSAUR", "type": "1603", "exp": "117360", 'bst': [60, 62, 63, 60, 80 ], "Moveset": [75, 79, 72, 38]},
|
||||||
|
{"name": "VENUSAUR", "type": "1603", "exp": "117360", 'bst': [80, 82, 83, 80, 100], "Moveset": [73, 77, 76, 36]},
|
||||||
|
{"name": "CHARMANDER", "type": "1414", "exp": "117360", 'bst': [39, 52, 43, 65, 50 ], "Moveset": [53, 163, 69, 91]},
|
||||||
|
{"name": "CHARMELEON", "type": "1414", "exp": "117360", 'bst': [58, 64, 58, 80, 65 ], "Moveset": [126, 68, 82, 163]},
|
||||||
|
{"name": "CHARIZARD", "type": "1402", "exp": "117360", 'bst': [78, 84, 78, 100, 85 ], "Moveset": [19, 91, 83, 102]},
|
||||||
|
{"name": "SQUIRTLE", "type": "1515", "exp": "117360", 'bst': [44, 48, 65, 43, 50 ], "Moveset": [57, 59, 91, 69]},
|
||||||
|
{"name": "WARTORTLE", "type": "1515", "exp": "117360", 'bst': [59, 63, 80, 58, 65 ], "Moveset": [57, 68, 66, 58]},
|
||||||
|
{"name": "BLASTOISE", "type": "1515", "exp": "117360", 'bst': [79, 83, 100, 78, 85 ], "Moveset": [56, 117, 70, 110]},
|
||||||
|
{"name": "CATERPIE", "type": "0707", "exp": "125000", 'bst': [45, 30, 35, 45, 20 ], "Moveset": [33, 81, 0, 0]},
|
||||||
|
{"name": "METAPOD", "type": "0707", "exp": "125000", 'bst': [50, 20, 55, 30, 25 ], "Moveset": [33, 81, 0, 0]},
|
||||||
|
{"name": "BUTTERFREE", "type": "0702", "exp": "125000", 'bst': [60, 45, 50, 70, 80 ], "Moveset": [94, 48, 63, 72]},
|
||||||
|
{"name": "WEEDLE", "type": "0703", "exp": "125000", 'bst': [40, 35, 30, 50, 20 ], "Moveset": [81, 40, 0, 0]},
|
||||||
|
{"name": "KAKUNA", "type": "0703", "exp": "125000", 'bst': [45, 25, 50, 35, 25 ], "Moveset": [81, 40, 0, 0]},
|
||||||
|
{"name": "BEEDRILL", "type": "0703", "exp": "125000", 'bst': [65, 80, 40, 75, 45 ], "Moveset": [41, 116, 38, 72]},
|
||||||
|
{"name": "PIDGEY", "type": "0002", "exp": "117360", 'bst': [40, 45, 40, 56, 35 ], "Moveset": [19, 38, 92, 104]},
|
||||||
|
{"name": "PIDGEOTTO", "type": "0002", "exp": "117360", 'bst': [63, 60, 55, 71, 50 ], "Moveset": [19, 97, 28, 36]},
|
||||||
|
{"name": "PIDGEOT", "type": "0002", "exp": "117360", 'bst': [83, 80, 75, 91, 70 ], "Moveset": [119, 19, 98, 63]},
|
||||||
|
{"name": "RATTATA", "type": "0000", "exp": "125000", 'bst': [30, 56, 35, 72, 25 ], "Moveset": [162, 158, 59, 91]},
|
||||||
|
{"name": "RATICATE", "type": "0000", "exp": "125000", 'bst': [55, 81, 60, 97, 50 ], "Moveset": [158, 61, 116, 91]},
|
||||||
|
{"name": "SPEAROW", "type": "0002", "exp": "125000", 'bst': [40, 60, 30, 70, 31 ], "Moveset": [65, 119, 104, 38]},
|
||||||
|
{"name": "FEAROW", "type": "0002", "exp": "125000", 'bst': [65, 90, 65, 100, 61 ], "Moveset": [97, 104, 19, 129]},
|
||||||
|
{"name": "EKANS", "type": "0303", "exp": "125000", 'bst': [35, 60, 44, 55, 40 ], "Moveset": [89, 70, 137, 51]},
|
||||||
|
{"name": "ARBOK", "type": "0303", "exp": "125000", 'bst': [60, 85, 69, 80, 65 ], "Moveset": [137, 157, 51, 91]},
|
||||||
|
{"name": "PIKACHU", "type": "1717", "exp": "125000", 'bst': [35, 55, 30, 90, 50 ], "Moveset": [85, 69, 86, 148]},
|
||||||
|
{"name": "RAICHU", "type": "1717", "exp": "125000", 'bst': [60, 90, 55, 100, 90 ], "Moveset": [87, 86, 45, 25]},
|
||||||
|
{"name": "SANDSHREW", "type": "0404", "exp": "125000", 'bst': [50, 75, 85, 40, 30 ], "Moveset": [89, 163, 69, 28]},
|
||||||
|
{"name": "SANDSLASH", "type": "0404", "exp": "125000", 'bst': [75, 100, 110, 65, 55 ], "Moveset": [91, 157, 28, 154]},
|
||||||
|
{"name": "NIDORAN", "type": "0303", "exp": "117360", 'bst': [55, 47, 52, 41, 40 ], "Moveset": [34, 92, 85, 59]},
|
||||||
|
{"name": "NIDORINA", "type": "0303", "exp": "117360", 'bst': [70, 62, 67, 56, 55 ], "Moveset": [87, 58, 92, 34]},
|
||||||
|
{"name": "NIDOQUEEN", "type": "0304", "exp": "117360", 'bst': [90, 82, 87, 76, 75 ], "Moveset": [24, 92, 34, 87]},
|
||||||
|
{"name": "NIDORAN", "type": "0303", "exp": "117360", 'bst': [46, 57, 40, 50, 40 ], "Moveset": [32, 92, 85, 59]},
|
||||||
|
{"name": "NIDORINO", "type": "0303", "exp": "117360", 'bst': [61, 72, 57, 65, 55 ], "Moveset": [92, 32, 58, 38]},
|
||||||
|
{"name": "NIDOKING", "type": "0304", "exp": "117360", 'bst': [81, 92, 77, 85, 75 ], "Moveset": [89, 32, 24, 40]},
|
||||||
|
{"name": "CLEFAIRY", "type": "0000", "exp": "100000", 'bst': [70, 45, 48, 35, 60 ], "Moveset": [85, 59, 34, 118]},
|
||||||
|
{"name": "CLEFABLE", "type": "0000", "exp": "100000", 'bst': [95, 70, 73, 60, 85 ], "Moveset": [47, 118, 161, 58]},
|
||||||
|
{"name": "VULPIX", "type": "1414", "exp": "125000", 'bst': [38, 41, 40, 65, 65 ], "Moveset": [53, 115, 109, 91]},
|
||||||
|
{"name": "NINETALES", "type": "1414", "exp": "125000", 'bst': [73, 76, 75, 100, 100], "Moveset": [109, 91, 83, 117]},
|
||||||
|
{"name": "JIGGLYPUFF", "type": "0000", "exp": "100000", 'bst': [115, 45, 20, 20, 25 ], "Moveset": [47, 34, 69, 94]},
|
||||||
|
{"name": "WIGGLYTUFF", "type": "0000", "exp": "100000", 'bst': [140, 70, 45, 45, 50 ], "Moveset": [47, 70, 50, 94]},
|
||||||
|
{"name": "ZUBAT", "type": "0302", "exp": "125000", 'bst': [40, 45, 35, 55, 40 ], "Moveset": [109, 72, 92, 38]},
|
||||||
|
{"name": "GOLBAT", "type": "0302", "exp": "125000", 'bst': [75, 80, 70, 90, 75 ], "Moveset": [109, 72, 63, 114]},
|
||||||
|
{"name": "ODDISH", "type": "1603", "exp": "117360", 'bst': [45, 50, 55, 30, 75 ], "Moveset": [78, 80, 72, 38]},
|
||||||
|
{"name": "GLOOM", "type": "1603", "exp": "117360", 'bst': [60, 65, 70, 40, 85 ], "Moveset": [78, 80, 51, 36]},
|
||||||
|
{"name": "VILEPLUME", "type": "1603", "exp": "117360", 'bst': [75, 80, 85, 50, 100], "Moveset": [80, 51, 15, 78]},
|
||||||
|
{"name": "PARAS", "type": "0716", "exp": "125000", 'bst': [35, 70, 55, 25, 55 ], "Moveset": [147, 163, 91, 72]},
|
||||||
|
{"name": "PARASECT", "type": "0716", "exp": "125000", 'bst': [60, 95, 80, 30, 80 ], "Moveset": [147, 91, 74, 72]},
|
||||||
|
{"name": "VENONAT", "type": "0703", "exp": "125000", 'bst': [60, 55, 50, 45, 40 ], "Moveset": [94, 72, 38, 92]},
|
||||||
|
{"name": "VENOMOTH", "type": "0703", "exp": "125000", 'bst': [70, 65, 60, 90, 90 ], "Moveset": [94, 48, 129, 92]},
|
||||||
|
{"name": "DIGLETT", "type": "0404", "exp": "125000", 'bst': [10, 55, 25, 95, 45 ], "Moveset": [89, 163, 90, 157]},
|
||||||
|
{"name": "DUGTRIO", "type": "0404", "exp": "125000", 'bst': [35, 80, 50, 120, 70 ], "Moveset": [91, 28, 157, 164]},
|
||||||
|
{"name": "MEOWTH", "type": "0000", "exp": "125000", 'bst': [40, 45, 35, 90, 40 ], "Moveset": [163, 85, 61, 104]},
|
||||||
|
{"name": "PERSIAN", "type": "0000", "exp": "125000", 'bst': [65, 70, 60, 115, 65 ], "Moveset": [85, 38, 117, 103]},
|
||||||
|
{"name": "PSYDUCK", "type": "1515", "exp": "125000", 'bst': [50, 52, 48, 55, 50 ], "Moveset": [57, 69, 91, 59]},
|
||||||
|
{"name": "GOLDUCK", "type": "1515", "exp": "125000", 'bst': [80, 82, 78, 85, 80 ], "Moveset": [50, 57, 93, 25]},
|
||||||
|
{"name": "MANKEY", "type": "0101", "exp": "125000", 'bst': [40, 80, 35, 70, 35 ], "Moveset": [66, 91, 69, 70]},
|
||||||
|
{"name": "PRIMEAPE", "type": "0101", "exp": "125000", 'bst': [65, 105, 60, 95, 60 ], "Moveset": [69, 103, 5, 67]},
|
||||||
|
{"name": "GROWLITHE", "type": "1414", "exp": "156250", 'bst': [55, 70, 45, 60, 50 ], "Moveset": [53, 34, 115, 91]},
|
||||||
|
{"name": "ARCANINE", "type": "1414", "exp": "156250", 'bst': [90, 110, 80, 95, 80 ], "Moveset": [126, 36, 43, 97]},
|
||||||
|
{"name": "POLIWAG", "type": "1515", "exp": "117360", 'bst': [40, 50, 40, 90, 40 ], "Moveset": [34, 59, 57, 133]},
|
||||||
|
{"name": "POLIWHIRL", "type": "1515", "exp": "117360", 'bst': [65, 65, 65, 90, 50 ], "Moveset": [95, 56, 70, 89]},
|
||||||
|
{"name": "POLIWRATH", "type": "1501", "exp": "117360", 'bst': [90, 85, 95, 70, 70 ], "Moveset": [95, 66, 102, 57]},
|
||||||
|
{"name": "ABRA", "type": "1818", "exp": "117360", 'bst': [25, 20, 15, 90, 105], "Moveset": [94, 69, 115, 92]},
|
||||||
|
{"name": "KADABRA", "type": "1818", "exp": "117360", 'bst': [40, 35, 30, 105, 120], "Moveset": [60, 86, 105, 69]},
|
||||||
|
{"name": "ALAKAZAM", "type": "1818", "exp": "117360", 'bst': [55, 50, 45, 120, 135], "Moveset": [93, 115, 134, 91]},
|
||||||
|
{"name": "MACHOP", "type": "0101", "exp": "117360", 'bst': [70, 80, 50, 35, 35 ], "Moveset": [66, 157, 89, 34]},
|
||||||
|
{"name": "MACHOKE", "type": "0101", "exp": "117360", 'bst': [80, 100, 70, 45, 50 ], "Moveset": [89, 66, 70, 116]},
|
||||||
|
{"name": "MACHAMP", "type": "0101", "exp": "117360", 'bst': [90, 130, 80, 55, 65 ], "Moveset": [2, 67, 126, 91]},
|
||||||
|
{"name": "BELLSPROUT", "type": "1603", "exp": "117360", 'bst': [50, 75, 35, 40, 70 ], "Moveset": [51, 92, 74, 75]},
|
||||||
|
{"name": "WEEPINBELL", "type": "1603", "exp": "117360", 'bst': [65, 90, 50, 55, 85 ], "Moveset": [75, 51, 21, 92]},
|
||||||
|
{"name": "VICTREEBEL", "type": "1603", "exp": "117360", 'bst': [80, 105, 65, 70, 100], "Moveset": [72, 51, 35, 92]},
|
||||||
|
{"name": "TENTACOOL", "type": "1503", "exp": "156250", 'bst': [40, 40, 35, 70, 100], "Moveset": [57, 72, 51, 92]},
|
||||||
|
{"name": "TENTACRUEL", "type": "1503", "exp": "156250", 'bst': [80, 70, 65, 100, 120], "Moveset": [51, 103, 56, 15]},
|
||||||
|
{"name": "GEODUDE", "type": "0504", "exp": "117360", 'bst': [40, 80, 100, 20, 30 ], "Moveset": [89, 34, 157, 153]},
|
||||||
|
{"name": "GRAVELER", "type": "0504", "exp": "117360", 'bst': [55, 95, 115, 35, 45 ], "Moveset": [157, 89, 70, 120]},
|
||||||
|
{"name": "GOLEM", "type": "0504", "exp": "117360", 'bst': [80, 110, 130, 45, 55 ], "Moveset": [88, 5, 91, 120]},
|
||||||
|
{"name": "PONYTA", "type": "1414", "exp": "125000", 'bst': [50, 85, 55, 90, 65 ], "Moveset": [126, 115, 32, 34]},
|
||||||
|
{"name": "RAPIDASH", "type": "1414", "exp": "125000", 'bst': [65, 100, 70, 105, 80 ], "Moveset": [23, 97, 92, 83]},
|
||||||
|
{"name": "SLOWPOKE", "type": "1518", "exp": "125000", 'bst': [90, 65, 65, 15, 40 ], "Moveset": [57, 94, 86, 133]},
|
||||||
|
{"name": "SLOWBRO", "type": "1518", "exp": "125000", 'bst': [95, 75, 110, 30, 80 ], "Moveset": [57, 29, 91, 50]},
|
||||||
|
{"name": "MAGNEMITE", "type": "1717", "exp": "125000", 'bst': [25, 35, 70, 45, 95 ], "Moveset": [85, 86, 48, 38]},
|
||||||
|
{"name": "MAGNETON", "type": "1717", "exp": "125000", 'bst': [50, 60, 95, 70, 120], "Moveset": [86, 48, 87, 103]},
|
||||||
|
{"name": "FARFETCH'D", "type": "0002", "exp": "125000", 'bst': [52, 65, 55, 60, 58 ], "Moveset": [163, 28, 92, 19]},
|
||||||
|
{"name": "DODUO", "type": "0002", "exp": "125000", 'bst': [35, 85, 45, 75, 35 ], "Moveset": [65, 161, 104, 115]},
|
||||||
|
{"name": "DODRIO", "type": "0002", "exp": "125000", 'bst': [60, 110, 70, 100, 60 ], "Moveset": [19, 161, 115, 164]},
|
||||||
|
{"name": "SEEL", "type": "1515", "exp": "125000", 'bst': [65, 45, 55, 45, 70 ], "Moveset": [58, 70, 104, 57]},
|
||||||
|
{"name": "DEWGONG", "type": "1519", "exp": "125000", 'bst': [90, 70, 80, 70, 95 ], "Moveset": [36, 62, 156, 57]},
|
||||||
|
{"name": "GRIMER", "type": "0303", "exp": "125000", 'bst': [80, 80, 50, 25, 40 ], "Moveset": [124, 34, 153, 72]},
|
||||||
|
{"name": "MUK", "type": "0303", "exp": "125000", 'bst': [105, 105, 75, 50, 65 ], "Moveset": [124, 87, 72, 103]},
|
||||||
|
{"name": "SHELLDER", "type": "1515", "exp": "156250", 'bst': [30, 65, 100, 40, 45 ], "Moveset": [58, 153, 57, 161]},
|
||||||
|
{"name": "CLOYSTER", "type": "1519", "exp": "156250", 'bst': [50, 95, 180, 70, 85 ], "Moveset": [62, 120, 128, 131]},
|
||||||
|
{"name": "GASTLY", "type": "0803", "exp": "117360", 'bst': [30, 35, 30, 80, 100], "Moveset": [94, 101, 153, 109]},
|
||||||
|
{"name": "HAUNTER", "type": "0803", "exp": "117360", 'bst': [45, 50, 45, 95, 115], "Moveset": [94, 85, 120, 109]},
|
||||||
|
{"name": "GENGAR", "type": "0803", "exp": "117360", 'bst': [60, 65, 60, 110, 130], "Moveset": [95, 138, 85, 109]},
|
||||||
|
{"name": "ONIX", "type": "0504", "exp": "125000", 'bst': [35, 45, 160, 70, 30 ], "Moveset": [89, 157, 153, 103]},
|
||||||
|
{"name": "DROWZEE", "type": "1818", "exp": "125000", 'bst': [60, 48, 45, 42, 90 ], "Moveset": [95, 69, 94, 115]},
|
||||||
|
{"name": "HYPNO", "type": "1818", "exp": "125000", 'bst': [85, 73, 70, 67, 115], "Moveset": [95, 138, 68, 29]},
|
||||||
|
{"name": "KRABBY", "type": "1515", "exp": "125000", 'bst': [30, 105, 90, 50, 25 ], "Moveset": [152, 92, 34, 59]},
|
||||||
|
{"name": "KINGLER", "type": "1515", "exp": "125000", 'bst': [55, 130, 115, 75, 50 ], "Moveset": [152, 70, 117, 43]},
|
||||||
|
{"name": "VOLTORB", "type": "1717", "exp": "125000", 'bst': [40, 30, 50, 100, 55 ], "Moveset": [85, 86, 115, 153]},
|
||||||
|
{"name": "ELECTRODE", "type": "1717", "exp": "125000", 'bst': [60, 50, 70, 140, 80 ], "Moveset": [87, 92, 129, 120]},
|
||||||
|
{"name": "EXEGGCUTE", "type": "1618", "exp": "156250", 'bst': [60, 40, 80, 40, 60 ], "Moveset": [73, 76, 121, 94]},
|
||||||
|
{"name": "EXEGGUTOR", "type": "1618", "exp": "156250", 'bst': [95, 95, 85, 55, 125], "Moveset": [73, 95, 72, 121]},
|
||||||
|
{"name": "CUBONE", "type": "0404", "exp": "125000", 'bst': [50, 50, 95, 35, 40 ], "Moveset": [155, 34, 58, 69]},
|
||||||
|
{"name": "MAROWAK", "type": "0404", "exp": "125000", 'bst': [60, 80, 110, 45, 50 ], "Moveset": [155, 37, 126, 116]},
|
||||||
|
{"name": "HITMONLEE", "type": "0101", "exp": "125000", 'bst': [50, 120, 53, 87, 35 ], "Moveset": [136, 70, 68, 116]},
|
||||||
|
{"name": "HITMONCHAN", "type": "0101", "exp": "125000", 'bst': [50, 105, 79, 76, 35 ], "Moveset": [66, 70, 8, 9]},
|
||||||
|
{"name": "LICKITUNG", "type": "0000", "exp": "125000", 'bst': [90, 55, 75, 30, 60 ], "Moveset": [89, 34, 103, 48]},
|
||||||
|
{"name": "KOFFING", "type": "0303", "exp": "125000", 'bst': [40, 65, 95, 35, 60 ], "Moveset": [124, 92, 85, 126]},
|
||||||
|
{"name": "WEEZING", "type": "0303", "exp": "125000", 'bst': [65, 90, 120, 60, 85 ], "Moveset": [124, 63, 114, 108]},
|
||||||
|
{"name": "RHYHORN", "type": "0405", "exp": "156250", 'bst': [80, 85, 95, 25, 30 ], "Moveset": [34, 89, 87, 157]},
|
||||||
|
{"name": "RHYDON", "type": "0405", "exp": "156250", 'bst': [105, 130, 120, 40, 45 ], "Moveset": [70, 91, 57, 164]},
|
||||||
|
{"name": "CHANSEY", "type": "0000", "exp": "100000", 'bst': [250, 5, 5, 50, 105], "Moveset": [58, 87, 38, 115]},
|
||||||
|
{"name": "TANGELA", "type": "1616", "exp": "125000", 'bst': [65, 55, 115, 60, 100], "Moveset": [77, 36, 72, 74]},
|
||||||
|
{"name": "KANGASKHAN", "type": "0000", "exp": "125000", 'bst': [105, 95, 80, 90, 40 ], "Moveset": [146, 157, 43, 85]},
|
||||||
|
{"name": "HORSEA", "type": "1515", "exp": "125000", 'bst': [30, 40, 70, 60, 70 ], "Moveset": [56, 92, 108, 58]},
|
||||||
|
{"name": "SEADRA", "type": "1515", "exp": "125000", 'bst': [55, 65, 95, 85, 95 ], "Moveset": [108, 56, 129, 97]},
|
||||||
|
{"name": "GOLDEEN", "type": "1515", "exp": "125000", 'bst': [45, 67, 60, 63, 50 ], "Moveset": [57, 92, 38, 58]},
|
||||||
|
{"name": "SEAKING", "type": "1515", "exp": "125000", 'bst': [80, 92, 65, 68, 80 ], "Moveset": [127, 59, 48, 30]},
|
||||||
|
{"name": "STARYU", "type": "1515", "exp": "156250", 'bst': [30, 45, 55, 85, 70 ], "Moveset": [85, 105, 57, 94]},
|
||||||
|
{"name": "STARMIE", "type": "1518", "exp": "156250", 'bst': [60, 75, 85, 115, 100], "Moveset": [61, 87, 107, 161]},
|
||||||
|
{"name": "MR. MIME", "type": "1818", "exp": "125000", 'bst': [40, 45, 65, 90, 100], "Moveset": [112, 94, 69, 68]},
|
||||||
|
{"name": "SCYTHER", "type": "0702", "exp": "125000", 'bst': [70, 110, 80, 105, 55 ], "Moveset": [104, 17, 163, 92]},
|
||||||
|
{"name": "JYNX", "type": "1918", "exp": "125000", 'bst': [65, 50, 35, 95, 95 ], "Moveset": [142, 8, 37, 94]},
|
||||||
|
{"name": "ELECTABUZZ", "type": "1717", "exp": "125000", 'bst': [65, 83, 57, 105, 85 ], "Moveset": [9, 148, 86, 69]},
|
||||||
|
{"name": "MAGMAR", "type": "1414", "exp": "125000", 'bst': [65, 95, 57, 93, 85 ], "Moveset": [109, 7, 108, 70]},
|
||||||
|
{"name": "PINSIR", "type": "0707", "exp": "156250", 'bst': [65, 125, 100, 85, 55 ], "Moveset": [163, 102, 106, 12]},
|
||||||
|
{"name": "TAUROS", "type": "0000", "exp": "156250", 'bst': [75, 100, 95, 110, 70 ], "Moveset": [70, 117, 126, 39]},
|
||||||
|
{"name": "MAGIKARP", "type": "1515", "exp": "156250", 'bst': [20, 10, 55, 80, 20 ], "Moveset": [150, 33, 0, 0]},
|
||||||
|
{"name": "GYARADOS", "type": "1502", "exp": "156250", 'bst': [95, 125, 79, 81, 100], "Moveset": [82, 56, 36, 43]},
|
||||||
|
{"name": "LAPRAS", "type": "1519", "exp": "156250", 'bst': [130, 85, 80, 60, 95 ], "Moveset": [109, 47, 58, 61]},
|
||||||
|
{"name": "DITTO", "type": "0000", "exp": "125000", 'bst': [48, 48, 48, 48, 48 ], "Moveset": [144, 0, 0, 0]},
|
||||||
|
{"name": "EEVEE", "type": "0000", "exp": "125000", 'bst': [55, 55, 50, 55, 65 ], "Moveset": [92, 34, 28, 116]},
|
||||||
|
{"name": "VAPOREON", "type": "1515", "exp": "125000", 'bst': [130, 65, 60, 65, 110], "Moveset": [151, 62, 57, 98]},
|
||||||
|
{"name": "JOLTEON", "type": "1717", "exp": "125000", 'bst': [65, 65, 60, 130, 110], "Moveset": [87, 92, 42, 24]},
|
||||||
|
{"name": "FLAREON", "type": "1414", "exp": "125000", 'bst': [65, 130, 60, 65, 110], "Moveset": [126, 28, 92, 38]},
|
||||||
|
{"name": "PORYGON", "type": "0000", "exp": "125000", 'bst': [65, 60, 70, 40, 75 ], "Moveset": [160, 94, 105, 161]},
|
||||||
|
{"name": "OMANYTE", "type": "0515", "exp": "125000", 'bst': [35, 40, 100, 35, 90 ], "Moveset": [59, 57, 38, 104]},
|
||||||
|
{"name": "OMASTAR", "type": "0515", "exp": "125000", 'bst': [70, 60, 125, 55, 115], "Moveset": [56, 131, 43, 110]},
|
||||||
|
{"name": "KABUTO", "type": "0515", "exp": "125000", 'bst': [30, 80, 90, 55, 45 ], "Moveset": [163, 56, 58, 92]},
|
||||||
|
{"name": "KABUTOPS", "type": "0515", "exp": "125000", 'bst': [60, 115, 105, 80, 70 ], "Moveset": [56, 14, 66, 36]},
|
||||||
|
{"name": "AERODACTYL", "type": "0502", "exp": "156250", 'bst': [80, 105, 65, 130, 60 ], "Moveset": [48, 36, 19, 115]},
|
||||||
|
{"name": "SNORLAX", "type": "0000", "exp": "156250", 'bst': [160, 110, 65, 30, 65 ], "Moveset": [87, 29, 156, 117]},
|
||||||
|
{"name": "ARTICUNO", "type": "1902", "exp": "156250", 'bst': [90, 85, 100, 85, 125], "Moveset": [58, 143, 99, 102]},
|
||||||
|
{"name": "ZAPDOS", "type": "1702", "exp": "156250", 'bst': [90, 90, 85, 100, 125], "Moveset": [87, 143, 164, 148]},
|
||||||
|
{"name": "MOLTRES", "type": "1402", "exp": "156250", 'bst': [90, 100, 90, 90, 125], "Moveset": [126, 143, 36, 117]},
|
||||||
|
{"name": "DRATINI", "type": "1A1A", "exp": "156250", 'bst': [41, 64, 45, 50, 50 ], "Moveset": [34, 82, 59, 86]},
|
||||||
|
{"name": "DRAGONAIR", "type": "1A1A", "exp": "156250", 'bst': [61, 84, 65, 70, 70 ], "Moveset": [63, 85, 126, 86]},
|
||||||
|
{"name": "DRAGONITE", "type": "1A02", "exp": "156250", 'bst': [91, 134, 95, 80, 100], "Moveset": [21, 102, 57, 164]},
|
||||||
|
]
|
||||||
|
|
||||||
|
poke_cup_list = [
|
||||||
|
{"name": "BULBASAUR", "type": "1603", "exp": "117360", 'bst': [45, 49, 49, 45, 65 ], "Moveset": [73, 92, 34, 75]},
|
||||||
|
{"name": "IVYSAUR", "type": "1603", "exp": "117360", 'bst': [60, 62, 63, 60, 80 ], "Moveset": [75, 79, 74, 38]},
|
||||||
|
{"name": "VENUSAUR", "type": "1603", "exp": "117360", 'bst': [80, 82, 83, 80, 100], "Moveset": [73, 77, 76, 36]},
|
||||||
|
{"name": "CHARMANDER", "type": "1414", "exp": "117360", 'bst': [39, 52, 43, 65, 50 ], "Moveset": [53, 163, 91, 83]},
|
||||||
|
{"name": "CHARMELEON", "type": "1414", "exp": "117360", 'bst': [58, 64, 58, 80, 65 ], "Moveset": [53, 68, 69, 70]},
|
||||||
|
{"name": "CHARIZARD", "type": "1402", "exp": "117360", 'bst': [78, 84, 78, 100, 85 ], "Moveset": [19, 14, 83, 126]},
|
||||||
|
{"name": "SQUIRTLE", "type": "1515", "exp": "117360", 'bst': [44, 48, 65, 43, 50 ], "Moveset": [57, 59, 34, 91]},
|
||||||
|
{"name": "WARTORTLE", "type": "1515", "exp": "117360", 'bst': [59, 63, 80, 58, 65 ], "Moveset": [57, 70, 156, 58]},
|
||||||
|
{"name": "BLASTOISE", "type": "1515", "exp": "117360", 'bst': [79, 83, 100, 78, 85 ], "Moveset": [56, 130, 110, 69]},
|
||||||
|
{"name": "CATERPIE", "type": "0707", "exp": "125000", 'bst': [45, 30, 35, 45, 20 ], "Moveset": [81, 33, 0, 0]},
|
||||||
|
{"name": "METAPOD", "type": "0707", "exp": "125000", 'bst': [50, 20, 55, 30, 25 ], "Moveset": [81, 33, 0, 0]},
|
||||||
|
{"name": "BUTTERFREE", "type": "0702", "exp": "125000", 'bst': [60, 45, 50, 70, 80 ], "Moveset": [94, 48, 72, 78]},
|
||||||
|
{"name": "WEEDLE", "type": "0703", "exp": "125000", 'bst': [40, 35, 30, 50, 20 ], "Moveset": [81, 40, 0, 0]},
|
||||||
|
{"name": "KAKUNA", "type": "0703", "exp": "125000", 'bst': [45, 25, 50, 35, 25 ], "Moveset": [81, 40, 0, 0]},
|
||||||
|
{"name": "BEEDRILL", "type": "0703", "exp": "125000", 'bst': [65, 80, 40, 75, 45 ], "Moveset": [41, 63, 92, 116]},
|
||||||
|
{"name": "PIDGEY", "type": "0002", "exp": "117360", 'bst': [40, 45, 40, 56, 35 ], "Moveset": [19, 92, 38, 104]},
|
||||||
|
{"name": "PIDGEOTTO", "type": "0002", "exp": "117360", 'bst': [63, 60, 55, 71, 50 ], "Moveset": [19, 98, 28, 36]},
|
||||||
|
{"name": "PIDGEOT", "type": "0002", "exp": "117360", 'bst': [83, 80, 75, 91, 70 ], "Moveset": [119, 19, 98, 28]},
|
||||||
|
{"name": "RATTATA", "type": "0000", "exp": "125000", 'bst': [30, 56, 35, 72, 25 ], "Moveset": [162, 59, 98, 158]},
|
||||||
|
{"name": "RATICATE", "type": "0000", "exp": "125000", 'bst': [55, 81, 60, 97, 50 ], "Moveset": [158, 63, 116, 87]},
|
||||||
|
{"name": "SPEAROW", "type": "0002", "exp": "125000", 'bst': [40, 60, 30, 70, 31 ], "Moveset": [65, 119, 104, 38]},
|
||||||
|
{"name": "FEAROW", "type": "0002", "exp": "125000", 'bst': [65, 90, 65, 100, 61 ], "Moveset": [65, 119, 31, 129]},
|
||||||
|
{"name": "EKANS", "type": "0303", "exp": "125000", 'bst': [35, 60, 44, 55, 40 ], "Moveset": [89, 51, 103, 34]},
|
||||||
|
{"name": "ARBOK", "type": "0303", "exp": "125000", 'bst': [60, 85, 69, 80, 65 ], "Moveset": [137, 35, 91, 70]},
|
||||||
|
{"name": "PIKACHU", "type": "1717", "exp": "125000", 'bst': [35, 55, 30, 90, 50 ], "Moveset": [85, 21, 86, 69]},
|
||||||
|
{"name": "RAICHU", "type": "1717", "exp": "125000", 'bst': [60, 90, 55, 100, 90 ], "Moveset": [87, 86, 148, 25]},
|
||||||
|
{"name": "SANDSHREW", "type": "0404", "exp": "125000", 'bst': [50, 75, 85, 40, 30 ], "Moveset": [89, 163, 69, 28]},
|
||||||
|
{"name": "SANDSLASH", "type": "0404", "exp": "125000", 'bst': [75, 100, 110, 65, 55 ], "Moveset": [91, 129, 69, 28]},
|
||||||
|
{"name": "NIDORAN", "type": "0303", "exp": "117360", 'bst': [55, 47, 52, 41, 40 ], "Moveset": [92, 85, 34, 59]},
|
||||||
|
{"name": "NIDORINA", "type": "0303", "exp": "117360", 'bst': [70, 62, 67, 56, 55 ], "Moveset": [92, 87, 38, 58]},
|
||||||
|
{"name": "NIDOQUEEN", "type": "0304", "exp": "117360", 'bst': [90, 82, 87, 76, 75 ], "Moveset": [92, 24, 44, 89]},
|
||||||
|
{"name": "NIDORAN", "type": "0303", "exp": "117360", 'bst': [46, 57, 40, 50, 40 ], "Moveset": [59, 34, 116, 85]},
|
||||||
|
{"name": "NIDORINO", "type": "0303", "exp": "117360", 'bst': [61, 72, 57, 65, 55 ], "Moveset": [38, 32, 116, 87]},
|
||||||
|
{"name": "NIDOKING", "type": "0304", "exp": "117360", 'bst': [81, 92, 77, 85, 75 ], "Moveset": [89, 32, 99, 164]},
|
||||||
|
{"name": "CLEFAIRY", "type": "0000", "exp": "100000", 'bst': [70, 45, 48, 35, 60 ], "Moveset": [85, 94, 34, 59]},
|
||||||
|
{"name": "CLEFABLE", "type": "0000", "exp": "100000", 'bst': [95, 70, 73, 60, 85 ], "Moveset": [47, 161, 107, 58]},
|
||||||
|
{"name": "VULPIX", "type": "1414", "exp": "125000", 'bst': [38, 41, 40, 65, 65 ], "Moveset": [53, 91, 109, 38]},
|
||||||
|
{"name": "NINETALES", "type": "1414", "exp": "125000", 'bst': [73, 76, 75, 100, 100], "Moveset": [126, 130, 109, 39]},
|
||||||
|
{"name": "JIGGLYPUFF", "type": "0000", "exp": "100000", 'bst': [115, 45, 20, 20, 25 ], "Moveset": [47, 34, 69, 94]},
|
||||||
|
{"name": "WIGGLYTUFF", "type": "0000", "exp": "100000", 'bst': [140, 70, 45, 45, 50 ], "Moveset": [47, 38, 66, 85]},
|
||||||
|
{"name": "ZUBAT", "type": "0302", "exp": "125000", 'bst': [40, 45, 35, 55, 40 ], "Moveset": [109, 72, 92, 38]},
|
||||||
|
{"name": "GOLBAT", "type": "0302", "exp": "125000", 'bst': [75, 80, 70, 90, 75 ], "Moveset": [109, 72, 44, 114]},
|
||||||
|
{"name": "ODDISH", "type": "1603", "exp": "117360", 'bst': [45, 50, 55, 30, 75 ], "Moveset": [80, 92, 72, 38]},
|
||||||
|
{"name": "GLOOM", "type": "1603", "exp": "117360", 'bst': [60, 65, 70, 40, 85 ], "Moveset": [80, 36, 72, 78]},
|
||||||
|
{"name": "VILEPLUME", "type": "1603", "exp": "117360", 'bst': [75, 80, 85, 50, 100], "Moveset": [80, 79, 51, 15]},
|
||||||
|
{"name": "PARAS", "type": "0716", "exp": "125000", 'bst': [35, 70, 55, 25, 55 ], "Moveset": [147, 163, 91, 72]},
|
||||||
|
{"name": "PARASECT", "type": "0716", "exp": "125000", 'bst': [60, 95, 80, 30, 80 ], "Moveset": [147, 36, 91, 76]},
|
||||||
|
{"name": "VENONAT", "type": "0703", "exp": "125000", 'bst': [60, 55, 50, 45, 40 ], "Moveset": [94, 72, 38, 78]},
|
||||||
|
{"name": "VENOMOTH", "type": "0703", "exp": "125000", 'bst': [70, 65, 60, 90, 90 ], "Moveset": [94, 48, 76, 129]},
|
||||||
|
{"name": "DIGLETT", "type": "0404", "exp": "125000", 'bst': [10, 55, 25, 95, 45 ], "Moveset": [89, 163, 28, 157]},
|
||||||
|
{"name": "DUGTRIO", "type": "0404", "exp": "125000", 'bst': [35, 80, 50, 120, 70 ], "Moveset": [91, 28, 92, 63]},
|
||||||
|
{"name": "MEOWTH", "type": "0000", "exp": "125000", 'bst': [40, 45, 35, 90, 40 ], "Moveset": [163, 85, 129, 104]},
|
||||||
|
{"name": "PERSIAN", "type": "0000", "exp": "125000", 'bst': [65, 70, 60, 115, 65 ], "Moveset": [163, 61, 102, 45]},
|
||||||
|
{"name": "PSYDUCK", "type": "1515", "exp": "125000", 'bst': [50, 52, 48, 55, 50 ], "Moveset": [57, 93, 91, 59]},
|
||||||
|
{"name": "GOLDUCK", "type": "1515", "exp": "125000", 'bst': [80, 82, 78, 85, 80 ], "Moveset": [58, 57, 92, 50]},
|
||||||
|
{"name": "MANKEY", "type": "0101", "exp": "125000", 'bst': [40, 80, 35, 70, 35 ], "Moveset": [66, 157, 69, 103]},
|
||||||
|
{"name": "PRIMEAPE", "type": "0101", "exp": "125000", 'bst': [65, 105, 60, 95, 60 ], "Moveset": [154, 157, 67, 103]},
|
||||||
|
{"name": "GROWLITHE", "type": "1414", "exp": "156250", 'bst': [55, 70, 45, 60, 50 ], "Moveset": [53, 34, 115, 91]},
|
||||||
|
{"name": "ARCANINE", "type": "1414", "exp": "156250", 'bst': [90, 110, 80, 95, 80 ], "Moveset": [126, 36, 82, 164]},
|
||||||
|
{"name": "POLIWAG", "type": "1515", "exp": "117360", 'bst': [40, 50, 40, 90, 40 ], "Moveset": [34, 59, 57, 133]},
|
||||||
|
{"name": "POLIWHIRL", "type": "1515", "exp": "117360", 'bst': [65, 65, 65, 90, 50 ], "Moveset": [95, 57, 58, 89]},
|
||||||
|
{"name": "POLIWRATH", "type": "1501", "exp": "117360", 'bst': [90, 85, 95, 70, 70 ], "Moveset": [95, 66, 68, 56]},
|
||||||
|
{"name": "ABRA", "type": "1818", "exp": "117360", 'bst': [25, 20, 15, 90, 105], "Moveset": [94, 69, 115, 86]},
|
||||||
|
{"name": "KADABRA", "type": "1818", "exp": "117360", 'bst': [40, 35, 30, 105, 120], "Moveset": [94, 68, 105, 91]},
|
||||||
|
{"name": "ALAKAZAM", "type": "1818", "exp": "117360", 'bst': [55, 50, 45, 120, 135], "Moveset": [60, 118, 50, 161]},
|
||||||
|
{"name": "MACHOP", "type": "0101", "exp": "117360", 'bst': [70, 80, 50, 35, 35 ], "Moveset": [66, 157, 89, 116]},
|
||||||
|
{"name": "MACHOKE", "type": "0101", "exp": "117360", 'bst': [80, 100, 70, 45, 50 ], "Moveset": [66, 70, 157, 116]},
|
||||||
|
{"name": "MACHAMP", "type": "0101", "exp": "117360", 'bst': [90, 130, 80, 55, 65 ], "Moveset": [67, 70, 68, 116]},
|
||||||
|
{"name": "BELLSPROUT", "type": "1603", "exp": "117360", 'bst': [50, 75, 35, 40, 70 ], "Moveset": [75, 74, 72, 78]},
|
||||||
|
{"name": "WEEPINBELL", "type": "1603", "exp": "117360", 'bst': [65, 90, 50, 55, 85 ], "Moveset": [75, 51, 35, 92]},
|
||||||
|
{"name": "VICTREEBEL", "type": "1603", "exp": "117360", 'bst': [80, 105, 65, 70, 100], "Moveset": [76, 51, 115, 21]},
|
||||||
|
{"name": "TENTACOOL", "type": "1503", "exp": "156250", 'bst': [40, 40, 35, 70, 100], "Moveset": [57, 48, 72, 59]},
|
||||||
|
{"name": "TENTACRUEL", "type": "1503", "exp": "156250", 'bst': [80, 70, 65, 100, 120], "Moveset": [51, 48, 56, 15]},
|
||||||
|
{"name": "GEODUDE", "type": "0504", "exp": "117360", 'bst': [40, 80, 100, 20, 30 ], "Moveset": [89, 69, 157, 153]},
|
||||||
|
{"name": "GRAVELER", "type": "0504", "exp": "117360", 'bst': [55, 95, 115, 35, 45 ], "Moveset": [89, 69, 70, 120]},
|
||||||
|
{"name": "GOLEM", "type": "0504", "exp": "117360", 'bst': [80, 110, 130, 45, 55 ], "Moveset": [91, 69, 126, 118]},
|
||||||
|
{"name": "PONYTA", "type": "1414", "exp": "125000", 'bst': [50, 85, 55, 90, 65 ], "Moveset": [126, 97, 32, 34]},
|
||||||
|
{"name": "RAPIDASH", "type": "1414", "exp": "125000", 'bst': [65, 100, 70, 105, 80 ], "Moveset": [126, 23, 92, 83]},
|
||||||
|
{"name": "SLOWPOKE", "type": "1518", "exp": "125000", 'bst': [90, 65, 65, 15, 40 ], "Moveset": [57, 94, 86, 133]},
|
||||||
|
{"name": "SLOWBRO", "type": "1518", "exp": "125000", 'bst': [95, 75, 110, 30, 80 ], "Moveset": [57, 94, 50, 110]},
|
||||||
|
{"name": "MAGNEMITE", "type": "1717", "exp": "125000", 'bst': [25, 35, 70, 45, 95 ], "Moveset": [85, 86, 48, 38]},
|
||||||
|
{"name": "MAGNETON", "type": "1717", "exp": "125000", 'bst': [50, 60, 95, 70, 120], "Moveset": [87, 103, 48, 129]},
|
||||||
|
{"name": "FARFETCH'D", "type": "0002", "exp": "125000", 'bst': [52, 65, 55, 60, 58 ], "Moveset": [163, 28, 92, 19]},
|
||||||
|
{"name": "DODUO", "type": "0002", "exp": "125000", 'bst': [35, 85, 45, 75, 35 ], "Moveset": [65, 161, 104, 115]},
|
||||||
|
{"name": "DODRIO", "type": "0002", "exp": "125000", 'bst': [60, 110, 70, 100, 60 ], "Moveset": [19, 161, 97, 115]},
|
||||||
|
{"name": "SEEL", "type": "1515", "exp": "125000", 'bst': [65, 45, 55, 45, 70 ], "Moveset": [58, 34, 32, 57]},
|
||||||
|
{"name": "DEWGONG", "type": "1519", "exp": "125000", 'bst': [90, 70, 80, 70, 95 ], "Moveset": [62, 29, 156, 57]},
|
||||||
|
{"name": "GRIMER", "type": "0303", "exp": "125000", 'bst': [80, 80, 50, 25, 40 ], "Moveset": [124, 34, 103, 153]},
|
||||||
|
{"name": "MUK", "type": "0303", "exp": "125000", 'bst': [105, 105, 75, 50, 65 ], "Moveset": [124, 85, 63, 120]},
|
||||||
|
{"name": "SHELLDER", "type": "1515", "exp": "156250", 'bst': [30, 65, 100, 40, 45 ], "Moveset": [57, 153, 59, 161]},
|
||||||
|
{"name": "CLOYSTER", "type": "1519", "exp": "156250", 'bst': [50, 95, 180, 70, 85 ], "Moveset": [128, 131, 58, 48]},
|
||||||
|
{"name": "GASTLY", "type": "0803", "exp": "117360", 'bst': [30, 35, 30, 80, 100], "Moveset": [95, 138, 94, 109]},
|
||||||
|
{"name": "HAUNTER", "type": "0803", "exp": "117360", 'bst': [45, 50, 45, 95, 115], "Moveset": [72, 94, 153, 109]},
|
||||||
|
{"name": "GENGAR", "type": "0803", "exp": "117360", 'bst': [60, 65, 60, 110, 130], "Moveset": [85, 101, 95, 109]},
|
||||||
|
{"name": "ONIX", "type": "0504", "exp": "125000", 'bst': [35, 45, 160, 70, 30 ], "Moveset": [89, 157, 70, 153]},
|
||||||
|
{"name": "DROWZEE", "type": "1818", "exp": "125000", 'bst': [60, 48, 45, 42, 90 ], "Moveset": [95, 138, 94, 161]},
|
||||||
|
{"name": "HYPNO", "type": "1818", "exp": "125000", 'bst': [85, 73, 70, 67, 115], "Moveset": [95, 29, 138, 96]},
|
||||||
|
{"name": "KRABBY", "type": "1515", "exp": "125000", 'bst': [30, 105, 90, 50, 25 ], "Moveset": [152, 12, 38, 59]},
|
||||||
|
{"name": "KINGLER", "type": "1515", "exp": "125000", 'bst': [55, 130, 115, 75, 50 ], "Moveset": [152, 12, 23, 164]},
|
||||||
|
{"name": "VOLTORB", "type": "1717", "exp": "125000", 'bst': [40, 30, 50, 100, 55 ], "Moveset": [85, 86, 129, 153]},
|
||||||
|
{"name": "ELECTRODE", "type": "1717", "exp": "125000", 'bst': [60, 50, 70, 140, 80 ], "Moveset": [87, 86, 129, 120]},
|
||||||
|
{"name": "EXEGGCUTE", "type": "1618", "exp": "156250", 'bst': [60, 40, 80, 40, 60 ], "Moveset": [94, 153, 73, 92]},
|
||||||
|
{"name": "EXEGGUTOR", "type": "1618", "exp": "156250", 'bst': [95, 95, 85, 55, 125], "Moveset": [72, 78, 73, 121]},
|
||||||
|
{"name": "CUBONE", "type": "0404", "exp": "125000", 'bst': [50, 50, 95, 35, 40 ], "Moveset": [89, 66, 59, 70]},
|
||||||
|
{"name": "MAROWAK", "type": "0404", "exp": "125000", 'bst': [60, 80, 110, 45, 50 ], "Moveset": [155, 37, 126, 116]},
|
||||||
|
{"name": "HITMONLEE", "type": "0101", "exp": "125000", 'bst': [50, 120, 53, 87, 35 ], "Moveset": [136, 25, 118, 69]},
|
||||||
|
{"name": "HITMONCHAN", "type": "0101", "exp": "125000", 'bst': [50, 105, 79, 76, 35 ], "Moveset": [66, 9, 8, 70]},
|
||||||
|
{"name": "LICKITUNG", "type": "0000", "exp": "125000", 'bst': [90, 55, 75, 30, 60 ], "Moveset": [70, 59, 87, 126]},
|
||||||
|
{"name": "KOFFING", "type": "0303", "exp": "125000", 'bst': [40, 65, 95, 35, 60 ], "Moveset": [124, 92, 85, 153]},
|
||||||
|
{"name": "WEEZING", "type": "0303", "exp": "125000", 'bst': [65, 90, 120, 60, 85 ], "Moveset": [124, 63, 126, 120]},
|
||||||
|
{"name": "RHYHORN", "type": "0405", "exp": "156250", 'bst': [80, 85, 95, 25, 30 ], "Moveset": [89, 34, 157, 126]},
|
||||||
|
{"name": "RHYDON", "type": "0405", "exp": "156250", 'bst': [105, 130, 120, 40, 45 ], "Moveset": [91, 70, 87, 57]},
|
||||||
|
{"name": "CHANSEY", "type": "0000", "exp": "100000", 'bst': [250, 5, 5, 50, 105], "Moveset": [87, 126, 107, 156]},
|
||||||
|
{"name": "TANGELA", "type": "1616", "exp": "125000", 'bst': [65, 55, 115, 60, 100], "Moveset": [72, 74, 92, 38]},
|
||||||
|
{"name": "KANGASKHAN", "type": "0000", "exp": "125000", 'bst': [105, 95, 80, 90, 40 ], "Moveset": [146, 157, 57, 85]},
|
||||||
|
{"name": "HORSEA", "type": "1515", "exp": "125000", 'bst': [30, 40, 70, 60, 70 ], "Moveset": [56, 92, 108, 58]},
|
||||||
|
{"name": "SEADRA", "type": "1515", "exp": "125000", 'bst': [55, 65, 95, 85, 95 ], "Moveset": [57, 92, 108, 129]},
|
||||||
|
{"name": "GOLDEEN", "type": "1515", "exp": "125000", 'bst': [45, 67, 60, 63, 50 ], "Moveset": [57, 48, 32, 59]},
|
||||||
|
{"name": "SEAKING", "type": "1515", "exp": "125000", 'bst': [80, 92, 65, 68, 80 ], "Moveset": [127, 48, 30, 58]},
|
||||||
|
{"name": "STARYU", "type": "1515", "exp": "156250", 'bst': [30, 45, 55, 85, 70 ], "Moveset": [56, 105, 85, 94]},
|
||||||
|
{"name": "STARMIE", "type": "1518", "exp": "156250", 'bst': [60, 75, 85, 115, 100], "Moveset": [57, 87, 129, 106]},
|
||||||
|
{"name": "MR. MIME", "type": "1818", "exp": "125000", 'bst': [40, 45, 65, 90, 100], "Moveset": [112, 94, 118, 69]},
|
||||||
|
{"name": "SCYTHER", "type": "0702", "exp": "125000", 'bst': [70, 110, 80, 105, 55 ], "Moveset": [163, 17, 43, 104]},
|
||||||
|
{"name": "JYNX", "type": "1918", "exp": "125000", 'bst': [65, 50, 35, 95, 95 ], "Moveset": [8, 5, 94, 142]},
|
||||||
|
{"name": "ELECTABUZZ", "type": "1717", "exp": "125000", 'bst': [65, 83, 57, 105, 85 ], "Moveset": [9, 5, 94, 86]},
|
||||||
|
{"name": "MAGMAR", "type": "1414", "exp": "125000", 'bst': [65, 95, 57, 93, 85 ], "Moveset": [7, 5, 94, 108]},
|
||||||
|
{"name": "PINSIR", "type": "0707", "exp": "156250", 'bst': [65, 125, 100, 85, 55 ], "Moveset": [70, 106, 69, 12]},
|
||||||
|
{"name": "TAUROS", "type": "0000", "exp": "156250", 'bst': [75, 100, 95, 110, 70 ], "Moveset": [38, 126, 39, 117]},
|
||||||
|
{"name": "MAGIKARP", "type": "1515", "exp": "156250", 'bst': [20, 10, 55, 80, 20 ], "Moveset": [150, 33, 0, 0]},
|
||||||
|
{"name": "GYARADOS", "type": "1502", "exp": "156250", 'bst': [95, 125, 79, 81, 100], "Moveset": [57, 82, 44, 126]},
|
||||||
|
{"name": "LAPRAS", "type": "1519", "exp": "156250", 'bst': [130, 85, 80, 60, 95 ], "Moveset": [58, 76, 34, 47]},
|
||||||
|
{"name": "DITTO", "type": "0000", "exp": "125000", 'bst': [48, 48, 48, 48, 48 ], "Moveset": [144, 0, 0, 0]},
|
||||||
|
{"name": "EEVEE", "type": "0000", "exp": "125000", 'bst': [55, 55, 50, 55, 65 ], "Moveset": [34, 129, 28, 92]},
|
||||||
|
{"name": "VAPOREON", "type": "1515", "exp": "125000", 'bst': [130, 65, 60, 65, 110], "Moveset": [57, 98, 28, 151]},
|
||||||
|
{"name": "JOLTEON", "type": "1717", "exp": "125000", 'bst': [65, 65, 60, 130, 110], "Moveset": [85, 42, 92, 28]},
|
||||||
|
{"name": "FLAREON", "type": "1414", "exp": "125000", 'bst': [65, 130, 60, 65, 110], "Moveset": [126, 36, 123, 28]},
|
||||||
|
{"name": "PORYGON", "type": "0000", "exp": "125000", 'bst': [65, 60, 70, 40, 75 ], "Moveset": [161, 94, 159, 160]},
|
||||||
|
{"name": "OMANYTE", "type": "0515", "exp": "125000", 'bst': [35, 40, 100, 35, 90 ], "Moveset": [57, 58, 38, 104]},
|
||||||
|
{"name": "OMASTAR", "type": "0515", "exp": "125000", 'bst': [70, 60, 125, 55, 115], "Moveset": [56, 66, 131, 110]},
|
||||||
|
{"name": "KABUTO", "type": "0515", "exp": "125000", 'bst': [30, 80, 90, 55, 45 ], "Moveset": [56, 59, 163, 104]},
|
||||||
|
{"name": "KABUTOPS", "type": "0515", "exp": "125000", 'bst': [60, 115, 105, 80, 70 ], "Moveset": [57, 14, 25, 66]},
|
||||||
|
{"name": "AERODACTYL", "type": "0502", "exp": "156250", 'bst': [80, 105, 65, 130, 60 ], "Moveset": [19, 63, 48, 82]},
|
||||||
|
{"name": "SNORLAX", "type": "0000", "exp": "156250", 'bst': [160, 110, 65, 30, 65 ], "Moveset": [25, 157, 118, 156]},
|
||||||
|
{"name": "ARTICUNO", "type": "1902", "exp": "156250", 'bst': [90, 85, 100, 85, 125], "Moveset": [58, 143, 13, 164]},
|
||||||
|
{"name": "ZAPDOS", "type": "1702", "exp": "156250", 'bst': [90, 90, 85, 100, 125], "Moveset": [85, 143, 86, 148]},
|
||||||
|
{"name": "MOLTRES", "type": "1402", "exp": "156250", 'bst': [90, 100, 90, 90, 125], "Moveset": [126, 19, 129, 164]},
|
||||||
|
{"name": "DRATINI", "type": "1A1A", "exp": "156250", 'bst': [41, 64, 45, 50, 50 ], "Moveset": [63, 34, 85, 86]},
|
||||||
|
{"name": "DRAGONAIR", "type": "1A1A", "exp": "156250", 'bst': [61, 84, 65, 70, 70 ], "Moveset": [63, 129, 58, 86]},
|
||||||
|
{"name": "DRAGONITE", "type": "1A02", "exp": "156250", 'bst': [91, 134, 95, 80, 100], "Moveset": [21, 82, 87, 97]},
|
||||||
|
]
|
||||||
|
prime_cup_list = [
|
||||||
|
{"name": "BULBASAUR", "type": "1603", "exp": "1059860", 'bst': [45, 49, 49, 45, 65 ], "Moveset": [73, 75, 74, 34]},
|
||||||
|
{"name": "IVYSAUR", "type": "1603", "exp": "1059860", 'bst': [60, 62, 63, 60, 80 ], "Moveset": [73, 75, 74, 72]},
|
||||||
|
{"name": "VENUSAUR", "type": "1603", "exp": "1059860", 'bst': [80, 82, 83, 80, 100], "Moveset": [73, 76, 74, 79]},
|
||||||
|
{"name": "CHARMANDER", "type": "1414", "exp": "1059860", 'bst': [39, 52, 43, 65, 50 ], "Moveset": [53, 34, 69, 91]},
|
||||||
|
{"name": "CHARMELEON", "type": "1414", "exp": "1059860", 'bst': [58, 64, 58, 80, 65 ], "Moveset": [53, 163, 91, 66]},
|
||||||
|
{"name": "CHARIZARD", "type": "1402", "exp": "1059860", 'bst': [78, 84, 78, 100, 85 ], "Moveset": [126, 19, 83, 14]},
|
||||||
|
{"name": "SQUIRTLE", "type": "1515", "exp": "1059860", 'bst': [44, 48, 65, 43, 50 ], "Moveset": [56, 59, 34, 91]},
|
||||||
|
{"name": "WARTORTLE", "type": "1515", "exp": "1059860", 'bst': [59, 63, 80, 58, 65 ], "Moveset": [57, 69, 91, 92]},
|
||||||
|
{"name": "BLASTOISE", "type": "1515", "exp": "1059860", 'bst': [79, 83, 100, 78, 85 ], "Moveset": [56, 130, 110, 39]},
|
||||||
|
{"name": "CATERPIE", "type": "0707", "exp": "1000000", 'bst': [45, 30, 35, 45, 20 ], "Moveset": [33, 81, 0, 0]},
|
||||||
|
{"name": "METAPOD", "type": "0707", "exp": "1000000", 'bst': [50, 20, 55, 30, 25 ], "Moveset": [33, 81, 0, 0]},
|
||||||
|
{"name": "BUTTERFREE", "type": "0702", "exp": "1000000", 'bst': [60, 45, 50, 70, 80 ], "Moveset": [94, 72, 129, 78]},
|
||||||
|
{"name": "WEEDLE", "type": "0703", "exp": "1000000", 'bst': [40, 35, 30, 50, 20 ], "Moveset": [40, 81, 0, 0]},
|
||||||
|
{"name": "KAKUNA", "type": "0703", "exp": "1000000", 'bst': [45, 25, 50, 35, 25 ], "Moveset": [40, 81, 0, 0]},
|
||||||
|
{"name": "BEEDRILL", "type": "0703", "exp": "1000000", 'bst': [65, 80, 40, 75, 45 ], "Moveset": [41, 63, 72, 116]},
|
||||||
|
{"name": "PIDGEY", "type": "0002", "exp": "1059860", 'bst': [40, 45, 40, 56, 35 ], "Moveset": [19, 28, 119, 18]},
|
||||||
|
{"name": "PIDGEOTTO", "type": "0002", "exp": "1059860", 'bst': [63, 60, 55, 71, 50 ], "Moveset": [19, 28, 129, 92]},
|
||||||
|
{"name": "PIDGEOT", "type": "0002", "exp": "1059860", 'bst': [83, 80, 75, 91, 70 ], "Moveset": [98, 119, 28, 19]},
|
||||||
|
{"name": "RATTATA", "type": "0000", "exp": "1000000", 'bst': [30, 56, 35, 72, 25 ], "Moveset": [162, 34, 91, 92]},
|
||||||
|
{"name": "RATICATE", "type": "0000", "exp": "1000000", 'bst': [55, 81, 60, 97, 50 ], "Moveset": [162, 158, 98, 92]},
|
||||||
|
{"name": "SPEAROW", "type": "0002", "exp": "1000000", 'bst': [40, 60, 30, 70, 31 ], "Moveset": [65, 129, 104, 19]},
|
||||||
|
{"name": "FEAROW", "type": "0002", "exp": "1000000", 'bst': [65, 90, 65, 100, 61 ], "Moveset": [65, 119, 63, 45]},
|
||||||
|
{"name": "EKANS", "type": "0303", "exp": "1000000", 'bst': [35, 60, 44, 55, 40 ], "Moveset": [38, 137, 89, 72]},
|
||||||
|
{"name": "ARBOK", "type": "0303", "exp": "1000000", 'bst': [60, 85, 69, 80, 65 ], "Moveset": [91, 137, 70, 51]},
|
||||||
|
{"name": "PIKACHU", "type": "1717", "exp": "1000000", 'bst': [35, 55, 30, 90, 50 ], "Moveset": [85, 86, 129, 115]},
|
||||||
|
{"name": "RAICHU", "type": "1717", "exp": "1000000", 'bst': [60, 90, 55, 100, 90 ], "Moveset": [87, 86, 98, 25]},
|
||||||
|
{"name": "SANDSHREW", "type": "0404", "exp": "1000000", 'bst': [50, 75, 85, 40, 30 ], "Moveset": [28, 89, 163, 157]},
|
||||||
|
{"name": "SANDSLASH", "type": "0404", "exp": "1000000", 'bst': [75, 100, 110, 65, 55 ], "Moveset": [28, 91, 70, 157]},
|
||||||
|
{"name": "NIDORAN", "type": "0303", "exp": "1059860", 'bst': [55, 47, 52, 41, 40 ], "Moveset": [34, 59, 85, 92]},
|
||||||
|
{"name": "NIDORINA", "type": "0303", "exp": "1059860", 'bst': [70, 62, 67, 56, 55 ], "Moveset": [34, 61, 87, 92]},
|
||||||
|
{"name": "NIDOQUEEN", "type": "0304", "exp": "1059860", 'bst': [90, 82, 87, 76, 75 ], "Moveset": [89, 24, 157, 92]},
|
||||||
|
{"name": "NIDORAN", "type": "0303", "exp": "1059860", 'bst': [46, 57, 40, 50, 40 ], "Moveset": [34, 59, 87, 32]},
|
||||||
|
{"name": "NIDORINO", "type": "0303", "exp": "1059860", 'bst': [61, 72, 57, 65, 55 ], "Moveset": [34, 85, 58, 32]},
|
||||||
|
{"name": "NIDOKING", "type": "0304", "exp": "1059860", 'bst': [81, 92, 77, 85, 75 ], "Moveset": [30, 89, 117, 32]},
|
||||||
|
{"name": "CLEFAIRY", "type": "0000", "exp": "800000", 'bst': [70, 45, 48, 35, 60 ], "Moveset": [118, 34, 86, 59]},
|
||||||
|
{"name": "CLEFABLE", "type": "0000", "exp": "800000", 'bst': [95, 70, 73, 60, 85 ], "Moveset": [118, 70, 86, 87]},
|
||||||
|
{"name": "VULPIX", "type": "1414", "exp": "1000000", 'bst': [38, 41, 40, 65, 65 ], "Moveset": [53, 91, 109, 92]},
|
||||||
|
{"name": "NINETALES", "type": "1414", "exp": "1000000", 'bst': [73, 76, 75, 100, 100], "Moveset": [126, 98, 109, 39]},
|
||||||
|
{"name": "JIGGLYPUFF", "type": "0000", "exp": "800000", 'bst': [115, 45, 20, 20, 25 ], "Moveset": [47, 148, 34, 69]},
|
||||||
|
{"name": "WIGGLYTUFF", "type": "0000", "exp": "800000", 'bst': [140, 70, 45, 45, 50 ], "Moveset": [47, 50, 70, 63]},
|
||||||
|
{"name": "ZUBAT", "type": "0302", "exp": "1000000", 'bst': [40, 45, 35, 55, 40 ], "Moveset": [109, 129, 72, 114]},
|
||||||
|
{"name": "GOLBAT", "type": "0302", "exp": "1000000", 'bst': [75, 80, 70, 90, 75 ], "Moveset": [48, 63, 72, 114]},
|
||||||
|
{"name": "ODDISH", "type": "1603", "exp": "1059860", 'bst': [45, 50, 55, 30, 75 ], "Moveset": [80, 72, 78, 38]},
|
||||||
|
{"name": "GLOOM", "type": "1603", "exp": "1059860", 'bst': [60, 65, 70, 40, 85 ], "Moveset": [80, 72, 78, 51]},
|
||||||
|
{"name": "VILEPLUME", "type": "1603", "exp": "1059860", 'bst': [75, 80, 85, 50, 100], "Moveset": [76, 72, 78, 51]},
|
||||||
|
{"name": "PARAS", "type": "0716", "exp": "1000000", 'bst': [35, 70, 55, 25, 55 ], "Moveset": [163, 147, 91, 72]},
|
||||||
|
{"name": "PARASECT", "type": "0716", "exp": "1000000", 'bst': [60, 95, 80, 30, 80 ], "Moveset": [163, 147, 74, 72]},
|
||||||
|
{"name": "VENONAT", "type": "0703", "exp": "1000000", 'bst': [60, 55, 50, 45, 40 ], "Moveset": [94, 72, 38, 92]},
|
||||||
|
{"name": "VENOMOTH", "type": "0703", "exp": "1000000", 'bst': [70, 65, 60, 90, 90 ], "Moveset": [94, 72, 79, 148]},
|
||||||
|
{"name": "DIGLETT", "type": "0404", "exp": "1000000", 'bst': [10, 55, 25, 95, 45 ], "Moveset": [89, 90, 163, 28]},
|
||||||
|
{"name": "DUGTRIO", "type": "0404", "exp": "1000000", 'bst': [35, 80, 50, 120, 70 ], "Moveset": [91, 157, 45, 28]},
|
||||||
|
{"name": "MEOWTH", "type": "0000", "exp": "1000000", 'bst': [40, 45, 35, 90, 40 ], "Moveset": [61, 103, 163, 85]},
|
||||||
|
{"name": "PERSIAN", "type": "0000", "exp": "1000000", 'bst': [65, 70, 60, 115, 65 ], "Moveset": [63, 103, 44, 87]},
|
||||||
|
{"name": "PSYDUCK", "type": "1515", "exp": "1000000", 'bst': [50, 52, 48, 55, 50 ], "Moveset": [56, 59, 91, 50]},
|
||||||
|
{"name": "GOLDUCK", "type": "1515", "exp": "1000000", 'bst': [80, 82, 78, 85, 80 ], "Moveset": [61, 58, 93, 50]},
|
||||||
|
{"name": "MANKEY", "type": "0101", "exp": "1000000", 'bst': [40, 80, 35, 70, 35 ], "Moveset": [66, 37, 91, 68]},
|
||||||
|
{"name": "PRIMEAPE", "type": "0101", "exp": "1000000", 'bst': [65, 105, 60, 95, 60 ], "Moveset": [67, 37, 69, 68]},
|
||||||
|
{"name": "GROWLITHE", "type": "1414", "exp": "1250000", 'bst': [55, 70, 45, 60, 50 ], "Moveset": [53, 91, 34, 104]},
|
||||||
|
{"name": "ARCANINE", "type": "1414", "exp": "1250000", 'bst': [90, 110, 80, 95, 80 ], "Moveset": [126, 91, 43, 97]},
|
||||||
|
{"name": "POLIWAG", "type": "1515", "exp": "1059860", 'bst': [40, 50, 40, 90, 40 ], "Moveset": [56, 59, 94, 133]},
|
||||||
|
{"name": "POLIWHIRL", "type": "1515", "exp": "1059860", 'bst': [65, 65, 65, 90, 50 ], "Moveset": [57, 58, 94, 133]},
|
||||||
|
{"name": "POLIWRATH", "type": "1501", "exp": "1059860", 'bst': [90, 85, 95, 70, 70 ], "Moveset": [61, 66, 95, 133]},
|
||||||
|
{"name": "ABRA", "type": "1818", "exp": "1059860", 'bst': [25, 20, 15, 90, 105], "Moveset": [94, 86, 104, 34]},
|
||||||
|
{"name": "KADABRA", "type": "1818", "exp": "1059860", 'bst': [40, 35, 30, 105, 120], "Moveset": [94, 105, 115, 91]},
|
||||||
|
{"name": "ALAKAZAM", "type": "1818", "exp": "1059860", 'bst': [55, 50, 45, 120, 135], "Moveset": [60, 134, 115, 63]},
|
||||||
|
{"name": "MACHOP", "type": "0101", "exp": "1059860", 'bst': [70, 80, 50, 35, 35 ], "Moveset": [66, 34, 69, 116]},
|
||||||
|
{"name": "MACHOKE", "type": "0101", "exp": "1059860", 'bst': [80, 100, 70, 45, 50 ], "Moveset": [66, 91, 69, 116]},
|
||||||
|
{"name": "MACHAMP", "type": "0101", "exp": "1059860", 'bst': [90, 130, 80, 55, 65 ], "Moveset": [67, 5, 43, 116]},
|
||||||
|
{"name": "BELLSPROUT", "type": "1603", "exp": "1059860", 'bst': [50, 75, 35, 40, 70 ], "Moveset": [75, 92, 35, 38]},
|
||||||
|
{"name": "WEEPINBELL", "type": "1603", "exp": "1059860", 'bst': [65, 90, 50, 55, 85 ], "Moveset": [75, 72, 74, 78]},
|
||||||
|
{"name": "VICTREEBEL", "type": "1603", "exp": "1059860", 'bst': [80, 105, 65, 70, 100], "Moveset": [75, 51, 35, 79]},
|
||||||
|
{"name": "TENTACOOL", "type": "1503", "exp": "1250000", 'bst': [40, 40, 35, 70, 100], "Moveset": [57, 59, 72, 92]},
|
||||||
|
{"name": "TENTACRUEL", "type": "1503", "exp": "1250000", 'bst': [80, 70, 65, 100, 120], "Moveset": [61, 35, 103, 92]},
|
||||||
|
{"name": "GEODUDE", "type": "0504", "exp": "1059860", 'bst': [40, 80, 100, 20, 30 ], "Moveset": [157, 89, 69, 126]},
|
||||||
|
{"name": "GRAVELER", "type": "0504", "exp": "1059860", 'bst': [55, 95, 115, 35, 45 ], "Moveset": [157, 89, 126, 118]},
|
||||||
|
{"name": "GOLEM", "type": "0504", "exp": "1059860", 'bst': [80, 110, 130, 45, 55 ], "Moveset": [88, 91, 111, 126]},
|
||||||
|
{"name": "PONYTA", "type": "1414", "exp": "1000000", 'bst': [50, 85, 55, 90, 65 ], "Moveset": [83, 97, 32, 92]},
|
||||||
|
{"name": "RAPIDASH", "type": "1414", "exp": "1000000", 'bst': [65, 100, 70, 105, 80 ], "Moveset": [126, 23, 115, 39]},
|
||||||
|
{"name": "SLOWPOKE", "type": "1518", "exp": "1000000", 'bst': [90, 65, 65, 15, 40 ], "Moveset": [57, 94, 133, 86]},
|
||||||
|
{"name": "SLOWBRO", "type": "1518", "exp": "1000000", 'bst': [95, 75, 110, 30, 80 ], "Moveset": [57, 94, 50, 5]},
|
||||||
|
{"name": "MAGNEMITE", "type": "1717", "exp": "1000000", 'bst': [25, 35, 70, 45, 95 ], "Moveset": [85, 86, 129, 148]},
|
||||||
|
{"name": "MAGNETON", "type": "1717", "exp": "1000000", 'bst': [50, 60, 95, 70, 120], "Moveset": [87, 86, 48, 148]},
|
||||||
|
{"name": "FARFETCH'D", "type": "0002", "exp": "1000000", 'bst': [52, 65, 55, 60, 58 ], "Moveset": [163, 28, 19, 92]},
|
||||||
|
{"name": "DODUO", "type": "0002", "exp": "1000000", 'bst': [35, 85, 45, 75, 35 ], "Moveset": [65, 34, 115, 104]},
|
||||||
|
{"name": "DODRIO", "type": "0002", "exp": "1000000", 'bst': [60, 110, 70, 100, 60 ], "Moveset": [161, 19, 45, 97]},
|
||||||
|
{"name": "SEEL", "type": "1515", "exp": "1000000", 'bst': [65, 45, 55, 45, 70 ], "Moveset": [57, 59, 34, 104]},
|
||||||
|
{"name": "DEWGONG", "type": "1519", "exp": "1000000", 'bst': [90, 70, 80, 70, 95 ], "Moveset": [62, 57, 29, 32]},
|
||||||
|
{"name": "GRIMER", "type": "0303", "exp": "1000000", 'bst': [80, 80, 50, 25, 40 ], "Moveset": [124, 34, 85, 151]},
|
||||||
|
{"name": "MUK", "type": "0303", "exp": "1000000", 'bst': [105, 105, 75, 50, 65 ], "Moveset": [124, 126, 103, 151]},
|
||||||
|
{"name": "SHELLDER", "type": "1515", "exp": "1250000", 'bst': [30, 65, 100, 40, 45 ], "Moveset": [59, 57, 129, 48]},
|
||||||
|
{"name": "CLOYSTER", "type": "1519", "exp": "1250000", 'bst': [50, 95, 180, 70, 85 ], "Moveset": [58, 61, 128, 48]},
|
||||||
|
{"name": "GASTLY", "type": "0803", "exp": "1059860", 'bst': [30, 35, 30, 80, 100], "Moveset": [95, 94, 109, 101]},
|
||||||
|
{"name": "HAUNTER", "type": "0803", "exp": "1059860", 'bst': [45, 50, 45, 95, 115], "Moveset": [95, 138, 109, 94]},
|
||||||
|
{"name": "GENGAR", "type": "0803", "exp": "1059860", 'bst': [60, 65, 60, 110, 130], "Moveset": [95, 138, 118, 101]},
|
||||||
|
{"name": "ONIX", "type": "0504", "exp": "1000000", 'bst': [35, 45, 160, 70, 30 ], "Moveset": [157, 89, 90, 120]},
|
||||||
|
{"name": "DROWZEE", "type": "1818", "exp": "1000000", 'bst': [60, 48, 45, 42, 90 ], "Moveset": [95, 138, 69, 94]},
|
||||||
|
{"name": "HYPNO", "type": "1818", "exp": "1000000", 'bst': [85, 73, 70, 67, 115], "Moveset": [95, 139, 29, 94]},
|
||||||
|
{"name": "KRABBY", "type": "1515", "exp": "1000000", 'bst': [30, 105, 90, 50, 25 ], "Moveset": [57, 34, 12, 59]},
|
||||||
|
{"name": "KINGLER", "type": "1515", "exp": "1000000", 'bst': [55, 130, 115, 75, 50 ], "Moveset": [152, 70, 12, 92]},
|
||||||
|
{"name": "VOLTORB", "type": "1717", "exp": "1000000", 'bst': [40, 30, 50, 100, 55 ], "Moveset": [85, 86, 36, 115]},
|
||||||
|
{"name": "ELECTRODE", "type": "1717", "exp": "1000000", 'bst': [60, 50, 70, 140, 80 ], "Moveset": [87, 86, 129, 148]},
|
||||||
|
{"name": "EXEGGCUTE", "type": "1618", "exp": "1250000", 'bst': [60, 40, 80, 40, 60 ], "Moveset": [73, 92, 94, 120]},
|
||||||
|
{"name": "EXEGGUTOR", "type": "1618", "exp": "1250000", 'bst': [95, 95, 85, 55, 125], "Moveset": [23, 79, 94, 76]},
|
||||||
|
{"name": "CUBONE", "type": "0404", "exp": "1000000", 'bst': [50, 50, 95, 35, 40 ], "Moveset": [155, 59, 37, 116]},
|
||||||
|
{"name": "MAROWAK", "type": "0404", "exp": "1000000", 'bst': [60, 80, 110, 45, 50 ], "Moveset": [125, 29, 37, 116]},
|
||||||
|
{"name": "HITMONLEE", "type": "0101", "exp": "1000000", 'bst': [50, 120, 53, 87, 35 ], "Moveset": [27, 26, 136, 116]},
|
||||||
|
{"name": "HITMONCHAN", "type": "0101", "exp": "1000000", 'bst': [50, 105, 79, 76, 35 ], "Moveset": [5, 7, 8, 9]},
|
||||||
|
{"name": "LICKITUNG", "type": "0000", "exp": "1000000", 'bst': [90, 55, 75, 30, 60 ], "Moveset": [34, 87, 89, 59]},
|
||||||
|
{"name": "KOFFING", "type": "0303", "exp": "1000000", 'bst': [40, 65, 95, 35, 60 ], "Moveset": [124, 87, 114, 92]},
|
||||||
|
{"name": "WEEZING", "type": "0303", "exp": "1000000", 'bst': [65, 90, 120, 60, 85 ], "Moveset": [124, 87, 114, 102]},
|
||||||
|
{"name": "RHYHORN", "type": "0405", "exp": "1250000", 'bst': [80, 85, 95, 25, 30 ], "Moveset": [34, 89, 157, 90]},
|
||||||
|
{"name": "RHYDON", "type": "0405", "exp": "1250000", 'bst': [105, 130, 120, 40, 45 ], "Moveset": [30, 89, 87, 90]},
|
||||||
|
{"name": "CHANSEY", "type": "0000", "exp": "800000", 'bst': [250, 5, 5, 50, 105], "Moveset": [121, 156, 118, 69]},
|
||||||
|
{"name": "TANGELA", "type": "1616", "exp": "1000000", 'bst': [65, 55, 115, 60, 100], "Moveset": [72, 76, 74, 78]},
|
||||||
|
{"name": "KANGASKHAN", "type": "0000", "exp": "1000000", 'bst': [105, 95, 80, 90, 40 ], "Moveset": [146, 157, 57, 164]},
|
||||||
|
{"name": "HORSEA", "type": "1515", "exp": "1000000", 'bst': [30, 40, 70, 60, 70 ], "Moveset": [56, 58, 92, 108]},
|
||||||
|
{"name": "SEADRA", "type": "1515", "exp": "1000000", 'bst': [55, 65, 95, 85, 95 ], "Moveset": [57, 38, 92, 108]},
|
||||||
|
{"name": "GOLDEEN", "type": "1515", "exp": "1000000", 'bst': [45, 67, 60, 63, 50 ], "Moveset": [57, 32, 104, 97]},
|
||||||
|
{"name": "SEAKING", "type": "1515", "exp": "1000000", 'bst': [80, 92, 65, 68, 80 ], "Moveset": [127, 32, 48, 31]},
|
||||||
|
{"name": "STARYU", "type": "1515", "exp": "1250000", 'bst': [30, 45, 55, 85, 70 ], "Moveset": [57, 94, 107, 105]},
|
||||||
|
{"name": "STARMIE", "type": "1518", "exp": "1250000", 'bst': [60, 75, 85, 115, 100], "Moveset": [61, 87, 107, 129]},
|
||||||
|
{"name": "MR. MIME", "type": "1818", "exp": "1000000", 'bst': [40, 45, 65, 90, 100], "Moveset": [112, 113, 94, 63]},
|
||||||
|
{"name": "SCYTHER", "type": "0702", "exp": "1000000", 'bst': [70, 110, 80, 105, 55 ], "Moveset": [116, 63, 129, 104]},
|
||||||
|
{"name": "JYNX", "type": "1918", "exp": "1000000", 'bst': [65, 50, 35, 95, 95 ], "Moveset": [142, 34, 8, 94]},
|
||||||
|
{"name": "ELECTABUZZ", "type": "1717", "exp": "1000000", 'bst': [65, 83, 57, 105, 85 ], "Moveset": [9, 86, 118, 115]},
|
||||||
|
{"name": "MAGMAR", "type": "1414", "exp": "1000000", 'bst': [65, 95, 57, 93, 85 ], "Moveset": [7, 5, 109, 94]},
|
||||||
|
{"name": "PINSIR", "type": "0707", "exp": "1250000", 'bst': [65, 125, 100, 85, 55 ], "Moveset": [163, 12, 69, 92]},
|
||||||
|
{"name": "TAUROS", "type": "0000", "exp": "1250000", 'bst': [75, 100, 95, 110, 70 ], "Moveset": [23, 130, 117, 126]},
|
||||||
|
{"name": "MAGIKARP", "type": "1515", "exp": "1250000", 'bst': [20, 10, 55, 80, 20 ], "Moveset": [150, 33, 0, 0]},
|
||||||
|
{"name": "GYARADOS", "type": "1502", "exp": "1250000", 'bst': [95, 125, 79, 81, 100], "Moveset": [61, 44, 126, 43]},
|
||||||
|
{"name": "LAPRAS", "type": "1519", "exp": "1250000", 'bst': [130, 85, 80, 60, 95 ], "Moveset": [61, 54, 47, 58]},
|
||||||
|
{"name": "DITTO", "type": "0000", "exp": "1000000", 'bst': [48, 48, 48, 48, 48 ], "Moveset": [144, 0, 0, 0]},
|
||||||
|
{"name": "EEVEE", "type": "0000", "exp": "1000000", 'bst': [55, 55, 50, 55, 65 ], "Moveset": [38, 116, 28, 98]},
|
||||||
|
{"name": "VAPOREON", "type": "1515", "exp": "1000000", 'bst': [130, 65, 60, 65, 110], "Moveset": [56, 151, 114, 98]},
|
||||||
|
{"name": "JOLTEON", "type": "1717", "exp": "1000000", 'bst': [65, 65, 60, 130, 110], "Moveset": [87, 42, 28, 98]},
|
||||||
|
{"name": "FLAREON", "type": "1414", "exp": "1000000", 'bst': [65, 130, 60, 65, 110], "Moveset": [126, 123, 28, 98]},
|
||||||
|
{"name": "PORYGON", "type": "0000", "exp": "1000000", 'bst': [65, 60, 70, 40, 75 ], "Moveset": [60, 161, 160, 105]},
|
||||||
|
{"name": "OMANYTE", "type": "0515", "exp": "1000000", 'bst': [35, 40, 100, 35, 90 ], "Moveset": [56, 34, 58, 92]},
|
||||||
|
{"name": "OMASTAR", "type": "0515", "exp": "1000000", 'bst': [70, 60, 125, 55, 115], "Moveset": [57, 131, 32, 92]},
|
||||||
|
{"name": "KABUTO", "type": "0515", "exp": "1000000", 'bst': [30, 80, 90, 55, 45 ], "Moveset": [57, 59, 163, 104]},
|
||||||
|
{"name": "KABUTOPS", "type": "0515", "exp": "1000000", 'bst': [60, 115, 105, 80, 70 ], "Moveset": [56, 25, 58, 14]},
|
||||||
|
{"name": "AERODACTYL", "type": "0502", "exp": "1250000", 'bst': [80, 105, 65, 130, 60 ], "Moveset": [44, 48, 19, 126]},
|
||||||
|
{"name": "SNORLAX", "type": "0000", "exp": "1250000", 'bst': [160, 110, 65, 30, 65 ], "Moveset": [36, 118, 156, 117]},
|
||||||
|
{"name": "ARTICUNO", "type": "1902", "exp": "1250000", 'bst': [90, 85, 100, 85, 125], "Moveset": [58, 143, 54, 97]},
|
||||||
|
{"name": "ZAPDOS", "type": "1702", "exp": "1250000", 'bst': [90, 90, 85, 100, 125], "Moveset": [87, 143, 117, 148]},
|
||||||
|
{"name": "MOLTRES", "type": "1402", "exp": "1250000", 'bst': [90, 100, 90, 90, 125], "Moveset": [126, 143, 97, 115]},
|
||||||
|
{"name": "DRATINI", "type": "1A1A", "exp": "1250000", 'bst': [41, 64, 45, 50, 50 ], "Moveset": [59, 85, 34, 126]},
|
||||||
|
{"name": "DRAGONAIR", "type": "1A1A", "exp": "1250000", 'bst': [61, 84, 65, 70, 70 ], "Moveset": [85, 34, 58, 126]},
|
||||||
|
{"name": "DRAGONITE", "type": "1A02", "exp": "1250000", 'bst': [91, 134, 95, 80, 100], "Moveset": [87, 35, 21, 126]},
|
||||||
|
]
|
||||||
|
petit_cup_list = [
|
||||||
|
{"name": "BULBASAUR", "type": "1603", "exp": "11735", 'bst': [45, 49, 49, 45, 65 ], "DexNum": 1, "Moveset": [73, 72, 76, 15]},
|
||||||
|
{"name": "CHARMANDER", "type": "1414", "exp": "11735", 'bst': [39, 52, 43, 65, 50 ], "DexNum": 4, "Moveset": [126, 99, 45, 5]},
|
||||||
|
{"name": "SQUIRTLE", "type": "1515", "exp": "11735", 'bst': [44, 48, 65, 43, 50 ], "DexNum": 7, "Moveset": [44, 61, 92, 66]},
|
||||||
|
{"name": "CATERPIE", "type": "0707", "exp": "15625", 'bst': [45, 30, 35, 45, 20 ], "DexNum": 10, "Moveset": [33, 81, 0, 0]},
|
||||||
|
{"name": "WEEDLE", "type": "0703", "exp": "15625", 'bst': [40, 35, 30, 50, 20 ], "DexNum": 13, "Moveset": [40, 81, 0, 0]},
|
||||||
|
{"name": "PIDGEY", "type": "0002", "exp": "11735", 'bst': [40, 45, 40, 56, 35 ], "DexNum": 16, "Moveset": [28, 98, 19, 38]},
|
||||||
|
{"name": "RATTATA", "type": "0000", "exp": "15625", 'bst': [30, 56, 35, 72, 25 ], "DexNum": 19, "Moveset": [98, 158, 61, 91]},
|
||||||
|
{"name": "SPEAROW", "type": "0002", "exp": "15625", 'bst': [40, 60, 30, 70, 31 ], "DexNum": 21, "Moveset": [38, 119, 19, 92]},
|
||||||
|
{"name": "EKANS", "type": "0303", "exp": "15625", 'bst': [35, 60, 44, 55, 40 ], "DexNum": 23, "Moveset": [44, 137, 91, 72]},
|
||||||
|
{"name": "PIKACHU", "type": "1717", "exp": "15625", 'bst': [35, 55, 30, 90, 50 ], "DexNum": 25, "Moveset": [86, 21, 87, 148]},
|
||||||
|
{"name": "SANDSHREW", "type": "0404", "exp": "15625", 'bst': [50, 75, 85, 40, 30 ], "DexNum": 27, "Moveset": [163, 40, 91, 157]},
|
||||||
|
{"name": "NIDORAN", "type": "0303", "exp": "11735", 'bst': [55, 47, 52, 41, 40 ], "DexNum": 29, "Moveset": [24, 59, 36, 92]},
|
||||||
|
{"name": "NIDORAN", "type": "0303", "exp": "11735", 'bst': [46, 57, 40, 50, 40 ], "DexNum": 32, "Moveset": [24, 32, 34, 92]},
|
||||||
|
{"name": "CLEFAIRY", "type": "0000", "exp": "12500", 'bst': [70, 45, 48, 35, 60 ], "DexNum": 35, "Moveset": [47, 126, 161, 118]},
|
||||||
|
{"name": "VULPIX", "type": "1414", "exp": "15625", 'bst': [38, 41, 40, 65, 65 ], "DexNum": 37, "Moveset": [92, 38, 91, 52]},
|
||||||
|
{"name": "JIGGLYPUFF", "type": "0000", "exp": "12500", 'bst': [115, 45, 20, 20, 25 ], "DexNum": 39,"Moveset": [47, 94, 36, 66] },
|
||||||
|
{"name": "ZUBAT", "type": "0302", "exp": "15625", 'bst': [40, 45, 35, 55, 40 ], "DexNum": 41, "Moveset": [109, 38, 92, 72]},
|
||||||
|
{"name": "ODDISH", "type": "1603", "exp": "11735", 'bst': [45, 50, 55, 30, 75 ], "DexNum": 43, "Moveset": [51, 79, 76, 15]},
|
||||||
|
{"name": "PARAS", "type": "0716", "exp": "15625", 'bst': [35, 70, 55, 25, 55 ], "DexNum": 46, "Moveset": [78, 72, 141, 91]},
|
||||||
|
{"name": "DIGLETT", "type": "0404", "exp": "15625", 'bst': [10, 55, 25, 95, 45 ], "DexNum": 50, "Moveset": [91, 28, 15, 157]},
|
||||||
|
{"name": "MEOWTH", "type": "0000", "exp": "15625", 'bst': [40, 45, 35, 90, 40 ], "DexNum": 52, "Moveset": [44, 103, 61, 85]},
|
||||||
|
{"name": "PSYDUCK", "type": "1515", "exp": "15625", 'bst': [50, 52, 48, 55, 50 ], "DexNum": 54, "Moveset": [61, 102, 5, 66]},
|
||||||
|
{"name": "GROWLITHE", "type": "1414", "exp": "19531", 'bst': [55, 70, 45, 60, 50 ], "DexNum": 58, "Moveset": [126, 44, 102, 43]},
|
||||||
|
{"name": "POLIWAG", "type": "1515", "exp": "11735", 'bst': [40, 50, 40, 90, 40 ], "DexNum": 60, "Moveset": [95, 130, 149, 57]},
|
||||||
|
{"name": "ABRA", "type": "1818", "exp": "11735", 'bst': [25, 20, 15, 90, 105], "DexNum": 63, "Moveset": [118, 149, 34, 86]},
|
||||||
|
{"name": "MACHOP", "type": "0101", "exp": "11735", 'bst': [70, 80, 50, 35, 35 ], "DexNum": 66, "Moveset": [2, 67, 69, 126]},
|
||||||
|
{"name": "BELLSPROUT", "type": "1603", "exp": "11735", 'bst': [50, 75, 35, 40, 70 ], "DexNum": 69, "Moveset": [35, 72, 74, 77]},
|
||||||
|
{"name": "GEODUDE", "type": "0504", "exp": "11735", 'bst': [40, 80, 100, 20, 30 ], "DexNum": 74, "Moveset": [88, 120, 91, 70]},
|
||||||
|
{"name": "MAGNEMITE", "type": "1717", "exp": "15625", 'bst': [25, 35, 70, 45, 95 ], "DexNum": 81, "Moveset": [148, 129, 86, 87]},
|
||||||
|
{"name": "FARFETCH'D", "type": "0002", "exp": "15625", 'bst': [52, 65, 55, 60, 58 ], "DexNum": 83, "Moveset": [31, 14, 28, 19]},
|
||||||
|
{"name": "SHELLDER", "type": "1515", "exp": "19531", 'bst': [30, 65, 100, 40, 45 ], "DexNum": 90, "Moveset": [48, 128, 58, 120]},
|
||||||
|
{"name": "GASTLY", "type": "0803", "exp": "11735", 'bst': [30, 35, 30, 80, 100], "DexNum": 92, "Moveset": [109, 101, 87, 72]},
|
||||||
|
{"name": "KRABBY", "type": "1515", "exp": "15625", 'bst': [30, 105, 90, 50, 25 ], "DexNum": 98, "Moveset": [12, 57, 14, 70]},
|
||||||
|
{"name": "VOLTORB", "type": "1717", "exp": "15625", 'bst': [40, 30, 50, 100, 55 ], "DexNum": 100, "Moveset": [103, 86, 87, 36]},
|
||||||
|
{"name": "EXEGGCUTE", "type": "1618", "exp": "19531", 'bst': [60, 40, 80, 40, 60 ], "DexNum": 102, "Moveset": [95, 149, 121, 115]},
|
||||||
|
{"name": "CUBONE", "type": "0404", "exp": "15625", 'bst': [50, 50, 95, 35, 40 ], "DexNum": 104, "Moveset": [125, 39, 126, 29]},
|
||||||
|
{"name": "KOFFING", "type": "0303", "exp": "15625", 'bst': [40, 65, 95, 35, 60 ], "DexNum": 109, "Moveset": [123, 92, 126, 85]},
|
||||||
|
{"name": "HORSEA", "type": "1515", "exp": "15625", 'bst': [30, 40, 70, 60, 70 ], "DexNum": 116, "Moveset": [108, 61, 129, 58]},
|
||||||
|
{"name": "GOLDEEN", "type": "1515", "exp": "15625", 'bst': [45, 67, 60, 63, 50 ], "DexNum": 118, "Moveset": [48, 30, 57, 32]},
|
||||||
|
{"name": "MAGIKARP", "type": "1515", "exp": "19531", 'bst': [20, 10, 55, 80, 20 ], "DexNum": 129, "Moveset": [150, 33, 0, 0]},
|
||||||
|
{"name": "DITTO", "type": "0000", "exp": "15625", 'bst': [48, 48, 48, 48, 48 ], "DexNum": 132, "Moveset": [144, 0, 0, 0]},
|
||||||
|
{"name": "EEVEE", "type": "0000", "exp": "15625", 'bst': [55, 55, 50, 55, 65 ], "DexNum": 133, "Moveset": [28, 98, 38, 164]},
|
||||||
|
{"name": "OMANYTE", "type": "0515", "exp": "15625", 'bst': [35, 40, 100, 35, 90 ], "DexNum": 138, "Moveset": [110, 61, 38, 92]},
|
||||||
|
{"name": "KABUTO", "type": "0515", "exp": "15625", 'bst': [30, 80, 90, 55, 45 ], "DexNum": 140, "Moveset": [58, 36, 57, 117]},
|
||||||
|
{"name": "DRATINI", "type": "1A1A", "exp": "19531", 'bst': [41, 64, 45, 50, 50 ], "DexNum": 147, "Moveset": [86, 35, 87, 126]},
|
||||||
|
]
|
||||||
|
|
||||||
|
pika_cup_list = [
|
||||||
|
{"name": "BULBASAUR", "type": "1603", "exp": "2035", 'bst': [45, 49, 49, 45, 65 ], "DexNum": 1, "Moveset": [73, 92, 72, 38]},
|
||||||
|
{"name": "IVYSAUR", "type": "1603", "exp": "2035", 'bst': [60, 62, 63, 60, 80 ], "DexNum": 2, "Moveset": [14, 34, 76, 73]},
|
||||||
|
{"name": "CHARMANDER", "type": "1414", "exp": "2035", 'bst': [39, 52, 43, 65, 50 ], "DexNum": 4, "Moveset": [126, 69, 70, 45]},
|
||||||
|
{"name": "CHARMELEON", "type": "1414", "exp": "2035", 'bst': [58, 64, 58, 80, 65 ], "DexNum": 5, "Moveset": [14, 25, 92, 52]},
|
||||||
|
{"name": "SQUIRTLE", "type": "1515", "exp": "2035", 'bst': [44, 48, 65, 43, 50 ], "DexNum": 7, "Moveset": [33, 91, 57, 59]},
|
||||||
|
{"name": "WARTORTLE", "type": "1515", "exp": "2035", 'bst': [59, 63, 80, 58, 65 ], "DexNum": 8, "Moveset": [34, 117, 57, 115]},
|
||||||
|
{"name": "CATERPIE", "type": "0707", "exp": "3375", 'bst': [45, 30, 35, 45, 20 ], "DexNum": 10, "Moveset": [81, 33, 0, 0]},
|
||||||
|
{"name": "METAPOD", "type": "0707", "exp": "3375", 'bst': [50, 20, 55, 30, 25 ], "DexNum": 11, "Moveset": [33, 81, 0, 0]},
|
||||||
|
{"name": "BUTTERFREE", "type": "0702", "exp": "3375", 'bst': [60, 45, 50, 70, 80 ], "DexNum": 12, "Moveset": [77, 63, 149, 36]},
|
||||||
|
{"name": "WEEDLE", "type": "0703", "exp": "3375", 'bst': [40, 35, 30, 50, 20 ], "DexNum": 13, "Moveset": [81, 40, 0, 0]},
|
||||||
|
{"name": "KAKUNA", "type": "0703", "exp": "3375", 'bst': [45, 25, 50, 35, 25 ], "DexNum": 14, "Moveset": [81, 40, 0, 0]},
|
||||||
|
{"name": "BEEDRILL", "type": "0703", "exp": "3375", 'bst': [65, 80, 40, 75, 45 ], "DexNum": 15, "Moveset": [31, 104, 14, 63]},
|
||||||
|
{"name": "PIDGEY", "type": "0002", "exp": "2035", 'bst': [40, 45, 40, 56, 35 ], "DexNum": 16, "Moveset": [115, 19, 92, 38]},
|
||||||
|
{"name": "PIDGEOTTO", "type": "0002", "exp": "2035", 'bst': [63, 60, 55, 71, 50 ], "DexNum": 17, "Moveset": [143, 36, 98, 28]},
|
||||||
|
{"name": "RATTATA", "type": "0000", "exp": "3375", 'bst': [30, 56, 35, 72, 25 ], "DexNum": 19, "Moveset": [87, 98, 59, 91]},
|
||||||
|
{"name": "RATICATE", "type": "0000", "exp": "3375", 'bst': [55, 81, 60, 97, 50 ], "DexNum": 20, "Moveset": [158, 92, 58, 129]},
|
||||||
|
{"name": "SPEAROW", "type": "0002", "exp": "3375", 'bst': [40, 60, 30, 70, 31 ], "DexNum": 21, "Moveset": [38, 104, 19, 102]},
|
||||||
|
{"name": "FEAROW", "type": "0002", "exp": "3375", 'bst': [65, 90, 65, 100, 61 ], "DexNum": 22, "Moveset": [19, 104, 64, 102]},
|
||||||
|
{"name": "EKANS", "type": "0303", "exp": "3375", 'bst': [35, 60, 44, 55, 40 ], "DexNum": 23, "Moveset": [35, 40, 89, 43]},
|
||||||
|
{"name": "PIKACHU", "type": "1717", "exp": "3375", 'bst': [35, 55, 30, 90, 50 ], "DexNum": 25, "Moveset": [98, 66, 85, 86]},
|
||||||
|
{"name": "RAICHU", "type": "1717", "exp": "3375", 'bst': [60, 90, 55, 100, 90 ], "DexNum": 26, "Moveset": [87, 86, 69, 45]},
|
||||||
|
{"name": "SANDSHREW", "type": "0404", "exp": "3375", 'bst': [50, 75, 85, 40, 30 ], "DexNum": 27, "Moveset": [28, 89, 66, 14]},
|
||||||
|
{"name": "NIDORAN", "type": "0303", "exp": "2035", 'bst': [55, 47, 52, 41, 40 ], "DexNum": 29, "Moveset": [92, 87, 59, 34]},
|
||||||
|
{"name": "NIDORINA", "type": "0303", "exp": "2035", 'bst': [70, 62, 67, 56, 55 ], "DexNum": 30, "Moveset": [92, 58, 36, 32]},
|
||||||
|
{"name": "NIDOQUEEN", "type": "0304", "exp": "2035", 'bst': [90, 82, 87, 76, 75 ], "DexNum": 31, "Moveset": [90, 24, 57, 115]},
|
||||||
|
{"name": "NIDORAN", "type": "0303", "exp": "2035", 'bst': [46, 57, 40, 50, 40 ], "DexNum": 32, "Moveset": [59, 85, 34, 92]},
|
||||||
|
{"name": "NIDORINO", "type": "0303", "exp": "2035", 'bst': [61, 72, 57, 65, 55 ], "DexNum": 33, "Moveset": [32, 58, 24, 30]},
|
||||||
|
{"name": "NIDOKING", "type": "0304", "exp": "2035", 'bst': [81, 92, 77, 85, 75 ], "DexNum": 34, "Moveset": [40, 89, 61, 24]},
|
||||||
|
{"name": "CLEFAIRY", "type": "0000", "exp": "2700", 'bst': [70, 45, 48, 35, 60 ], "DexNum": 35, "Moveset": [86, 161, 94, 118]},
|
||||||
|
{"name": "CLEFABLE", "type": "0000", "exp": "2700", 'bst': [95, 70, 73, 60, 85 ], "DexNum": 36, "Moveset": [118, 161, 47, 104]},
|
||||||
|
{"name": "VULPIX", "type": "1414", "exp": "3375", 'bst': [38, 41, 40, 65, 65 ], "DexNum": 37, "Moveset": [38, 126, 91, 104]},
|
||||||
|
{"name": "NINETALES", "type": "1414", "exp": "3375", 'bst': [73, 76, 75, 100, 100], "DexNum": 38, "Moveset": [91, 52, 63, 115]},
|
||||||
|
{"name": "JIGGLYPUFF", "type": "0000", "exp": "2700", 'bst': [115, 45, 20, 20, 25 ], "DexNum": 39, "Moveset": [47, 34, 86, 58]},
|
||||||
|
{"name": "WIGGLYTUFF", "type": "0000", "exp": "2700", 'bst': [140, 70, 45, 45, 50 ], "DexNum": 40, "Moveset": [87, 5, 47, 104]},
|
||||||
|
{"name": "ZUBAT", "type": "0302", "exp": "3375", 'bst': [40, 45, 35, 55, 40 ], "DexNum": 41, "Moveset": [48, 129, 72, 92]},
|
||||||
|
{"name": "ODDISH", "type": "1603", "exp": "2035", 'bst': [45, 50, 55, 30, 75 ], "DexNum": 43, "Moveset": [92, 14, 72, 36]},
|
||||||
|
{"name": "PARAS", "type": "0716", "exp": "3375", 'bst': [35, 70, 55, 25, 55 ], "DexNum": 46, "Moveset": [78, 91, 72, 36]},
|
||||||
|
{"name": "VENONAT", "type": "0703", "exp": "3375", 'bst': [60, 55, 50, 45, 40 ], "DexNum": 48, "Moveset": [48, 94, 148, 38]},
|
||||||
|
{"name": "DIGLETT", "type": "0404", "exp": "3375", 'bst': [10, 55, 25, 95, 45 ], "DexNum": 50, "Moveset": [89, 104, 36, 90]},
|
||||||
|
{"name": "MEOWTH", "type": "0000", "exp": "3375", 'bst': [40, 45, 35, 90, 40 ], "DexNum": 52, "Moveset": [104, 85, 34, 156]},
|
||||||
|
{"name": "PSYDUCK", "type": "1515", "exp": "3375", 'bst': [50, 52, 48, 55, 50 ], "DexNum": 54, "Moveset": [61, 59, 91, 102]},
|
||||||
|
{"name": "MANKEY", "type": "0101", "exp": "3375", 'bst': [40, 80, 35, 70, 35 ], "DexNum": 56, "Moveset": [67, 2, 91, 68]},
|
||||||
|
{"name": "GROWLITHE", "type": "1414", "exp": "4218", 'bst': [55, 70, 45, 60, 50 ], "DexNum": 58, "Moveset": [91, 126, 38, 115]},
|
||||||
|
{"name": "ARCANINE", "type": "1414", "exp": "4218", 'bst': [90, 110, 80, 95, 80 ], "DexNum": 59, "Moveset": [91, 44, 52, 104]},
|
||||||
|
{"name": "POLIWAG", "type": "1515", "exp": "2035", 'bst': [40, 50, 40, 90, 40 ], "DexNum": 60, "Moveset": [57, 34, 59, 92]},
|
||||||
|
{"name": "POLIWHIRL", "type": "1515", "exp": "2035", 'bst': [65, 65, 65, 90, 50 ], "DexNum": 61, "Moveset": [57, 38, 118, 89]},
|
||||||
|
{"name": "POLIWRATH", "type": "1501", "exp": "2035", 'bst': [90, 85, 95, 70, 70 ], "DexNum": 62, "Moveset": [57, 3, 118, 95]},
|
||||||
|
{"name": "ABRA", "type": "1818", "exp": "2035", 'bst': [25, 20, 15, 90, 105], "DexNum": 63, "Moveset": [94, 86, 69, 115]},
|
||||||
|
{"name": "KADABRA", "type": "1818", "exp": "2035", 'bst': [40, 35, 30, 105, 120], "DexNum": 64, "Moveset": [94, 118, 104, 69]},
|
||||||
|
{"name": "ALAKAZAM", "type": "1818", "exp": "2035", 'bst': [55, 50, 45, 120, 135], "DexNum": 65, "Moveset": [149, 118, 86, 5]},
|
||||||
|
{"name": "MACHOP", "type": "0101", "exp": "2035", 'bst': [70, 80, 50, 35, 35 ], "DexNum": 66, "Moveset": [2, 66, 126, 117]},
|
||||||
|
{"name": "BELLSPROUT", "type": "1603", "exp": "2035", 'bst': [50, 75, 35, 40, 70 ], "DexNum": 69, "Moveset": [74, 36, 72, 115]},
|
||||||
|
{"name": "TENTACOOL", "type": "1503", "exp": "4218", 'bst': [40, 40, 35, 70, 100], "DexNum": 72, "Moveset": [57, 51, 48, 92]},
|
||||||
|
{"name": "TENTACRUEL", "type": "1503", "exp": "4218", 'bst': [80, 70, 65, 100, 120], "DexNum": 73, "Moveset": [48, 35, 92, 72]},
|
||||||
|
{"name": "GEODUDE", "type": "0504", "exp": "2035", 'bst': [40, 80, 100, 20, 30 ], "DexNum": 74, "Moveset": [5, 89, 157, 111]},
|
||||||
|
{"name": "PONYTA", "type": "1414", "exp": "3375", 'bst': [50, 85, 55, 90, 65 ], "DexNum": 77, "Moveset": [126, 32, 115, 129]},
|
||||||
|
{"name": "SLOWPOKE", "type": "1518", "exp": "3375", 'bst': [90, 65, 65, 15, 40 ], "DexNum": 79, "Moveset": [94, 57, 148, 91]},
|
||||||
|
{"name": "MAGNEMITE", "type": "1717", "exp": "3375", 'bst': [25, 35, 70, 45, 95 ], "DexNum": 81, "Moveset": [86, 85, 129, 164]},
|
||||||
|
{"name": "FARFETCH'D", "type": "0002", "exp": "3375", 'bst': [52, 65, 55, 60, 58 ], "DexNum": 83, "Moveset": [28, 31, 19, 115]},
|
||||||
|
{"name": "SEEL", "type": "1515", "exp": "3375", 'bst': [65, 45, 55, 45, 70 ], "DexNum": 86, "Moveset": [57, 29, 32, 59]},
|
||||||
|
{"name": "SHELLDER", "type": "1515", "exp": "4218", 'bst': [30, 65, 100, 40, 45 ], "DexNum": 90, "Moveset": [59, 161, 153, 57]},
|
||||||
|
{"name": "CLOYSTER", "type": "1519", "exp": "4218", 'bst': [50, 95, 180, 70, 85 ], "DexNum": 91, "Moveset": [48, 128, 63, 62]},
|
||||||
|
{"name": "GASTLY", "type": "0803", "exp": "2035", 'bst': [30, 35, 30, 80, 100], "DexNum": 92, "Moveset": [109, 94, 101, 153]},
|
||||||
|
{"name": "HAUNTER", "type": "0803", "exp": "2035", 'bst': [45, 50, 45, 95, 115], "DexNum": 93, "Moveset": [109, 85, 101, 120]},
|
||||||
|
{"name": "GENGAR", "type": "0803", "exp": "2035", 'bst': [60, 65, 60, 110, 130], "DexNum": 94, "Moveset": [109, 101, 72, 118]},
|
||||||
|
{"name": "ONIX", "type": "0504", "exp": "3375", 'bst': [35, 45, 160, 70, 30 ], "DexNum": 95, "Moveset": [157, 70, 89, 120]},
|
||||||
|
{"name": "DROWZEE", "type": "1818", "exp": "3375", 'bst': [60, 48, 45, 42, 90 ], "DexNum": 96, "Moveset": [95, 94, 138, 161]},
|
||||||
|
{"name": "KRABBY", "type": "1515", "exp": "3375", 'bst': [30, 105, 90, 50, 25 ], "DexNum": 98, "Moveset": [58, 34, 57, 92]},
|
||||||
|
{"name": "KINGLER", "type": "1515", "exp": "3375", 'bst': [55, 130, 115, 75, 50 ], "DexNum": 99, "Moveset": [57, 70, 104, 102]},
|
||||||
|
{"name": "VOLTORB", "type": "1717", "exp": "3375", 'bst': [40, 30, 50, 100, 55 ], "DexNum": 100, "Moveset": [153, 36, 85, 86]},
|
||||||
|
{"name": "EXEGGCUTE", "type": "1618", "exp": "4218", 'bst': [60, 40, 80, 40, 60 ], "DexNum": 102, "Moveset": [94, 104, 121, 92]},
|
||||||
|
{"name": "EXEGGUTOR", "type": "1618", "exp": "4218", 'bst': [95, 95, 85, 55, 125], "DexNum": 103, "Moveset": [92, 140, 72, 149]},
|
||||||
|
{"name": "CUBONE", "type": "0404", "exp": "3375", 'bst': [50, 50, 95, 35, 40 ], "DexNum": 104, "Moveset": [70, 89, 39, 59]},
|
||||||
|
{"name": "LICKITUNG", "type": "0000", "exp": "3375", 'bst': [90, 55, 75, 30, 60 ], "DexNum": 108, "Moveset": [38, 48, 126, 87]},
|
||||||
|
{"name": "KOFFING", "type": "0303", "exp": "3375", 'bst': [40, 65, 95, 35, 60 ], "DexNum": 109, "Moveset": [126, 92, 85, 120]},
|
||||||
|
{"name": "RHYHORN", "type": "0405", "exp": "4218", 'bst': [80, 85, 95, 25, 30 ], "DexNum": 111, "Moveset": [157, 89, 30, 164]},
|
||||||
|
{"name": "CHANSEY", "type": "0000", "exp": "2700", 'bst': [250, 5, 5, 50, 105], "DexNum": 113, "Moveset": [161, 68, 61, 85]},
|
||||||
|
{"name": "HORSEA", "type": "1515", "exp": "3375", 'bst': [30, 40, 70, 60, 70 ], "DexNum": 116, "Moveset": [57, 59, 92, 129]},
|
||||||
|
{"name": "SEADRA", "type": "1515", "exp": "3375", 'bst': [55, 65, 95, 85, 95 ], "DexNum": 117, "Moveset": [108, 61, 58, 102]},
|
||||||
|
{"name": "GOLDEEN", "type": "1515", "exp": "3375", 'bst': [45, 67, 60, 63, 50 ], "DexNum": 118, "Moveset": [57, 38, 58, 32]},
|
||||||
|
{"name": "STARYU", "type": "1515", "exp": "4218", 'bst': [30, 45, 55, 85, 70 ], "DexNum": 120, "Moveset": [57, 94, 161, 86]},
|
||||||
|
{"name": "STARMIE", "type": "1518", "exp": "4218", 'bst': [60, 75, 85, 115, 100], "DexNum": 121, "Moveset": [149, 61, 87, 164]},
|
||||||
|
{"name": "MR. MIME", "type": "1818", "exp": "3375", 'bst': [40, 45, 65, 90, 100], "DexNum": 122, "Moveset": [25, 94, 112, 118]},
|
||||||
|
{"name": "SCYTHER", "type": "0702", "exp": "3375", 'bst': [70, 110, 80, 105, 55 ], "DexNum": 123, "Moveset": [98, 14, 63, 104]},
|
||||||
|
{"name": "PINSIR", "type": "0707", "exp": "4218", 'bst': [65, 125, 100, 85, 55 ], "DexNum": 127, "Moveset": [36, 66, 117, 102]},
|
||||||
|
{"name": "MAGIKARP", "type": "1515", "exp": "4218", 'bst': [20, 10, 55, 80, 20 ], "DexNum": 129, "Moveset": [150, 33, 0, 0]},
|
||||||
|
{"name": "GYARADOS", "type": "1502", "exp": "4218", 'bst': [95, 125, 79, 81, 100], "DexNum": 130, "Moveset": [56, 44, 156, 43]},
|
||||||
|
{"name": "LAPRAS", "type": "1519", "exp": "4218", 'bst': [130, 85, 80, 60, 95 ], "DexNum": 131, "Moveset": [61, 58, 45, 130]},
|
||||||
|
{"name": "DITTO", "type": "0000", "exp": "3375", 'bst': [48, 48, 48, 48, 48 ], "DexNum": 132, "Moveset": [144, 0, 0, 0]},
|
||||||
|
{"name": "PORYGON", "type": "0000", "exp": "3375", 'bst': [65, 60, 70, 40, 75 ], "DexNum": 137, "Moveset": [160, 159, 161, 94]},
|
||||||
|
{"name": "DRATINI", "type": "1A1A", "exp": "4218", 'bst': [41, 64, 45, 50, 50 ], "DexNum": 147, "Moveset": [126, 59, 34, 86]},
|
||||||
|
]
|
||||||
|
|
||||||
|
kanto_attack_dict = {
|
||||||
|
"PHY1": [34, 89, 163],
|
||||||
|
"PHY2": [38, 63, 65, 70, 136, 155, 161],
|
||||||
|
"PHY3": [23, 24, 25, 26, 29, 30, 36, 37, 44, 66, 120, 124, 146, 153, 157, 158],
|
||||||
|
"PHY4": [2, 4, 5, 11, 15, 21, 27, 41, 67, 69, 91, 101, 121, 125, 129, 131, 143, 154, 162],
|
||||||
|
"PHY5": [1, 3, 6, 10, 16, 17, 20, 31, 33, 35, 42, 51, 64, 88, 98, 117, 130, 140],
|
||||||
|
"PHY6": [13, 19, 40, 49, 99, 122, 123, 132, 141, 68],
|
||||||
|
"PHY7": [68],
|
||||||
|
"SPE1": [53, 57, 58, 85, 94, 59],
|
||||||
|
"SPE2": [87, 126, 7, 8, 9],
|
||||||
|
"SPE3": [56, 127, 128, 152],
|
||||||
|
"SPE4": [60, 61, 62, 75, 76, 80, 93],
|
||||||
|
"SPE5": [138, 149, 55, 72, 83, 84],
|
||||||
|
"SPE6": [52, 145],
|
||||||
|
"SPE7": [22, 71, 82],
|
||||||
|
"STA1": [86, 79, 142],
|
||||||
|
"STA2": [95, 78, 109, 137],
|
||||||
|
"STA3": [47, 97, 133, 156],
|
||||||
|
"STA4": [14, 28, 48, 74, 77, 92, 103, 104, 105, 107, 108, 112, 113, 114, 115, 116, 134, 135, 139, 151, 164],
|
||||||
|
"STA5": [12, 32, 39, 43, 45, 50, 54, 81, 90, 96, 106, 110, 111, 148, 159],
|
||||||
|
"STA6": [73, 102, 118, 119, 144, 160],
|
||||||
|
"STA7": [18, 46, 100, 150],
|
||||||
|
"NORMAL": [1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 15, 16, 20, 21, 23, 25, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 44, 49, 63, 70, 98, 99, 117, 120, 121, 129, 130, 131, 132, 140, 146, 153, 154, 158, 161, 162, 163],
|
||||||
|
"FIGHTING": [24, 26, 27, 66, 67, 68, 69, 136],
|
||||||
|
"FLYING": [17, 19, 64, 65, 143],
|
||||||
|
"POISON": [40, 51, 123, 124],
|
||||||
|
"GROUND": [89, 90, 91, 125, 155],
|
||||||
|
"ROCK": [88, 157],
|
||||||
|
"BUG": [41, 42, 141],
|
||||||
|
"GHOST": [101, 122],
|
||||||
|
"FIRE": [7, 52, 53, 83, 126],
|
||||||
|
"WATER": [55, 56, 57, 61, 127, 128, 145, 152],
|
||||||
|
"GRASS": [22, 71, 72, 75, 76, 80],
|
||||||
|
"ELECTRIC": [9, 84, 85, 87],
|
||||||
|
"PSYCHIC": [60, 93, 94, 138, 149],
|
||||||
|
"ICE": [8, 58, 59, 62],
|
||||||
|
"DRAGON": [82]
|
||||||
|
}
|
||||||
|
|
||||||
|
# random number rolling boundaries for picking a move bucket
|
||||||
|
# lower BSTs are weighted towards the left, which should give weaker mons better moves on average
|
||||||
|
# as you go up in BST teir, the weights shift to the right towards a tendancy for weaker moves
|
||||||
|
stat_distribution_list = [
|
||||||
|
[18.0, 41.4, 59.3, 74.0, 86.6, 98.0, 100.0],
|
||||||
|
[14.9, 31.9, 52.9, 69.9, 84.8, 98.0, 100.0],
|
||||||
|
[13.2, 27.6, 44.6, 65.6, 82.7, 97.0, 100.0],
|
||||||
|
[12.3, 25.8, 40.9, 58.3, 79.6, 97.0, 100.0],
|
||||||
|
[11.5, 23.6, 36.8, 52.3, 71.3, 96.0, 100.0],
|
||||||
|
]
|
||||||
|
|
||||||
|
bst_weights = [
|
||||||
|
[100, 100, 100, 100, 100], # uniform distribution
|
||||||
|
[200, 150, 80, 50, 20], # weight one side
|
||||||
|
[180, 80, 80, 80, 80], # heavily weight one stat
|
||||||
|
[160, 50, 150, 90, 50] # random bursts
|
||||||
|
]
|
||||||
|
#We don't need lists for pokeball and greatball cup round 1 since they are all level 50 and level 51 respectively
|
||||||
|
pokecupr1_ultra_levels = [
|
||||||
|
[53, 51, 51, 53, 51, 51],
|
||||||
|
[51, 50, 54, 51, 50, 50],
|
||||||
|
[50, 51, 50, 54, 54, 51],
|
||||||
|
[53, 50, 50, 52, 50, 55],
|
||||||
|
[50, 51, 50, 54, 51, 54],
|
||||||
|
[51, 51, 51, 51, 51, 53],
|
||||||
|
[52, 51, 50, 54, 50, 52],
|
||||||
|
[55, 50, 50, 50, 50, 50]
|
||||||
|
]
|
||||||
|
|
||||||
|
pokecupr1_master_levels = [
|
||||||
|
[51, 52, 51, 51, 52, 52],
|
||||||
|
[50, 50, 53, 51, 54, 51],
|
||||||
|
[51, 54, 51, 50, 50, 53],
|
||||||
|
[53, 52, 50, 51, 51, 50],
|
||||||
|
[50, 51, 52, 53, 54, 50],
|
||||||
|
[51, 53, 50, 52, 52, 50],
|
||||||
|
[50, 52, 53, 53, 50, 50],
|
||||||
|
[55, 50, 50, 50, 50, 55]
|
||||||
|
]
|
||||||
|
|
||||||
|
petitcupr1_levels = [
|
||||||
|
[25, 25, 25, 25, 25, 25],
|
||||||
|
[25, 26, 26, 26, 25, 25],
|
||||||
|
[25, 25, 25, 30, 25, 30],
|
||||||
|
[26, 26, 27, 26, 26, 27],
|
||||||
|
[26, 26, 27, 26, 27, 27],
|
||||||
|
[26, 27, 26, 27, 27, 27],
|
||||||
|
[30, 25, 25, 25, 25, 30],
|
||||||
|
[25, 25, 25, 25, 30, 30]
|
||||||
|
]
|
||||||
|
|
||||||
|
pikacupr1_levels = [
|
||||||
|
[16, 15, 15, 15, 15, 15],
|
||||||
|
[15, 16, 15, 15, 15, 15],
|
||||||
|
[16, 15, 16, 15, 15, 16],
|
||||||
|
[16, 17, 16, 16, 16, 15],
|
||||||
|
[16, 15, 15, 16, 18, 18],
|
||||||
|
[20, 16, 15, 15, 16, 18],
|
||||||
|
[20, 20, 15, 15, 15, 15],
|
||||||
|
[18, 16, 16, 18, 16, 16]
|
||||||
|
]
|
||||||
22
worlds/PokemonStadium/randomizer/levelFunctions.py
Normal file
22
worlds/PokemonStadium/randomizer/levelFunctions.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import math
|
||||||
|
|
||||||
|
class levelExpCalculator:
|
||||||
|
@classmethod
|
||||||
|
def getExpValue(self, lvl, growthRate: str):
|
||||||
|
|
||||||
|
expValue = 0
|
||||||
|
if(growthRate == "slow"):
|
||||||
|
expValue = 5 * math.pow(lvl, 3) / 4
|
||||||
|
return expValue
|
||||||
|
if(growthRate == "mediumslow"):
|
||||||
|
expValue = ((6/5) * math.pow(lvl, 3)) - (15*(math.pow(lvl, 2))) + (100*lvl) - 140
|
||||||
|
return expValue
|
||||||
|
if(growthRate == "mediumfast"):
|
||||||
|
expValue = math.pow(lvl, 3)
|
||||||
|
return expValue
|
||||||
|
if(growthRate == "fast"):
|
||||||
|
expValue = 4 * math.pow(lvl, 3) / 5
|
||||||
|
return expValue
|
||||||
|
else:
|
||||||
|
print("Invalid growth rate.")
|
||||||
|
return expValue
|
||||||
144
worlds/PokemonStadium/randomizer/randomMovesetGenerator.py
Normal file
144
worlds/PokemonStadium/randomizer/randomMovesetGenerator.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
from . import constants
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_move(attack_type, distribution):
|
||||||
|
key_str = ""
|
||||||
|
|
||||||
|
if attack_type not in ['PHY', 'SPE', 'STA']:
|
||||||
|
key_str = attack_type
|
||||||
|
else:
|
||||||
|
roll = random.randrange(1, 100)
|
||||||
|
if roll <= distribution[0]:
|
||||||
|
key_str = attack_type + "1"
|
||||||
|
elif roll <= distribution[1]:
|
||||||
|
key_str = attack_type + "2"
|
||||||
|
elif roll <= distribution[2]:
|
||||||
|
key_str = attack_type + "3"
|
||||||
|
elif roll <= distribution[3]:
|
||||||
|
key_str = attack_type + "4"
|
||||||
|
elif roll > distribution[4]:
|
||||||
|
key_str = attack_type + "5"
|
||||||
|
elif roll <= distribution[5]:
|
||||||
|
key_str = attack_type + "6"
|
||||||
|
elif roll <= distribution[6]:
|
||||||
|
key_str = attack_type + "7"
|
||||||
|
|
||||||
|
# Spore clause
|
||||||
|
if attack_type == 'STA' and random.randint(1, 200) == 1:
|
||||||
|
return 147
|
||||||
|
|
||||||
|
return random.choice(constants.kanto_attack_dict[key_str])
|
||||||
|
|
||||||
|
def get_type_name(type_num):
|
||||||
|
if random.random() < 0.5:
|
||||||
|
type_str = type_num.hex()[0:2].upper()
|
||||||
|
else:
|
||||||
|
type_str = type_num.hex()[2:].upper()
|
||||||
|
|
||||||
|
if type_str == '01':
|
||||||
|
return 'FIGHTING'
|
||||||
|
elif type_str == '02':
|
||||||
|
return 'FLYING'
|
||||||
|
elif type_str == '03':
|
||||||
|
return 'POISON'
|
||||||
|
elif type_str == '04':
|
||||||
|
return 'GROUND'
|
||||||
|
elif type_str == '05':
|
||||||
|
return 'ROCK'
|
||||||
|
elif type_str == '07':
|
||||||
|
return 'BUG'
|
||||||
|
elif type_str == '08':
|
||||||
|
return 'GHOST'
|
||||||
|
elif type_str == '14':
|
||||||
|
return 'FIRE'
|
||||||
|
elif type_str == '15':
|
||||||
|
return 'WATER'
|
||||||
|
elif type_str == '16':
|
||||||
|
return 'GRASS'
|
||||||
|
elif type_str == '17':
|
||||||
|
return 'ELECTRIC'
|
||||||
|
elif type_str == '18':
|
||||||
|
return 'PSYCHIC'
|
||||||
|
elif type_str == '19':
|
||||||
|
return 'ICE'
|
||||||
|
elif type_str == '1A':
|
||||||
|
return 'DRAGON'
|
||||||
|
else: # type == '00' or a bad value got in here
|
||||||
|
return 'NORMAL'
|
||||||
|
|
||||||
|
class MovesetGenerator:
|
||||||
|
@staticmethod
|
||||||
|
def get_random_moveset(bst_list, rando_factor, pkm_type):
|
||||||
|
bst = sum(bst_list)
|
||||||
|
|
||||||
|
# first type of move is always a damaging move that lines up with higher attacking stat
|
||||||
|
first_type = "PHY" if bst_list[1] > bst_list[4] else "SPE"
|
||||||
|
|
||||||
|
# second move is a STAB damaging move if factor is at least 2
|
||||||
|
if (rando_factor < 4):
|
||||||
|
second_type = get_type_name(pkm_type)
|
||||||
|
else:
|
||||||
|
one_in_three = random.randrange(1, 99)
|
||||||
|
if one_in_three <= 33:
|
||||||
|
second_type = "PHY"
|
||||||
|
elif one_in_three <= 66:
|
||||||
|
second_type = "SPE"
|
||||||
|
else:
|
||||||
|
second_type = "STA"
|
||||||
|
|
||||||
|
# third move afflicts a status or affects stats if factor is at least 3
|
||||||
|
if (rando_factor < 3):
|
||||||
|
third_type = "STA"
|
||||||
|
else:
|
||||||
|
one_in_three = random.randrange(1, 99)
|
||||||
|
if one_in_three <= 33:
|
||||||
|
third_type = "PHY"
|
||||||
|
elif one_in_three <= 66:
|
||||||
|
third_type = "SPE"
|
||||||
|
else:
|
||||||
|
third_type = "STA"
|
||||||
|
|
||||||
|
# fourth move is random
|
||||||
|
one_in_three = random.randrange(1, 99)
|
||||||
|
if one_in_three <= 33:
|
||||||
|
fourth_type = "PHY"
|
||||||
|
elif one_in_three <= 66:
|
||||||
|
fourth_type = "SPE"
|
||||||
|
else:
|
||||||
|
fourth_type = "STA"
|
||||||
|
|
||||||
|
attack_types = [first_type, second_type, third_type, fourth_type]
|
||||||
|
moveset = []
|
||||||
|
if (rando_factor == 2):
|
||||||
|
if bst <= 225:
|
||||||
|
distribution = constants.stat_distribution_list[0]
|
||||||
|
elif bst <= 300:
|
||||||
|
distribution = constants.stat_distribution_list[1]
|
||||||
|
elif bst <= 375:
|
||||||
|
distribution = constants.stat_distribution_list[2]
|
||||||
|
elif bst <= 450:
|
||||||
|
distribution = constants.stat_distribution_list[3]
|
||||||
|
else:
|
||||||
|
distribution = constants.stat_distribution_list[4]
|
||||||
|
elif (rando_factor == 3):
|
||||||
|
if bst <= 300:
|
||||||
|
distribution = constants.stat_distribution_list[random.randrange(0, 1)]
|
||||||
|
elif bst <= 450:
|
||||||
|
distribution = constants.stat_distribution_list[random.randrange(2, 3)]
|
||||||
|
else:
|
||||||
|
distribution = constants.stat_distribution_list[4]
|
||||||
|
else:
|
||||||
|
distribution = constants.stat_distribution_list[random.randrange(0, 4)]
|
||||||
|
|
||||||
|
for atk_type in attack_types:
|
||||||
|
random_move = get_random_move(atk_type, distribution)
|
||||||
|
while True:
|
||||||
|
if random_move in moveset:
|
||||||
|
random_move = get_random_move(atk_type, distribution)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
moveset.append(random_move)
|
||||||
|
|
||||||
|
return moveset
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
from . import constants
|
||||||
|
|
||||||
|
class BaseValuesRandomizer:
|
||||||
|
@classmethod
|
||||||
|
def randomize_stats(cls, vanilla_stats, random_factor):
|
||||||
|
min_val = 20
|
||||||
|
max_val = 235
|
||||||
|
|
||||||
|
bst_list = []
|
||||||
|
for stat in vanilla_stats:
|
||||||
|
bst_list.append(stat)
|
||||||
|
bst = sum(bst_list)
|
||||||
|
new_stats_bytes = bytearray()
|
||||||
|
|
||||||
|
# Start with an array of 5 numbers, all at the minimum value
|
||||||
|
new_stats = [min_val] * 5
|
||||||
|
current_sum = sum(new_stats)
|
||||||
|
|
||||||
|
# Increment numbers until we reach BST
|
||||||
|
while current_sum < bst:
|
||||||
|
# Randomly select an index to increase
|
||||||
|
idx = cls.select_index(random_factor)
|
||||||
|
|
||||||
|
# Only increase if it won't exceed max_val
|
||||||
|
if new_stats[idx] < max_val:
|
||||||
|
new_stats[idx] += 1
|
||||||
|
current_sum += 1
|
||||||
|
else:
|
||||||
|
# Check if all numbers are maxed out (should never happen with correct BST input)
|
||||||
|
if all(n == max_val for n in new_stats):
|
||||||
|
raise RuntimeError("All stats reached max_val but BST is not yet met. Something went wrong!")
|
||||||
|
|
||||||
|
random.shuffle(new_stats)
|
||||||
|
for stat in new_stats:
|
||||||
|
try:
|
||||||
|
new_stats_bytes.extend(stat.to_bytes(1, "big"))
|
||||||
|
except OverflowError:
|
||||||
|
print("ERROR: BST is too high.")
|
||||||
|
print("BST_STR: " + str(vanilla_stats))
|
||||||
|
print("BST: " + str(bst))
|
||||||
|
print("STATS: " + str(new_stats))
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
return new_stats_bytes
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def select_index(cls, random_factor):
|
||||||
|
random_factor = random_factor - 1
|
||||||
|
weight_map = {
|
||||||
|
1: constants.bst_weights[0] if random.random() < 0.5 else constants.bst_weights[1],
|
||||||
|
2: constants.bst_weights[2],
|
||||||
|
3: constants.bst_weights[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
return random.choices([0, 1, 2, 3, 4], weights=weight_map.get(random_factor, constants.bst_weights[0]))[0]
|
||||||
1542
worlds/PokemonStadium/randomizer/stadium_randomizer.py
Normal file
1542
worlds/PokemonStadium/randomizer/stadium_randomizer.py
Normal file
File diff suppressed because it is too large
Load Diff
25
worlds/PokemonStadium/randomizer/util.py
Normal file
25
worlds/PokemonStadium/randomizer/util.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import math
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class Util:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def calculate_stat(stat, ev, iv, level):
|
||||||
|
return int((((stat + iv) * 2 + math.floor(math.ceil(math.sqrt(ev)) / 4)) * level)/100) + 5
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def calculate_hp_stat(stat, ev, iv, level):
|
||||||
|
return int((((stat + iv) * 2 + math.floor(math.ceil(math.sqrt(ev)) / 4)) * level)/100) + level + 10
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def random_int_set(min_val, max_val, count):
|
||||||
|
return random.sample(range(min_val, max_val), count)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def random_string_hex(length):
|
||||||
|
int_set = random.sample(range(0, 15), length)
|
||||||
|
return_hex = ""
|
||||||
|
for integer in int_set:
|
||||||
|
return_hex = return_hex + hex(integer)[2:]
|
||||||
|
return return_hex
|
||||||
23
worlds/PokemonStadium/randomizer/writeDisplayData.py
Normal file
23
worlds/PokemonStadium/randomizer/writeDisplayData.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import math
|
||||||
|
|
||||||
|
from . import util
|
||||||
|
|
||||||
|
class DisplayDataWriter:
|
||||||
|
@staticmethod
|
||||||
|
def write_gym_tower_display(new_display_stats_set, evs, iv_str, lvl):
|
||||||
|
ivs = [0, 0, 0, 0, 0]
|
||||||
|
iv_binary = "{0:016b}".format(int(iv_str, 16))
|
||||||
|
for i in range(0, 4):
|
||||||
|
int_val = int(iv_binary[i * 4:(i * 4 + 4)], 2)
|
||||||
|
ivs[i + 1] = int_val
|
||||||
|
if int_val % 2 != 0:
|
||||||
|
ivs[0] = int(ivs[0] + math.pow(2, 3 - i))
|
||||||
|
|
||||||
|
display_stats = bytearray()
|
||||||
|
new_displays_int = [int(x) for x in new_display_stats_set]
|
||||||
|
display = util.Util.calculate_hp_stat(new_displays_int[0], evs[0], ivs[0], lvl)
|
||||||
|
display_stats.extend(display.to_bytes(2, "big"))
|
||||||
|
for j in range(1, 5):
|
||||||
|
display = util.Util.calculate_stat(new_displays_int[j], evs[j], ivs[j], lvl)
|
||||||
|
display_stats.extend(display.to_bytes(2, "big"))
|
||||||
|
return display_stats
|
||||||
11
worlds/Schedule_I/__init__.py
Normal file
11
worlds/Schedule_I/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# The first thing you should make for your world is an archipelago.json manifest file.
|
||||||
|
# You can reference APQuest's, but you should change the "game" field (obviously),
|
||||||
|
# and you should also change the "minimum_ap_version" - probably to the current value of Utils.__version__.
|
||||||
|
|
||||||
|
# Apart from the regular apworld code that allows generating multiworld seeds with your game,
|
||||||
|
# your apworld might have other "components" that should be launchable from the Archipelago Launcher.
|
||||||
|
# You can ignore this for now. If you are specifically interested in components, you can read components.py.
|
||||||
|
|
||||||
|
# The main thing we do in our __init__.py is importing our world class from our world.py to initialize it.
|
||||||
|
# Obviously, this world class needs to exist first. For this, read world.py.
|
||||||
|
from .world import Schedule1World as Schedule1World
|
||||||
1
worlds/Schedule_I/archipelago.json
Normal file
1
worlds/Schedule_I/archipelago.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"game": "Schedule I", "minimum_ap_version": "0.6.6", "world_version": "3.5.4", "authors": ["MacH8s"], "compatible_version": 7, "version": 7}
|
||||||
1289
worlds/Schedule_I/data/items.json
Normal file
1289
worlds/Schedule_I/data/items.json
Normal file
File diff suppressed because it is too large
Load Diff
1761
worlds/Schedule_I/data/locations.json
Normal file
1761
worlds/Schedule_I/data/locations.json
Normal file
File diff suppressed because it is too large
Load Diff
250
worlds/Schedule_I/data/regions.json
Normal file
250
worlds/Schedule_I/data/regions.json
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
{
|
||||||
|
"Overworld": {
|
||||||
|
"connections": {
|
||||||
|
"Welcome to Hyland Point": true,
|
||||||
|
"Northtown" : true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Northtown": {
|
||||||
|
"connections": {"Westville": {"randomize_level_unlocks&!randomize_customers" : {"has": "Westville Region Unlock"}},
|
||||||
|
"Weed Recipe Checks": {"randomize_level_unlocks" : {"has": "Mixing Station Unlock"}}}
|
||||||
|
},
|
||||||
|
"Westville": {
|
||||||
|
"connections": {"Downtown": {"randomize_cartel_influence&!randomize_customers" : {"has_all_counts": {"Cartel Influence, Westville": 2}}},
|
||||||
|
"Meth Recipe Checks": {"randomize_level_unlocks" : {"has_any": [["Mixing Station Mk II Unlock", "Mixing Station Unlock"]],
|
||||||
|
"has_all" : ["Acid Unlock",
|
||||||
|
"Phosphorus Unlock",
|
||||||
|
"Chemistry Station Unlock",
|
||||||
|
"Lab Oven Unlock",
|
||||||
|
"Warehouse Access"]},
|
||||||
|
"randomize_suppliers" : {"has": "Shirley Watts Unlocked"},
|
||||||
|
"randomize_customers&!randomize_suppliers" : {"has_any" : [["Meg Cooley Unlocked", "Jerry Montero Unlocked"]]}}}
|
||||||
|
},
|
||||||
|
"Downtown": {
|
||||||
|
"connections": {"Docks": {"randomize_cartel_influence&!randomize_customers" : {"has_all_counts": {"Cartel Influence, Downtown": 7}},
|
||||||
|
"randomize_level_unlocks" : {"has": "Fertilizer Unlock"}},
|
||||||
|
"Vibin' on the 'Cybin": {"randomize_level_unlocks" : {"has" : "Warehouse Access"},
|
||||||
|
"randomize_suppliers" : {"has": "Fungal Phil Unlocked"},
|
||||||
|
"randomize_customers&!randomize_suppliers" : {"has_any" : [["Elizabeth Homley Unlocked", "Kevin Oakley Unlocked"]]}}}
|
||||||
|
},
|
||||||
|
"Docks": {
|
||||||
|
"connections": {"Suburbia": {"randomize_cartel_influence&!randomize_customers" : {"has_all_counts": {"Cartel Influence, Docks": 7}}},
|
||||||
|
"Cocaine Recipe Checks": {"randomize_level_unlocks" : {"has_any": [["Mixing Station Mk II Unlock", "Mixing Station Unlock"]],
|
||||||
|
"has_all" : ["Cauldron Unlock", "Lab Oven Unlock", "Gasoline Unlock", "Warehouse Access"]},
|
||||||
|
"randomize_suppliers" : {"has": "Salvador Moreno Unlocked"},
|
||||||
|
"randomize_customers&!randomize_suppliers" : {"has_any" : [["Mac Cooper Unlocked", "Javier Pérez Unlocked"]]}}}
|
||||||
|
},
|
||||||
|
"Suburbia": {
|
||||||
|
"connections": {"Uptown": {"randomize_cartel_influence&!randomize_customers" : {"has_all_counts": {"Cartel Influence, Suburbia": 7}},
|
||||||
|
"randomize_level_unlocks" : {"has": "Drying Rack Unlock"}}}
|
||||||
|
},
|
||||||
|
"Uptown": {
|
||||||
|
"connections": {}
|
||||||
|
},
|
||||||
|
"Weed Recipe Checks": {
|
||||||
|
"connections": {}
|
||||||
|
},
|
||||||
|
"Meth Recipe Checks": {
|
||||||
|
"connections": {}
|
||||||
|
},
|
||||||
|
"Shrooms Recipe Checks": {
|
||||||
|
"connections": {}
|
||||||
|
},
|
||||||
|
"Cocaine Recipe Checks": {
|
||||||
|
"connections": {}
|
||||||
|
},
|
||||||
|
"Welcome to Hyland Point": {
|
||||||
|
"connections": {
|
||||||
|
"Getting Started": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Getting Started": {
|
||||||
|
"connections": {
|
||||||
|
"Money Management": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Money Management": {
|
||||||
|
"connections": {
|
||||||
|
"Gearing Up|1": true,
|
||||||
|
"Clean Cash": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Clean Cash": {
|
||||||
|
"connections": {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Gearing Up|1": {
|
||||||
|
"connections": {
|
||||||
|
"Gearing Up|2": true,
|
||||||
|
"Packin'": true,
|
||||||
|
"Keeping it Fresh": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Keeping it Fresh": {
|
||||||
|
"connections": {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Packin'": {
|
||||||
|
"connections": {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Gearing Up|2": {
|
||||||
|
"connections": {
|
||||||
|
"On the Grind|1": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"On the Grind|1": {
|
||||||
|
"connections": {
|
||||||
|
"Moving Up": true,
|
||||||
|
"On the Grind|2": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"On the Grind|2": {
|
||||||
|
"connections": {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Moving Up": {
|
||||||
|
"connections": {
|
||||||
|
"Dodgy Dealing": {"randomize_customers": {"has_from_list":
|
||||||
|
{"Austin Steiner Unlocked": 10,
|
||||||
|
"Beth Penn Unlocked": 10,
|
||||||
|
"Chloe Bowers Unlocked": 10,
|
||||||
|
"Donna Martin Unlocked": 10,
|
||||||
|
"Geraldine Poon Unlocked": 10,
|
||||||
|
"Jessi Waters Unlocked": 10,
|
||||||
|
"Kathy Henderson Unlocked": 10,
|
||||||
|
"Kyle Cooley Unlocked": 10,
|
||||||
|
"Ludwig Meyer Unlocked": 10,
|
||||||
|
"Mick Lubbin Unlocked": 10,
|
||||||
|
"Mrs. Ming Unlocked": 10,
|
||||||
|
"Peggy Myers Unlocked": 10,
|
||||||
|
"Peter File Unlocked": 10,
|
||||||
|
"Sam Thompson Unlocked": 10,
|
||||||
|
"Charles Rowland Unlocked": 10,
|
||||||
|
"Dean Webster Unlocked": 10,
|
||||||
|
"Doris Lubbin Unlocked": 10,
|
||||||
|
"George Greene Unlocked": 10,
|
||||||
|
"Jerry Montero Unlocked": 10,
|
||||||
|
"Joyce Ball Unlocked": 10,
|
||||||
|
"Keith Wagner Unlocked": 10,
|
||||||
|
"Kim Delaney Unlocked": 10,
|
||||||
|
"Meg Cooley Unlocked": 10,
|
||||||
|
"Trent Sherman Unlocked": 10,
|
||||||
|
"Bruce Norton Unlocked": 10,
|
||||||
|
"Elizabeth Homley Unlocked": 10,
|
||||||
|
"Eugene Buckley Unlocked": 10,
|
||||||
|
"Greg Figgle Unlocked": 10,
|
||||||
|
"Jeff Gilmore Unlocked": 10,
|
||||||
|
"Jennifer Rivera Unlocked": 10,
|
||||||
|
"Kevin Oakley Unlocked": 10,
|
||||||
|
"Louis Fourier Unlocked": 10,
|
||||||
|
"Philip Wentworth Unlocked": 10,
|
||||||
|
"Randy Caulfield Unlocked": 10,
|
||||||
|
"Lucy Pennington Unlocked": 10,
|
||||||
|
"Anna Chesterfield Unlocked": 10,
|
||||||
|
"Billy Kramer Unlocked": 10,
|
||||||
|
"Cranky Frank Unlocked": 10,
|
||||||
|
"Genghis Barn Unlocked": 10,
|
||||||
|
"Javier Pérez Unlocked": 10,
|
||||||
|
"Kelly Reynolds Unlocked": 10,
|
||||||
|
"Lisa Gardener Unlocked": 10,
|
||||||
|
"Mac Cooper Unlocked": 10,
|
||||||
|
"Marco Barone Unlocked": 10,
|
||||||
|
"Melissa Wood Unlocked": 10,
|
||||||
|
"Sherman Giles Unlocked": 10,
|
||||||
|
"Alison Knight Unlocked": 10,
|
||||||
|
"Carl Bundy Unlocked": 10,
|
||||||
|
"Chris Sullivan Unlocked": 10,
|
||||||
|
"Dennis Kennedy Unlocked": 10,
|
||||||
|
"Hank Stevenson Unlocked": 10,
|
||||||
|
"Harold Colt Unlocked": 10,
|
||||||
|
"Jack Knight Unlocked": 10,
|
||||||
|
"Jackie Stevenson Unlocked": 10,
|
||||||
|
"Jeremy Wilkinson Unlocked": 10,
|
||||||
|
"Karen Kennedy Unlocked": 10,
|
||||||
|
"Fiona Hancock Unlocked": 10,
|
||||||
|
"Herbert Bleuball Unlocked": 10,
|
||||||
|
"Irene Meadows Unlocked": 10,
|
||||||
|
"Jen Heard Unlocked": 10,
|
||||||
|
"Lily Turner Unlocked": 10,
|
||||||
|
"Michael Boog Unlocked": 10,
|
||||||
|
"Pearl Moore Unlocked": 10,
|
||||||
|
"Ray Hoffman Unlocked": 10,
|
||||||
|
"Tobias Wentworth Unlocked": 10,
|
||||||
|
"Walter Cussler Unlocked": 10}}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Dodgy Dealing": {
|
||||||
|
"connections": {
|
||||||
|
"Mixing Mania": {"randomize_customers": {"has_any" : [["Chloe Bowers Unlocked", "Ludwig Meyer Unlocked", "Beth Penn Unlocked"]]}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Mixing Mania": {
|
||||||
|
"connections": {
|
||||||
|
"Making the Rounds": {"randomize_level_unlocks" : {"has": "Mixing Station Unlock"}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Making the Rounds": {
|
||||||
|
"connections": {
|
||||||
|
"Needin' the Green": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Needin' the Green": {
|
||||||
|
"connections": {
|
||||||
|
"Wretched Hive of Scum and Villainy": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Vibin' on the 'Cybin": {
|
||||||
|
"connections": {
|
||||||
|
"Shrooms Recipe Checks": {"randomize_level_unlocks": {"has_any": [["Mixing Station Mk II Unlock", "Mixing Station Unlock"]]}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Wretched Hive of Scum and Villainy": {
|
||||||
|
"connections": {
|
||||||
|
"We Need To Cook|1": {"randomize_level_unlocks": {"has": "Warehouse Access"}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"We Need To Cook|1": {
|
||||||
|
"connections": {
|
||||||
|
"We Need To Cook|2": {"randomize_customers": {"has_any": [["Meg Cooley Unlocked", "Jerry Montero Unlocked"]]},
|
||||||
|
"randomize_suppliers": {"has": "Shirley Watts Unlocked"}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"We Need To Cook|2": {
|
||||||
|
"connections": {
|
||||||
|
"Unfavourable Agreements": {"randomize_level_unlocks": {"has_all" :["Chemistry Station Unlock",
|
||||||
|
"Lab Oven Unlock",
|
||||||
|
"Acid Unlock",
|
||||||
|
"Phosphorus Unlock"]},
|
||||||
|
"randomize_customers": {"has_from_list": {
|
||||||
|
"Charles Rowland Unlocked": 5,
|
||||||
|
"Dean Webster Unlocked": 5,
|
||||||
|
"Doris Lubbin Unlocked": 5,
|
||||||
|
"George Greene Unlocked": 5,
|
||||||
|
"Jerry Montero Unlocked": 5,
|
||||||
|
"Joyce Ball Unlocked": 5,
|
||||||
|
"Kim Delaney Unlocked": 5,
|
||||||
|
"Meg Cooley Unlocked": 5,
|
||||||
|
"Trent Sherman Unlocked": 5,
|
||||||
|
"Keith Wagner Unlocked": 5}}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Unfavourable Agreements": {
|
||||||
|
"connections": {
|
||||||
|
"Finishing the Job": true,
|
||||||
|
"Cartel Influence": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Cartel Influence": {
|
||||||
|
"connections": {}
|
||||||
|
},
|
||||||
|
"Finishing the Job": {
|
||||||
|
"connections": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
worlds/Schedule_I/data/victory.json
Normal file
93
worlds/Schedule_I/data/victory.json
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
{
|
||||||
|
"randomize_customers" : {"has_from_list" : [{"Charles Rowland Unlocked": 5,
|
||||||
|
"Dean Webster Unlocked": 5,
|
||||||
|
"Doris Lubbin Unlocked": 5,
|
||||||
|
"George Greene Unlocked": 5,
|
||||||
|
"Jerry Montero Unlocked": 5,
|
||||||
|
"Joyce Ball Unlocked": 5,
|
||||||
|
"Kim Delaney Unlocked": 5,
|
||||||
|
"Meg Cooley Unlocked": 5,
|
||||||
|
"Trent Sherman Unlocked": 5,
|
||||||
|
"Keith Wagner Unlocked": 5},
|
||||||
|
{"Austin Steiner Unlocked": 10,
|
||||||
|
"Beth Penn Unlocked": 10,
|
||||||
|
"Chloe Bowers Unlocked": 10,
|
||||||
|
"Donna Martin Unlocked": 10,
|
||||||
|
"Geraldine Poon Unlocked": 10,
|
||||||
|
"Jessi Waters Unlocked": 10,
|
||||||
|
"Kathy Henderson Unlocked": 10,
|
||||||
|
"Kyle Cooley Unlocked": 10,
|
||||||
|
"Ludwig Meyer Unlocked": 10,
|
||||||
|
"Mick Lubbin Unlocked": 10,
|
||||||
|
"Mrs. Ming Unlocked": 10,
|
||||||
|
"Peggy Myers Unlocked": 10,
|
||||||
|
"Peter File Unlocked": 10,
|
||||||
|
"Sam Thompson Unlocked": 10,
|
||||||
|
"Charles Rowland Unlocked": 10,
|
||||||
|
"Dean Webster Unlocked": 10,
|
||||||
|
"Doris Lubbin Unlocked": 10,
|
||||||
|
"George Greene Unlocked": 10,
|
||||||
|
"Jerry Montero Unlocked": 10,
|
||||||
|
"Joyce Ball Unlocked": 10,
|
||||||
|
"Keith Wagner Unlocked": 10,
|
||||||
|
"Kim Delaney Unlocked": 10,
|
||||||
|
"Meg Cooley Unlocked": 10,
|
||||||
|
"Trent Sherman Unlocked": 10,
|
||||||
|
"Bruce Norton Unlocked": 10,
|
||||||
|
"Elizabeth Homley Unlocked": 10,
|
||||||
|
"Eugene Buckley Unlocked": 10,
|
||||||
|
"Greg Figgle Unlocked": 10,
|
||||||
|
"Jeff Gilmore Unlocked": 10,
|
||||||
|
"Jennifer Rivera Unlocked": 10,
|
||||||
|
"Kevin Oakley Unlocked": 10,
|
||||||
|
"Louis Fourier Unlocked": 10,
|
||||||
|
"Philip Wentworth Unlocked": 10,
|
||||||
|
"Randy Caulfield Unlocked": 10,
|
||||||
|
"Lucy Pennington Unlocked": 10,
|
||||||
|
"Anna Chesterfield Unlocked": 10,
|
||||||
|
"Billy Kramer Unlocked": 10,
|
||||||
|
"Cranky Frank Unlocked": 10,
|
||||||
|
"Genghis Barn Unlocked": 10,
|
||||||
|
"Javier Pérez Unlocked": 10,
|
||||||
|
"Kelly Reynolds Unlocked": 10,
|
||||||
|
"Lisa Gardener Unlocked": 10,
|
||||||
|
"Mac Cooper Unlocked": 10,
|
||||||
|
"Marco Barone Unlocked": 10,
|
||||||
|
"Melissa Wood Unlocked": 10,
|
||||||
|
"Sherman Giles Unlocked": 10,
|
||||||
|
"Alison Knight Unlocked": 10,
|
||||||
|
"Carl Bundy Unlocked": 10,
|
||||||
|
"Chris Sullivan Unlocked": 10,
|
||||||
|
"Dennis Kennedy Unlocked": 10,
|
||||||
|
"Hank Stevenson Unlocked": 10,
|
||||||
|
"Harold Colt Unlocked": 10,
|
||||||
|
"Jack Knight Unlocked": 10,
|
||||||
|
"Jackie Stevenson Unlocked": 10,
|
||||||
|
"Jeremy Wilkinson Unlocked": 10,
|
||||||
|
"Karen Kennedy Unlocked": 10,
|
||||||
|
"Fiona Hancock Unlocked": 10,
|
||||||
|
"Herbert Bleuball Unlocked": 10,
|
||||||
|
"Irene Meadows Unlocked": 10,
|
||||||
|
"Jen Heard Unlocked": 10,
|
||||||
|
"Lily Turner Unlocked": 10,
|
||||||
|
"Michael Boog Unlocked": 10,
|
||||||
|
"Pearl Moore Unlocked": 10,
|
||||||
|
"Ray Hoffman Unlocked": 10,
|
||||||
|
"Tobias Wentworth Unlocked": 10,
|
||||||
|
"Walter Cussler Unlocked": 10}],
|
||||||
|
"has_any" : [["Chloe Bowers Unlocked", "Ludwig Meyer Unlocked", "Beth Penn Unlocked"],
|
||||||
|
["Meg Cooley Unlocked", "Jerry Montero Unlocked"],
|
||||||
|
["Mac Cooper Unlocked", "Javier Pérez Unlocked"]],
|
||||||
|
"has_all" : ["Billy Kramer Unlocked", "Sam Thompson Unlocked"]},
|
||||||
|
"randomize_level_unlocks" : {"has_any" : [["Mixing Station Mk II Unlock", "Mixing Station Unlock"]],
|
||||||
|
"has_all" : [
|
||||||
|
"Cauldron Unlock",
|
||||||
|
"Gasoline Unlock",
|
||||||
|
"Warehouse Access",
|
||||||
|
"Chemistry Station Unlock",
|
||||||
|
"Lab Oven Unlock",
|
||||||
|
"Acid Unlock",
|
||||||
|
"Phosphorus Unlock"]},
|
||||||
|
"randomize_suppliers" : {"has_all": ["Salvador Moreno Unlocked", "Shirley Watts Unlocked"]},
|
||||||
|
"randomize_cartel_influence" : {"has_all_counts": {"Cartel Influence, Suburbia" : 7}}
|
||||||
|
}
|
||||||
38
worlds/Schedule_I/docs/en_Schedule1.md
Normal file
38
worlds/Schedule_I/docs/en_Schedule1.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Schedule I
|
||||||
|
|
||||||
|
## 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 does randomization do to this game?
|
||||||
|
|
||||||
|
- Sends checks from all missions.
|
||||||
|
- Sends and receives all customer checks and items.
|
||||||
|
- Customers can be unlocked by receiving them through archipelago when randomize_customers is true in the YAML.
|
||||||
|
- checks for samples will be sent no matter the settings and are functional.
|
||||||
|
- Dealers will send checks when recruiting regardless of settings.
|
||||||
|
- Dealer AP unlock will allow user to then recruit them in game. Check is still possible when having them as a possible contact.
|
||||||
|
- Suppliers will not be unlockable if suppliers are randomized and only unlocked through ap items
|
||||||
|
- Suppliers give checks for unlocking them when suppliers are not randomized
|
||||||
|
- Every Action that would cause cartel influence in a region to drop is a check (x7 per region)
|
||||||
|
- Unable to reduce cartel influence naturally and cartel influence items added to pool when randomize_cartel_influence is true
|
||||||
|
- Level up rewards are suppressed when randomize_level_up_rewards is true
|
||||||
|
- Level up rewards are added to the item pool when randomize_level_up_rewards is true
|
||||||
|
- Whenever you'd nomrally get unlocks for leveling up, you get a check regardless of the option
|
||||||
|
- Deathlink is sent when a player dies or when they arrested. Recieved deathlink causes player to get arrested
|
||||||
|
- Property and busniesses give checks when purchased
|
||||||
|
- Randomized properties or businesses will not be unlocked when purchased if randomization on, properties and/or businesses will be added to the item pool
|
||||||
|
- Recipe checks are sent when recipes are learned
|
||||||
|
- Cash for trash are sent every 10 trash burned
|
||||||
|
- Filler items will be sent as deaddrop quests
|
||||||
|
|
||||||
|
## Once I'm inside Schedule1, how do I play Schedule1AP
|
||||||
|
|
||||||
|
Use In-Game UI to connect to server. Once connected, Create a new world and skip the prologue.
|
||||||
|
Make sure to save as often as you can, and you are able to rejoin. Restart your game if you need to rejoin the world!
|
||||||
|
If you want to play with friends (Untested): Invite them to your lobby. All of you connect as same archipelago Info, Load into world.
|
||||||
|
|
||||||
|
## A statement on the ownership over Schedule1AP
|
||||||
|
|
||||||
|
Schedule I apworld is MIT license. Created by MacH8s
|
||||||
33
worlds/Schedule_I/docs/setup_en.md
Normal file
33
worlds/Schedule_I/docs/setup_en.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Schedule 1 Archipelago Randomizer Setup Guide
|
||||||
|
|
||||||
|
## Required Software
|
||||||
|
|
||||||
|
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
|
||||||
|
- [The Schedule I apworld](https://github.com/MacH8s/Narcopelago/releases/latest),
|
||||||
|
- [Thunderstore Mod Manager](https://www.overwolf.com/app/thunderstore-thunderstore_mod_manager)
|
||||||
|
- [Narcopelago Mod](https://thunderstore.io/c/schedule-i/p/Narcopelago/Narcopelago/)
|
||||||
|
|
||||||
|
## How to play
|
||||||
|
|
||||||
|
First, you need a room to connect to. For this, you or someone you know has to generate a game.
|
||||||
|
This will not be explained here,
|
||||||
|
but you can check the [Archipelago Setup Guide](/tutorial/Archipelago/setup_en#generating-a-game).
|
||||||
|
|
||||||
|
You also need to have [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest) installed
|
||||||
|
and the [The Schedule I apworld](https://github.com/MacH8s/Narcopelago/releases/latest) installed into Archipelago.
|
||||||
|
|
||||||
|
### Install Mod
|
||||||
|
|
||||||
|
Install Thunderstore Mod Manager and open it.
|
||||||
|
Choose Schedule I and make a profile for Archipelago, name it whatever you like.
|
||||||
|
Search for 'Narcopelago' in the mod search and install it.
|
||||||
|
From there you can launch the game as Modded on the top right and your install has been complete! You must launch the game this way every time you want to play Archipelago.
|
||||||
|
|
||||||
|
### Joining Game
|
||||||
|
|
||||||
|
Use In-Game UI to connect to server. Once connected, Create a new world and skip the prologue.
|
||||||
|
Make sure to save as often as you can, and you are able to rejoin. Restart your game if you need to rejoin the world!
|
||||||
|
If you want to play with friends (Untested): Invite them to your lobby. All of you connect as same archipelago Info, Load into world.
|
||||||
|
|
||||||
|
## Switching Rooms
|
||||||
|
Restart your game to switch rooms. There may be some issues if you don't do so even if it shows things are working.
|
||||||
265
worlds/Schedule_I/items.py
Normal file
265
worlds/Schedule_I/items.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from BaseClasses import Item, ItemClassification
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .world import Schedule1World
|
||||||
|
|
||||||
|
ITEM_NAME_TO_ID = {}
|
||||||
|
RAW_ITEM_CLASSIFICATIONS = {}
|
||||||
|
|
||||||
|
fillers = []
|
||||||
|
traps = []
|
||||||
|
|
||||||
|
# Mapping from JSON classification strings to ItemClassification flags
|
||||||
|
CLASSIFICATION_MAP = {
|
||||||
|
"USEFUL": ItemClassification.useful,
|
||||||
|
"PROGRESSION": ItemClassification.progression,
|
||||||
|
"FILLER": ItemClassification.filler,
|
||||||
|
"PROGRESSION_SKIP_BALANCING": ItemClassification.progression_skip_balancing,
|
||||||
|
"TRAP": ItemClassification.trap
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_items_data(data):
|
||||||
|
"""Load item data from JSON and populate ITEM_NAME_TO_ID and RAW_ITEM_CLASSIFICATIONS."""
|
||||||
|
global ITEM_NAME_TO_ID, RAW_ITEM_CLASSIFICATIONS
|
||||||
|
|
||||||
|
ITEM_NAME_TO_ID = {item.name: item.modern_id for item in data.items.values()}
|
||||||
|
RAW_ITEM_CLASSIFICATIONS = {item.name: item.classification for item in data.items.values()}
|
||||||
|
|
||||||
|
|
||||||
|
# Each Item instance must correctly report the "game" it belongs to.
|
||||||
|
# To make this simple, it is common practice to subclass the basic Item class and override the "game" field.
|
||||||
|
class Schedule1Item(Item):
|
||||||
|
game = "Schedule I"
|
||||||
|
|
||||||
|
# To do this, it must define a function called world.get_filler_item_name(), which we will define in world.py later.
|
||||||
|
# For now, let's make a function that returns the name of a random filler item here in items.py.
|
||||||
|
def get_random_filler_item_name(world: Schedule1World) -> str:
|
||||||
|
# For this purpose, we need to use a random generator.
|
||||||
|
|
||||||
|
# IMPORTANT: Whenever you need to use a random generator, you must use world.random.
|
||||||
|
# This ensures that generating with the same generator seed twice yields the same output.
|
||||||
|
# DO NOT use a bare random object from Python's built-in random module.
|
||||||
|
|
||||||
|
# Check if we should generate a trap item based on the trap_chance option.
|
||||||
|
if world.random.randint(0, 99) < world.options.trap_chance:
|
||||||
|
return world.random.choice(traps)
|
||||||
|
|
||||||
|
# Otherwise, return a random filler item.
|
||||||
|
return world.random.choice(fillers)
|
||||||
|
|
||||||
|
|
||||||
|
def check_option_enabled(world: Schedule1World, option_name: str) -> bool:
|
||||||
|
"""Check if an option is enabled based on option name string."""
|
||||||
|
option_map = {
|
||||||
|
"randomize_customers": world.options.randomize_customers,
|
||||||
|
"randomize_dealers": world.options.randomize_dealers,
|
||||||
|
"randomize_suppliers": world.options.randomize_suppliers,
|
||||||
|
"randomize_level_unlocks": world.options.randomize_level_unlocks,
|
||||||
|
"randomize_cartel_influence": world.options.randomize_cartel_influence,
|
||||||
|
"randomize_business_properties": world.options.randomize_business_properties,
|
||||||
|
"randomize_drug_making_properties": world.options.randomize_drug_making_properties,
|
||||||
|
}
|
||||||
|
return bool(option_map.get(option_name, False))
|
||||||
|
|
||||||
|
|
||||||
|
def check_option_condition(world: Schedule1World, condition_key: str) -> bool:
|
||||||
|
"""
|
||||||
|
Parse and evaluate a compound option condition string.
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
- Simple: "randomize_level_unlocks" (option must be true)
|
||||||
|
- Negation: "!randomize_level_unlocks" (option must be false)
|
||||||
|
- Compound AND: "randomize_level_unlocks&!randomize_customers"
|
||||||
|
(first must be true AND second must be false)
|
||||||
|
|
||||||
|
Returns True if the condition is satisfied, False otherwise.
|
||||||
|
"""
|
||||||
|
parts = condition_key.split('&')
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
part = part.strip()
|
||||||
|
if not part:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if part.startswith('!'):
|
||||||
|
option_name = part[1:]
|
||||||
|
expected_value = False
|
||||||
|
else:
|
||||||
|
option_name = part
|
||||||
|
expected_value = True
|
||||||
|
|
||||||
|
actual_value = check_option_enabled(world, option_name)
|
||||||
|
|
||||||
|
if actual_value != expected_value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_classification(world: Schedule1World, classification_data) -> ItemClassification:
|
||||||
|
"""
|
||||||
|
Resolve the classification from raw JSON data based on world options.
|
||||||
|
|
||||||
|
classification_data can be:
|
||||||
|
- A string: "PROGRESSION"
|
||||||
|
- A list: ["PROGRESSION", "USEFUL"]
|
||||||
|
- A dict with conditions: {"!randomize_customers": ["PROGRESSION", "USEFUL"], "default": ["USEFUL"]}
|
||||||
|
"""
|
||||||
|
# Determine which classification strings to use
|
||||||
|
if isinstance(classification_data, dict):
|
||||||
|
# Conditional classification - find matching condition
|
||||||
|
classification_strings = None
|
||||||
|
for condition_key, value in classification_data.items():
|
||||||
|
if condition_key == "default":
|
||||||
|
continue # Handle default last
|
||||||
|
if check_option_condition(world, condition_key):
|
||||||
|
classification_strings = value
|
||||||
|
break
|
||||||
|
|
||||||
|
# Fall back to default if no condition matched
|
||||||
|
if classification_strings is None:
|
||||||
|
classification_strings = classification_data.get("default", "FILLER")
|
||||||
|
else:
|
||||||
|
classification_strings = classification_data
|
||||||
|
|
||||||
|
# Convert to ItemClassification
|
||||||
|
if isinstance(classification_strings, list):
|
||||||
|
classification = CLASSIFICATION_MAP[classification_strings[0]]
|
||||||
|
for class_name in classification_strings[1:]:
|
||||||
|
classification |= CLASSIFICATION_MAP[class_name]
|
||||||
|
else:
|
||||||
|
classification = CLASSIFICATION_MAP[classification_strings]
|
||||||
|
|
||||||
|
return classification
|
||||||
|
|
||||||
|
|
||||||
|
def create_item_with_correct_classification(world: Schedule1World, name: str) -> Schedule1Item:
|
||||||
|
# Our world class must have a create_item() function that can create any of our items by name at any time.
|
||||||
|
# So, we make this helper function that creates the item by name with the correct classification.
|
||||||
|
# Note: This function's content could just be the contents of world.create_item in world.py directly,
|
||||||
|
# but it seemed nicer to have it in its own function over here in items.py.
|
||||||
|
classification = resolve_classification(world, RAW_ITEM_CLASSIFICATIONS[name])
|
||||||
|
|
||||||
|
return Schedule1Item(name, classification, ITEM_NAME_TO_ID[name], world.player)
|
||||||
|
|
||||||
|
|
||||||
|
# With those two helper functions defined, let's now get to actually creating and submitting our itempool.
|
||||||
|
def create_all_items(world: Schedule1World, data) -> None:
|
||||||
|
# Creating items should generally be done via the world's create_item method.
|
||||||
|
# First, we create a list containing all the items that always exist.
|
||||||
|
|
||||||
|
itempool: list[Item] = []
|
||||||
|
|
||||||
|
# Create bundles bundles
|
||||||
|
# Hard coding the bundles here based on options is more efficient than adding them through the json data
|
||||||
|
for _ in range(world.options.number_of_cash_bundles):
|
||||||
|
itempool += [world.create_item("Cash Bundle")]
|
||||||
|
|
||||||
|
for _ in range(world.options.number_of_xp_bundles):
|
||||||
|
itempool += [world.create_item("XP Bundle")]
|
||||||
|
|
||||||
|
if world.options.randomize_level_unlocks:
|
||||||
|
# If the randomize_level_unlocks option is enabled, create all items tagged as "Level Up Reward".
|
||||||
|
itempool += [world.create_item(item.name) for item in data.items.values()
|
||||||
|
if "Level Up Reward" in item.tags]
|
||||||
|
|
||||||
|
# Add cartel influence items based on options
|
||||||
|
if world.options.randomize_cartel_influence:
|
||||||
|
if not world.options.randomize_customers:
|
||||||
|
for _ in range(world.options.cartel_influence_items_per_region):
|
||||||
|
itempool += [world.create_item(item.name) for item in data.items.values()
|
||||||
|
if "Cartel Influence" in item.tags and "Westville" not in item.tags
|
||||||
|
and "Suburbia" not in item.tags]
|
||||||
|
|
||||||
|
# Suburbia is required for Finishing the Job
|
||||||
|
for _ in range(world.options.cartel_influence_items_per_region):
|
||||||
|
itempool += [world.create_item(item.name) for item in data.items.values()
|
||||||
|
if "Cartel Influence" in item.tags and "Suburbia" in item.tags]
|
||||||
|
|
||||||
|
# Westville starts at 500 less cartel influence. Will have 5 less cartel items as well to declutter
|
||||||
|
# Westville is required for Vibin the Cybin
|
||||||
|
for _ in range(world.options.cartel_influence_items_per_region - 5):
|
||||||
|
itempool += [world.create_item(item.name) for item in data.items.values()
|
||||||
|
if "Cartel Influence" in item.tags and "Westville" in item.tags]
|
||||||
|
|
||||||
|
if world.options.randomize_business_properties:
|
||||||
|
itempool += [world.create_item(item.name) for item in data.items.values()
|
||||||
|
if "Business Property" in item.tags]
|
||||||
|
|
||||||
|
if world.options.randomize_drug_making_properties:
|
||||||
|
itempool += [world.create_item(item.name) for item in data.items.values()
|
||||||
|
if "Drug Making Property" in item.tags]
|
||||||
|
|
||||||
|
if world.options.randomize_dealers:
|
||||||
|
itempool += [world.create_item(item.name) for item in data.items.values()
|
||||||
|
if "Dealer" in item.tags and "Default" not in item.tags]
|
||||||
|
|
||||||
|
if world.options.randomize_customers:
|
||||||
|
itempool += [world.create_item(item.name) for item in data.items.values()
|
||||||
|
if "Customer" in item.tags and "Default" not in item.tags]
|
||||||
|
|
||||||
|
if world.options.randomize_suppliers:
|
||||||
|
itempool += [world.create_item(item.name) for item in data.items.values()
|
||||||
|
if "Supplier" in item.tags and "Default" not in item.tags]
|
||||||
|
|
||||||
|
if world.options.randomize_sewer_key:
|
||||||
|
itempool += [world.create_item(item.name) for item in data.items.values()
|
||||||
|
if "Sewer" in item.tags]
|
||||||
|
|
||||||
|
# Removed these from checks
|
||||||
|
if world.options.randomize_customers:
|
||||||
|
starting_kyle_cooley = world.create_item("Kyle Cooley Unlocked")
|
||||||
|
world.push_precollected(starting_kyle_cooley)
|
||||||
|
starting_austin_steiner = world.create_item("Austin Steiner Unlocked")
|
||||||
|
world.push_precollected(starting_austin_steiner)
|
||||||
|
starting_kathy_henderson = world.create_item("Kathy Henderson Unlocked")
|
||||||
|
world.push_precollected(starting_kathy_henderson)
|
||||||
|
starting_jessi_waters = world.create_item("Jessi Waters Unlocked")
|
||||||
|
world.push_precollected(starting_jessi_waters)
|
||||||
|
starting_sam_thompson = world.create_item("Sam Thompson Unlocked")
|
||||||
|
world.push_precollected(starting_sam_thompson)
|
||||||
|
starting_mick_lubbin = world.create_item("Mick Lubbin Unlocked")
|
||||||
|
world.push_precollected(starting_mick_lubbin)
|
||||||
|
|
||||||
|
# Set up traps
|
||||||
|
for item in data.items.values():
|
||||||
|
resolved_classification = resolve_classification(world, item.classification)
|
||||||
|
if resolved_classification == ItemClassification.trap:
|
||||||
|
# Create list of traps
|
||||||
|
traps.append(item.name)
|
||||||
|
|
||||||
|
filler_conditions = {
|
||||||
|
"Bad Filler" : world.options.ban_bad_filler_items,
|
||||||
|
"Ban Progression Skip" : world.options.ban_progression_skip_items}
|
||||||
|
|
||||||
|
# set up fillers
|
||||||
|
for item in data.items.values():
|
||||||
|
resolved_classification = resolve_classification(world, item.classification)
|
||||||
|
if resolved_classification == ItemClassification.filler:
|
||||||
|
is_valid = True
|
||||||
|
for tag, should_ban in filler_conditions.items():
|
||||||
|
if should_ban and tag in item.tags:
|
||||||
|
is_valid = False
|
||||||
|
break
|
||||||
|
if is_valid:
|
||||||
|
fillers.append(item.name)
|
||||||
|
|
||||||
|
# The length of our itempool is easy to determine, since we have it as a list.
|
||||||
|
number_of_items = len(itempool)
|
||||||
|
|
||||||
|
# What we actually want is the number of *unfilled* locations. Luckily, there is a helper method for this:
|
||||||
|
number_of_unfilled_locations = len(world.multiworld.get_unfilled_locations(world.player))
|
||||||
|
|
||||||
|
# Now, we just subtract the number of items from the number of locations to get the number of empty item slots.
|
||||||
|
needed_number_of_filler_items = number_of_unfilled_locations - number_of_items
|
||||||
|
|
||||||
|
# Finally, we create that many filler items and add them to the itempool.
|
||||||
|
itempool += [world.create_filler() for _ in range(needed_number_of_filler_items)]
|
||||||
|
|
||||||
|
# This is how the generator actually knows about the existence of our items.
|
||||||
|
world.multiworld.itempool += itempool
|
||||||
|
|
||||||
104
worlds/Schedule_I/json_data.py
Normal file
104
worlds/Schedule_I/json_data.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
"""
|
||||||
|
Centralized data loader for Schedule1 world.
|
||||||
|
Loads and parses items.json, locations.json, and regions.json into structured objects for easy access.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
import pkgutil
|
||||||
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
|
||||||
|
def load_json_data(data_name: str) -> Union[List[Any], Dict[str, Any]]:
|
||||||
|
return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name).decode("utf-8-sig"))
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ItemData:
|
||||||
|
"""Represents item data from items.json"""
|
||||||
|
name: str
|
||||||
|
modern_id: int
|
||||||
|
classification: Union[str, List[str], Dict[str, Union[str, List[str]]]]
|
||||||
|
tags: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LocationData:
|
||||||
|
"""Represents location data from locations.json"""
|
||||||
|
name: str
|
||||||
|
region: str
|
||||||
|
requirements: Union[bool, Dict[str, Any]]
|
||||||
|
tags: List[str]
|
||||||
|
modern_id: int
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RegionData:
|
||||||
|
"""Represents region data from regions.json"""
|
||||||
|
name: str
|
||||||
|
connections: Dict[str, Union[bool, Dict[str, Any]]]
|
||||||
|
|
||||||
|
class Schedule1ItemData:
|
||||||
|
"""Container for all Schedule1 game data loaded from JSON files"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
items_raw = load_json_data("items.json")
|
||||||
|
|
||||||
|
# Parse items into ItemData objects
|
||||||
|
# Classification is stored raw - resolution happens in items.py based on world options
|
||||||
|
self.items: Dict[str, ItemData] = {}
|
||||||
|
for item_name, item_info in items_raw.items():
|
||||||
|
self.items[item_name] = ItemData(
|
||||||
|
name=item_name,
|
||||||
|
modern_id=item_info["modern_id"],
|
||||||
|
classification=item_info["classification"],
|
||||||
|
tags=item_info["tags"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Schedule1LocationData:
|
||||||
|
"""Container for all Schedule1 location data loaded from JSON files"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
locations_raw = load_json_data("locations.json")
|
||||||
|
|
||||||
|
# Parse locations into LocationData objects
|
||||||
|
self.locations: Dict[str, LocationData] = {}
|
||||||
|
for location_name, location_info in locations_raw.items():
|
||||||
|
self.locations[location_name] = LocationData(
|
||||||
|
name=location_name,
|
||||||
|
region=location_info["region"],
|
||||||
|
requirements=location_info["requirements"],
|
||||||
|
tags=location_info["tags"],
|
||||||
|
modern_id=location_info["modern_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
class Schedule1RegionData:
|
||||||
|
"""Container for all Schedule1 region data loaded from JSON files"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
regions_raw = load_json_data("regions.json")
|
||||||
|
|
||||||
|
# Parse regions into RegionData objects
|
||||||
|
self.regions: Dict[str, RegionData] = {}
|
||||||
|
for region_name, region_info in regions_raw.items():
|
||||||
|
self.regions[region_name] = RegionData(
|
||||||
|
name=region_name,
|
||||||
|
connections=region_info["connections"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Schedule1VictoryData:
|
||||||
|
"""Container for victory conditions loaded from victory.json"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# victory.json is structured as {option_name: {method: value, ...}, ...}
|
||||||
|
# This is the same structure as requirements in locations/regions
|
||||||
|
self.requirements: Dict[str, Any] = load_json_data("victory.json")
|
||||||
|
|
||||||
|
|
||||||
|
# Create singleton instances for easy import
|
||||||
|
schedule1_item_data = Schedule1ItemData()
|
||||||
|
schedule1_location_data = Schedule1LocationData()
|
||||||
|
schedule1_region_data = Schedule1RegionData()
|
||||||
|
schedule1_victory_data = Schedule1VictoryData()
|
||||||
118
worlds/Schedule_I/locations.py
Normal file
118
worlds/Schedule_I/locations.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Dict
|
||||||
|
|
||||||
|
from BaseClasses import ItemClassification, Location
|
||||||
|
|
||||||
|
from . import items
|
||||||
|
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .world import Schedule1World
|
||||||
|
|
||||||
|
# Every location must have a unique integer ID associated with it.
|
||||||
|
LOCATION_NAME_TO_ID = {}
|
||||||
|
def load_locations_data(data):
|
||||||
|
"""Load location data from JSON and populate LOCATION_NAME_TO_ID."""
|
||||||
|
global LOCATION_NAME_TO_ID
|
||||||
|
LOCATION_NAME_TO_ID = {name: loc.modern_id for name, loc in data.locations.items()}
|
||||||
|
# must include all locations, even those not created based on options
|
||||||
|
drug_types = [
|
||||||
|
("Weed", 195),
|
||||||
|
("Meth", 210),
|
||||||
|
("Shrooms", 225),
|
||||||
|
("Cocaine", 240),
|
||||||
|
]
|
||||||
|
for drug_name, start_id in drug_types:
|
||||||
|
for i in range(1, 16):
|
||||||
|
LOCATION_NAME_TO_ID[f"{drug_name} Recipe Check, {i}"] = start_id + (i - 1)
|
||||||
|
|
||||||
|
start_id = 600 # Starting ID for Cash for Trash locations
|
||||||
|
for i in range(1, 51):
|
||||||
|
LOCATION_NAME_TO_ID[f"Cash for Trash {i}, Collect {i * 10} pieces of trash"] = start_id + (i - 1)
|
||||||
|
# Each Location instance must correctly report the "game" it belongs to.
|
||||||
|
# To make this simple, it is common practice to subclass the basic Location class and override the "game" field.
|
||||||
|
class Schedule1Location(Location):
|
||||||
|
game = "Schedule I"
|
||||||
|
|
||||||
|
|
||||||
|
# Let's make one more helper method before we begin actually creating locations.
|
||||||
|
# Later on in the code, we'll want specific subsections of LOCATION_NAME_TO_ID.
|
||||||
|
# To reduce the chance of copy-paste errors writing something like {"Chest": LOCATION_NAME_TO_ID["Chest"]},
|
||||||
|
# let's make a helper method that takes a list of location names and returns them as a dict with their IDs.
|
||||||
|
# Note: There is a minor typing quirk here. Some functions want location addresses to be an "int | None",
|
||||||
|
# so while our function here only ever returns dict[str, int], we annotate it as dict[str, int | None].
|
||||||
|
def get_location_names_with_ids(location_names: list[str]) -> dict[str, int | None]:
|
||||||
|
return {location_name: LOCATION_NAME_TO_ID[location_name] for location_name in location_names}
|
||||||
|
|
||||||
|
|
||||||
|
def create_all_locations(world: Schedule1World, locationData) -> None:
|
||||||
|
create_regular_locations(world, locationData)
|
||||||
|
|
||||||
|
|
||||||
|
def create_regular_locations(world: Schedule1World, data) -> None:
|
||||||
|
# Get all unique region names from location data
|
||||||
|
region_names = set(loc.region for loc in data.locations.values())
|
||||||
|
|
||||||
|
# Load all regions into a dictionary once for efficient access
|
||||||
|
regions_dict: Dict[str, any] = {}
|
||||||
|
for region_name in sorted(region_names):
|
||||||
|
regions_dict[region_name] = world.get_region(region_name)
|
||||||
|
|
||||||
|
# Group locations by region, excluding suppliers if randomized
|
||||||
|
locations_by_region: Dict[str, list[str]] = {region: [] for region in sorted(region_names)}
|
||||||
|
|
||||||
|
for loc_name, loc_data in data.locations.items():
|
||||||
|
# Skip supplier locations if randomize_suppliers is enabled
|
||||||
|
if world.options.randomize_suppliers and "Supplier" in loc_data.tags:
|
||||||
|
continue
|
||||||
|
locations_by_region[loc_data.region].append(loc_name)
|
||||||
|
|
||||||
|
# Add all locations to their respective regions
|
||||||
|
for region_name, location_names in locations_by_region.items():
|
||||||
|
if location_names: # Only add if there are locations
|
||||||
|
region = regions_dict[region_name]
|
||||||
|
region.add_locations(get_location_names_with_ids(location_names), Schedule1Location)
|
||||||
|
|
||||||
|
# Recipe checks - Only include the number specified by the RecipeChecks option
|
||||||
|
if world.options.recipe_checks > 0:
|
||||||
|
# Get each recipe region
|
||||||
|
weed_recipe_region = world.get_region("Weed Recipe Checks")
|
||||||
|
meth_recipe_region = world.get_region("Meth Recipe Checks")
|
||||||
|
shrooms_recipe_region = world.get_region("Shrooms Recipe Checks")
|
||||||
|
cocaine_recipe_region = world.get_region("Cocaine Recipe Checks")
|
||||||
|
|
||||||
|
# Drug types with their regions and starting IDs in LOCATION_NAME_TO_ID
|
||||||
|
# These magic numbers correspond to reserved IDs for recipe checks
|
||||||
|
drug_types = [
|
||||||
|
("Weed", weed_recipe_region),
|
||||||
|
("Meth", meth_recipe_region),
|
||||||
|
("Shrooms", shrooms_recipe_region),
|
||||||
|
("Cocaine", cocaine_recipe_region),
|
||||||
|
]
|
||||||
|
# Add needed recipe locations to location name to id
|
||||||
|
for drug_name, region in drug_types:
|
||||||
|
recipe_locations = []
|
||||||
|
for i in range(1, world.options.recipe_checks + 1):
|
||||||
|
recipe_locations.append(f"{drug_name} Recipe Check, {i}")
|
||||||
|
|
||||||
|
recipe_locations_dict = get_location_names_with_ids(recipe_locations)
|
||||||
|
region.add_locations(recipe_locations_dict, Schedule1Location)
|
||||||
|
|
||||||
|
# Cash for Trash checks - Only include the number specified by the CashForTrash option
|
||||||
|
# Add to Overworld region
|
||||||
|
cash_for_trash_count = world.options.cash_for_trash
|
||||||
|
if cash_for_trash_count > 0:
|
||||||
|
regions = {
|
||||||
|
100 : regions_dict["Overworld"],
|
||||||
|
200 : regions_dict["Dodgy Dealing"],
|
||||||
|
300 : regions_dict["Mixing Mania"],
|
||||||
|
400 : regions_dict["We Need To Cook|2"],
|
||||||
|
500 : regions_dict["Finishing the Job"]
|
||||||
|
}
|
||||||
|
cash_for_trash_locations = []
|
||||||
|
for i in range(1, cash_for_trash_count + 1):
|
||||||
|
cash_for_trash_locations.append(f"Cash for Trash {i}, Collect {i * 10} pieces of trash")
|
||||||
|
cash_for_trash_locations_dict = get_location_names_with_ids(cash_for_trash_locations)
|
||||||
|
regions[ceil(201 / 100) * 100].add_locations(cash_for_trash_locations_dict, Schedule1Location)
|
||||||
322
worlds/Schedule_I/options.py
Normal file
322
worlds/Schedule_I/options.py
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from Options import (Choice, DefaultOnToggle, OptionGroup, PerGameCommonOptions,
|
||||||
|
Range, DeathLink)
|
||||||
|
|
||||||
|
# In this file, we define the options the player can pick.
|
||||||
|
# The most common types of options are Toggle, Range and Choice.
|
||||||
|
|
||||||
|
# Options will be in the game's template yaml.
|
||||||
|
# They will be represented by checkboxes, sliders etc. on the game's options page on the website.
|
||||||
|
# (Note: Options can also be made invisible from either of these places by overriding Option.visibility.
|
||||||
|
# APQuest doesn't have an example of this, but this can be used for secret / hidden / advanced options.)
|
||||||
|
|
||||||
|
# For further reading on options, you can also read the Options API Document:
|
||||||
|
# https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/options%20api.md
|
||||||
|
|
||||||
|
|
||||||
|
# The first type of Option we'll discuss is the Toggle.
|
||||||
|
# A toggle is an option that can either be on or off. This will be represented by a checkbox on the website.
|
||||||
|
# The default for a toggle is "off".
|
||||||
|
# If you want a toggle to be on by default, you can use the "DefaultOnToggle" class instead of the "Toggle" class.
|
||||||
|
class Goal(Choice):
|
||||||
|
"""
|
||||||
|
The goal to win the game.
|
||||||
|
missions_networth: Reach a specificed net worth and complete the main story.
|
||||||
|
networth_only: Reach a specificed net worth.
|
||||||
|
missions_only: Complete the main story.
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Goal"
|
||||||
|
|
||||||
|
option_networth_only = 0
|
||||||
|
option_missions_networth = 1
|
||||||
|
option_missions_only = 2
|
||||||
|
|
||||||
|
# Choice options must define an explicit default value.
|
||||||
|
default = option_missions_networth
|
||||||
|
|
||||||
|
class RandomizeLevelUnlocks(DefaultOnToggle):
|
||||||
|
"""
|
||||||
|
Leveling up is always a check regardless of this option.
|
||||||
|
Block Boss I, 30,350xp, will be the highest checks will go to.
|
||||||
|
Things you unlock by leveling up will be shuffled into the item pool if this option is enabled.
|
||||||
|
When this is enabled, you will no longer get level up rewards naturally.
|
||||||
|
"""
|
||||||
|
display_name = "Randomize Level Unlocks"
|
||||||
|
|
||||||
|
|
||||||
|
class NumberOfXpBundles(Range):
|
||||||
|
"""
|
||||||
|
Number of XP bundles to include in the item pool.
|
||||||
|
min to max xp bundles will be interpolated based on number of bundles.
|
||||||
|
Meaning there will be 1 min bundle and 1 max bundle and the rest will be evenly distributed in between.
|
||||||
|
If default min/max are used, 25 bundles give max level xp. 12 gives out about a quarter of that.
|
||||||
|
16,425 xp is needed to complete the game, but this can be done naturally, bundles speed up process.
|
||||||
|
If 1 bundle is chosen, only the minimum xp bundle will be in the item pool.
|
||||||
|
"""
|
||||||
|
range_start = 0
|
||||||
|
range_end = 20
|
||||||
|
default = 12
|
||||||
|
|
||||||
|
|
||||||
|
class AmountOfXpPerBundleMin(Range):
|
||||||
|
"""
|
||||||
|
Min amount of XP per bundle included in the item pool.
|
||||||
|
Each bundle is worth the specified amount of XP.
|
||||||
|
This option is only relevant if NumberOfXpBundles > 0.
|
||||||
|
"""
|
||||||
|
range_start = 1
|
||||||
|
range_end = 1000
|
||||||
|
default = 100
|
||||||
|
|
||||||
|
|
||||||
|
class AmountOfXpPerBundleMax(Range):
|
||||||
|
"""
|
||||||
|
Max amount of XP per bundle included in the item pool.
|
||||||
|
Each bundle is worth the specified amount of XP.
|
||||||
|
This option is only relevant if NumberOfXpBundles > 0.
|
||||||
|
"""
|
||||||
|
range_start = 1000
|
||||||
|
range_end = 10000
|
||||||
|
default = 5000
|
||||||
|
|
||||||
|
|
||||||
|
class NumberOfCashBundles(Range):
|
||||||
|
"""
|
||||||
|
Number of cash bundles to include in the item pool.
|
||||||
|
min to max cash bundles will be interpolated based on number of bundles.
|
||||||
|
Meaning there will be 1 min bundle and 1 max bundle and the rest will be evenly distributed in between.
|
||||||
|
If 1 bundle is chosen, only the minimum cash bundle will be in the item pool.
|
||||||
|
Defaults will give out ~87k cash with 20 bundles. 46 bundles gives out ~200k cash.
|
||||||
|
"""
|
||||||
|
range_start = 0
|
||||||
|
range_end = 20
|
||||||
|
default = 12
|
||||||
|
|
||||||
|
|
||||||
|
class AmountOfCashPerBundleMin(Range):
|
||||||
|
"""
|
||||||
|
Min amount of cash per bundle included in the item pool.
|
||||||
|
Each bundle is worth the specified amount of cash.
|
||||||
|
This option is only relevant if NumberOfCashBundles > 0.
|
||||||
|
"""
|
||||||
|
range_start = 1
|
||||||
|
range_end = 1500
|
||||||
|
default = 1500
|
||||||
|
|
||||||
|
|
||||||
|
class AmountOfCashPerBundleMax(Range):
|
||||||
|
"""
|
||||||
|
Max amount of cash per bundle included in the item pool.
|
||||||
|
Each bundle is worth the specified amount of cash.
|
||||||
|
This option is only relevant if NumberOfCashBundles > 0.
|
||||||
|
"""
|
||||||
|
range_start = 1500
|
||||||
|
range_end = 100000
|
||||||
|
default = 10000
|
||||||
|
|
||||||
|
|
||||||
|
class NetworthAmountRequired(Range):
|
||||||
|
"""
|
||||||
|
The net worth amount required to win the game.
|
||||||
|
This option is only relevant if the goal includes net worth.
|
||||||
|
"""
|
||||||
|
range_start = 10000
|
||||||
|
range_end = 10000000
|
||||||
|
default = 100000
|
||||||
|
|
||||||
|
class BanBadFillerItems(DefaultOnToggle):
|
||||||
|
"""
|
||||||
|
If enabled, bad filler items will not be included in the item pool.
|
||||||
|
"""
|
||||||
|
display_name = "Ban Bad Filler Items"
|
||||||
|
|
||||||
|
|
||||||
|
class BanProgressionSkipItems(DefaultOnToggle):
|
||||||
|
"""
|
||||||
|
If enabled, filler items that allow for progression skips will not be included in the item pool.
|
||||||
|
This can add variety to the seed to allow for out of logic skips.
|
||||||
|
"""
|
||||||
|
display_name = "Ban Progression Skip Filler Items"
|
||||||
|
|
||||||
|
|
||||||
|
class TrapChance(Range):
|
||||||
|
"""
|
||||||
|
Percentage chance that a filler item will be a trap.
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Trap Chance"
|
||||||
|
|
||||||
|
range_start = 0
|
||||||
|
range_end = 100
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeCartelInfluence(DefaultOnToggle):
|
||||||
|
"""
|
||||||
|
Determines if cartel influence will be randomized into the item pool.
|
||||||
|
7 Bundles of 100 cartel influence per in-game region that applies.
|
||||||
|
Every 100 cartel influcence by the player counts as a check regardless of this option.
|
||||||
|
This option removes the player's ability to earn cartel influence naturally
|
||||||
|
"""
|
||||||
|
display_name = "Randomize Cartel Influence"
|
||||||
|
|
||||||
|
|
||||||
|
class CartelInfluenceItemsPerRegion(Range):
|
||||||
|
"""
|
||||||
|
Number of cartel influence Items to include in the item pool per region.
|
||||||
|
Each item is worth 100 cartel influence.
|
||||||
|
7 needed to unlock region, recommend to add extra of each region.
|
||||||
|
Westville has 5 less checks and 5 less cartel influence items
|
||||||
|
This option is only relevant if Randomize Cartel Influence is enabled.
|
||||||
|
"""
|
||||||
|
range_start = 7
|
||||||
|
range_end = 12
|
||||||
|
default = 10
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeDrugMakingProperties(DefaultOnToggle):
|
||||||
|
"""
|
||||||
|
Determines if drug making properties will be added into the item pool.
|
||||||
|
Purchasing drug making properties become checks, but you do not purchase them.
|
||||||
|
Realtor will have AP items instead of drug making properties if this is enabled.
|
||||||
|
This does not include ones you must purchase through missions.
|
||||||
|
"""
|
||||||
|
display_name = "Randomize drug making Properties"
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeBusinessProperties(DefaultOnToggle):
|
||||||
|
"""
|
||||||
|
Determines if business properties will be added into the item pool.
|
||||||
|
The Realtor will have AP items instead of business properties if this is enabled.
|
||||||
|
"""
|
||||||
|
display_name = "Randomize business making Properties"
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeDealers(DefaultOnToggle):
|
||||||
|
"""
|
||||||
|
Determines if dealers will be added into the item pool.
|
||||||
|
Recruiting dealers become checks, but you do not recruit them.
|
||||||
|
This does not include Benji, who is required for story progression.
|
||||||
|
"""
|
||||||
|
display_name = "Randomize Dealers"
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeCustomers(DefaultOnToggle):
|
||||||
|
"""
|
||||||
|
Determines if customers will be added into the item pool.
|
||||||
|
Customers are checks regardless if this is toggled on or off
|
||||||
|
Player can still get successful samples as checks
|
||||||
|
"""
|
||||||
|
display_name = "Randomize Customers"
|
||||||
|
|
||||||
|
class RandomizeSuppliers(DefaultOnToggle):
|
||||||
|
"""
|
||||||
|
Determines if suppliers will be added into the item pool.
|
||||||
|
If enabled, befriending suppliers no longer become checks.
|
||||||
|
Albert Hoover is unlocked by default
|
||||||
|
"""
|
||||||
|
display_name = "Randomize Suppliers"
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeSewerKey(DefaultOnToggle):
|
||||||
|
"""
|
||||||
|
Determines if the Sewer Key will be added into the item pool.
|
||||||
|
If enabled, Jen Herd will no longer sell sewer key.
|
||||||
|
Buying the sewer key from Jen Herd is a check no matter if this option is toggled on or off.
|
||||||
|
"""
|
||||||
|
display_name = "Randomize Sewer Key"
|
||||||
|
|
||||||
|
class RecipeChecks(Range):
|
||||||
|
"""
|
||||||
|
Number of recipe checks to include in the item pool.
|
||||||
|
These are recipes per drug type.
|
||||||
|
10 means 10 weed recipes, 10 meth recipes, etc.
|
||||||
|
"""
|
||||||
|
range_start = 0
|
||||||
|
range_end = 15
|
||||||
|
default = 5
|
||||||
|
|
||||||
|
class CashForTrash(Range):
|
||||||
|
"""
|
||||||
|
Number of checks for each 10 pieces of trash collected
|
||||||
|
50 = 500 total pieces of trash which is equal to the achiemvement.
|
||||||
|
"""
|
||||||
|
range_start = 0
|
||||||
|
range_end = 50
|
||||||
|
default = 5
|
||||||
|
|
||||||
|
# We must now define a dataclass inheriting from PerGameCommonOptions that we put all our options in.
|
||||||
|
# This is in the format "option_name_in_snake_case: OptionClassName".
|
||||||
|
@dataclass
|
||||||
|
class Schedule1Options(PerGameCommonOptions):
|
||||||
|
goal: Goal
|
||||||
|
networth_amount_required: NetworthAmountRequired
|
||||||
|
ban_bad_filler_items: BanBadFillerItems
|
||||||
|
ban_progression_skip_items: BanProgressionSkipItems
|
||||||
|
trap_chance: TrapChance
|
||||||
|
number_of_xp_bundles: NumberOfXpBundles
|
||||||
|
amount_of_xp_per_bundle_min: AmountOfXpPerBundleMin
|
||||||
|
amount_of_xp_per_bundle_max: AmountOfXpPerBundleMax
|
||||||
|
number_of_cash_bundles: NumberOfCashBundles
|
||||||
|
amount_of_cash_per_bundle_min: AmountOfCashPerBundleMin
|
||||||
|
amount_of_cash_per_bundle_max: AmountOfCashPerBundleMax
|
||||||
|
randomize_level_unlocks: RandomizeLevelUnlocks
|
||||||
|
randomize_cartel_influence: RandomizeCartelInfluence
|
||||||
|
cartel_influence_items_per_region: CartelInfluenceItemsPerRegion
|
||||||
|
randomize_drug_making_properties: RandomizeDrugMakingProperties
|
||||||
|
randomize_business_properties: RandomizeBusinessProperties
|
||||||
|
randomize_dealers: RandomizeDealers
|
||||||
|
randomize_customers: RandomizeCustomers
|
||||||
|
randomize_suppliers: RandomizeSuppliers
|
||||||
|
randomize_sewer_key: RandomizeSewerKey
|
||||||
|
recipe_checks: RecipeChecks
|
||||||
|
cash_for_trash: CashForTrash
|
||||||
|
death_link: DeathLink
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# If we want to group our options by similar type, we can do so as well. This looks nice on the website.
|
||||||
|
option_groups = [
|
||||||
|
OptionGroup(
|
||||||
|
"Gameplay Options",
|
||||||
|
[Goal, NetworthAmountRequired, NumberOfXpBundles, AmountOfXpPerBundleMin, AmountOfXpPerBundleMax,
|
||||||
|
NumberOfCashBundles, AmountOfCashPerBundleMin, AmountOfCashPerBundleMax,
|
||||||
|
BanBadFillerItems, BanProgressionSkipItems, TrapChance,
|
||||||
|
RandomizeLevelUnlocks, RandomizeCartelInfluence, CartelInfluenceItemsPerRegion,
|
||||||
|
RandomizeCustomers, RandomizeDealers, RandomizeSuppliers, RandomizeSewerKey,
|
||||||
|
RandomizeDrugMakingProperties, RandomizeBusinessProperties,
|
||||||
|
RecipeChecks, CashForTrash,
|
||||||
|
DeathLink],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Finally, we can define some option presets if we want the player to be able to quickly choose a specific "mode".
|
||||||
|
option_presets = {
|
||||||
|
"Default": {
|
||||||
|
"goal": Goal.default,
|
||||||
|
"number_of_xp_bundles": NumberOfXpBundles.default,
|
||||||
|
"amount_of_xp_per_bundle_min": AmountOfXpPerBundleMin.default,
|
||||||
|
"amount_of_xp_per_bundle_max": AmountOfXpPerBundleMax.default,
|
||||||
|
"number_of_cash_bundles": NumberOfCashBundles.default,
|
||||||
|
"amount_of_cash_per_bundle_min": AmountOfCashPerBundleMin.default,
|
||||||
|
"amount_of_cash_per_bundle_max": AmountOfCashPerBundleMax.default,
|
||||||
|
"networth_amount_required": NetworthAmountRequired.default,
|
||||||
|
"ban_bad_filler_items": BanBadFillerItems.default,
|
||||||
|
"ban_progression_skip_items": BanProgressionSkipItems.default,
|
||||||
|
"trap_chance": TrapChance.default,
|
||||||
|
"randomize_cartel_influence": True,
|
||||||
|
"randomize_drug_making_properties": True,
|
||||||
|
"randomize_business_properties": True,
|
||||||
|
"randomize_dealers": True,
|
||||||
|
"randomize_customers": True,
|
||||||
|
"cartel_influence_items_per_region": CartelInfluenceItemsPerRegion.default,
|
||||||
|
"recipe_checks": RecipeChecks.default,
|
||||||
|
"cash_for_trash": CashForTrash.default,
|
||||||
|
"randomize_level_unlocks": True,
|
||||||
|
"randomize_suppliers": True,
|
||||||
|
"randomize_sewer_key": True,
|
||||||
|
"death_link": DeathLink.default,
|
||||||
|
}
|
||||||
|
}
|
||||||
52
worlds/Schedule_I/regions.py
Normal file
52
worlds/Schedule_I/regions.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Dict
|
||||||
|
|
||||||
|
from BaseClasses import Region
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .world import Schedule1World as Schedule1World
|
||||||
|
|
||||||
|
# A region is a container for locations ("checks"), which connects to other regions via "Entrance" objects.
|
||||||
|
# Many games will model their Regions after physical in-game places, but you can also have more abstract regions.
|
||||||
|
# For a location to be in logic, its containing region must be reachable.
|
||||||
|
# The Entrances connecting regions can have rules - more on that in rules.py.
|
||||||
|
# This makes regions especially useful for traversal logic ("Can the player reach this part of the map?")
|
||||||
|
|
||||||
|
# Every location must be inside a region, and you must have at least one region.
|
||||||
|
# This is why we create regions first, and then later we create the locations (in locations.py).
|
||||||
|
|
||||||
|
|
||||||
|
def create_and_connect_regions(world: Schedule1World, region_data) -> None:
|
||||||
|
create_all_regions(world, region_data)
|
||||||
|
connect_regions(world, region_data)
|
||||||
|
|
||||||
|
|
||||||
|
def create_all_regions(world: Schedule1World, region_data) -> None:
|
||||||
|
# Create all regions from regions.json
|
||||||
|
regions = []
|
||||||
|
|
||||||
|
for region_name in region_data.regions.keys():
|
||||||
|
region = Region(region_name, world.player, world.multiworld)
|
||||||
|
regions.append(region)
|
||||||
|
|
||||||
|
# Add all regions to multiworld.regions so that AP knows about their existence
|
||||||
|
world.multiworld.regions += regions
|
||||||
|
|
||||||
|
|
||||||
|
def connect_regions(world: Schedule1World, region_data) -> None:
|
||||||
|
# Load all regions into a dictionary once to avoid repeated get_region calls
|
||||||
|
regions_dict: Dict[str, Region] = {
|
||||||
|
region_name: world.get_region(region_name)
|
||||||
|
for region_name in region_data.regions.keys()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Connect all regions based on the connections defined in regions.json
|
||||||
|
for region_name, region_info in region_data.regions.items():
|
||||||
|
source_region = regions_dict[region_name]
|
||||||
|
|
||||||
|
# Iterate through all connections for this region
|
||||||
|
for connected_region_name in region_info.connections.keys():
|
||||||
|
target_region = regions_dict[connected_region_name]
|
||||||
|
entrance_name = f"{region_name} to {connected_region_name}"
|
||||||
|
source_region.connect(target_region, entrance_name)
|
||||||
251
worlds/Schedule_I/rules.py
Normal file
251
worlds/Schedule_I/rules.py
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Callable, Dict, Any, Union
|
||||||
|
|
||||||
|
from BaseClasses import CollectionState
|
||||||
|
from worlds.generic.Rules import add_rule, set_rule
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .world import Schedule1World
|
||||||
|
|
||||||
|
|
||||||
|
def set_all_rules(world: Schedule1World, locationData, regionData, victoryData) -> None:
|
||||||
|
# In order for AP to generate an item layout that is actually possible for the player to complete,
|
||||||
|
# we need to define rules for our Entrances and Locations.
|
||||||
|
# Note: Regions do not have rules, the Entrances connecting them do!
|
||||||
|
# We'll do entrances first, then locations, and then finally we set our victory condition.
|
||||||
|
|
||||||
|
set_all_entrance_rules(world, regionData)
|
||||||
|
set_all_location_rules(world, locationData)
|
||||||
|
set_completion_condition(world, victoryData)
|
||||||
|
|
||||||
|
|
||||||
|
def check_option_enabled(world: Schedule1World, option_name: str) -> bool:
|
||||||
|
"""Check if an option is enabled based on option name string from JSON."""
|
||||||
|
option_map = {
|
||||||
|
"randomize_customers": world.options.randomize_customers,
|
||||||
|
"randomize_dealers": world.options.randomize_dealers,
|
||||||
|
"randomize_suppliers": world.options.randomize_suppliers,
|
||||||
|
"randomize_level_unlocks": world.options.randomize_level_unlocks,
|
||||||
|
"randomize_cartel_influence": world.options.randomize_cartel_influence,
|
||||||
|
"randomize_business_properties": world.options.randomize_business_properties,
|
||||||
|
"randomize_drug_making_properties": world.options.randomize_drug_making_properties,
|
||||||
|
}
|
||||||
|
return bool(option_map.get(option_name, False))
|
||||||
|
|
||||||
|
|
||||||
|
def check_option_condition(world: Schedule1World, condition_key: str) -> bool:
|
||||||
|
"""
|
||||||
|
Parse and evaluate a compound option condition string.
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
- Simple: "randomize_level_unlocks" (option must be true)
|
||||||
|
- Negation: "!randomize_level_unlocks" (option must be false)
|
||||||
|
- Compound AND: "randomize_level_unlocks&!randomize_customers"
|
||||||
|
(first must be true AND second must be false)
|
||||||
|
|
||||||
|
Returns True if the condition is satisfied, False otherwise.
|
||||||
|
"""
|
||||||
|
# Split by '&' to get individual conditions
|
||||||
|
parts = condition_key.split('&')
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
part = part.strip()
|
||||||
|
if not part:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for negation prefix
|
||||||
|
if part.startswith('!'):
|
||||||
|
option_name = part[1:]
|
||||||
|
expected_value = False
|
||||||
|
else:
|
||||||
|
option_name = part
|
||||||
|
expected_value = True
|
||||||
|
|
||||||
|
# Get the actual option value
|
||||||
|
actual_value = check_option_enabled(world, option_name)
|
||||||
|
|
||||||
|
# If this part doesn't match expected, the whole condition fails
|
||||||
|
if actual_value != expected_value:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def build_requirement_check(world: Schedule1World, method_name: str, value: Any) -> Callable[[CollectionState], bool]:
|
||||||
|
"""Build a requirement check function based on the method name and value from JSON."""
|
||||||
|
|
||||||
|
if method_name == "has":
|
||||||
|
# value is a single item name string
|
||||||
|
return lambda state, v=value: state.has(v, world.player)
|
||||||
|
|
||||||
|
elif method_name == "has_any":
|
||||||
|
# value is a list of lists, e.g. [["Item1", "Item2"]]
|
||||||
|
# We take the first list as the items to check
|
||||||
|
items = value[0] if isinstance(value[0], list) else value
|
||||||
|
return lambda state, v=items: state.has_any(v, world.player)
|
||||||
|
|
||||||
|
elif method_name == "has_all":
|
||||||
|
# value is a list of item names
|
||||||
|
return lambda state, v=value: state.has_all(v, world.player)
|
||||||
|
|
||||||
|
elif method_name == "has_all_counts":
|
||||||
|
# value is a dict of {item_name: count}
|
||||||
|
return lambda state, v=value: state.has_all_counts(v, world.player)
|
||||||
|
|
||||||
|
elif method_name == "has_from_list":
|
||||||
|
# value can be:
|
||||||
|
# - A single dict: {item_name: count, ...} where all counts are the same
|
||||||
|
# - A list of dicts: [{item_name: count, ...}, ...] for multiple tiers
|
||||||
|
# For a list, we build checks for each dict and require all to pass
|
||||||
|
if isinstance(value, list):
|
||||||
|
# List of dicts - build a check for each dict
|
||||||
|
checks = []
|
||||||
|
for tier_dict in value:
|
||||||
|
keys = list(tier_dict.keys())
|
||||||
|
count = list(tier_dict.values())[0] # All values in a tier should be the same
|
||||||
|
checks.append((keys, count))
|
||||||
|
return lambda state, c=checks: all(
|
||||||
|
state.has_from_list(keys, world.player, count) for keys, count in c
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Single dict
|
||||||
|
keys = list(value.keys())
|
||||||
|
count = list(value.values())[0] # All values should be the same count
|
||||||
|
return lambda state, k=keys, c=count: state.has_from_list(k, world.player, c)
|
||||||
|
|
||||||
|
# Default: always true
|
||||||
|
return lambda state: True
|
||||||
|
|
||||||
|
|
||||||
|
def build_rule_from_requirements(world: Schedule1World, requirements: Union[bool, Dict[str, Any]], use_or_logic: bool = False) -> Callable[[CollectionState], bool]:
|
||||||
|
"""
|
||||||
|
Build a rule function from the requirements structure.
|
||||||
|
|
||||||
|
requirements can be:
|
||||||
|
- True (always accessible)
|
||||||
|
- A dict with option conditions as keys
|
||||||
|
|
||||||
|
use_or_logic: If True, only ONE condition needs to be satisfied (for Customer/Dealer/Supplier tags)
|
||||||
|
If False, ALL applicable conditions must be satisfied
|
||||||
|
"""
|
||||||
|
if requirements is True:
|
||||||
|
return lambda state: True
|
||||||
|
|
||||||
|
if not isinstance(requirements, dict):
|
||||||
|
return lambda state: True
|
||||||
|
|
||||||
|
# Build list of (option_name, checks) pairs
|
||||||
|
condition_checks: list[tuple[str, list[Callable[[CollectionState], bool]]]] = []
|
||||||
|
|
||||||
|
for option_name, checks in requirements.items():
|
||||||
|
if not isinstance(checks, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
check_functions = []
|
||||||
|
for method_name, value in checks.items():
|
||||||
|
check_func = build_requirement_check(world, method_name, value)
|
||||||
|
check_functions.append(check_func)
|
||||||
|
|
||||||
|
if check_functions:
|
||||||
|
condition_checks.append((option_name, check_functions))
|
||||||
|
|
||||||
|
if not condition_checks:
|
||||||
|
return lambda state: True
|
||||||
|
|
||||||
|
def rule_function(state: CollectionState) -> bool:
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for condition_key, check_functions in condition_checks:
|
||||||
|
if check_option_condition(world, condition_key):
|
||||||
|
# This option condition is satisfied, so its checks matter
|
||||||
|
# All checks within this condition must pass
|
||||||
|
option_result = all(check(state) for check in check_functions)
|
||||||
|
results.append(option_result)
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
# No applicable options enabled - rule passes
|
||||||
|
return True
|
||||||
|
|
||||||
|
if use_or_logic:
|
||||||
|
# For Customer/Dealer/Supplier: only one needs to pass
|
||||||
|
return any(results)
|
||||||
|
else:
|
||||||
|
# For all others: all must pass
|
||||||
|
return all(results)
|
||||||
|
|
||||||
|
return rule_function
|
||||||
|
|
||||||
|
|
||||||
|
def set_all_entrance_rules(world: Schedule1World, regionData) -> None:
|
||||||
|
"""Set entrance rules based on region connection requirements from regions.json."""
|
||||||
|
|
||||||
|
# Load all entrances into a dictionary once
|
||||||
|
entrances_dict: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
for region_name, region_info in regionData.regions.items():
|
||||||
|
for connected_region_name, requirements in region_info.connections.items():
|
||||||
|
entrance_name = f"{region_name} to {connected_region_name}"
|
||||||
|
try:
|
||||||
|
entrances_dict[entrance_name] = world.get_entrance(entrance_name)
|
||||||
|
except KeyError:
|
||||||
|
# Entrance might not exist if region wasn't created
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Set rules for each entrance
|
||||||
|
for region_name, region_info in regionData.regions.items():
|
||||||
|
for connected_region_name, requirements in region_info.connections.items():
|
||||||
|
entrance_name = f"{region_name} to {connected_region_name}"
|
||||||
|
|
||||||
|
if entrance_name not in entrances_dict:
|
||||||
|
continue
|
||||||
|
|
||||||
|
entrance = entrances_dict[entrance_name]
|
||||||
|
rule = build_rule_from_requirements(world, requirements, use_or_logic=False)
|
||||||
|
set_rule(entrance, rule)
|
||||||
|
|
||||||
|
|
||||||
|
def set_all_location_rules(world: Schedule1World, locationData) -> None:
|
||||||
|
"""Set location rules based on requirements from locations.json."""
|
||||||
|
|
||||||
|
# Build a dict of location name -> location object for locations that exist
|
||||||
|
locations_dict: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
for loc_name, loc_data in locationData.locations.items():
|
||||||
|
# Skip supplier locations if randomize_suppliers is enabled (they don't exist)
|
||||||
|
if world.options.randomize_suppliers and "Supplier" in loc_data.tags:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
locations_dict[loc_name] = world.get_location(loc_name)
|
||||||
|
except KeyError:
|
||||||
|
# Location might not exist
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Set rules for each location
|
||||||
|
for loc_name, loc_data in locationData.locations.items():
|
||||||
|
if loc_name not in locations_dict:
|
||||||
|
continue
|
||||||
|
|
||||||
|
location = locations_dict[loc_name]
|
||||||
|
requirements = loc_data.requirements
|
||||||
|
|
||||||
|
# Determine if this location uses OR logic (Customer, Dealer, or Supplier tags)
|
||||||
|
tags = loc_data.tags
|
||||||
|
use_or_logic = any(tag in tags for tag in ["Customer", "Dealer", "Supplier"])
|
||||||
|
|
||||||
|
rule = build_rule_from_requirements(world, requirements, use_or_logic=use_or_logic)
|
||||||
|
set_rule(location, rule)
|
||||||
|
|
||||||
|
|
||||||
|
def set_completion_condition(world: Schedule1World, victoryData) -> None:
|
||||||
|
# Victory conditions are loaded from victory.json
|
||||||
|
# > 0 means cartel is necessary, and we need to check all applicable conditions
|
||||||
|
if world.options.goal > 0:
|
||||||
|
# Build the victory rule from the requirements in victory.json
|
||||||
|
# All applicable option conditions must pass (AND logic)
|
||||||
|
rule = build_rule_from_requirements(world, victoryData.requirements, use_or_logic=False)
|
||||||
|
world.multiworld.completion_condition[world.player] = rule
|
||||||
|
else:
|
||||||
|
# Otherwise, money is farmable no matter what.
|
||||||
|
world.multiworld.completion_condition[world.player] = lambda state: True
|
||||||
37
worlds/Schedule_I/web_world.py
Normal file
37
worlds/Schedule_I/web_world.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from BaseClasses import Tutorial
|
||||||
|
from worlds.AutoWorld import WebWorld
|
||||||
|
|
||||||
|
from .options import option_groups, option_presets
|
||||||
|
|
||||||
|
|
||||||
|
# For our game to display correctly on the website, we need to define a WebWorld subclass.
|
||||||
|
class APSchedule1(WebWorld):
|
||||||
|
# We need to override the "game" field of the WebWorld superclass.
|
||||||
|
# This must be the same string as the regular World class.
|
||||||
|
game = "Schedule I"
|
||||||
|
|
||||||
|
# Your game pages will have a visual theme (affecting e.g. the background image).
|
||||||
|
# You can choose between dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, and stone.
|
||||||
|
theme = "partyTime"
|
||||||
|
|
||||||
|
# A WebWorld can have any number of tutorials, but should always have at least an English setup guide.
|
||||||
|
# Many WebWorlds just have one setup guide, but some have multiple, e.g. for different languages.
|
||||||
|
# We need to create a Tutorial object for every setup guide.
|
||||||
|
# In order, we need to provide a title, a description, a language, a filepath, a link, and authors.
|
||||||
|
# The filepath is relative to a "/docs/" directory in the root folder of your apworld.
|
||||||
|
# The "link" parameter is unused, but we still need to provide it.
|
||||||
|
setup_en = Tutorial(
|
||||||
|
"Multiworld Setup Guide",
|
||||||
|
"A guide to setting up Schedule 1 for MultiWorld.",
|
||||||
|
"English",
|
||||||
|
"setup_en.md",
|
||||||
|
"setup/en",
|
||||||
|
["NewSoupVi"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# We add these tutorials to our WebWorld by overriding the "tutorials" field.
|
||||||
|
tutorials = [setup_en]
|
||||||
|
|
||||||
|
# If we have option groups and/or option presets, we need to specify these here as well.
|
||||||
|
option_groups = option_groups
|
||||||
|
options_presets = option_presets
|
||||||
110
worlds/Schedule_I/world.py
Normal file
110
worlds/Schedule_I/world.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
# Imports of base Archipelago modules must be absolute.
|
||||||
|
from worlds.AutoWorld import World
|
||||||
|
|
||||||
|
# Imports of your world's files must be relative.
|
||||||
|
from . import items, locations, options, regions, rules, web_world, json_data
|
||||||
|
|
||||||
|
# APQuest will go through all the parts of the world api one step at a time,
|
||||||
|
# with many examples and comments across multiple files.
|
||||||
|
# If you'd rather read one continuous document, or just like reading multiple sources,
|
||||||
|
# we also have this document specifying the entire world api:
|
||||||
|
# https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md
|
||||||
|
|
||||||
|
|
||||||
|
# The world class is the heart and soul of an apworld implementation.
|
||||||
|
# It holds all the data and functions required to build the world and submit it to the multiworld generator.
|
||||||
|
# You could have all your world code in just this one class, but for readability and better structure,
|
||||||
|
# it is common to split up world functionality into multiple files.
|
||||||
|
# This implementation in particular has the following additional files, each covering one topic:
|
||||||
|
# regions.py, locations.py, rules.py, items.py, options.py and web_world.py.
|
||||||
|
# It is recommended that you read these in that specific order, then come back to the world class.
|
||||||
|
class Schedule1World(World):
|
||||||
|
"""
|
||||||
|
Scheudle 1 is a game about manufacturing. Produce a range of drugs. Purchase properties and equipment.
|
||||||
|
Distribute your products through a network of dealers. Avoid the law and rival manufacturers.
|
||||||
|
Expand your empire and become the ultimate drug lord!
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The docstring should contain a description of the game, to be displayed on the WebHost.
|
||||||
|
|
||||||
|
# You must override the "game" field to say the name of the game.
|
||||||
|
game = "Schedule I"
|
||||||
|
|
||||||
|
# The WebWorld is a definition class that governs how this world will be displayed on the website.
|
||||||
|
web = web_world.APSchedule1()
|
||||||
|
|
||||||
|
# This is how we associate the options defined in our options.py with our world.
|
||||||
|
options_dataclass = options.Schedule1Options
|
||||||
|
options: options.Schedule1Options # Common mistake: This has to be a colon (:), not an equals sign (=).
|
||||||
|
|
||||||
|
# Our world class must have a static location_name_to_id and item_name_to_id defined.
|
||||||
|
# We define these in regions.py and items.py respectively, so we just set them here.
|
||||||
|
# Load items from json into needed dicts.
|
||||||
|
items.load_items_data(json_data.schedule1_item_data)
|
||||||
|
locations.load_locations_data(json_data.schedule1_location_data)
|
||||||
|
location_name_to_id = locations.LOCATION_NAME_TO_ID
|
||||||
|
item_name_to_id = items.ITEM_NAME_TO_ID
|
||||||
|
|
||||||
|
# There is always one region that the generator starts from & assumes you can always go back to.
|
||||||
|
# This defaults to "Menu", but you can change it by overriding origin_region_name.
|
||||||
|
origin_region_name = "Overworld"
|
||||||
|
|
||||||
|
# Our world class must have certain functions ("steps") that get called during generation.
|
||||||
|
# The main ones are: create_regions, set_rules, create_items.
|
||||||
|
# For better structure and readability, we put each of these in their own file.
|
||||||
|
def create_regions(self) -> None:
|
||||||
|
regions.create_and_connect_regions(self, json_data.schedule1_region_data)
|
||||||
|
locations.create_all_locations(self, json_data.schedule1_location_data)
|
||||||
|
|
||||||
|
def set_rules(self) -> None:
|
||||||
|
rules.set_all_rules(self, json_data.schedule1_location_data, json_data.schedule1_region_data, json_data.schedule1_victory_data)
|
||||||
|
|
||||||
|
def create_items(self) -> None:
|
||||||
|
items.create_all_items(self, json_data.schedule1_item_data)
|
||||||
|
|
||||||
|
# Our world class must also have a create_item function that can create any one of our items by name at any time.
|
||||||
|
# We also put this in a different file, the same one that create_items is in.
|
||||||
|
def create_item(self, name: str) -> items.Schedule1Item:
|
||||||
|
return items.create_item_with_correct_classification(self, name)
|
||||||
|
|
||||||
|
# For features such as item links and panic-method start inventory, AP may ask your world to create extra filler.
|
||||||
|
# The way it does this is by calling get_filler_item_name.
|
||||||
|
# For this purpose, your world *must* have at least one infinitely repeatable item (usually filler).
|
||||||
|
# You must override this function and return this infinitely repeatable item's name.
|
||||||
|
# In our case, we defined a function called get_random_filler_item_name for this purpose in our items.py.
|
||||||
|
def get_filler_item_name(self) -> str:
|
||||||
|
return items.get_random_filler_item_name(self)
|
||||||
|
|
||||||
|
# There may be data that the game client will need to modify the behavior of the game.
|
||||||
|
# This is what slot_data exists for. Upon every client connection, the slot's slot_data is sent to the client.
|
||||||
|
# slot_data is just a dictionary using basic types, that will be converted to json when sent to the client.
|
||||||
|
def fill_slot_data(self) -> Mapping[str, Any]:
|
||||||
|
# If you need access to the player's chosen options on the client side, there is a helper for that.
|
||||||
|
return self.options.as_dict(
|
||||||
|
"goal",
|
||||||
|
"number_of_xp_bundles",
|
||||||
|
"amount_of_xp_per_bundle_min",
|
||||||
|
"amount_of_xp_per_bundle_max",
|
||||||
|
"number_of_cash_bundles",
|
||||||
|
"amount_of_cash_per_bundle_min",
|
||||||
|
"amount_of_cash_per_bundle_max",
|
||||||
|
"networth_amount_required",
|
||||||
|
"ban_bad_filler_items",
|
||||||
|
"ban_progression_skip_items",
|
||||||
|
"trap_chance",
|
||||||
|
"randomize_cartel_influence",
|
||||||
|
"randomize_drug_making_properties",
|
||||||
|
"randomize_business_properties",
|
||||||
|
"randomize_dealers",
|
||||||
|
"randomize_customers",
|
||||||
|
"randomize_suppliers",
|
||||||
|
"cartel_influence_items_per_region",
|
||||||
|
"recipe_checks",
|
||||||
|
"cash_for_trash",
|
||||||
|
"randomize_level_unlocks",
|
||||||
|
"randomize_sewer_key",
|
||||||
|
"death_link"
|
||||||
|
)
|
||||||
265
worlds/SonicFrontiers/__init__.py
Normal file
265
worlds/SonicFrontiers/__init__.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import settings
|
||||||
|
from typing import Dict, Any
|
||||||
|
from BaseClasses import MultiWorld, Region, Item, Tutorial
|
||||||
|
from worlds.AutoWorld import World, WebWorld
|
||||||
|
from Utils import visualize_regions
|
||||||
|
from worlds.generic.Rules import set_rule
|
||||||
|
from .items import (SonicFrontiersItem, SonicFrontiersItemData, item_list, kronos_amount, ares_amount, chaos_amount, ouranos_amount, fillers)
|
||||||
|
from .locations import (kronosRegion, SonicFrontiersAdvancement, aresRegion, chaosRegion, ouranosRegion, kronosMemoryTokenSet,
|
||||||
|
aresMemoryTokenSet, chaosMemoryTokenSet, ouranosMemoryTokenSet, kronosPurpleSet, kronosKocoSet, aresKocoSet,
|
||||||
|
aresPurpleSet, chaosKocoSet, chaosPurpleSet, ouranosKocoSet, ouranosPurpleSet, kronosNewKocoSet,
|
||||||
|
kronosMusicSet, aresMusicSet, aresNewKocoSet, chaosMusicSet, chaosNewKocoSet, ouranosMusicSet, ouranosNewKocoSet, all_items)
|
||||||
|
from .options import SonicFrontiersOptions
|
||||||
|
|
||||||
|
class SonicFrontiersWebWorld(WebWorld):
|
||||||
|
setup_en = Tutorial(
|
||||||
|
"Multiworld Setup Guide",
|
||||||
|
"A guide to playing Sonic Frontiers with Archipelago.",
|
||||||
|
"English",
|
||||||
|
"setup_en.md",
|
||||||
|
"setup/en",
|
||||||
|
["custom"]
|
||||||
|
)
|
||||||
|
|
||||||
|
tutorials = [setup_en]
|
||||||
|
|
||||||
|
class SonicFrontiersWorld(World):
|
||||||
|
game = "Sonic Frontiers"
|
||||||
|
topology_present = False
|
||||||
|
web = SonicFrontiersWebWorld()
|
||||||
|
item_name_to_id = {name: data.id for name, data in item_list.items()}
|
||||||
|
location_name_to_id = {name: data.id for name, data in all_items.items()}
|
||||||
|
|
||||||
|
options_dataclass = SonicFrontiersOptions
|
||||||
|
options: SonicFrontiersOptions
|
||||||
|
def create_kronos_island(self) -> Region:
|
||||||
|
return
|
||||||
|
def create_ares_island(self) -> Region:
|
||||||
|
return
|
||||||
|
|
||||||
|
def create_regions(self) -> None:
|
||||||
|
menu_region = Region("Menu", self.player, self.multiworld)
|
||||||
|
|
||||||
|
kronos_region = Region("Kronos", self.player, self.multiworld)
|
||||||
|
kronos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, kronos_region)for loc_name, loc_data in kronosRegion.items()]
|
||||||
|
|
||||||
|
if(self.options.memory_token_sanity):
|
||||||
|
kronos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, kronos_region)for loc_name, loc_data in kronosMemoryTokenSet.items()]
|
||||||
|
if(self.options.challenge_kocos):
|
||||||
|
kronos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, kronos_region)for loc_name, loc_data in kronosNewKocoSet.items()]
|
||||||
|
if(self.options.music_notes):
|
||||||
|
kronos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, kronos_region)for loc_name, loc_data in kronosMusicSet.items()]
|
||||||
|
if(self.options.purple_coin_sanity):
|
||||||
|
kronos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, kronos_region)for loc_name, loc_data in kronosPurpleSet.items()]
|
||||||
|
if(self.options.koco_sanity):
|
||||||
|
kronos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, kronos_region)for loc_name, loc_data in kronosKocoSet.items()]
|
||||||
|
menu_region.connect(kronos_region)
|
||||||
|
self.multiworld.regions += [menu_region, kronos_region]
|
||||||
|
|
||||||
|
if(self.options.goal > 0):
|
||||||
|
ares_region = Region("Ares", self.player, self.multiworld)
|
||||||
|
ares_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ares_region)for loc_name, loc_data in aresRegion.items()]
|
||||||
|
|
||||||
|
if(self.options.memory_token_sanity):
|
||||||
|
ares_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ares_region)for loc_name, loc_data in aresMemoryTokenSet.items()]
|
||||||
|
if(self.options.challenge_kocos):
|
||||||
|
ares_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ares_region)for loc_name, loc_data in aresNewKocoSet.items()]
|
||||||
|
if(self.options.music_notes):
|
||||||
|
ares_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ares_region)for loc_name, loc_data in aresMusicSet.items()]
|
||||||
|
if(self.options.purple_coin_sanity):
|
||||||
|
ares_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ares_region)for loc_name, loc_data in aresPurpleSet.items()]
|
||||||
|
if(self.options.koco_sanity):
|
||||||
|
ares_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ares_region)for loc_name, loc_data in aresKocoSet.items()]
|
||||||
|
self.multiworld.regions += [ares_region]
|
||||||
|
kronos_region.add_exits({"Ares": "Ares Entrance"})
|
||||||
|
|
||||||
|
if(self.options.goal > 1):
|
||||||
|
chaos_region = Region("Chaos", self.player, self.multiworld)
|
||||||
|
chaos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, chaos_region)for loc_name, loc_data in chaosRegion.items()]
|
||||||
|
if(self.options.memory_token_sanity):
|
||||||
|
chaos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, chaos_region)for loc_name, loc_data in chaosMemoryTokenSet.items()]
|
||||||
|
if(self.options.challenge_kocos):
|
||||||
|
chaos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, chaos_region)for loc_name, loc_data in chaosNewKocoSet.items()]
|
||||||
|
if(self.options.music_notes):
|
||||||
|
chaos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, chaos_region)for loc_name, loc_data in chaosMusicSet.items()]
|
||||||
|
if(self.options.purple_coin_sanity):
|
||||||
|
chaos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, chaos_region)for loc_name, loc_data in chaosPurpleSet.items()]
|
||||||
|
if(self.options.koco_sanity):
|
||||||
|
chaos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, chaos_region)for loc_name, loc_data in chaosKocoSet.items()]
|
||||||
|
self.multiworld.regions += [chaos_region]
|
||||||
|
ares_region.add_exits({"Chaos": "Chaos Entrance"})
|
||||||
|
|
||||||
|
if(self.options.goal > 2):
|
||||||
|
ouranos_region = Region("Ouranos", self.player, self.multiworld)
|
||||||
|
ouranos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ouranos_region)for loc_name, loc_data in ouranosRegion.items()]
|
||||||
|
if(self.options.memory_token_sanity):
|
||||||
|
ouranos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ouranos_region)for loc_name, loc_data in ouranosMemoryTokenSet.items()]
|
||||||
|
if(self.options.challenge_kocos):
|
||||||
|
ouranos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ouranos_region)for loc_name, loc_data in ouranosNewKocoSet.items()]
|
||||||
|
if(self.options.music_notes):
|
||||||
|
ouranos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ouranos_region)for loc_name, loc_data in ouranosMusicSet.items()]
|
||||||
|
if(self.options.purple_coin_sanity):
|
||||||
|
ouranos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ouranos_region)for loc_name, loc_data in ouranosPurpleSet.items()]
|
||||||
|
if(self.options.koco_sanity):
|
||||||
|
ouranos_region.locations += [SonicFrontiersAdvancement(self.player, loc_name, loc_data.id, ouranos_region)for loc_name, loc_data in ouranosKocoSet.items()]
|
||||||
|
self.multiworld.regions += [ouranos_region]
|
||||||
|
chaos_region.add_exits({"Ouranos": "Ouranos Entrance"})
|
||||||
|
|
||||||
|
if(self.options.goal == 0):
|
||||||
|
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach_location("Defeat Giganto", self.player)
|
||||||
|
if(self.options.goal == 1):
|
||||||
|
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach_location("Defeat Wyvern", self.player)
|
||||||
|
if(self.options.goal == 2):
|
||||||
|
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach_location("Defeat Knight", self.player)
|
||||||
|
if(self.options.goal == 3):
|
||||||
|
self.multiworld.completion_condition[self.player] = lambda state: state.can_reach_location("Defeat Ouranos", self.player)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def set_rules(self) -> None:
|
||||||
|
|
||||||
|
for i in range(7):
|
||||||
|
set_rule(self.multiworld.get_location((f"1-2 All Missions ({i+1})"), self.player), lambda state: state.has("1-2 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"1-3 All Missions ({i+1})"), self.player), lambda state: state.has("1-3 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"1-4 All Missions ({i+1})"), self.player), lambda state: state.has("1-4 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"1-5 All Missions ({i+1})"), self.player), lambda state: state.has("1-5 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"1-6 All Missions ({i+1})"), self.player), lambda state: state.has("1-6 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"1-7 All Missions ({i+1})"), self.player), lambda state: state.has("1-7 Unlocked", self.player, 1))
|
||||||
|
if(self.options.goal > 0):
|
||||||
|
set_rule(self.multiworld.get_location((f"2-1 All Missions ({i+1})"), self.player), lambda state: state.has("2-1 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"2-2 All Missions ({i+1})"), self.player), lambda state: state.has("2-2 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"2-3 All Missions ({i+1})"), self.player), lambda state: state.has("2-3 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"2-4 All Missions ({i+1})"), self.player), lambda state: state.has("2-4 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"2-5 All Missions ({i+1})"), self.player), lambda state: state.has("2-5 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"2-6 All Missions ({i+1})"), self.player), lambda state: state.has("2-6 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"2-7 All Missions ({i+1})"), self.player), lambda state: state.has("2-7 Unlocked", self.player, 1))
|
||||||
|
if(self.options.goal > 1):
|
||||||
|
set_rule(self.multiworld.get_location((f"3-1 All Missions ({i+1})"), self.player), lambda state: state.has("3-1 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"3-2 All Missions ({i+1})"), self.player), lambda state: state.has("3-2 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"3-3 All Missions ({i+1})"), self.player), lambda state: state.has("3-3 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"3-4 All Missions ({i+1})"), self.player), lambda state: state.has("3-4 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"3-5 All Missions ({i+1})"), self.player), lambda state: state.has("3-5 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"3-6 All Missions ({i+1})"), self.player), lambda state: state.has("3-6 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"3-7 All Missions ({i+1})"), self.player), lambda state: state.has("3-7 Unlocked", self.player, 1)
|
||||||
|
and state.can_reach_location("Chaos Cyan Emerald", self.player))
|
||||||
|
if(self.options.goal > 2):
|
||||||
|
set_rule(self.multiworld.get_location((f"4-1 All Missions ({i+1})"), self.player), lambda state: state.has("4-1 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"4-2 All Missions ({i+1})"), self.player), lambda state: state.has("4-2 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"4-3 All Missions ({i+1})"), self.player), lambda state: state.has("4-3 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"4-4 All Missions ({i+1})"), self.player), lambda state: state.has("4-4 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"4-5 All Missions ({i+1})"), self.player), lambda state: state.has("4-5 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"4-6 All Missions ({i+1})"), self.player), lambda state: state.has("4-6 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"4-7 All Missions ({i+1})"), self.player), lambda state: state.has("4-7 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"4-8 All Missions ({i+1})"), self.player), lambda state: state.has("4-8 Unlocked", self.player, 1))
|
||||||
|
set_rule(self.multiworld.get_location((f"4-9 All Missions ({i+1})"), self.player), lambda state: state.has("4-9 Unlocked", self.player, 1))
|
||||||
|
|
||||||
|
set_rule(self.multiworld.get_location(("Kronos Blue Emerald"), self.player), lambda state: state.has("Kronos Vault Key", self.player, 2))
|
||||||
|
set_rule(self.multiworld.get_location(("Kronos Red Emerald"), self.player), lambda state: state.has("Kronos Vault Key", self.player, 5))
|
||||||
|
|
||||||
|
set_rule(self.multiworld.get_location(("Kronos Green Emerald"), self.player), lambda state: state.has("Kronos Vault Key", self.player, 5)
|
||||||
|
and state.has("Kronos Memory Treasure", self.player, 3) and state.has("Progressive Chaos Emerald", self.player, 2))
|
||||||
|
|
||||||
|
set_rule(self.multiworld.get_location(("Kronos Yellow Emerald"), self.player), lambda state: state.has("Kronos Vault Key", self.player, 13))
|
||||||
|
|
||||||
|
set_rule(self.multiworld.get_location(("Kronos Cyan Emerald"), self.player), lambda state: state.has("Kronos Vault Key", self.player, 13)
|
||||||
|
and state.has("Kronos Memory Treasure", self.player, 6) and state.has("Progressive Chaos Emerald", self.player, 4))
|
||||||
|
|
||||||
|
set_rule(self.multiworld.get_location(("Kronos White Emerald"), self.player), lambda state: state.has("Kronos Vault Key", self.player, 20))
|
||||||
|
if(self.options.goal > 0):
|
||||||
|
set_rule(self.multiworld.get_location(("Ares Blue Emerald"), self.player), lambda state: state.has("Ares Vault Key", self.player, 7))
|
||||||
|
set_rule(self.multiworld.get_location(("Ares Red Emerald"), self.player), lambda state: state.has("Ares Vault Key", self.player, 14))
|
||||||
|
set_rule(self.multiworld.get_location(("Ares Green Emerald"), self.player), lambda state: state.has("Ares Vault Key", self.player, 14) and state.has("Progressive Chaos Emerald", self.player, 8))
|
||||||
|
set_rule(self.multiworld.get_location(("Ares Yellow Emerald"), self.player), lambda state: state.has("Ares Vault Key", self.player, 20))
|
||||||
|
set_rule(self.multiworld.get_location(("Ares Cyan Emerald"), self.player), lambda state: state.has("Ares Vault Key", self.player, 20) and state.has("Progressive Chaos Emerald", self.player, 10))
|
||||||
|
set_rule(self.multiworld.get_location(("Ares White Emerald"), self.player), lambda state: state.has("Ares Vault Key", self.player, 24))
|
||||||
|
|
||||||
|
if(self.options.goal > 1):
|
||||||
|
set_rule(self.multiworld.get_location(("Chaos Blue Emerald"), self.player), lambda state: state.has("Chaos Vault Key", self.player, 7))
|
||||||
|
set_rule(self.multiworld.get_location(("Chaos Red Emerald"), self.player), lambda state: state.has("Chaos Vault Key", self.player, 14))
|
||||||
|
set_rule(self.multiworld.get_location(("Chaos Green Emerald"), self.player), lambda state: state.has("Chaos Vault Key", self.player, 14) and state.has("Progressive Chaos Emerald", self.player, 14))
|
||||||
|
set_rule(self.multiworld.get_location(("Chaos Yellow Emerald"), self.player), lambda state: state.has("Chaos Vault Key", self.player, 20))
|
||||||
|
set_rule(self.multiworld.get_location(("Chaos Cyan Emerald"), self.player), lambda state: state.has("Chaos Vault Key", self.player, 20) and state.has("Progressive Chaos Emerald", self.player, 16))
|
||||||
|
set_rule(self.multiworld.get_location(("Chaos White Emerald"), self.player), lambda state: state.has("Chaos Vault Key", self.player, 25))
|
||||||
|
if(self.options.goal > 2):
|
||||||
|
set_rule(self.multiworld.get_location(("Ouranos Blue Emerald"), self.player), lambda state: state.has("Ouranos Vault Key", self.player, 3))
|
||||||
|
set_rule(self.multiworld.get_location(("Ouranos Red Emerald"), self.player), lambda state: state.has("Ouranos Vault Key", self.player, 9))
|
||||||
|
set_rule(self.multiworld.get_location(("Ouranos Green Emerald"), self.player), lambda state: state.has("Ouranos Vault Key", self.player, 16))
|
||||||
|
set_rule(self.multiworld.get_location(("Ouranos Yellow Emerald"), self.player), lambda state: state.has("Ouranos Vault Key", self.player, 23))
|
||||||
|
set_rule(self.multiworld.get_location(("Ouranos Cyan Emerald"), self.player), lambda state: state.has("Ouranos Vault Key", self.player, 30))
|
||||||
|
set_rule(self.multiworld.get_location(("Ouranos White Emerald"), self.player), lambda state: state.has("Ouranos Vault Key", self.player, 33))
|
||||||
|
if(self.options.goal > 0):
|
||||||
|
set_rule(self.multiworld.get_entrance("Ares Entrance", self.player), lambda state: state.has("Progressive Chaos Emerald", self.player, 6)
|
||||||
|
and state.has("Kronos Memory Treasure", self.player, 9) and state.has("Kronos Vault Key", self.player, 20)
|
||||||
|
and state.has("Stomp Attack", self.player) and state.has("Parry", self.player))
|
||||||
|
|
||||||
|
if(self.options.goal > 1):
|
||||||
|
set_rule(self.multiworld.get_entrance("Chaos Entrance", self.player),
|
||||||
|
lambda state: state.has("Progressive Chaos Emerald", self.player, 12) and state.has("Ares Memory Treasure", self.player, 32) and state.has("Ares Vault Key", self.player, 24))
|
||||||
|
|
||||||
|
if(self.options.goal > 2):
|
||||||
|
set_rule(self.multiworld.get_entrance("Ouranos Entrance", self.player),
|
||||||
|
lambda state: state.has("Progressive Chaos Emerald", self.player, 18) and state.has("Chaos Memory Treasure", self.player, 20) and state.has("Chaos Vault Key", self.player, 25))
|
||||||
|
|
||||||
|
if(self.options.goal == 0):
|
||||||
|
set_rule(self.multiworld.get_location("Defeat Giganto", self.player), lambda state: state.has("Progressive Chaos Emerald", self.player, 6)
|
||||||
|
and state.has("Kronos Memory Treasure", self.player, 9) and state.has("Kronos Vault Key", self.player, 20)
|
||||||
|
and state.has("Stomp Attack", self.player) and state.has("Parry", self.player))
|
||||||
|
if(self.options.goal == 1):
|
||||||
|
set_rule(self.multiworld.get_location("Defeat Wyvern", self.player), lambda state: state.has("Progressive Chaos Emerald", self.player, 12)
|
||||||
|
and state.has("Ares Memory Treasure", self.player, 32) and state.has("Ares Vault Key", self.player, 24))
|
||||||
|
if(self.options.goal == 2):
|
||||||
|
set_rule(self.multiworld.get_location("Defeat Knight", self.player),
|
||||||
|
lambda state: state.has("Progressive Chaos Emerald", self.player, 18) and state.has("Chaos Memory Treasure", self.player, 20) and state.has("Chaos Vault Key", self.player, 25))
|
||||||
|
if(self.options.goal == 3):
|
||||||
|
set_rule(self.multiworld.get_location("Defeat Supreme", self.player),
|
||||||
|
lambda state: state.has("Progressive Chaos Emerald", self.player, 24) and state.has("Ouranos Memory Treasure", self.player, 24) and state.has("Ouranos Vault Key", self.player, 33))
|
||||||
|
|
||||||
|
def create_items(self) -> None:
|
||||||
|
numItems = 0
|
||||||
|
for name, quantity in kronos_amount.items():
|
||||||
|
for i in range(quantity):
|
||||||
|
item = self.create_item(name)
|
||||||
|
self.multiworld.itempool.append(item)
|
||||||
|
numItems += 1
|
||||||
|
if self.options.goal > 0:
|
||||||
|
for name, quantity in ares_amount.items():
|
||||||
|
for i in range(quantity):
|
||||||
|
item = self.create_item(name)
|
||||||
|
self.multiworld.itempool.append(item)
|
||||||
|
numItems += 1
|
||||||
|
if self.options.goal > 1:
|
||||||
|
for name, quantity in chaos_amount.items():
|
||||||
|
for i in range(quantity):
|
||||||
|
item = self.create_item(name)
|
||||||
|
self.multiworld.itempool.append(item)
|
||||||
|
numItems += 1
|
||||||
|
if self.options.goal > 2:
|
||||||
|
for name, quantity in ouranos_amount.items():
|
||||||
|
for i in range(quantity):
|
||||||
|
item = self.create_item(name)
|
||||||
|
self.multiworld.itempool.append(item)
|
||||||
|
numItems += 1
|
||||||
|
filler = len(self.multiworld.get_locations(self.player)) - numItems
|
||||||
|
for _ in range(filler):
|
||||||
|
name = self.random.choices(list(fillers.keys()), weights = list(fillers.values()))[0]
|
||||||
|
item = self.create_item(name)
|
||||||
|
self.multiworld.itempool.append(item)
|
||||||
|
def create_item(self, name: str) -> Item:
|
||||||
|
item_data = item_list[name]
|
||||||
|
item = SonicFrontiersItem(name, item_data.item_class, item_data.id, self.player)
|
||||||
|
return item
|
||||||
|
def fill_slot_data(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"death_link": self.options.death_link.value,
|
||||||
|
"goal": self.options.goal.value,
|
||||||
|
"memory_token_sanity": self.options.memory_token_sanity.value,
|
||||||
|
"cyberspace_times": self.options.cyberspace_times.value,
|
||||||
|
"music_notes": self.options.music_notes.value,
|
||||||
|
"challenge_kocos": self.options.challenge_kocos.value,
|
||||||
|
"purple_coin_sanity": self.options.purple_coin_sanity.value,
|
||||||
|
"koco_sanity": self.options.koco_sanity.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
1
worlds/SonicFrontiers/docs/setup_en.md
Normal file
1
worlds/SonicFrontiers/docs/setup_en.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
stop making me do this
|
||||||
157
worlds/SonicFrontiers/items.py
Normal file
157
worlds/SonicFrontiers/items.py
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
from typing import NamedTuple
|
||||||
|
from BaseClasses import Item, ItemClassification
|
||||||
|
|
||||||
|
class SonicFrontiersItem(Item):
|
||||||
|
game: str = "Sonic Frontiers"
|
||||||
|
|
||||||
|
class SonicFrontiersItemData(NamedTuple):
|
||||||
|
id: int
|
||||||
|
item_class: ItemClassification = ItemClassification.progression
|
||||||
|
|
||||||
|
offset: int = 101000
|
||||||
|
item_list = {
|
||||||
|
"Kronos Memory Token": SonicFrontiersItemData(offset, ItemClassification.filler),
|
||||||
|
"Kronos Portal Gear": SonicFrontiersItemData(offset + 1, ItemClassification.filler),
|
||||||
|
"Kronos Vault Key": SonicFrontiersItemData(offset + 2, ItemClassification.progression),
|
||||||
|
"Ares Memory Token": SonicFrontiersItemData(offset + 3, ItemClassification.filler),
|
||||||
|
"Ares Portal Gear": SonicFrontiersItemData(offset + 4, ItemClassification.filler),
|
||||||
|
"Ares Vault Key": SonicFrontiersItemData(offset + 5, ItemClassification.progression),
|
||||||
|
"Chaos Memory Token": SonicFrontiersItemData(offset + 6, ItemClassification.filler),
|
||||||
|
"Chaos Portal Gear": SonicFrontiersItemData(offset + 7, ItemClassification.filler),
|
||||||
|
"Chaos Vault Key": SonicFrontiersItemData(offset + 8, ItemClassification.progression),
|
||||||
|
"Ouranos Memory Token": SonicFrontiersItemData(offset + 9, ItemClassification.filler),
|
||||||
|
"Ouranos Portal Gear": SonicFrontiersItemData(offset + 10, ItemClassification.filler),
|
||||||
|
"Ouranos Vault Key": SonicFrontiersItemData(offset + 11, ItemClassification.progression),
|
||||||
|
"Progressive Chaos Emerald": SonicFrontiersItemData(offset + 12, ItemClassification.progression),
|
||||||
|
"Kronos Memory Treasure": SonicFrontiersItemData(offset + 36, ItemClassification.progression),
|
||||||
|
"Ares Memory Treasure": SonicFrontiersItemData(offset + 37, ItemClassification.progression),
|
||||||
|
"Chaos Memory Treasure": SonicFrontiersItemData(offset + 38, ItemClassification.progression),
|
||||||
|
"Ouranos Memory Treasure": SonicFrontiersItemData(offset + 39, ItemClassification.progression),
|
||||||
|
"Red Power Seed": SonicFrontiersItemData(offset + 40, ItemClassification.filler),
|
||||||
|
"Blue Power Seed": SonicFrontiersItemData(offset + 41, ItemClassification.filler),
|
||||||
|
"Nothing!": SonicFrontiersItemData(offset + 42, ItemClassification.filler),
|
||||||
|
"Phantom Rush": SonicFrontiersItemData(offset + 43, ItemClassification.useful),
|
||||||
|
"Air Trick":SonicFrontiersItemData(offset + 44, ItemClassification.useful),
|
||||||
|
"Stomp Attack": SonicFrontiersItemData(offset + 45, ItemClassification.progression),
|
||||||
|
"Quick Cyloop": SonicFrontiersItemData(offset + 46, ItemClassification.useful),
|
||||||
|
"Spin Dash": SonicFrontiersItemData(offset + 47, ItemClassification.useful),
|
||||||
|
"Sonic Boom": SonicFrontiersItemData(offset + 48, ItemClassification.useful),
|
||||||
|
"Parry": SonicFrontiersItemData(offset + 49, ItemClassification.progression),
|
||||||
|
"Homing Shot": SonicFrontiersItemData(offset + 50, ItemClassification.useful),
|
||||||
|
"Spin Slash": SonicFrontiersItemData(offset + 51, ItemClassification.useful),
|
||||||
|
"Recovery Smash": SonicFrontiersItemData(offset +52, ItemClassification.useful),
|
||||||
|
"Cyclone Kick": SonicFrontiersItemData(offset + 53, ItemClassification.useful),
|
||||||
|
"Cross Slash": SonicFrontiersItemData(offset + 54, ItemClassification.useful),
|
||||||
|
"Grand Slam": SonicFrontiersItemData(offset + 55, ItemClassification.useful),
|
||||||
|
"1-2 Unlocked": SonicFrontiersItemData(offset + 56,ItemClassification.progression),
|
||||||
|
"1-3 Unlocked": SonicFrontiersItemData(offset + 57,ItemClassification.progression),
|
||||||
|
"1-4 Unlocked": SonicFrontiersItemData(offset + 58,ItemClassification.progression),
|
||||||
|
"1-5 Unlocked": SonicFrontiersItemData(offset + 59,ItemClassification.progression),
|
||||||
|
"1-6 Unlocked": SonicFrontiersItemData(offset + 60,ItemClassification.progression),
|
||||||
|
"1-7 Unlocked": SonicFrontiersItemData(offset + 61,ItemClassification.progression),
|
||||||
|
"2-1 Unlocked": SonicFrontiersItemData(offset + 62,ItemClassification.progression),
|
||||||
|
"2-2 Unlocked": SonicFrontiersItemData(offset + 63,ItemClassification.progression),
|
||||||
|
"2-3 Unlocked": SonicFrontiersItemData(offset + 64,ItemClassification.progression),
|
||||||
|
"2-4 Unlocked": SonicFrontiersItemData(offset + 65,ItemClassification.progression),
|
||||||
|
"2-5 Unlocked": SonicFrontiersItemData(offset + 66,ItemClassification.progression),
|
||||||
|
"2-6 Unlocked": SonicFrontiersItemData(offset + 67,ItemClassification.progression),
|
||||||
|
"2-7 Unlocked": SonicFrontiersItemData(offset + 68,ItemClassification.progression),
|
||||||
|
"3-1 Unlocked": SonicFrontiersItemData(offset + 69,ItemClassification.progression),
|
||||||
|
"3-2 Unlocked": SonicFrontiersItemData(offset + 70,ItemClassification.progression),
|
||||||
|
"3-3 Unlocked": SonicFrontiersItemData(offset + 71,ItemClassification.progression),
|
||||||
|
"3-4 Unlocked": SonicFrontiersItemData(offset + 72,ItemClassification.progression),
|
||||||
|
"3-5 Unlocked": SonicFrontiersItemData(offset + 73,ItemClassification.progression),
|
||||||
|
"3-6 Unlocked": SonicFrontiersItemData(offset + 74,ItemClassification.progression),
|
||||||
|
"3-7 Unlocked": SonicFrontiersItemData(offset + 75,ItemClassification.progression),
|
||||||
|
"4-1 Unlocked": SonicFrontiersItemData(offset + 76,ItemClassification.progression),
|
||||||
|
"4-2 Unlocked": SonicFrontiersItemData(offset + 78,ItemClassification.progression),
|
||||||
|
"4-3 Unlocked": SonicFrontiersItemData(offset + 79,ItemClassification.progression),
|
||||||
|
"4-4 Unlocked": SonicFrontiersItemData(offset + 80,ItemClassification.progression),
|
||||||
|
"4-5 Unlocked": SonicFrontiersItemData(offset + 81,ItemClassification.progression),
|
||||||
|
"4-6 Unlocked": SonicFrontiersItemData(offset + 82,ItemClassification.progression),
|
||||||
|
"4-7 Unlocked": SonicFrontiersItemData(offset + 83,ItemClassification.progression),
|
||||||
|
"4-8 Unlocked": SonicFrontiersItemData(offset + 84,ItemClassification.progression),
|
||||||
|
"4-9 Unlocked": SonicFrontiersItemData(offset + 85,ItemClassification.progression),
|
||||||
|
"Victory": SonicFrontiersItemData(offset + 86,ItemClassification.progression),
|
||||||
|
}
|
||||||
|
kronos_amount = {
|
||||||
|
"Kronos Memory Treasure": 9,
|
||||||
|
"Kronos Vault Key": 20,
|
||||||
|
"Progressive Chaos Emerald": 6,
|
||||||
|
"1-2 Unlocked" : 1,
|
||||||
|
"1-3 Unlocked" : 1,
|
||||||
|
"1-4 Unlocked" : 1,
|
||||||
|
"1-5 Unlocked" : 1,
|
||||||
|
"1-6 Unlocked" : 1,
|
||||||
|
"1-7 Unlocked" : 1,
|
||||||
|
"Phantom Rush": 1,
|
||||||
|
"Air Trick": 1,
|
||||||
|
"Stomp Attack": 1,
|
||||||
|
"Quick Cyloop": 1,
|
||||||
|
"Spin Dash": 1,
|
||||||
|
"Sonic Boom": 1,
|
||||||
|
"Parry": 1,
|
||||||
|
"Homing Shot": 1,
|
||||||
|
"Spin Slash": 1,
|
||||||
|
"Recovery Smash": 1,
|
||||||
|
"Cyclone Kick": 1,
|
||||||
|
"Cross Slash": 1,
|
||||||
|
"Grand Slam": 1,
|
||||||
|
}
|
||||||
|
ares_amount = {
|
||||||
|
"Ares Memory Treasure": 32,
|
||||||
|
"Ares Vault Key": 24,
|
||||||
|
"Progressive Chaos Emerald": 6,
|
||||||
|
"2-1 Unlocked" : 1,
|
||||||
|
"2-2 Unlocked" : 1,
|
||||||
|
"2-3 Unlocked" : 1,
|
||||||
|
"2-4 Unlocked" : 1,
|
||||||
|
"2-5 Unlocked" : 1,
|
||||||
|
"2-6 Unlocked" : 1,
|
||||||
|
"2-7 Unlocked" : 1,
|
||||||
|
}
|
||||||
|
chaos_amount = {
|
||||||
|
"Chaos Memory Treasure": 20,
|
||||||
|
"Chaos Vault Key": 25,
|
||||||
|
"Progressive Chaos Emerald": 6,
|
||||||
|
"3-1 Unlocked" : 1,
|
||||||
|
"3-2 Unlocked" : 1,
|
||||||
|
"3-3 Unlocked" : 1,
|
||||||
|
"3-4 Unlocked" : 1,
|
||||||
|
"3-5 Unlocked" : 1,
|
||||||
|
"3-6 Unlocked" : 1,
|
||||||
|
"3-7 Unlocked" : 1,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ouranos_amount = {
|
||||||
|
"4-1 Unlocked" : 1,
|
||||||
|
"4-2 Unlocked" : 1,
|
||||||
|
"4-3 Unlocked" : 1,
|
||||||
|
"4-4 Unlocked" : 1,
|
||||||
|
"4-5 Unlocked" : 1,
|
||||||
|
"4-6 Unlocked" : 1,
|
||||||
|
"4-7 Unlocked" : 1,
|
||||||
|
"4-8 Unlocked" : 1,
|
||||||
|
"4-9 Unlocked" : 1,
|
||||||
|
"Ouranos Memory Treasure": 26,
|
||||||
|
"Ouranos Vault Key": 33,
|
||||||
|
"Progressive Chaos Emerald": 6,
|
||||||
|
}
|
||||||
|
fillers = {
|
||||||
|
"Red Power Seed": 15,
|
||||||
|
"Blue Power Seed": 15,
|
||||||
|
"Nothing!": 10,
|
||||||
|
"Kronos Portal Gear": 5,
|
||||||
|
"Kronos Vault Key": 5,
|
||||||
|
"Ares Portal Gear": 4,
|
||||||
|
"Ares Vault Key": 4,
|
||||||
|
"Chaos Vault Key": 3,
|
||||||
|
"Chaos Portal Gear": 3,
|
||||||
|
"Ouranos Portal Gear": 2,
|
||||||
|
"Ouranos Vault Key": 2,
|
||||||
|
}
|
||||||
|
#
|
||||||
|
#traps = {
|
||||||
|
# "Water",
|
||||||
|
# "Timescale";
|
||||||
344
worlds/SonicFrontiers/locations.py
Normal file
344
worlds/SonicFrontiers/locations.py
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
from BaseClasses import Location
|
||||||
|
import typing
|
||||||
|
class AdvData(typing.NamedTuple):
|
||||||
|
id: typing.Optional[int]
|
||||||
|
|
||||||
|
class SonicFrontiersAdvancement(Location):
|
||||||
|
game: str = "Sonic Frontiers"
|
||||||
|
def create_locations():
|
||||||
|
counter = 0
|
||||||
|
kronosItems = [
|
||||||
|
"Kronos Memory Token", "Kronos Memory Token Treasure", "Kronos Portal Gear", "Kronos Vault Key" "Kronos Map Challenge"
|
||||||
|
]
|
||||||
|
aresItems = [
|
||||||
|
"Ares Memory Token", "Ares Memory Token Treasure", "Ares Portal Gear", "Ares Vault Key", "Ares Map Challenge"
|
||||||
|
]
|
||||||
|
chaosItems = [
|
||||||
|
"Chaos Memory Token", "Chaos Memory Token Treasure", "Chaos Portal Gear", "Chaos Vault Key", "Chaos Map Challenge"
|
||||||
|
]
|
||||||
|
ouranosItems = [
|
||||||
|
"Ouranos Memory Token", "Ouranos Portal Gear", "Ouranos Vault Key", "Ouranos Map Challenge"
|
||||||
|
]
|
||||||
|
kronosStages = [
|
||||||
|
"1-1",
|
||||||
|
"1-2",
|
||||||
|
"1-3",
|
||||||
|
"1-4",
|
||||||
|
"1-5",
|
||||||
|
"1-6",
|
||||||
|
"1-7",
|
||||||
|
]
|
||||||
|
aresStages = [
|
||||||
|
"2-1",
|
||||||
|
"2-2",
|
||||||
|
"2-3",
|
||||||
|
"2-4",
|
||||||
|
"2-5",
|
||||||
|
"2-6",
|
||||||
|
"2-7",
|
||||||
|
]
|
||||||
|
chaosStages = [
|
||||||
|
"3-1",
|
||||||
|
"3-2",
|
||||||
|
"3-3",
|
||||||
|
"3-4",
|
||||||
|
"3-5",
|
||||||
|
"3-6",
|
||||||
|
"3-7",
|
||||||
|
]
|
||||||
|
ouranosStages = [
|
||||||
|
"4-1",
|
||||||
|
"4-2",
|
||||||
|
"4-3",
|
||||||
|
"4-4",
|
||||||
|
"4-5",
|
||||||
|
"4-6",
|
||||||
|
"4-7",
|
||||||
|
"4-8",
|
||||||
|
"4-9",
|
||||||
|
]
|
||||||
|
skills = [
|
||||||
|
"Phantom Rush", "Air Trick", "Stomp Attack",
|
||||||
|
"Quick Cyloop", "Loop Kick", "Sonic Boom",
|
||||||
|
"Wild Rush", "Homing Shot", "Spin Slash",
|
||||||
|
"Recovery Smash", "Cyclone Kick","Cross Slash",
|
||||||
|
"Grand Slam"
|
||||||
|
]
|
||||||
|
kronosEmeralds = [
|
||||||
|
"Kronos Blue Emerald",
|
||||||
|
"Kronos Red Emerald",
|
||||||
|
"Kronos Green Emerald",
|
||||||
|
"Kronos Yellow Emerald",
|
||||||
|
"Kronos Cyan Emerald",
|
||||||
|
"Kronos White Emerald"
|
||||||
|
]
|
||||||
|
aresEmeralds = [
|
||||||
|
"Ares Blue Emerald",
|
||||||
|
"Ares Red Emerald",
|
||||||
|
"Ares Green Emerald",
|
||||||
|
"Ares Yellow Emerald",
|
||||||
|
"Ares Cyan Emerald",
|
||||||
|
"Ares White Emerald"
|
||||||
|
]
|
||||||
|
chaosEmeralds = [
|
||||||
|
"Chaos Blue Emerald",
|
||||||
|
"Chaos Red Emerald",
|
||||||
|
"Chaos Green Emerald",
|
||||||
|
"Chaos Yellow Emerald",
|
||||||
|
"Chaos Cyan Emerald",
|
||||||
|
"Chaos White Emerald"
|
||||||
|
]
|
||||||
|
ouranosEmeralds = [
|
||||||
|
"Ouranos Blue Emerald",
|
||||||
|
"Ouranos Red Emerald",
|
||||||
|
"Ouranos Green Emerald",
|
||||||
|
"Ouranos Yellow Emerald",
|
||||||
|
"Ouranos Cyan Emerald",
|
||||||
|
"Ouranos White Emerald"
|
||||||
|
]
|
||||||
|
sonicItems = [
|
||||||
|
"Skill Points (200)",
|
||||||
|
"Red Power Seed",
|
||||||
|
"Blue Power Seed",
|
||||||
|
"Kocos (20)"
|
||||||
|
]
|
||||||
|
|
||||||
|
##
|
||||||
|
## Kronos
|
||||||
|
##
|
||||||
|
|
||||||
|
#10000
|
||||||
|
counter = 0
|
||||||
|
for i in range(91):
|
||||||
|
kronosMemoryTokenSet[f"Kronos Memory Token {counter+1}"] = AdvData(kronosOff + counter)
|
||||||
|
counter += 1
|
||||||
|
#10091
|
||||||
|
counter = 0
|
||||||
|
for i in range(9):
|
||||||
|
kronosRegion[f"Kronos Memory Treasure {counter+1}"] = AdvData(kronosOff + tokenDigOffset + counter)
|
||||||
|
counter += 1
|
||||||
|
counter = 0
|
||||||
|
for i in range(17):
|
||||||
|
kronosRegion[f"Kronos Portal Gear {i+1}"] = AdvData(kronosOff + gearOffset + counter)
|
||||||
|
counter += 1
|
||||||
|
counter = 0
|
||||||
|
for i in range(5):
|
||||||
|
kronosRegion[f"Kronos Vault Key {1+i}"] = AdvData(kronosOff+keyOffset+counter)
|
||||||
|
counter += 1
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
for emerald in kronosEmeralds:
|
||||||
|
kronosRegion[f"{emerald}"] = AdvData(kronosOff+counter+emeraldOffset)
|
||||||
|
counter += 1
|
||||||
|
counter = 0
|
||||||
|
mapCounter = 1
|
||||||
|
for i in range(25):
|
||||||
|
if(mapCounter< 10):
|
||||||
|
kronosRegion[f"Map Challenge M-00{mapCounter}"] = AdvData(kronosOff+counter+mapChallengeOffset)
|
||||||
|
counter += 1
|
||||||
|
mapCounter += 1
|
||||||
|
else:
|
||||||
|
kronosRegion[f"Map Challenge M-0{mapCounter}"] = AdvData(kronosOff+counter+mapChallengeOffset)
|
||||||
|
counter += 1
|
||||||
|
mapCounter += 1
|
||||||
|
for i in range(8):
|
||||||
|
kronosNewKocoSet[f"Kronos Challenge Koco {i+1}"] = AdvData(kronosOff+i+newKocoOffset)
|
||||||
|
for i in range(13):
|
||||||
|
kronosMusicSet[f"Kronos Music Note {i+1}"] = AdvData(kronosOff+i+musicOffset)
|
||||||
|
for i in range(65):
|
||||||
|
kronosPurpleSet[f"Kronos Purple Coin {i+1}"] = AdvData(kronosOff + i + purpleCoinOffset)
|
||||||
|
for i in range(274):
|
||||||
|
kronosKocoSet[f"Kronos Koco {i+1}"] = AdvData(kronosOff + i + kocoOffset)
|
||||||
|
|
||||||
|
kronosRegion["Defeat Giganto"] = AdvData(kronosOff+bossOffset)
|
||||||
|
|
||||||
|
##
|
||||||
|
## Ares
|
||||||
|
##
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
for i in range(285):
|
||||||
|
aresMemoryTokenSet[f"Ares Memory Token {i+1}"] = AdvData(aresOff + counter)
|
||||||
|
counter += 1
|
||||||
|
for i in range(26):
|
||||||
|
aresRegion[f"Ares Memory Treasure {i+1}"] = AdvData(aresOff + tokenDigOffset + i)
|
||||||
|
counter = 0
|
||||||
|
for i in range(11):
|
||||||
|
aresRegion[f"Ares Portal Gear {i+1}"] = AdvData(aresOff + gearOffset+counter)
|
||||||
|
counter += 1
|
||||||
|
counter = 0
|
||||||
|
for i in range(7):
|
||||||
|
aresRegion[f"Ares Vault Key {1+i}"] = AdvData(aresOff+keyOffset+counter)
|
||||||
|
counter += 1
|
||||||
|
counter = 0
|
||||||
|
for emerald in aresEmeralds:
|
||||||
|
aresRegion[f"{emerald}"] = AdvData(aresOff+emeraldOffset+counter)
|
||||||
|
counter += 1
|
||||||
|
counter = 0
|
||||||
|
for i in range(29):
|
||||||
|
aresRegion[f"Map Challenge M-0{mapCounter}"] = AdvData(aresOff+mapChallengeOffset+counter)
|
||||||
|
counter += 1
|
||||||
|
mapCounter += 1
|
||||||
|
for i in range(8):
|
||||||
|
aresNewKocoSet[f"Ares Challenge Koco {i+1}"] = AdvData(aresOff+i+newKocoOffset)
|
||||||
|
for i in range(13):
|
||||||
|
aresMusicSet[f"Ares Music Note {i+1}"] = AdvData(aresOff+i+musicOffset)
|
||||||
|
for i in range(148):
|
||||||
|
aresPurpleSet[f"Ares Purple Coin {i+1}"] = AdvData(aresOff + i + purpleCoinOffset)
|
||||||
|
for i in range(360):
|
||||||
|
aresKocoSet[f"Ares Koco {i+1}"] = AdvData(aresOff + i + kocoOffset)
|
||||||
|
aresRegion["Defeat Wyvern"] = AdvData(aresOff+bossOffset)
|
||||||
|
|
||||||
|
##
|
||||||
|
## Chaos
|
||||||
|
##
|
||||||
|
|
||||||
|
for i in range(252):
|
||||||
|
chaosMemoryTokenSet[f"Chaos Memory Token {counter+1}"] = AdvData(chaosOff + i)
|
||||||
|
for i in range(20):
|
||||||
|
chaosRegion[f"Chaos Memory Treasure {i+1}"] = AdvData(chaosOff + tokenDigOffset+i)
|
||||||
|
counter = 0
|
||||||
|
for emerald in chaosEmeralds:
|
||||||
|
chaosRegion[f"{emerald}"] = AdvData(chaosOff+emeraldOffset+counter)
|
||||||
|
counter += 1
|
||||||
|
for i in range(8):
|
||||||
|
chaosRegion[f"Chaos Vault Key {1+i}"] = AdvData(chaosOff+keyOffset+i)
|
||||||
|
for i in range(13):
|
||||||
|
chaosRegion[f"Chaos Portal Gear {i+1}"] = AdvData(chaosOff +gearOffset+ i)
|
||||||
|
for i in range(24):
|
||||||
|
chaosRegion[f"Map Challenge M-0{mapCounter}"] = AdvData(chaosOff+mapChallengeOffset+i)
|
||||||
|
mapCounter += 1
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
for i in range(8):
|
||||||
|
chaosNewKocoSet[f"Chaos Challenge Koco {i+1}"] = AdvData(chaosOff+i+newKocoOffset)
|
||||||
|
for i in range(13):
|
||||||
|
chaosMusicSet[f"Chaos Music Note {i+1}"] = AdvData(chaosOff+i+musicOffset)
|
||||||
|
|
||||||
|
for i in range(188):
|
||||||
|
chaosPurpleSet[f"Chaos Purple Coin {i+1}"] = AdvData(chaosOff+purpleCoinOffset+i)
|
||||||
|
for i in range(356):
|
||||||
|
chaosKocoSet[f"Chaos Koco {i+1}"] = AdvData(chaosOff+kocoOffset+i)
|
||||||
|
chaosRegion["Defeat Knight"] = AdvData(chaosOff+bossOffset)
|
||||||
|
##
|
||||||
|
## Ouranos
|
||||||
|
##
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
for i in range(200):
|
||||||
|
ouranosMemoryTokenSet[f"Ouranos Memory Token {counter+1}"] = AdvData(ouranosOff + counter)
|
||||||
|
counter += 1
|
||||||
|
for i in range(16):
|
||||||
|
ouranosRegion[f"Ouranos Memory Treasure {i+1}"] = AdvData(ouranosOff + tokenDigOffset+i)
|
||||||
|
counter = 0
|
||||||
|
for i in range(21):
|
||||||
|
ouranosRegion[f"Ouranos Portal Gear {i+1}"] = AdvData(ouranosOff + gearOffset+counter)
|
||||||
|
counter += 1
|
||||||
|
counter = 0
|
||||||
|
for i in range(8):
|
||||||
|
ouranosRegion[f"Ouranos Vault Key {1+i}"] = AdvData(ouranosOff+keyOffset+counter)
|
||||||
|
counter += 1
|
||||||
|
counter = 0
|
||||||
|
for emerald in ouranosEmeralds:
|
||||||
|
ouranosRegion[f"{emerald}"] = AdvData(ouranosOff+emeraldOffset+counter)
|
||||||
|
counter += 1
|
||||||
|
counter = 0
|
||||||
|
for i in range(27):
|
||||||
|
if(mapCounter > 100):
|
||||||
|
ouranosRegion[f"Map Challenge M-{mapCounter}"] = AdvData(ouranosOff+mapChallengeOffset+counter)
|
||||||
|
counter += 1
|
||||||
|
mapCounter += 1
|
||||||
|
else:
|
||||||
|
ouranosRegion[f"Map Challenge M-0{mapCounter}"] = AdvData(ouranosOff+mapChallengeOffset+counter)
|
||||||
|
counter += 1
|
||||||
|
mapCounter += 1
|
||||||
|
counter = 0
|
||||||
|
for stage in kronosStages:
|
||||||
|
for i in range(7):
|
||||||
|
kronosRegion[f"{stage} All Missions ({i+1})"] = AdvData(cyberspaceOffset + counter)
|
||||||
|
counter += 1
|
||||||
|
for stage in aresStages:
|
||||||
|
for i in range(7):
|
||||||
|
aresRegion[f"{stage} All Missions ({i+1})"] = AdvData(cyberspaceOffset + counter)
|
||||||
|
counter += 1
|
||||||
|
for stage in chaosStages:
|
||||||
|
for i in range(7):
|
||||||
|
chaosRegion[f"{stage} All Missions ({i+1})"] = AdvData(cyberspaceOffset+counter)
|
||||||
|
counter += 1
|
||||||
|
for stage in ouranosStages:
|
||||||
|
for i in range(7):
|
||||||
|
ouranosRegion[f"{stage} All Missions ({i+1})"] = AdvData(cyberspaceOffset+ counter)
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
for i in range(8):
|
||||||
|
ouranosNewKocoSet[f"Ouranos Challenge Koco {i+1}"] = AdvData(ouranosOff+i+newKocoOffset)
|
||||||
|
for i in range(13):
|
||||||
|
ouranosMusicSet[f"Ouranos Music Note {i+1}"] = AdvData(ouranosOff+i+musicOffset)
|
||||||
|
for i in range(317):
|
||||||
|
ouranosPurpleSet[f"Ouranos Purple Coin {i+1}"] = AdvData(ouranosOff+purpleCoinOffset+i)
|
||||||
|
for i in range(358):
|
||||||
|
ouranosKocoSet[f"Ouranos Koco {i+1}"] = AdvData(ouranosOff+kocoOffset+i)
|
||||||
|
ouranosRegion["Defeat Supreme"] = AdvData(ouranosOff+bossOffset)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##Essentially add all of these into a region. How do I add them? idfk
|
||||||
|
kronosRegion = {}
|
||||||
|
aresRegion = {}
|
||||||
|
chaosRegion = {}
|
||||||
|
ouranosRegion = {}
|
||||||
|
victoryRegion = {}
|
||||||
|
|
||||||
|
kronosMemoryTokenSet = {}
|
||||||
|
aresMemoryTokenSet = {}
|
||||||
|
chaosMemoryTokenSet = {}
|
||||||
|
ouranosMemoryTokenSet = {}
|
||||||
|
|
||||||
|
kronosPurpleSet = {}
|
||||||
|
aresPurpleSet = {}
|
||||||
|
chaosPurpleSet = {}
|
||||||
|
ouranosPurpleSet = {}
|
||||||
|
|
||||||
|
kronosKocoSet = {}
|
||||||
|
aresKocoSet = {}
|
||||||
|
chaosKocoSet = {}
|
||||||
|
ouranosKocoSet = {}
|
||||||
|
|
||||||
|
kronosNewKocoSet = {}
|
||||||
|
aresNewKocoSet = {}
|
||||||
|
chaosNewKocoSet = {}
|
||||||
|
ouranosNewKocoSet = {}
|
||||||
|
|
||||||
|
kronosMusicSet = {}
|
||||||
|
aresMusicSet = {}
|
||||||
|
chaosMusicSet = {}
|
||||||
|
ouranosMusicSet = {}
|
||||||
|
|
||||||
|
kronosOff: int = 10000
|
||||||
|
aresOff: int = 20000
|
||||||
|
chaosOff: int = 30000
|
||||||
|
ouranosOff: int = 40000
|
||||||
|
cyberspaceOffset: int = 50000
|
||||||
|
|
||||||
|
tokenDigOffset: int = 500
|
||||||
|
emeraldOffset: int = 1000
|
||||||
|
musicOffset: int = 1500
|
||||||
|
gearOffset: int = 2000
|
||||||
|
newKocoOffset: int = 2500
|
||||||
|
keyOffset: int = 3000
|
||||||
|
purpleCoinOffset: int = 4000 #10000 + i + 4000
|
||||||
|
mapChallengeOffset: int = 5000
|
||||||
|
kocoOffset: int = 6000
|
||||||
|
bossOffset: int = 7000
|
||||||
|
ringOffset: int = 100000
|
||||||
|
|
||||||
|
|
||||||
|
create_locations()
|
||||||
|
|
||||||
|
kronosItems = kronosRegion | kronosMusicSet | kronosKocoSet | kronosMemoryTokenSet | kronosNewKocoSet | kronosPurpleSet
|
||||||
|
aresItems = aresRegion | aresMusicSet | aresKocoSet | aresMemoryTokenSet | aresNewKocoSet | aresPurpleSet
|
||||||
|
chaosItems = chaosRegion | chaosMusicSet | chaosKocoSet | chaosMemoryTokenSet | chaosNewKocoSet | chaosPurpleSet
|
||||||
|
ouranosItems = ouranosRegion | ouranosMusicSet | ouranosKocoSet | ouranosMemoryTokenSet | ouranosNewKocoSet | ouranosPurpleSet
|
||||||
|
|
||||||
|
all_items = kronosItems | aresItems | chaosItems | ouranosItems
|
||||||
71
worlds/SonicFrontiers/options.py
Normal file
71
worlds/SonicFrontiers/options.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from Options import Toggle, DefaultOnToggle, DeathLink, Range, Choice, PerGameCommonOptions, ExcludeLocations # , OptionGroup
|
||||||
|
|
||||||
|
|
||||||
|
class Goal(Choice):
|
||||||
|
"""
|
||||||
|
Which Titan to defeat in order to complete the randomizer
|
||||||
|
|
||||||
|
Note: Only Giganto/Kronos Island for this release
|
||||||
|
"""
|
||||||
|
display_name = "Goal"
|
||||||
|
option_defeat_giganto = 0
|
||||||
|
#option_defeat_wyvern = 1
|
||||||
|
#option_defeat_knight = 2
|
||||||
|
#option_defeat_supreme = 3
|
||||||
|
default = 0
|
||||||
|
class MemoryTokenSanity(Toggle):
|
||||||
|
"""
|
||||||
|
Set whether All Memory Tokens should be locations
|
||||||
|
"""
|
||||||
|
display_name = "Memory Token Sanity"
|
||||||
|
default = 0
|
||||||
|
class MapChallenges(DefaultOnToggle):
|
||||||
|
display_name = "Map Challenge Sanity"
|
||||||
|
class HarderCyberspaceTimes(Toggle):
|
||||||
|
"""
|
||||||
|
This makes all Cyberspace stages have a harder S-Rank requirement.
|
||||||
|
|
||||||
|
Note: This is meant for speedrunners, do not enable this unless you're up for a challenge.
|
||||||
|
"""
|
||||||
|
display_name = "Harder Cyberspace Challenge Times"
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
class MusicNotes(Toggle):
|
||||||
|
"""
|
||||||
|
Set whether Music Notes should be locations
|
||||||
|
"""
|
||||||
|
display_name = "Music Notes"
|
||||||
|
default = 0
|
||||||
|
class ChallengeKocos(Toggle):
|
||||||
|
"""
|
||||||
|
Set whether Challenge Kocos should be locations
|
||||||
|
"""
|
||||||
|
display_name = "Challenge Kocos"
|
||||||
|
default = 0
|
||||||
|
class CyberspaceStages(Toggle):
|
||||||
|
display_name = "Cyberspace Stages Missions"
|
||||||
|
default = 0
|
||||||
|
class PurpleCoinSanity(Toggle):
|
||||||
|
"""
|
||||||
|
Set whether All Purple Coins should be locations
|
||||||
|
"""
|
||||||
|
display_name = "Purple Coin Sanity"
|
||||||
|
default = 0
|
||||||
|
class KocoSanity(Toggle):
|
||||||
|
"""
|
||||||
|
Set whether All Kocos should be locations
|
||||||
|
"""
|
||||||
|
display_name = "Koco Sanity"
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SonicFrontiersOptions(PerGameCommonOptions):
|
||||||
|
goal: Goal
|
||||||
|
death_link: DeathLink
|
||||||
|
memory_token_sanity: MemoryTokenSanity
|
||||||
|
cyberspace_times: HarderCyberspaceTimes
|
||||||
|
music_notes: MusicNotes
|
||||||
|
challenge_kocos: ChallengeKocos
|
||||||
|
purple_coin_sanity: PurpleCoinSanity
|
||||||
|
koco_sanity: KocoSanity
|
||||||
4115
worlds/apeescape/Client.py
Normal file
4115
worlds/apeescape/Client.py
Normal file
File diff suppressed because it is too large
Load Diff
666
worlds/apeescape/ItemHandlers.py
Normal file
666
worlds/apeescape/ItemHandlers.py
Normal file
@@ -0,0 +1,666 @@
|
|||||||
|
import random
|
||||||
|
import time
|
||||||
|
import worlds._bizhawk as bizhawk
|
||||||
|
|
||||||
|
from .RAMAddress import RAM
|
||||||
|
from typing import TYPE_CHECKING, Optional, Dict, Set, ClassVar, Any, Tuple, Union
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from worlds._bizhawk.context import BizHawkClientContext, BizHawkClientCommandProcessor
|
||||||
|
|
||||||
|
class ApeEscapeMemoryInput:
|
||||||
|
def __init__(self, bizhawk_client_context: "BizHawkClientContext"):
|
||||||
|
self.bizhawk_client_context = bizhawk_client_context
|
||||||
|
|
||||||
|
self.all_digital_buttons = list(RAM.BUTTON_BIT_MAP.keys())
|
||||||
|
# Combined list of all possible inputs for the handler to choose from (includes Pseudo-Right Joystick)
|
||||||
|
self.all_inputs = self.all_digital_buttons + [RAM.RIGHT_JOYSTICK_PSEUDO_INPUT]
|
||||||
|
|
||||||
|
async def set_inputs(self, desired_inputs: dict):
|
||||||
|
"""
|
||||||
|
Constructs and sends memory write requests to BizHawk to set controller inputs.
|
||||||
|
This method sends writes ONLY for the inputs explicitly present in desired_inputs.
|
||||||
|
If an input (digital or analog) is NOT in desired_inputs, it's assumed to be released,
|
||||||
|
and its memory address will NOT be explicitly written by this client for that frame.
|
||||||
|
This relies on the game's natural input polling to reset non-written inputs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
desired_inputs (dict): A dictionary where keys are input names (e.g., "P1 X", "P1 R_X")
|
||||||
|
and values are True/False for digital buttons, or 0-255 for analog.
|
||||||
|
"""
|
||||||
|
writes_list = []
|
||||||
|
|
||||||
|
# --- 1. Construct Digital Button Bytes ---
|
||||||
|
# Start with all bits set to 1 (all digital buttons unpressed in inverse logic)
|
||||||
|
# This will be the base for the digital button memory write.
|
||||||
|
new_digital_word = 0xFFFF
|
||||||
|
|
||||||
|
for input_name, state in desired_inputs.items():
|
||||||
|
if input_name in RAM.BUTTON_BIT_MAP:
|
||||||
|
bit_pos = RAM.BUTTON_BIT_MAP[input_name]
|
||||||
|
if state is True: # If button is desired to be pressed
|
||||||
|
# Clear its corresponding bit (set to 0) in the 16-bit word
|
||||||
|
new_digital_word &= ~(1 << bit_pos)
|
||||||
|
# If state is False, leave the bit as 1 (unpressed), which is default in new_digital_word.
|
||||||
|
|
||||||
|
# Split the 16-bit word into two 8-bit integers
|
||||||
|
byte_low_value = new_digital_word & 0xFF
|
||||||
|
byte_high_value = (new_digital_word >> 8) & 0xFF
|
||||||
|
|
||||||
|
# Add digital button writes to the list
|
||||||
|
writes_list.append((RAM.BUTTON_BYTE_ADDR_LOW, [byte_low_value], "MainRAM"))
|
||||||
|
writes_list.append((RAM.BUTTON_BYTE_ADDR_HIGH, [byte_high_value], "MainRAM"))
|
||||||
|
|
||||||
|
# --- 2. Construct Analog Stick Bytes ---
|
||||||
|
analog_axis_addresses = {
|
||||||
|
"P1 R_Y": RAM.ANALOG_START_ADDR,
|
||||||
|
"P1 R_X": RAM.ANALOG_START_ADDR + 1,
|
||||||
|
"P1 L_Y": RAM.ANALOG_START_ADDR + 2,
|
||||||
|
"P1 L_X": RAM.ANALOG_START_ADDR + 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Iterate through desired_inputs and add writes only for present analog axes.
|
||||||
|
for stick_axis, value_to_set in desired_inputs.items():
|
||||||
|
if stick_axis in analog_axis_addresses:
|
||||||
|
clamped_value = max(0, min(255, value_to_set))
|
||||||
|
writes_list.append((analog_axis_addresses[stick_axis], [clamped_value], "MainRAM"))
|
||||||
|
|
||||||
|
# --- 3. Send all constructed writes to BizHawk ---
|
||||||
|
try:
|
||||||
|
await bizhawk.write(self.bizhawk_client_context.bizhawk_ctx, writes_list)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to send input memory writes: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# --- MonkeyMashHandler class ---
|
||||||
|
|
||||||
|
class MonkeyMashHandler:
|
||||||
|
MAX_TRAP_DURATION = 20 # Maximum duration for the trap in seconds
|
||||||
|
|
||||||
|
def __init__(self, bizhawk_client_context: Union["BizHawkClientContext", None]):
|
||||||
|
self.bizhawk_client_context = bizhawk_client_context
|
||||||
|
self.bizhawk_context = bizhawk_client_context.bizhawk_ctx if bizhawk_client_context else None
|
||||||
|
|
||||||
|
self.is_active = False
|
||||||
|
self.duration = 0
|
||||||
|
self.remaining_time = 0
|
||||||
|
self.last_update = 0
|
||||||
|
|
||||||
|
self.pause = False
|
||||||
|
|
||||||
|
self.input_controller = ApeEscapeMemoryInput(
|
||||||
|
self.bizhawk_client_context) if self.bizhawk_client_context else None
|
||||||
|
|
||||||
|
self.input_frequency = 1 # Time between NEW random inputs (e.g., generate new input every 1s)
|
||||||
|
self.last_input_time = 0
|
||||||
|
|
||||||
|
self.input_hold_time = 0.5 # How long the inputs will be pressed
|
||||||
|
|
||||||
|
self.current_held_inputs = {} # Stores inputs that are currently being pressed
|
||||||
|
self.press_start_time = None # Timestamp when the current brief press started
|
||||||
|
self.sentMessage = True # To track if the last activation sent a Bizhawk message on expiration
|
||||||
|
|
||||||
|
def activate_monkey(self, duration_seconds: int):
|
||||||
|
if not self.is_active:
|
||||||
|
self.is_active = True
|
||||||
|
self.duration = duration_seconds
|
||||||
|
self.remaining_time = duration_seconds
|
||||||
|
self.last_update = time.time()
|
||||||
|
self.last_input_time = 0
|
||||||
|
self.current_held_inputs = {}
|
||||||
|
self.press_start_time = None
|
||||||
|
print(f"Monkey Button Mash activated for {duration_seconds} seconds.")
|
||||||
|
else:
|
||||||
|
new_remaining_time = self.remaining_time + duration_seconds
|
||||||
|
self.remaining_time = min(new_remaining_time, self.MAX_TRAP_DURATION)
|
||||||
|
self.duration = self.remaining_time
|
||||||
|
print(f"Monkey Button Mash extended by {duration_seconds} seconds. Total remaining: {self.remaining_time:.2f}s (capped at {self.MAX_TRAP_DURATION}s)")
|
||||||
|
self.sentMessage = False
|
||||||
|
async def send_monkey_inputs(self):
|
||||||
|
if self.input_controller is None or self.bizhawk_client_context.bizhawk_ctx.connection_status != bizhawk.ConnectionStatus.CONNECTED:
|
||||||
|
print("Error: BizHawk connection not ready for inputs. Cannot send inputs.")
|
||||||
|
self.is_active = False
|
||||||
|
self.current_held_inputs = {}
|
||||||
|
self.press_start_time = None
|
||||||
|
return
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
if self.pause:
|
||||||
|
self.current_held_inputs = {}
|
||||||
|
self.press_start_time = None
|
||||||
|
self.last_update = current_time
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.is_active and self.remaining_time > 0:
|
||||||
|
elapsed_time_since_last_update = current_time - self.last_update
|
||||||
|
self.remaining_time -= elapsed_time_since_last_update
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
if self.remaining_time <= 0:
|
||||||
|
self.remaining_time = 0
|
||||||
|
|
||||||
|
# State 1: It's time to generate a NEW input sequence (press for hold_time)
|
||||||
|
if current_time - self.last_input_time >= self.input_frequency:
|
||||||
|
newly_generated_inputs = {}
|
||||||
|
|
||||||
|
# Randomly select MULTIPLE inputs (digital buttons or the "Right Joystick" pseudo-input)
|
||||||
|
num_inputs_to_change = random.randint(1, 3)
|
||||||
|
inputs_to_change = random.sample(self.input_controller.all_inputs, num_inputs_to_change)
|
||||||
|
|
||||||
|
for input_name in inputs_to_change:
|
||||||
|
if input_name == RAM.RIGHT_JOYSTICK_PSEUDO_INPUT: # Handle the Right Joystick as one unit
|
||||||
|
# Randomly pick one axis to be 0xFF, the other to be random
|
||||||
|
axis_to_be_max = random.choice(["P1 R_Y", "P1 R_X"])
|
||||||
|
axis_to_be_random = "P1 R_Y" if axis_to_be_max == "P1 R_X" else "P1 R_X"
|
||||||
|
|
||||||
|
newly_generated_inputs[axis_to_be_max] = 0xFF
|
||||||
|
newly_generated_inputs[axis_to_be_random] = random.randint(0x00, 0xFF)
|
||||||
|
# Left Joystick values are not touched here.
|
||||||
|
else:
|
||||||
|
# For digital buttons, set to pressed (True)
|
||||||
|
newly_generated_inputs[input_name] = True
|
||||||
|
|
||||||
|
self.current_held_inputs = newly_generated_inputs
|
||||||
|
self.press_start_time = current_time
|
||||||
|
self.last_input_time = current_time
|
||||||
|
|
||||||
|
#print(f"[{self.remaining_time:.2f}s remaining] Initiating brief press of: {self.current_held_inputs}")
|
||||||
|
|
||||||
|
# State 2: Check if a brief press is active and within its hold window
|
||||||
|
if self.press_start_time is not None and (current_time - self.press_start_time < self.input_hold_time):
|
||||||
|
try:
|
||||||
|
await self.input_controller.set_inputs(self.current_held_inputs)
|
||||||
|
except bizhawk.NotConnectedError:
|
||||||
|
print("BizHawk connection lost during input hold. Deactivating monkey.")
|
||||||
|
self.is_active = False
|
||||||
|
self.current_held_inputs = {}
|
||||||
|
self.press_start_time = None
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to hold inputs via memory write: {e}")
|
||||||
|
self.is_active = False
|
||||||
|
print("Monkey Button Mash deactivated due to input hold error.")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# State 3: If no press is active, or hold time has expired, stop sending *active* inputs.
|
||||||
|
if self.current_held_inputs or self.press_start_time is not None:
|
||||||
|
print(f"[{self.remaining_time:.2f}s remaining] Releasing inputs. Held: {self.current_held_inputs}")
|
||||||
|
|
||||||
|
self.current_held_inputs = {} # Clear tracker for *briefly held* inputs
|
||||||
|
self.press_start_time = None # Reset press start time
|
||||||
|
|
||||||
|
if self.is_active and self.remaining_time <= 0:
|
||||||
|
self.is_active = False
|
||||||
|
self.remaining_time = 0
|
||||||
|
self.current_held_inputs = {}
|
||||||
|
self.press_start_time = None
|
||||||
|
print("Monkey Button Mash finished.")
|
||||||
|
|
||||||
|
class RainbowCookieHandler:
|
||||||
|
"""
|
||||||
|
Manages the state and effects of the Rainbow Cookie power-up.
|
||||||
|
When active, makes Spike invincible and activates his golden form.
|
||||||
|
"""
|
||||||
|
MAX_DURATION = 20 # Maximum duration for the Rainbow Cookie in seconds
|
||||||
|
|
||||||
|
def __init__(self, bizhawk_client_context: Union["BizHawkClientContext", None]):
|
||||||
|
self.bizhawk_client_context = bizhawk_client_context
|
||||||
|
self.bizhawk_context = bizhawk_client_context.bizhawk_ctx if bizhawk_client_context else None
|
||||||
|
|
||||||
|
self.is_active = False # True if Rainbow Cookie effects are currently active
|
||||||
|
self.duration = 0 # The initial or current duration set for the cookie
|
||||||
|
self.remaining_time = 0 # How much time is left for the effects
|
||||||
|
self.last_update = 0 # Timestamp of the last update, for calculating elapsed time
|
||||||
|
self.pause = False # Flag to pause the cookie's timer/effects
|
||||||
|
self.sentMessage = True # To track if the last activation sent a Bizhawk message on expiration
|
||||||
|
async def activate_rainbow_cookie(self, duration_seconds: int):
|
||||||
|
"""
|
||||||
|
Activates the Rainbow Cookie effects (invincibility and golden form).
|
||||||
|
If already active, extends the duration up to MAX_DURATION.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
duration_seconds (int): The number of seconds to activate/extend the cookie's effects.
|
||||||
|
"""
|
||||||
|
if not self.is_active:
|
||||||
|
# First activation
|
||||||
|
self.is_active = True
|
||||||
|
self.duration = duration_seconds
|
||||||
|
self.remaining_time = duration_seconds
|
||||||
|
self.last_update = time.time()
|
||||||
|
print(f"Rainbow Cookie activated for {duration_seconds} seconds.")
|
||||||
|
await self._apply_effects(True) # Apply effects immediately
|
||||||
|
else:
|
||||||
|
# Extend existing duration
|
||||||
|
new_remaining_time = self.remaining_time + duration_seconds
|
||||||
|
self.remaining_time = min(new_remaining_time, self.MAX_DURATION)
|
||||||
|
self.duration = self.remaining_time # Update current duration if extended
|
||||||
|
print(
|
||||||
|
f"Rainbow Cookie extended by {duration_seconds} seconds. Total remaining: {self.remaining_time:.2f}s (capped at {self.MAX_DURATION}s)")
|
||||||
|
self.sentMessage = False
|
||||||
|
async def _apply_effects(self, enable: bool):
|
||||||
|
"""
|
||||||
|
Internal method to apply or remove the Rainbow Cookie's effects
|
||||||
|
by writing to BizHawk memory addresses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enable (bool): If True, enables effects; if False, disables them.
|
||||||
|
"""
|
||||||
|
if self.bizhawk_context is None or self.bizhawk_context.connection_status != bizhawk.ConnectionStatus.CONNECTED:
|
||||||
|
print("Warning: BizHawk not connected. Cannot apply/remove Rainbow Cookie effects.")
|
||||||
|
return
|
||||||
|
|
||||||
|
writes_list = []
|
||||||
|
invincible_value = RAM.INVINCIBLE_ON_VALUE if enable else RAM.INVINCIBLE_OFF_VALUE
|
||||||
|
golden_value = RAM.GOLDEN_ON_VALUE if enable else RAM.GOLDEN_OFF_VALUE
|
||||||
|
|
||||||
|
invincible_bytes = list(invincible_value.to_bytes(4, "little"))
|
||||||
|
|
||||||
|
writes_list.append((RAM.SPIKE_INVINCIBILITY_ADDR, invincible_bytes, "MainRAM"))
|
||||||
|
writes_list.append((RAM.SPIKE_GOLDEN_FORM_ADDR, [golden_value], "MainRAM"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bizhawk.write(self.bizhawk_context, writes_list)
|
||||||
|
print(f"Rainbow Cookie effects {'applied' if enable else 'removed'}.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to {'apply' if enable else 'remove'} Rainbow Cookie effects: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def update_state_and_deactivate(self):
|
||||||
|
"""
|
||||||
|
Updates the remaining time for the Rainbow Cookie.
|
||||||
|
If the duration runs out, deactivates the effects.
|
||||||
|
This method should be called periodically in the main loop of the client.
|
||||||
|
It also re-applies the golden visual effect if it's lost and the cookie is active.
|
||||||
|
"""
|
||||||
|
if not self.is_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.pause:
|
||||||
|
# If paused, don't decrement remaining_time, but update last_update
|
||||||
|
# to prevent a large time jump when unpaused.
|
||||||
|
self.last_update = time.time()
|
||||||
|
return
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
elapsed_time_since_last_update = current_time - self.last_update
|
||||||
|
self.remaining_time -= elapsed_time_since_last_update
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
# Check and re-apply golden visual effect if it's not active but the cookie is
|
||||||
|
if self.bizhawk_context and self.bizhawk_context.connection_status == bizhawk.ConnectionStatus.CONNECTED:
|
||||||
|
try:
|
||||||
|
# Read the current value of the golden form address
|
||||||
|
current_golden_value_bytes = await bizhawk.read(self.bizhawk_context, [(RAM.SPIKE_GOLDEN_FORM_ADDR, 1, "MainRAM")])
|
||||||
|
current_golden_value = int.from_bytes(current_golden_value_bytes[0], byteorder="little")
|
||||||
|
|
||||||
|
if current_golden_value is not None and current_golden_value != RAM.GOLDEN_ON_VALUE:
|
||||||
|
print("Rainbow Cookie active, but golden visual effect lost. Reapplying...")
|
||||||
|
await self._apply_effects(True)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to read golden form address for reapplication: {e}")
|
||||||
|
# Log error but don't stop the loop for this non-critical re-application check
|
||||||
|
|
||||||
|
if self.remaining_time <= 0:
|
||||||
|
self.remaining_time = 0
|
||||||
|
self.is_active = False
|
||||||
|
print("Rainbow Cookie duration finished. Deactivating effects.")
|
||||||
|
await self._apply_effects(False) # Remove effects
|
||||||
|
|
||||||
|
class StunTrapHandler:
|
||||||
|
"""
|
||||||
|
Manages the state and effects of the Stun Trap.
|
||||||
|
When active, makes Spike invincible and activates his golden form.
|
||||||
|
"""
|
||||||
|
MAX_DURATION = 2
|
||||||
|
|
||||||
|
def __init__(self, bizhawk_client_context: Union["BizHawkClientContext", None]):
|
||||||
|
self.bizhawk_client_context = bizhawk_client_context
|
||||||
|
self.bizhawk_context = bizhawk_client_context.bizhawk_ctx if bizhawk_client_context else None
|
||||||
|
|
||||||
|
self.is_active = False # True if Rainbow Cookie effects are currently active
|
||||||
|
self.RoomType = "Special"
|
||||||
|
self.duration = 0 # The initial or current duration set for the cookie
|
||||||
|
self.remaining_time = 0 # How much time is left for the effects
|
||||||
|
self.last_update = 0 # Timestamp of the last update, for calculating elapsed time
|
||||||
|
self.pause = False # Flag to pause the cookie's timer/effects
|
||||||
|
self.lastspikestate = 0x00 # To store last SpikeState on activation
|
||||||
|
self.sentMessage = True # To track if the last activation sent a Bizhawk message on expiration
|
||||||
|
async def activate_StunTrap(self, duration_seconds: int,lastspikestate,currentRoom):
|
||||||
|
"""
|
||||||
|
Activates the Stun Trap effects.
|
||||||
|
If already active, extends the duration up to MAX_DURATION.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
duration_seconds (int): The number of seconds to activate/extend the cookie's effects.
|
||||||
|
"""
|
||||||
|
# Only store a new lastspikestate if the trap is not activated
|
||||||
|
# (Since if it's already active we already have the last spike state)
|
||||||
|
if not self.is_active:
|
||||||
|
self.lastspikestate = lastspikestate
|
||||||
|
# First activation
|
||||||
|
self.is_active = True
|
||||||
|
|
||||||
|
read_list = []
|
||||||
|
read_list += [(RAM.SpecialRoom_CameraMode, 1, "MainRAM"),(RAM.Boss_CameraMode, 1, "MainRAM"),(RAM.Inside_CameraMode, 1, "MainRAM")]
|
||||||
|
CameraMode_reads = await bizhawk.read(self.bizhawk_context, read_list)
|
||||||
|
isSpecialRoom = int.from_bytes(CameraMode_reads[0], byteorder="little") == 0x01
|
||||||
|
isBossRoom = int.from_bytes(CameraMode_reads[1], byteorder="little") == 0x01
|
||||||
|
isInside = int.from_bytes(CameraMode_reads[2], byteorder="little") == 0x01
|
||||||
|
if isSpecialRoom:
|
||||||
|
self.RoomType = "Special"
|
||||||
|
elif isBossRoom:
|
||||||
|
self.RoomType = "Boss"
|
||||||
|
elif isInside:
|
||||||
|
self.RoomType = "Inside"
|
||||||
|
else:
|
||||||
|
self.RoomType = "Outside"
|
||||||
|
|
||||||
|
self.duration = duration_seconds
|
||||||
|
self.remaining_time = duration_seconds
|
||||||
|
self.last_update = time.time()
|
||||||
|
print(f"Stun Trap activated for {duration_seconds} seconds.")
|
||||||
|
await self._apply_effects(True,currentRoom) # Apply effects immediately
|
||||||
|
#else:
|
||||||
|
# Activate it each time, do not extend it
|
||||||
|
# Extend existing duration
|
||||||
|
#new_remaining_time = self.remaining_time + duration_seconds
|
||||||
|
#self.remaining_time = min(new_remaining_time, self.MAX_DURATION)
|
||||||
|
#self.duration = self.remaining_time # Update current duration if extended
|
||||||
|
#print(f"Stun Trap extended by {duration_seconds} seconds. Total remaining: {self.remaining_time:.2f}s (capped at {self.MAX_DURATION}s)")
|
||||||
|
#self.sentMessage = False
|
||||||
|
async def _apply_effects(self, enable: bool,currentRoom):
|
||||||
|
"""
|
||||||
|
Internal method to apply or remove the Rainbow Cookie's effects
|
||||||
|
by writing to BizHawk memory addresses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enable (bool): If True, enables effects; if False, disables them.
|
||||||
|
"""
|
||||||
|
if self.bizhawk_context is None or self.bizhawk_context.connection_status != bizhawk.ConnectionStatus.CONNECTED:
|
||||||
|
print("Warning: BizHawk not connected. Cannot apply/remove Stun Trap effects.")
|
||||||
|
return
|
||||||
|
|
||||||
|
writes_list = []
|
||||||
|
Spike_PosUpdatesAddress = RAM.Spike_PosUpdates
|
||||||
|
Spike_PosUpdates_keys = list(Spike_PosUpdatesAddress.keys())
|
||||||
|
Spike_PosUpdates_values = list(Spike_PosUpdatesAddress.values())
|
||||||
|
|
||||||
|
#for x in range(len(Spike_PosUpdates_keys)):
|
||||||
|
# PosUpdates_values = list(Spike_PosUpdates_values[x])
|
||||||
|
# PosUpdates_bytes = PosUpdates_values[0]
|
||||||
|
# PosUpdates_onvalue = PosUpdates_values[1].to_bytes(PosUpdates_bytes, "little")
|
||||||
|
# PosUpdates_offvalue = PosUpdates_values[2].to_bytes(PosUpdates_bytes, "little")
|
||||||
|
# PosUpdates_address = (Spike_PosUpdates_keys[x])
|
||||||
|
if enable:
|
||||||
|
writes_list += [(RAM.Spike_CanMove, 0x01.to_bytes(1,"little"), "MainRAM")]
|
||||||
|
else:
|
||||||
|
writes_list += [(RAM.Spike_CanMove, 0x00.to_bytes(1,"little"), "MainRAM")]
|
||||||
|
|
||||||
|
Spike_VelocityUpdatesAddress = RAM.Spike_VelocityUpdates
|
||||||
|
Spike_VelocityUpdates_keys = list(Spike_VelocityUpdatesAddress.keys())
|
||||||
|
Spike_VelocityUpdates_values = list(Spike_VelocityUpdatesAddress.values())
|
||||||
|
|
||||||
|
#for x in range(len(Spike_VelocityUpdates_keys)):
|
||||||
|
# VelocityUpdates_values = list(Spike_VelocityUpdates_values[x])
|
||||||
|
# VelocityUpdates_bytes = VelocityUpdates_values[0]
|
||||||
|
# VelocityUpdates_onvalue = VelocityUpdates_values[1].to_bytes(VelocityUpdates_bytes, "little")
|
||||||
|
# VelocityUpdates_offvalue = VelocityUpdates_values[2].to_bytes(VelocityUpdates_bytes, "little")
|
||||||
|
# VelocityUpdates_address = (Spike_VelocityUpdates_keys[x])
|
||||||
|
# if enable:
|
||||||
|
# writes_list += [(VelocityUpdates_address, VelocityUpdates_offvalue, "MainRAM")]
|
||||||
|
# else:
|
||||||
|
# writes_list += [(VelocityUpdates_address, VelocityUpdates_onvalue, "MainRAM")]
|
||||||
|
|
||||||
|
#LastState = self.lastspikestate
|
||||||
|
|
||||||
|
#InvalidLastStates = [0x80, 0x81, 0x82, 0x83, 0x84,0x2F,0x30,0x58]
|
||||||
|
# If enabling the Trap set it to 0x58, else set it to the last saved state
|
||||||
|
#if enable:
|
||||||
|
# Spikestate2_value = 0x58
|
||||||
|
# CameraMode = 0x00
|
||||||
|
#else:
|
||||||
|
# #If LastState is invalid,
|
||||||
|
# Spikestate2_value = 0x00 if LastState in InvalidLastStates else LastState
|
||||||
|
# CameraMode = 0x01
|
||||||
|
# self.lastspikestate = 0x00
|
||||||
|
|
||||||
|
#SpecialRooms = [30,83, 84, 87, 88, 90, 91]
|
||||||
|
#BossRooms = [item for item in RAM.bossListLocal.keys() if item not in SpecialRooms]
|
||||||
|
|
||||||
|
#if self.RoomType == "Special":
|
||||||
|
# CameraModeAddress = RAM.SpecialRoom_CameraMode
|
||||||
|
#elif self.RoomType == "Boss":
|
||||||
|
# CameraModeAddress = RAM.Boss_CameraMode
|
||||||
|
#else:
|
||||||
|
# if self.RoomType == "Inside":
|
||||||
|
# CameraModeAddress = RAM.Inside_CameraMode
|
||||||
|
# else:
|
||||||
|
# CameraModeAddress = RAM.Outside_CameraMode
|
||||||
|
|
||||||
|
#Spikestate2_bytes = list(Spikestate2_value.to_bytes(1, "little"))
|
||||||
|
#CameraMode_bytes = list(CameraMode.to_bytes(1, "little"))
|
||||||
|
|
||||||
|
#writes_list.append((RAM.spikeState2Address, Spikestate2_bytes, "MainRAM"))
|
||||||
|
#writes_list.append((CameraModeAddress, CameraMode_bytes, "MainRAM"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bizhawk.write(self.bizhawk_context, writes_list)
|
||||||
|
print(f"Stun Trap effects {'applied' if enable else 'removed'}.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to {'apply' if enable else 'remove'} Stun Trap effects: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def update_state_and_deactivate(self,currentRoom):
|
||||||
|
"""
|
||||||
|
Updates the remaining time for the Rainbow Cookie.
|
||||||
|
If the duration runs out, deactivates the effects.
|
||||||
|
This method should be called periodically in the main loop of the client.
|
||||||
|
It also re-applies the golden visual effect if it's lost and the cookie is active.
|
||||||
|
"""
|
||||||
|
if not self.is_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.pause:
|
||||||
|
# If paused, don't decrement remaining_time, but update last_update
|
||||||
|
# to prevent a large time jump when unpaused.
|
||||||
|
self.last_update = time.time()
|
||||||
|
return
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
elapsed_time_since_last_update = current_time - self.last_update
|
||||||
|
self.remaining_time -= elapsed_time_since_last_update
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
if self.remaining_time <= 0:
|
||||||
|
self.remaining_time = 0
|
||||||
|
self.is_active = False
|
||||||
|
print("Stun Trap duration finished. Deactivating effects.")
|
||||||
|
await self._apply_effects(False,currentRoom) # Remove effects
|
||||||
|
|
||||||
|
class CameraRotateHandler:
|
||||||
|
"""
|
||||||
|
Manages the state and effects of the Rainbow Cookie power-up.
|
||||||
|
When active, makes Spike invincible and activates his golden form.
|
||||||
|
"""
|
||||||
|
MAX_DURATION = 40 # Maximum duration for the Rainbow Cookie in seconds
|
||||||
|
|
||||||
|
def __init__(self, bizhawk_client_context: Union["BizHawkClientContext", None]):
|
||||||
|
self.bizhawk_client_context = bizhawk_client_context
|
||||||
|
self.bizhawk_context = bizhawk_client_context.bizhawk_ctx if bizhawk_client_context else None
|
||||||
|
self.chosen_side = "Left" # Store which side the Camera Rotate is currently on
|
||||||
|
self.RoomType = "Special"
|
||||||
|
self.is_active = False # True if effects are currently active
|
||||||
|
self.duration = 0 # The initial or current duration set
|
||||||
|
self.remaining_time = 0 # How much time is left for the effects
|
||||||
|
self.last_update = 0 # Timestamp of the last update, for calculating elapsed time
|
||||||
|
self.pause = False # Flag to pause the timer/effects
|
||||||
|
self.sentMessage = True # To track if the last activation sent a Bizhawk message on expiration
|
||||||
|
async def activate_camera_rotate(self, duration_seconds: int, currentRoom):
|
||||||
|
"""
|
||||||
|
Activates the Rainbow Cookie effects (invincibility and golden form).
|
||||||
|
If already active, extends the duration up to MAX_DURATION.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
duration_seconds (int): The number of seconds to activate/extend the cookie's effects.
|
||||||
|
"""
|
||||||
|
if not self.is_active:
|
||||||
|
# First activation
|
||||||
|
self.is_active = True
|
||||||
|
self.duration = duration_seconds
|
||||||
|
self.remaining_time = duration_seconds
|
||||||
|
self.last_update = time.time()
|
||||||
|
possibleSides = ["Left","Right"]
|
||||||
|
self.chosen_side = possibleSides[random.randint(0,1)]
|
||||||
|
print(f"Camera Rotate activated for {duration_seconds} seconds.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
## Extend existing duration
|
||||||
|
new_remaining_time = self.remaining_time + duration_seconds
|
||||||
|
self.remaining_time = min(new_remaining_time, self.MAX_DURATION)
|
||||||
|
self.duration = self.remaining_time # Update current duration if extended
|
||||||
|
print(f"Camera Rotate extended by {duration_seconds} seconds. Total remaining: {self.remaining_time:.2f}s (capped at {self.MAX_DURATION}s)")
|
||||||
|
|
||||||
|
await self._apply_effects(True, currentRoom) # Apply effects immediately
|
||||||
|
self.sentMessage = False
|
||||||
|
async def _apply_effects(self, enable: bool,currentRoom):
|
||||||
|
"""
|
||||||
|
Internal method to apply or remove the Rainbow Cookie's effects
|
||||||
|
by writing to BizHawk memory addresses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enable (bool): If True, enables effects; if False, disables them.
|
||||||
|
"""
|
||||||
|
if self.bizhawk_context is None or self.bizhawk_context.connection_status != bizhawk.ConnectionStatus.CONNECTED:
|
||||||
|
print("Warning: BizHawk not connected. Cannot apply/remove Rainbow Cookie effects.")
|
||||||
|
return
|
||||||
|
read_list = []
|
||||||
|
read_list += [(RAM.SpecialRoom_CameraMode, 1, "MainRAM"),(RAM.Boss_CameraMode, 1, "MainRAM"),(RAM.Inside_CameraMode, 1, "MainRAM")]
|
||||||
|
CameraMode_reads = await bizhawk.read(self.bizhawk_context, read_list)
|
||||||
|
isSpecialRoom = int.from_bytes(CameraMode_reads[0], byteorder="little") == 0x01
|
||||||
|
isBossRoom = int.from_bytes(CameraMode_reads[1], byteorder="little") == 0x01
|
||||||
|
isInside = int.from_bytes(CameraMode_reads[2], byteorder="little") == 0x01
|
||||||
|
if isSpecialRoom:
|
||||||
|
self.RoomType = "Special"
|
||||||
|
elif isBossRoom:
|
||||||
|
self.RoomType = "Boss"
|
||||||
|
elif isInside:
|
||||||
|
self.RoomType = "Inside"
|
||||||
|
else:
|
||||||
|
self.RoomType = "Outside"
|
||||||
|
writes_list = []
|
||||||
|
#SpecialRooms = [83, 84, 87, 88, 90, 91]
|
||||||
|
#BossRooms = [item for item in RAM.bossListLocal.keys() if item not in SpecialRooms]
|
||||||
|
|
||||||
|
#InABossRoom = False
|
||||||
|
|
||||||
|
CameraRotate_value = 0xFF if enable else 0x00
|
||||||
|
CameraRotate_bytes = list(CameraRotate_value.to_bytes(1, "little"))
|
||||||
|
|
||||||
|
LeftRotateAddress2 = ""
|
||||||
|
LeftRotateAddress = ""
|
||||||
|
RightRotateAddress = ""
|
||||||
|
RightRotateAddress2 = ""
|
||||||
|
|
||||||
|
if self.RoomType == "Special":
|
||||||
|
LeftRotateAddress = RAM.SpecialRoom_CameraRotateLeft
|
||||||
|
RightRotateAddress = RAM.SpecialRoom_CameraRotateRight
|
||||||
|
elif self.RoomType == "Boss":
|
||||||
|
LeftRotateAddress = RAM.Boss_CameraRotateLeft
|
||||||
|
RightRotateAddress = RAM.Boss_CameraRotateRight
|
||||||
|
else:
|
||||||
|
if self.RoomType == "Inside":
|
||||||
|
LeftRotateAddress = RAM.Inside_CameraRotateLeft
|
||||||
|
RightRotateAddress = RAM.Inside_CameraRotateLeft
|
||||||
|
else:
|
||||||
|
LeftRotateAddress = RAM.Outside_CameraRotateLeft
|
||||||
|
RightRotateAddress = RAM.Outside_CameraRotateRight
|
||||||
|
|
||||||
|
if self.chosen_side == "Left":
|
||||||
|
writes_list.append((LeftRotateAddress, CameraRotate_bytes, "MainRAM"))
|
||||||
|
else:
|
||||||
|
writes_list.append((RightRotateAddress, CameraRotate_bytes, "MainRAM"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bizhawk.write(self.bizhawk_context, writes_list)
|
||||||
|
print(f"Camera Rotate effects {'applied' if enable else 'removed'}.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to {'apply' if enable else 'remove'} Camera Rotate effects: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def update_state_and_deactivate(self,currentRoom):
|
||||||
|
"""
|
||||||
|
Updates the remaining time for the Camera Rotate.
|
||||||
|
If the duration runs out, deactivates the effects.
|
||||||
|
This method should be called periodically in the main loop of the client.
|
||||||
|
It also re-applies the camera Rotate effect if it's lost and the cookie is active.
|
||||||
|
"""
|
||||||
|
if not self.is_active:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.pause:
|
||||||
|
# If paused, don't decrement remaining_time, but update last_update
|
||||||
|
# to prevent a large time jump when unpaused.
|
||||||
|
self.last_update = time.time()
|
||||||
|
return
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
elapsed_time_since_last_update = current_time - self.last_update
|
||||||
|
self.remaining_time -= elapsed_time_since_last_update
|
||||||
|
self.last_update = current_time
|
||||||
|
|
||||||
|
# Check and re-apply Rotate effect if it's not active but the effect is
|
||||||
|
if self.bizhawk_context and self.bizhawk_context.connection_status == bizhawk.ConnectionStatus.CONNECTED:
|
||||||
|
try:
|
||||||
|
#SpecialRooms = [83, 84, 87, 88, 90, 91]
|
||||||
|
#BossRooms = [item for item in RAM.bossListLocal.keys() if item not in SpecialRooms]
|
||||||
|
#InABossRoom = False
|
||||||
|
read_list = []
|
||||||
|
read_list += [(RAM.SpecialRoom_CameraMode, 1, "MainRAM"), (RAM.Boss_CameraMode, 1, "MainRAM"),(RAM.Inside_CameraMode, 1, "MainRAM")]
|
||||||
|
CameraMode_reads = await bizhawk.read(self.bizhawk_context, read_list)
|
||||||
|
isSpecialRoom = int.from_bytes(CameraMode_reads[0], byteorder="little") == 0x01
|
||||||
|
isBossRoom = int.from_bytes(CameraMode_reads[1], byteorder="little") == 0x01
|
||||||
|
isInside = int.from_bytes(CameraMode_reads[2], byteorder="little") == 0x01
|
||||||
|
if isSpecialRoom:
|
||||||
|
self.RoomType = "Special"
|
||||||
|
elif isBossRoom:
|
||||||
|
self.RoomType = "Boss"
|
||||||
|
elif isInside:
|
||||||
|
self.RoomType = "Inside"
|
||||||
|
else:
|
||||||
|
self.RoomType = "Outside"
|
||||||
|
CameraRotate_value = 0xFF
|
||||||
|
if self.RoomType == "Special":
|
||||||
|
LeftRotateAddress = RAM.SpecialRoom_CameraRotateLeft
|
||||||
|
RightRotateAddress = RAM.SpecialRoom_CameraRotateRight
|
||||||
|
elif self.RoomType == "Boss":
|
||||||
|
LeftRotateAddress = RAM.Boss_CameraRotateLeft
|
||||||
|
RightRotateAddress = RAM.Boss_CameraRotateRight
|
||||||
|
else:
|
||||||
|
if self.RoomType == "Inside":
|
||||||
|
LeftRotateAddress = RAM.Inside_CameraRotateLeft
|
||||||
|
RightRotateAddress = RAM.Inside_CameraRotateLeft
|
||||||
|
else:
|
||||||
|
LeftRotateAddress = RAM.Outside_CameraRotateLeft
|
||||||
|
RightRotateAddress = RAM.Outside_CameraRotateRight
|
||||||
|
|
||||||
|
if self.chosen_side == "Left":
|
||||||
|
CameraRotateAddress = LeftRotateAddress
|
||||||
|
else:
|
||||||
|
CameraRotateAddress = RightRotateAddress
|
||||||
|
|
||||||
|
# Read the current value of the chosen CameraRotate address
|
||||||
|
current_camera_rotate_bytes = await bizhawk.read(self.bizhawk_context, [(CameraRotateAddress, 1, "MainRAM")])
|
||||||
|
current_camera_rotate = int.from_bytes(current_camera_rotate_bytes[0], byteorder="little")
|
||||||
|
|
||||||
|
if current_camera_rotate is not None and current_camera_rotate != 0xFF:
|
||||||
|
print("Camera Rotate active, but rotate effect lost. Reapplying...")
|
||||||
|
await self._apply_effects(True,currentRoom)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Failed to read rotate form address for reapplication: {e}")
|
||||||
|
# Log error but don't stop the loop for this non-critical re-application check
|
||||||
|
|
||||||
|
if self.remaining_time <= 0:
|
||||||
|
self.remaining_time = 0
|
||||||
|
self.is_active = False
|
||||||
|
print("Camera Rotate duration finished. Deactivating effects.")
|
||||||
|
await self._apply_effects(False,currentRoom) # Remove effects
|
||||||
178
worlds/apeescape/Items.py
Normal file
178
worlds/apeescape/Items.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import typing
|
||||||
|
from typing import Optional, Dict, Set
|
||||||
|
from BaseClasses import ItemClassification, Item
|
||||||
|
from .Strings import AEItem
|
||||||
|
from .RAMAddress import RAM
|
||||||
|
|
||||||
|
base_apeescape_item_id = 128000000
|
||||||
|
|
||||||
|
|
||||||
|
class ApeEscapeItem(Item):
|
||||||
|
game: str = "Ape Escape"
|
||||||
|
|
||||||
|
GROUPED_ITEMS: Dict[str, Set[str]] = {}
|
||||||
|
|
||||||
|
# base IDs are the index in the static item data table, which is
|
||||||
|
# not the same order as the items in RAM (but offset 0 is a 16-bit address of
|
||||||
|
# location of room and position data)
|
||||||
|
item_table = {
|
||||||
|
# Gadgets
|
||||||
|
AEItem.Club.value: RAM.items["Club"],
|
||||||
|
AEItem.Net.value: RAM.items["Net"],
|
||||||
|
AEItem.Radar.value: RAM.items["Radar"],
|
||||||
|
AEItem.Sling.value: RAM.items["Sling"],
|
||||||
|
AEItem.Hoop.value: RAM.items["Hoop"],
|
||||||
|
AEItem.Punch.value: RAM.items["Punch"],
|
||||||
|
AEItem.Flyer.value: RAM.items["Flyer"],
|
||||||
|
AEItem.Car.value: RAM.items["Car"],
|
||||||
|
AEItem.WaterNet.value: RAM.items["WaterNet"],
|
||||||
|
AEItem.ProgWaterNet.value: RAM.items["ProgWaterNet"],
|
||||||
|
AEItem.WaterCatch.value: RAM.items["WaterCatch"],
|
||||||
|
|
||||||
|
# Keys
|
||||||
|
AEItem.Key.value: RAM.items["Key"],
|
||||||
|
# No longer needed since we made it an event item
|
||||||
|
#AEItem.Victory.value: RAM.items["Victory"],
|
||||||
|
|
||||||
|
# Monkey Lamps
|
||||||
|
AEItem.CB_Lamp.value: RAM.items["CB_Lamp"],
|
||||||
|
AEItem.DI_Lamp.value: RAM.items["DI_Lamp"],
|
||||||
|
AEItem.CrC_Lamp.value: RAM.items["CrC_Lamp"],
|
||||||
|
AEItem.CP_Lamp.value: RAM.items["CP_Lamp"],
|
||||||
|
AEItem.SF_Lamp.value: RAM.items["SF_Lamp"],
|
||||||
|
AEItem.TVT_Lobby_Lamp.value: RAM.items["TVT_Lobby_Lamp"],
|
||||||
|
AEItem.TVT_Tank_Lamp.value: RAM.items["TVT_Tank_Lamp"],
|
||||||
|
AEItem.MM_Lamp.value: RAM.items["MM_Lamp"],
|
||||||
|
AEItem.MM_DoubleDoorKey.value: RAM.items["MM_DoubleDoorKey"],
|
||||||
|
|
||||||
|
# Other
|
||||||
|
AEItem.Token.value: RAM.items["Token"],
|
||||||
|
|
||||||
|
# Junk
|
||||||
|
AEItem.Nothing.value: RAM.items["Nothing"],
|
||||||
|
AEItem.Shirt.value: RAM.items["Shirt"],
|
||||||
|
AEItem.Triangle.value: RAM.items["Triangle"],
|
||||||
|
AEItem.BigTriangle.value: RAM.items["BigTriangle"],
|
||||||
|
AEItem.BiggerTriangle.value: RAM.items["BiggerTriangle"],
|
||||||
|
AEItem.Cookie.value: RAM.items["Cookie"],
|
||||||
|
AEItem.FiveCookies.value: RAM.items["FiveCookies"],
|
||||||
|
AEItem.Flash.value: RAM.items["Flash"],
|
||||||
|
AEItem.ThreeFlash.value: RAM.items["ThreeFlash"],
|
||||||
|
AEItem.Rocket.value: RAM.items["Rocket"],
|
||||||
|
AEItem.ThreeRocket.value: RAM.items["ThreeRocket"],
|
||||||
|
|
||||||
|
# Traps
|
||||||
|
AEItem.BananaPeelTrap.value: RAM.items["BananaPeelTrap"],
|
||||||
|
AEItem.GadgetShuffleTrap.value: RAM.items["GadgetShuffleTrap"],
|
||||||
|
AEItem.MonkeyMashTrap.value: RAM.items["MonkeyMashTrap"],
|
||||||
|
AEItem.IcyHotPantsTrap.value: RAM.items["IcyHotPantsTrap"],
|
||||||
|
AEItem.StunTrap.value: RAM.items["StunTrap"],
|
||||||
|
AEItem.CameraRotateTrap.value: RAM.items["CameraRotateTrap"],
|
||||||
|
|
||||||
|
# SpecialItems
|
||||||
|
AEItem.RainbowCookie.value: RAM.items["RainbowCookie"],
|
||||||
|
AEItem.FAKE_OOL_ITEM.value: RAM.items["FAKE_OOL_ITEM"],
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
gadgetsValues = {
|
||||||
|
AEItem.Club.value: 0x00,
|
||||||
|
AEItem.Net.value: 0x01,
|
||||||
|
AEItem.Radar.value: 0x02,
|
||||||
|
AEItem.Sling.value: 0x03,
|
||||||
|
AEItem.Hoop.value: 0x04,
|
||||||
|
AEItem.Punch.value: 0x05,
|
||||||
|
AEItem.Flyer.value: 0x06,
|
||||||
|
AEItem.Car.value: 0x07,
|
||||||
|
}
|
||||||
|
|
||||||
|
event_table = {
|
||||||
|
}
|
||||||
|
|
||||||
|
trap_to_local_traps: typing.Dict[str, str] = {
|
||||||
|
# Converts received traps to local trap names
|
||||||
|
# Our native Traps
|
||||||
|
AEItem.BananaPeelTrap.value: AEItem.BananaPeelTrap.value,
|
||||||
|
AEItem.GadgetShuffleTrap.value: AEItem.GadgetShuffleTrap.value,
|
||||||
|
AEItem.MonkeyMashTrap.value: AEItem.MonkeyMashTrap.value,
|
||||||
|
AEItem.IcyHotPantsTrap.value: AEItem.IcyHotPantsTrap.value,
|
||||||
|
AEItem.StunTrap.value: AEItem.StunTrap.value,
|
||||||
|
AEItem.CameraRotateTrap.value: AEItem.CameraRotateTrap.value,
|
||||||
|
|
||||||
|
# Common other trap names
|
||||||
|
"Banana Trap": AEItem.BananaPeelTrap.value,
|
||||||
|
"Chaos Control Trap": AEItem.StunTrap.value,
|
||||||
|
"Confuse Trap": AEItem.MonkeyMashTrap.value,
|
||||||
|
"Confusion Trap": AEItem.MonkeyMashTrap.value,
|
||||||
|
"Freeze Trap": AEItem.StunTrap.value,
|
||||||
|
"Frozen Trap": AEItem.StunTrap.value,
|
||||||
|
"Hiccup Trap": AEItem.IcyHotPantsTrap.value,
|
||||||
|
"Ice Floor Trap": AEItem.BananaPeelTrap.value,
|
||||||
|
"Jump Trap": AEItem.IcyHotPantsTrap.value,
|
||||||
|
"Jumping Jacks Trap": AEItem.IcyHotPantsTrap.value,
|
||||||
|
"Paralyze Trap": AEItem.StunTrap.value,
|
||||||
|
"Push Trap": AEItem.BananaPeelTrap.value,
|
||||||
|
"Screen Flip Trap": AEItem.CameraRotateTrap.value,
|
||||||
|
"Slip Trap": AEItem.BananaPeelTrap.value,
|
||||||
|
"Spring Trap": AEItem.IcyHotPantsTrap.value,
|
||||||
|
"SvC Effect": AEItem.CameraRotateTrap.value,
|
||||||
|
"Swap Trap" : AEItem.GadgetShuffleTrap.value,
|
||||||
|
|
||||||
|
# Traps idea :
|
||||||
|
# Fast Trap (Depending on direction always set to max velocity?)
|
||||||
|
# Home Trap (Time Hub Trap? or maybe only warp to level entry?)
|
||||||
|
# Ice Trap (Slipery Floor?)
|
||||||
|
# Zoom Trap
|
||||||
|
# Mailbox Trap (Tells a message to the player in a mailbox
|
||||||
|
}
|
||||||
|
|
||||||
|
trap_name_to_value: typing.Dict[str, int] = {
|
||||||
|
AEItem.BananaPeelTrap.value: RAM.items["BananaPeelTrap"],
|
||||||
|
AEItem.GadgetShuffleTrap.value: RAM.items["GadgetShuffleTrap"],
|
||||||
|
AEItem.MonkeyMashTrap.value: RAM.items["MonkeyMashTrap"],
|
||||||
|
AEItem.IcyHotPantsTrap.value: RAM.items["IcyHotPantsTrap"],
|
||||||
|
AEItem.StunTrap.value: RAM.items["StunTrap"],
|
||||||
|
AEItem.CameraRotateTrap.value: RAM.items["CameraRotateTrap"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def createItemGroups():
|
||||||
|
# Alliases for items
|
||||||
|
GROUPED_ITEMS.setdefault("Club", []).append("Stun Club")
|
||||||
|
GROUPED_ITEMS.setdefault("Net", []).append("Time Net")
|
||||||
|
GROUPED_ITEMS.setdefault("Radar", []).append("Monkey Radar")
|
||||||
|
GROUPED_ITEMS.setdefault("Slingshot", []).append("Slingback Shooter")
|
||||||
|
GROUPED_ITEMS.setdefault("Sling", []).append("Slingback Shooter")
|
||||||
|
GROUPED_ITEMS.setdefault("Hoop", []).append("Super Hoop")
|
||||||
|
GROUPED_ITEMS.setdefault("Punch", []).append("Magic Punch")
|
||||||
|
GROUPED_ITEMS.setdefault("Flyer", []).append("Sky Flyer")
|
||||||
|
GROUPED_ITEMS.setdefault("Car", []).append("R.C. Car")
|
||||||
|
|
||||||
|
# Removed because unit tests said having a group and item named the same is bad
|
||||||
|
# GROUPED_ITEMS.setdefault("Water Net", []).append("Progressive Water Net")
|
||||||
|
|
||||||
|
# Item Groups
|
||||||
|
GROUPED_ITEMS.setdefault("Gadgets", []).append("Stun Club")
|
||||||
|
GROUPED_ITEMS.setdefault("Gadgets", []).append("Time Net")
|
||||||
|
GROUPED_ITEMS.setdefault("Gadgets", []).append("Monkey Radar")
|
||||||
|
GROUPED_ITEMS.setdefault("Gadgets", []).append("Slingback Shooter")
|
||||||
|
GROUPED_ITEMS.setdefault("Gadgets", []).append("Super Hoop")
|
||||||
|
GROUPED_ITEMS.setdefault("Gadgets", []).append("Magic Punch")
|
||||||
|
GROUPED_ITEMS.setdefault("Gadgets", []).append("Sky Flyer")
|
||||||
|
GROUPED_ITEMS.setdefault("Gadgets", []).append("R.C. Car")
|
||||||
|
GROUPED_ITEMS.setdefault("Gadgets", []).append("Water Net")
|
||||||
|
GROUPED_ITEMS.setdefault("Gadgets", []).append("Progressive Water Net")
|
||||||
|
GROUPED_ITEMS.setdefault("Gadgets", []).append("Water Catch")
|
||||||
|
|
||||||
|
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.CB_Lamp.value)
|
||||||
|
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.DI_Lamp.value)
|
||||||
|
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.CrC_Lamp.value)
|
||||||
|
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.CP_Lamp.value)
|
||||||
|
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.SF_Lamp.value)
|
||||||
|
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.TVT_Lobby_Lamp.value)
|
||||||
|
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.TVT_Tank_Lamp.value)
|
||||||
|
GROUPED_ITEMS.setdefault("Lamps", []).append(AEItem.MM_Lamp.value)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
createItemGroups()
|
||||||
758
worlds/apeescape/Locations.py
Normal file
758
worlds/apeescape/Locations.py
Normal file
@@ -0,0 +1,758 @@
|
|||||||
|
from typing import Optional, Dict, Set
|
||||||
|
from BaseClasses import Location
|
||||||
|
from worlds.apeescape.Strings import AELocation, AEDoor
|
||||||
|
|
||||||
|
base_location_id = 128000000
|
||||||
|
|
||||||
|
|
||||||
|
class ApeEscapeLocation(Location):
|
||||||
|
game: str = "Ape Escape"
|
||||||
|
|
||||||
|
GROUPED_LOCATIONS: Dict[str, Set[str]] = {}
|
||||||
|
|
||||||
|
location_table = {
|
||||||
|
# 1-1 Fossil Field
|
||||||
|
AELocation.W1L1Noonan.value: 1,
|
||||||
|
AELocation.W1L1Jorjy.value: 2,
|
||||||
|
AELocation.W1L1Nati.value: 3,
|
||||||
|
AELocation.W1L1TrayC.value: 4,
|
||||||
|
# 1-2 Primordial Ooze
|
||||||
|
AELocation.W1L2Shay.value: 5,
|
||||||
|
AELocation.W1L2DrMonk.value: 6,
|
||||||
|
AELocation.W1L2Grunt.value: 7,
|
||||||
|
AELocation.W1L2Ahchoo.value: 8,
|
||||||
|
AELocation.W1L2Gornif.value: 9,
|
||||||
|
AELocation.W1L2Tyrone.value: 10,
|
||||||
|
# 1-3 Molten Lava
|
||||||
|
AELocation.W1L3Scotty.value: 11,
|
||||||
|
AELocation.W1L3Coco.value: 12,
|
||||||
|
AELocation.W1L3JThomas.value: 13,
|
||||||
|
AELocation.W1L3Mattie.value: 14,
|
||||||
|
AELocation.W1L3Barney.value: 15,
|
||||||
|
AELocation.W1L3Rocky.value: 16,
|
||||||
|
AELocation.W1L3Moggan.value: 17,
|
||||||
|
# 2-1 Thick Jungle
|
||||||
|
AELocation.W2L1Marquez.value: 18,
|
||||||
|
AELocation.W2L1Livinston.value: 19,
|
||||||
|
AELocation.W2L1George.value: 20,
|
||||||
|
AELocation.W2L1Maki.value: 21,
|
||||||
|
AELocation.W2L1Herb.value: 22,
|
||||||
|
AELocation.W2L1Dilweed.value: 23,
|
||||||
|
AELocation.W2L1Mitong.value: 24,
|
||||||
|
AELocation.W2L1Stoddy.value: 25,
|
||||||
|
AELocation.W2L1Nasus.value: 26,
|
||||||
|
AELocation.W2L1Selur.value: 27,
|
||||||
|
AELocation.W2L1Elehcim.value: 28,
|
||||||
|
AELocation.W2L1Gonzo.value: 29,
|
||||||
|
AELocation.W2L1Alphonse.value: 30,
|
||||||
|
AELocation.W2L1Zanzibar.value: 31,
|
||||||
|
# 2-2 Dark Ruins
|
||||||
|
AELocation.W2L2Mooshy.value: 32,
|
||||||
|
AELocation.W2L2Kyle.value: 33,
|
||||||
|
AELocation.W2L2Cratman.value: 34,
|
||||||
|
AELocation.W2L2Nuzzy.value: 35,
|
||||||
|
AELocation.W2L2Mav.value: 36,
|
||||||
|
AELocation.W2L2Stan.value: 37,
|
||||||
|
AELocation.W2L2Bernt.value: 38,
|
||||||
|
AELocation.W2L2Runt.value: 39,
|
||||||
|
AELocation.W2L2Hoolah.value: 40,
|
||||||
|
AELocation.W2L2Papou.value: 41,
|
||||||
|
AELocation.W2L2Kenny.value: 42,
|
||||||
|
AELocation.W2L2Trance.value: 43,
|
||||||
|
AELocation.W2L2Chino.value: 44,
|
||||||
|
# 2-3 Cryptic Relics
|
||||||
|
AELocation.W2L3Troopa.value: 45,
|
||||||
|
AELocation.W2L3Spanky.value: 46,
|
||||||
|
AELocation.W2L3Stymie.value: 47,
|
||||||
|
AELocation.W2L3Pally.value: 48,
|
||||||
|
AELocation.W2L3Freeto.value: 49,
|
||||||
|
AELocation.W2L3Jesta.value: 50,
|
||||||
|
AELocation.W2L3Bazzle.value: 51,
|
||||||
|
AELocation.W2L3Crash.value: 52,
|
||||||
|
# 4-1 Crabby Beach
|
||||||
|
AELocation.W4L1CoolBlue.value: 53,
|
||||||
|
AELocation.W4L1Sandy.value: 54,
|
||||||
|
AELocation.W4L1ShellE.value: 55,
|
||||||
|
AELocation.W4L1Gidget.value: 56,
|
||||||
|
AELocation.W4L1Shaka.value: 57,
|
||||||
|
AELocation.W4L1MaxMahalo.value: 58,
|
||||||
|
AELocation.W4L1Moko.value: 59,
|
||||||
|
AELocation.W4L1Puka.value: 60,
|
||||||
|
# 4-2 Coral Cave
|
||||||
|
AELocation.W4L2Chip.value: 61,
|
||||||
|
AELocation.W4L2Oreo.value: 62,
|
||||||
|
AELocation.W4L2Puddles.value: 63,
|
||||||
|
AELocation.W4L2Kalama.value: 64,
|
||||||
|
AELocation.W4L2Iz.value: 65,
|
||||||
|
AELocation.W4L2Jux.value: 66,
|
||||||
|
AELocation.W4L2BongBong.value: 67,
|
||||||
|
AELocation.W4L2Pickles.value: 68,
|
||||||
|
# 4-3 Dexter's Island
|
||||||
|
AELocation.W4L3Stuw.value: 69,
|
||||||
|
AELocation.W4L3TonTon.value: 70,
|
||||||
|
AELocation.W4L3Murky.value: 71,
|
||||||
|
AELocation.W4L3Howeerd.value: 72,
|
||||||
|
AELocation.W4L3Robbin.value: 73,
|
||||||
|
AELocation.W4L3Jakkee.value: 74,
|
||||||
|
AELocation.W4L3Frederic.value: 75,
|
||||||
|
AELocation.W4L3Baba.value: 76,
|
||||||
|
AELocation.W4L3Mars.value: 77,
|
||||||
|
AELocation.W4L3Horke.value: 78,
|
||||||
|
AELocation.W4L3Quirck.value: 79,
|
||||||
|
# 5-1 Snowy Mammoth
|
||||||
|
AELocation.W5L1Popcicle.value: 80,
|
||||||
|
AELocation.W5L1Iced.value: 81,
|
||||||
|
AELocation.W5L1Denggoy.value: 82,
|
||||||
|
AELocation.W5L1Skeens.value: 83,
|
||||||
|
AELocation.W5L1Rickets.value: 84,
|
||||||
|
AELocation.W5L1Chilly.value: 85,
|
||||||
|
# 5-2 Frosty Retreat
|
||||||
|
AELocation.W5L2Storm.value: 86,
|
||||||
|
AELocation.W5L2Qube.value: 87,
|
||||||
|
AELocation.W5L2Gash.value: 88,
|
||||||
|
AELocation.W5L2Kundra.value: 89,
|
||||||
|
AELocation.W5L2Shadow.value: 90,
|
||||||
|
AELocation.W5L2Ranix.value: 91,
|
||||||
|
AELocation.W5L2Sticky.value: 92,
|
||||||
|
AELocation.W5L2Sharpe.value: 93,
|
||||||
|
AELocation.W5L2Droog.value: 94,
|
||||||
|
# 5-3 Hot Springs
|
||||||
|
AELocation.W5L3Punky.value: 95,
|
||||||
|
AELocation.W5L3Ameego.value: 96,
|
||||||
|
AELocation.W5L3Roti.value: 97,
|
||||||
|
AELocation.W5L3Dissa.value: 98,
|
||||||
|
AELocation.W5L3Yoky.value: 99,
|
||||||
|
AELocation.W5L3Jory.value: 100,
|
||||||
|
AELocation.W5L3Crank.value: 101,
|
||||||
|
AELocation.W5L3Claxter.value: 102,
|
||||||
|
AELocation.W5L3Looza.value: 103,
|
||||||
|
# 7-1 Sushi Temple
|
||||||
|
AELocation.W7L1Taku.value: 104,
|
||||||
|
AELocation.W7L1Rocka.value: 105,
|
||||||
|
AELocation.W7L1Maralea.value: 106,
|
||||||
|
AELocation.W7L1Wog.value: 107,
|
||||||
|
AELocation.W7L1Long.value: 108,
|
||||||
|
AELocation.W7L1Mayi.value: 109,
|
||||||
|
AELocation.W7L1Owyang.value: 110,
|
||||||
|
AELocation.W7L1QuelTin.value: 111,
|
||||||
|
AELocation.W7L1Phaldo.value: 112,
|
||||||
|
AELocation.W7L1Voti.value: 113,
|
||||||
|
AELocation.W7L1Elly.value: 114,
|
||||||
|
AELocation.W7L1Chunky.value: 115,
|
||||||
|
# 7-2 Wabi Sabi Wall
|
||||||
|
AELocation.W7L2Minky.value: 116,
|
||||||
|
AELocation.W7L2Zobbro.value: 117,
|
||||||
|
AELocation.W7L2Xeeto.value: 118,
|
||||||
|
AELocation.W7L2Moops.value: 119,
|
||||||
|
AELocation.W7L2Zanabi.value: 120,
|
||||||
|
AELocation.W7L2Buddha.value: 121,
|
||||||
|
AELocation.W7L2Fooey.value: 122,
|
||||||
|
AELocation.W7L2Doxs.value: 123,
|
||||||
|
AELocation.W7L2Kong.value: 124,
|
||||||
|
AELocation.W7L2Phool.value: 125,
|
||||||
|
# 7-3 Crumbling Castle
|
||||||
|
AELocation.W7L3Naners.value: 126,
|
||||||
|
AELocation.W7L3Robart.value: 127,
|
||||||
|
AELocation.W7L3Neeners.value: 128,
|
||||||
|
AELocation.W7L3Gustav.value: 129,
|
||||||
|
AELocation.W7L3Wilhelm.value: 130,
|
||||||
|
AELocation.W7L3Emmanuel.value: 131,
|
||||||
|
AELocation.W7L3SirCutty.value: 132,
|
||||||
|
AELocation.W7L3Calligan.value: 133,
|
||||||
|
AELocation.W7L3Castalist.value: 134,
|
||||||
|
AELocation.W7L3Deveneom.value: 135,
|
||||||
|
AELocation.W7L3Igor.value: 136,
|
||||||
|
AELocation.W7L3Charles.value: 137,
|
||||||
|
AELocation.W7L3Astur.value: 138,
|
||||||
|
AELocation.W7L3Kilserack.value: 139,
|
||||||
|
AELocation.W7L3Ringo.value: 140,
|
||||||
|
AELocation.W7L3Densil.value: 141,
|
||||||
|
AELocation.W7L3Figero.value: 142,
|
||||||
|
AELocation.W7L3Fej.value: 143,
|
||||||
|
AELocation.W7L3Joey.value: 144,
|
||||||
|
AELocation.W7L3Donqui.value: 145,
|
||||||
|
# 8-1 City Park
|
||||||
|
AELocation.W8L1Kaine.value: 146,
|
||||||
|
AELocation.W8L1Jaxx.value: 147,
|
||||||
|
AELocation.W8L1Gehry.value: 148,
|
||||||
|
AELocation.W8L1Alcatraz.value: 149,
|
||||||
|
AELocation.W8L1Tino.value: 150,
|
||||||
|
AELocation.W8L1QBee.value: 151,
|
||||||
|
AELocation.W8L1McManic.value: 152,
|
||||||
|
AELocation.W8L1Dywan.value: 153,
|
||||||
|
AELocation.W8L1CKHutch.value: 154,
|
||||||
|
AELocation.W8L1Winky.value: 155,
|
||||||
|
AELocation.W8L1BLuv.value: 156,
|
||||||
|
AELocation.W8L1Camper.value: 157,
|
||||||
|
AELocation.W8L1Huener.value: 158,
|
||||||
|
# 8-2 Specter's Factory
|
||||||
|
AELocation.W8L2BigShow.value: 159,
|
||||||
|
AELocation.W8L2Dreos.value: 160,
|
||||||
|
AELocation.W8L2Reznor.value: 161,
|
||||||
|
AELocation.W8L2Urkel.value: 162,
|
||||||
|
AELocation.W8L2VanillaS.value: 163,
|
||||||
|
AELocation.W8L2Radd.value: 164,
|
||||||
|
AELocation.W8L2Shimbo.value: 165,
|
||||||
|
AELocation.W8L2Hurt.value: 166,
|
||||||
|
AELocation.W8L2String.value: 167,
|
||||||
|
AELocation.W8L2Khamo.value: 168,
|
||||||
|
# 8-3 TV Tower
|
||||||
|
AELocation.W8L3Fredo.value: 169,
|
||||||
|
AELocation.W8L3Charlee.value: 170,
|
||||||
|
AELocation.W8L3Mach3.value: 171,
|
||||||
|
AELocation.W8L3Tortuss.value: 172,
|
||||||
|
AELocation.W8L3Manic.value: 173,
|
||||||
|
AELocation.W8L3Ruptdis.value: 174,
|
||||||
|
AELocation.W8L3Eighty7.value: 175,
|
||||||
|
AELocation.W8L3Danio.value: 176,
|
||||||
|
AELocation.W8L3Roosta.value: 177,
|
||||||
|
AELocation.W8L3Tellis.value: 178,
|
||||||
|
AELocation.W8L3Whack.value: 179,
|
||||||
|
AELocation.W8L3Frostee.value: 180,
|
||||||
|
# 9-1 Monkey Madness
|
||||||
|
AELocation.W9L1Goopo.value: 181,
|
||||||
|
AELocation.W9L1Porto.value: 182,
|
||||||
|
AELocation.W9L1Slam.value: 183,
|
||||||
|
AELocation.W9L1Junk.value: 184,
|
||||||
|
AELocation.W9L1Crib.value: 185,
|
||||||
|
AELocation.W9L1Nak.value: 186,
|
||||||
|
AELocation.W9L1Cloy.value: 187,
|
||||||
|
AELocation.W9L1Shaw.value: 188,
|
||||||
|
AELocation.W9L1Flea.value: 189,
|
||||||
|
AELocation.W9L1Schafette.value: 190,
|
||||||
|
AELocation.W9L1Donovan.value: 191,
|
||||||
|
AELocation.W9L1Laura.value: 192,
|
||||||
|
AELocation.W9L1Uribe.value: 193,
|
||||||
|
AELocation.W9L1Gordo.value: 194,
|
||||||
|
AELocation.W9L1Raeski.value: 195,
|
||||||
|
AELocation.W9L1Poopie.value: 196,
|
||||||
|
AELocation.W9L1Teacup.value: 197,
|
||||||
|
AELocation.W9L1Shine.value: 198,
|
||||||
|
AELocation.W9L1Wrench.value: 199,
|
||||||
|
AELocation.W9L1Bronson.value: 200,
|
||||||
|
AELocation.W9L1Bungee.value: 201,
|
||||||
|
AELocation.W9L1Carro.value: 202,
|
||||||
|
AELocation.W9L1Carlito.value: 203,
|
||||||
|
AELocation.W9L1BG.value: 204,
|
||||||
|
AELocation.Specter.value: 205,
|
||||||
|
# 9-2 Peak Point Matrix
|
||||||
|
AELocation.Specter2.value: 206,
|
||||||
|
|
||||||
|
# Coins
|
||||||
|
AELocation.Coin1.value: 301,
|
||||||
|
AELocation.Coin2.value: 302,
|
||||||
|
AELocation.Coin3.value: 303,
|
||||||
|
AELocation.Coin6.value: 306,
|
||||||
|
AELocation.Coin7.value: 307,
|
||||||
|
AELocation.Coin8.value: 308,
|
||||||
|
AELocation.Coin9.value: 309,
|
||||||
|
AELocation.Coin11.value: 311,
|
||||||
|
AELocation.Coin12.value: 312,
|
||||||
|
AELocation.Coin13.value: 313,
|
||||||
|
AELocation.Coin14.value: 314,
|
||||||
|
AELocation.Coin17.value: 317,
|
||||||
|
AELocation.Coin19A.value: 295,
|
||||||
|
AELocation.Coin19B.value: 296,
|
||||||
|
AELocation.Coin19C.value: 297,
|
||||||
|
AELocation.Coin19D.value: 298,
|
||||||
|
AELocation.Coin19E.value: 299,
|
||||||
|
AELocation.Coin21.value: 321,
|
||||||
|
AELocation.Coin23.value: 323,
|
||||||
|
AELocation.Coin24.value: 324,
|
||||||
|
AELocation.Coin25.value: 325,
|
||||||
|
AELocation.Coin28.value: 328,
|
||||||
|
AELocation.Coin29.value: 329,
|
||||||
|
AELocation.Coin30.value: 330,
|
||||||
|
AELocation.Coin31.value: 331,
|
||||||
|
AELocation.Coin32.value: 332,
|
||||||
|
AELocation.Coin34.value: 334,
|
||||||
|
AELocation.Coin35.value: 335,
|
||||||
|
AELocation.Coin36A.value: 290,
|
||||||
|
AELocation.Coin36B.value: 291,
|
||||||
|
AELocation.Coin36C.value: 292,
|
||||||
|
AELocation.Coin36D.value: 293,
|
||||||
|
AELocation.Coin36E.value: 294,
|
||||||
|
AELocation.Coin37.value: 337,
|
||||||
|
AELocation.Coin38.value: 338,
|
||||||
|
AELocation.Coin39.value: 339,
|
||||||
|
AELocation.Coin40.value: 340,
|
||||||
|
AELocation.Coin41.value: 341,
|
||||||
|
AELocation.Coin44.value: 344,
|
||||||
|
AELocation.Coin45.value: 345,
|
||||||
|
AELocation.Coin46.value: 346,
|
||||||
|
AELocation.Coin49.value: 349,
|
||||||
|
AELocation.Coin50.value: 350,
|
||||||
|
AELocation.Coin53.value: 353,
|
||||||
|
AELocation.Coin54.value: 354,
|
||||||
|
AELocation.Coin55.value: 355,
|
||||||
|
AELocation.Coin58.value: 358,
|
||||||
|
AELocation.Coin59.value: 359,
|
||||||
|
AELocation.Coin64.value: 364,
|
||||||
|
AELocation.Coin66.value: 366,
|
||||||
|
AELocation.Coin73.value: 373,
|
||||||
|
AELocation.Coin74.value: 374,
|
||||||
|
AELocation.Coin75.value: 375,
|
||||||
|
AELocation.Coin77.value: 377,
|
||||||
|
AELocation.Coin78.value: 378,
|
||||||
|
AELocation.Coin79.value: 379,
|
||||||
|
AELocation.Coin80.value: 380,
|
||||||
|
AELocation.Coin85.value: 385,
|
||||||
|
AELocation.Coin84.value: 384,
|
||||||
|
AELocation.Coin82.value: 382,
|
||||||
|
|
||||||
|
# Mailboxes
|
||||||
|
AELocation.Mailbox1.value: 401,
|
||||||
|
AELocation.Mailbox2.value: 402,
|
||||||
|
AELocation.Mailbox3.value: 403,
|
||||||
|
AELocation.Mailbox4.value: 404,
|
||||||
|
AELocation.Mailbox5.value: 405,
|
||||||
|
AELocation.Mailbox6.value: 406,
|
||||||
|
AELocation.Mailbox7.value: 407,
|
||||||
|
AELocation.Mailbox8.value: 408,
|
||||||
|
AELocation.Mailbox9.value: 409,
|
||||||
|
AELocation.Mailbox10.value: 410,
|
||||||
|
AELocation.Mailbox11.value: 411,
|
||||||
|
AELocation.Mailbox12.value: 412,
|
||||||
|
AELocation.Mailbox13.value: 413,
|
||||||
|
AELocation.Mailbox14.value: 414,
|
||||||
|
AELocation.Mailbox15.value: 415,
|
||||||
|
AELocation.Mailbox16.value: 416,
|
||||||
|
AELocation.Mailbox17.value: 417,
|
||||||
|
AELocation.Mailbox18.value: 418,
|
||||||
|
AELocation.Mailbox19.value: 419,
|
||||||
|
AELocation.Mailbox20.value: 420,
|
||||||
|
AELocation.Mailbox21.value: 421,
|
||||||
|
AELocation.Mailbox22.value: 422,
|
||||||
|
AELocation.Mailbox23.value: 423,
|
||||||
|
AELocation.Mailbox24.value: 424,
|
||||||
|
AELocation.Mailbox25.value: 425,
|
||||||
|
AELocation.Mailbox26.value: 426,
|
||||||
|
AELocation.Mailbox27.value: 427,
|
||||||
|
AELocation.Mailbox28.value: 428,
|
||||||
|
AELocation.Mailbox29.value: 429,
|
||||||
|
AELocation.Mailbox30.value: 430,
|
||||||
|
AELocation.Mailbox31.value: 431,
|
||||||
|
AELocation.Mailbox32.value: 432,
|
||||||
|
AELocation.Mailbox33.value: 433,
|
||||||
|
AELocation.Mailbox34.value: 434,
|
||||||
|
AELocation.Mailbox35.value: 435,
|
||||||
|
AELocation.Mailbox36.value: 436,
|
||||||
|
AELocation.Mailbox37.value: 437,
|
||||||
|
AELocation.Mailbox38.value: 438,
|
||||||
|
AELocation.Mailbox39.value: 439,
|
||||||
|
AELocation.Mailbox40.value: 440,
|
||||||
|
AELocation.Mailbox41.value: 441,
|
||||||
|
AELocation.Mailbox42.value: 442,
|
||||||
|
AELocation.Mailbox43.value: 443,
|
||||||
|
AELocation.Mailbox44.value: 444,
|
||||||
|
AELocation.Mailbox45.value: 445,
|
||||||
|
AELocation.Mailbox46.value: 446,
|
||||||
|
AELocation.Mailbox47.value: 447,
|
||||||
|
AELocation.Mailbox48.value: 448,
|
||||||
|
AELocation.Mailbox49.value: 449,
|
||||||
|
AELocation.Mailbox50.value: 450,
|
||||||
|
AELocation.Mailbox51.value: 451,
|
||||||
|
AELocation.Mailbox52.value: 452,
|
||||||
|
AELocation.Mailbox53.value: 453,
|
||||||
|
AELocation.Mailbox54.value: 454,
|
||||||
|
AELocation.Mailbox55.value: 455,
|
||||||
|
AELocation.Mailbox56.value: 456,
|
||||||
|
AELocation.Mailbox57.value: 457,
|
||||||
|
AELocation.Mailbox58.value: 458,
|
||||||
|
AELocation.Mailbox59.value: 459,
|
||||||
|
AELocation.Mailbox60.value: 460,
|
||||||
|
AELocation.Mailbox61.value: 461,
|
||||||
|
AELocation.Mailbox62.value: 462,
|
||||||
|
AELocation.Mailbox63.value: 463,
|
||||||
|
|
||||||
|
# Bosses
|
||||||
|
AELocation.Boss73.value: 500,
|
||||||
|
AELocation.Boss83.value: 501,
|
||||||
|
AELocation.W9L1Professor.value: 502,
|
||||||
|
AELocation.W9L1Jake.value: 503
|
||||||
|
}
|
||||||
|
|
||||||
|
#Where RAM.levels[address] : Total monkeys count
|
||||||
|
hundoMonkeysCount = {
|
||||||
|
0x01: 4, # Fossil
|
||||||
|
0x02: 6, # Primordial
|
||||||
|
0x03: 7, # Molten
|
||||||
|
0x04: 14, # Thick
|
||||||
|
0x05: 13, # Dark
|
||||||
|
0x06: 8, # Cryptic
|
||||||
|
0x07: 0, # Stadium
|
||||||
|
0x08: 8, # Crabby
|
||||||
|
0x09: 8, # Coral
|
||||||
|
0x0A: 11, # Dexter
|
||||||
|
0x0B: 6, # Snowy
|
||||||
|
0x0C: 9, # Frosty
|
||||||
|
0x0D: 9, # Hot
|
||||||
|
0x0E: 0, # Gladiator
|
||||||
|
0x0F: 12, # Sushi
|
||||||
|
0x10: 10, # Wabi
|
||||||
|
0x11: 20, # Crumbling
|
||||||
|
0x14: 13, # City
|
||||||
|
0x15: 10, # Factory
|
||||||
|
0x16: 12, # TV
|
||||||
|
0x18: 24 # Specter
|
||||||
|
#0x1E: 0 # Specter2
|
||||||
|
}
|
||||||
|
hundoCoinsCount = {
|
||||||
|
0x01: 1, # Fossil
|
||||||
|
0x02: 1, # Primordial
|
||||||
|
0x03: 1, # Molten
|
||||||
|
0x04: 4, # Thick
|
||||||
|
0x05: 4, # Dark
|
||||||
|
0x06: 1, # Cryptic
|
||||||
|
0x07: 5, # Stadium
|
||||||
|
0x08: 1, # Crabby
|
||||||
|
0x09: 1, # Coral
|
||||||
|
0x0A: 3, # Dexter
|
||||||
|
0x0B: 1, # Snowy
|
||||||
|
0x0C: 3, # Frosty
|
||||||
|
0x0D: 2, # Hot
|
||||||
|
0x0E: 5, # Gladiator
|
||||||
|
0x0F: 3, # Sushi
|
||||||
|
0x10: 3, # Wabi
|
||||||
|
0x11: 4, # Crumbling
|
||||||
|
0x14: 3, # City
|
||||||
|
0x15: 2, # Factory
|
||||||
|
0x16: 2, # TV
|
||||||
|
0x18: 10 # Specter
|
||||||
|
#0x1E: 0 # Specter2
|
||||||
|
}
|
||||||
|
cointable = {
|
||||||
|
301,
|
||||||
|
302,
|
||||||
|
303,
|
||||||
|
306,
|
||||||
|
307,
|
||||||
|
308,
|
||||||
|
309,
|
||||||
|
311,
|
||||||
|
312,
|
||||||
|
313,
|
||||||
|
314,
|
||||||
|
317,
|
||||||
|
295,
|
||||||
|
296,
|
||||||
|
297,
|
||||||
|
298,
|
||||||
|
299,
|
||||||
|
321,
|
||||||
|
323,
|
||||||
|
324,
|
||||||
|
325,
|
||||||
|
328,
|
||||||
|
329,
|
||||||
|
330,
|
||||||
|
331,
|
||||||
|
332,
|
||||||
|
334,
|
||||||
|
335,
|
||||||
|
290,
|
||||||
|
291,
|
||||||
|
292,
|
||||||
|
293,
|
||||||
|
294,
|
||||||
|
337,
|
||||||
|
338,
|
||||||
|
339,
|
||||||
|
340,
|
||||||
|
341,
|
||||||
|
344,
|
||||||
|
345,
|
||||||
|
346,
|
||||||
|
349,
|
||||||
|
350,
|
||||||
|
353,
|
||||||
|
354,
|
||||||
|
355,
|
||||||
|
358,
|
||||||
|
359,
|
||||||
|
364,
|
||||||
|
366,
|
||||||
|
373,
|
||||||
|
374,
|
||||||
|
375,
|
||||||
|
377,
|
||||||
|
378,
|
||||||
|
379,
|
||||||
|
380,
|
||||||
|
385,
|
||||||
|
384,
|
||||||
|
382,
|
||||||
|
374,
|
||||||
|
375,
|
||||||
|
377,
|
||||||
|
378,
|
||||||
|
379,
|
||||||
|
380,
|
||||||
|
385,
|
||||||
|
384,
|
||||||
|
382,
|
||||||
|
}
|
||||||
|
# These values are the room ID of the room, and the door ID the room is entered through. To get these, stand near the transition that leads to that entrance. As an example, {45, 5} will spawn Spike at the top of the Bell Tower, as though he entered from outside.
|
||||||
|
# Array order: {TargetRoom, TargetDoor, Transition ID}
|
||||||
|
# -- TargetRoom : (Exit) If you want to warp to this Room, set the Entering door's TargetRoomAddress value to this
|
||||||
|
# -- TargetDoor : (Exit) If you want to warp to this Room, set the Entering door's TargetDoorAddress value to this
|
||||||
|
# -- Transition ID : (Enter) Transition ID to use with RAM.transitionAddresses.You should modify the addresses referred by this ID to what you want the target room to be
|
||||||
|
# Transition ID should be 0 if this is a transition that has no door related to it (Like a level spawn or inaccessible transition)
|
||||||
|
doorTransitions = {
|
||||||
|
AEDoor.FF_ENTRY.value: [1, 0, 0],
|
||||||
|
AEDoor.PO_ENTRY.value: [2, 0, 0],
|
||||||
|
AEDoor.ML_ENTRY.value: [3, 0, 0],
|
||||||
|
AEDoor.ML_ENTRY_VOLCANO.value: [3, 2, 1],
|
||||||
|
AEDoor.ML_ENTRY_TRICERATOPS.value: [3, 3,2],
|
||||||
|
AEDoor.ML_VOLCANO_ENTRY.value: [4, 0, 1],
|
||||||
|
AEDoor.ML_TRICERATOPS_ENTRY.value: [5, 0, 1],
|
||||||
|
AEDoor.TJ_ENTRY.value: [6, 0, 0],
|
||||||
|
AEDoor.TJ_ENTRY_MUSHROOM.value: [6, 2, 1],
|
||||||
|
AEDoor.TJ_ENTRY_FISH.value: [6, 3, 2],
|
||||||
|
AEDoor.TJ_ENTRY_BOULDER.value: [6, 4, 3],
|
||||||
|
AEDoor.TJ_MUSHROOM_ENTRY.value: [7, 0, 1],
|
||||||
|
AEDoor.TJ_FISH_ENTRY.value: [8, 0, 1],
|
||||||
|
AEDoor.TJ_FISH_TENT.value: [8, 2, 2],
|
||||||
|
AEDoor.TJ_TENT_FISH.value: [9, 0, 1],
|
||||||
|
AEDoor.TJ_TENT_BOULDER.value: [9, 2, 2],
|
||||||
|
AEDoor.TJ_BOULDER_ENTRY.value: [10, 2, 2],
|
||||||
|
AEDoor.TJ_BOULDER_TENT.value: [10, 0, 1],
|
||||||
|
AEDoor.DR_ENTRY.value: [11, 0, 0],
|
||||||
|
AEDoor.DR_OUTSIDE_FENCE.value: [11, 2, 1],
|
||||||
|
AEDoor.DR_OUTSIDE_HOLE.value: [11, 3, 2],
|
||||||
|
AEDoor.DR_OUTSIDE_OBELISK_BOTTOM.value: [11, 4, 3],
|
||||||
|
AEDoor.DR_OUTSIDE_OBELISK_TOP.value: [11, 5, 4],
|
||||||
|
AEDoor.DR_OUTSIDE_WATER_BUTTON.value: [11, 6, 5],
|
||||||
|
AEDoor.DR_OUTSIDE_WATER_LEDGE.value: [11, 7, 6],
|
||||||
|
AEDoor.DR_FAN_OUTSIDE_FENCE.value: [12, 2, 2],
|
||||||
|
AEDoor.DR_FAN_OUTSIDE_HOLE.value: [12, 0, 1],
|
||||||
|
AEDoor.DR_OBELISK_BOTTOM.value: [13, 0, 1],
|
||||||
|
AEDoor.DR_OBELISK_TOP.value: [13, 2, 2],
|
||||||
|
AEDoor.DR_WATER_SIDE.value: [14, 0, 1],
|
||||||
|
AEDoor.DR_WATER_LEDGE.value: [14, 2, 2],
|
||||||
|
AEDoor.CR_ENTRY.value: [15, 0, 0],
|
||||||
|
AEDoor.CR_ENTRY_SIDE_ROOM.value: [15, 2, 1],
|
||||||
|
AEDoor.CR_ENTRY_MAIN_RUINS.value: [15, 3, 2],
|
||||||
|
AEDoor.CR_SIDE_ROOM_ENTRY.value: [16, 0, 1],
|
||||||
|
AEDoor.CR_MAIN_RUINS_ENTRY.value: [17, 0, 1],
|
||||||
|
AEDoor.CR_MAIN_RUINS_PILLAR_ROOM.value: [17, 2, 2],
|
||||||
|
AEDoor.CR_PILLAR_ROOM_MAIN_RUINS.value: [18, 0, 1],
|
||||||
|
AEDoor.SA_ENTRY.value: [19, 0, 0],
|
||||||
|
AEDoor.CB_ENTRY.value: [20, 0, 0],
|
||||||
|
AEDoor.CB_ENTRY_SECOND_ROOM.value: [20, 2, 1],
|
||||||
|
AEDoor.CB_SECOND_ROOM_ENTRY.value: [21, 0, 1],
|
||||||
|
AEDoor.CCAVE_ENTRY.value: [22, 0, 0],
|
||||||
|
AEDoor.CCAVE_ENTRY_SECOND_ROOM.value: [22, 2, 1],
|
||||||
|
AEDoor.CCAVE_SECOND_ROOM_ENTRY.value: [23, 0, 1],
|
||||||
|
AEDoor.DI_ENTRY.value: [24, 0, 0],
|
||||||
|
AEDoor.DI_ENTRY_STOMACH.value: [24, 2, 1],
|
||||||
|
AEDoor.DI_STOMACH_ENTRY.value: [25, 0, 1],
|
||||||
|
AEDoor.DI_STOMACH_SLIDE_ROOM.value: [25, 2, 2],
|
||||||
|
AEDoor.DI_GALLERY_SLIDE_ELEVATOR.value: [26, 0, 1],
|
||||||
|
AEDoor.DI_GALLERY_TENTACLE.value: [26, 2, 2],
|
||||||
|
AEDoor.DI_GALLERY_SLIDE_ROOM_TOP.value: [26, 3, 3],
|
||||||
|
AEDoor.DI_TENTACLE.value: [27, 0, 1],
|
||||||
|
AEDoor.DI_SLIDE_ROOM_STOMACH.value: [28, 0, 1],
|
||||||
|
AEDoor.DI_SLIDE_ROOM_GALLERY.value: [28, 2, 2],
|
||||||
|
AEDoor.DI_SLIDE_ROOM_GALLERY_WATER.value: [28, 3, 3],
|
||||||
|
AEDoor.SM_ENTRY.value: [29, 0, 0],
|
||||||
|
AEDoor.FR_ENTRY.value: [30, 0, 0],
|
||||||
|
AEDoor.FR_ENTRY_CAVERNS.value: [30, 2, 1],
|
||||||
|
AEDoor.FR_WATER_CAVERNS.value: [31, 0, 1],
|
||||||
|
AEDoor.FR_CAVERNS_ENTRY.value: [32, 0, 1],
|
||||||
|
AEDoor.FR_CAVERNS_WATER.value: [32, 2, 2],
|
||||||
|
AEDoor.HS_ENTRY.value: [33, 0, 0],
|
||||||
|
AEDoor.HS_ENTRY_HOT_SPRING.value: [33, 2, 1],
|
||||||
|
AEDoor.HS_ENTRY_POLAR_BEAR_CAVE.value: [33, 3, 2],
|
||||||
|
AEDoor.HS_HOT_SPRING.value: [34, 0, 1],
|
||||||
|
AEDoor.HS_POLAR_BEAR_CAVE.value: [35, 0, 1],
|
||||||
|
AEDoor.GA_ENTRY.value: [36, 0, 0],
|
||||||
|
AEDoor.ST_ENTRY.value: [37, 0, 0],
|
||||||
|
AEDoor.ST_ENTRY_TEMPLE.value: [37, 2, 1],
|
||||||
|
AEDoor.ST_ENTRY_WELL.value: [37, 3, 2],
|
||||||
|
AEDoor.ST_TEMPLE.value: [38, 0, 1],
|
||||||
|
AEDoor.ST_WELL.value: [39, 0, 1],
|
||||||
|
AEDoor.WSW_ENTRY.value: [40, 0, 0],
|
||||||
|
AEDoor.WSW_ENTRY_GONG.value: [40, 2, 1],
|
||||||
|
AEDoor.WSW_GONG_ENTRY.value: [41, 0, 1],
|
||||||
|
AEDoor.WSW_GONG_MIDDLE.value: [41, 2, 2],
|
||||||
|
AEDoor.WSW_MIDDLE_GONG.value: [42, 0, 2],
|
||||||
|
AEDoor.WSW_MIDDLE_OBSTACLE.value: [42, 2, 1],
|
||||||
|
AEDoor.WSW_OBSTACLE_MIDDLE.value: [43, 0, 2],
|
||||||
|
AEDoor.WSW_OBSTACLE_BARREL.value: [43, 2, 1],
|
||||||
|
AEDoor.WSW_BARREL_OBSTACLE.value: [44, 0, 1],
|
||||||
|
AEDoor.CC_ENTRY.value: [45, 0, 0],
|
||||||
|
AEDoor.CC_ENTRY_BASEMENT.value: [45, 4, 1],
|
||||||
|
AEDoor.CC_ENTRY_CASTLE.value: [45, 2, 2],
|
||||||
|
AEDoor.CC_ENTRY_BELL.value: [45, 5, 3],
|
||||||
|
AEDoor.CC_ENTRY_BOSS.value: [45, 6, 4],
|
||||||
|
AEDoor.CC_CASTLEMAIN_ENTRY.value: [46, 0, 1],
|
||||||
|
AEDoor.CC_CASTLEMAIN_BELL.value: [46, 2, 2],
|
||||||
|
AEDoor.CC_CASTLEMAIN_ELEVATOR.value: [46, 3, 3],
|
||||||
|
AEDoor.CC_BASEMENT_ENTRY.value: [47, 0, 1],
|
||||||
|
AEDoor.CC_BASEMENT_BUTTON_DOWN.value: [47, 2, 4],
|
||||||
|
AEDoor.CC_BASEMENT_BUTTON_UP.value: [47, 3, 2],
|
||||||
|
AEDoor.CC_BASEMENT_ELEVATOR.value: [47, 4, 3],
|
||||||
|
AEDoor.CC_BOSS_ROOM.value: [48, 0, 1],
|
||||||
|
AEDoor.CC_BUTTON_BASEMENT_WATER.value: [49, 0, 1],
|
||||||
|
AEDoor.CC_BUTTON_BASEMENT_LEDGE.value: [49, 2, 2],
|
||||||
|
AEDoor.CC_ELEVATOR_CASTLEMAIN.value: [50, 0, 1],
|
||||||
|
AEDoor.CC_ELEVATOR_BASEMENT.value: [50, 2, 2],
|
||||||
|
AEDoor.CC_BELL_CASTLE.value: [51, 0, 1],
|
||||||
|
AEDoor.CC_BELL_ENTRY.value: [51, 2, 2],
|
||||||
|
AEDoor.CP_ENTRY.value: [53, 0, 0],
|
||||||
|
AEDoor.CP_OUTSIDE_SEWERS_FRONT.value: [53, 2, 1],
|
||||||
|
AEDoor.CP_OUTSIDE_BARREL.value: [53, 3, 2],
|
||||||
|
AEDoor.CP_SEWERSFRONT_OUTSIDE.value: [54, 0, 1],
|
||||||
|
AEDoor.CP_SEWERSFRONT_BARREL.value: [54, 2, 2],
|
||||||
|
AEDoor.CP_BARREL_SEWERS_FRONT.value: [55, 0, 1],
|
||||||
|
AEDoor.CP_BARREL_OUTSIDE.value: [55, 2, 2],
|
||||||
|
AEDoor.SF_ENTRY.value: [56, 0, 0],
|
||||||
|
AEDoor.SF_OUTSIDE_FACTORY.value: [56, 2, 1],
|
||||||
|
AEDoor.SF_FACTORY_RC_CAR.value: [57, 2 , 1],
|
||||||
|
AEDoor.SF_FACTORY_WHEEL_BOTTOM.value: [57, 3, 2],
|
||||||
|
AEDoor.SF_FACTORY_WHEEL_TOP.value: [57, 4, 3],
|
||||||
|
AEDoor.SF_FACTORY_MECH.value: [57, 5, 4],
|
||||||
|
AEDoor.SF_FACTORY_OUTSIDE.value: [57, 0, 5],
|
||||||
|
AEDoor.SF_RC_CAR_FACTORY.value: [58, 0, 1],
|
||||||
|
AEDoor.SF_LAVA_MECH.value: [59, 0, 1],
|
||||||
|
AEDoor.SF_LAVA_CONVEYOR.value: [59, 2, 2],
|
||||||
|
AEDoor.SF_WHEEL_FACTORY_BOTTOM.value: [60, 0, 1],
|
||||||
|
AEDoor.SF_WHEEL_FACTORY_TOP.value: [60, 2, 2],
|
||||||
|
AEDoor.SF_CONVEYOR_LAVA.value: [61, 0, 1],
|
||||||
|
AEDoor.SF_CONVEYOR1_ENTRY.value: [61, 7, 8],
|
||||||
|
AEDoor.SF_CONVEYOR2_ENTRY.value: [61, 7, 7],
|
||||||
|
AEDoor.SF_CONVEYOR3_ENTRY.value: [61, 6, 6],
|
||||||
|
AEDoor.SF_CONVEYOR4_ENTRY.value: [61, 5, 5],
|
||||||
|
AEDoor.SF_CONVEYOR5_ENTRY.value: [61, 4, 4],
|
||||||
|
AEDoor.SF_CONVEYOR6_ENTRY.value: [61, 3, 3],
|
||||||
|
AEDoor.SF_CONVEYOR7_ENTRY.value: [61, 2, 2],
|
||||||
|
AEDoor.SF_MECH_FACTORY.value: [62, 0, 1],
|
||||||
|
AEDoor.SF_MECH_LAVA.value: [62, 2, 2],
|
||||||
|
AEDoor.TVT_ENTRY.value: [63, 0, 0],
|
||||||
|
AEDoor.TVT_OUTSIDE_LOBBY.value: [63, 2, 1],
|
||||||
|
AEDoor.TVT_WATER_LOBBY.value: [64, 0, 1],
|
||||||
|
AEDoor.TVT_LOBBY_OUTSIDE.value: [65, 0, 1],
|
||||||
|
AEDoor.TVT_LOBBY_WATER.value: [65, 2, 2],
|
||||||
|
AEDoor.TVT_LOBBY_TANK.value: [65, 3, 3],
|
||||||
|
AEDoor.TVT_TANK_LOBBY.value: [66, 0, 1],
|
||||||
|
AEDoor.TVT_TANK_BOSS.value: [66, 2, 2],
|
||||||
|
AEDoor.TVT_TANK_FAN.value: [66, 3, 3],
|
||||||
|
AEDoor.TVT_FAN_TANK.value: [67, 0, 1],
|
||||||
|
AEDoor.TVT_BOSS_TANK.value: [68, 0, 1],
|
||||||
|
AEDoor.MM_SL_HUB.value: [69, 0, 0],
|
||||||
|
AEDoor.MM_SL_HUB_COASTER.value: [69, 2, 1],
|
||||||
|
AEDoor.MM_SL_HUB_CIRCUS.value: [69, 3, 2],
|
||||||
|
AEDoor.MM_SL_HUB_WESTERN.value: [69, 4, 3],
|
||||||
|
AEDoor.MM_SL_HUB_GO_KARZ.value: [69, 5, 4],
|
||||||
|
AEDoor.MM_SL_HUB_CRATER.value: [69, 6, 5],
|
||||||
|
AEDoor.MM_GO_KARZ_SL_HUB.value: [70, 0, 1],
|
||||||
|
AEDoor.MM_CIRCUS_SL_HUB.value: [71, 0, 1],
|
||||||
|
AEDoor.MM_COASTER_ENTRY_SL_HUB.value: [72, 0, 1],
|
||||||
|
AEDoor.MM_COASTER_ENTRY_COASTER1.value: [72, 2, 2], # IHNN note - Entrance only door, normally inaccessible. Equivalent to 72, 3.
|
||||||
|
AEDoor.MM_COASTER_ENTRY_DISEMBARK.value: [72, 3, 0],
|
||||||
|
AEDoor.MM_COASTER1_ENTRY.value: [73, 0, 0],
|
||||||
|
AEDoor.MM_COASTER1_COASTER2.value: [73, 2, 1], # IHNN note - Entrance only door, normally inaccessible. Equivalent to 73, 0.
|
||||||
|
AEDoor.MM_COASTER2_ENTRY.value: [74, 0, 0],
|
||||||
|
AEDoor.MM_COASTER2_HAUNTED_HOUSE.value: [74, 2, 1], # IHNN note - Entrance only door, normally inaccessible. Equivalent to 74, 0.
|
||||||
|
AEDoor.MM_HAUNTED_HOUSE_DISEMBARK.value: [75, 0, 0],
|
||||||
|
AEDoor.MM_HAUNTED_HOUSE_COFFIN.value: [75, 3, 1],
|
||||||
|
AEDoor.MM_COFFIN_HAUNTED_HOUSE.value: [76, 0, 1],
|
||||||
|
AEDoor.MM_COFFIN_COASTER_ENTRY.value: [76, 2, 2], # IHNN note - Entrance only door, normally inaccessible. Spawns Spike in the center of the coffins.
|
||||||
|
AEDoor.MM_WESTERN_SL_HUB.value: [77, 0, 1],
|
||||||
|
AEDoor.MM_CRATER_SL_HUB.value: [78, 0, 1],
|
||||||
|
AEDoor.MM_CRATER_OUTSIDE_CASTLE.value: [78, 2, 2],
|
||||||
|
AEDoor.MM_OUTSIDE_CASTLE_CASTLE_MAIN.value: [79, 2, 1],
|
||||||
|
AEDoor.MM_OUTSIDE_CASTLE_SIDE_ENTRY.value: [79, 3, 2],
|
||||||
|
AEDoor.MM_OUTSIDE_CASTLE_CRATER.value: [79, 0, 3],
|
||||||
|
AEDoor.MM_CASTLE_MAIN_OUTSIDE_CASTLE.value: [80, 0, 1],
|
||||||
|
AEDoor.MM_CASTLE_MAIN_MONKEY_HEAD.value: [80, 2, 2],
|
||||||
|
AEDoor.MM_CASTLE_MAIN_INSIDE_CLIMB.value: [80, 3, 3],
|
||||||
|
AEDoor.MM_CASTLE_MAIN_FROM_OUTSIDE.value: [80, 4, 0],
|
||||||
|
AEDoor.MM_CASTLE_MAIN_SPECTER1.value: [80, 5, 4], # IHNN note - Invalid connection.
|
||||||
|
# Thedragon005 note - The connection exists, but it is oneway only (Correspond to entering the transition to specter)
|
||||||
|
# The invalid connection here would be AEDoor.MM_SPECTER1_ROOM_CASTLE_MAIN since you cannot exit the Specter fight once in the room
|
||||||
|
AEDoor.MM_INSIDE_CLIMB_OUTSIDE_CLIMB.value: [81, 2, 1],
|
||||||
|
AEDoor.MM_INSIDE_CLIMB_CASTLE_MAIN.value: [81, 0, 2],
|
||||||
|
AEDoor.MM_OUTSIDE_CLIMB_INSIDE_CLIMB.value: [82, 0, 1],
|
||||||
|
AEDoor.MM_OUTSIDE_CLIMB_CASTLE_MAIN.value: [82, 2, 2], # IHNN note - Entrance only door, normally inaccessible. Spawns Spike at the top, by the mech.
|
||||||
|
AEDoor.MM_SPECTER1_ROOM.value: [83, 0, 0],
|
||||||
|
AEDoor.MM_MONKEY_HEAD_CASTLE_MAIN.value: [84, 0, 1],
|
||||||
|
AEDoor.MM_SIDE_ENTRY_OUTSIDE_CASTLE.value: [85, 0, 1],
|
||||||
|
AEDoor.PPM_ENTRY.value: [87, 0, 0], # IHNN note - noticed this was missing, have not validated the room.
|
||||||
|
AEDoor.TIME_ENTRY.value: [88, 0, 0],
|
||||||
|
AEDoor.TIME_MAIN_TRAINING.value: [88, 3, 1],
|
||||||
|
AEDoor.TIME_MAIN_MINIGAME.value: [88, 2, 2],
|
||||||
|
AEDoor.TIME_MINIGAME_MAIN.value: [91, 1, 1],
|
||||||
|
AEDoor.TIME_TRAINING_MAIN.value: [90, 0, 1],
|
||||||
|
AEDoor.TIME_TRAINING_WATERNET.value: [90, 1, 2],
|
||||||
|
AEDoor.TIME_TRAINING_RADAR.value: [90, 2, 3],
|
||||||
|
AEDoor.TIME_TRAINING_SLING.value: [90, 3, 4],
|
||||||
|
AEDoor.TIME_TRAINING_HOOP.value: [90, 4, 5],
|
||||||
|
AEDoor.TIME_TRAINING_FLYER.value: [90, 5, 6],
|
||||||
|
AEDoor.TIME_TRAINING_CAR.value: [90, 6, 7],
|
||||||
|
AEDoor.TIME_TRAINING_PUNCH.value: [90, 7, 8],
|
||||||
|
}
|
||||||
|
|
||||||
|
def createLocationGroups():
|
||||||
|
# Iterate through all locations
|
||||||
|
for x in range (0, len(location_table)):
|
||||||
|
locname = list(location_table.keys())[x]
|
||||||
|
# Add to location group for each level
|
||||||
|
if "1-1" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Fossil Field", []).append(locname)
|
||||||
|
elif "1-2" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Primordial Ooze", []).append(locname)
|
||||||
|
elif "1-3" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Molten Lava", []).append(locname)
|
||||||
|
elif "2-1" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Thick Jungle", []).append(locname)
|
||||||
|
elif "2-2" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Dark Ruins", []).append(locname)
|
||||||
|
elif "2-3" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Cryptic Relics", []).append(locname)
|
||||||
|
elif "3-1" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Stadium Attack", []).append(locname)
|
||||||
|
GROUPED_LOCATIONS.setdefault("Races", []).append(locname)
|
||||||
|
elif "4-1" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Crabby Beach", []).append(locname)
|
||||||
|
elif "4-2" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Coral Cave", []).append(locname)
|
||||||
|
elif "4-3" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Dexters Island", []).append(locname)
|
||||||
|
elif "5-1" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Snowy Mammoth", []).append(locname)
|
||||||
|
elif "5-2" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Frosty Retreat", []).append(locname)
|
||||||
|
elif "5-3" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Hot Springs", []).append(locname)
|
||||||
|
elif "6-1" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Gladiator Attack", []).append(locname)
|
||||||
|
GROUPED_LOCATIONS.setdefault("Races", []).append(locname)
|
||||||
|
elif "7-1" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Sushi Temple", []).append(locname)
|
||||||
|
elif "7-2" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Wabi Sabi Wall", []).append(locname)
|
||||||
|
elif "7-3" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Crumbling Castle", []).append(locname)
|
||||||
|
elif "8-1" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("City Park", []).append(locname)
|
||||||
|
elif "8-2" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Specters Factory", []).append(locname)
|
||||||
|
elif "8-3" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("TV Tower", []).append(locname)
|
||||||
|
elif "9-1" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Monkey Madness", []).append(locname)
|
||||||
|
elif "Time Station" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Time Station", []).append(locname)
|
||||||
|
|
||||||
|
if "Mailbox" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Mailboxes", []).append(locname)
|
||||||
|
elif "Coin" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Specter Coins", []).append(locname)
|
||||||
|
elif ("Specter" in locname and "Factory" not in locname) or "Boss" in locname or "Jake" in locname or "Professor" in locname:
|
||||||
|
GROUPED_LOCATIONS.setdefault("Bosses", []).append(locname)
|
||||||
|
elif ("Monkey" in locname and "Monkey Madness" not in locname) or ("Monkey Madness Monkey" in locname):
|
||||||
|
GROUPED_LOCATIONS.setdefault("Monkeys", []).append(locname)
|
||||||
|
|
||||||
|
createLocationGroups()
|
||||||
6
worlds/apeescape/Notes.txt
Normal file
6
worlds/apeescape/Notes.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Thick Jungle :
|
||||||
|
Random Starting Room to Fish Room not giving control back to player
|
||||||
|
|
||||||
|
Dark Ruins :
|
||||||
|
|
||||||
|
RSR fails sometimes ?
|
||||||
571
worlds/apeescape/Options.py
Normal file
571
worlds/apeescape/Options.py
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from Options import Choice, Range, DeathLink, PerGameCommonOptions, OptionDict, FreeText, OptionSet, OptionCounter, \
|
||||||
|
Toggle, OptionList, DefaultOnToggle
|
||||||
|
from .Items import AEItem
|
||||||
|
|
||||||
|
class GoalOption(Choice):
|
||||||
|
"""Choose the victory condition for this world.
|
||||||
|
|
||||||
|
mm: First Specter fight in Monkey Madness, with the vanilla condition (just get there).
|
||||||
|
ppm: Second Specter fight in Peak Point Matrix, with the vanilla condition (catch all monkeys). Peak Point Matrix will only have the vanilla entry condition for Specter 1 and Specter 2 goals.
|
||||||
|
tokenhunt: Collecting enough Specter Token items throughout the world.
|
||||||
|
mmtoken: First Specter fight in Monkey Madness, after collecting enough Specter Token items.
|
||||||
|
ppmtoken: Second Specter fight in Peak Point Matrix, after collecting enough Specter Token items.
|
||||||
|
|
||||||
|
Supported values: mm, ppm, tokenhunt, mmtoken, ppmtoken
|
||||||
|
Default value: mm
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Goal"
|
||||||
|
option_mm = 0x00
|
||||||
|
option_ppm = 0x01
|
||||||
|
option_tokenhunt = 0x02
|
||||||
|
option_mmtoken = 0x03
|
||||||
|
option_ppmtoken = 0x04
|
||||||
|
default = option_mm
|
||||||
|
|
||||||
|
class FastTokenGoalOption(Choice):
|
||||||
|
"""If this is enabled and the chosen Goal is `mmtoken` or `ppmtoken`, provides easy access to the end boss once enough tokens have been obtained.
|
||||||
|
The warp will be available in the Training Room where the first training warp would be (Water Net).
|
||||||
|
NOTE : Music does not change when warping. Using this warp will result in fighting your goal boss with the Time Station music.
|
||||||
|
|
||||||
|
off: Depending on goal, MM/PPM will be unlocked with world keys and will need to be accessed through Level Select
|
||||||
|
on: When enough tokens have been obtained,a warp taking you directly to your Goal boss in the Training Room will be activated, skipping all other requirements
|
||||||
|
|
||||||
|
Supported values: off, on
|
||||||
|
Default value: off
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Fast Token Goal"
|
||||||
|
option_off = 0x00
|
||||||
|
option_on = 0x01
|
||||||
|
default = option_off
|
||||||
|
|
||||||
|
class AllowCollectOption(Toggle):
|
||||||
|
"""
|
||||||
|
Allows for !collect to catch Monkeys or collect Coins containing items for other players.
|
||||||
|
"""
|
||||||
|
display_name = "Allow Collect"
|
||||||
|
|
||||||
|
|
||||||
|
class RequiredTokensOption(Range):
|
||||||
|
"""Choose the required number of Specter Tokens for goal.
|
||||||
|
|
||||||
|
Supported values: 5 - 60
|
||||||
|
Default value: 20
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Required Tokens"
|
||||||
|
range_start = 5
|
||||||
|
range_end = 60
|
||||||
|
default = 20
|
||||||
|
|
||||||
|
|
||||||
|
class TotalTokensOption(Range):
|
||||||
|
"""Choose the total number of Specter Tokens in the item pool.
|
||||||
|
If a world requests a token requirement greater than the number of tokens created, then the total and required values will be swapped.
|
||||||
|
|
||||||
|
Supported values: 5 - 60
|
||||||
|
Default value: 30
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Total Tokens"
|
||||||
|
range_start = 5
|
||||||
|
range_end = 60
|
||||||
|
default = 30
|
||||||
|
|
||||||
|
|
||||||
|
class TokenLocationsOption(Choice):
|
||||||
|
"""Choose where Specter Tokens can be placed in the multiworld.
|
||||||
|
|
||||||
|
anywhere: Specter Tokens can be placed anywhere in the multiworld.
|
||||||
|
ownworld: Specter Tokens can only be placed in your world.
|
||||||
|
|
||||||
|
Supported values: anywhere, ownworld
|
||||||
|
Default value: ownworld
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Token Locations"
|
||||||
|
option_anywhere = 0x00
|
||||||
|
option_ownworld = 0x01
|
||||||
|
default = option_ownworld
|
||||||
|
|
||||||
|
|
||||||
|
class LogicOption(Choice):
|
||||||
|
"""Choose expected trick knowledge.
|
||||||
|
|
||||||
|
normal: No advanced movement tech or out of bounds required, and hard monkeys will guarantee a helpful gadget. Some additional difficult or precise jumps won't be required either. May still require some out of the box thinking or non-standard routes. Suitable for casual players.
|
||||||
|
hard: Movement tech can be required in places with a low penalty for failing. Suitable for players with speedrun knowledge.
|
||||||
|
expert: All tricks and glitches can be required, and some monkeys may require resetting the room if not caught in a certain way. Can also require obscure game knowledge. Suitable for those seeking the ultimate challenge.
|
||||||
|
|
||||||
|
Supported values: normal, hard, expert
|
||||||
|
Default value: normal
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Logic"
|
||||||
|
option_normal = 0x00
|
||||||
|
option_hard = 0x01
|
||||||
|
option_expert = 0x02
|
||||||
|
default = option_normal
|
||||||
|
|
||||||
|
|
||||||
|
class InfiniteJumpOption(Choice):
|
||||||
|
"""Choose if the Infinite Jump trick should be put into logic.
|
||||||
|
|
||||||
|
false: Infinite Jump is not put into logic.
|
||||||
|
true: Infinite Jump is put into logic.
|
||||||
|
|
||||||
|
Supported values: false, true
|
||||||
|
Default value: false
|
||||||
|
"""
|
||||||
|
display_name = "Infinite Jump"
|
||||||
|
option_false = 0x00
|
||||||
|
option_true = 0x01
|
||||||
|
default = option_false
|
||||||
|
|
||||||
|
|
||||||
|
class SuperFlyerOption(Choice):
|
||||||
|
"""Choose if the Super Flyer trick should be put into logic.
|
||||||
|
|
||||||
|
false: Super Flyer is not put into logic.
|
||||||
|
true: Super Flyer is put into logic.
|
||||||
|
|
||||||
|
Supported values: false, true
|
||||||
|
Default value: false
|
||||||
|
"""
|
||||||
|
display_name = "Super Flyer"
|
||||||
|
option_false = 0x00
|
||||||
|
option_true = 0x01
|
||||||
|
default = option_false
|
||||||
|
|
||||||
|
|
||||||
|
class EntranceOption(Choice):
|
||||||
|
"""Choose which level entrances should be randomized. Peak Point Matrix will always be the last level when it's postgame or the goal level. Races will be included in randomization if coin shuffle is on, and excluded otherwise.
|
||||||
|
|
||||||
|
off: Levels will be in the vanilla order.
|
||||||
|
on: Levels will be in a random order.
|
||||||
|
lockmm: Levels will be in a random order, and Monkey Madness will be locked to its original entrance.
|
||||||
|
|
||||||
|
Supported values: off, on, lockmm
|
||||||
|
Default value: on
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Entrance"
|
||||||
|
option_off = 0x00
|
||||||
|
option_on = 0x01
|
||||||
|
option_lockmm = 0x02
|
||||||
|
default = option_on
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeStartingRoomOption(Choice):
|
||||||
|
"""Choose if the starting room for each level should be randomized.
|
||||||
|
|
||||||
|
off: The starting room for each level is the original starting room.
|
||||||
|
on: The starting room for each level is a random room from within that level.
|
||||||
|
|
||||||
|
Supported values: off, on
|
||||||
|
Default value: on
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Randomize Starting Room"
|
||||||
|
option_off = 0x00
|
||||||
|
option_on = 0x01
|
||||||
|
default = option_off
|
||||||
|
|
||||||
|
|
||||||
|
class KeyOption(Choice):
|
||||||
|
"""Choose how many levels each World Key should unlock. The first three levels will always start unlocked.
|
||||||
|
Races will be skipped if coin shuffle is off. Peak Point Matrix will require the same number of keys as the Monkey Madness entrance on a boss goal, and one additional key on a token hunt or token boss goal.
|
||||||
|
|
||||||
|
world: Each World Key unlocks the 1 or 3 levels in the next world. Creates between 6 and 9 World Keys.
|
||||||
|
level: Each World Key unlocks the next level. Creates between 16 and 19 World Keys.
|
||||||
|
twolevels: Each World Key unlocks the next two levels. Creates between 8 and 10 World Keys.
|
||||||
|
none: All levels are open from the beginning of the game.
|
||||||
|
|
||||||
|
Supported values: world, level, twolevels, none
|
||||||
|
Default value: world
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Unlocks per Key"
|
||||||
|
option_world = 0x00
|
||||||
|
option_level = 0x01
|
||||||
|
option_twolevels = 0x02
|
||||||
|
option_none = 0x03
|
||||||
|
default = option_world
|
||||||
|
|
||||||
|
|
||||||
|
class ExtraKeysOption(Range):
|
||||||
|
"""Choose the number of extra World Keys that should be created.
|
||||||
|
|
||||||
|
Supported values: 0 - 10
|
||||||
|
Default value: 0
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Extra Keys"
|
||||||
|
range_start = 0
|
||||||
|
range_end = 10
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class CoinOption(Choice):
|
||||||
|
"""Choose if Specter Coins should be added as locations.
|
||||||
|
|
||||||
|
false: Specter Coins are not locations.
|
||||||
|
true: The 60 Specter Coins are added as locations.
|
||||||
|
|
||||||
|
Supported values: false, true
|
||||||
|
Default value: false
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Coin"
|
||||||
|
option_false = 0x00
|
||||||
|
option_true = 0x01
|
||||||
|
default = option_false
|
||||||
|
|
||||||
|
|
||||||
|
class MailboxOption(Choice):
|
||||||
|
"""Choose if mailboxes should act as locations.
|
||||||
|
Mailboxes in training rooms will never be locations.
|
||||||
|
|
||||||
|
false: Mailboxes are not locations.
|
||||||
|
true: The 63 available mailboxes are added as locations.
|
||||||
|
|
||||||
|
Supported values: false, true
|
||||||
|
Default value: false
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Mailbox"
|
||||||
|
option_false = 0x00
|
||||||
|
option_true = 0x01
|
||||||
|
default = option_false
|
||||||
|
|
||||||
|
|
||||||
|
class LampOption(Choice):
|
||||||
|
"""Choose if Monkey Lamps should be locked and shuffled into the multiworld.
|
||||||
|
|
||||||
|
false: Monkey Lamps will act in vanilla (catch enough monkeys in their level to open the door)
|
||||||
|
true: The 8 Monkey Lamps will be items in the multiworld, that open their respective door when received.
|
||||||
|
|
||||||
|
Supported values: false, true
|
||||||
|
Default value: false
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Monkey Lamps"
|
||||||
|
option_false = 0x00
|
||||||
|
option_true = 0x01
|
||||||
|
default = option_false
|
||||||
|
|
||||||
|
|
||||||
|
class GadgetOption(Choice):
|
||||||
|
"""Choose a starting gadget aside from the Time Net.
|
||||||
|
|
||||||
|
club: Start with the Stun Club.
|
||||||
|
radar: Start with the Monkey Radar.
|
||||||
|
sling: Start with the Slingback Shooter.
|
||||||
|
hoop: Start with the Super Hoop.
|
||||||
|
flyer: Start with the Sky Flyer.
|
||||||
|
car: Start with the RC Car.
|
||||||
|
punch: Start with the Magic Punch.
|
||||||
|
waternet: Start with the Water Net.
|
||||||
|
none: Start with no additional gadgets.
|
||||||
|
|
||||||
|
Supported values: club, radar, sling, hoop, flyer, car, punch, waternet, none
|
||||||
|
Default value: club
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Gadget"
|
||||||
|
option_club = 0x00
|
||||||
|
option_radar = 0x01
|
||||||
|
option_sling = 0x02
|
||||||
|
option_hoop = 0x03
|
||||||
|
option_flyer = 0x04
|
||||||
|
option_car = 0x05
|
||||||
|
option_punch = 0x06
|
||||||
|
option_waternet = 0x07
|
||||||
|
option_none = 0x08
|
||||||
|
default = option_club
|
||||||
|
|
||||||
|
|
||||||
|
class ShuffleNetOption(Choice):
|
||||||
|
"""Choose if the Time Net should be shuffled.
|
||||||
|
This option requires at least one of coins and mailboxes to be shuffled to be used - if all locations in this world require the net, the net will be given at game start.
|
||||||
|
|
||||||
|
false: Time Net is not shuffled, and is given at game start.
|
||||||
|
true: Time Net is shuffled into the pool. The mailboxes in the Time Station will also be locations if this happens.
|
||||||
|
|
||||||
|
Supported values: false, true
|
||||||
|
Default value: false
|
||||||
|
"""
|
||||||
|
display_name = "Shuffle Net"
|
||||||
|
option_false = 0x00
|
||||||
|
option_true = 0x01
|
||||||
|
default = option_false
|
||||||
|
|
||||||
|
|
||||||
|
class ShuffleWaterNetOption(Choice):
|
||||||
|
"""Choose if the Water Net should be shuffled.
|
||||||
|
|
||||||
|
off: Water Net is not shuffled, and is given at game start.
|
||||||
|
progressive: Water Net is shuffled and split into parts, adding two Progressive Water Nets and Water Catch to the pool.
|
||||||
|
- Progressive Water Net : The first allows Spike to swim on the surface and avoid drowning after a few seconds. The second allows Spike to dive underwater.
|
||||||
|
- Water Catch: Allows shooting the Water Net.
|
||||||
|
on: Water Net is shuffled, adding it to the pool as a single item.
|
||||||
|
|
||||||
|
Supported values: off, progressive, on
|
||||||
|
Default value: off
|
||||||
|
"""
|
||||||
|
display_name = "Shuffle Water Net"
|
||||||
|
option_off = 0x00
|
||||||
|
option_progressive = 0x01
|
||||||
|
option_on = 0x02
|
||||||
|
default = option_off
|
||||||
|
|
||||||
|
|
||||||
|
class LowOxygenSounds(Choice):
|
||||||
|
"""Choose how quickly the low oxygen beep sound effect will play when underwater.
|
||||||
|
|
||||||
|
off: Low Oxygen sounds will not play at all.
|
||||||
|
half: Low Oxygen sounds will play less frequently.
|
||||||
|
on: Low Oxygen Sounds will play normally.
|
||||||
|
|
||||||
|
Supported values: off, half, on
|
||||||
|
Default value: half
|
||||||
|
"""
|
||||||
|
display_name = "Low Oxygen Sounds"
|
||||||
|
option_off = 0x00
|
||||||
|
option_half = 0x01
|
||||||
|
option_on = 0x02
|
||||||
|
default = option_half
|
||||||
|
|
||||||
|
|
||||||
|
class FillerPreset(Choice):
|
||||||
|
"""Choose the distribution of filler items.
|
||||||
|
|
||||||
|
Normal: Balanced distribution with better items appearing less often.
|
||||||
|
Bountiful: Nearly every filler item will be useful.
|
||||||
|
Stingy: Nearly every filler item will be its smallest quantity.
|
||||||
|
Nothing: Replace all filler items with Nothing.
|
||||||
|
Custom: Use custom weights set with the option "customfillerweights".
|
||||||
|
|
||||||
|
Supported values: normal, bountiful, stingy, nothing, custom
|
||||||
|
Default value: normal
|
||||||
|
"""
|
||||||
|
display_name = "Filler Preset"
|
||||||
|
option_normal = 0x00
|
||||||
|
option_bountiful = 0x01
|
||||||
|
option_stingy = 0x02
|
||||||
|
option_nothing = 0x03
|
||||||
|
option_custom = 0x04
|
||||||
|
default = option_normal
|
||||||
|
|
||||||
|
|
||||||
|
class CustomFillerWeights(OptionCounter):
|
||||||
|
"""Use custom weights for filler item distribution by choosing "Custom" in the "Filler Preset" option.
|
||||||
|
This works the same way as other weighted options.
|
||||||
|
You can use a weight of 0 to prevent that filler item from appearing.
|
||||||
|
If all weights are set to 0, then all filler items will be Nothing.
|
||||||
|
|
||||||
|
Range: 0 - 100
|
||||||
|
Default values are the same as the Normal preset.
|
||||||
|
"""
|
||||||
|
internal_name = "customfillerweights"
|
||||||
|
display_name = "Custom Filler Weights"
|
||||||
|
default_weight = 10
|
||||||
|
min = 0
|
||||||
|
max = 100
|
||||||
|
valid_keys = frozenset({
|
||||||
|
AEItem.Shirt.value, AEItem.Cookie.value, AEItem.FiveCookies.value, AEItem.Triangle.value,
|
||||||
|
AEItem.BigTriangle.value, AEItem.BiggerTriangle.value, AEItem.Flash.value, AEItem.ThreeFlash.value,
|
||||||
|
AEItem.Rocket.value, AEItem.ThreeRocket.value, AEItem.RainbowCookie.value, AEItem.Nothing.value})
|
||||||
|
default = {
|
||||||
|
AEItem.Shirt.value: 7,
|
||||||
|
AEItem.Cookie.value: 16,
|
||||||
|
AEItem.FiveCookies.value: 3,
|
||||||
|
AEItem.Triangle.value: 31,
|
||||||
|
AEItem.BigTriangle.value: 14,
|
||||||
|
AEItem.BiggerTriangle.value: 4,
|
||||||
|
AEItem.Flash.value: 9,
|
||||||
|
AEItem.ThreeFlash.value: 3,
|
||||||
|
AEItem.Rocket.value: 9,
|
||||||
|
AEItem.ThreeRocket.value: 3,
|
||||||
|
AEItem.RainbowCookie.value: 6,
|
||||||
|
AEItem.Nothing.value: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TrapPercentage(Range):
|
||||||
|
"""Replace a percentage of filler items in the item pool with random traps.
|
||||||
|
|
||||||
|
Range: 0 - 100
|
||||||
|
Default value: 0
|
||||||
|
"""
|
||||||
|
display_name = "Trap Percentage"
|
||||||
|
range_start = 0
|
||||||
|
range_end = 100
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class TrapWeights(OptionCounter):
|
||||||
|
"""Specify the weighted chance of rolling individual trap items.
|
||||||
|
|
||||||
|
You can use a weight of 0 to guarantee a particular trap will never appear.
|
||||||
|
**This option is ignored when "TrapPercentage" option is set to an other value than "custom"
|
||||||
|
|
||||||
|
Range: 0 - 100
|
||||||
|
Default values:
|
||||||
|
Banana Peel Trap: 15
|
||||||
|
Gadget Shuffle Trap: 13
|
||||||
|
Monkey Mash Trap: 5
|
||||||
|
Icy Hot Pants Trap: 10
|
||||||
|
Stun Trap: 7
|
||||||
|
Camera Rotate Trap: 10
|
||||||
|
"""
|
||||||
|
internal_name = "customtrapweights"
|
||||||
|
display_name = "Custom Trap Weights"
|
||||||
|
min = 0
|
||||||
|
max = 100
|
||||||
|
valid_keys = frozenset({
|
||||||
|
AEItem.BananaPeelTrap.value, AEItem.GadgetShuffleTrap.value, AEItem.MonkeyMashTrap.value, AEItem.IcyHotPantsTrap.value, AEItem.StunTrap.value, AEItem.CameraRotateTrap.value
|
||||||
|
})
|
||||||
|
default = {
|
||||||
|
AEItem.BananaPeelTrap.value: 15, AEItem.GadgetShuffleTrap.value : 13, AEItem.MonkeyMashTrap.value: 5, AEItem.IcyHotPantsTrap.value: 10, AEItem.StunTrap.value: 7, AEItem.CameraRotateTrap.value: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TrapsOnReconnect(OptionSet):
|
||||||
|
"""Determine which traps are sent when reconnecting.
|
||||||
|
|
||||||
|
This option determines which traps will be sent when reconnecting to the client.
|
||||||
|
Traps that are not in this list will only activate when connected
|
||||||
|
|
||||||
|
Supported values: "Banana Peel Trap", "Gadget Shuffle Trap", "Monkey Mash Trap", "Icy Hot Pants Trap", "Stun Trap", "Camera Rotate Trap"
|
||||||
|
"""
|
||||||
|
internal_name = "trapsonreconnect"
|
||||||
|
display_name = "Traps On Reconnect"
|
||||||
|
supports_weighting = False
|
||||||
|
valid_keys = frozenset({
|
||||||
|
AEItem.BananaPeelTrap.value,AEItem.GadgetShuffleTrap.value, AEItem.MonkeyMashTrap.value, AEItem.IcyHotPantsTrap.value, AEItem.StunTrap.value, AEItem.CameraRotateTrap.value
|
||||||
|
})
|
||||||
|
|
||||||
|
class TrapLink(Toggle):
|
||||||
|
"""
|
||||||
|
Whether your received traps are linked to other players
|
||||||
|
|
||||||
|
You will also receive any linked traps from other players with Trap Link enabled,
|
||||||
|
if you have a weight above "none" set for that trap
|
||||||
|
"""
|
||||||
|
display_name = "Trap Link"
|
||||||
|
|
||||||
|
|
||||||
|
class ItemDisplayOption(Choice):
|
||||||
|
"""Set the default for the Bizhawk item display command. This can be changed in the client at any time. The position and duration of these messages can be changed in Bizhawk config at any time.
|
||||||
|
|
||||||
|
off: Receiving an item will not show a message in Bizhawk.
|
||||||
|
on: Receiving an item will show a message in Bizhawk.
|
||||||
|
|
||||||
|
Supported values: off, on
|
||||||
|
Default value: on
|
||||||
|
"""
|
||||||
|
display_name = "Item Display"
|
||||||
|
option_off = 0x00
|
||||||
|
option_on = 0x01
|
||||||
|
default = option_on
|
||||||
|
|
||||||
|
|
||||||
|
class KickoutPreventionOption(Choice):
|
||||||
|
"""Set the default for Kickout Prevention behavior. This can be changed in the client at any time.
|
||||||
|
|
||||||
|
off: Will always kick you out after catching the level's last monkey or defeating a boss.
|
||||||
|
on: Prevents the kickout when catching the last monkey or defeating a boss.
|
||||||
|
|
||||||
|
Supported values: off, on
|
||||||
|
Default value: on
|
||||||
|
"""
|
||||||
|
display_name = "Kickout Prevention"
|
||||||
|
option_off = 0x00
|
||||||
|
option_on = 0x01
|
||||||
|
default = option_on
|
||||||
|
|
||||||
|
|
||||||
|
class AutoEquipOption(Choice):
|
||||||
|
"""Set the default for Auto Equipping new gadgets. This can be changed in the client at any time.
|
||||||
|
|
||||||
|
off: Received gadgets need to be manually equipped.
|
||||||
|
on: Received gadgets will automatically be equipped to an open face button, if one exists.
|
||||||
|
|
||||||
|
Supported values: off, on
|
||||||
|
Default value: on
|
||||||
|
"""
|
||||||
|
display_name = "Auto Equip"
|
||||||
|
option_off = 0x00
|
||||||
|
option_on = 0x01
|
||||||
|
default = option_on
|
||||||
|
|
||||||
|
|
||||||
|
class SpikeColor(Choice):
|
||||||
|
"""Determine the color of Spike in-game. This can be changed in the client at any time.
|
||||||
|
Can select between the following presets, or choose "custom" to use a custom color set with the "CustomSpikeColor" option.
|
||||||
|
|
||||||
|
Supported values: vanilla, dark, white, red, green, blue, yellow, cyan, magenta, custom
|
||||||
|
Default value: vanilla
|
||||||
|
"""
|
||||||
|
display_name = "Spike Color"
|
||||||
|
option_vanilla = 0
|
||||||
|
option_dark = 1
|
||||||
|
option_white = 2
|
||||||
|
option_red = 3
|
||||||
|
option_green = 4
|
||||||
|
option_blue = 5
|
||||||
|
option_yellow = 6
|
||||||
|
option_cyan = 7
|
||||||
|
option_magenta = 8
|
||||||
|
option_custom = -1
|
||||||
|
default = option_vanilla
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSpikeColor(FreeText):
|
||||||
|
"""Use a custom color for Spike by choosing "Custom" in the "Spike Color" option.
|
||||||
|
Enter an RGB hexadecimal value for the desired color.
|
||||||
|
**Note: If an invalid color is entered, it will be set to the "Vanilla" preset!
|
||||||
|
|
||||||
|
Range: 000000 to FFFFFF
|
||||||
|
Default value: FFFFFF
|
||||||
|
"""
|
||||||
|
display_name = "Custom Spike Color"
|
||||||
|
default = "FFFFFF"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ApeEscapeOptions(PerGameCommonOptions):
|
||||||
|
goal: GoalOption
|
||||||
|
fasttokengoal: FastTokenGoalOption
|
||||||
|
allowcollect: AllowCollectOption
|
||||||
|
requiredtokens: RequiredTokensOption
|
||||||
|
totaltokens: TotalTokensOption
|
||||||
|
tokenlocations: TokenLocationsOption
|
||||||
|
logic: LogicOption
|
||||||
|
infinitejump: InfiniteJumpOption
|
||||||
|
superflyer: SuperFlyerOption
|
||||||
|
entrance: EntranceOption
|
||||||
|
randomizestartingroom: RandomizeStartingRoomOption
|
||||||
|
unlocksperkey: KeyOption
|
||||||
|
extrakeys: ExtraKeysOption
|
||||||
|
coin: CoinOption
|
||||||
|
mailbox: MailboxOption
|
||||||
|
lamp: LampOption
|
||||||
|
gadget: GadgetOption
|
||||||
|
shufflenet: ShuffleNetOption
|
||||||
|
shufflewaternet: ShuffleWaterNetOption
|
||||||
|
lowoxygensounds: LowOxygenSounds
|
||||||
|
fillerpreset: FillerPreset
|
||||||
|
customfillerweights: CustomFillerWeights
|
||||||
|
trappercentage: TrapPercentage
|
||||||
|
trapweights: TrapWeights
|
||||||
|
trapsonreconnect: TrapsOnReconnect
|
||||||
|
trap_link: TrapLink
|
||||||
|
itemdisplay: ItemDisplayOption
|
||||||
|
kickoutprevention: KickoutPreventionOption
|
||||||
|
autoequip: AutoEquipOption
|
||||||
|
spikecolor: SpikeColor
|
||||||
|
customspikecolor: CustomSpikeColor
|
||||||
|
death_link: DeathLink
|
||||||
2788
worlds/apeescape/RAMAddress.py
Normal file
2788
worlds/apeescape/RAMAddress.py
Normal file
File diff suppressed because it is too large
Load Diff
1344
worlds/apeescape/Regions.py
Normal file
1344
worlds/apeescape/Regions.py
Normal file
File diff suppressed because it is too large
Load Diff
2998
worlds/apeescape/Rules.py
Normal file
2998
worlds/apeescape/Rules.py
Normal file
File diff suppressed because it is too large
Load Diff
604
worlds/apeescape/Strings.py
Normal file
604
worlds/apeescape/Strings.py
Normal file
@@ -0,0 +1,604 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class AELocation(Enum):
|
||||||
|
W1L1Noonan = "1-1 Fossil Field Monkey 1 - Noonan"
|
||||||
|
W1L1Jorjy = "1-1 Fossil Field Monkey 2 - Jorjy"
|
||||||
|
W1L1Nati = "1-1 Fossil Field Monkey 3 - Nati"
|
||||||
|
W1L1TrayC = "1-1 Fossil Field Monkey 4 - Tray C"
|
||||||
|
W1L2Shay = "1-2 Primordial Ooze Monkey 1 - Shay"
|
||||||
|
W1L2DrMonk = "1-2 Primordial Ooze Monkey 2 - Dr. Monk"
|
||||||
|
W1L2Grunt = "1-2 Primordial Ooze Monkey 3 - Grunt"
|
||||||
|
W1L2Ahchoo = "1-2 Primordial Ooze Monkey 4 - Ah-choo"
|
||||||
|
W1L2Gornif = "1-2 Primordial Ooze Monkey 5 - Gornif"
|
||||||
|
W1L2Tyrone = "1-2 Primordial Ooze Monkey 6 - Tyrone"
|
||||||
|
W1L3Scotty = "1-3 Molten Lava Monkey 1 - Scotty"
|
||||||
|
W1L3Coco = "1-3 Molten Lava Monkey 2 - Coco"
|
||||||
|
W1L3JThomas = "1-3 Molten Lava Monkey 3 - J. Thomas"
|
||||||
|
W1L3Mattie = "1-3 Molten Lava Monkey 4 - Mattie"
|
||||||
|
W1L3Barney = "1-3 Molten Lava Monkey 5 - Barney"
|
||||||
|
W1L3Rocky = "1-3 Molten Lava Monkey 6 - Rocky"
|
||||||
|
W1L3Moggan = "1-3 Molten Lava Monkey 7 - Moggan"
|
||||||
|
W2L1Marquez = "2-1 Thick Jungle Monkey 1 - Marquez"
|
||||||
|
W2L1Livinston = "2-1 Thick Jungle Monkey 2 - Livinston"
|
||||||
|
W2L1George = "2-1 Thick Jungle Monkey 3 - George"
|
||||||
|
W2L1Maki = "2-1 Thick Jungle Monkey 4 - Maki"
|
||||||
|
W2L1Herb = "2-1 Thick Jungle Monkey 5 - Herb"
|
||||||
|
W2L1Dilweed = "2-1 Thick Jungle Monkey 6 - Dilweed"
|
||||||
|
W2L1Mitong = "2-1 Thick Jungle Monkey 7 - Mitong"
|
||||||
|
W2L1Stoddy = "2-1 Thick Jungle Monkey 8 - Stoddy"
|
||||||
|
W2L1Nasus = "2-1 Thick Jungle Monkey 9 - Nasus"
|
||||||
|
W2L1Selur = "2-1 Thick Jungle Monkey 10 - Selur"
|
||||||
|
W2L1Elehcim = "2-1 Thick Jungle Monkey 11 - Elehcim"
|
||||||
|
W2L1Gonzo = "2-1 Thick Jungle Monkey 12 - Gonzo"
|
||||||
|
W2L1Alphonse = "2-1 Thick Jungle Monkey 13 - Alphonse"
|
||||||
|
W2L1Zanzibar = "2-1 Thick Jungle Monkey 14 - Zanzibar"
|
||||||
|
W2L2Mooshy = "2-2 Dark Ruins Monkey 1 - Mooshy"
|
||||||
|
W2L2Kyle = "2-2 Dark Ruins Monkey 2 - Kyle"
|
||||||
|
W2L2Cratman = "2-2 Dark Ruins Monkey 3 - Cratman"
|
||||||
|
W2L2Nuzzy = "2-2 Dark Ruins Monkey 4 - Nuzzy"
|
||||||
|
W2L2Mav = "2-2 Dark Ruins Monkey 5 - Mav"
|
||||||
|
W2L2Stan = "2-2 Dark Ruins Monkey 6 - Stan"
|
||||||
|
W2L2Bernt = "2-2 Dark Ruins Monkey 7 - Bernt"
|
||||||
|
W2L2Runt = "2-2 Dark Ruins Monkey 8 - Runt"
|
||||||
|
W2L2Hoolah = "2-2 Dark Ruins Monkey 9 - Hoolah"
|
||||||
|
W2L2Papou = "2-2 Dark Ruins Monkey 10 - Papou"
|
||||||
|
W2L2Kenny = "2-2 Dark Ruins Monkey 11 - Kenny"
|
||||||
|
W2L2Trance = "2-2 Dark Ruins Monkey 12 - Trance"
|
||||||
|
W2L2Chino = "2-2 Dark Ruins Monkey 13 - Chino"
|
||||||
|
W2L3Troopa = "2-3 Cryptic Relics Monkey 1 - Troopa"
|
||||||
|
W2L3Spanky = "2-3 Cryptic Relics Monkey 2 - Spanky"
|
||||||
|
W2L3Stymie = "2-3 Cryptic Relics Monkey 3 - Stymie"
|
||||||
|
W2L3Pally = "2-3 Cryptic Relics Monkey 4 - Pally"
|
||||||
|
W2L3Freeto = "2-3 Cryptic Relics Monkey 5 - Freeto"
|
||||||
|
W2L3Jesta = "2-3 Cryptic Relics Monkey 6 - Jesta"
|
||||||
|
W2L3Bazzle = "2-3 Cryptic Relics Monkey 7 - Bazzle"
|
||||||
|
W2L3Crash = "2-3 Cryptic Relics Monkey 8 - Crash"
|
||||||
|
W4L1CoolBlue = "4-1 Crabby Beach Monkey 1 - Cool Blue"
|
||||||
|
W4L1Sandy = "4-1 Crabby Beach Monkey 2 - Sandy"
|
||||||
|
W4L1ShellE = "4-1 Crabby Beach Monkey 3 - Shell E."
|
||||||
|
W4L1Gidget = "4-1 Crabby Beach Monkey 4 - Gidget"
|
||||||
|
W4L1Shaka = "4-1 Crabby Beach Monkey 5 - Shaka"
|
||||||
|
W4L1MaxMahalo = "4-1 Crabby Beach Monkey 6 - MaxMahalo"
|
||||||
|
W4L1Moko = "4-1 Crabby Beach Monkey 7 - Moko"
|
||||||
|
W4L1Puka = "4-1 Crabby Beach Monkey 8 - Puka"
|
||||||
|
W4L2Chip = "4-2 Coral Cave Monkey 1 - Chip"
|
||||||
|
W4L2Oreo = "4-2 Coral Cave Monkey 2 - Oreo"
|
||||||
|
W4L2Puddles = "4-2 Coral Cave Monkey 3 - Puddles"
|
||||||
|
W4L2Kalama = "4-2 Coral Cave Monkey 4 - Kalama"
|
||||||
|
W4L2Iz = "4-2 Coral Cave Monkey 5 - Iz"
|
||||||
|
W4L2Jux = "4-2 Coral Cave Monkey 6 - Jux"
|
||||||
|
W4L2BongBong = "4-2 Coral Cave Monkey 7 - Bong-Bong"
|
||||||
|
W4L2Pickles = "4-2 Coral Cave Monkey 8 - Pickles"
|
||||||
|
W4L3Stuw = "4-3 Dexters Island Monkey 1 - Stuw"
|
||||||
|
W4L3TonTon = "4-3 Dexters Island Monkey 2 - Ton Ton"
|
||||||
|
W4L3Murky = "4-3 Dexters Island Monkey 3 - Murky"
|
||||||
|
W4L3Howeerd = "4-3 Dexters Island Monkey 4 - Howeerd"
|
||||||
|
W4L3Robbin = "4-3 Dexters Island Monkey 5 - Robbin"
|
||||||
|
W4L3Jakkee = "4-3 Dexters Island Monkey 6 - Jakkee"
|
||||||
|
W4L3Frederic = "4-3 Dexters Island Monkey 7 - Frederic"
|
||||||
|
W4L3Baba = "4-3 Dexters Island Monkey 8 - Baba"
|
||||||
|
W4L3Mars = "4-3 Dexters Island Monkey 9 - Mars"
|
||||||
|
W4L3Horke = "4-3 Dexters Island Monkey 10 - Horke"
|
||||||
|
W4L3Quirck = "4-3 Dexters Island Monkey 11 - Quirck"
|
||||||
|
W5L1Popcicle = "5-1 Snowy Mammoth Monkey 1 - Popcicle"
|
||||||
|
W5L1Iced = "5-1 Snowy Mammoth Monkey 2 - Iced"
|
||||||
|
W5L1Denggoy = "5-1 Snowy Mammoth Monkey 3 - Denggoy"
|
||||||
|
W5L1Skeens = "5-1 Snowy Mammoth Monkey 4 - Skeens"
|
||||||
|
W5L1Rickets = "5-1 Snowy Mammoth Monkey 5 - Rickets"
|
||||||
|
W5L1Chilly = "5-1 Snowy Mammoth Monkey 6 - Chilly"
|
||||||
|
W5L2Storm = "5-2 Frosty Retreat Monkey 1 - Storm"
|
||||||
|
W5L2Qube = "5-2 Frosty Retreat Monkey 2 - Qube"
|
||||||
|
W5L2Gash = "5-2 Frosty Retreat Monkey 3 - Gash"
|
||||||
|
W5L2Kundra = "5-2 Frosty Retreat Monkey 4 - Kundra"
|
||||||
|
W5L2Shadow = "5-2 Frosty Retreat Monkey 5 - Shadow"
|
||||||
|
W5L2Ranix = "5-2 Frosty Retreat Monkey 6 - Ranix"
|
||||||
|
W5L2Sticky = "5-2 Frosty Retreat Monkey 7 - Sticky"
|
||||||
|
W5L2Sharpe = "5-2 Frosty Retreat Monkey 8 - Sharpe"
|
||||||
|
W5L2Droog = "5-2 Frosty Retreat Monkey 9 - Droog"
|
||||||
|
W5L3Punky = "5-3 Hot Springs Monkey 1 - Punky"
|
||||||
|
W5L3Ameego = "5-3 Hot Springs Monkey 2 - Ameego"
|
||||||
|
W5L3Roti = "5-3 Hot Springs Monkey 3 - Roti"
|
||||||
|
W5L3Dissa = "5-3 Hot Springs Monkey 4 - Dissa"
|
||||||
|
W5L3Yoky = "5-3 Hot Springs Monkey 5 - Yoky"
|
||||||
|
W5L3Jory = "5-3 Hot Springs Monkey 6 - Jory"
|
||||||
|
W5L3Crank = "5-3 Hot Springs Monkey 7 - Crank"
|
||||||
|
W5L3Claxter = "5-3 Hot Springs Monkey 8 - Claxter"
|
||||||
|
W5L3Looza = "5-3 Hot Springs Monkey 9 - Looza"
|
||||||
|
W7L1Taku = "7-1 Sushi Temple Monkey 1 - Taku"
|
||||||
|
W7L1Rocka = "7-1 Sushi Temple Monkey 2 - Rocka"
|
||||||
|
W7L1Maralea = "7-1 Sushi Temple Monkey 3 - Mara-lea"
|
||||||
|
W7L1Wog = "7-1 Sushi Temple Monkey 4 - Wog"
|
||||||
|
W7L1Long = "7-1 Sushi Temple Monkey 5 - Long"
|
||||||
|
W7L1Mayi = "7-1 Sushi Temple Monkey 6 - Mayi"
|
||||||
|
W7L1Owyang = "7-1 Sushi Temple Monkey 7 - Owyang"
|
||||||
|
W7L1QuelTin = "7-1 Sushi Temple Monkey 8 - Quel Tin"
|
||||||
|
W7L1Phaldo = "7-1 Sushi Temple Monkey 9 - Phaldo"
|
||||||
|
W7L1Voti = "7-1 Sushi Temple Monkey 10 - Voti"
|
||||||
|
W7L1Elly = "7-1 Sushi Temple Monkey 11 - Elly"
|
||||||
|
W7L1Chunky = "7-1 Sushi Temple Monkey 12 - Chunky"
|
||||||
|
W7L2Minky = "7-2 Wabi Sabi Wall Monkey 1 - Minky"
|
||||||
|
W7L2Zobbro = "7-2 Wabi Sabi Wall Monkey 2 - Zobbro"
|
||||||
|
W7L2Xeeto = "7-2 Wabi Sabi Wall Monkey 3 - Xeeto"
|
||||||
|
W7L2Moops = "7-2 Wabi Sabi Wall Monkey 4 - Moops"
|
||||||
|
W7L2Zanabi = "7-2 Wabi Sabi Wall Monkey 5 - Zanabi"
|
||||||
|
W7L2Buddha = "7-2 Wabi Sabi Wall Monkey 6 - Buddha"
|
||||||
|
W7L2Fooey = "7-2 Wabi Sabi Wall Monkey 7 - Fooey"
|
||||||
|
W7L2Doxs = "7-2 Wabi Sabi Wall Monkey 8 - Doxs"
|
||||||
|
W7L2Kong = "7-2 Wabi Sabi Wall Monkey 9 - Kong"
|
||||||
|
W7L2Phool = "7-2 Wabi Sabi Wall Monkey 10 - Phool"
|
||||||
|
W7L3Naners = "7-3 Crumbling Castle Monkey 1 - Naners"
|
||||||
|
W7L3Robart = "7-3 Crumbling Castle Monkey 2 - Robart"
|
||||||
|
W7L3Neeners = "7-3 Crumbling Castle Monkey 3 - Neeners"
|
||||||
|
W7L3Gustav = "7-3 Crumbling Castle Monkey 4 - Gustav"
|
||||||
|
W7L3Wilhelm = "7-3 Crumbling Castle Monkey 5 - Willhelm"
|
||||||
|
W7L3Emmanuel = "7-3 Crumbling Castle Monkey 6 - Emmanuel"
|
||||||
|
W7L3SirCutty = "7-3 Crumbling Castle Monkey 7 - Sir Cutty"
|
||||||
|
W7L3Calligan = "7-3 Crumbling Castle Monkey 8 - Calligan"
|
||||||
|
W7L3Castalist = "7-3 Crumbling Castle Monkey 9 - Castalist"
|
||||||
|
W7L3Deveneom = "7-3 Crumbling Castle Monkey 10 - Deveneom"
|
||||||
|
W7L3Igor = "7-3 Crumbling Castle Monkey 11 - Igor"
|
||||||
|
W7L3Charles = "7-3 Crumbling Castle Monkey 12 - Charles"
|
||||||
|
W7L3Astur = "7-3 Crumbling Castle Monkey 13 - Astur"
|
||||||
|
W7L3Kilserack = "7-3 Crumbling Castle Monkey 14 - Kilserack"
|
||||||
|
W7L3Ringo = "7-3 Crumbling Castle Monkey 15 - Ringo"
|
||||||
|
W7L3Densil = "7-3 Crumbling Castle Monkey 16 - Densil"
|
||||||
|
W7L3Figero = "7-3 Crumbling Castle Monkey 17 - Figero"
|
||||||
|
W7L3Fej = "7-3 Crumbling Castle Monkey 18 - Fej"
|
||||||
|
W7L3Joey = "7-3 Crumbling Castle Monkey 19 - Joey"
|
||||||
|
W7L3Donqui = "7-3 Crumbling Castle Monkey 20 - Donqui"
|
||||||
|
W7L3Boss73 = "7-3 Crumbling Castle Boss"
|
||||||
|
W8L1Kaine = "8-1 City Park Monkey 1 - Kaine"
|
||||||
|
W8L1Jaxx = "8-1 City Park Monkey 2 - Jaxx"
|
||||||
|
W8L1Gehry = "8-1 City Park Monkey 3 - Gehry"
|
||||||
|
W8L1Alcatraz = "8-1 City Park Monkey 4 - Alcatraz"
|
||||||
|
W8L1Tino = "8-1 City Park Monkey 5 - Tino"
|
||||||
|
W8L1QBee = "8-1 City Park Monkey 6 - Q. Bee"
|
||||||
|
W8L1McManic = "8-1 City Park Monkey 7 - McManic"
|
||||||
|
W8L1Dywan = "8-1 City Park Monkey 8 - Dywan"
|
||||||
|
W8L1CKHutch = "8-1 City Park Monkey 9 - CK Hutch"
|
||||||
|
W8L1Winky = "8-1 City Park Monkey 10 - Winky"
|
||||||
|
W8L1BLuv = "8-1 City Park Monkey 11 - B Luv"
|
||||||
|
W8L1Camper = "8-1 City Park Monkey 12 - Camper"
|
||||||
|
W8L1Huener = "8-1 City Park Monkey 13 - Huener"
|
||||||
|
W8L2BigShow = "8-2 Specters Factory Monkey 1 - Big Show"
|
||||||
|
W8L2Dreos = "8-2 Specters Factory Monkey 2 - Dreos"
|
||||||
|
W8L2Reznor = "8-2 Specters Factory Monkey 3 - Reznor"
|
||||||
|
W8L2Urkel = "8-2 Specters Factory Monkey 4 - Urkel"
|
||||||
|
W8L2VanillaS = "8-2 Specters Factory Monkey 5 - Vanilla S"
|
||||||
|
W8L2Radd = "8-2 Specters Factory Monkey 6 - Radd"
|
||||||
|
W8L2Shimbo = "8-2 Specters Factory Monkey 7 - Shimbo"
|
||||||
|
W8L2Hurt = "8-2 Specters Factory Monkey 8 - Hurt"
|
||||||
|
W8L2String = "8-2 Specters Factory Monkey 9 - String"
|
||||||
|
W8L2Khamo = "8-2 Specters Factory Monkey 10 - Khamo"
|
||||||
|
W8L3Fredo = "8-3 TV Tower Monkey 1 - Fredo"
|
||||||
|
W8L3Charlee = "8-3 TV Tower Monkey 2 - Charlee"
|
||||||
|
W8L3Mach3 = "8-3 TV Tower Monkey 3 - Mach 3"
|
||||||
|
W8L3Tortuss = "8-3 TV Tower Monkey 4 - Tortuss"
|
||||||
|
W8L3Manic = "8-3 TV Tower Monkey 5 - Manic"
|
||||||
|
W8L3Ruptdis = "8-3 TV Tower Monkey 6 - Ruptdis"
|
||||||
|
W8L3Eighty7 = "8-3 TV Tower Monkey 7 - Eighty 7"
|
||||||
|
W8L3Danio = "8-3 TV Tower Monkey 8 - Danio"
|
||||||
|
W8L3Roosta = "8-3 TV Tower Monkey 9 - Roosta"
|
||||||
|
W8L3Tellis = "8-3 TV Tower Monkey 10 - Tellis"
|
||||||
|
W8L3Whack = "8-3 TV Tower Monkey 11 - Whack"
|
||||||
|
W8L3Frostee = "8-3 TV Tower Monkey 12 - Frostee"
|
||||||
|
W8L3Boss83 = "8-3 TV Tower Boss"
|
||||||
|
W9L1Goopo = "9-1 Monkey Madness Monkey 1 - Goopo"
|
||||||
|
W9L1Porto = "9-1 Monkey Madness Monkey 2 - Porto"
|
||||||
|
W9L1Slam = "9-1 Monkey Madness Monkey 3 - Slam"
|
||||||
|
W9L1Junk = "9-1 Monkey Madness Monkey 4 - Junk"
|
||||||
|
W9L1Crib = "9-1 Monkey Madness Monkey 5 - Crib"
|
||||||
|
W9L1Nak = "9-1 Monkey Madness Monkey 6 - Nak"
|
||||||
|
W9L1Cloy = "9-1 Monkey Madness Monkey 7 - Cloy"
|
||||||
|
W9L1Shaw = "9-1 Monkey Madness Monkey 8 - Shaw"
|
||||||
|
W9L1Flea = "9-1 Monkey Madness Monkey 9 - Flea"
|
||||||
|
W9L1Schafette = "9-1 Monkey Madness Monkey 10 - Schafette"
|
||||||
|
W9L1Donovan = "9-1 Monkey Madness Monkey 11 - Donovan"
|
||||||
|
W9L1Laura = "9-1 Monkey Madness Monkey 12 - Laura"
|
||||||
|
W9L1Uribe = "9-1 Monkey Madness Monkey 13 - Uribe"
|
||||||
|
W9L1Gordo = "9-1 Monkey Madness Monkey 14 - Gordo"
|
||||||
|
W9L1Raeski = "9-1 Monkey Madness Monkey 15 - Raeski"
|
||||||
|
W9L1Poopie = "9-1 Monkey Madness Monkey 16 - Poo-pie"
|
||||||
|
W9L1Teacup = "9-1 Monkey Madness Monkey 17 - Teacup"
|
||||||
|
W9L1Shine = "9-1 Monkey Madness Monkey 18 - Shine"
|
||||||
|
W9L1Wrench = "9-1 Monkey Madness Monkey 19 - Wrench"
|
||||||
|
W9L1Bronson = "9-1 Monkey Madness Monkey 20 - Bronson"
|
||||||
|
W9L1Bungee = "9-1 Monkey Madness Monkey 21 - Bungee"
|
||||||
|
W9L1Carro = "9-1 Monkey Madness Monkey 22 - Carro"
|
||||||
|
W9L1Carlito = "9-1 Monkey Madness Monkey 23 - Carlito"
|
||||||
|
W9L1BG = "9-1 Monkey Madness Monkey 24 - BG"
|
||||||
|
Boss73 = "7-3 Crumbling Castle - Boss"
|
||||||
|
Boss83 = "8-3 TV Tower - Boss"
|
||||||
|
W9L1Professor = "9-1 Monkey Madness - Rescue Professor"
|
||||||
|
W9L1Jake = "9-1 Monkey Madness - Defeat Jake"
|
||||||
|
Specter = "9-1 Monkey Madness - Specter"
|
||||||
|
Specter2 = "9-2 Peak Point Matrix - Specter"
|
||||||
|
Coin1 = "1-1 Fossil Field Coin - Main"
|
||||||
|
Coin2 = "1-2 Primordial Ooze Coin - Main"
|
||||||
|
Coin3 = "1-3 Molten Lava Coin - Entry"
|
||||||
|
Coin6 = "2-1 Thick Jungle Coin - Entry"
|
||||||
|
Coin7 = "2-1 Thick Jungle Coin - Mushroom Area"
|
||||||
|
Coin8 = "2-1 Thick Jungle Coin - Fish Room"
|
||||||
|
Coin9 = "2-1 Thick Jungle Coin - Tent/Vine Room"
|
||||||
|
Coin11 = "2-2 Dark Ruins Coin - Outside"
|
||||||
|
Coin12 = "2-2 Dark Ruins Coin - Fan Basement"
|
||||||
|
Coin13 = "2-2 Dark Ruins Coin - Obelisk Inside"
|
||||||
|
Coin14 = "2-2 Dark Ruins Coin - Water Basement"
|
||||||
|
Coin17 = "2-3 Cryptic Relics Coin - Main Ruins"
|
||||||
|
Coin19A = "3-1 Stadium Attack Coin 1"
|
||||||
|
Coin19B = "3-1 Stadium Attack Coin 2"
|
||||||
|
Coin19C = "3-1 Stadium Attack Coin 3"
|
||||||
|
Coin19D = "3-1 Stadium Attack Coin 4"
|
||||||
|
Coin19E = "3-1 Stadium Attack Coin 5"
|
||||||
|
Coin21 = "4-1 Crabby Beach Coin - Second Room"
|
||||||
|
Coin23 = "4-2 Coral Cave Coin - Second Room"
|
||||||
|
Coin24 = "4-3 Dexters Island Coin - Outside"
|
||||||
|
Coin25 = "4-3 Dexters Island Coin - Stomach"
|
||||||
|
Coin28 = "4-3 Dexters Island Coin - Slide Room"
|
||||||
|
Coin29 = "5-1 Snowy Mammoth Coin - Main"
|
||||||
|
Coin30 = "5-2 Frosty Retreat Coin - Entry"
|
||||||
|
Coin31 = "5-2 Frosty Retreat Coin - Water Room"
|
||||||
|
Coin32 = "5-2 Frosty Retreat Coin - Caverns"
|
||||||
|
Coin34 = "5-3 Hot Springs Coin - Hot Spring"
|
||||||
|
Coin35 = "5-3 Hot Springs Coin - Polar Bear Cave"
|
||||||
|
Coin36A = "6-1 Gladiator Attack Coin 1"
|
||||||
|
Coin36B = "6-1 Gladiator Attack Coin 2"
|
||||||
|
Coin36C = "6-1 Gladiator Attack Coin 3"
|
||||||
|
Coin36D = "6-1 Gladiator Attack Coin 4"
|
||||||
|
Coin36E = "6-1 Gladiator Attack Coin 5"
|
||||||
|
Coin37 = "7-1 Sushi Temple Coin - Outside"
|
||||||
|
Coin38 = "7-1 Sushi Temple Coin - Temple"
|
||||||
|
Coin39 = "7-1 Sushi Temple Coin - Well"
|
||||||
|
Coin40 = "7-2 Wabi Sabi Wall Coin - First Room"
|
||||||
|
Coin41 = "7-2 Wabi Sabi Wall Coin - Gong Room"
|
||||||
|
Coin44 = "7-2 Wabi Sabi Wall Coin - Barrel Room"
|
||||||
|
Coin45 = "7-3 Crumbling Castle Coin - Outside"
|
||||||
|
Coin46 = "7-3 Crumbling Castle Coin - Castle Main"
|
||||||
|
Coin49 = "7-3 Crumbling Castle Coin - Button Room"
|
||||||
|
Coin50 = "7-3 Crumbling Castle Coin - Elevator Room"
|
||||||
|
Coin53 = "8-1 City Park Coin - Outside"
|
||||||
|
Coin54 = "8-1 City Park Coin - Sewers Front"
|
||||||
|
Coin55 = "8-1 City Park Coin - Barrel Room"
|
||||||
|
Coin58 = "8-2 Specters Factory Coin - RC Car Room"
|
||||||
|
Coin59 = "8-2 Specters Factory Coin - Lava Room"
|
||||||
|
Coin64 = "8-3 TV Tower Coin - Water Basement"
|
||||||
|
Coin66 = "8-3 TV Tower Coin - Tank Room"
|
||||||
|
Coin73 = "9-1 Monkey Madness Coin - Coaster (Room 1)"
|
||||||
|
Coin74 = "9-1 Monkey Madness Coin - Coaster (Room 2)"
|
||||||
|
Coin75 = "9-1 Monkey Madness Coin - Haunted House"
|
||||||
|
Coin77 = "9-1 Monkey Madness Coin - Western Land"
|
||||||
|
Coin78 = "9-1 Monkey Madness Coin - Crater"
|
||||||
|
Coin79 = "9-1 Monkey Madness Coin - Outside Castle"
|
||||||
|
Coin80 = "9-1 Monkey Madness Coin - Castle Main"
|
||||||
|
Coin82 = "9-1 Monkey Madness Coin - Climb (Outside)"
|
||||||
|
Coin84 = "9-1 Monkey Madness Coin - Monkey Head"
|
||||||
|
Coin85 = "9-1 Monkey Madness Coin - Side Entry"
|
||||||
|
Mailbox1 = "1-1 Fossil Field Mailbox - Stun Club Tutorial (Main)"
|
||||||
|
Mailbox2 = "1-1 Fossil Field Mailbox - Switch Gadgets Tutorial (Main)"
|
||||||
|
Mailbox3 = "1-1 Fossil Field Mailbox - Don't Toss Your Cookies! (Main)"
|
||||||
|
Mailbox4 = "1-2 Primordial Ooze Mailbox - Dive Reminder (Main)"
|
||||||
|
Mailbox5 = "1-2 Primordial Ooze Mailbox - Climb a Tree (Main)"
|
||||||
|
Mailbox6 = "1-2 Primordial Ooze Mailbox - Camera Tutorial (Main)"
|
||||||
|
Mailbox7 = "1-2 Primordial Ooze Mailbox - Crawl Tutorial (Main)"
|
||||||
|
Mailbox8 = "1-3 Molten Lava Mailbox - Surrounded by a Cliff (Entry)"
|
||||||
|
Mailbox9 = "1-3 Molten Lava Mailbox - Energy Chips (Entry)"
|
||||||
|
Mailbox10 = "1-3 Molten Lava Mailbox - The T-Rex is Aggressive (Volcano)"
|
||||||
|
Mailbox11 = "1-3 Molten Lava Mailbox - Did I Give You That Yet? (Triceratops)"
|
||||||
|
Mailbox12 = "1-3 Molten Lava Mailbox - Hit the Bombs! (Triceratops)"
|
||||||
|
Mailbox13 = "2-1 Thick Jungle Mailbox - Monkeys are Hiding (Entry)"
|
||||||
|
Mailbox14 = "2-1 Thick Jungle Mailbox - Monkey Camera Tutorial (Entry)"
|
||||||
|
Mailbox15 = "2-1 Thick Jungle Mailbox - A Flying Gadget (Mushroom Area)"
|
||||||
|
Mailbox16 = "2-1 Thick Jungle Mailbox - Completely Cleared (Mushroom Area)"
|
||||||
|
Mailbox17 = "2-1 Thick Jungle Mailbox - You \"Must\" Rowboat (Fish Room)"
|
||||||
|
Mailbox18 = "2-1 Thick Jungle Mailbox - Come Back Later! (Fish Room)"
|
||||||
|
Mailbox19 = "2-1 Thick Jungle Mailbox - A Strange Device (Fish Room)"
|
||||||
|
Mailbox20 = "2-1 Thick Jungle Mailbox - Don't Hurt the Plants. (Tent/Vine Room)"
|
||||||
|
Mailbox21 = "2-1 Thick Jungle Mailbox - Find Specter Coins (Boulder Room)"
|
||||||
|
Mailbox22 = "2-2 Dark Ruins Mailbox - Monkey Attributes (Outside)"
|
||||||
|
Mailbox23 = "2-2 Dark Ruins Mailbox - Out of Place Block (Outside)"
|
||||||
|
Mailbox24 = "2-2 Dark Ruins Mailbox - Difficult for Beginners (Outside)"
|
||||||
|
Mailbox25 = "2-2 Dark Ruins Mailbox - Hmmm... Red Switch (Outside)"
|
||||||
|
Mailbox26 = "2-2 Dark Ruins Mailbox - It'll Blow You Away (Fan Basement)"
|
||||||
|
Mailbox27 = "2-2 Dark Ruins Mailbox - D-pad Camera Tutorial (Fan Basement)"
|
||||||
|
Mailbox28 = "2-2 Dark Ruins Mailbox - Monkeys Lights (Obelisk Inside)"
|
||||||
|
Mailbox29 = "2-3 Cryptic Relics Mailbox - Get Rid of the Enemy (Outside)"
|
||||||
|
Mailbox30 = "2-3 Cryptic Relics Mailbox - Don't Forget Your Sling (Outside)"
|
||||||
|
Mailbox31 = "2-3 Cryptic Relics Mailbox - Back Camera Tutorial (Main Ruins)"
|
||||||
|
Mailbox32 = "2-3 Cryptic Relics Mailbox - Remember to Save (Main Ruins)"
|
||||||
|
Mailbox33 = "2-3 Cryptic Relics Mailbox - Come Back Later! (Pillar Room)"
|
||||||
|
Mailbox34 = "4-1 Crabby Beach Mailbox - Hip Drop Attack (First Room)"
|
||||||
|
Mailbox35 = "4-1 Crabby Beach Mailbox - Monkey Lamp Tutorial (First Room)"
|
||||||
|
Mailbox36 = "4-1 Crabby Beach Mailbox - Dangerous to Fall (Second Room)"
|
||||||
|
Mailbox37 = "4-2 Coral Cave Mailbox - Yellow Pants = Normal (Second Room)"
|
||||||
|
Mailbox38 = "4-2 Coral Cave Mailbox - Red Pants = Strong (Second Room)"
|
||||||
|
Mailbox39 = "4-3 Dexters Island Mailbox - I Named Him Dexter (Outside)"
|
||||||
|
Mailbox40 = "4-3 Dexters Island Mailbox - Blue Pants = Very Fast (Outside)"
|
||||||
|
Mailbox41 = "4-3 Dexters Island Mailbox - Don't Get Lost! (Slide Room)"
|
||||||
|
Mailbox42 = "4-3 Dexters Island Mailbox - Normal Pellet Scare (Gallery/Boulder)"
|
||||||
|
Mailbox43 = "5-1 Snowy Mammoth Mailbox - Severely Cold Sea (Main)"
|
||||||
|
Mailbox44 = "5-1 Snowy Mammoth Mailbox - You Are Lost! (Main)"
|
||||||
|
Mailbox45 = "5-1 Snowy Mammoth Mailbox - This Hazardous Thing (Main)"
|
||||||
|
Mailbox46 = "5-2 Frosty Retreat Mailbox - Select Button Tutorial (Caverns)"
|
||||||
|
Mailbox47 = "5-3 Hot Springs Mailbox - Ice Bridge Ahead (Outside)"
|
||||||
|
Mailbox48 = "5-3 Hot Springs Mailbox - Wonderful Hot Springs!! (Hot Spring)"
|
||||||
|
Mailbox49 = "5-3 Hot Springs Mailbox - Blocked by an Ice Column (Polar Bear Cave)"
|
||||||
|
Mailbox50 = "7-1 Sushi Temple Mailbox - Green Pants = Great Vision (Temple)"
|
||||||
|
Mailbox51 = "7-1 Sushi Temple Mailbox - Light Blue Pants = Gentle (Temple)"
|
||||||
|
Mailbox52 = "7-1 Sushi Temple Mailbox - Rest and Relax (Well)"
|
||||||
|
Mailbox53 = "7-2 Wabi Sabi Wall Mailbox - White Pants = Very Alert (Gong Room)"
|
||||||
|
Mailbox54 = "7-2 Wabi Sabi Wall Mailbox - Guided Pellet Tutorial (Middle Room)"
|
||||||
|
Mailbox55 = "7-2 Wabi Sabi Wall Mailbox - Big Cogwheel (Middle Room)"
|
||||||
|
Mailbox56 = "7-2 Wabi Sabi Wall Mailbox - Black Pants = Dangerous (Obstacle Course)"
|
||||||
|
Mailbox57 = "7-3 Crumbling Castle Mailbox - Think Clearly (Outside)"
|
||||||
|
Mailbox58 = "8-2 Specters Factory Mailbox - Hop In The Tank (Outside)"
|
||||||
|
Mailbox59 = "9-1 Monkey Madness Mailbox - The Terror Coaster (Coaster Entry)"
|
||||||
|
Mailbox60 = "Time Station Mailbox - Welcome! (Main)"
|
||||||
|
Mailbox61 = "Time Station Mailbox - Save Station (Main)"
|
||||||
|
Mailbox62 = "Time Station Mailbox - Mini Game Corner (Mini Game Corner)"
|
||||||
|
Mailbox63 = "Time Station Mailbox - Training Space (Gadget Training)"
|
||||||
|
|
||||||
|
|
||||||
|
class AEDoor(Enum):
|
||||||
|
TIME_ENTRY = "Time Station: Entry"
|
||||||
|
TIME_MAIN_TRAINING = "Time Station: Main - Gadget Training"
|
||||||
|
TIME_MAIN_MINIGAME = "Time Station: Main - Minigame Corner"
|
||||||
|
TIME_TRAINING_MAIN = "Time Station: Gadget Training - Main"
|
||||||
|
TIME_MINIGAME_MAIN = "Time Station: Minigame Corner - Main"
|
||||||
|
TIME_TRAINING_WATERNET = "Time Station: Gadget Training - Water Net"
|
||||||
|
TIME_TRAINING_RADAR = "Time Station: Gadget Training - Radar"
|
||||||
|
TIME_TRAINING_SLING = "Time Station: Gadget Training - Slingback Shooter"
|
||||||
|
TIME_TRAINING_HOOP = "Time Station: Gadget Training - Super Hoop"
|
||||||
|
TIME_TRAINING_FLYER = "Time Station: Gadget Training - Sky Flyer"
|
||||||
|
TIME_TRAINING_CAR = "Time Station: Gadget Training - R.C. Car"
|
||||||
|
TIME_TRAINING_PUNCH = "Time Station: Gadget Training - Magic Punch"
|
||||||
|
FF_ENTRY = "Fossil Field: Main"
|
||||||
|
PO_ENTRY = "Primordial Ooze: Main"
|
||||||
|
ML_ENTRY = "Molten Lava: Entry"
|
||||||
|
ML_ENTRY_VOLCANO = "Molten Lava: Entry - Volcano"
|
||||||
|
ML_ENTRY_TRICERATOPS = "Molten Lava: Entry - Triceratops"
|
||||||
|
ML_VOLCANO_ENTRY = "Molten Lava: Volcano - Entry"
|
||||||
|
ML_TRICERATOPS_ENTRY = "Molten Lava: Triceratops - Entry"
|
||||||
|
TJ_ENTRY = "Thick Jungle: Entry"
|
||||||
|
TJ_ENTRY_MUSHROOM = "Thick Jungle: Entry - Mushroom"
|
||||||
|
TJ_ENTRY_FISH = "Thick Jungle: Entry - Fish"
|
||||||
|
TJ_ENTRY_BOULDER = "Thick Jungle: Entry - Boulder"
|
||||||
|
TJ_MUSHROOM_ENTRY = "Thick Jungle: Mushroom - Entry"
|
||||||
|
TJ_FISH_ENTRY = "Thick Jungle: Fish Room - Entry"
|
||||||
|
TJ_FISH_TENT = "Thick Jungle: Fish Room - Tent/Vine Room"
|
||||||
|
TJ_TENT_FISH = "Thick Jungle: Tent/Vine Room - Fish Room"
|
||||||
|
TJ_TENT_BOULDER = "Thick Jungle: Tent/Vine Room - Boulder Room"
|
||||||
|
TJ_BOULDER_ENTRY = "Thick Jungle: Boulder Room - Entry"
|
||||||
|
TJ_BOULDER_TENT = "Thick Jungle: Boulder Room - Tent/Vine Room"
|
||||||
|
TJ_MUSHROOMMAIN = "Thick Jungle: Mushroom - Main Area" # Helper Region
|
||||||
|
TJ_FISHBOAT = "Thick Jungle: Fish Room - Boat Access" # Helper Region
|
||||||
|
DR_ENTRY = "Dark Ruins: Entry"
|
||||||
|
DR_OUTSIDE_FENCE = "Dark Ruins: Outside - Fan Basement (Fence)"
|
||||||
|
DR_OUTSIDE_HOLE = "Dark Ruins: Outside - Fan Basement (Hole)"
|
||||||
|
DR_OUTSIDE_OBELISK_BOTTOM = "Dark Ruins: Outside - Obelisk Inside (Bottom)"
|
||||||
|
DR_OUTSIDE_OBELISK_TOP = "Dark Ruins: Outside - Obelisk Inside (Top)"
|
||||||
|
DR_OUTSIDE_WATER_BUTTON = "Dark Ruins: Outside - Water Room (Side)"
|
||||||
|
DR_OUTSIDE_WATER_LEDGE = "Dark Ruins: Outside - Water Room (Ledge)"
|
||||||
|
DR_FAN_OUTSIDE_FENCE = "Dark Ruins: Fan Basement - Outside (Fence)"
|
||||||
|
DR_FAN_OUTSIDE_HOLE = "Dark Ruins: Fan Basement - Outside (Hole)"
|
||||||
|
DR_OBELISK_BOTTOM = "Dark Ruins: Obelisk Inside (Bottom)"
|
||||||
|
DR_OBELISK_TOP = "Dark Ruins: Obelisk Inside (Top)"
|
||||||
|
DR_WATER_SIDE = "Dark Ruins: Water Basement (Side)"
|
||||||
|
DR_WATER_LEDGE = "Dark Ruins: Water Basement (Ledge)"
|
||||||
|
CR_ENTRY = "Cryptic Relics: Entry"
|
||||||
|
CR_ENTRY_SIDE_ROOM = "Cryptic Relics: Entry - Side Room"
|
||||||
|
CR_ENTRY_MAIN_RUINS = "Cryptic Relics: Entry - Main Ruins"
|
||||||
|
CR_SIDE_ROOM_ENTRY = "Cryptic Relics: Side Room - Entry"
|
||||||
|
CR_MAIN_RUINS_ENTRY = "Cryptic Relics: Main Ruins - Entry"
|
||||||
|
CR_MAIN_RUINS_PILLAR_ROOM = "Cryptic Relics: Main Ruins - Pillar Room"
|
||||||
|
CR_PILLAR_ROOM_MAIN_RUINS = "Cryptic Relics: Pillar Room - Main Ruins"
|
||||||
|
CR_ENTRYOBA = "Cryptic Relics: Entry - Outside Button Area" # Helper Region
|
||||||
|
SA_ENTRY = "Stadium Attack: Entry"
|
||||||
|
SA_COMPLETE = "Stadium Attack: Complete"
|
||||||
|
CB_ENTRY = "Crabby Beach: Entry"
|
||||||
|
CB_ENTRY_SECOND_ROOM = "Crabby Beach: Entry - Second Room"
|
||||||
|
CB_SECOND_ROOM_ENTRY = "Crabby Beach: Second Room - Entry"
|
||||||
|
CCAVE_ENTRY = "Coral Cave: Entry"
|
||||||
|
CCAVE_ENTRY_SECOND_ROOM = "Coral Cave: Entry - Second Room"
|
||||||
|
CCAVE_SECOND_ROOM_ENTRY = "Coral Cave: Second Room - Entry"
|
||||||
|
DI_ENTRY = "Dexter's Island: Entry"
|
||||||
|
DI_ENTRY_STOMACH = "Dexter's Island: Entry - Stomach"
|
||||||
|
DI_STOMACH_ENTRY = "Dexter's Island: Stomach - Entry"
|
||||||
|
DI_STOMACH_SLIDE_ROOM = "Dexter's Island: Stomach - Slide Room"
|
||||||
|
DI_SLIDE_ROOM_STOMACH = "Dexter's Island: Slide Room - Stomach"
|
||||||
|
DI_SLIDE_ROOM_GALLERY = "Dexter's Island: Slide Room - Gallery"
|
||||||
|
DI_SLIDE_ROOM_GALLERY_WATER = "Dexter's Island: Slide Room - Gallery (Water)"
|
||||||
|
DI_GALLERY_SLIDE_ROOM_TOP = "Dexter's Island: Gallery - Slide Room (Top)"
|
||||||
|
DI_GALLERY_SLIDE_ELEVATOR = "Dexter's Island: Gallery - Slide Room (Elevator)"
|
||||||
|
DI_GALLERY_TENTACLE = "Dexter's Island: Gallery - Tentacle Room"
|
||||||
|
DI_TENTACLE = "Dexter's Island: Tentacle Room"
|
||||||
|
DI_GALLERYBOULDER = "Dexter's Island: Gallery - Boulder Area" # Helper Region
|
||||||
|
SM_ENTRY = "Snowy Mammoth: Main"
|
||||||
|
FR_ENTRY = "Frosty Retreat: Entry"
|
||||||
|
FR_ENTRY_CAVERNS = "Frosty Retreat: Entry - Caverns"
|
||||||
|
FR_CAVERNS_ENTRY = "Frosty Retreat: Caverns - Entry"
|
||||||
|
FR_CAVERNS_WATER = "Frosty Retreat: Caverns - Water Room"
|
||||||
|
FR_WATER_CAVERNS = "Frosty Retreat: Water Room - Caverns"
|
||||||
|
HS_ENTRY = "Hot Springs: Entry"
|
||||||
|
HS_ENTRY_HOT_SPRING = "Hot Springs: Entry - Hot Springs"
|
||||||
|
HS_ENTRY_POLAR_BEAR_CAVE = "Hot Springs: Entry - Polar Bear Cave"
|
||||||
|
HS_HOT_SPRING = "Hot Springs: Hot Springs - Entry"
|
||||||
|
HS_POLAR_BEAR_CAVE = "Hot Springs: Polar Bear Cave - Entry"
|
||||||
|
GA_ENTRY = "Gladiator Attack: Entry"
|
||||||
|
GA_COMPLETE = "Gladiator Attack: Complete"
|
||||||
|
ST_ENTRY = "Sushi Temple: Entry"
|
||||||
|
ST_ENTRY_TEMPLE = "Sushi Temple: Entry - Temple"
|
||||||
|
ST_ENTRY_WELL = "Sushi Temple: Entry - Well"
|
||||||
|
ST_TEMPLE = "Sushi Temple: Temple - Entry"
|
||||||
|
ST_WELL = "Sushi Temple: Well - Entry"
|
||||||
|
WSW_ENTRY = "Wabi Sabi Wall: Entry"
|
||||||
|
WSW_ENTRY_GONG = "Wabi Sabi Wall: Entry - Gong Room"
|
||||||
|
WSW_GONG_ENTRY = "Wabi Sabi Wall: Gong Room - Entry"
|
||||||
|
WSW_GONG_MIDDLE = "Wabi Sabi Wall: Gong Room - Middle Room"
|
||||||
|
WSW_MIDDLE_GONG = "Wabi Sabi Wall: Middle Room - Gong Room"
|
||||||
|
WSW_MIDDLE_OBSTACLE = "Wabi Sabi Wall: Middle Room - Obstacle Course"
|
||||||
|
WSW_OBSTACLE_MIDDLE = "Wabi Sabi Wall: Obstacle Course - Middle Room"
|
||||||
|
WSW_OBSTACLE_BARREL = "Wabi Sabi Wall: Obstacle Course - Barrel Room"
|
||||||
|
WSW_BARREL_OBSTACLE = "Wabi Sabi Wall: Barrel Room - Obstacle Course"
|
||||||
|
CC_ENTRY = "Crumbling Castle: Entry"
|
||||||
|
CC_ENTRY_CASTLE = "Crumbling Castle: Outside - Castle Main"
|
||||||
|
CC_ENTRY_BELL = "Crumbling Castle: Outside - Bell Tower"
|
||||||
|
CC_ENTRY_BASEMENT = "Crumbling Castle: Outside - Flooded Basement"
|
||||||
|
CC_ENTRY_BOSS = "Crumbling Castle: Outside - Boss"
|
||||||
|
CC_CASTLEMAIN_ENTRY = "Crumbling Castle: Castle Main - Outside"
|
||||||
|
CC_CASTLEMAIN_BELL = "Crumbling Castle: Castle Main - Bell Tower"
|
||||||
|
CC_CASTLEMAIN_ELEVATOR = "Crumbling Castle: Castle Main - Elevator Room"
|
||||||
|
CC_BELL_ENTRY = "Crumbling Castle: Bell Tower - Outside"
|
||||||
|
CC_BELL_CASTLE = "Crumbling Castle: Bell Tower - Castle Main"
|
||||||
|
CC_ELEVATOR_CASTLEMAIN = "Crumbling Castle: Elevator Room - Castle Main"
|
||||||
|
CC_ELEVATOR_BASEMENT = "Crumbling Castle: Elevator Room - Flooded Basement"
|
||||||
|
CC_BASEMENT_ENTRY = "Crumbling Castle: Flooded Basement - Entry"
|
||||||
|
CC_BASEMENT_ELEVATOR = "Crumbling Castle: Flooded Basement - Elevator"
|
||||||
|
CC_BASEMENT_BUTTON_DOWN = "Crumbling Castle: Flooded Basement - Button Room (Downstairs)"
|
||||||
|
CC_BASEMENT_BUTTON_UP = "Crumbling Castle: Flooded Basement - Button Room (Upstairs)"
|
||||||
|
CC_BUTTON_BASEMENT_WATER = "Crumbling Castle: Button Room - Flooded Basement (Water)"
|
||||||
|
CC_BUTTON_BASEMENT_LEDGE = "Crumbling Castle: Button Room - Flooded Basement (Ledge)"
|
||||||
|
CC_BOSS_ROOM = "Crumbling Castle: Boss - Outside"
|
||||||
|
CC_CASTLEMAINTHRONEROOM = "Crumbling Castle: Castle Main - Throne Room" # Helper Region
|
||||||
|
CP_ENTRY = "City Park: Entry"
|
||||||
|
CP_OUTSIDE_SEWERS_FRONT = "City Park: Outside - Sewers Front"
|
||||||
|
CP_OUTSIDE_BARREL = "City Park: Outside - Barrel Room"
|
||||||
|
CP_SEWERSFRONT_OUTSIDE = "City Park: Sewers Front - Outside"
|
||||||
|
CP_SEWERSFRONT_BARREL = "City Park: Sewers Front - Barrel Room"
|
||||||
|
CP_BARREL_OUTSIDE = "City Park: Barrel Room - Outside"
|
||||||
|
CP_BARREL_SEWERS_FRONT = "City Park: Barrel Room - Sewers Front"
|
||||||
|
CP_BARRELSEWERMIDDLE = "City Park: Barrel Room - Middle Sewer" # Helper Region
|
||||||
|
SF_ENTRY = "Specter's Factory: Entry"
|
||||||
|
SF_OUTSIDE_FACTORY = "Specter's Factory: Outside - Main Factory"
|
||||||
|
SF_FACTORY_OUTSIDE = "Specter's Main Factory: Main Factory - Outside"
|
||||||
|
SF_FACTORY_RC_CAR = "Specter's Main Factory: Main Factory - RC Car Room"
|
||||||
|
SF_FACTORY_WHEEL_BOTTOM = "Specter's Main Factory: Main Factory - Triple Wheel Room (Bottom)"
|
||||||
|
SF_FACTORY_WHEEL_TOP = "Specter's Main Factory: Main Factory - Triple Wheel Room (Top)"
|
||||||
|
SF_FACTORY_MECH = "Specter's Main Factory: Main Factory - Mech Room"
|
||||||
|
SF_RC_CAR_FACTORY = "Specter's Main Factory: RC Car Room - Main Factory"
|
||||||
|
SF_WHEEL_FACTORY_BOTTOM = "Specter's Main Factory: Triple Wheel Room (Bottom) - Main Factory"
|
||||||
|
SF_WHEEL_FACTORY_TOP = "Specter's Main Factory: Triple Wheel Room (Top) - Main Factory"
|
||||||
|
SF_MECH_FACTORY = "Specter's Main Factory: Mech Room - Main Factory"
|
||||||
|
SF_MECH_LAVA = "Specter's Main Factory: Mech Room - Lava Room"
|
||||||
|
SF_LAVA_MECH = "Specter's Main Factory: Lava Room - Mech Room"
|
||||||
|
SF_LAVA_CONVEYOR = "Specter's Main Factory: Lava Room - Conveyor Room"
|
||||||
|
SF_CONVEYOR_LAVA = "Specter's Main Factory: Entry - Lava Room"
|
||||||
|
SF_CONVEYOR1_ENTRY = "Specter's Main Factory: Conveyor Room - Conveyor 1 Entry"
|
||||||
|
SF_CONVEYOR1_EXIT = "Specter's Main Factory: Conveyor Room - Conveyor 1 Exit"
|
||||||
|
SF_CONVEYOR2_ENTRY = "Specter's Main Factory: Conveyor Room - Conveyor 2 Entry"
|
||||||
|
SF_CONVEYOR2_EXIT = "Specter's Main Factory: Conveyor Room - Conveyor 2 Exit"
|
||||||
|
SF_CONVEYOR3_ENTRY = "Specter's Main Factory: Conveyor Room - Conveyor 3 Entry"
|
||||||
|
SF_CONVEYOR3_EXIT = "Specter's Main Factory: Conveyor Room - Conveyor 3 Exit"
|
||||||
|
SF_CONVEYOR4_ENTRY = "Specter's Main Factory: Conveyor Room - Conveyor 4 Entry"
|
||||||
|
SF_CONVEYOR4_EXIT = "Specter's Main Factory: Conveyor Room - Conveyor 4 Exit"
|
||||||
|
SF_CONVEYOR5_ENTRY = "Specter's Main Factory: Conveyor Room - Conveyor 5 Entry"
|
||||||
|
SF_CONVEYOR5_EXIT = "Specter's Main Factory: Conveyor Room - Conveyor 5 Exit"
|
||||||
|
SF_CONVEYOR6_ENTRY = "Specter's Main Factory: Conveyor Room - Conveyor 6 Entry"
|
||||||
|
SF_CONVEYOR6_EXIT = "Specter's Main Factory: Conveyor Room - Conveyor 6 Exit"
|
||||||
|
SF_CONVEYOR7_ENTRY = "Specter's Main Factory: Conveyor Room - Conveyor 7 Entry"
|
||||||
|
TVT_ENTRY = "TV Tower: Entry"
|
||||||
|
TVT_OUTSIDE_LOBBY = "TV Tower: Outside - Lobby"
|
||||||
|
TVT_LOBBY_OUTSIDE = "TV Tower: Lobby - Outside"
|
||||||
|
TVT_LOBBY_WATER = "TV Tower: Lobby - Water Basement"
|
||||||
|
TVT_LOBBY_TANK = "TV Tower: Lobby - Tank Room"
|
||||||
|
TVT_WATER_LOBBY = "TV Tower: Water Basement - Lobby"
|
||||||
|
TVT_TANK_LOBBY = "TV Tower: Tank Room - Lobby"
|
||||||
|
TVT_TANK_FAN = "TV Tower: Tank Room - Fan Room"
|
||||||
|
TVT_TANK_BOSS = "TV Tower: Tank Room - Boss"
|
||||||
|
TVT_FAN_TANK = "TV Tower: Fan Room - Tank Room"
|
||||||
|
TVT_BOSS_TANK = "TV Tower: Boss - Tank Room"
|
||||||
|
MM_SL_HUB = "Monkey Madness: Specter Land Hub"
|
||||||
|
MM_SL_HUB_WESTERN = "Monkey Madness: Specter Land Hub - Western Land"
|
||||||
|
MM_SL_HUB_COASTER = "Monkey Madness: Specter Land Hub - Coaster Entry"
|
||||||
|
MM_SL_HUB_CIRCUS = "Monkey Madness: Specter Land Hub - Circus"
|
||||||
|
MM_SL_HUB_GO_KARZ = "Monkey Madness: Specter Land Hub - Go Karz"
|
||||||
|
MM_SL_HUB_CRATER = "Monkey Madness: Specter Land Hub - Crater"
|
||||||
|
MM_WESTERN_SL_HUB = "Monkey Madness: Western Land - Specter Land Hub"
|
||||||
|
MM_COASTER_ENTRY_SL_HUB = "Monkey Madness: Coaster Entry - Specter Land Hub"
|
||||||
|
MM_COASTER_ENTRY_COASTER1 = "Monkey Madness: Coaster Entry - Coaster 1"
|
||||||
|
MM_COASTER_ENTRY_DISEMBARK = "Monkey Madness: Coaster Entry Disembark"
|
||||||
|
MM_CIRCUS_SL_HUB = "Monkey Madness: Circus - Specter Land Hub"
|
||||||
|
MM_GO_KARZ_SL_HUB = "Monkey Madness: Go Karz - Specter Land Hub"
|
||||||
|
MM_COASTER1_ENTRY = "Monkey Madness: Coaster Room 1 Entry"
|
||||||
|
MM_COASTER1_COASTER2 = "Monkey Madness: Coaster Room 1 - Coaster Room 2"
|
||||||
|
MM_COASTER2_ENTRY = "Monkey Madness: Coaster Room 2 Entry"
|
||||||
|
MM_COASTER2_HAUNTED_HOUSE = "Monkey Madness: Coaster Room 2 - Haunted House"
|
||||||
|
MM_HAUNTED_HOUSE_DISEMBARK = "Monkey Madness: Haunted House Disembark"
|
||||||
|
MM_HAUNTED_HOUSE_COFFIN = "Monkey Madness: Haunted House - Coffin Room"
|
||||||
|
MM_COFFIN_HAUNTED_HOUSE = "Monkey Madness: Coffin Room - Haunted House"
|
||||||
|
MM_COFFIN_COASTER_ENTRY = "Monkey Madness: Coffin Room - Coaster Entry"
|
||||||
|
MM_CRATER_SL_HUB = "Monkey Madness: Crater - Specter Land Hub"
|
||||||
|
MM_CRATER_OUTSIDE_CASTLE = "Monkey Madness: Crater - Outside Castle"
|
||||||
|
MM_OUTSIDE_CASTLE_CRATER = "Monkey Madness: Outside Castle - Crater"
|
||||||
|
MM_OUTSIDE_CASTLE_SIDE_ENTRY = "Monkey Madness: Outside Castle - Castle Side Entry"
|
||||||
|
MM_OUTSIDE_CASTLE_CASTLE_MAIN = "Monkey Madness: Outside Castle - Castle Main"
|
||||||
|
MM_SIDE_ENTRY_OUTSIDE_CASTLE = "Monkey Madness: Castle Side Entry - Outside Castle"
|
||||||
|
MM_CASTLE_MAIN_OUTSIDE_CASTLE = "Monkey Madness: Castle Main - Outside Castle"
|
||||||
|
MM_CASTLE_MAIN_MONKEY_HEAD = "Monkey Madness: Castle Main - Monkey Head"
|
||||||
|
MM_CASTLE_MAIN_INSIDE_CLIMB = "Monkey Madness: Castle Main - Inside Climb"
|
||||||
|
MM_CASTLE_MAIN_FROM_OUTSIDE = "Monkey Madness: Castle Main From Outside"
|
||||||
|
MM_CASTLE_MAIN_SPECTER1 = "Monkey Madness: Specter 1 Entrance"
|
||||||
|
MM_MONKEY_HEAD_CASTLE_MAIN = "Monkey Madness: Monkey Head - Castle Main"
|
||||||
|
MM_INSIDE_CLIMB_CASTLE_MAIN = "Monkey Madness: Inside Climb - Castle Main"
|
||||||
|
MM_INSIDE_CLIMB_OUTSIDE_CLIMB = "Monkey Madness: Inside Climb - Outside Climb"
|
||||||
|
MM_OUTSIDE_CLIMB_INSIDE_CLIMB = "Monkey Madness: Outside Climb - Inside Climb"
|
||||||
|
MM_OUTSIDE_CLIMB_CASTLE_MAIN = "Monkey Madness: Outside Climb - Castle Main"
|
||||||
|
MM_SPECTER1_ROOM = "Monkey Madness: Specter 1 Room"
|
||||||
|
PPM_ENTRY = "Peak Point Matrix: Entry"
|
||||||
|
|
||||||
|
class AEItem(Enum):
|
||||||
|
Club = "Stun Club"
|
||||||
|
Net = "Time Net"
|
||||||
|
Radar = "Monkey Radar"
|
||||||
|
Sling = "Slingback Shooter"
|
||||||
|
Hoop = "Super Hoop"
|
||||||
|
Punch = "Magic Punch"
|
||||||
|
Flyer = "Sky Flyer"
|
||||||
|
Car = "R.C. Car"
|
||||||
|
Key = "World Key"
|
||||||
|
Victory = "Victory"
|
||||||
|
WaterNet = "Water Net"
|
||||||
|
ProgWaterNet = "Progressive Water Net"
|
||||||
|
WaterCatch = "Water Catch"
|
||||||
|
CB_Lamp = "Crabby Beach Monkey Lamp"
|
||||||
|
DI_Lamp = "Dexter's Island Monkey Lamp"
|
||||||
|
CrC_Lamp = "Crumbling Castle Monkey Lamp"
|
||||||
|
CP_Lamp = "City Park Monkey Lamp"
|
||||||
|
SF_Lamp = "Specter's Factory Monkey Lamp"
|
||||||
|
TVT_Lobby_Lamp = "TV Tower Monkey Lamp (Lobby)"
|
||||||
|
TVT_Tank_Lamp = "TV Tower Monkey Lamp (Tank Room)"
|
||||||
|
MM_Lamp = "Monkey Madness Monkey Lamp"
|
||||||
|
MM_DoubleDoorKey = "Monkey Madness Double Door Key"
|
||||||
|
Token = "Specter Token"
|
||||||
|
Nothing = "Nothing"
|
||||||
|
Shirt = "Jacket"
|
||||||
|
Triangle = "Energy Chip"
|
||||||
|
BigTriangle = "5 Energy Chips"
|
||||||
|
BiggerTriangle = "25 Energy Chips"
|
||||||
|
Cookie = "Cookie"
|
||||||
|
FiveCookies = "5 Cookies"
|
||||||
|
Flash = "Explosive Pellet"
|
||||||
|
ThreeFlash = "3 Explosive Pellets"
|
||||||
|
Rocket = "Guided Pellet"
|
||||||
|
ThreeRocket = "3 Guided Pellets"
|
||||||
|
BananaPeelTrap = "Banana Peel Trap"
|
||||||
|
GadgetShuffleTrap = "Gadget Shuffle Trap"
|
||||||
|
MonkeyMashTrap = "Monkey Mash Trap"
|
||||||
|
IcyHotPantsTrap = "Icy Hot Pants Trap"
|
||||||
|
StunTrap = "Stun Trap"
|
||||||
|
CameraRotateTrap = "Camera Rotate Trap"
|
||||||
|
RainbowCookie = "Rainbow Cookie"
|
||||||
|
FAKE_OOL_ITEM = "Fake OOL Item"
|
||||||
|
|
||||||
|
DS_Options = ["kickoutprevention","deathlink","autoequip","bhdisplay"]
|
||||||
|
DS_ButtonAndDoors = ["DIButton","CrCWaterButton","MM_Painting_Button","MM_MonkeyHead_Button","TVT_Lobby_Button","DR_Block"]
|
||||||
|
|
||||||
|
Commands_Dict = {
|
||||||
|
"ae_commands" : "cmd_ae_commands",
|
||||||
|
"bh_itemdisplay" : "cmd_bh_itemdisplay",
|
||||||
|
"prevent_kickout":"cmd_prevent_kickout",
|
||||||
|
"deathlink" : "cmd_deathlink",
|
||||||
|
"auto_equip" : "cmd_auto_equip",
|
||||||
|
"syncprogress" : "cmd_syncprogress",
|
||||||
|
"spikecolor" : "cmd_spikecolor",
|
||||||
|
}
|
||||||
520
worlds/apeescape/__init__.py
Normal file
520
worlds/apeescape/__init__.py
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
import math
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from typing import ClassVar, Dict, List, Tuple, Optional, TextIO, Any
|
||||||
|
|
||||||
|
from BaseClasses import ItemClassification, MultiWorld, Tutorial, CollectionState
|
||||||
|
from logging import warning
|
||||||
|
from Options import OptionError
|
||||||
|
from worlds.AutoWorld import WebWorld, World
|
||||||
|
|
||||||
|
from .Items import item_table, ApeEscapeItem, GROUPED_ITEMS
|
||||||
|
from .Locations import location_table, base_location_id, GROUPED_LOCATIONS
|
||||||
|
from .Regions import create_regions, ApeEscapeLevel
|
||||||
|
from .Rules import set_rules, get_required_keys
|
||||||
|
from .Client import ApeEscapeClient
|
||||||
|
from .Strings import AEItem, AELocation
|
||||||
|
from .RAMAddress import RAM
|
||||||
|
from .Options import ApeEscapeOptions
|
||||||
|
|
||||||
|
|
||||||
|
class ApeEscapeWeb(WebWorld):
|
||||||
|
theme = "stone"
|
||||||
|
|
||||||
|
# Verify this placeholder text is accurate
|
||||||
|
setup_en = Tutorial(
|
||||||
|
"Ape Escape Multiworld Setup Guide",
|
||||||
|
"A guide to setting up Ape Escape in Archipelago.",
|
||||||
|
"English",
|
||||||
|
"setup_en.md",
|
||||||
|
"setup/en",
|
||||||
|
["CDRomatron, Thedragon005, IHNN"]
|
||||||
|
)
|
||||||
|
setup_fr = Tutorial(
|
||||||
|
setup_en.tutorial_name,
|
||||||
|
setup_en.description,
|
||||||
|
"Français",
|
||||||
|
"setup_fr.md",
|
||||||
|
"setup/fr",
|
||||||
|
["Thedragon005"]
|
||||||
|
)
|
||||||
|
|
||||||
|
tutorials = [setup_en, setup_fr]
|
||||||
|
|
||||||
|
|
||||||
|
class ApeEscapeWorld(World):
|
||||||
|
"""
|
||||||
|
Ape Escape is a platform game published and developed by Sony for the PlayStation, released in 1999.
|
||||||
|
The story revolves around the main protagonist, Spike, who has to prevent history from being changed
|
||||||
|
by an army of monkeys led by Specter, the main antagonist.
|
||||||
|
"""
|
||||||
|
game = "Ape Escape"
|
||||||
|
web: ClassVar[WebWorld] = ApeEscapeWeb()
|
||||||
|
topology_present = True
|
||||||
|
|
||||||
|
options_dataclass = ApeEscapeOptions
|
||||||
|
options: ApeEscapeOptions
|
||||||
|
|
||||||
|
item_name_to_id = item_table
|
||||||
|
|
||||||
|
for key, value in item_name_to_id.items():
|
||||||
|
item_name_to_id[key] = value + base_location_id
|
||||||
|
|
||||||
|
location_name_to_id = location_table
|
||||||
|
|
||||||
|
for key, value in location_name_to_id.items():
|
||||||
|
location_name_to_id[key] = value + base_location_id
|
||||||
|
|
||||||
|
item_name_groups = GROUPED_ITEMS
|
||||||
|
location_name_groups = GROUPED_LOCATIONS
|
||||||
|
|
||||||
|
glitches_item_name = AEItem.FAKE_OOL_ITEM.value
|
||||||
|
ut_can_gen_without_yaml = True # class var that tells it to ignore the player yaml
|
||||||
|
using_ut: bool # so we can check if we're using UT only once
|
||||||
|
passthrough: Dict[str, Any]
|
||||||
|
|
||||||
|
def __init__(self, multiworld: MultiWorld, player: int):
|
||||||
|
self.goal: Optional[int] = 0
|
||||||
|
self.requiredtokens: Optional[int] = 0
|
||||||
|
self.totaltokens: Optional[int] = 0
|
||||||
|
self.tokenlocations: Optional[int] = 0
|
||||||
|
self.fasttokengoal: Optional[int] = 0
|
||||||
|
self.logic: Optional[int] = 0
|
||||||
|
self.infinitejump: Optional[int] = 0
|
||||||
|
self.superflyer: Optional[int] = 0
|
||||||
|
self.entrance: Optional[int] = 0
|
||||||
|
self.randomizestartingroom: Optional[int] = 0
|
||||||
|
self.unlocksperkey: Optional[int] = 0
|
||||||
|
self.extrakeys: Optional[int] = 0
|
||||||
|
self.coin: Optional[int] = 0
|
||||||
|
self.mailbox: Optional[int] = 0
|
||||||
|
self.lamp: Optional[int] = 0
|
||||||
|
self.gadget: Optional[int] = 0
|
||||||
|
self.shufflenet: Optional[int] = 0
|
||||||
|
self.shufflewaternet: Optional[int] = 0
|
||||||
|
self.lowoxygensounds: Optional[int] = 0
|
||||||
|
self.trappercentage: Optional[int] = 0
|
||||||
|
self.itemdisplay: Optional[int] = 0
|
||||||
|
self.itempool: List[ApeEscapeItem] = []
|
||||||
|
self.levellist: List[ApeEscapeLevel] = []
|
||||||
|
self.entranceorder: List[ApeEscapeLevel] = []
|
||||||
|
self.firstrooms = []
|
||||||
|
super(ApeEscapeWorld, self).__init__(multiworld, player)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_early(self) -> None:
|
||||||
|
self.goal = self.options.goal.value
|
||||||
|
self.requiredtokens = self.options.requiredtokens.value
|
||||||
|
self.totaltokens = self.options.totaltokens.value
|
||||||
|
self.tokenlocations = self.options.tokenlocations.value
|
||||||
|
self.fasttokengoal = self.options.fasttokengoal.value
|
||||||
|
self.logic = self.options.logic.value
|
||||||
|
self.infinitejump = self.options.infinitejump.value
|
||||||
|
self.superflyer = self.options.superflyer.value
|
||||||
|
self.entrance = self.options.entrance.value
|
||||||
|
self.randomizestartingroom = self.options.randomizestartingroom.value
|
||||||
|
self.unlocksperkey = self.options.unlocksperkey.value
|
||||||
|
self.extrakeys = self.options.extrakeys.value
|
||||||
|
self.coin = self.options.coin.value
|
||||||
|
self.mailbox = self.options.mailbox.value
|
||||||
|
self.lamp = self.options.lamp.value
|
||||||
|
self.gadget = self.options.gadget.value
|
||||||
|
self.shufflenet = self.options.shufflenet.value
|
||||||
|
self.shufflewaternet = self.options.shufflewaternet.value
|
||||||
|
self.lowoxygensounds = self.options.lowoxygensounds.value
|
||||||
|
self.trappercentage = self.options.trappercentage.value
|
||||||
|
self.itemdisplay = self.options.itemdisplay.value
|
||||||
|
self.itempool = []
|
||||||
|
|
||||||
|
# Universal tracker stuff, shouldn't do anything in standard gen
|
||||||
|
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||||
|
if "Ape Escape" in self.multiworld.re_gen_passthrough:
|
||||||
|
self.using_ut = True
|
||||||
|
self.passthrough = self.multiworld.re_gen_passthrough["Ape Escape"]
|
||||||
|
self.options.goal.value = self.passthrough["goal"]
|
||||||
|
self.options.fasttokengoal.value = self.passthrough["fasttokengoal"]
|
||||||
|
self.options.allowcollect.value = self.passthrough["allowcollect"]
|
||||||
|
self.options.requiredtokens.value = self.passthrough["requiredtokens"]
|
||||||
|
self.options.totaltokens.value = self.passthrough["totaltokens"]
|
||||||
|
self.options.tokenlocations.value = self.passthrough["tokenlocations"]
|
||||||
|
self.options.logic.value = self.passthrough["logic"]
|
||||||
|
self.options.infinitejump.value = self.passthrough["infinitejump"]
|
||||||
|
self.options.superflyer.value = self.passthrough["superflyer"]
|
||||||
|
self.options.entrance.value = self.passthrough["entrance"]
|
||||||
|
self.options.randomizestartingroom.value = self.passthrough["randomizestartingroom"]
|
||||||
|
self.options.unlocksperkey.value = self.passthrough["unlocksperkey"]
|
||||||
|
self.options.extrakeys.value = self.passthrough["extrakeys"]
|
||||||
|
self.options.coin.value = self.passthrough["coin"]
|
||||||
|
self.options.mailbox.value = self.passthrough["mailbox"]
|
||||||
|
self.options.lamp.value = self.passthrough["lamp"]
|
||||||
|
self.options.gadget.value = self.passthrough["gadget"]
|
||||||
|
self.options.shufflenet.value = self.passthrough["shufflenet"]
|
||||||
|
self.options.shufflewaternet.value = self.passthrough["shufflewaternet"]
|
||||||
|
self.options.lowoxygensounds.value = self.passthrough["lowoxygensounds"]
|
||||||
|
self.options.trappercentage.value = self.passthrough["trappercentage"]
|
||||||
|
self.options.itemdisplay.value = self.passthrough["itemdisplay"]
|
||||||
|
else:
|
||||||
|
self.using_ut = False
|
||||||
|
else:
|
||||||
|
self.using_ut = False
|
||||||
|
|
||||||
|
def create_regions(self):
|
||||||
|
create_regions(self)
|
||||||
|
|
||||||
|
|
||||||
|
def set_rules(self):
|
||||||
|
set_rules(self)
|
||||||
|
|
||||||
|
|
||||||
|
def create_item(self, name: str) -> ApeEscapeItem:
|
||||||
|
item_id = item_table[name]
|
||||||
|
classification = ItemClassification.progression
|
||||||
|
|
||||||
|
item = ApeEscapeItem(name, classification, item_id, self.player)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def create_item_skipbalancing(self, name: str) -> ApeEscapeItem:
|
||||||
|
item_id = item_table[name]
|
||||||
|
classification = ItemClassification.progression_skip_balancing
|
||||||
|
|
||||||
|
item = ApeEscapeItem(name, classification, item_id, self.player)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def create_item_useful(self, name: str) -> ApeEscapeItem:
|
||||||
|
item_id = item_table[name]
|
||||||
|
classification = ItemClassification.useful
|
||||||
|
|
||||||
|
item = ApeEscapeItem(name, classification, item_id, self.player)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def create_item_filler(self, name: str) -> ApeEscapeItem:
|
||||||
|
item_id = item_table[name]
|
||||||
|
classification = ItemClassification.filler
|
||||||
|
|
||||||
|
item = ApeEscapeItem(name, classification, item_id, self.player)
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def create_item_trap(self, name: str) -> ApeEscapeItem:
|
||||||
|
item_id = item_table[name]
|
||||||
|
classification = ItemClassification.trap
|
||||||
|
|
||||||
|
item = ApeEscapeItem(name, classification, item_id, self.player)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def create_event_item(self, name: str) -> ApeEscapeItem:
|
||||||
|
classification = ItemClassification.progression
|
||||||
|
|
||||||
|
item = ApeEscapeItem(name, classification, None, self.player)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def create_items(self):
|
||||||
|
reservedlocations = 0
|
||||||
|
|
||||||
|
club = self.create_item(AEItem.Club.value)
|
||||||
|
net = self.create_item(AEItem.Net.value)
|
||||||
|
radar = self.create_item(AEItem.Radar.value)
|
||||||
|
shooter = self.create_item(AEItem.Sling.value)
|
||||||
|
hoop = self.create_item(AEItem.Hoop.value)
|
||||||
|
flyer = self.create_item(AEItem.Flyer.value)
|
||||||
|
car = self.create_item(AEItem.Car.value)
|
||||||
|
punch = self.create_item(AEItem.Punch.value)
|
||||||
|
victory = self.create_event_item("Victory")
|
||||||
|
|
||||||
|
waternet = self.create_item(AEItem.WaterNet.value)
|
||||||
|
# progwaternet = self.create_item(AEItem.ProgWaterNet.value)
|
||||||
|
watercatch = self.create_item(AEItem.WaterCatch.value)
|
||||||
|
|
||||||
|
CB_Lamp = self.create_item(AEItem.CB_Lamp.value)
|
||||||
|
DI_Lamp = self.create_item(AEItem.DI_Lamp.value)
|
||||||
|
CrC_Lamp = self.create_item(AEItem.CrC_Lamp.value)
|
||||||
|
CP_Lamp = self.create_item(AEItem.CP_Lamp.value)
|
||||||
|
SF_Lamp = self.create_item(AEItem.SF_Lamp.value)
|
||||||
|
TVT_Lobby_Lamp = self.create_item(AEItem.TVT_Lobby_Lamp.value)
|
||||||
|
TVT_Tank_Lamp = self.create_item(AEItem.TVT_Tank_Lamp.value)
|
||||||
|
MM_Lamp = self.create_item(AEItem.MM_Lamp.value)
|
||||||
|
MM_DoubleDoorKey = self.create_item(AEItem.MM_DoubleDoorKey.value)
|
||||||
|
|
||||||
|
self.itempool += [MM_DoubleDoorKey]
|
||||||
|
|
||||||
|
# Create the desired amount of Specter Tokens if the settings require them, and make them local if requested.
|
||||||
|
if self.options.goal == "tokenhunt" or self.options.goal == "mmtoken" or self.options.goal == "ppmtoken":
|
||||||
|
self.itempool += [self.create_item_skipbalancing(AEItem.Token.value) for _ in range(0, max(self.options.requiredtokens, self.options.totaltokens))]
|
||||||
|
if self.options.tokenlocations == "ownworld":
|
||||||
|
self.options.local_items.value.add("Specter Token")
|
||||||
|
|
||||||
|
# Create enough keys to access every level, if keys are on, plus the desired amount of extra keys.
|
||||||
|
if self.options.unlocksperkey != "none":
|
||||||
|
numkeys = get_required_keys(self.options.unlocksperkey.value, self.options.goal.value, self.options.coin.value)
|
||||||
|
self.itempool += [self.create_item(AEItem.Key.value) for _ in range(0, numkeys[21] + self.options.extrakeys.value)]
|
||||||
|
|
||||||
|
# Monkey Lamp shuffle - only add to the pool if the option is on (treat as vanilla otherwise)
|
||||||
|
if self.options.lamp == "true":
|
||||||
|
self.itempool += [CB_Lamp]
|
||||||
|
self.itempool += [DI_Lamp]
|
||||||
|
self.itempool += [CrC_Lamp]
|
||||||
|
self.itempool += [CP_Lamp]
|
||||||
|
self.itempool += [SF_Lamp]
|
||||||
|
self.itempool += [TVT_Lobby_Lamp]
|
||||||
|
self.itempool += [TVT_Tank_Lamp]
|
||||||
|
self.itempool += [MM_Lamp]
|
||||||
|
|
||||||
|
# Water Net shuffle handling
|
||||||
|
if self.options.shufflewaternet == 0x00 or self.options.gadget == 0x07: # Off or Starting Gadget
|
||||||
|
self.multiworld.push_precollected(waternet)
|
||||||
|
elif self.options.shufflewaternet == 0x01: # Progressive
|
||||||
|
self.itempool += [watercatch]
|
||||||
|
self.itempool += [self.create_item(AEItem.ProgWaterNet.value)]
|
||||||
|
self.itempool += [self.create_item(AEItem.ProgWaterNet.value)]
|
||||||
|
else: # On
|
||||||
|
self.itempool += [waternet]
|
||||||
|
|
||||||
|
# Net shuffle handling
|
||||||
|
if self.options.shufflenet == "false":
|
||||||
|
self.multiworld.push_precollected(net)
|
||||||
|
elif self.options.shufflenet == "true":
|
||||||
|
# If net shuffle is on, make sure there are locations that don't require net.
|
||||||
|
if self.options.coin == "true" or self.options.mailbox == "true":
|
||||||
|
self.itempool += [net]
|
||||||
|
else:
|
||||||
|
# All locations require net with these options, so throw a warning about incompatible options and just give the net anyway.
|
||||||
|
# if instead we want to error out and prevent generation, uncomment this line:
|
||||||
|
# raise OptionError(f"{self.player_name} has no sphere 1 locations!")
|
||||||
|
warning(
|
||||||
|
f"Warning: selected options for {self.player_name} have no sphere 1 locations. Giving Time Net.")
|
||||||
|
self.multiworld.push_precollected(net)
|
||||||
|
|
||||||
|
if self.options.gadget == "club":
|
||||||
|
self.multiworld.push_precollected(club)
|
||||||
|
self.itempool += [radar, shooter, hoop, flyer, car, punch]
|
||||||
|
elif self.options.gadget == "radar":
|
||||||
|
self.multiworld.push_precollected(radar)
|
||||||
|
self.itempool += [club, shooter, hoop, flyer, car, punch]
|
||||||
|
elif self.options.gadget == "sling":
|
||||||
|
self.multiworld.push_precollected(shooter)
|
||||||
|
self.itempool += [club, radar, hoop, flyer, car, punch]
|
||||||
|
elif self.options.gadget == "hoop":
|
||||||
|
self.multiworld.push_precollected(hoop)
|
||||||
|
self.itempool += [club, radar, shooter, flyer, car, punch]
|
||||||
|
elif self.options.gadget == "flyer":
|
||||||
|
self.multiworld.push_precollected(flyer)
|
||||||
|
self.itempool += [club, radar, shooter, hoop, car, punch]
|
||||||
|
elif self.options.gadget == "car":
|
||||||
|
self.multiworld.push_precollected(car)
|
||||||
|
self.itempool += [club, radar, shooter, hoop, flyer, punch]
|
||||||
|
elif self.options.gadget == "punch":
|
||||||
|
self.multiworld.push_precollected(punch)
|
||||||
|
self.itempool += [club, radar, shooter, hoop, flyer, car]
|
||||||
|
elif self.options.gadget == "none" or self.options.gadget == "waternet":
|
||||||
|
self.itempool += [club, radar, shooter, hoop, flyer, car, punch]
|
||||||
|
|
||||||
|
# Create "Victory" item for goals where the goal is at a location.
|
||||||
|
if self.options.goal == "mm" or self.options.goal == "mmtoken":
|
||||||
|
self.get_location(AELocation.Specter.value).place_locked_item(victory)
|
||||||
|
elif self.options.goal == "ppm" or self.options.goal == "ppmtoken":
|
||||||
|
self.get_location(AELocation.Specter2.value).place_locked_item(victory)
|
||||||
|
|
||||||
|
# This is where creating items for increasing special pellet maximums would go.
|
||||||
|
|
||||||
|
# Trap item fill: randomly pick items according to a set of weights.
|
||||||
|
# Trap weights: Banana Peel, Gadget Shuffle , Monkey Mash, Icy Hot Pants, Stun Trap
|
||||||
|
if self.options.trappercentage != 0:
|
||||||
|
custom_trapweights = [
|
||||||
|
self.options.trapweights[AEItem.BananaPeelTrap.value],
|
||||||
|
self.options.trapweights[AEItem.GadgetShuffleTrap.value],
|
||||||
|
self.options.trapweights[AEItem.MonkeyMashTrap.value],
|
||||||
|
self.options.trapweights[AEItem.IcyHotPantsTrap.value],
|
||||||
|
self.options.trapweights[AEItem.StunTrap.value],
|
||||||
|
self.options.trapweights[AEItem.CameraRotateTrap.value]
|
||||||
|
]
|
||||||
|
# If custom_trapweights are all zeros, reset to default values
|
||||||
|
if not any(y > 0 for y in custom_trapweights):
|
||||||
|
trap_weights = [15, 13, 5, 10, 7, 10]
|
||||||
|
else:
|
||||||
|
trap_weights = list(custom_trapweights)
|
||||||
|
|
||||||
|
trap_percentage = self.options.trappercentage / 100
|
||||||
|
trap_count = round((len(self.multiworld.get_unfilled_locations(self.player)) - len(self.itempool) - reservedlocations) * trap_percentage, None)
|
||||||
|
|
||||||
|
for x in range(1, len(trap_weights)):
|
||||||
|
trap_weights[x] = trap_weights[x] + trap_weights[x - 1]
|
||||||
|
|
||||||
|
for _ in range(trap_count):
|
||||||
|
randomTrap = self.random.randint(1, trap_weights[len(trap_weights) - 1])
|
||||||
|
if 0 < randomTrap <= trap_weights[0]:
|
||||||
|
self.itempool += [self.create_item_trap(AEItem.BananaPeelTrap.value)]
|
||||||
|
elif trap_weights[0] < randomTrap <= trap_weights[1]:
|
||||||
|
self.itempool += [self.create_item_trap(AEItem.GadgetShuffleTrap.value)]
|
||||||
|
elif trap_weights[1] < randomTrap <= trap_weights[2]:
|
||||||
|
self.itempool += [self.create_item_trap(AEItem.MonkeyMashTrap.value)]
|
||||||
|
elif trap_weights[2] < randomTrap <= trap_weights[3]:
|
||||||
|
self.itempool += [self.create_item_trap(AEItem.IcyHotPantsTrap.value)]
|
||||||
|
elif trap_weights[3] < randomTrap <= trap_weights[4]:
|
||||||
|
self.itempool += [self.create_item_trap(AEItem.CameraRotateTrap.value)]
|
||||||
|
else:
|
||||||
|
self.itempool += [self.create_item_trap(AEItem.StunTrap.value)]
|
||||||
|
|
||||||
|
# Junk item fill: randomly pick items according to a set of weights.
|
||||||
|
# Filler item weights are for 1 Jacket, 1/5 Cookies, 1/5/25 Energy Chips, 1/3 Explosive/Guided Pellets, Rainbow Cookie and Nothing, respectively.
|
||||||
|
custom_fillervalues = [
|
||||||
|
self.options.customfillerweights[AEItem.Shirt.value],
|
||||||
|
self.options.customfillerweights[AEItem.Cookie.value],
|
||||||
|
self.options.customfillerweights[AEItem.FiveCookies.value],
|
||||||
|
self.options.customfillerweights[AEItem.Triangle.value],
|
||||||
|
self.options.customfillerweights[AEItem.BigTriangle.value],
|
||||||
|
self.options.customfillerweights[AEItem.BiggerTriangle.value],
|
||||||
|
self.options.customfillerweights[AEItem.Flash.value],
|
||||||
|
self.options.customfillerweights[AEItem.ThreeFlash.value],
|
||||||
|
self.options.customfillerweights[AEItem.Rocket.value],
|
||||||
|
self.options.customfillerweights[AEItem.ThreeRocket.value],
|
||||||
|
self.options.customfillerweights[AEItem.RainbowCookie.value],
|
||||||
|
self.options.customfillerweights[AEItem.Nothing.value]
|
||||||
|
]
|
||||||
|
|
||||||
|
# Set filler item weights
|
||||||
|
allnothing = False
|
||||||
|
if self.options.fillerpreset == 0x00: # Normal
|
||||||
|
weights = [7, 16, 3, 31, 14, 4, 9, 3, 9, 3, 6, 0]
|
||||||
|
elif self.options.fillerpreset == 0x01: # Bountiful
|
||||||
|
weights = [11, 3, 8, 1, 4, 12, 2, 6, 2, 6, 5, 0] # Total of 60
|
||||||
|
elif self.options.fillerpreset == 0x02: # Stingy
|
||||||
|
weights = [3, 7, 1, 28, 7, 2, 5, 1, 5, 1, 3, 7] # Total of 70
|
||||||
|
elif self.options.fillerpreset == 0x03: # Nothing
|
||||||
|
allnothing = True
|
||||||
|
weights = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99]
|
||||||
|
elif self.options.fillerpreset == 0x04: # Custom
|
||||||
|
# Failsafe: if the list is all zeroes, make every item a "Nothing"
|
||||||
|
if not any(y > 0 for y in custom_fillervalues):
|
||||||
|
allnothing = True
|
||||||
|
weights = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 99]
|
||||||
|
else:
|
||||||
|
weights = list(custom_fillervalues)
|
||||||
|
|
||||||
|
# Create filler items
|
||||||
|
for x in range(1, len(weights)):
|
||||||
|
weights[x] = weights[x] + weights[x - 1]
|
||||||
|
filler_count = len(self.multiworld.get_unfilled_locations(self.player)) - len(self.itempool) - reservedlocations
|
||||||
|
# Don't use weights if every filler item will be set to Nothing, as an optimization.
|
||||||
|
if allnothing == True:
|
||||||
|
for _ in range(filler_count):
|
||||||
|
self.itempool += [self.create_item_filler(AEItem.Nothing.value)]
|
||||||
|
else:
|
||||||
|
for _ in range(filler_count):
|
||||||
|
randomFiller = self.random.randint(1, weights[len(weights) - 1])
|
||||||
|
if 0 < randomFiller <= weights[0]:
|
||||||
|
self.itempool += [self.create_item_useful(AEItem.Shirt.value)]
|
||||||
|
elif weights[0] < randomFiller <= weights[1]:
|
||||||
|
self.itempool += [self.create_item_filler(AEItem.Cookie.value)]
|
||||||
|
elif weights[1] < randomFiller <= weights[2]:
|
||||||
|
self.itempool += [self.create_item_filler(AEItem.FiveCookies.value)]
|
||||||
|
elif weights[2] < randomFiller <= weights[3]:
|
||||||
|
self.itempool += [self.create_item_filler(AEItem.Triangle.value)]
|
||||||
|
elif weights[3] < randomFiller <= weights[4]:
|
||||||
|
self.itempool += [self.create_item_filler(AEItem.BigTriangle.value)]
|
||||||
|
elif weights[4] < randomFiller <= weights[5]:
|
||||||
|
self.itempool += [self.create_item_filler(AEItem.BiggerTriangle.value)]
|
||||||
|
elif weights[5] < randomFiller <= weights[6]:
|
||||||
|
self.itempool += [self.create_item_filler(AEItem.Flash.value)]
|
||||||
|
elif weights[6] < randomFiller <= weights[7]:
|
||||||
|
self.itempool += [self.create_item_useful(AEItem.ThreeFlash.value)]
|
||||||
|
elif weights[7] < randomFiller <= weights[8]:
|
||||||
|
self.itempool += [self.create_item_filler(AEItem.Rocket.value)]
|
||||||
|
elif weights[8] < randomFiller <= weights[9]:
|
||||||
|
self.itempool += [self.create_item_useful(AEItem.ThreeRocket.value)]
|
||||||
|
elif weights[9] < randomFiller <= weights[10]:
|
||||||
|
self.itempool += [self.create_item_useful(AEItem.RainbowCookie.value)]
|
||||||
|
else:
|
||||||
|
self.itempool += [self.create_item_filler(AEItem.Nothing.value)]
|
||||||
|
|
||||||
|
self.multiworld.itempool += self.itempool
|
||||||
|
|
||||||
|
|
||||||
|
def fill_slot_data(self):
|
||||||
|
bytestowrite = []
|
||||||
|
entranceids = []
|
||||||
|
newpositions = []
|
||||||
|
orderedfirstroomids = list(self.firstrooms)
|
||||||
|
for x in range(0, 22):
|
||||||
|
newpositions.append(self.levellist[x].newpos)
|
||||||
|
entranceids.append(self.entranceorder[x].entrance)
|
||||||
|
bytestowrite += self.entranceorder[x].bytes
|
||||||
|
bytestowrite.append(0) # We need a separator byte after each level name.
|
||||||
|
#self.firstrooms = orderedfirstroomids
|
||||||
|
|
||||||
|
return {
|
||||||
|
"goal": self.options.goal.value,
|
||||||
|
"fasttokengoal": self.options.fasttokengoal.value,
|
||||||
|
"allowcollect": self.options.allowcollect.value,
|
||||||
|
"requiredtokens": self.options.requiredtokens.value,
|
||||||
|
"totaltokens": self.options.totaltokens.value,
|
||||||
|
"tokenlocations": self.options.tokenlocations.value,
|
||||||
|
"logic": self.options.logic.value,
|
||||||
|
"infinitejump": self.options.infinitejump.value,
|
||||||
|
"superflyer": self.options.superflyer.value,
|
||||||
|
"entrance": self.options.entrance.value,
|
||||||
|
"randomizestartingroom": self.options.randomizestartingroom.value,
|
||||||
|
"unlocksperkey": self.options.unlocksperkey.value,
|
||||||
|
"extrakeys": self.options.extrakeys.value,
|
||||||
|
"coin": self.options.coin.value,
|
||||||
|
"mailbox": self.options.mailbox.value,
|
||||||
|
"lamp": self.options.lamp.value,
|
||||||
|
"gadget": self.options.gadget.value,
|
||||||
|
"shufflenet": self.options.shufflenet.value,
|
||||||
|
"shufflewaternet": self.options.shufflewaternet.value,
|
||||||
|
"lowoxygensounds": self.options.lowoxygensounds.value,
|
||||||
|
"fillerpreset": self.options.fillerpreset.value,
|
||||||
|
"customfillerweights": self.options.customfillerweights.value,
|
||||||
|
"trappercentage": self.options.trappercentage.value,
|
||||||
|
"trapweights": self.options.trapweights.value,
|
||||||
|
"trapsonreconnect": list(self.options.trapsonreconnect.value),
|
||||||
|
"trap_link": self.options.trap_link.value,
|
||||||
|
"itemdisplay": self.options.itemdisplay.value,
|
||||||
|
"kickoutprevention": self.options.kickoutprevention.value,
|
||||||
|
"autoequip": self.options.autoequip.value,
|
||||||
|
"spikecolor": self.options.spikecolor.value,
|
||||||
|
"customspikecolor": self.options.customspikecolor.value,
|
||||||
|
"levelnames": bytestowrite, # List of level names in entrance order. FF leads to the first.
|
||||||
|
"entranceids": entranceids, # Not used by the client. List of level ids in entrance order.
|
||||||
|
"newpositions": newpositions, # List of positions a level is moved to. The position of FF is first.
|
||||||
|
"firstrooms": orderedfirstroomids, # List of first rooms in entrance order.
|
||||||
|
"reqkeys": get_required_keys(self.options.unlocksperkey.value, self.options.goal.value, self.options.coin.value),
|
||||||
|
"death_link": self.options.death_link.value
|
||||||
|
}
|
||||||
|
|
||||||
|
# for the universal tracker, doesn't get called in standard gen
|
||||||
|
# docs: https://github.com/FarisTheAncient/Archipelago/blob/tracker/worlds/tracker/docs/re-gen-passthrough.md
|
||||||
|
@staticmethod
|
||||||
|
def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
# returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough
|
||||||
|
# we are using re_gen_passthrough over modifying the world here due to complexities with ER
|
||||||
|
return slot_data
|
||||||
|
|
||||||
|
def write_spoiler(self, spoiler_handle: TextIO):
|
||||||
|
if self.options.entrance.value != 0x00:
|
||||||
|
spoiler_handle.write(
|
||||||
|
f"\n\nApe Escape entrance connections for {self.multiworld.get_player_name(self.player)}:")
|
||||||
|
for x in range(0, 22):
|
||||||
|
spoiler_handle.write(f"\n {self.levellist[x].name} ==> {self.entranceorder[x].name}")
|
||||||
|
spoiler_handle.write(f"\n")
|
||||||
|
|
||||||
|
#def generate_output(self, output_directory: str):
|
||||||
|
#data = {
|
||||||
|
# "slot_data": self.fill_slot_data(),
|
||||||
|
# "location_to_item": {self.location_name_to_id[i.name] : item_table[i.item.name] for i in self.multiworld.get_locations() if not i.is_event},
|
||||||
|
# "data_package": {
|
||||||
|
# "data": {
|
||||||
|
# "games": {
|
||||||
|
# self.game: {
|
||||||
|
# "item_name_to_id": self.item_name_to_id,
|
||||||
|
# "location_name_to_id": self.location_name_to_id
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#}
|
||||||
|
#filename = f"{self.multiworld.get_out_file_name_base(self.player)}.apae"
|
||||||
|
#with open(os.path.join(output_directory, filename), 'w') as f:
|
||||||
|
# json.dump(data, f)
|
||||||
8
worlds/apeescape/archipelago.json
Normal file
8
worlds/apeescape/archipelago.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"minimum_ap_version": "0.6.4",
|
||||||
|
"world_version": "0.9.6",
|
||||||
|
"authors": ["Thedragon005","IHNN"],
|
||||||
|
"version": 7,
|
||||||
|
"compatible_version": 7,
|
||||||
|
"game": "Ape Escape"
|
||||||
|
}
|
||||||
41
worlds/apeescape/docs/en_Ape Escape.md
Normal file
41
worlds/apeescape/docs/en_Ape Escape.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Ape Escape
|
||||||
|
|
||||||
|
## 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 does randomization do to this game?
|
||||||
|
|
||||||
|
Access to levels is determined by World Keys instead of the standard progression of catching enough monkeys in a level.
|
||||||
|
Which entrance leads to which level can also be randomized.
|
||||||
|
Gadgets are shuffled throughout the multiworld instead of being unlocked at specific points, optionally including the
|
||||||
|
Time Net and Water Net.
|
||||||
|
"Specter Token" items can also be included to unlock the chosen goal boss (Specter in either Monkey Madness or Peak
|
||||||
|
Point Matrix) or as the goal.
|
||||||
|
|
||||||
|
## What items and locations get shuffled?
|
||||||
|
|
||||||
|
The Stun Club, Monkey Radar, Slingback Shooter, Super Hoop, Sky Flyer, RC Car and Magic Punch are always shuffled.
|
||||||
|
The Time Net and Water Net can also be shuffled.
|
||||||
|
The door to the second half of Monkey Madness is also always an item, instead of its vanilla requirement.
|
||||||
|
Monkey Lamps can also be shuffled, unlocking their doors on receiving an item instead of catching enough monkeys.
|
||||||
|
World Keys unlock access to 1, 2 or 3 levels, depending on the options chosen. These can also be turned off.
|
||||||
|
Specter Tokens can also be created, as an optional requirement for goal, similar to a Triforce Hunt.
|
||||||
|
Energy Chips, special pellets, Cookies and even Jackets fill the rest of the item pool.
|
||||||
|
|
||||||
|
Monkeys and non-goal bosses are always locations.
|
||||||
|
Specter Coins and Mailboxes can optionally be included as locations. The mailboxes in the gadget training rooms are
|
||||||
|
not included in this due to technical limitations with the gadget training.
|
||||||
|
Even if Mailbox Shuffle is turned off, the Time Station mailboxes will be included if the Time Net is shuffled.
|
||||||
|
|
||||||
|
## How do I check my settings?
|
||||||
|
|
||||||
|
The mailbox in the center of the Time Station contains a lot of useful information about the seed, including logic
|
||||||
|
difficulties and included tricks, what remote doors are open, and how many Specter Tokens are needed or found.
|
||||||
|
|
||||||
|
## When the player receives an item, what happens?
|
||||||
|
|
||||||
|
A gadget will be added to the inventory, a level or door will be unlocked, or a consumable will be given.
|
||||||
|
Optionally, a message will be sent as a BizHawk message, displayed over the emulator window.
|
||||||
|
There is currently no other in-game indication or sound effect on receiving an item.
|
||||||
53
worlds/apeescape/docs/setup_en.md
Normal file
53
worlds/apeescape/docs/setup_en.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Ape Escape - Setup Guide
|
||||||
|
|
||||||
|
## Required Software
|
||||||
|
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Please use version 0.6.1 or later for integrated
|
||||||
|
BizHawk support.
|
||||||
|
- Ape Escape (USA) ISO or BIN/CUE. Either an original black label version or the Greatest Hits version should work.
|
||||||
|
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 to 2.9.1. Bizhawk version 2.10 or other emulators are **not** supported.
|
||||||
|
- The latest `apeescape.apworld` file. You can find this on the [Releases page](https://github.com/Thedragon005/Archipelago-Ape-Escape/releases/latest). Put this in your `Archipelago/custom_worlds` folder.
|
||||||
|
|
||||||
|
## Configuring BizHawk
|
||||||
|
Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
|
||||||
|
|
||||||
|
- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
|
||||||
|
`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
|
||||||
|
- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
|
||||||
|
tabbed out of EmuHawk.
|
||||||
|
- Under `Config > Preferred Cores > PSX`, select NymaShock.
|
||||||
|
- Open any PlayStation game in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
|
||||||
|
`Controllers…`, it's because you need to load a game first.
|
||||||
|
- Ape Escape is very particular about its analog setup: go to `PSX > Settings` and select `Sync Settings`. Change Virtual
|
||||||
|
Port 1 to Dual Analog to help prevent deadzone issues.
|
||||||
|
You may need to invert Sensitivity for the up/down axis to -100%.
|
||||||
|
This can be found under Analog Controls through `Config > Controllers…`.
|
||||||
|
Depending on your controller, you may also want to tweak the Deadzone. Something like 6% is recommended for a DualShock 4.
|
||||||
|
- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to
|
||||||
|
clear it.
|
||||||
|
|
||||||
|
## Generating a Game
|
||||||
|
|
||||||
|
1. Create your options file (YAML). After installing the `apeescape.apworld` file, you can generate a template within the Archipelago Launcher by clicking `Generate Template Settings`.
|
||||||
|
2. Follow the general Archipelago instructions for [generating a game](https://archipelago.gg/tutorial/Archipelago/setup/en#generating-a-game).
|
||||||
|
3. Open `ArchipelagoLauncher.exe`
|
||||||
|
4. Select "BizHawk Client" in the right-side column. On your first time opening BizHawk Client, you will also be asked to
|
||||||
|
locate `EmuHawk.exe` in your BizHawk install.
|
||||||
|
|
||||||
|
## Connecting to a Server
|
||||||
|
|
||||||
|
1. If EmuHawk didn't launch automatically, open it manually.
|
||||||
|
2. Open your Ape Escape (USA) ISO or CUE file in EmuHawk.
|
||||||
|
3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing. Be careful to avoid clicking "TAStudio" below it in the menu, as this is known to delete your savefile.
|
||||||
|
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 and client will eventually connect to each other. The BizHawk Client window should indicate that it
|
||||||
|
connected and recognized Ape Escape.
|
||||||
|
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.
|
||||||
|
|
||||||
|
You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect.
|
||||||
|
|
||||||
|
## Ending a session
|
||||||
|
- Ape Escape does not automatically save your progress. When you're done playing, either save your game to a virtual
|
||||||
|
memory card in the save station (recommended) or make a savestate to resume later. When reconnecting later, Archipelago
|
||||||
|
will send you any items you received while disconnected.
|
||||||
52
worlds/apeescape/docs/setup_fr.md
Normal file
52
worlds/apeescape/docs/setup_fr.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Ape Escape - Guide de configuration
|
||||||
|
|
||||||
|
## Logiciels Requis
|
||||||
|
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Veuillez utiliser la version 0.6.1 ou ultérieure pour le support intégré dans Bizhawk.
|
||||||
|
- Fichier ISO ou BIN/CUE Ape Escape (USA). Soit une version originale en label noir, soit la version Greatest Hits devrait fonctionner.
|
||||||
|
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) version 2.7 à 2.9.1. La version 2.10 de BizHawk ou tout autre émulateur n'est **pas** pris en charge.
|
||||||
|
- Le fichier `apeescape.apworld` le plus récent. Vous pouvez le trouver sur la [page GitHub](https://github.com/Thedragon005/Archipelago-Ape-Escape/releases/latest). Déposer le fichier dans le dossier `Archipelago/custom_worlds`.
|
||||||
|
|
||||||
|
### Configuration de BizHawk
|
||||||
|
Après avoir installé BizHawk, ouvrez `EmuHawk.exe` et allez changer les configurations suivantes :
|
||||||
|
|
||||||
|
- Si vous utilisez la version 2.7 ou 2.8 de Bizhawk, naviguez jusqu'à `Config > Customize`. Dans l'onglet `Advanced`,
|
||||||
|
changez le paramètre Lua Core de `NLua+KopiLua` à `Lua+LuaInterface`, ensuite redémarrer EmuHawk.
|
||||||
|
(Si vous utilisez la version 2.9 de BizHawk, vous pouvez passer cette étape.)
|
||||||
|
- Sous `Config > Customize`, cochez la case "Run in background" pour empêcher la déconnexion du client quand Bizhawk s'exécute en arrière-plan.
|
||||||
|
- Sous `Config > Preferred Cores > PSX`, sélectionnez NymaShock.
|
||||||
|
- Ouvrez n'importe quel jeu PlayStation dans EmuHawk et allez dans `Config > Controllers…` pour configurer les entrées.
|
||||||
|
Si vous ne pouvez pas cliquer sur `Controllers…`, c'est parce que vous devez d'abord charger un jeu.
|
||||||
|
- Ape Escape est vraiment particulier à propos des configurations analogues : Aller dans `PSX > Settings` et sélectionnez `Sync Settings`. Changez Virtual
|
||||||
|
Port 1 à Dual Analog pour aider à prévenir les problèmes de deadzone.
|
||||||
|
Vous pourriez avoir à inverser la Sensitivité pour l'axe "Up/Down" à -100%.
|
||||||
|
Cela peux être trouvé sous Analog Controls through `Config > Controllers…`.
|
||||||
|
Selon votre contrôleur, vous pourriez aussi vouloir ajuster les Deadzone. Quelque chose aux alentours de 6% est recommandé pour un contrôleur de type DualShock 4.
|
||||||
|
- Envisagez d'effacer les raccourcis clavier dans `Config > Hotkeys…` si vous ne prévoyez pas les utiliser. Sélectionnez
|
||||||
|
le raccourci, puis appuyez sur Esc pour l'effacer.
|
||||||
|
|
||||||
|
## Générer une partie
|
||||||
|
|
||||||
|
1. Créez votre fichier de configuration (YAML). Après avoir installé le fichier `apeescape.apworld`, vous pouvez générer un modèle dans le menu Archipelago Launcher en cliquant sur l'option `Generate Template Settings`.
|
||||||
|
2. Suivez les instructions générales d'Archipelago pour [Générer une partie](https://archipelago.gg/tutorial/Archipelago/setup/en#generating-a-game) (En anglais).
|
||||||
|
3. Ouvrez `ArchipelagoLauncher.exe`
|
||||||
|
4. Sélectionnez "BizHawk Client" dans la colonne de droite. À la première ouverture, on vous demandera également de
|
||||||
|
repérer `EmuHawk.exe` dans votre installation de Bizhawk.
|
||||||
|
|
||||||
|
## Se connecter à un Serveur
|
||||||
|
|
||||||
|
1. Si EmuHawk n'a pas démarré automatiquement, ouvrez-le manuellement.
|
||||||
|
2. Ouvrez votre fichier ISO ou CUE d'Ape Escape (USA) dans EmuHawk.
|
||||||
|
3. Dans EmuHawk, allez à `Tools > Lua Console`. Cette fenêtre doit rester ouverte quand vous jouez. Faites attention de ne pas cliquer sur "TAStudio" directement en dessous, car cela est connu pour supprimer votre sauvegarde.
|
||||||
|
4. Dans la fenêtre Lua Console, allez à `Script > Open Script…`.
|
||||||
|
5. Naviguez jusqu'à votre répertoire d'installation Archipelago et ouvrez `data/lua/connector_bizhawk_generic.lua`.
|
||||||
|
6. L'émulateur et le client vont éventuellement se connecter l'un à l'autre. La fenêtre Bizhawk Client devrait indiquer qu'il s'est connecté et a reconnu Ape Escape.
|
||||||
|
7. Pour connecter le client au serveur, entrez l'adresse de la salle et le port (ex. `archipelago.gg:38281`) dans le
|
||||||
|
champ situé en haut du client et appuyez sur Connect.
|
||||||
|
|
||||||
|
Vous devriez maintenant être en mesure de recevoir et d'envoyer des objets. Vous devrez faire ces étapes chaque fois que vous voulez vous reconnecter.
|
||||||
|
|
||||||
|
## Terminer une session
|
||||||
|
- Ape Escape ne sauvegarde pas automatiquement votre progression. Quand vous avez fini de jouer, sauvegardez soit votre
|
||||||
|
partie dans une carte mémoire virtuelle à partir de la station de sauvegarde (recommandé) ou bien effectuez une
|
||||||
|
sauvegarde d'état (savestate) pour pouvoir reprendre plus tard. Lors d'une reconnexion ultérieure, Archipelago
|
||||||
|
vous enverra tous les objets que vous avez reçus pendant la déconnexion.
|
||||||
4
worlds/apeescape/test/__init__.py
Normal file
4
worlds/apeescape/test/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from test.bases import WorldTestBase
|
||||||
|
|
||||||
|
class MyGameTestBase(WorldTestBase):
|
||||||
|
game = "Ape Escape"
|
||||||
1298
worlds/apeescape3/AE3_Client.py
Normal file
1298
worlds/apeescape3/AE3_Client.py
Normal file
File diff suppressed because it is too large
Load Diff
791
worlds/apeescape3/AE3_Interface.py
Normal file
791
worlds/apeescape3/AE3_Interface.py
Normal file
@@ -0,0 +1,791 @@
|
|||||||
|
from typing import Optional, Sequence
|
||||||
|
from logging import Logger
|
||||||
|
from enum import Enum
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
|
from .data.Addresses import VersionAddresses, get_version_addresses
|
||||||
|
from .data.Items import HUD_OFFSETS
|
||||||
|
from .data.Locations import CELLPHONES_ID_DUPLICATES, CELLPHONES_STAGE_DUPLICATES, LOCATIONS_ALTERNATIVE
|
||||||
|
from .data.Stages import LEVELS_ID_BY_ORDER
|
||||||
|
from .data.Strings import Itm, Loc, Meta, Game, APHelper, APConsole
|
||||||
|
from .interface.pine import Pine
|
||||||
|
|
||||||
|
|
||||||
|
### [< --- HELPERS --- >]
|
||||||
|
class ConnectionStatus(Enum):
|
||||||
|
WRONG_GAME = -1
|
||||||
|
DISCONNECTED = 0
|
||||||
|
CONNECTED = 1
|
||||||
|
IN_GAME = 2
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return self.value > 0
|
||||||
|
|
||||||
|
|
||||||
|
### [< --- INTERFACE --- >]
|
||||||
|
class AEPS2Interface:
|
||||||
|
pine : Pine = Pine()
|
||||||
|
status : ConnectionStatus = ConnectionStatus.DISCONNECTED
|
||||||
|
|
||||||
|
loaded_game : Optional[str] = None
|
||||||
|
addresses : VersionAddresses = None
|
||||||
|
|
||||||
|
sync_task = None
|
||||||
|
logger : Logger
|
||||||
|
|
||||||
|
def __init__(self, logger : Logger, slot: int = 28011, linux_platform: str = "auto"):
|
||||||
|
self.logger = logger
|
||||||
|
self.pine = Pine(slot, linux_platform)
|
||||||
|
|
||||||
|
self.active_slot = slot
|
||||||
|
self.active_platform = self.pine.active_platform
|
||||||
|
|
||||||
|
# { PINE Network }
|
||||||
|
def connect_game(self):
|
||||||
|
# Check for connection with PCSX2
|
||||||
|
if not self.pine.is_connected():
|
||||||
|
self.pine.connect()
|
||||||
|
|
||||||
|
if not self.pine.is_connected():
|
||||||
|
self.status = ConnectionStatus.DISCONNECTED
|
||||||
|
|
||||||
|
self.active_slot = None
|
||||||
|
self.active_platform = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.info(APConsole.Info.init.value)
|
||||||
|
|
||||||
|
self.active_slot = self.pine.active_slot
|
||||||
|
self.active_platform = self.pine.active_platform
|
||||||
|
|
||||||
|
# Check for Game running in PCSX2
|
||||||
|
try:
|
||||||
|
if self.status is ConnectionStatus.CONNECTED:
|
||||||
|
self.logger.info(APConsole.Info.p_init_g.value)
|
||||||
|
|
||||||
|
game_id : str = self.pine.get_game_id()
|
||||||
|
|
||||||
|
self.loaded_game = None
|
||||||
|
|
||||||
|
if game_id in Meta.supported_versions:
|
||||||
|
self.loaded_game = game_id
|
||||||
|
self.addresses = get_version_addresses(self.loaded_game)
|
||||||
|
self.status = ConnectionStatus.IN_GAME
|
||||||
|
elif not self.status is ConnectionStatus.WRONG_GAME:
|
||||||
|
self.logger.warning(APConsole.Err.game_wrong.value)
|
||||||
|
self.status = ConnectionStatus.WRONG_GAME
|
||||||
|
except RuntimeError:
|
||||||
|
return
|
||||||
|
except ConnectionError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.status is ConnectionStatus.DISCONNECTED:
|
||||||
|
self.status = ConnectionStatus.CONNECTED
|
||||||
|
|
||||||
|
def disconnect_game(self, status: int = 0):
|
||||||
|
self.pine.disconnect()
|
||||||
|
self.loaded_game = None
|
||||||
|
|
||||||
|
if status:
|
||||||
|
self.logger.info(APConsole.Err.sock_disc.value)
|
||||||
|
else:
|
||||||
|
self.logger.info("[-!-] Closed connection to PCSX2.")
|
||||||
|
|
||||||
|
def get_connection_state(self) -> bool:
|
||||||
|
try:
|
||||||
|
connected : bool = self.pine.is_connected()
|
||||||
|
|
||||||
|
return not (not connected or self.loaded_game is None)
|
||||||
|
except RuntimeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_slot(self, slot: int) -> None:
|
||||||
|
self.pine.set_slot(slot)
|
||||||
|
|
||||||
|
def set_linux_platform(self, linux_platform: str = "auto") -> None:
|
||||||
|
self.pine.set_linux_platform(linux_platform)
|
||||||
|
|
||||||
|
# { Generic }
|
||||||
|
def follow_pointer_chain(self, start_address : int, pointer_chain : str) -> int:
|
||||||
|
# Get first pointer
|
||||||
|
addr : int = self.pine.read_int32(start_address)
|
||||||
|
|
||||||
|
# If pointer is 0, return immediately
|
||||||
|
if addr <= 0x0:
|
||||||
|
return 0x0
|
||||||
|
|
||||||
|
# Loop through remaining pointers and adding the offsets
|
||||||
|
ptrs : Sequence = self.addresses.Pointers[pointer_chain]
|
||||||
|
amt : int = len(ptrs) - 1
|
||||||
|
for i, offset in enumerate(self.addresses.Pointers[pointer_chain]):
|
||||||
|
addr += offset
|
||||||
|
|
||||||
|
# Do not read value for the last offset
|
||||||
|
if i >= amt:
|
||||||
|
return addr
|
||||||
|
|
||||||
|
addr = self.pine.read_int32(addr)
|
||||||
|
|
||||||
|
# Getting an Address of 0 means the pointer has not been set yet
|
||||||
|
if addr == 0x0:
|
||||||
|
return 0x0
|
||||||
|
|
||||||
|
return 0x0
|
||||||
|
|
||||||
|
# { Game Check }
|
||||||
|
def get_progress(self) -> str:
|
||||||
|
addr : int = self.addresses.GameStates[Game.progress.value]
|
||||||
|
addr = self.follow_pointer_chain(addr, Game.progress.value)
|
||||||
|
|
||||||
|
if addr == 0:
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
value: bytes = self.pine.read_bytes(addr, 8)
|
||||||
|
value_decoded: str = bytes.decode(value).replace("\x00", "")
|
||||||
|
return value_decoded
|
||||||
|
|
||||||
|
def get_unlocked_channels(self) -> int:
|
||||||
|
return self.pine.read_int32(self.addresses.GameStates[Game.channels_unlocked.value])
|
||||||
|
|
||||||
|
def get_selected_channel(self) -> int:
|
||||||
|
return self.pine.read_int32(self.addresses.GameStates[Game.channel_selected.value])
|
||||||
|
|
||||||
|
def get_next_channel_choice(self) -> str:
|
||||||
|
addr: int = self.follow_pointer_chain(self.addresses.GameStates[Game.progress.value],
|
||||||
|
Game.channel_next_choice.value)
|
||||||
|
|
||||||
|
if addr == 0x0:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
length: int = 4
|
||||||
|
|
||||||
|
# Check length of string in multiples of 4
|
||||||
|
for _ in range(2):
|
||||||
|
if self.pine.read_bytes(addr + (4 * (_ + 1)), 1) == b'\x00':
|
||||||
|
break
|
||||||
|
|
||||||
|
length = max(length + 4, 12)
|
||||||
|
|
||||||
|
id_as_bytes : bytes = self.pine.read_bytes(self.addresses.GameStates[Game.current_channel.value], length)
|
||||||
|
|
||||||
|
# Convert to String
|
||||||
|
return id_as_bytes.decode("utf-8").replace("\x00", "")
|
||||||
|
|
||||||
|
def get_channel(self) -> str:
|
||||||
|
channel_as_bytes : bytes = self.pine.read_bytes(self.addresses.GameStates[Game.current_channel.value], 4)
|
||||||
|
# Decode to String and remove null bytes if present
|
||||||
|
return channel_as_bytes.decode("utf-8").replace("\x00", "")
|
||||||
|
|
||||||
|
def get_stage(self) -> str:
|
||||||
|
address : int = self.addresses.GameStates[Game.current_room.value]
|
||||||
|
length : int = 4
|
||||||
|
|
||||||
|
# Check length of string in multiples of 4
|
||||||
|
for _ in range(2):
|
||||||
|
if self.pine.read_bytes(address + (4 * (_ + 1)), 1) == b'\x00':
|
||||||
|
break
|
||||||
|
|
||||||
|
length = max(length + 4, 12)
|
||||||
|
|
||||||
|
# Decode to string and remove null bytes
|
||||||
|
room_as_bytes : bytes = self.pine.read_bytes(self.addresses.GameStates[Game.current_room.value], length)
|
||||||
|
return room_as_bytes.decode("utf-8").replace("\x00", "")
|
||||||
|
|
||||||
|
def get_activated_game_mode(self) -> int:
|
||||||
|
address = self.addresses.GameStates[Game.game_mode.value]
|
||||||
|
|
||||||
|
return self.pine.read_int32(address)
|
||||||
|
|
||||||
|
def get_current_game_mode(self) -> int:
|
||||||
|
address = self.follow_pointer_chain(self.addresses.GameStates[Game.status_tracker.value], Game.game_mode.value)
|
||||||
|
if not address:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
return self.pine.read_int32(address)
|
||||||
|
|
||||||
|
def check_in_stage(self) -> bool:
|
||||||
|
value : int = self.pine.read_int8(self.addresses.GameStates[Game.current_channel.value])
|
||||||
|
return value > 0
|
||||||
|
|
||||||
|
def is_on_warp_gate(self) -> bool:
|
||||||
|
value : int = self.pine.read_int8(self.addresses.GameStates[Game.on_warp_gate.value])
|
||||||
|
return value != 0
|
||||||
|
|
||||||
|
def is_a_level_confirmed(self) -> bool:
|
||||||
|
value: int = self.pine.read_int8(self.addresses.GameStates[Game.channel_confirmed.value])
|
||||||
|
return value != 0
|
||||||
|
|
||||||
|
def get_character(self) -> int:
|
||||||
|
return self.pine.read_int32(self.addresses.GameStates[Game.character.value])
|
||||||
|
|
||||||
|
def get_jackets(self) -> int:
|
||||||
|
return self.pine.read_int32(self.addresses.GameStates[Game.jackets.value])
|
||||||
|
|
||||||
|
def get_cookies(self) -> float:
|
||||||
|
return self.pine.read_float(self.addresses.GameStates[Game.cookies.value])
|
||||||
|
|
||||||
|
def get_morph_gauge_recharge_value(self) -> float:
|
||||||
|
return self.pine.read_float(self.addresses.GameStates[Game.morph_gauge_recharge.value])
|
||||||
|
|
||||||
|
def get_morph_stock(self):
|
||||||
|
return int(self.pine.read_float(self.addresses.GameStates[Game.morph_stocks.value]) / 100)
|
||||||
|
|
||||||
|
def get_coins(self):
|
||||||
|
return int(self.pine.read_int32(self.addresses.GameStates[Game.chips.value]))
|
||||||
|
|
||||||
|
def is_equipment_unlocked(self, address_name : str) -> bool:
|
||||||
|
# Redirect address to RC Car if the unlocked equipment is an RC Car Chassis
|
||||||
|
if "Chassis" in address_name:
|
||||||
|
is_variant_unlocked = self.is_chassis_unlocked(address_name)
|
||||||
|
address_name = Itm.gadget_rcc.value
|
||||||
|
else:
|
||||||
|
is_variant_unlocked = True
|
||||||
|
|
||||||
|
return self.pine.read_int32(self.addresses.Items[address_name]) == 0x2 and is_variant_unlocked
|
||||||
|
|
||||||
|
def is_chassis_unlocked(self, chassis_name : str) -> bool:
|
||||||
|
if chassis_name not in Itm.get_chassis_by_id():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.pine.read_int8(self.addresses.Items[chassis_name]) == 0x1
|
||||||
|
|
||||||
|
def is_real_chassis_unlocked(self, chassis_name : str) -> bool:
|
||||||
|
if chassis_name not in Itm.get_real_chassis_by_id():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self.pine.read_int8(self.addresses.Items[chassis_name]) == 0x1
|
||||||
|
|
||||||
|
def get_current_morph(self):
|
||||||
|
return self.pine.read_int8(self.addresses.GameStates[Game.current_morph.value])
|
||||||
|
|
||||||
|
def get_morph_duration(self, character : int = 0) -> float:
|
||||||
|
return self.pine.read_int32(self.addresses.get_morph_duration_addresses(character)[0])
|
||||||
|
|
||||||
|
def get_player_state(self) -> int:
|
||||||
|
return self.pine.read_int32(self.addresses.GameStates[Game.state.value])
|
||||||
|
|
||||||
|
def get_current_gadget(self) -> int:
|
||||||
|
address : int = self.follow_pointer_chain(self.addresses.GameStates[Game.equip_current.value],
|
||||||
|
Game.equip_current.value)
|
||||||
|
|
||||||
|
if address == 0x0:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
return self.pine.read_int8(address)
|
||||||
|
|
||||||
|
def is_on_water(self) -> bool:
|
||||||
|
return self.get_current_gadget() == 0xB
|
||||||
|
|
||||||
|
def is_in_control(self) -> bool:
|
||||||
|
state : int = self.get_player_state()
|
||||||
|
|
||||||
|
return state != 0x00 and state != 0x02
|
||||||
|
|
||||||
|
def is_selecting_morph(self) -> bool:
|
||||||
|
return self.get_player_state() == 0x03
|
||||||
|
|
||||||
|
def get_button_pressed(self) -> int:
|
||||||
|
return self.pine.read_int8(self.addresses.GameStates[Game.pressed.value])
|
||||||
|
|
||||||
|
def check_screen_fading(self) -> int:
|
||||||
|
return self.pine.read_int8(self.addresses.GameStates[Game.screen_fade.value])
|
||||||
|
|
||||||
|
def get_screen_fade_count(self) -> int:
|
||||||
|
return self.pine.read_int8(self.addresses.GameStates[Game.screen_fade_count.value])
|
||||||
|
|
||||||
|
def get_gui_status(self) -> int:
|
||||||
|
return self.pine.read_int8(self.addresses.GameStates[Game.gui_status.value])
|
||||||
|
|
||||||
|
def is_location_checked(self, name : str) -> bool:
|
||||||
|
address : int = self.addresses.Locations[name]
|
||||||
|
alt_address : int = address
|
||||||
|
alt_checked : bool = False
|
||||||
|
|
||||||
|
# Check Permanent State Storage Addresses as well for locations that use it
|
||||||
|
has_alt : bool = name in LOCATIONS_ALTERNATIVE.keys()
|
||||||
|
if has_alt:
|
||||||
|
alt_address = self.addresses.Locations[LOCATIONS_ALTERNATIVE[name]]
|
||||||
|
alt_checked = self.pine.read_int8(alt_address) == 0x01
|
||||||
|
|
||||||
|
if not alt_checked:
|
||||||
|
checked : bool = self.pine.read_int8(address) == 0x01
|
||||||
|
|
||||||
|
# Mark the Permanent Address as well if the original address is checked
|
||||||
|
if has_alt and checked:
|
||||||
|
self.pine.write_int8(alt_address, 0x01)
|
||||||
|
else:
|
||||||
|
checked : bool = True
|
||||||
|
|
||||||
|
return checked
|
||||||
|
|
||||||
|
def is_data_desk_interacted(self):
|
||||||
|
address: int = self.follow_pointer_chain(self.addresses.GameStates[Game.interact_data.value],
|
||||||
|
Game.interact_data.value)
|
||||||
|
address += self.addresses.GameStates[Game.data_desk.value]
|
||||||
|
|
||||||
|
# Return False when the address is invalid
|
||||||
|
if address <= 0x0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
as_bytes: bytes = self.pine.read_bytes(address, 4)
|
||||||
|
try:
|
||||||
|
as_string: str = as_bytes.decode().replace("\x00", "")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return as_string == Game.save.value
|
||||||
|
|
||||||
|
def is_in_monkey_mart(self):
|
||||||
|
address: int = self.follow_pointer_chain(self.addresses.GameStates[Game.interact_data.value],
|
||||||
|
Game.interact_data.value)
|
||||||
|
address += self.addresses.GameStates[Game.shop.value]
|
||||||
|
|
||||||
|
# Return False when the address is invalid
|
||||||
|
if address <= 0x0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
as_bytes: bytes = self.pine.read_bytes(address, 8)
|
||||||
|
try:
|
||||||
|
as_string: str = as_bytes.decode().replace("\x00", "")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return as_string == Game.shop_super.value
|
||||||
|
|
||||||
|
def is_camera_interacted(self) -> bool:
|
||||||
|
address : int = self.follow_pointer_chain(self.addresses.GameStates[Game.interact_data.value],
|
||||||
|
Game.interact_data.value)
|
||||||
|
address += self.addresses.GameStates[Game.pipo_camera.value]
|
||||||
|
|
||||||
|
# Return False when the address is invalid
|
||||||
|
if address <= 0x0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
as_bytes : bytes = self.pine.read_bytes(address, 5)
|
||||||
|
# Try to decode to string, and immediately return if it cannot be decoded
|
||||||
|
try:
|
||||||
|
as_string: str = as_bytes.decode().replace("\x00", "")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return as_string == Game.conte.value
|
||||||
|
|
||||||
|
def get_cellphone_interacted(self, stage : str = "") -> str:
|
||||||
|
base_address : int = self.follow_pointer_chain(self.addresses.GameStates[Game.interact_data.value],
|
||||||
|
Game.interact_data.value)
|
||||||
|
address : int = base_address + self.addresses.GameStates[Game.cellphone.value]
|
||||||
|
|
||||||
|
# Return an empty string if either addresses return 0
|
||||||
|
if not address <= 0x0:
|
||||||
|
as_bytes: bytes = self.pine.read_bytes(address, 3)
|
||||||
|
|
||||||
|
# Try to decode to string, and immediately return if it cannot be decoded
|
||||||
|
try:
|
||||||
|
as_string : str = as_bytes.decode().replace("\x00", "")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
as_string = ""
|
||||||
|
|
||||||
|
if as_string.isdigit():
|
||||||
|
if as_string in CELLPHONES_ID_DUPLICATES and stage in CELLPHONES_STAGE_DUPLICATES:
|
||||||
|
as_string = as_string.replace("0", "1", 1)
|
||||||
|
return as_string
|
||||||
|
|
||||||
|
# Use alternative Cellphone address when the first one fails
|
||||||
|
address = base_address + self.addresses.GameStates[Game.cellphone2.value]
|
||||||
|
|
||||||
|
if address <= 0x0:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
as_bytes: bytes = self.pine.read_bytes(address, 3)
|
||||||
|
|
||||||
|
try:
|
||||||
|
as_string : str = as_bytes.decode().replace("\x00", "")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
as_string = ""
|
||||||
|
|
||||||
|
if as_string.isdigit():
|
||||||
|
if as_string in CELLPHONES_ID_DUPLICATES and stage in CELLPHONES_STAGE_DUPLICATES:
|
||||||
|
as_string = as_string.replace("0", "1", 1)
|
||||||
|
return as_string
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def is_saving(self) -> bool:
|
||||||
|
address : int = self.follow_pointer_chain(self.addresses.GameStates[Game.interact_data.value],
|
||||||
|
Game.save.value)
|
||||||
|
value : bytes = self.pine.read_bytes(address, 4)
|
||||||
|
|
||||||
|
try:
|
||||||
|
decoded : str = bytes.decode(value).replace("\x00", "")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
boolean : bool = decoded == Game.save.value
|
||||||
|
return boolean
|
||||||
|
|
||||||
|
def is_in_pink_boss(self) -> bool:
|
||||||
|
return self.pine.read_int8(self.addresses.GameStates[Game.in_pink_stage.value]) == 0x02
|
||||||
|
|
||||||
|
def is_tomoki_defeated(self) -> bool:
|
||||||
|
# Check Permanent Address first
|
||||||
|
permanent_checked : bool = self.is_location_checked(Loc.boss_alt_tomoki.value)
|
||||||
|
|
||||||
|
if permanent_checked:
|
||||||
|
return True
|
||||||
|
|
||||||
|
address : int = self.follow_pointer_chain(self.addresses.Locations[Loc.boss_tomoki.value],
|
||||||
|
Loc.boss_tomoki.value)
|
||||||
|
|
||||||
|
# Return false if pointer is still not initialized
|
||||||
|
if address <= 0x0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
value : float = self.pine.read_float(address)
|
||||||
|
|
||||||
|
# Change the State value in Dr. Tomoki's Permanent State Address
|
||||||
|
if value <= 0.0:
|
||||||
|
self.mark_location(Loc.boss_alt_tomoki.value)
|
||||||
|
|
||||||
|
return value <= 0.0
|
||||||
|
|
||||||
|
def get_last_item_index(self) -> int:
|
||||||
|
return self.pine.read_int32(self.addresses.GameStates[Game.last_item_index.value])
|
||||||
|
|
||||||
|
def get_persistent_cookie_value(self) -> int:
|
||||||
|
return self.pine.read_int8(self.addresses.GameStates[Game.last_cookies.value])
|
||||||
|
|
||||||
|
def get_persistent_morph_energy_value(self) -> int:
|
||||||
|
return self.pine.read_int8(self.addresses.GameStates[Game.last_morph_energy.value])
|
||||||
|
|
||||||
|
def get_persistent_morph_stock_value(self) -> int:
|
||||||
|
return self.pine.read_int8(self.addresses.GameStates[Game.last_morph_stock.value])
|
||||||
|
|
||||||
|
def get_shop_morph_stock_checked(self) -> int:
|
||||||
|
return self.pine.read_int8(self.addresses.GameStates[Game.shop_morph_stock.value])
|
||||||
|
|
||||||
|
# { Game Manipulation }
|
||||||
|
def set_progress(self, progress : str = APHelper.pr_round2.value):
|
||||||
|
addr : int = self.addresses.GameStates[Game.progress.value]
|
||||||
|
addr = self.follow_pointer_chain(addr, Game.progress.value)
|
||||||
|
|
||||||
|
if addr == 0x0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Clear out current value
|
||||||
|
clearing_address: int = addr
|
||||||
|
for _ in range(6):
|
||||||
|
self.pine.write_int32(clearing_address, 0x0)
|
||||||
|
self.pine.write_int32(clearing_address, 0x0)
|
||||||
|
clearing_address += 4
|
||||||
|
|
||||||
|
as_bytes : bytes = progress.encode() + b'\x00'
|
||||||
|
self.pine.write_bytes(addr, as_bytes)
|
||||||
|
|
||||||
|
def set_unlocked_stages(self, index : int):
|
||||||
|
self.pine.write_int32(self.addresses.GameStates[Game.channels_unlocked.value], index)
|
||||||
|
|
||||||
|
def set_selected_channel(self, index : int):
|
||||||
|
self.pine.write_int32(self.addresses.GameStates[Game.channel_selected.value], index)
|
||||||
|
|
||||||
|
def set_next_channel_choice(self, index : int):
|
||||||
|
if index > len(LEVELS_ID_BY_ORDER):
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
addr: int = self.follow_pointer_chain(self.addresses.GameStates[Game.progress.value],
|
||||||
|
Game.channel_next_choice.value)
|
||||||
|
|
||||||
|
if addr == 0x0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Clear out current value
|
||||||
|
clearing_address: int = addr
|
||||||
|
for _ in range(6):
|
||||||
|
self.pine.write_int32(clearing_address, 0x0)
|
||||||
|
self.pine.write_int32(clearing_address, 0x0)
|
||||||
|
clearing_address += 4
|
||||||
|
|
||||||
|
# Convert ID to bytes
|
||||||
|
id_as_bytes : bytes = LEVELS_ID_BY_ORDER[index].encode() + b'\x00'
|
||||||
|
|
||||||
|
# Write new value
|
||||||
|
self.pine.write_bytes(addr, id_as_bytes)
|
||||||
|
|
||||||
|
def reset_level_confirm_status(self):
|
||||||
|
self.pine.write_int8(self.addresses.GameStates[Game.channel_confirmed.value], 0x0)
|
||||||
|
|
||||||
|
def set_change_area_destination(self, area : str):
|
||||||
|
as_bytes : bytes = area.encode() + b'\x00'
|
||||||
|
self.pine.write_bytes(self.addresses.GameStates[Game.area_dest.value], as_bytes)
|
||||||
|
|
||||||
|
def set_enter_norma_destination(self, area : str):
|
||||||
|
as_bytes : bytes = area.encode() + b'\x00'
|
||||||
|
self.pine.write_bytes(self.addresses.GameStates[Game.enter_norma.value], as_bytes)
|
||||||
|
|
||||||
|
def clear_spawn(self):
|
||||||
|
spawn_address : int = self.addresses.GameStates[Game.spawn.value]
|
||||||
|
dest_address : int = self.addresses.GameStates[Game.area_dest.value]
|
||||||
|
for _ in range(6):
|
||||||
|
self.pine.write_int32(spawn_address, 0x0)
|
||||||
|
self.pine.write_int32(dest_address, 0x0)
|
||||||
|
spawn_address += 4
|
||||||
|
dest_address += 4
|
||||||
|
|
||||||
|
def clear_norma(self):
|
||||||
|
norma_address : int = self.addresses.GameStates[Game.enter_norma.value]
|
||||||
|
for _ in range(6):
|
||||||
|
self.pine.write_int32(norma_address, 0x0)
|
||||||
|
norma_address += 4
|
||||||
|
|
||||||
|
def set_game_mode(self, mode : int = 0x100, restart : bool = True):
|
||||||
|
address = self.addresses.GameStates[Game.game_mode.value]
|
||||||
|
|
||||||
|
self.pine.write_int32(address, mode)
|
||||||
|
|
||||||
|
if restart:
|
||||||
|
self.send_command(Game.restart_stage.value)
|
||||||
|
|
||||||
|
def set_cookies(self, amount : float):
|
||||||
|
self.pine.write_float(self.addresses.GameStates[Game.cookies.value], amount)
|
||||||
|
|
||||||
|
def set_morph_gauge_recharge(self, amount : float):
|
||||||
|
self.pine.write_float(self.addresses.GameStates[Game.morph_gauge_recharge.value], amount)
|
||||||
|
|
||||||
|
def clear_equipment(self):
|
||||||
|
for button in self.addresses.BUTTONS_BY_INTERNAL:
|
||||||
|
self.pine.write_int32(button, 0x0)
|
||||||
|
|
||||||
|
def unlock_equipment(self, address_name : str, auto_equip : bool = False, is_in_shop : bool = False):
|
||||||
|
is_equipped : int = False
|
||||||
|
|
||||||
|
# Redirect address to RC Car if the unlocked equipment is an RC Car Chassis
|
||||||
|
if "Chassis" in address_name:
|
||||||
|
is_equipped = self.unlock_chassis(address_name, is_in_shop)
|
||||||
|
address : int = self.addresses.Items[Itm.gadget_rcc.value]
|
||||||
|
address_name = Itm.gadget_rcc.value
|
||||||
|
else:
|
||||||
|
address : int = self.addresses.Items[address_name]
|
||||||
|
|
||||||
|
self.pine.write_int32(self.addresses.Items[address_name], 0x2)
|
||||||
|
|
||||||
|
if auto_equip and not is_equipped and address_name in Itm.get_gadgets_ordered():
|
||||||
|
self.auto_equip(self.addresses.get_gadget_id(address))
|
||||||
|
|
||||||
|
def unlock_chassis(self, address_name : str, is_in_shop : bool = False) -> bool:
|
||||||
|
if address_name in Itm.get_chassis_by_id(True):
|
||||||
|
id : int = Itm.get_chassis_by_id(True).index(address_name)
|
||||||
|
self.pine.write_int8(self.addresses.Items[address_name], 0x1)
|
||||||
|
|
||||||
|
if not is_in_shop:
|
||||||
|
self.pine.write_int8(self.addresses.Items[Itm.get_real_chassis_by_id()[id]], 0x0)
|
||||||
|
|
||||||
|
is_rcc_unlocked : bool = self.pine.read_int32(self.addresses.Items[Itm.gadget_rcc.value]) == 0x2
|
||||||
|
|
||||||
|
return is_rcc_unlocked
|
||||||
|
|
||||||
|
def unlock_chassis_direct(self, chassis_idx):
|
||||||
|
chassis : str = Itm.get_real_chassis_by_id()[chassis_idx]
|
||||||
|
self.pine.write_int8(self.addresses.Items[chassis], 0x1)
|
||||||
|
|
||||||
|
def lock_chassis_direct(self, chassis_idx):
|
||||||
|
chassis : str = Itm.get_real_chassis_by_id()[chassis_idx]
|
||||||
|
|
||||||
|
if chassis:
|
||||||
|
self.pine.write_int8(self.addresses.Items[chassis], 0x0)
|
||||||
|
|
||||||
|
def set_chassis_direct(self, chassis_idx : int):
|
||||||
|
self.pine.write_int32(self.addresses.GameStates[Game.equip_chassis_active.value], chassis_idx)
|
||||||
|
|
||||||
|
def lock_equipment(self, address_name : str):
|
||||||
|
self.pine.write_int32(self.addresses.Items[address_name], 0x1)
|
||||||
|
|
||||||
|
def auto_equip(self, gadget_id: int):
|
||||||
|
if gadget_id <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
target : int = -1
|
||||||
|
for button in self.addresses.BUTTONS_BY_INTUIT:
|
||||||
|
value = self.pine.read_int32(button)
|
||||||
|
|
||||||
|
# Do not auto-equip when gadget is already assigned
|
||||||
|
if value == gadget_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
if value != 0x0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if target < 0:
|
||||||
|
target = button
|
||||||
|
continue
|
||||||
|
|
||||||
|
if target >= 0:
|
||||||
|
self.pine.write_int32(target, gadget_id)
|
||||||
|
|
||||||
|
def check_pgc_cache(self) -> bool:
|
||||||
|
return self.pine.read_int8(self.addresses.GameStates[Game.pgc_cache.value]) == 0x1
|
||||||
|
|
||||||
|
def set_pgc_cache(self):
|
||||||
|
self.pine.write_int8(self.addresses.GameStates[Game.pgc_cache.value], 0x1)
|
||||||
|
|
||||||
|
def set_morph_duration(self, character : int, duration : float, dummy : str = ""):
|
||||||
|
if character < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
durations : Sequence[int] = self.addresses.get_morph_duration_addresses(character)
|
||||||
|
if not durations:
|
||||||
|
return
|
||||||
|
|
||||||
|
dummy_index : int = Itm.get_morphs_ordered().index(dummy) if dummy else - 1
|
||||||
|
|
||||||
|
for idx, morph in enumerate(durations):
|
||||||
|
duration_to_set : float = duration
|
||||||
|
# Set duration to 0 if not specified in morphs and exclusive is false
|
||||||
|
if dummy and idx == dummy_index:
|
||||||
|
duration_to_set = 0.0
|
||||||
|
|
||||||
|
self.pine.write_float(morph, duration_to_set)
|
||||||
|
|
||||||
|
def set_morph_stock(self, stocks : int):
|
||||||
|
self.pine.write_float(self.addresses.GameStates[Game.morph_stocks.value],stocks * 100)
|
||||||
|
|
||||||
|
def give_collectable(self, address_name : str, amount : int | float = 0x1, maximum : int | float = 0x0,
|
||||||
|
is_in_shop : bool = False, stocks_shuffled: bool = False, monkey_mart:bool = True):
|
||||||
|
address : int = self.addresses.GameStates[address_name]
|
||||||
|
|
||||||
|
use_main: bool = True
|
||||||
|
if is_in_shop and address_name in [Game.morph_stocks.value, Game.cookies.value]:
|
||||||
|
use_main = False
|
||||||
|
if stocks_shuffled and address_name == Game.morph_stocks.value:
|
||||||
|
current = self.get_persistent_morph_stock_value()
|
||||||
|
self.set_persistent_morph_stock_value(current + 1)
|
||||||
|
elif address_name == Game.cookies.value:
|
||||||
|
if not monkey_mart:
|
||||||
|
current = self.get_persistent_cookie_value()
|
||||||
|
self.set_persistent_cookie_value(min(int(current + amount), 100))
|
||||||
|
else:
|
||||||
|
use_main = True
|
||||||
|
|
||||||
|
if use_main:
|
||||||
|
value: int = 0
|
||||||
|
|
||||||
|
if isinstance(amount, int):
|
||||||
|
current: int = self.pine.read_int32(address)
|
||||||
|
|
||||||
|
value = min(current + amount, maximum)
|
||||||
|
self.pine.write_int32(address, value)
|
||||||
|
elif isinstance(amount, float):
|
||||||
|
current: float = self.pine.read_float(address)
|
||||||
|
|
||||||
|
value = int(min(current + amount, maximum))
|
||||||
|
self.pine.write_float(address, min(current + amount, maximum))
|
||||||
|
|
||||||
|
self.update_hud(address_name, value)
|
||||||
|
|
||||||
|
def update_hud(self, address_name : str, value : int):
|
||||||
|
if address_name not in HUD_OFFSETS:
|
||||||
|
return
|
||||||
|
|
||||||
|
address = self.follow_pointer_chain(self.addresses.GameStates[Game.hud_pointer.value], Game.hud_pointer.value)
|
||||||
|
if address <= 0x0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Apply Offset
|
||||||
|
address += HUD_OFFSETS[address_name]
|
||||||
|
|
||||||
|
# Get byte length of data and use the correct write function accordingly
|
||||||
|
size : int = ceil(value.bit_length() / 8)
|
||||||
|
|
||||||
|
if size <= 1:
|
||||||
|
self.pine.write_int8(address, value)
|
||||||
|
elif 1 < size <= 2:
|
||||||
|
self.pine.write_int16(address, value)
|
||||||
|
elif size > 2:
|
||||||
|
self.pine.write_int32(address, value)
|
||||||
|
|
||||||
|
def give_morph_energy(self, amount : float = 3.0):
|
||||||
|
# Check recharge state first
|
||||||
|
address : int = self.addresses.GameStates[Game.morph_gauge_recharge.value]
|
||||||
|
current : float = self.pine.read_float(address)
|
||||||
|
|
||||||
|
if current != 0x0:
|
||||||
|
# Ranges from 0 to 100 for every Morph Stock, with a maximum of 1100 for all 10 Stocks filled.
|
||||||
|
self.pine.write_float(address, current + (amount / 30.0 * 100.0))
|
||||||
|
return
|
||||||
|
|
||||||
|
# If recharge state is 0, we check the active gauge, following its pointer chain
|
||||||
|
address = self.follow_pointer_chain(self.addresses.GameStates[Game.morph_gauge_active.value],
|
||||||
|
Game.morph_gauge_active.value)
|
||||||
|
|
||||||
|
if address == 0x0:
|
||||||
|
return
|
||||||
|
|
||||||
|
current = self.pine.read_float(address)
|
||||||
|
# Ranges from 0 to 30 in vanilla game.
|
||||||
|
self.pine.write_float(address, current + amount)
|
||||||
|
|
||||||
|
def set_morph_gauge_charge(self, amount : float = 0.0):
|
||||||
|
# Check recharge state first
|
||||||
|
address: int = self.addresses.GameStates[Game.morph_gauge_recharge.value]
|
||||||
|
|
||||||
|
# Ranges from 0 to 100 for every Morph Stock, with a maximum of 1100 for all 10 Stocks filled.
|
||||||
|
self.pine.write_float(address, amount)
|
||||||
|
|
||||||
|
def set_morph_gauge_timer(self, amount : float = 0.0):
|
||||||
|
address = self.follow_pointer_chain(self.addresses.GameStates[Game.morph_gauge_active.value],
|
||||||
|
Game.morph_gauge_active.value)
|
||||||
|
|
||||||
|
if address == 0x0:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.pine.write_float(address, amount)
|
||||||
|
|
||||||
|
def mark_location(self, name : str):
|
||||||
|
if name not in self.addresses.Locations: return
|
||||||
|
|
||||||
|
address : int = self.addresses.Locations[name]
|
||||||
|
self.pine.write_int8(address, 0x01)
|
||||||
|
|
||||||
|
def unmark_location(self, name : str):
|
||||||
|
address : int = self.addresses.Locations[name]
|
||||||
|
self.pine.write_int8(address, 0x00)
|
||||||
|
|
||||||
|
def send_command(self, command : str):
|
||||||
|
as_bytes : bytes = command.encode() + b'\x00'
|
||||||
|
self.pine.write_bytes(self.addresses.GameStates[Game.command.value], as_bytes)
|
||||||
|
|
||||||
|
def kill_player(self, cookies_lost : float = 0.0):
|
||||||
|
if cookies_lost != 0.0:
|
||||||
|
cookies : float = self.get_cookies()
|
||||||
|
self.set_cookies(max(0.0, cookies - cookies_lost))
|
||||||
|
|
||||||
|
## self.send_command(Game.kill_player.value) has a transition delay that takes too long
|
||||||
|
## changeArea is more instantaneous, but introduces a buggy respawn when all cookies are depleted
|
||||||
|
self.change_area(self.get_stage())
|
||||||
|
|
||||||
|
def enter_norma(self, destination : str):
|
||||||
|
self.set_enter_norma_destination(destination)
|
||||||
|
self.send_command(Game.enter_norma.value)
|
||||||
|
|
||||||
|
def change_area(self, destination : str):
|
||||||
|
self.set_change_area_destination(destination)
|
||||||
|
self.send_command(Game.change_area.value)
|
||||||
|
|
||||||
|
def set_last_item_index(self, value : int):
|
||||||
|
self.pine.write_int32(self.addresses.GameStates[Game.last_item_index.value], value)
|
||||||
|
|
||||||
|
def set_persistent_cookie_value(self, value : int):
|
||||||
|
self.pine.write_int8(self.addresses.GameStates[Game.last_cookies.value], value)
|
||||||
|
|
||||||
|
def set_persistent_morph_energy_value(self, value : int):
|
||||||
|
self.pine.write_int8(self.addresses.GameStates[Game.last_morph_energy.value], value)
|
||||||
|
|
||||||
|
def set_persistent_morph_stock_value(self, value : int):
|
||||||
|
self.pine.write_int8(self.addresses.GameStates[Game.last_morph_stock.value], value)
|
||||||
|
|
||||||
|
def set_shop_morph_stock_checked(self, value : int):
|
||||||
|
self.pine.write_int8(self.addresses.GameStates[Game.shop_morph_stock.value], value)
|
||||||
|
|
||||||
|
def save_state(self, slot : int):
|
||||||
|
self.pine.save_state(slot)
|
||||||
|
|
||||||
|
def load_state(self, slot : int):
|
||||||
|
self.pine.load_state(slot)
|
||||||
1006
worlds/apeescape3/AE3_Options.py
Normal file
1006
worlds/apeescape3/AE3_Options.py
Normal file
File diff suppressed because it is too large
Load Diff
1047
worlds/apeescape3/Checker.py
Normal file
1047
worlds/apeescape3/Checker.py
Normal file
File diff suppressed because it is too large
Load Diff
21
worlds/apeescape3/LICENSE.md
Normal file
21
worlds/apeescape3/LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 aidanii
|
||||||
|
|
||||||
|
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.
|
||||||
41
worlds/apeescape3/README.md
Normal file
41
worlds/apeescape3/README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
Ape Escape 3 Archipelago
|
||||||
|
========================
|
||||||
|
#### An Archipelago Implementation for Ape Escape 3
|
||||||
|
|
||||||
|
Play through Ape Escape 3 randomized through the Archipelago Randomization framework.
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
- Gadgets, Morphs and Morph Stocks are randomized, hidden through the game
|
||||||
|
- Other Items such as Coins, Energy, Cookies and Jackets are also randomized.
|
||||||
|
- Randomize and Upgrade the Morph Duration
|
||||||
|
- Find your Items when Capturing Monkeys, using Pipo Cameras, Activating Cellphones or Buying Shop Items
|
||||||
|
- Randomize the Channels and choose how they should be unlocked
|
||||||
|
- Choose how you want to goal
|
||||||
|
- Use Free Play even if the Channel is not cleared
|
||||||
|
- DeathLink Support
|
||||||
|
|
||||||
|
Setup
|
||||||
|
-----
|
||||||
|
Please see our [setup guide](./docs/setup.md) for a full guide on getting started with Ape Escape 3 Archipelago.
|
||||||
|
|
||||||
|
### Prerequisites:
|
||||||
|
- Archipelago (0.6.1 or higher) [[Install](https://github.com/ArchipelagoMW/Archipelago)] [[Guide](https://archipelago.gg/tutorial/Archipelago/setup/en)]
|
||||||
|
- PCSX2 Emulator (1.7 or higher) [[Install](https://pcsx2.net/downloads)] [[Guide](https://pcsx2.net/docs/category/setup)]
|
||||||
|
- Ape Escape 3 [Please acquire and dump your own copy]
|
||||||
|
- NTSC-U `SCUS-97501`
|
||||||
|
- Ape Escape 3 APWorld [[Releases](https://github.com/aidanii24/ae3-archipelago/releases)]
|
||||||
|
|
||||||
|
Acknowledgements
|
||||||
|
---
|
||||||
|
Thank you to these projects and individuals for helping us make this Archipelago Implementation possible:
|
||||||
|
- The Archipelago Core Team and Contributors
|
||||||
|
- evilwb's PCSX2 Interface
|
||||||
|
- coltonious for helping with the first initiative of the Implementation
|
||||||
|
- ButterKnife and Moaks for their technical knowledge with the game
|
||||||
|
- Ape Escape Speedrunning Discord Server and general Ape Escape 3 Community for consultation with glitches
|
||||||
|
- All of our players for playing and enjoying the implementation, as well as giving feedback and suggestions!
|
||||||
|
|
||||||
|
Support
|
||||||
|
-------
|
||||||
|
If you have come across any issues using Ape Escape 3 Archipelago, please get in touch in the Ape Escape 3 thread of the future-games-design forum in the official [Archipelago Discord Server](https://discord.com/channels/731205301247803413/1336332485788831825).
|
||||||
277
worlds/apeescape3/Regions.py
Normal file
277
worlds/apeescape3/Regions.py
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from BaseClasses import Entrance, Location, Region
|
||||||
|
|
||||||
|
from .data.Stages import STAGES_BREAK_ROOMS, STAGES_DIRECTORY, STAGES_MASTER, ENTRANCES_MASTER, STAGES_DIRECTORY_LABEL, \
|
||||||
|
STAGES_SHOP_PROGRESSION, STAGES_FARMABLE, AE3EntranceMeta, STAGES_FARMABLE_SNEAKY_BORG
|
||||||
|
from .data.Locations import CAMERAS_INDEX, CELLPHONES_INDEX, MONKEYS_PASSWORDS, MONKEYS_INDEX, EVENTS_INDEX, \
|
||||||
|
SHOP_PROGRESSION_MASTER, SHOP_PROGRESSION_MORPH, SHOP_COLLECTION_INDEX, CameraLocation, CellphoneLocation, \
|
||||||
|
EventMeta, MonkeyLocation, ShopItemLocation, SHOP_PROGRESSION_DIRECTORY, SHOP_EVENT_ACCESS_DIRECTORY, \
|
||||||
|
SHOP_PROGRESSION_75COMPLETION
|
||||||
|
from .data.Logic import Rulesets
|
||||||
|
from .data.Strings import Stage
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import AE3World
|
||||||
|
|
||||||
|
|
||||||
|
### [< --- HELPERS --- >]
|
||||||
|
def establish_entrance(player : int, name : str, parent_region : Region, destination : Region,
|
||||||
|
ruleset : Rulesets = None):
|
||||||
|
"""Connects the parent region to its destinations and assigns access rules where present."""
|
||||||
|
entrance : Entrance = Entrance(player, name, parent_region)
|
||||||
|
|
||||||
|
if ruleset is not None and ruleset:
|
||||||
|
entrance.access_rule = ruleset.condense(player)
|
||||||
|
|
||||||
|
parent_region.exits.append(entrance)
|
||||||
|
entrance.connect(destination)
|
||||||
|
|
||||||
|
def create_regions(world : "AE3World"):
|
||||||
|
entrance_rules : dict[str, Rulesets] = {**world.logic_preference.entrance_rules,
|
||||||
|
**world.shop_rules.entrance_rules,
|
||||||
|
**world.progression.generate_rules(world),}
|
||||||
|
# world.logic_preference.entrance_rules.update(world.progression.generate_rules(world))
|
||||||
|
|
||||||
|
add_cameras : bool = bool(world.options.camerasanity)
|
||||||
|
add_cellphones : bool = bool(world.options.cellphonesanity.value)
|
||||||
|
add_break_rooms : bool = bool(world.options.monkeysanity_break_rooms.value)
|
||||||
|
|
||||||
|
# Initialize Regions
|
||||||
|
stages : dict[str, Region] = {name : Region(name, world.player, world.multiworld) for name in STAGES_MASTER
|
||||||
|
if name not in world.shop_rules.blacklisted_stages}
|
||||||
|
entrances : list[AE3EntranceMeta] = [*ENTRANCES_MASTER,
|
||||||
|
*world.shop_rules.entrances,
|
||||||
|
*world.progression.level_select_entrances]
|
||||||
|
blacklisted_entrances : list[Entrance] = [*world.logic_preference.blacklisted_entrances,
|
||||||
|
*world.shop_rules.blacklisted_entrances]
|
||||||
|
|
||||||
|
# Connect Regions, building a name->Entrance lookup for indirect condition registration
|
||||||
|
entrance_objects : dict[str, Entrance] = {}
|
||||||
|
for entrance in entrances:
|
||||||
|
if entrance.name in blacklisted_entrances:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ruleset : Rulesets = Rulesets()
|
||||||
|
|
||||||
|
if entrance.parent in stages:
|
||||||
|
parent = stages[entrance.parent]
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if entrance.destination in stages:
|
||||||
|
destination = stages[entrance.destination]
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if entrance.name in entrance_rules:
|
||||||
|
ruleset = entrance_rules[entrance.name]
|
||||||
|
|
||||||
|
establish_entrance(world.player, entrance.name, parent, destination, ruleset)
|
||||||
|
entrance_objects[entrance.name] = destination.entrances[-1]
|
||||||
|
|
||||||
|
# Register Indirect Connections
|
||||||
|
farm_entrances : set[str] = set()
|
||||||
|
pgc_entrances : set[str] = set()
|
||||||
|
|
||||||
|
if world.options.shoppingsanity.value >= 1:
|
||||||
|
farm_entrances.add(Stage.entrance_shop_expensive.value)
|
||||||
|
if world.options.cheap_items_minimum_requirement:
|
||||||
|
farm_entrances.add(Stage.entrance_travel_ab.value)
|
||||||
|
|
||||||
|
if world.options.post_game_condition_cameras:
|
||||||
|
pgc_entrances.update(entrance_rules.keys() & world.progression.pgc_entrance_names)
|
||||||
|
|
||||||
|
if world.options.shoppingsanity.value >= 1:
|
||||||
|
pgc_entrances.update(world.shop_rules.post_game_entrances)
|
||||||
|
if world.options.cheap_items_minimum_requirement.value >= 100:
|
||||||
|
pgc_entrances.add(Stage.entrance_travel_ab.value)
|
||||||
|
|
||||||
|
if farm_entrances:
|
||||||
|
farmable_stages : list[str] = [*STAGES_FARMABLE]
|
||||||
|
if world.options.farm_logic_sneaky_borgs.value:
|
||||||
|
farmable_stages.extend(STAGES_FARMABLE_SNEAKY_BORG)
|
||||||
|
farmable_regions = [region for name, region in stages.items() if name in farmable_stages]
|
||||||
|
for ent_name in farm_entrances:
|
||||||
|
if ent_name in entrance_objects:
|
||||||
|
for region in farmable_regions:
|
||||||
|
world.multiworld.register_indirect_condition(region, entrance_objects[ent_name])
|
||||||
|
|
||||||
|
if pgc_entrances:
|
||||||
|
camera_regions = [region for name, region in stages.items() if name in CAMERAS_INDEX]
|
||||||
|
for ent_name in pgc_entrances:
|
||||||
|
if ent_name in entrance_objects:
|
||||||
|
for region in camera_regions:
|
||||||
|
world.multiworld.register_indirect_condition(region, entrance_objects[ent_name])
|
||||||
|
|
||||||
|
# Define Regions
|
||||||
|
blacklist : list[str] = [stage for channel in world.options.blacklist_channel.value
|
||||||
|
for stage in STAGES_DIRECTORY_LABEL[channel]
|
||||||
|
if channel in STAGES_DIRECTORY_LABEL]
|
||||||
|
|
||||||
|
for stage in stages.values():
|
||||||
|
# Skip Blacklisted Stages
|
||||||
|
if stage.name in blacklist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip stage if Monkeysanity Break Rooms is enabled and the stage is a break room.
|
||||||
|
# It should be safe to skip outright since there are no break rooms with Cameras or Cellphones in them.
|
||||||
|
if not add_break_rooms and stage.name in STAGES_BREAK_ROOMS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Define Locations
|
||||||
|
## Monkeys
|
||||||
|
if stage.name in MONKEYS_INDEX:
|
||||||
|
for monkeys in MONKEYS_INDEX[stage.name]:
|
||||||
|
if not world.options.monkeysanity_passwords and monkeys in MONKEYS_PASSWORDS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
meta : MonkeyLocation = MonkeyLocation(monkeys)
|
||||||
|
loc : Location = meta.to_location(world.player, stage)
|
||||||
|
|
||||||
|
# Initialize Ruleset for Location
|
||||||
|
ruleset : Rulesets = Rulesets()
|
||||||
|
if monkeys in world.logic_preference.monkey_rules.keys():
|
||||||
|
ruleset = world.logic_preference.monkey_rules[monkeys]
|
||||||
|
|
||||||
|
ruleset.critical.update(world.logic_preference.default_critical_rule)
|
||||||
|
|
||||||
|
loc.access_rule = ruleset.condense(world.player)
|
||||||
|
|
||||||
|
stage.locations.append(loc)
|
||||||
|
|
||||||
|
## Cameras
|
||||||
|
if add_cameras and stage.name in CAMERAS_INDEX:
|
||||||
|
camera : str = CAMERAS_INDEX[stage.name]
|
||||||
|
meta : CameraLocation = CameraLocation(camera)
|
||||||
|
loc : Location = meta.to_location(world.player, stage)
|
||||||
|
|
||||||
|
# Add Access Rule for completing the stage to ensure maximum accessibility,
|
||||||
|
# if the player chooses to require the monkey actors for the Cameras,
|
||||||
|
# and they did not choose to have early Freeplay
|
||||||
|
if world.options.camerasanity == 1 and not world.options.early_free_play:
|
||||||
|
ruleset : Rulesets = Rulesets()
|
||||||
|
parent_channel : str = ""
|
||||||
|
for channel, regions in STAGES_DIRECTORY.items():
|
||||||
|
if not stage.name in regions:
|
||||||
|
continue
|
||||||
|
|
||||||
|
parent_channel = channel
|
||||||
|
break
|
||||||
|
|
||||||
|
if parent_channel:
|
||||||
|
ruleset = world.logic_preference.get_channel_clear_rules(parent_channel)
|
||||||
|
|
||||||
|
loc.access_rule = ruleset.condense(world.player)
|
||||||
|
|
||||||
|
stage.locations.append(loc)
|
||||||
|
|
||||||
|
## Cellphones
|
||||||
|
if add_cellphones:
|
||||||
|
if stage.name in CELLPHONES_INDEX:
|
||||||
|
for cellphone in CELLPHONES_INDEX[stage.name]:
|
||||||
|
meta : CellphoneLocation = CellphoneLocation(cellphone)
|
||||||
|
loc : Location = meta.to_location(world.player, stage)
|
||||||
|
|
||||||
|
stage.locations.append(loc)
|
||||||
|
|
||||||
|
## Events
|
||||||
|
if stage.name in EVENTS_INDEX:
|
||||||
|
for event in EVENTS_INDEX[stage.name]:
|
||||||
|
meta : EventMeta = EventMeta(event)
|
||||||
|
loc : Location = meta.to_event_location(world.player, stage)
|
||||||
|
|
||||||
|
if event in world.logic_preference.event_rules:
|
||||||
|
loc.access_rule = world.logic_preference.event_rules[event].condense(world.player)
|
||||||
|
|
||||||
|
stage.locations.append(loc)
|
||||||
|
|
||||||
|
# Handle Shop Regions
|
||||||
|
shopping_area: Region = stages[Stage.travel_station_b.value]
|
||||||
|
expensive_area : Region = stages[Stage.region_shop_expensive.value]
|
||||||
|
|
||||||
|
if world.options.shoppingsanity != 0:
|
||||||
|
## Handle Shop Items that require specific events
|
||||||
|
for region, items in SHOP_EVENT_ACCESS_DIRECTORY.items():
|
||||||
|
if region in blacklist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
meta: ShopItemLocation = ShopItemLocation(item)
|
||||||
|
loc: Location = meta.to_location(world.player, shopping_area)
|
||||||
|
|
||||||
|
if item in world.shop_rules.item_rules:
|
||||||
|
loc.access_rule = world.shop_rules.item_rules[item].condense(world.player)
|
||||||
|
|
||||||
|
shopping_area.locations.append(loc)
|
||||||
|
|
||||||
|
if not world.options.blacklist_channel.value:
|
||||||
|
meta: ShopItemLocation = ShopItemLocation(SHOP_PROGRESSION_75COMPLETION[0])
|
||||||
|
loc: Location = meta.to_location(world.player, shopping_area)
|
||||||
|
|
||||||
|
if loc.name in world.shop_rules.item_rules:
|
||||||
|
loc.access_rule = world.shop_rules.item_rules[loc.name].condense(world.player)
|
||||||
|
|
||||||
|
shopping_area.locations.append(loc)
|
||||||
|
|
||||||
|
## Handle Morph Stocks
|
||||||
|
if world.options.shoppingsanity != 2:
|
||||||
|
stocks_region : Region = stages[Stage.region_shop_morph.value]
|
||||||
|
|
||||||
|
for i, item in enumerate(SHOP_PROGRESSION_MORPH):
|
||||||
|
meta : ShopItemLocation = ShopItemLocation(item, 1, i)
|
||||||
|
loc : Location = meta.to_location(world.player, stocks_region)
|
||||||
|
|
||||||
|
if item in world.shop_rules.item_rules:
|
||||||
|
loc.access_rule = world.shop_rules.item_rules[item].condense(world.player)
|
||||||
|
|
||||||
|
stocks_region.locations.append(loc)
|
||||||
|
|
||||||
|
## Handle Shoppingsanity options Enabled/Collection
|
||||||
|
if 0 < world.options.shoppingsanity.value < 3:
|
||||||
|
shop_locations_meta : list[ShopItemLocation] = []
|
||||||
|
|
||||||
|
if world.options.shoppingsanity.value == 1:
|
||||||
|
for item in [*SHOP_PROGRESSION_MASTER]:
|
||||||
|
|
||||||
|
meta : ShopItemLocation = ShopItemLocation(item)
|
||||||
|
shop_locations_meta.append(meta)
|
||||||
|
else:
|
||||||
|
for category_index, category in enumerate(SHOP_COLLECTION_INDEX):
|
||||||
|
for offset, item in enumerate(category):
|
||||||
|
meta : ShopItemLocation = ShopItemLocation(item, category_index, offset)
|
||||||
|
shop_locations_meta.append(meta)
|
||||||
|
|
||||||
|
if shop_locations_meta:
|
||||||
|
for item in shop_locations_meta:
|
||||||
|
parent = shopping_area if item.name in world.shop_rules.cheap_early_items else expensive_area
|
||||||
|
|
||||||
|
loc : Location = item.to_location(world.player, parent)
|
||||||
|
|
||||||
|
if item.name in world.shop_rules.item_rules:
|
||||||
|
loc.access_rule = world.shop_rules.item_rules[item.name].condense(world.player)
|
||||||
|
|
||||||
|
parent.locations.append(loc)
|
||||||
|
|
||||||
|
## Handle Shoppingsanity Options Progressive/Restock
|
||||||
|
elif 2 < world.options.shoppingsanity.value < 5:
|
||||||
|
shop_progression_regions : list[Region] = [region for name, region in stages.items()
|
||||||
|
if name in STAGES_SHOP_PROGRESSION]
|
||||||
|
|
||||||
|
for region in shop_progression_regions:
|
||||||
|
for location in SHOP_PROGRESSION_DIRECTORY[region.name]:
|
||||||
|
meta : ShopItemLocation = ShopItemLocation(location)
|
||||||
|
loc : Location = meta.to_location(world.player, region)
|
||||||
|
|
||||||
|
if location in world.shop_rules.item_rules:
|
||||||
|
loc.access_rule = world.shop_rules.item_rules[location].condense(world.player)
|
||||||
|
|
||||||
|
region.locations.append(loc)
|
||||||
|
|
||||||
|
# Send Regions to Archipelago
|
||||||
|
world.multiworld.regions.extend(list(stages.values()))
|
||||||
|
|
||||||
|
# # <!> DEBUG
|
||||||
|
# # Connection Diagrams
|
||||||
|
# from Utils import visualize_regions
|
||||||
|
# visualize_regions(world.multiworld.get_region("Menu", world.player), "_region_diagram.puml")
|
||||||
848
worlds/apeescape3/__init__.py
Normal file
848
worlds/apeescape3/__init__.py
Normal file
@@ -0,0 +1,848 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
from typing import ClassVar, List, TextIO
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from worlds.AutoWorld import World, WebWorld
|
||||||
|
from BaseClasses import MultiWorld, Tutorial, Location
|
||||||
|
from Options import OptionError
|
||||||
|
import settings
|
||||||
|
|
||||||
|
from .data.Items import AE3Item, AE3ItemMeta, ITEMS_MASTER, Nothing, generate_collectables
|
||||||
|
from .data.Locations import Cellphone_Name_to_ID, MONKEYS_BOSSES, MONKEYS_MASTER_ORDERED, CAMERAS_MASTER_ORDERED, \
|
||||||
|
CELLPHONES_MASTER_ORDERED, MONKEYS_PASSWORDS, MONKEYS_BREAK_ROOMS, SHOP_PROGRESSION_75COMPLETION, \
|
||||||
|
SHOP_EVENT_ACCESS_DIRECTORY, SHOP_HINT_BOOK, SHOP_COLLECTION_HINT_BOOK, SHOP_PERSISTENT_HINT_BOOK
|
||||||
|
from .data.Stages import LEVELS_BY_ORDER, STAGES_BOSSES, STAGES_BREAK_ROOMS, STAGES_DIRECTORY_LABEL
|
||||||
|
from .data.Rules import GoalTarget, GoalTargetOptions, LogicPreference, LogicPreferenceOptions, PostGameCondition, \
|
||||||
|
ShopItemRules
|
||||||
|
from .data.Strings import Loc, Meta, APHelper, APConsole, Itm
|
||||||
|
from .data.Logic import is_goal_achieved, are_goals_achieved, Rulesets, ProgressionMode, ProgressionModeOptions
|
||||||
|
from .AE3_Options import AE3Options, create_option_groups, slot_data_options
|
||||||
|
from .Regions import create_regions
|
||||||
|
from .data import Items, Locations
|
||||||
|
|
||||||
|
|
||||||
|
# Load Client component for Archipelago to recognize the Client
|
||||||
|
from . import components as components
|
||||||
|
|
||||||
|
class AE3Settings(settings.Group):
|
||||||
|
class SessionPreferences(settings.Bool):
|
||||||
|
"""
|
||||||
|
Preferences for game session management.
|
||||||
|
|
||||||
|
> save_state_on_room_transition: Automatically create a save state when transitioning between rooms.
|
||||||
|
> save_state_on_item_received: Automatically create a save state when receiving a new progressive item.
|
||||||
|
> save_state_on_location_check: Automatically create a save state when checking a new location.
|
||||||
|
> load_state_on_connect: Load a state automatically after connecting to the multiworld if the client
|
||||||
|
is already connected to the game and that the last save is from a save state and not a normal game save.
|
||||||
|
> pine_connect_offline: Make attempts to connect to PCSX2 even if the client has not yet connected to a room.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class SessionsPreferences(settings.Bool):
|
||||||
|
""""""
|
||||||
|
|
||||||
|
class GamePreferences(settings.Bool):
|
||||||
|
"""
|
||||||
|
Preferences for game/client-enforcement behavior
|
||||||
|
|
||||||
|
> auto-equip : Automatically assign received gadgets to a face button
|
||||||
|
"""
|
||||||
|
|
||||||
|
class GenerationPreferences(settings.Bool):
|
||||||
|
"""
|
||||||
|
Preferences for game generation. Only relevant for world generation and not the setup of or during play.
|
||||||
|
|
||||||
|
> whitelist_pgc_bypass: Allow Ape Escape 3 players to enable "PGC Bypass" as a possible outcome for
|
||||||
|
Lucky Ticket Consolation Prize.
|
||||||
|
> whitelist_instant_goal: Allow Ape Escape 3 players to enable "Instant Goal" as a possible outcome for
|
||||||
|
Lucky Ticket Consolation Prize.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self[index]
|
||||||
|
|
||||||
|
class GenerationPreference(settings.Bool):
|
||||||
|
""""""
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
return self[index]
|
||||||
|
|
||||||
|
|
||||||
|
save_state_on_room_transition : SessionPreferences | bool = False
|
||||||
|
save_state_on_item_received : SessionsPreferences | bool = True
|
||||||
|
save_state_on_location_check : SessionsPreferences | bool = False
|
||||||
|
load_state_on_connect : SessionsPreferences | bool = False
|
||||||
|
pine_connect_offline : SessionsPreferences | bool = True
|
||||||
|
|
||||||
|
auto_equip : GamePreferences | bool = True
|
||||||
|
|
||||||
|
whitelist_pgc_bypass: GenerationPreferences | bool = False
|
||||||
|
whitelist_instant_goal: GenerationPreference | bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class AE3Web(WebWorld):
|
||||||
|
theme = "ocean"
|
||||||
|
option_groups = create_option_groups()
|
||||||
|
|
||||||
|
tutorials = [Tutorial(
|
||||||
|
"Multiworld Guide Setup",
|
||||||
|
" - A guide to setting up Ape Escape 3 for Archipelago",
|
||||||
|
"English",
|
||||||
|
"setup.md",
|
||||||
|
"setup/en",
|
||||||
|
["aidanii"]
|
||||||
|
)]
|
||||||
|
|
||||||
|
|
||||||
|
class AE3World(World):
|
||||||
|
"""
|
||||||
|
Ape Escape 3 is a 3D platformer published and developed by Sony Computer Entertainment, released
|
||||||
|
in 2005 for the Sony Playstation 2. Specter for the third time has escaped again, and this time,
|
||||||
|
he and his Pipo Monkey army has taken over Television and programs anyone who watches into a
|
||||||
|
mindless couch potato. Even our previous heroes have fallen for the trap, and now its up to Kei
|
||||||
|
and Yumi to save the world from the control of Specter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define Basic Game Parameters
|
||||||
|
game = Meta.game
|
||||||
|
settings : AE3Settings
|
||||||
|
web : ClassVar[WebWorld] = AE3Web()
|
||||||
|
topology_present = True
|
||||||
|
|
||||||
|
# Initialize Randomizer Options
|
||||||
|
options_dataclass = AE3Options
|
||||||
|
options : AE3Options
|
||||||
|
|
||||||
|
# Define the Items and Locations to/for Archipelago
|
||||||
|
item_name_to_id = Items.generate_name_to_id()
|
||||||
|
location_name_to_id = Locations.generate_name_to_id()
|
||||||
|
|
||||||
|
item_name_groups = Items.generate_item_groups()
|
||||||
|
location_name_groups = Locations.generate_location_groups()
|
||||||
|
|
||||||
|
logic_preference : LogicPreference
|
||||||
|
goal_target : GoalTarget = GoalTarget
|
||||||
|
progression : ProgressionMode
|
||||||
|
post_game_condition : PostGameCondition
|
||||||
|
shop_rules : ShopItemRules
|
||||||
|
|
||||||
|
exclude_locations: list
|
||||||
|
|
||||||
|
logger: logging.Logger = logging.getLogger()
|
||||||
|
|
||||||
|
def __init__(self, multiworld : MultiWorld, player: int):
|
||||||
|
self.item_pool : List[AE3Item] = []
|
||||||
|
|
||||||
|
super(AE3World, self).__init__(multiworld, player)
|
||||||
|
|
||||||
|
def generate_early(self):
|
||||||
|
ut_initialized: bool = self.prepare_ut()
|
||||||
|
if ut_initialized: return
|
||||||
|
|
||||||
|
# Limit Post/Blacklist Channels to 8 items
|
||||||
|
if len(self.options.post_channel.value) > 8:
|
||||||
|
additive: bool = APHelper.additive.value in self.options.post_channel
|
||||||
|
new_post: list[str] = sorted(self.options.post_channel.value)[:8]
|
||||||
|
if additive:
|
||||||
|
new_post.append(APHelper.additive.value)
|
||||||
|
|
||||||
|
self.options.post_channel.value = {*deepcopy(new_post)}
|
||||||
|
|
||||||
|
if len(self.options.blacklist_channel.value) > 8:
|
||||||
|
self.options.blacklist_channel.value = {*sorted(self.options.blacklist_channel.value)[:8]}
|
||||||
|
|
||||||
|
# Handle duplicate entries between Channel Options
|
||||||
|
## Remove Preserve Channels that exists in Push, Post and Blacklist Channel Options
|
||||||
|
if self.options.preserve_channel:
|
||||||
|
self.options.preserve_channel.value.difference_update(self.options.blacklist_channel)
|
||||||
|
self.options.preserve_channel.value.difference_update(self.options.post_channel)
|
||||||
|
self.options.preserve_channel.value.difference_update(self.options.push_channel)
|
||||||
|
|
||||||
|
## Remove Push Channels that exists in Post and Blacklist Channel Options
|
||||||
|
if self.options.push_channel:
|
||||||
|
additive : bool = APHelper.additive.value in self.options.push_channel.value
|
||||||
|
self.options.push_channel.value.difference_update(self.options.blacklist_channel)
|
||||||
|
self.options.push_channel.value.difference_update(self.options.post_channel)
|
||||||
|
if additive and APHelper.additive.value not in self.options.push_channel.value:
|
||||||
|
self.options.push_channel.value.add(APHelper.additive.value)
|
||||||
|
|
||||||
|
## Remove Post Channels that exists in Blacklist Channel Option
|
||||||
|
if self.options.post_channel:
|
||||||
|
self.options.post_channel.value.difference_update(self.options.blacklist_channel)
|
||||||
|
|
||||||
|
## If Goal Target is either of Specter bosses and Specters Goal Target As Post is enabled,
|
||||||
|
## Add the corresponding Specter Boss to Post Channel
|
||||||
|
if self.options.goal_target.value < 2 and self.options.specters_goal_target_as_post.value:
|
||||||
|
if self.options.blacklist_bosses == 1:
|
||||||
|
raise OptionError("Specters Goal Target As Post is enabled and should take effect "
|
||||||
|
"but Blacklist Bosses has also been set to Blacklist Specters."
|
||||||
|
"Either change the Blacklist Bosses option, "
|
||||||
|
"disable the Specters Goal Target As Post option, "
|
||||||
|
"or change the Goal Target Option.")
|
||||||
|
|
||||||
|
if len(self.options.post_channel.value) > 8:
|
||||||
|
raise OptionError("Specters Goal Target As Post is enabled, "
|
||||||
|
"but the Post Channel Option has already reached "
|
||||||
|
"the maximum amount of Channels it can use. "
|
||||||
|
"Either reduce the Channels in the Post Channel option, "
|
||||||
|
"or disable Specters Goal Target As Post.")
|
||||||
|
|
||||||
|
specter_channel = LEVELS_BY_ORDER[26 + self.options.goal_target.value]
|
||||||
|
self.options.push_channel.value.discard(specter_channel)
|
||||||
|
self.options.blacklist_channel.value.discard(specter_channel)
|
||||||
|
|
||||||
|
self.options.post_channel.value.add(specter_channel)
|
||||||
|
|
||||||
|
## If Exclude Bosses is enabled, add the bosses to Blacklist Channel
|
||||||
|
if self.options.blacklist_bosses.value > 0:
|
||||||
|
if self.options.blacklist_bosses.value == 1:
|
||||||
|
bosses = {*STAGES_BOSSES}
|
||||||
|
else:
|
||||||
|
bosses = {*[*STAGES_BOSSES][:-2]}
|
||||||
|
|
||||||
|
if len(self.options.blacklist_channel.value) + len(bosses) > 8:
|
||||||
|
raise OptionError("Exclude Bosses is enabled, but the Blacklist Channel Option has already reached "
|
||||||
|
"the maximum amount of Channels it can use. "
|
||||||
|
"Either reduce the Channels in the Blacklist Channel option, "
|
||||||
|
"or change the option for Exclude Bosses.")
|
||||||
|
|
||||||
|
self.options.push_channel.value.difference_update(bosses)
|
||||||
|
self.options.post_channel.value.difference_update(bosses)
|
||||||
|
|
||||||
|
self.options.blacklist_channel.value.update(bosses)
|
||||||
|
|
||||||
|
total_moved_channels: set = {*self.options.push_channel.value,
|
||||||
|
*self.options.post_channel.value,
|
||||||
|
*self.options.blacklist_channel.value}
|
||||||
|
|
||||||
|
if self.options.progression_mode.value == 4 and len(total_moved_channels) > 15:
|
||||||
|
raise OptionError("Too many channels have been assigned to Post/Push/Blacklist options for Open Progression to generate properly. "
|
||||||
|
"Please reduce the amount of channels from these options!")
|
||||||
|
|
||||||
|
## If Progression Mode is set to Group or World, but there are too many bosses pushed, posted
|
||||||
|
## and or blacklisted, abort the generation
|
||||||
|
if 0 < self.options.progression_mode.value < 3:
|
||||||
|
moved_bosses: set = total_moved_channels.intersection(STAGES_BOSSES)
|
||||||
|
|
||||||
|
print(len(moved_bosses), moved_bosses)
|
||||||
|
|
||||||
|
if len(moved_bosses) > 6:
|
||||||
|
raise OptionError("Group/World Progression Mode relies on bosses for Channel Set splitting, "
|
||||||
|
"but too many bosses have been moved using the Push/Post/Blacklist Channel options. "
|
||||||
|
"Either remove more bosses from these options, "
|
||||||
|
"or choose a different progression mode.")
|
||||||
|
|
||||||
|
|
||||||
|
# Get Logic Preference
|
||||||
|
self.logic_preference = LogicPreferenceOptions[self.options.logic_preference]()
|
||||||
|
self.logic_preference.apply_unlimited_gadget_float_rules(
|
||||||
|
bool(self.options.hip_drop_storage_logic.value),
|
||||||
|
bool(self.options.prolonged_quad_jump_logic.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.options.base_morph_duration.value >= 30 or self.options.add_morph_extensions.value:
|
||||||
|
self.logic_preference.apply_timed_kung_fu_rule(
|
||||||
|
self.options.base_morph_duration.value,
|
||||||
|
bool(self.options.add_morph_extensions.value)
|
||||||
|
)
|
||||||
|
self.logic_preference.apply_timed_morph_float(
|
||||||
|
self.options.base_morph_duration.value,
|
||||||
|
bool(self.options.add_morph_extensions.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get ProgressionMode
|
||||||
|
self.progression = ProgressionModeOptions[self.options.progression_mode.value](self)
|
||||||
|
|
||||||
|
# Shuffle Channel if desired
|
||||||
|
if self.options.shuffle_channel:
|
||||||
|
self.progression.shuffle(self)
|
||||||
|
# Directly Apply Channel Rules otherwise
|
||||||
|
else:
|
||||||
|
self.progression.reorder(-1, sorted(self.options.blacklist_channel.value))
|
||||||
|
self.progression.reorder(-2, sorted(self.options.post_channel.value))
|
||||||
|
self.progression.reorder(-3, sorted(self.options.push_channel.value))
|
||||||
|
self.progression.regenerate_level_select_entrances()
|
||||||
|
|
||||||
|
# Get Post Game Access Rule and exclude locations as necessary
|
||||||
|
exclude_regions: list[str] = []
|
||||||
|
exclude_locations: list[str] = []
|
||||||
|
|
||||||
|
exclude_locations.extend(MONKEYS_PASSWORDS)
|
||||||
|
|
||||||
|
# Exclude Blacklisted Channels
|
||||||
|
# Exclude Channels in Post Game from being required for Post Game to be unlocked
|
||||||
|
if self.progression.progression[-1]:
|
||||||
|
for channel in self.progression.order[-self.progression.progression[-1]:]:
|
||||||
|
exclude_locations.extend(MONKEYS_MASTER_ORDERED[channel])
|
||||||
|
exclude_locations.append(CAMERAS_MASTER_ORDERED[channel])
|
||||||
|
|
||||||
|
excluded_phones_id: list[str] = CELLPHONES_MASTER_ORDERED[channel]
|
||||||
|
exclude_locations.extend(Cellphone_Name_to_ID[cell_id] for cell_id in excluded_phones_id)
|
||||||
|
|
||||||
|
# Force-enable shoppingsanity early if required by goal target or post-game condition
|
||||||
|
goal_target_index = self.options.goal_target.value
|
||||||
|
if goal_target_index == 7 or self.options.post_game_condition_shop:
|
||||||
|
if not self.options.shoppingsanity:
|
||||||
|
self.options.shoppingsanity.value = 1
|
||||||
|
|
||||||
|
# Exclude Shop Items based on Shoppingsanity Type and Blacklisted Channels
|
||||||
|
if self.options.blacklist_channel.value and self.options.shoppingsanity.value > 0:
|
||||||
|
## Always exclude Ultim-ape Fighter Minigame if anything is blacklisted
|
||||||
|
exclude_locations.extend(SHOP_PROGRESSION_75COMPLETION)
|
||||||
|
|
||||||
|
## Exclude Event/Condition-sensitive Items based on excluded levels
|
||||||
|
blacklisted_stages = {stage for channel in self.options.blacklist_channel.value
|
||||||
|
for stage in STAGES_DIRECTORY_LABEL[channel]}
|
||||||
|
for region, item in SHOP_EVENT_ACCESS_DIRECTORY.items():
|
||||||
|
if region in blacklisted_stages:
|
||||||
|
exclude_locations.extend(item)
|
||||||
|
|
||||||
|
# Check for Options that may override Monkeysanity Break Rooms Option
|
||||||
|
# and exclude if not needed
|
||||||
|
if not self.options.monkeysanity_break_rooms:
|
||||||
|
exclude_regions.extend([*STAGES_BREAK_ROOMS])
|
||||||
|
|
||||||
|
post_game_conditions: dict[str, int] = {}
|
||||||
|
if self.options.post_game_condition_monkeys:
|
||||||
|
amount: int = 441 if self.options.post_game_condition_monkeys < 0 \
|
||||||
|
else self.options.post_game_condition_monkeys
|
||||||
|
post_game_conditions[APHelper.monkey.value] = amount
|
||||||
|
|
||||||
|
# Force Break Room Monkeys to be disabled on Vanilla Preset
|
||||||
|
if self.options.post_game_condition_monkeys == -2:
|
||||||
|
if not self.options.monkeysanity_break_rooms:
|
||||||
|
self.options.monkeysanity_break_rooms.value = 1
|
||||||
|
# Respect Monkeysanity BreakRooms option otherwise
|
||||||
|
elif not self.options.monkeysanity_break_rooms:
|
||||||
|
exclude_regions.extend([*STAGES_BREAK_ROOMS])
|
||||||
|
|
||||||
|
# Get Goal Target
|
||||||
|
self.goal_target = GoalTargetOptions[goal_target_index](self.options.goal_target_override,
|
||||||
|
[*exclude_regions], [*exclude_locations],
|
||||||
|
self.options.shoppingsanity.value)
|
||||||
|
|
||||||
|
if goal_target_index == 5 and not self.options.camerasanity:
|
||||||
|
self.options.camerasanity.value = 1
|
||||||
|
elif goal_target_index == 6 and not self.options.cellphonesanity:
|
||||||
|
self.options.cellphonesanity.value = True
|
||||||
|
|
||||||
|
# Exclude Channels in Post Game from being required for Post Game to be unlocked
|
||||||
|
post_game_start_index = sum(self.progression.progression[:-2]) + 1
|
||||||
|
for channel in (self.progression.order[post_game_start_index :
|
||||||
|
post_game_start_index + self.progression.progression[-2]]):
|
||||||
|
exclude_locations.extend(MONKEYS_MASTER_ORDERED[channel])
|
||||||
|
exclude_locations.append(CAMERAS_MASTER_ORDERED[channel])
|
||||||
|
|
||||||
|
excluded_phones_id : list[str] = CELLPHONES_MASTER_ORDERED[channel]
|
||||||
|
exclude_locations.extend(Cellphone_Name_to_ID[cell_id] for cell_id in excluded_phones_id)
|
||||||
|
|
||||||
|
for region, item in SHOP_EVENT_ACCESS_DIRECTORY.items():
|
||||||
|
if region in [*STAGES_DIRECTORY_LABEL.values()][channel]:
|
||||||
|
exclude_locations.extend(item)
|
||||||
|
|
||||||
|
# Exclude Ultim-ape Fighter from being a PGC requirement, as it requires as many monkeys as possible
|
||||||
|
exclude_locations.extend(SHOP_PROGRESSION_75COMPLETION)
|
||||||
|
|
||||||
|
# Record remaining Post Game Condition options
|
||||||
|
if self.options.post_game_condition_bosses:
|
||||||
|
post_game_conditions[APHelper.bosses.value] = self.options.post_game_condition_bosses.value
|
||||||
|
|
||||||
|
if self.options.post_game_condition_cameras:
|
||||||
|
post_game_conditions[APHelper.camera.value] = self.options.post_game_condition_cameras.value
|
||||||
|
|
||||||
|
# Force Camerasanity to enabled if disabled
|
||||||
|
if not self.options.camerasanity.value:
|
||||||
|
self.options.camerasanity.value = 1
|
||||||
|
|
||||||
|
if self.options.post_game_condition_cellphones:
|
||||||
|
post_game_conditions[APHelper.cellphone.value] = self.options.post_game_condition_cellphones.value
|
||||||
|
|
||||||
|
# Force Cellphonesanity if disabled
|
||||||
|
if not self.options.cellphonesanity:
|
||||||
|
self.options.cellphonesanity.value = True
|
||||||
|
|
||||||
|
if self.options.post_game_condition_shop:
|
||||||
|
post_game_conditions[APHelper.shop.value] = self.options.post_game_condition_shop.value
|
||||||
|
|
||||||
|
if self.options.post_game_condition_keys:
|
||||||
|
post_game_conditions[APHelper.keys.value] = self.options.post_game_condition_keys.value
|
||||||
|
|
||||||
|
self.shop_rules: ShopItemRules = ShopItemRules(self)
|
||||||
|
if self.shop_rules.post_game_items:
|
||||||
|
exclude_locations.extend(self.shop_rules.post_game_items)
|
||||||
|
|
||||||
|
self.post_game_condition = PostGameCondition(post_game_conditions, exclude_regions, exclude_locations,
|
||||||
|
self.options.shoppingsanity.value)
|
||||||
|
|
||||||
|
self.shop_rules.set_pgc_rules(self)
|
||||||
|
self.item_pool = []
|
||||||
|
|
||||||
|
if self.options.lucky_ticket_consolation_effects:
|
||||||
|
current: set[str] = set(self.options.consolation_effects_whitelist)
|
||||||
|
exclude: set[str] = set()
|
||||||
|
|
||||||
|
if not self.settings.whitelist_pgc_bypass:
|
||||||
|
exclude.add(APHelper.bypass_pgc.value)
|
||||||
|
|
||||||
|
if not self.settings.whitelist_instant_goal:
|
||||||
|
exclude.add(APHelper.instant_goal.value)
|
||||||
|
|
||||||
|
current.difference_update(exclude)
|
||||||
|
current.add(APHelper.nothing.value)
|
||||||
|
|
||||||
|
self.options.consolation_effects_whitelist.value = sorted(current)
|
||||||
|
|
||||||
|
self.exclude_locations = exclude_locations
|
||||||
|
|
||||||
|
# Once options are resolved, apply logic dependent on options
|
||||||
|
self.logic_preference.apply_option_logic(self.options)
|
||||||
|
|
||||||
|
# self.log_debug()
|
||||||
|
|
||||||
|
def create_regions(self):
|
||||||
|
create_regions(self)
|
||||||
|
|
||||||
|
def create_item(self, item : str) -> AE3Item:
|
||||||
|
for itm in ITEMS_MASTER:
|
||||||
|
if isinstance(itm, AE3ItemMeta):
|
||||||
|
if itm.name == item:
|
||||||
|
return itm.to_item(self.player)
|
||||||
|
|
||||||
|
return Nothing.to_item(self.player)
|
||||||
|
|
||||||
|
def create_items(self):
|
||||||
|
# Define Items
|
||||||
|
stun_club = Items.Gadget_Club.to_item(self.player)
|
||||||
|
monkey_net = Items.Gadget_Net.to_item(self.player)
|
||||||
|
monkey_radar = Items.Gadget_Radar.to_item(self.player)
|
||||||
|
super_hoop = Items.Gadget_Hoop.to_item(self.player)
|
||||||
|
slingback_shooter = Items.Gadget_Sling.to_item(self.player)
|
||||||
|
water_net = Items.Gadget_Swim.to_item(self.player)
|
||||||
|
rc_car = Items.Gadget_RCC.to_item(self.player)
|
||||||
|
sky_flyer = Items.Gadget_Fly.to_item(self.player)
|
||||||
|
|
||||||
|
knight = Items.Morph_Knight.to_item(self.player)
|
||||||
|
cowboy = Items.Morph_Cowboy.to_item(self.player)
|
||||||
|
ninja = Items.Morph_Ninja.to_item(self.player)
|
||||||
|
magician = Items.Morph_Magician.to_item(self.player)
|
||||||
|
kungfu = Items.Morph_Kungfu.to_item(self.player)
|
||||||
|
hero = Items.Morph_Hero.to_item(self.player)
|
||||||
|
monkey = Items.Morph_Monkey.to_item(self.player)
|
||||||
|
|
||||||
|
equipment : List[AE3Item] = [stun_club, monkey_radar, super_hoop, slingback_shooter, water_net, rc_car,
|
||||||
|
sky_flyer]
|
||||||
|
|
||||||
|
# Push Starting Gadget as pre-collected
|
||||||
|
if self.options.starting_gadget > 0:
|
||||||
|
self.multiworld.push_precollected(equipment[self.options.starting_gadget - 1])
|
||||||
|
del equipment[self.options.starting_gadget - 1]
|
||||||
|
|
||||||
|
self.multiworld.push_precollected(monkey_net)
|
||||||
|
|
||||||
|
# Remove any Gadgets specified in Starting Inventory
|
||||||
|
equipment = [ gadget for gadget in equipment if gadget.name not in self.options.start_inventory]
|
||||||
|
|
||||||
|
self.item_pool += [*equipment]
|
||||||
|
|
||||||
|
equipment.clear()
|
||||||
|
equipment = [knight, cowboy, ninja, magician, kungfu, hero, monkey]
|
||||||
|
|
||||||
|
# Push Starting Morph as precollected
|
||||||
|
if self.options.starting_morph > 0:
|
||||||
|
self.multiworld.push_precollected(equipment[self.options.starting_morph - 1])
|
||||||
|
del equipment[self.options.starting_morph - 1]
|
||||||
|
|
||||||
|
# Remove any Morphs specified in Starting Inventory
|
||||||
|
equipment = [ morph for morph in equipment if morph.name not in self.options.start_inventory]
|
||||||
|
|
||||||
|
self.item_pool += [*equipment]
|
||||||
|
|
||||||
|
if self.options.shuffle_chassis:
|
||||||
|
if rc_car in self.item_pool:
|
||||||
|
self.item_pool.remove(rc_car)
|
||||||
|
|
||||||
|
chassis_twin = Items.Chassis_Twin.to_item(self.player)
|
||||||
|
chassis_black = Items.Chassis_Black.to_item(self.player)
|
||||||
|
chassis_pudding = Items.Chassis_Pudding.to_item(self.player)
|
||||||
|
|
||||||
|
self.item_pool += [chassis_twin, chassis_pudding, chassis_black]
|
||||||
|
|
||||||
|
# Add Upgradeables
|
||||||
|
if self.options.shuffle_morph_stocks:
|
||||||
|
self.item_pool += Items.Acc_Morph_Stock.to_items(self.player)
|
||||||
|
|
||||||
|
if self.options.add_morph_extensions:
|
||||||
|
self.item_pool += Items.Acc_Morph_Ext.to_items(self.player)
|
||||||
|
|
||||||
|
# Add Archipelago Items
|
||||||
|
self.item_pool += self.progression.generate_keys(self)
|
||||||
|
|
||||||
|
if self.options.shoppingsanity.value == 4:
|
||||||
|
amount = self.options.restock_progression.value + self.options.extra_shop_stocks.value - 1
|
||||||
|
|
||||||
|
self.item_pool.extend([self.create_item(APHelper.shop_stock.value)
|
||||||
|
for _ in range(amount)])
|
||||||
|
|
||||||
|
# Fill remaining locations with Collectables
|
||||||
|
unfilled : int = len(self.multiworld.get_unfilled_locations(self.player)) - len(self.item_pool)
|
||||||
|
if self.options.shoppingsanity.value and self.options.hints_from_hintbooks.value:
|
||||||
|
if self.options.shoppingsanity.value == 2:
|
||||||
|
hint_books = set([*SHOP_PERSISTENT_HINT_BOOK, *SHOP_COLLECTION_HINT_BOOK])
|
||||||
|
else:
|
||||||
|
hint_books = set(SHOP_HINT_BOOK)
|
||||||
|
unfilled -= len(hint_books.difference(self.exclude_locations))
|
||||||
|
|
||||||
|
if unfilled < 0:
|
||||||
|
raise OptionError(
|
||||||
|
f"AE3: Too many progression items for available locations (overflow: {-unfilled}). "
|
||||||
|
f"Reduce extra_keys, extra_shop_stocks, or blacklisted channels.")
|
||||||
|
|
||||||
|
self.item_pool += generate_collectables(self.random, self.player, unfilled)
|
||||||
|
|
||||||
|
# Add Items to ItemPool
|
||||||
|
self.multiworld.itempool += self.item_pool
|
||||||
|
|
||||||
|
# Set Goal
|
||||||
|
self.multiworld.completion_condition[self.player] = Rulesets(self.goal_target.enact()).condense(
|
||||||
|
self.player)
|
||||||
|
|
||||||
|
def pre_fill(self) -> None:
|
||||||
|
if self.options.shoppingsanity.value and self.options.hints_from_hintbooks.value:
|
||||||
|
if self.options.shoppingsanity.value == 2:
|
||||||
|
hint_books = [*SHOP_PERSISTENT_HINT_BOOK, *SHOP_COLLECTION_HINT_BOOK]
|
||||||
|
else:
|
||||||
|
hint_books = [*SHOP_HINT_BOOK]
|
||||||
|
|
||||||
|
for hint_book in hint_books:
|
||||||
|
if hint_book in self.exclude_locations:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.multiworld.get_location(hint_book, self.player).place_locked_item(
|
||||||
|
self.create_item(APHelper.hint_book.value))
|
||||||
|
|
||||||
|
def generate_hints(self):
|
||||||
|
hints: dict[int, dict[str, int] | list[dict[str, int]]] = {}
|
||||||
|
progressive_scouts: list[dict[str, int]] = []
|
||||||
|
book_scouts: list[Location] = []
|
||||||
|
excluded_items: list[str] = [Itm.gadget_net.value, APHelper.hint_book.value]
|
||||||
|
items: list[str] = [*self.item_name_groups[APHelper.equipment.value],
|
||||||
|
*self.item_name_groups[APHelper.archipelago.value]]
|
||||||
|
|
||||||
|
if self.options.starting_gadget:
|
||||||
|
gadgets: list[str] = [Itm.gadget_club.value,
|
||||||
|
Itm.gadget_radar.value,
|
||||||
|
Itm.gadget_hoop.value,
|
||||||
|
Itm.gadget_sling.value,
|
||||||
|
Itm.gadget_swim.value,
|
||||||
|
Itm.gadget_rcc.value,
|
||||||
|
Itm.gadget_fly.value]
|
||||||
|
|
||||||
|
excluded_items.append(gadgets[self.options.starting_gadget - 1])
|
||||||
|
|
||||||
|
if self.options.starting_morph:
|
||||||
|
excluded_items.append(Itm.get_morphs_ordered()[self.options.starting_morph - 1])
|
||||||
|
|
||||||
|
if self.options.shuffle_chassis:
|
||||||
|
items.extend(Itm.get_chassis_by_id(no_default=True))
|
||||||
|
|
||||||
|
if self.options.shuffle_morph_stocks:
|
||||||
|
items.append(Itm.acc_morph_stock.value)
|
||||||
|
|
||||||
|
if self.options.add_morph_extensions:
|
||||||
|
items.append(Itm.acc_morph_ext.value)
|
||||||
|
|
||||||
|
items = [item for item in items if item not in excluded_items]
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
book_scouts.extend([loc for loc in self.multiworld.find_item_locations(item, self.player)])
|
||||||
|
|
||||||
|
# Use fillers when not enough Progressive Item Locations are scouted
|
||||||
|
if self.options.hints_from_hintbooks and len(book_scouts) < 20:
|
||||||
|
if self.options.lucky_ticket_consolation_effects:
|
||||||
|
for scout in book_scouts:
|
||||||
|
progressive_scouts.append({"name": scout.name, "id": scout.address, "player": scout.player})
|
||||||
|
|
||||||
|
fillers: list[str] = [Itm.jacket.value, Itm.energy_mega.value, Itm.cookie_giant.value, Itm.chip_10x.value]
|
||||||
|
for i, filler in enumerate(fillers):
|
||||||
|
book_scouts.extend([loc for loc in self.multiworld.find_item_locations(filler, self.player)])
|
||||||
|
if len(book_scouts) >= 20: break
|
||||||
|
|
||||||
|
if self.options.lucky_ticket_consolation_effects:
|
||||||
|
if not progressive_scouts:
|
||||||
|
for scout in book_scouts:
|
||||||
|
progressive_scouts.append({"name": scout.name, "id": scout.address, "player": scout.player})
|
||||||
|
|
||||||
|
hints[0] = progressive_scouts
|
||||||
|
|
||||||
|
if self.options.hints_from_hintbooks:
|
||||||
|
book_scouts = self.random.sample(book_scouts, 20)
|
||||||
|
|
||||||
|
if self.options.shoppingsanity == 2:
|
||||||
|
hint_books = [self.location_name_to_id[book] for book in [*SHOP_COLLECTION_HINT_BOOK,
|
||||||
|
*SHOP_PERSISTENT_HINT_BOOK]]
|
||||||
|
else:
|
||||||
|
hint_books = [self.location_name_to_id[book] for book in SHOP_HINT_BOOK]
|
||||||
|
|
||||||
|
for i, loc in enumerate(book_scouts):
|
||||||
|
hints[hint_books[i]] = {
|
||||||
|
"id" : loc.address,
|
||||||
|
"player" : loc.player
|
||||||
|
}
|
||||||
|
|
||||||
|
return hints
|
||||||
|
|
||||||
|
def fill_slot_data(self):
|
||||||
|
slot_data : dict = self.options.as_dict(*slot_data_options())
|
||||||
|
slot_data[APHelper.progression.value] = self.progression.progression
|
||||||
|
slot_data[APHelper.channel_order.value] = self.progression.order
|
||||||
|
slot_data[APHelper.shop_progression.value] = self.shop_rules.sets
|
||||||
|
|
||||||
|
if (self.options.shoppingsanity.value and self.options.hints_from_hintbooks.value or
|
||||||
|
self.options.lucky_ticket_consolation_effects):
|
||||||
|
slot_data[APHelper.hints.value] = self.generate_hints()
|
||||||
|
|
||||||
|
slot_data[APHelper.version.value] = APConsole.Info.world_ver.value
|
||||||
|
|
||||||
|
return slot_data
|
||||||
|
|
||||||
|
def write_spoiler(self, spoiler_handle: TextIO) -> None:
|
||||||
|
spoiler_handle.write(
|
||||||
|
f"\n\n[AE3] ============================================"
|
||||||
|
f"\n Channel Order for {self.multiworld.get_player_name(self.player)} ({self.player})\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
group_set: list[list[int]] = []
|
||||||
|
count: int = 0
|
||||||
|
for i, channel_set in enumerate(self.progression.progression):
|
||||||
|
offset: int = 0
|
||||||
|
if i == 0:
|
||||||
|
offset = 1
|
||||||
|
|
||||||
|
target: int = count + channel_set + offset
|
||||||
|
group_set.append([_ for _ in self.progression.order[count: target]])
|
||||||
|
count = target
|
||||||
|
|
||||||
|
count: int = 0
|
||||||
|
for i, sets in enumerate(group_set):
|
||||||
|
if not sets:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if i and i < len(group_set) - 2:
|
||||||
|
spoiler_handle.write(f"\n- < {i} > ---------------------------------------")
|
||||||
|
elif i and i == len(group_set) - 1:
|
||||||
|
spoiler_handle.write(f"\n- < X > ---------------------------------------")
|
||||||
|
elif i:
|
||||||
|
tag: str = ""
|
||||||
|
|
||||||
|
if self.options.post_game_condition_keys:
|
||||||
|
tag += f"{self.options.post_game_condition_keys.value + i - 1}"
|
||||||
|
if any([bool(self.options.post_game_condition_monkeys),
|
||||||
|
bool(self.options.post_game_condition_bosses),
|
||||||
|
bool(self.options.post_game_condition_cameras),
|
||||||
|
bool(self.options.post_game_condition_cellphones)]):
|
||||||
|
tag += "!"
|
||||||
|
|
||||||
|
spoiler_handle.write(f"\n- < {tag} > ---------------------------------------")
|
||||||
|
|
||||||
|
for channels in sets:
|
||||||
|
spoiler_handle.write(f"\n [{count + 1}]\t{LEVELS_BY_ORDER[channels]}")
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
spoiler_handle.write("\n")
|
||||||
|
|
||||||
|
def log_debug(self):
|
||||||
|
print("====================")
|
||||||
|
print("Channel Order:")
|
||||||
|
count = 0
|
||||||
|
for lset in self.progression.progression:
|
||||||
|
print(f"- < {count} > ---------------------")
|
||||||
|
current = lset if count > 0 else lset + 1
|
||||||
|
for channel in range(current):
|
||||||
|
print(LEVELS_BY_ORDER[self.progression.order[count]])
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
print("\nPost Game Condition:")
|
||||||
|
for condition, amount in self.post_game_condition.amounts.items():
|
||||||
|
print(f"\t{condition}: {amount}")
|
||||||
|
|
||||||
|
print("\nGoal Target:")
|
||||||
|
print(f"\t{self.goal_target.amount} / {len(self.goal_target.locations)}")
|
||||||
|
for target in self.goal_target.locations:
|
||||||
|
print(target)
|
||||||
|
|
||||||
|
def generate_output(self, directory : str):
|
||||||
|
datas = {
|
||||||
|
"slot_data" : self.fill_slot_data()
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def interpret_slot_data(slot_data: dict) -> dict:
|
||||||
|
return slot_data
|
||||||
|
|
||||||
|
def prepare_ut(self) -> bool:
|
||||||
|
re_gen_passthrough = getattr(self.multiworld, "re_gen_passthrough", {})
|
||||||
|
is_in_ut: bool = re_gen_passthrough and self.game in re_gen_passthrough
|
||||||
|
if is_in_ut:
|
||||||
|
slot_data = re_gen_passthrough[self.game]
|
||||||
|
# Re-instate important YAML Options
|
||||||
|
self.options.blacklist_channel.value = slot_data[APHelper.blacklist_channel.value]
|
||||||
|
|
||||||
|
self.options.monkeysanity_break_rooms.value = slot_data[APHelper.monkeysanitybr.value]
|
||||||
|
self.options.monkeysanity_passwords.value = slot_data[APHelper.monkeysanitypw.value]
|
||||||
|
self.options.camerasanity.value = slot_data[APHelper.camerasanity.value]
|
||||||
|
self.options.cellphonesanity.value = slot_data[APHelper.cellphonesanity.value]
|
||||||
|
self.options.shoppingsanity.value = slot_data[APHelper.shoppingsanity.value]
|
||||||
|
|
||||||
|
self.options.restock_progression.value = slot_data[APHelper.restock_progression.value]
|
||||||
|
self.options.cheap_items_minimum_requirement.value = slot_data[APHelper.cheap_items_min.value]
|
||||||
|
self.options.cheap_items_early_amount.value = slot_data[APHelper.cheap_items_early_amount.value]
|
||||||
|
self.options.farm_logic_sneaky_borgs.value = slot_data[APHelper.farm_logic_sneaky_borgs.value]
|
||||||
|
|
||||||
|
self.options.early_free_play.value = slot_data[APHelper.early_free_play.value]
|
||||||
|
|
||||||
|
# Regenerate Logic Preference
|
||||||
|
self.logic_preference = LogicPreferenceOptions[slot_data[APHelper.logic_preference.value]]()
|
||||||
|
self.logic_preference.apply_unlimited_gadget_float_rules(
|
||||||
|
bool(slot_data[APHelper.hds_logic.value]),
|
||||||
|
bool(slot_data[APHelper.pqj_logic.value]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.options.base_morph_duration.value >= 30 or self.options.add_morph_extensions.value:
|
||||||
|
self.logic_preference.apply_timed_kung_fu_rule(
|
||||||
|
self.options.base_morph_duration.value,
|
||||||
|
bool(self.options.add_morph_extensions.value)
|
||||||
|
)
|
||||||
|
self.logic_preference.apply_timed_morph_float(
|
||||||
|
self.options.base_morph_duration.value,
|
||||||
|
bool(self.options.add_morph_extensions.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
if slot_data[APHelper.base_morph_duration.value] >= 30 or slot_data[APHelper.add_morph_extensions.value]:
|
||||||
|
self.logic_preference.apply_timed_kung_fu_rule(
|
||||||
|
slot_data[APHelper.base_morph_duration.value],
|
||||||
|
bool(slot_data[APHelper.add_morph_extensions.value])
|
||||||
|
)
|
||||||
|
self.logic_preference.apply_timed_morph_float(
|
||||||
|
slot_data[APHelper.base_morph_duration.value],
|
||||||
|
bool(slot_data[APHelper.add_morph_extensions.value])
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logic_preference.apply_option_logic(self.options)
|
||||||
|
|
||||||
|
# Regenerate Progression, Goal Target, PGC and Rules
|
||||||
|
## Progression Mode
|
||||||
|
if APHelper.progression_mode.value in slot_data:
|
||||||
|
self.progression = ProgressionModeOptions[slot_data[APHelper.progression_mode.value]]()
|
||||||
|
|
||||||
|
## Progression
|
||||||
|
if APHelper.progression.value in slot_data and self.progression:
|
||||||
|
self.progression.set_progression(slot_data[APHelper.progression.value])
|
||||||
|
|
||||||
|
## Channel Order
|
||||||
|
if APHelper.channel_order.value in slot_data and self.progression:
|
||||||
|
self.progression.set_order(slot_data[APHelper.channel_order.value])
|
||||||
|
|
||||||
|
self.progression.regenerate_level_select_entrances()
|
||||||
|
|
||||||
|
|
||||||
|
# Get initial exclusions for Goal Target
|
||||||
|
excluded_stages: list[str] = []
|
||||||
|
excluded_locations: list[str] = [*MONKEYS_PASSWORDS]
|
||||||
|
|
||||||
|
# Exclude Shop Items based on Shoppingsanity Type and Blacklisted Channels
|
||||||
|
if slot_data[APHelper.blacklist_channel.value] and slot_data[APHelper.shoppingsanity.value] > 0:
|
||||||
|
## Always exclude Ultim-ape Fighter Minigame if anything is blacklisted
|
||||||
|
excluded_locations.extend(SHOP_PROGRESSION_75COMPLETION)
|
||||||
|
|
||||||
|
## Exclude Event/Condition-sensitive Items based on excluded levels
|
||||||
|
for region, item in SHOP_EVENT_ACCESS_DIRECTORY.items():
|
||||||
|
if region in slot_data[APHelper.blacklist_channel.value]:
|
||||||
|
excluded_locations.extend(item)
|
||||||
|
|
||||||
|
if not self.options.monkeysanity_break_rooms:
|
||||||
|
excluded_stages.extend([*STAGES_BREAK_ROOMS])
|
||||||
|
|
||||||
|
### Exclude Blacklisted Channels from Goal Target and Post Game Condition
|
||||||
|
if self.progression.progression[-1]:
|
||||||
|
for channel in self.progression.order[-self.progression.progression[-1]:]:
|
||||||
|
excluded_locations.extend(MONKEYS_MASTER_ORDERED[channel])
|
||||||
|
excluded_locations.append(CAMERAS_MASTER_ORDERED[channel])
|
||||||
|
|
||||||
|
excluded_phones_id: list[str] = CELLPHONES_MASTER_ORDERED[channel]
|
||||||
|
excluded_locations.extend(Cellphone_Name_to_ID[cell_id] for cell_id in excluded_phones_id)
|
||||||
|
|
||||||
|
# Exclude Ultim-ape Fighter if any blacklisted channels exist
|
||||||
|
if self.progression.progression[-1]:
|
||||||
|
excluded_locations.extend(SHOP_PROGRESSION_75COMPLETION)
|
||||||
|
|
||||||
|
goal_amount: int = 0
|
||||||
|
if APHelper.goal_target_ovr.value in slot_data:
|
||||||
|
goal_amount: int = slot_data[APHelper.goal_target_ovr.value]
|
||||||
|
|
||||||
|
# Goal Target
|
||||||
|
if APHelper.goal_target.value in slot_data:
|
||||||
|
goal_target = slot_data[APHelper.goal_target.value]
|
||||||
|
self.goal_target = GoalTargetOptions[goal_target](goal_amount,
|
||||||
|
excluded_stages,
|
||||||
|
excluded_locations,
|
||||||
|
self.options.shoppingsanity.value)
|
||||||
|
|
||||||
|
## Get Post Game Conditions
|
||||||
|
amounts: dict[str, int] = {}
|
||||||
|
|
||||||
|
if APHelper.pgc_monkeys.value in slot_data and slot_data[APHelper.pgc_monkeys.value]:
|
||||||
|
amount: int = 434 if slot_data[APHelper.pgc_monkeys.value] < 0 \
|
||||||
|
else slot_data[APHelper.pgc_monkeys.value]
|
||||||
|
amounts[APHelper.monkey.value] = amount
|
||||||
|
|
||||||
|
if APHelper.pgc_bosses.value in slot_data and slot_data[APHelper.pgc_bosses.value]:
|
||||||
|
amounts[APHelper.bosses.value] = slot_data[APHelper.pgc_bosses.value]
|
||||||
|
|
||||||
|
if APHelper.pgc_cameras.value in slot_data and slot_data[APHelper.pgc_cameras.value]:
|
||||||
|
amounts[APHelper.camera.value] = slot_data[APHelper.pgc_cameras.value]
|
||||||
|
|
||||||
|
if APHelper.pgc_cellphones.value in slot_data and slot_data[APHelper.pgc_cellphones.value]:
|
||||||
|
amounts[APHelper.cellphone.value] = slot_data[APHelper.pgc_cellphones.value]
|
||||||
|
|
||||||
|
if APHelper.pgc_shop.value in slot_data and slot_data[APHelper.pgc_shop.value]:
|
||||||
|
amounts[APHelper.shop.value] = slot_data[APHelper.pgc_shop.value]
|
||||||
|
|
||||||
|
if APHelper.pgc_keys.value in slot_data and slot_data[APHelper.pgc_keys.value]:
|
||||||
|
amounts[APHelper.keys.value] = slot_data[APHelper.pgc_keys.value]
|
||||||
|
|
||||||
|
# Exclude Channels in Post Game from being required for Post Game to be unlocked
|
||||||
|
post_game_start_index = sum(self.progression.progression[:-2]) + 1
|
||||||
|
for channel in (self.progression.order[post_game_start_index:
|
||||||
|
post_game_start_index + self.progression.progression[-2]]):
|
||||||
|
excluded_locations.extend(MONKEYS_MASTER_ORDERED[channel])
|
||||||
|
excluded_locations.append(CAMERAS_MASTER_ORDERED[channel])
|
||||||
|
|
||||||
|
excluded_phones_id: list[str] = CELLPHONES_MASTER_ORDERED[channel]
|
||||||
|
excluded_locations.extend(Cellphone_Name_to_ID[cell_id] for cell_id in excluded_phones_id)
|
||||||
|
|
||||||
|
# Exclude Ultim-ape Fighter from being a PGC requirement, as it requires as many monkeys as possible
|
||||||
|
excluded_locations.extend(SHOP_PROGRESSION_75COMPLETION)
|
||||||
|
|
||||||
|
## Post Game Access Rule Initialization
|
||||||
|
self.post_game_condition = PostGameCondition(amounts, excluded_stages, excluded_locations)
|
||||||
|
|
||||||
|
## Set up Shop Rules
|
||||||
|
self.shop_rules: ShopItemRules = ShopItemRules(self)
|
||||||
|
if self.shop_rules.post_game_items:
|
||||||
|
excluded_locations.extend(self.shop_rules.post_game_items)
|
||||||
|
|
||||||
|
self.post_game_condition = PostGameCondition(amounts, excluded_stages, excluded_locations,
|
||||||
|
self.options.shoppingsanity.value)
|
||||||
|
|
||||||
|
# Set Shop Rules PGC
|
||||||
|
self.shop_rules.set_pgc_rules(self)
|
||||||
|
|
||||||
|
# Store excluded locations
|
||||||
|
self.exclude_locations = excluded_locations
|
||||||
|
|
||||||
|
return is_in_ut
|
||||||
1
worlds/apeescape3/archipelago.json
Normal file
1
worlds/apeescape3/archipelago.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"game": "Ape Escape 3", "minimum_ap_version": "0.6.3", "world_version": "2.0.4", "authors": ["aidanii"], "compatible_version": 7, "version": 7}
|
||||||
19
worlds/apeescape3/components.py
Normal file
19
worlds/apeescape3/components.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from worlds.LauncherComponents import Component, Type, components, launch
|
||||||
|
|
||||||
|
from .data.Strings import APConsole, Meta
|
||||||
|
|
||||||
|
def run_client(*args: str) -> None:
|
||||||
|
from .AE3_Client import launch_init
|
||||||
|
|
||||||
|
launch(launch_init, name=APConsole.Info.client_name.value, args=args)
|
||||||
|
|
||||||
|
components.append(
|
||||||
|
Component(
|
||||||
|
APConsole.Info.client_name.value,
|
||||||
|
func=run_client,
|
||||||
|
game_name=Meta.game,
|
||||||
|
component_type=Type.CLIENT,
|
||||||
|
supports_uri=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
1150
worlds/apeescape3/data/Addresses.py
Normal file
1150
worlds/apeescape3/data/Addresses.py
Normal file
File diff suppressed because it is too large
Load Diff
29
worlds/apeescape3/data/Distribution.py
Normal file
29
worlds/apeescape3/data/Distribution.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from typing import Sequence
|
||||||
|
from .Strings import APHelper
|
||||||
|
|
||||||
|
RATES0: dict[str, float] = {
|
||||||
|
APHelper.nothing.value : 70.0,
|
||||||
|
APHelper.hint_filler.value : 15.0,
|
||||||
|
APHelper.hint_progressive.value : 5.5,
|
||||||
|
APHelper.check_filler.value : 5.25,
|
||||||
|
APHelper.check_progressive.value : 3.0,
|
||||||
|
APHelper.check_pgc.value : 0.25,
|
||||||
|
APHelper.check_gt.value : 0.1,
|
||||||
|
}
|
||||||
|
|
||||||
|
RATES1: dict[str, float] = {
|
||||||
|
APHelper.nothing.value : 40.0,
|
||||||
|
APHelper.hint_filler.value : 25.0,
|
||||||
|
APHelper.hint_progressive.value : 13,
|
||||||
|
APHelper.check_filler.value : 12.245,
|
||||||
|
APHelper.check_progressive.value : 8.0,
|
||||||
|
APHelper.check_pgc.value : 1.2,
|
||||||
|
APHelper.check_gt.value : 0.525,
|
||||||
|
APHelper.bypass_pgc.value : 0.025,
|
||||||
|
APHelper.instant_goal.value : 0.005,
|
||||||
|
}
|
||||||
|
|
||||||
|
CONSOLATION_RATES: Sequence[dict[str, float]] = [
|
||||||
|
RATES0,
|
||||||
|
RATES1,
|
||||||
|
]
|
||||||
323
worlds/apeescape3/data/Items.py
Normal file
323
worlds/apeescape3/data/Items.py
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from abc import ABC
|
||||||
|
import random
|
||||||
|
|
||||||
|
from BaseClasses import Item, ItemClassification
|
||||||
|
from .Strings import Itm, Game, Meta, APHelper
|
||||||
|
from .Addresses import NTSCU
|
||||||
|
|
||||||
|
### [< --- HELPERS --- >]
|
||||||
|
class AE3Item(Item):
|
||||||
|
"""
|
||||||
|
Defines an Item in Ape Escape 3. These include but are not limited to the Gadgets, Morphs and select buyable items
|
||||||
|
in the Shopping District.
|
||||||
|
"""
|
||||||
|
|
||||||
|
game : str = Meta.game
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AE3ItemMeta(ABC):
|
||||||
|
"""Base Data Class for all Items in Ape Escape 3."""
|
||||||
|
name : str
|
||||||
|
item_id : int
|
||||||
|
address : int
|
||||||
|
|
||||||
|
def to_item(self, player : int) -> AE3Item:
|
||||||
|
return AE3Item(self.name, ItemClassification.filler, self.item_id, player)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EquipmentItem(AE3ItemMeta):
|
||||||
|
"""
|
||||||
|
Base Data Class for any Item that the player can only have one of. They can only be either locked or unlocked.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
name : Name of Item from Strings.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name : str):
|
||||||
|
self.name = name
|
||||||
|
# Equipment can be assumed to always be in Addresses.Items. NTSCU version will be used as basis for the ID.
|
||||||
|
self.item_id = NTSCU.Items[name]
|
||||||
|
self.address = self.item_id
|
||||||
|
|
||||||
|
def to_item(self, player : int, classification : ItemClassification = ItemClassification.progression) -> AE3Item:
|
||||||
|
return AE3Item(self.name, classification, self.item_id, player)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CollectableItem(AE3ItemMeta):
|
||||||
|
"""
|
||||||
|
Base Data Class for any Item that the player can obtain multiples of continuously regardless of whether the player
|
||||||
|
is allowed to collect more.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
name : Name of Item from Strings.py
|
||||||
|
resource : Name of resource affected by Item from Strings.py
|
||||||
|
amount : Amount of the Item to give
|
||||||
|
weight : How often to be chosen to fill a location
|
||||||
|
id_offset : (default : 0) Added Offset to ID for Items that target the same Memory Address
|
||||||
|
"""
|
||||||
|
|
||||||
|
resource : str
|
||||||
|
amount : int | float
|
||||||
|
capacity : int
|
||||||
|
weight : int
|
||||||
|
|
||||||
|
def __init__(self, name : str, resource : str, amount : int | float, weight : int, id_offset : int = 0):
|
||||||
|
self.name = name
|
||||||
|
# Collectables can be assumed to always be in Addresses.Items. NTSCU version will be used as basis for the ID.
|
||||||
|
self.address = NTSCU.GameStates[resource]
|
||||||
|
self.item_id = self.address + id_offset
|
||||||
|
self.resource = resource
|
||||||
|
|
||||||
|
self.amount = amount
|
||||||
|
self.capacity = Capacities[resource]
|
||||||
|
self.weight = weight
|
||||||
|
|
||||||
|
|
||||||
|
class UpgradeableItem(AE3ItemMeta):
|
||||||
|
"""
|
||||||
|
Base class for any item the player can obtain multiples of but only exists in specific amounts.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
name : Name of Item from Strings.py
|
||||||
|
resource : Name of resource affected by Item from Strings.py
|
||||||
|
limit : Maximum amount of the item that is expected to exist in the game
|
||||||
|
id_offset : (default : 0) Added Offset to ID for Items that target the same Memory Address
|
||||||
|
"""
|
||||||
|
|
||||||
|
resource : str
|
||||||
|
amount : int | float
|
||||||
|
limit : int
|
||||||
|
|
||||||
|
def __init__(self, name : str, resource : str, amount : int | float, limit : int, id_offset : int = 0):
|
||||||
|
self.name = name
|
||||||
|
# Upgradeables can be assumed to always be in Addresses.Items. NTSCU version will be used as basis for the ID.
|
||||||
|
self.address = NTSCU.GameStates[resource]
|
||||||
|
self.item_id = self.address + id_offset
|
||||||
|
self.resource = resource
|
||||||
|
|
||||||
|
self.amount = amount
|
||||||
|
self.limit = limit
|
||||||
|
|
||||||
|
def to_item(self, player : int, classification : ItemClassification = ItemClassification.useful) -> AE3Item:
|
||||||
|
return AE3Item(self.name, classification, self.item_id, player)
|
||||||
|
|
||||||
|
def to_items(self, player : int, classification : ItemClassification = ItemClassification.useful) -> list[AE3Item]:
|
||||||
|
return [self.to_item(player, classification) for _ in range(self.limit)]
|
||||||
|
|
||||||
|
class ArchipelagoItem(AE3ItemMeta):
|
||||||
|
"""Base class for any non in-game item"""
|
||||||
|
def __init__(self, name : str):
|
||||||
|
self.name = name
|
||||||
|
self.item_id = AP[name]
|
||||||
|
|
||||||
|
def to_item(self, player : int) -> AE3Item:
|
||||||
|
return AE3Item(self.name, ItemClassification.progression, self.item_id, player)
|
||||||
|
|
||||||
|
def to_items(self, player : int, amount : int):
|
||||||
|
return [self.to_item(player) for _ in range(amount)]
|
||||||
|
|
||||||
|
### [< --- DATA --->]
|
||||||
|
Capacities : dict[str, int | float] = {
|
||||||
|
Game.morph_duration.value : 30.0,
|
||||||
|
Game.nothing.value : 0x0,
|
||||||
|
Game.cookies.value : 100.0,
|
||||||
|
Game.jackets.value : 0x63,
|
||||||
|
Game.chips.value : 0x270F,
|
||||||
|
Game.morph_gauge_active.value : 30.0,
|
||||||
|
Game.morph_stocks.value : 1100.0,
|
||||||
|
Game.ammo_boom.value : 0x9,
|
||||||
|
Game.ammo_homing.value : 0x9,
|
||||||
|
}
|
||||||
|
|
||||||
|
HUD_OFFSETS : dict[str, int] = {
|
||||||
|
Game.jackets.value : 0x0,
|
||||||
|
Game.cookies.value : 0x4,
|
||||||
|
Game.chips.value : 0x6,
|
||||||
|
}
|
||||||
|
|
||||||
|
AP : dict[str, int] = {
|
||||||
|
APHelper.channel_key.value : 0x3E8,
|
||||||
|
APHelper.victory.value : 0x3E9,
|
||||||
|
APHelper.shop_stock.value : 0x3EA,
|
||||||
|
APHelper.hint_book.value : 0x3EB,
|
||||||
|
}
|
||||||
|
|
||||||
|
### [< --- ITEMS --- >]
|
||||||
|
# Gadgets
|
||||||
|
Gadget_Club = EquipmentItem(Itm.gadget_club.value)
|
||||||
|
Gadget_Net = EquipmentItem(Itm.gadget_net.value)
|
||||||
|
Gadget_Radar = EquipmentItem(Itm.gadget_radar.value)
|
||||||
|
Gadget_Hoop = EquipmentItem(Itm.gadget_hoop.value)
|
||||||
|
Gadget_Sling = EquipmentItem(Itm.gadget_sling.value)
|
||||||
|
Gadget_Swim = EquipmentItem(Itm.gadget_swim.value)
|
||||||
|
Gadget_RCC = EquipmentItem(Itm.gadget_rcc.value)
|
||||||
|
Gadget_Fly = EquipmentItem(Itm.gadget_fly.value)
|
||||||
|
|
||||||
|
# Morphs
|
||||||
|
Morph_Knight = EquipmentItem(Itm.morph_knight.value)
|
||||||
|
Morph_Cowboy = EquipmentItem(Itm.morph_cowboy.value)
|
||||||
|
Morph_Ninja = EquipmentItem(Itm.morph_ninja.value)
|
||||||
|
Morph_Magician = EquipmentItem(Itm.morph_magician.value)
|
||||||
|
Morph_Kungfu = EquipmentItem(Itm.morph_kungfu.value)
|
||||||
|
Morph_Hero = EquipmentItem(Itm.morph_hero.value)
|
||||||
|
Morph_Monkey = EquipmentItem(Itm.morph_monkey.value)
|
||||||
|
|
||||||
|
# Accessories
|
||||||
|
Chassis_Twin = EquipmentItem(Itm.chassis_twin.value)
|
||||||
|
Chassis_Black = EquipmentItem(Itm.chassis_black.value)
|
||||||
|
Chassis_Pudding = EquipmentItem(Itm.chassis_pudding.value)
|
||||||
|
|
||||||
|
# Upgradeables
|
||||||
|
Acc_Morph_Stock = UpgradeableItem(Itm.acc_morph_stock.value, Game.morph_stocks.value, 100.0, 10)
|
||||||
|
Acc_Morph_Ext = UpgradeableItem(Itm.acc_morph_ext.value, Game.morph_duration.value, 2.0, 10)
|
||||||
|
|
||||||
|
# Collectables
|
||||||
|
Nothing = CollectableItem(Itm.nothing.value, Game.nothing.value,0, 1)
|
||||||
|
|
||||||
|
Cookie = CollectableItem(Itm.cookie.value, Game.cookies.value, 20.0, 20)
|
||||||
|
Cookie_Giant = CollectableItem(Itm.cookie_giant.value, Game.cookies.value, 100.0, 10, 0x01)
|
||||||
|
Jacket = CollectableItem(Itm.jacket.value, Game.jackets.value, 1, 5)
|
||||||
|
Chip_1x = CollectableItem(Itm.chip_1x.value, Game.chips.value, 1, 40)
|
||||||
|
Chip_5x = CollectableItem(Itm.chip_5x.value, Game.chips.value, 5, 35, 0x01)
|
||||||
|
Chip_10x = CollectableItem(Itm.chip_10x.value, Game.chips.value, 10, 30, 0x02)
|
||||||
|
Energy = CollectableItem(Itm.energy.value, Game.morph_gauge_active.value, 3.0, 40,0x0)
|
||||||
|
Energy_Mega = CollectableItem(Itm.energy_mega.value, Game.morph_gauge_active.value, 30.0, 10,
|
||||||
|
0x01)
|
||||||
|
|
||||||
|
Ammo_Boom = CollectableItem(Itm.ammo_boom.value, Game.ammo_boom.value, 1, 25)
|
||||||
|
Ammo_Homing = CollectableItem(Itm.ammo_homing.value, Game.ammo_homing.value, 1, 25)
|
||||||
|
|
||||||
|
# Archipelago
|
||||||
|
Channel_Key = ArchipelagoItem(APHelper.channel_key.value)
|
||||||
|
Shop_Stock = ArchipelagoItem(APHelper.shop_stock.value)
|
||||||
|
Hint_Book = ArchipelagoItem(APHelper.hint_book.value)
|
||||||
|
Victory = ArchipelagoItem(APHelper.victory.value)
|
||||||
|
|
||||||
|
### [< --- ITEM GROUPS --- >]
|
||||||
|
GADGETS : Sequence[EquipmentItem] = [
|
||||||
|
Gadget_Swim, Gadget_Club, Gadget_Net, Gadget_Radar, Gadget_Hoop, Gadget_Sling, Gadget_RCC, Gadget_Fly
|
||||||
|
]
|
||||||
|
|
||||||
|
MORPHS : Sequence[EquipmentItem] = [
|
||||||
|
Morph_Knight, Morph_Cowboy, Morph_Ninja, Morph_Magician, Morph_Kungfu, Morph_Hero, Morph_Monkey
|
||||||
|
]
|
||||||
|
|
||||||
|
EQUIPMENT : Sequence[EquipmentItem] = [
|
||||||
|
*GADGETS, *MORPHS
|
||||||
|
]
|
||||||
|
|
||||||
|
ACCESSORIES : Sequence[EquipmentItem] = [
|
||||||
|
Chassis_Twin, Chassis_Black, Chassis_Pudding
|
||||||
|
]
|
||||||
|
|
||||||
|
UPGRADEABLES : Sequence[UpgradeableItem] = [
|
||||||
|
Acc_Morph_Stock, Acc_Morph_Ext
|
||||||
|
]
|
||||||
|
|
||||||
|
COLLECTABLES : Sequence[CollectableItem] = [
|
||||||
|
Nothing, Cookie, Cookie_Giant, Jacket, Chip_1x, Chip_5x, Chip_10x, Energy, Energy_Mega, Ammo_Boom, Ammo_Homing
|
||||||
|
]
|
||||||
|
|
||||||
|
ARCHIPELAGO : Sequence[ArchipelagoItem] = [
|
||||||
|
Channel_Key, Shop_Stock, Hint_Book, Victory
|
||||||
|
]
|
||||||
|
|
||||||
|
ITEMS_MASTER : Sequence[AE3ItemMeta] = [
|
||||||
|
*GADGETS, *MORPHS, *ACCESSORIES, *UPGRADEABLES, *COLLECTABLES, *ARCHIPELAGO
|
||||||
|
]
|
||||||
|
|
||||||
|
ITEMS_INDEX : Sequence[Sequence] = [
|
||||||
|
ITEMS_MASTER, GADGETS, MORPHS, EQUIPMENT, ACCESSORIES, UPGRADEABLES, COLLECTABLES, ARCHIPELAGO
|
||||||
|
]
|
||||||
|
|
||||||
|
### [< --- METHODS --- >]
|
||||||
|
def from_id(item_id = int, category : int = 0):
|
||||||
|
"""Get Item by its ID"""
|
||||||
|
ref : Sequence = ITEMS_INDEX[category]
|
||||||
|
|
||||||
|
i : AE3ItemMeta = next((i for i in ref if i.item_id == item_id), None)
|
||||||
|
return i
|
||||||
|
|
||||||
|
def generate_name_to_id() -> dict[str : int]:
|
||||||
|
"""Get a Dictionary of all Items in Name-ID pairs"""
|
||||||
|
i : AE3ItemMeta
|
||||||
|
return {i.name : i.item_id for i in ITEMS_MASTER}
|
||||||
|
|
||||||
|
def generate_item_groups() -> dict[str : set[str]]:
|
||||||
|
"""Get a Dictionary of Item Groups"""
|
||||||
|
groups : dict[str : set[str]] = {}
|
||||||
|
|
||||||
|
i : AE3ItemMeta
|
||||||
|
# Gadgets
|
||||||
|
for i in GADGETS:
|
||||||
|
groups.setdefault(APHelper.gadgets.value, set()).add(i.name)
|
||||||
|
|
||||||
|
# Morphs
|
||||||
|
for i in MORPHS:
|
||||||
|
groups.setdefault(APHelper.morphs.value, set()).add(i.name)
|
||||||
|
|
||||||
|
# Equipment
|
||||||
|
groups.setdefault(APHelper.equipment.value, set()).update(groups[APHelper.gadgets.value])
|
||||||
|
groups.setdefault(APHelper.equipment.value, set()).update(groups[APHelper.morphs.value])
|
||||||
|
|
||||||
|
# Morphs (without Super Monkey)
|
||||||
|
groups.setdefault(APHelper.morphs_no_monkey.value, groups[APHelper.morphs.value]).remove(Itm.morph_monkey.value)
|
||||||
|
|
||||||
|
# RC Cars
|
||||||
|
groups.setdefault(APHelper.rc_cars.value, set()).update([
|
||||||
|
Itm.gadget_rcc.value, Itm.chassis_twin.value, Itm.chassis_black.value, Itm.chassis_pudding.value
|
||||||
|
])
|
||||||
|
|
||||||
|
# Catch (Long)
|
||||||
|
groups.setdefault(APHelper.catch_long.value, set()).update([
|
||||||
|
Itm.morph_cowboy.value, Itm.morph_ninja.value, Itm.morph_magician.value, Itm.morph_hero.value
|
||||||
|
])
|
||||||
|
|
||||||
|
# Attack
|
||||||
|
groups.setdefault(APHelper.attack.value, set()).update([
|
||||||
|
Itm.gadget_club.value, Itm.gadget_sling.value, *groups[APHelper.morphs_no_monkey.value]
|
||||||
|
])
|
||||||
|
|
||||||
|
# Hit
|
||||||
|
groups.setdefault(APHelper.hit.value, set()).update([
|
||||||
|
Itm.gadget_hoop.value, Itm.morph_monkey.value, Itm.gadget_fly.value, *groups[APHelper.rc_cars.value],
|
||||||
|
*groups[APHelper.attack.value]
|
||||||
|
])
|
||||||
|
|
||||||
|
# Dash
|
||||||
|
groups.setdefault(APHelper.dash.value, set()).update([
|
||||||
|
Itm.gadget_hoop.value, Itm.morph_ninja.value, Itm.morph_hero.value
|
||||||
|
])
|
||||||
|
|
||||||
|
# Shoot
|
||||||
|
groups.setdefault(APHelper.shoot.value, set()).update([
|
||||||
|
Itm.gadget_sling.value, Itm.morph_cowboy.value, Itm.morph_hero.value
|
||||||
|
])
|
||||||
|
|
||||||
|
# Fly
|
||||||
|
groups.setdefault(APHelper.fly.value, set()).update([
|
||||||
|
Itm.gadget_fly.value, Itm.morph_ninja.value
|
||||||
|
])
|
||||||
|
|
||||||
|
# Glide
|
||||||
|
groups.setdefault(APHelper.glide.value, set()).update([
|
||||||
|
Itm.morph_hero.value, *groups[APHelper.fly.value]
|
||||||
|
])
|
||||||
|
|
||||||
|
# Archipelago
|
||||||
|
groups.setdefault(APHelper.archipelago.value, set()).update([
|
||||||
|
APHelper.channel_key.value, APHelper.shop_stock.value
|
||||||
|
])
|
||||||
|
|
||||||
|
return groups
|
||||||
|
|
||||||
|
def generate_collectables(rand : random, player : int, amt : int) -> list[AE3Item]:
|
||||||
|
"""Get a list of Items of the specified Archipelago"""
|
||||||
|
weights : list[int] = [w.weight for w in COLLECTABLES]
|
||||||
|
|
||||||
|
result : list[CollectableItem] = rand.choices([*COLLECTABLES], [*weights], k = amt)
|
||||||
|
items : list[AE3Item] = [c.to_item(player) for c in result]
|
||||||
|
|
||||||
|
return items
|
||||||
3549
worlds/apeescape3/data/Locations.py
Normal file
3549
worlds/apeescape3/data/Locations.py
Normal file
File diff suppressed because it is too large
Load Diff
802
worlds/apeescape3/data/Logic.py
Normal file
802
worlds/apeescape3/data/Logic.py
Normal file
@@ -0,0 +1,802 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
from typing import TYPE_CHECKING, Set, Sequence, Callable
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from BaseClasses import CollectionState, Item
|
||||||
|
|
||||||
|
from .Items import Channel_Key
|
||||||
|
from .Stages import AE3EntranceMeta, ENTRANCES_STAGE_SELECT, ENTRANCES_CHANNELS, LEVELS_BY_ORDER, STAGES_FARMABLE, \
|
||||||
|
STAGES_FARMABLE_SNEAKY_BORG
|
||||||
|
from .Strings import Itm, Stage, APHelper
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import AE3World
|
||||||
|
|
||||||
|
### [< --- ACCESS RULES --- >]
|
||||||
|
## Check if Player can Catch Monkeys
|
||||||
|
def can_catch(state : CollectionState, player : int):
|
||||||
|
return can_net(state, player) or can_morph_not_monkey(state, player)
|
||||||
|
|
||||||
|
def can_catch_long(state : CollectionState, player : int):
|
||||||
|
return state.has_group(APHelper.catch_long.value, player)
|
||||||
|
|
||||||
|
def can_net(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.gadget_net.value, player)
|
||||||
|
|
||||||
|
## Check if Player can Morph
|
||||||
|
def can_morph(state : CollectionState, player : int):
|
||||||
|
return state.has_group(APHelper.morphs.value, player)
|
||||||
|
|
||||||
|
def can_morph_not_monkey(state : CollectionState, player : int):
|
||||||
|
return state.has_group(APHelper.morphs_no_monkey.value, player)
|
||||||
|
|
||||||
|
# Gadget Checks
|
||||||
|
def has_radar(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.gadget_radar.value, player)
|
||||||
|
|
||||||
|
def has_club(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.gadget_club.value, player)
|
||||||
|
|
||||||
|
def has_hoop(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.gadget_hoop.value, player)
|
||||||
|
|
||||||
|
def has_flyer(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.gadget_fly.value, player)
|
||||||
|
|
||||||
|
## Check if Player can use the Slingback Shooter
|
||||||
|
def can_sling(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.gadget_sling.value, player)
|
||||||
|
|
||||||
|
## Check if Player has Water Net
|
||||||
|
def can_swim(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.gadget_swim.value, player)
|
||||||
|
|
||||||
|
## Check if Player can use the RC Car
|
||||||
|
def can_rcc(state : CollectionState, player : int):
|
||||||
|
return state.has_group(APHelper.rc_cars.value, player)
|
||||||
|
|
||||||
|
# Morph Checks
|
||||||
|
def can_knight(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.morph_knight.value, player)
|
||||||
|
|
||||||
|
def can_cowboy(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.morph_cowboy.value, player)
|
||||||
|
|
||||||
|
def can_ninja(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.morph_ninja.value, player)
|
||||||
|
|
||||||
|
def can_genie(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.morph_magician.value, player)
|
||||||
|
|
||||||
|
def can_kungfu(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.morph_kungfu.value, player)
|
||||||
|
|
||||||
|
def can_hero(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.morph_hero.value, player)
|
||||||
|
|
||||||
|
def can_monkey(state : CollectionState, player : int):
|
||||||
|
return state.has(Itm.morph_monkey.value, player)
|
||||||
|
|
||||||
|
# General Ability Checks
|
||||||
|
## Check if player can hit beyond basic hip drops
|
||||||
|
def can_attack(state : CollectionState, player : int):
|
||||||
|
return state.has_group(APHelper.attack.value, player)
|
||||||
|
|
||||||
|
def can_hit(state : CollectionState, player : int):
|
||||||
|
return state.has_group(APHelper.hit.value, player)
|
||||||
|
|
||||||
|
## Check if player has the ability to move fast
|
||||||
|
def can_dash(state : CollectionState, player : int):
|
||||||
|
return state.has_group(APHelper.dash.value, player)
|
||||||
|
|
||||||
|
## Check if Player can use long-ranged attacks
|
||||||
|
def can_shoot(state : CollectionState, player : int):
|
||||||
|
return state.has_group(APHelper.shoot.value, player)
|
||||||
|
|
||||||
|
def can_shoot_boom(state : CollectionState, player : int):
|
||||||
|
return can_shoot(state, player) and state.has(Itm.ammo_boom.value, player)
|
||||||
|
|
||||||
|
## Check if Player can fly (can gain height)
|
||||||
|
def can_fly(state : CollectionState, player : int):
|
||||||
|
return state.has_group(APHelper.fly.value, player)
|
||||||
|
|
||||||
|
## Check if the Player can glide
|
||||||
|
def can_glide(state : CollectionState, player : int):
|
||||||
|
return state.has_group(APHelper.glide.value, player)
|
||||||
|
|
||||||
|
## Boost Jump
|
||||||
|
def can_boost_jump(state : CollectionState, player : int):
|
||||||
|
return (state.has_any([Itm.gadget_net.value, Itm.gadget_club.value], player),
|
||||||
|
state.has_from_list_unique([Itm.gadget_net.value, Itm.gadget_club.value,
|
||||||
|
Itm.gadget_radar.value, Itm.gadget_hoop.value,
|
||||||
|
Itm.gadget_sling.value, Itm.gadget_rcc.value,
|
||||||
|
Itm.gadget_fly.value], player, 2))
|
||||||
|
|
||||||
|
## Boost Fly
|
||||||
|
def can_boost_fly(state : CollectionState, player : int):
|
||||||
|
return (state.has(Itm.gadget_fly.value, player) and
|
||||||
|
state.has_any([Itm.gadget_net.value, Itm.gadget_club.value], player))
|
||||||
|
|
||||||
|
## Quad Jump
|
||||||
|
def can_qj(state : CollectionState, player : int):
|
||||||
|
return (state.has(Itm.gadget_club.value, player) and
|
||||||
|
state.has_any([
|
||||||
|
Itm.gadget_net.value,
|
||||||
|
Itm.gadget_radar.value,
|
||||||
|
Itm.gadget_hoop.value,
|
||||||
|
Itm.gadget_rcc.value,
|
||||||
|
Itm.gadget_fly.value,
|
||||||
|
], player))
|
||||||
|
|
||||||
|
## Glitch Float (Encompasses both Net Float and HDS)
|
||||||
|
def can_glitch_float(state: CollectionState, player : int):
|
||||||
|
return state.has_all([Itm.gadget_net.value, Itm.gadget_sling.value], player)
|
||||||
|
|
||||||
|
## Glitch Float related to Morphs
|
||||||
|
def can_glitch_float_morph(state: CollectionState, player : int):
|
||||||
|
return state.has_any([
|
||||||
|
Itm.morph_ninja.value,
|
||||||
|
Itm.morph_monkey.value
|
||||||
|
], player)
|
||||||
|
|
||||||
|
# Access Checks
|
||||||
|
def can_reach_region(state : CollectionState, player : int, region : str):
|
||||||
|
return state.can_reach_region(region, player)
|
||||||
|
|
||||||
|
def can_access_region(region : str):
|
||||||
|
return lambda state, player : can_reach_region(state, player, region)
|
||||||
|
|
||||||
|
def can_farm_boxes():
|
||||||
|
return lambda state, player : any(can_reach_region(state, player, region) for region in STAGES_FARMABLE)
|
||||||
|
|
||||||
|
def can_farm_sneaky_borgs():
|
||||||
|
return lambda state, player : any(can_reach_region(state, player, region) for region in STAGES_FARMABLE_SNEAKY_BORG)
|
||||||
|
|
||||||
|
def has_enough_morph_stocks(state : CollectionState, player : int, stocks : int = 1):
|
||||||
|
return state.has(Itm.acc_morph_stock.value, player, stocks)
|
||||||
|
|
||||||
|
def has_morph_stocks(stocks : int = 1):
|
||||||
|
return lambda state, player : has_enough_morph_stocks(state, player, stocks)
|
||||||
|
|
||||||
|
def has_enough_morph_extensions(state : CollectionState, player : int, extensions : int = 10):
|
||||||
|
return state.has(Itm.acc_morph_ext.value, player, extensions)
|
||||||
|
|
||||||
|
def has_morph_extensions(extensions: int = 10):
|
||||||
|
return lambda state, player : has_enough_morph_extensions(state, player, extensions)
|
||||||
|
|
||||||
|
def has_enough_keys(state : CollectionState, player : int, keys : int):
|
||||||
|
return state.has(APHelper.channel_key.value, player, keys)
|
||||||
|
|
||||||
|
def has_keys(keys : int):
|
||||||
|
return lambda state, player : has_enough_keys(state, player, keys=keys)
|
||||||
|
|
||||||
|
def has_enough_shop_stock(state : CollectionState, player : int, stock : int):
|
||||||
|
return state.has(APHelper.shop_stock.value, player, stock)
|
||||||
|
|
||||||
|
def has_shop_stock(stock : int):
|
||||||
|
return lambda state, player : has_enough_shop_stock(state, player, stock)
|
||||||
|
|
||||||
|
# Event Checks
|
||||||
|
def is_event_invoked(state : CollectionState, player : int, event_name : str):
|
||||||
|
return state.has(event_name, player)
|
||||||
|
|
||||||
|
def is_event_not_invoked(state : CollectionState, player : int, event_name : str):
|
||||||
|
return not state.has(event_name, player)
|
||||||
|
|
||||||
|
def event_invoked(event_name : str):
|
||||||
|
return lambda state, player : is_event_invoked(state, player, event_name)
|
||||||
|
|
||||||
|
def is_goal_achieved(state : CollectionState, player : int, count : int = 1):
|
||||||
|
return state.has(APHelper.victory.value, player, count)
|
||||||
|
|
||||||
|
def are_goals_achieved(goal_count : int):
|
||||||
|
return lambda state, player : is_goal_achieved(state, player, goal_count)
|
||||||
|
|
||||||
|
### [< --- WRAPPER SHORTHAND --- >]
|
||||||
|
class AccessRule:
|
||||||
|
"""
|
||||||
|
Defines required states for the player to achieve in order for an item to be considered "reachable".
|
||||||
|
"""
|
||||||
|
|
||||||
|
# General
|
||||||
|
CATCH = can_catch # Monkey Net unlocked or any Morph that can Catch Monkeys
|
||||||
|
CATCH_LONG = can_catch_long # Has any morph with ranged capture
|
||||||
|
MORPH = can_morph
|
||||||
|
MORPH_NO_MONKEY = can_morph_not_monkey # Unlocked any morph that is not Super Monkey
|
||||||
|
ATTACK = can_attack # Can attack reasonably
|
||||||
|
HIT = can_hit # Can hit at all
|
||||||
|
DASH = can_dash # Unlocked Super Hoop or any fast moving Morph
|
||||||
|
SHOOT = can_shoot # Slingback Shooter unlocked or has any morph with long range attacks
|
||||||
|
SHOOT_BOOM = can_shoot_boom # Slingback Shooter unlocked or has any morph with long range attacks
|
||||||
|
SWIM = can_swim
|
||||||
|
FLY = can_fly # Sky Flyer unlocked or has any morph that can fly (gain height)
|
||||||
|
GLIDE = can_glide # Sky Flyer unlocked or has any morph that can glide
|
||||||
|
|
||||||
|
# Gadget
|
||||||
|
NET = can_net # Monkey Net Unlocked
|
||||||
|
CLUB = has_club # Unlocked Stun Club
|
||||||
|
RADAR = has_radar # Unlocked Monkey Radar
|
||||||
|
HOOP = has_hoop # Unlocked Dash Hoop
|
||||||
|
FLYER = has_flyer # Unlocked Sky Flyer
|
||||||
|
SLING = can_sling
|
||||||
|
RCC = can_rcc
|
||||||
|
|
||||||
|
# Morph
|
||||||
|
KNIGHT = can_knight
|
||||||
|
COWBOY = can_cowboy
|
||||||
|
NINJA = can_ninja
|
||||||
|
MAGICIAN = can_genie
|
||||||
|
KUNGFU = can_kungfu
|
||||||
|
HERO = can_hero
|
||||||
|
MONKEY = can_monkey
|
||||||
|
|
||||||
|
# Access
|
||||||
|
FARM = can_farm_boxes()
|
||||||
|
FARM_DUPE = can_farm_sneaky_borgs()
|
||||||
|
|
||||||
|
# NULL
|
||||||
|
NULL = (lambda state, player : False)
|
||||||
|
|
||||||
|
# Glitches
|
||||||
|
BOOST_JUMP = can_boost_jump # Can Boost Jump
|
||||||
|
BOOST_FLY = can_boost_fly # Can Boost Fly
|
||||||
|
QJ = can_qj # Can use Quad Jumps (Stun Club and most other gadgets)
|
||||||
|
G_FLOAT = can_glitch_float # Can use Net Floats/HDS (Monkey Net and Slingback Shooter)
|
||||||
|
G_FLOAT_M = can_glitch_float_morph # Can do Infinite Jumps with Eligible Morphs
|
||||||
|
|
||||||
|
# Victory
|
||||||
|
GOAL = is_goal_achieved
|
||||||
|
|
||||||
|
### [< --- MANAGING CLASS --- >]
|
||||||
|
class Rulesets:
|
||||||
|
"""
|
||||||
|
Helper for Storing and Managing Access Rules of Locations.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
critical : Set of AccessRules that must always be true for a Location to be reachable.
|
||||||
|
rules : Normal Sets of AccessRules. In addition to adhering to AccessRules set in Critical,
|
||||||
|
at least one set of AccessRules must also be adhered to.
|
||||||
|
"""
|
||||||
|
critical : Set[Callable] = None
|
||||||
|
rules : list[list[Callable]] = None
|
||||||
|
|
||||||
|
def __init__(self, *rules : Callable | list[Callable] | list[list[Callable]] | None,
|
||||||
|
critical : Set[Callable] = None):
|
||||||
|
self.critical = set()
|
||||||
|
self.rules = []
|
||||||
|
|
||||||
|
if critical:
|
||||||
|
self.critical = critical
|
||||||
|
|
||||||
|
if rules:
|
||||||
|
for rule in rules:
|
||||||
|
if isinstance(rule, Callable):
|
||||||
|
self.rules.append([rule])
|
||||||
|
elif isinstance(rule, list):
|
||||||
|
if all(isinstance(_, Callable) for _ in rule):
|
||||||
|
self.rules.append(rule)
|
||||||
|
elif all(isinstance(x, list) and all(isinstance(_, Callable) for _ in x) for x in rule):
|
||||||
|
self.rules.extend(rule)
|
||||||
|
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(self.critical) or bool(self.rules)
|
||||||
|
|
||||||
|
def update(self, rulesets : "Rulesets"):
|
||||||
|
if not rulesets:
|
||||||
|
return
|
||||||
|
|
||||||
|
if rulesets.critical:
|
||||||
|
self.critical.update(rulesets.critical)
|
||||||
|
|
||||||
|
if rulesets.rules:
|
||||||
|
for i, rule in enumerate(rulesets.rules):
|
||||||
|
if rule in self.rules:
|
||||||
|
rulesets.rules.pop(i)
|
||||||
|
|
||||||
|
self.rules.extend(rulesets.rules)
|
||||||
|
|
||||||
|
def check(self, state : CollectionState, player : int) -> bool:
|
||||||
|
# Any Critical Rules that return False should immediately mark the item as inaccessible with the current state
|
||||||
|
if self.critical:
|
||||||
|
for rule in self.critical:
|
||||||
|
if not rule(state, player):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# At least one set of normal rules (if any) must return true to mark the item as reachable
|
||||||
|
if not self.rules:
|
||||||
|
return True
|
||||||
|
|
||||||
|
reachable: bool = True
|
||||||
|
|
||||||
|
for rulesets in self.rules:
|
||||||
|
reachable = all(rule(state, player) for rule in rulesets)
|
||||||
|
|
||||||
|
if reachable:
|
||||||
|
break
|
||||||
|
|
||||||
|
return reachable
|
||||||
|
|
||||||
|
def condense(self, player) -> Callable[[CollectionState], bool]:
|
||||||
|
return lambda state : self.check(state, player)
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressionMode:
|
||||||
|
name : str = "Generic Progression Mode"
|
||||||
|
progression : list[int] = None
|
||||||
|
order : list[int] = None
|
||||||
|
level_select_entrances : list[AE3EntranceMeta] = None
|
||||||
|
|
||||||
|
boss_indices : Sequence[int] = [ 3, 8, 12, 17, 21, 24, 26, 27 ]
|
||||||
|
small_starting_channels : Sequence[int] = [ 6, 9, 11, 13, 15, 18, 20, 22, 23 ]
|
||||||
|
|
||||||
|
def __init__(self, world : 'AE3World' = None):
|
||||||
|
self.progression = []
|
||||||
|
self.order = [ _ for _ in range(28) ]
|
||||||
|
self.level_select_entrances : list[AE3EntranceMeta] = [ *ENTRANCES_STAGE_SELECT ]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def shuffle(self, world : 'AE3World'):
|
||||||
|
new_order: list[int] = self.generate_new_order(world)
|
||||||
|
|
||||||
|
# Update with the new orders
|
||||||
|
self.order = [*new_order]
|
||||||
|
|
||||||
|
# Apply Channel Rules
|
||||||
|
self.reorder(-1, sorted(world.options.blacklist_channel.value))
|
||||||
|
self.reorder(-2, sorted(world.options.post_channel.value))
|
||||||
|
self.reorder(-3, sorted(world.options.push_channel.value))
|
||||||
|
|
||||||
|
self.regenerate_level_select_entrances()
|
||||||
|
|
||||||
|
def generate_new_order(self, world : 'AE3World') -> list[int]:
|
||||||
|
new_order : list[int] = [_ for _ in range(28)]
|
||||||
|
world.random.shuffle(new_order)
|
||||||
|
|
||||||
|
self.small_starting_channels = world.logic_preference.small_starting_channels.copy()
|
||||||
|
|
||||||
|
# Do not allow Bosses or problematic levels to be in the first few levels
|
||||||
|
if (len(set(new_order[:5]).intersection(self.small_starting_channels)) > 0 or
|
||||||
|
len(set(new_order[:3]).intersection([*self.boss_indices, *self.small_starting_channels])) > 0):
|
||||||
|
blacklists : list[int] = [*self.small_starting_channels, *self.boss_indices]
|
||||||
|
swap_indexes : list[int] = [ _ for _ in range(7, 26) if new_order[_] not in blacklists ]
|
||||||
|
for idx, level in enumerate(new_order):
|
||||||
|
if level not in blacklists:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if idx > 3 and len(blacklists) > len(self.small_starting_channels):
|
||||||
|
blacklists = [*self.small_starting_channels]
|
||||||
|
|
||||||
|
swap : int = -1
|
||||||
|
swap_idx : int = -1
|
||||||
|
while swap < 0 or swap in blacklists:
|
||||||
|
if not swap_indexes:
|
||||||
|
break
|
||||||
|
|
||||||
|
swap_idx = world.random.choice(swap_indexes)
|
||||||
|
swap = new_order[swap_idx]
|
||||||
|
|
||||||
|
new_order[idx], new_order[swap_idx] = new_order[swap_idx], new_order[idx]
|
||||||
|
|
||||||
|
if swap_idx in swap_indexes:
|
||||||
|
swap_indexes.remove(swap_idx)
|
||||||
|
|
||||||
|
if idx >= 5: break
|
||||||
|
# Re-insert Channels specified to be preserved in their vanilla indices
|
||||||
|
if world.options.preserve_channel:
|
||||||
|
preserve_indices : list[int] = [
|
||||||
|
LEVELS_BY_ORDER.index(channel) for channel in sorted(world.options.preserve_channel)
|
||||||
|
]
|
||||||
|
|
||||||
|
if preserve_indices:
|
||||||
|
new_order = [_ for _ in new_order if _ not in preserve_indices]
|
||||||
|
|
||||||
|
for index in preserve_indices:
|
||||||
|
new_order.insert(index, index)
|
||||||
|
|
||||||
|
# Apply the chosen Shuffle Mode
|
||||||
|
if world.options.shuffle_channel == 1:
|
||||||
|
new_boss_order: list[int] = [_ for _ in new_order if _ in self.boss_indices]
|
||||||
|
|
||||||
|
new_order = [_ for _ in new_order if _ not in self.boss_indices]
|
||||||
|
|
||||||
|
for index in range(len(self.boss_indices)):
|
||||||
|
new_order.insert(self.boss_indices[index], new_boss_order[index])
|
||||||
|
|
||||||
|
return new_order
|
||||||
|
|
||||||
|
def reorder(self, set_interest : int, channels : list[str]):
|
||||||
|
temp_progression = deepcopy(self.progression)
|
||||||
|
# In the presence of padding sets, remove them first
|
||||||
|
# Any ProgressionModes that requires the padding will handle putting it back themselves
|
||||||
|
if 0 in self.progression[1:-1]:
|
||||||
|
temp_progression = [
|
||||||
|
self.progression[0], *[_ for _ in self.progression[1:-1] if _ > 0], self.progression[-1]
|
||||||
|
]
|
||||||
|
|
||||||
|
if set_interest < 0:
|
||||||
|
set_interest = len(temp_progression) + set_interest
|
||||||
|
|
||||||
|
targets : list[int] = [
|
||||||
|
LEVELS_BY_ORDER.index(channel) for channel in channels
|
||||||
|
if channel in LEVELS_BY_ORDER
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
if not targets:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.progression = deepcopy(temp_progression)
|
||||||
|
|
||||||
|
additive = APHelper.additive.value in channels
|
||||||
|
|
||||||
|
# Group the Sets
|
||||||
|
group_set : list[list[int]] = []
|
||||||
|
count : int = 0
|
||||||
|
for i, channel_set in enumerate(self.progression):
|
||||||
|
offset : int = 0
|
||||||
|
if i == 0:
|
||||||
|
offset = 1
|
||||||
|
|
||||||
|
target : int = count + channel_set + offset
|
||||||
|
group_set.append([_ if _ not in targets else -1
|
||||||
|
for _ in self.order[count : target]])
|
||||||
|
count = target
|
||||||
|
|
||||||
|
if additive:
|
||||||
|
group_set[set_interest].extend(targets)
|
||||||
|
else:
|
||||||
|
group_set.insert(set_interest + 1, targets)
|
||||||
|
|
||||||
|
if set_interest <= 1:
|
||||||
|
set_interest += 1
|
||||||
|
|
||||||
|
# Create Temporary Values
|
||||||
|
temp_order : list[int] = [channel for sets in group_set[:set_interest]
|
||||||
|
for channel in sets if channel != -1]
|
||||||
|
temp_progression : list[int] = [len(_) for _ in group_set[:set_interest]]
|
||||||
|
temp_set : list[list[int]] = []
|
||||||
|
|
||||||
|
# Regenerate Group Set with new order for all the interest set and all sets before it
|
||||||
|
if temp_order:
|
||||||
|
count : int = 0
|
||||||
|
for i, channel_set in enumerate(temp_progression):
|
||||||
|
target : int = count + channel_set
|
||||||
|
temp_set.append([_ for _ in temp_order[count : target]])
|
||||||
|
count = target
|
||||||
|
|
||||||
|
temp_set.extend(group_set[set_interest:])
|
||||||
|
group_set = deepcopy(temp_set)
|
||||||
|
|
||||||
|
# Clean Up
|
||||||
|
for i, sets in enumerate(group_set):
|
||||||
|
if -1 in sets:
|
||||||
|
group_set[i] = [_ for _ in sets if _ != -1]
|
||||||
|
|
||||||
|
new_order : list[int] = [channel for sets in group_set for channel in sets]
|
||||||
|
new_progression : list[int] = []
|
||||||
|
for i, sets in enumerate(group_set):
|
||||||
|
amount : int = len(sets)
|
||||||
|
|
||||||
|
if amount == 0 and i < len(group_set) - 1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not new_progression:
|
||||||
|
amount -= 1
|
||||||
|
|
||||||
|
new_progression.append(amount)
|
||||||
|
|
||||||
|
self.order = deepcopy(new_order)
|
||||||
|
self.progression = deepcopy(new_progression)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pgc_entrance_names(self) -> set[str]:
|
||||||
|
post_game_set_idx = len(self.progression) - 2
|
||||||
|
if post_game_set_idx <= 0 or self.progression[post_game_set_idx] <= 0:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
total_before = sum(self.progression[:post_game_set_idx]) + 1
|
||||||
|
return {
|
||||||
|
self.level_select_entrances[total_before + ch].name
|
||||||
|
for ch in range(self.progression[post_game_set_idx])
|
||||||
|
if total_before + ch < len(self.level_select_entrances)
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate_rules(self, world : 'AE3World') -> dict[str, Rulesets]:
|
||||||
|
channel_rules : dict[str, Rulesets] = {}
|
||||||
|
|
||||||
|
for i, channel_set in enumerate(self.progression):
|
||||||
|
# The first set of levels should NOT have any access rules
|
||||||
|
# Filler sets should also be ignored
|
||||||
|
if i == 0 or channel_set == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get total channels up until this set's point (not counting levels in current set)
|
||||||
|
total_from_current : int = sum(self.progression[:i]) + 1
|
||||||
|
required_keys : int = i
|
||||||
|
|
||||||
|
# Get current channel index to be processed for access rules
|
||||||
|
# by adding total from current and the range of the current set
|
||||||
|
for channel in range(channel_set):
|
||||||
|
if i == len(self.progression) - 1:
|
||||||
|
break
|
||||||
|
|
||||||
|
channel_idx : int = total_from_current + channel
|
||||||
|
access_rule : Rulesets = Rulesets(has_keys(required_keys))
|
||||||
|
|
||||||
|
if i == len(self.progression) - 2:
|
||||||
|
access_rule = Rulesets(world.post_game_condition.enact(required_keys - 1,
|
||||||
|
world.options.monkeysanity_break_rooms.value))
|
||||||
|
|
||||||
|
channel_rules.update({self.level_select_entrances[channel_idx].name : access_rule})
|
||||||
|
|
||||||
|
return channel_rules
|
||||||
|
|
||||||
|
def generate_keys(self, world : 'AE3World') -> list[Item]:
|
||||||
|
# The first set of levels and blacklisted set of levels will not cost keys.
|
||||||
|
# Keys required by post game is handled by its corresponding option
|
||||||
|
amount: int = len(self.progression) - 3 + world.options.post_game_condition_keys + world.options.extra_keys
|
||||||
|
|
||||||
|
return Channel_Key.to_items(world.player, amount)
|
||||||
|
|
||||||
|
def regenerate_level_select_entrances(self):
|
||||||
|
base_destination_order: list[str] = [entrance.destination for entrance in ENTRANCES_STAGE_SELECT]
|
||||||
|
new_entrances : list[AE3EntranceMeta] = []
|
||||||
|
|
||||||
|
for slot, channel in enumerate(self.order):
|
||||||
|
entrance: AE3EntranceMeta = AE3EntranceMeta(ENTRANCES_CHANNELS[slot], Stage.travel_station_a.value,
|
||||||
|
base_destination_order[channel])
|
||||||
|
new_entrances.append(entrance)
|
||||||
|
|
||||||
|
self.level_select_entrances = [*new_entrances]
|
||||||
|
|
||||||
|
def set_progression(self, progression : list[int] = None):
|
||||||
|
if progression is None or not progression:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.progression = progression
|
||||||
|
|
||||||
|
def set_order(self, order : list[int] = None):
|
||||||
|
if order is None or not order:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.order = order
|
||||||
|
|
||||||
|
def get_progress(self, keys : int, post_game_status : bool = False):
|
||||||
|
# Offset key count if Channel Keys are not part of Post Game Condition
|
||||||
|
if post_game_status and keys <= len(self.progression) - 2:
|
||||||
|
keys += 1
|
||||||
|
|
||||||
|
# Do not include Blacklist set when checking for progress
|
||||||
|
# Only include Post Game set when checking Progress if Post Game Conditions have been met
|
||||||
|
limit : int = -2 if not post_game_status else -1
|
||||||
|
unlocked : int = sum(self.progression[:limit][:keys + 1])
|
||||||
|
|
||||||
|
return unlocked
|
||||||
|
|
||||||
|
|
||||||
|
class Singles(ProgressionMode):
|
||||||
|
def __init__(self, world : 'AE3World' = None):
|
||||||
|
super().__init__(world)
|
||||||
|
|
||||||
|
self.name = "Singles"
|
||||||
|
self.progression = [0, *[1 for _ in range(1, 28)], 0]
|
||||||
|
|
||||||
|
class Group(ProgressionMode):
|
||||||
|
def __init__(self, world : 'AE3World' = None):
|
||||||
|
super().__init__(world)
|
||||||
|
|
||||||
|
self.name = "Group"
|
||||||
|
self.progression = [2, 1, 4, 1, 3, 1, 4, 1, 3, 1, 2, 1, 1, 1, 1, 0] # 15 Sets
|
||||||
|
|
||||||
|
|
||||||
|
def shuffle(self, world : 'AE3World'):
|
||||||
|
new_order : list[int] = self.generate_new_order(world)
|
||||||
|
|
||||||
|
## Pre-emptively remove Blacklisted Channels
|
||||||
|
blacklist : list[int] = [LEVELS_BY_ORDER.index(channel) for channel in sorted(world.options.blacklist_channel)
|
||||||
|
if channel in LEVELS_BY_ORDER]
|
||||||
|
new_order : list[int] = [_ for _ in new_order if _ not in blacklist]
|
||||||
|
|
||||||
|
# Track channel being processed to create the new progression.
|
||||||
|
new_progression : list[int] = [-1]
|
||||||
|
is_last_index_boss : bool = False
|
||||||
|
sets : int = 0
|
||||||
|
|
||||||
|
for slot, level in enumerate(new_order):
|
||||||
|
has_incremented : bool = False
|
||||||
|
|
||||||
|
# Split the level group before and after boss
|
||||||
|
if level in self.boss_indices:
|
||||||
|
is_last_index_boss = True
|
||||||
|
|
||||||
|
# If the current set has no levels counted yet, increment it first before incrementing the set number
|
||||||
|
if (not sets and new_progression[sets] < 0) or (sets and new_progression[sets] < 1):
|
||||||
|
new_progression[sets] += 1
|
||||||
|
has_incremented = True
|
||||||
|
|
||||||
|
if not new_progression[sets] < 0:
|
||||||
|
sets += 1
|
||||||
|
|
||||||
|
if len(new_progression) - sets <= 0:
|
||||||
|
new_progression.insert(sets, 0)
|
||||||
|
|
||||||
|
elif is_last_index_boss:
|
||||||
|
# Do not increment set when coming from the first set that only has a boss level
|
||||||
|
|
||||||
|
if sets == 1 and new_progression[1] > 0:
|
||||||
|
sets += 1
|
||||||
|
elif sets > 1 and new_progression[sets] > 0:
|
||||||
|
sets += 1
|
||||||
|
|
||||||
|
is_last_index_boss = False
|
||||||
|
|
||||||
|
if len(new_progression) - sets <= 0:
|
||||||
|
new_progression.insert(sets, 0)
|
||||||
|
|
||||||
|
if not has_incremented:
|
||||||
|
new_progression[sets] += 1
|
||||||
|
|
||||||
|
# Add Blacklisted Channels at end
|
||||||
|
new_progression.append(len(blacklist))
|
||||||
|
new_order.extend(blacklist)
|
||||||
|
|
||||||
|
# Update with the new orders
|
||||||
|
self.progression = [*new_progression]
|
||||||
|
self.order = [*new_order]
|
||||||
|
|
||||||
|
# Apply Channel Rules
|
||||||
|
self.reorder(-2, sorted(world.options.post_channel.value))
|
||||||
|
self.reorder(-3, sorted(world.options.push_channel.value))
|
||||||
|
|
||||||
|
# Update new Channel Destinations
|
||||||
|
self.regenerate_level_select_entrances()
|
||||||
|
|
||||||
|
|
||||||
|
class World(ProgressionMode):
|
||||||
|
def __init__(self, world : 'AE3World' = None):
|
||||||
|
super().__init__(world)
|
||||||
|
|
||||||
|
self.name = "World"
|
||||||
|
self.progression = [ 3, 5, 4, 5, 4, 3, 1, 1, 1, 0 ]
|
||||||
|
|
||||||
|
def shuffle(self, world : 'AE3World'):
|
||||||
|
new_order : list[int] = self.generate_new_order(world)
|
||||||
|
|
||||||
|
# Pre-emptively remove Blacklisted Channels
|
||||||
|
blacklist : list[int] = [LEVELS_BY_ORDER.index(channel) for channel in sorted(world.options.blacklist_channel)
|
||||||
|
if channel in LEVELS_BY_ORDER]
|
||||||
|
if blacklist:
|
||||||
|
new_order = [_ for _ in new_order if _ not in blacklist]
|
||||||
|
|
||||||
|
# Track channel being processed to create the new progression.
|
||||||
|
new_progression : list[int] = [-1]
|
||||||
|
sets : int = 0
|
||||||
|
for slot, level in enumerate(new_order):
|
||||||
|
new_progression[sets] += 1
|
||||||
|
|
||||||
|
# Split the level group only after the level boss
|
||||||
|
if level in self.boss_indices:
|
||||||
|
sets += 1
|
||||||
|
|
||||||
|
if len(new_progression) - sets <= 0 and level != new_order[-1]:
|
||||||
|
new_progression.append(0)
|
||||||
|
|
||||||
|
# Add Blacklisted Channels at end
|
||||||
|
new_progression.append(len(blacklist))
|
||||||
|
new_order.extend(blacklist)
|
||||||
|
|
||||||
|
# Update with the new orders
|
||||||
|
self.progression = [*new_progression]
|
||||||
|
self.order = [*new_order]
|
||||||
|
|
||||||
|
# Apply Channel Rules
|
||||||
|
self.reorder(-2, sorted(world.options.post_channel.value))
|
||||||
|
self.reorder(-3, sorted(world.options.push_channel.value))
|
||||||
|
|
||||||
|
# Update new Channel Destinations
|
||||||
|
self.regenerate_level_select_entrances()
|
||||||
|
|
||||||
|
class Quadruples(ProgressionMode):
|
||||||
|
def __init__(self, world : 'AE3World' = None):
|
||||||
|
super().__init__(world)
|
||||||
|
|
||||||
|
self.name = "Quadruples"
|
||||||
|
self.progression = [3, *[4 for _ in range(6)], 0]
|
||||||
|
|
||||||
|
class Open(ProgressionMode):
|
||||||
|
required_keys : int = 0
|
||||||
|
|
||||||
|
def __init__(self, world : 'AE3World' = None):
|
||||||
|
super().__init__(world)
|
||||||
|
|
||||||
|
self.name = "Open"
|
||||||
|
self.progression = [25, 1, 1, 0]
|
||||||
|
|
||||||
|
# Insert filler slots to simulate r
|
||||||
|
if world is not None:
|
||||||
|
self.required_keys = world.options.open_progression_keys.value - 1
|
||||||
|
required_keys : list[int] = [0 for _ in range(self.required_keys)]
|
||||||
|
self.progression[1:1] = required_keys
|
||||||
|
|
||||||
|
def reorder(self, set_interest : int, channels : list[str]):
|
||||||
|
super().reorder(set_interest, channels)
|
||||||
|
if len(self.progression) > 1 and self.progression[1] != 0 and self.required_keys:
|
||||||
|
self.progression[1:1] = [0 for _ in range(self.required_keys)]
|
||||||
|
|
||||||
|
class Randomize(ProgressionMode):
|
||||||
|
def __init__(self, world : 'AE3World' = None):
|
||||||
|
super().__init__(world)
|
||||||
|
|
||||||
|
self.name = "Randomize"
|
||||||
|
self.progression = []
|
||||||
|
|
||||||
|
if world is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
set_minimum : int = 1
|
||||||
|
set_maximum : int = 16
|
||||||
|
sets : int = world.options.randomize_progression_set_count.value
|
||||||
|
if world.options.randomize_progression_channel_range.value:
|
||||||
|
set_minimum = world.options.randomize_progression_channel_range.value[0]
|
||||||
|
set_maximum = world.options.randomize_progression_channel_range.value[1]
|
||||||
|
|
||||||
|
if sets:
|
||||||
|
set_maximum = int(28 / sets)
|
||||||
|
|
||||||
|
minimum : int = set_minimum
|
||||||
|
maximum : int = set_maximum
|
||||||
|
|
||||||
|
attempts : int = 0
|
||||||
|
while sum(self.progression) < 27:
|
||||||
|
# Enforce Set Count
|
||||||
|
if sets and len(self.progression) + 1 == sets:
|
||||||
|
self.progression.append(27 - sum(self.progression))
|
||||||
|
break
|
||||||
|
|
||||||
|
sets_amount : int = (world.random.randint(minimum, maximum))
|
||||||
|
|
||||||
|
# Adjust sets amount if it will lead to progression tracking more channels than exists
|
||||||
|
if sum(self.progression) + sets_amount > 27:
|
||||||
|
sets_amount = 28 - sum(self.progression)
|
||||||
|
|
||||||
|
# Subtract by an offset of 1 if this is the first set
|
||||||
|
if not self.progression:
|
||||||
|
sets_amount -= 1
|
||||||
|
|
||||||
|
self.progression.append(sets_amount)
|
||||||
|
|
||||||
|
# Recalibrate Maximum and Minimum as necessary
|
||||||
|
if maximum > 27 - sum(self.progression):
|
||||||
|
maximum = 27 - sum(self.progression)
|
||||||
|
|
||||||
|
if minimum > maximum:
|
||||||
|
minimum = maximum
|
||||||
|
|
||||||
|
# Reset and try again when channel count exceeds expected when minimum gets pushed to 0
|
||||||
|
if minimum <= 0 and sum(self.progression) != 27:
|
||||||
|
minimum = set_minimum
|
||||||
|
maximum = set_maximum
|
||||||
|
self.progression.clear()
|
||||||
|
|
||||||
|
attempts += 1
|
||||||
|
elif sum(self.progression) == 27:
|
||||||
|
break
|
||||||
|
|
||||||
|
# If there are too many attempts, fall back to Quadruples
|
||||||
|
if attempts > 5:
|
||||||
|
self.progression = [3, *[4 for _ in range(6)], 0]
|
||||||
|
warnings.warn("AE3 > Randomize: Generation failed to generate a valid Channel Set. "
|
||||||
|
"Falling back to Quadruples Progression...")
|
||||||
|
break
|
||||||
|
|
||||||
|
if sum(self.progression) != 27:
|
||||||
|
raise AssertionError("AE3 > Randomize: Generation failed to generate a valid Channel Set. ")
|
||||||
|
|
||||||
|
self.progression.append(0)
|
||||||
|
|
||||||
|
|
||||||
|
ProgressionModeOptions : Sequence[Callable] = [
|
||||||
|
Singles, Group, World, Quadruples, Open, Randomize
|
||||||
|
]
|
||||||
2136
worlds/apeescape3/data/Rules.py
Normal file
2136
worlds/apeescape3/data/Rules.py
Normal file
File diff suppressed because it is too large
Load Diff
2015
worlds/apeescape3/data/Stages.py
Normal file
2015
worlds/apeescape3/data/Stages.py
Normal file
File diff suppressed because it is too large
Load Diff
2797
worlds/apeescape3/data/Strings.py
Normal file
2797
worlds/apeescape3/data/Strings.py
Normal file
File diff suppressed because it is too large
Load Diff
0
worlds/apeescape3/data/__init__.py
Normal file
0
worlds/apeescape3/data/__init__.py
Normal file
1
worlds/apeescape3/docs/setup_en.md
Normal file
1
worlds/apeescape3/docs/setup_en.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
stop making me do this
|
||||||
21
worlds/apeescape3/interface/LICENSE.md
Normal file
21
worlds/apeescape3/interface/LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Will Becker
|
||||||
|
|
||||||
|
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.
|
||||||
0
worlds/apeescape3/interface/__init__.py
Normal file
0
worlds/apeescape3/interface/__init__.py
Normal file
305
worlds/apeescape3/interface/pine.py
Normal file
305
worlds/apeescape3/interface/pine.py
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
"""
|
||||||
|
The PINE API.
|
||||||
|
This is the client side implementation of the PINE protocol.
|
||||||
|
It allows for a three-way communication between the emulated game, the emulator and an external
|
||||||
|
tool, using the external tool as a relay for all communication. It is a socket based IPC that
|
||||||
|
is _very_ fast.
|
||||||
|
|
||||||
|
If you want to draw comparisons you can think of this as an equivalent of the BizHawk LUA API,
|
||||||
|
although with the logic out of the core and in an external tool. While BizHawk would run a lua
|
||||||
|
script at each frame in the core of the emulator we opt instead to keep the entire logic out of
|
||||||
|
the emulator to make it more easily extensible, more portable, require less code and be more
|
||||||
|
performant.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
from enum import IntEnum
|
||||||
|
from platform import system
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
|
class Pine:
|
||||||
|
""" Exposes PS2 memory within a running instance of the PCSX2 emulator using the Pine IPC Protocol. """
|
||||||
|
|
||||||
|
""" Maximum memory used by an IPC message request. Equivalent to 50,000 Write64 requests. """
|
||||||
|
MAX_IPC_SIZE: int = 650000
|
||||||
|
|
||||||
|
""" Maximum memory used by an IPC message reply. Equivalent to 50,000 Read64 replies. """
|
||||||
|
MAX_IPC_RETURN_SIZE: int = 450000
|
||||||
|
|
||||||
|
""" Maximum number of commands sent in a batch message. """
|
||||||
|
MAX_BATCH_REPLY_COUNT: int = 50000
|
||||||
|
|
||||||
|
class IPCResult(IntEnum):
|
||||||
|
""" IPC result codes. A list of possible result codes the IPC can send back. Each one of them is what we call an
|
||||||
|
"opcode" or "tag" and is the first byte sent by the IPC to differentiate between results.
|
||||||
|
"""
|
||||||
|
IPC_OK = 0, # IPC command successfully completed.
|
||||||
|
IPC_FAIL = 0xFF # IPC command failed to complete.
|
||||||
|
|
||||||
|
class IPCCommand(IntEnum):
|
||||||
|
READ8 = 0,
|
||||||
|
READ16 = 1,
|
||||||
|
READ32 = 2,
|
||||||
|
READ64 = 3,
|
||||||
|
WRITE8 = 4,
|
||||||
|
WRITE16 = 5,
|
||||||
|
WRITE32 = 6,
|
||||||
|
WRITE64 = 7,
|
||||||
|
VERSION = 8,
|
||||||
|
SAVE_STATE = 9,
|
||||||
|
LOAD_STATE = 0xA,
|
||||||
|
TITLE = 0xB,
|
||||||
|
ID = 0xC,
|
||||||
|
UUID = 0xD,
|
||||||
|
GAME_VERSION = 0xE,
|
||||||
|
STATUS = 0xF,
|
||||||
|
UNIMPLEMENTED = 0xFF,
|
||||||
|
|
||||||
|
class DataSize(IntEnum):
|
||||||
|
INT8 = 1,
|
||||||
|
INT16 = 2,
|
||||||
|
INT32 = 4,
|
||||||
|
INT64 = 8,
|
||||||
|
|
||||||
|
def __init__(self, slot: int = 28011, linux_platform: str = "auto"):
|
||||||
|
if not 0 < slot <= 65536:
|
||||||
|
raise ValueError("Provided slot number is outside valid range")
|
||||||
|
self._slot: int = slot
|
||||||
|
self._sock: socket.socket = socket.socket()
|
||||||
|
self._sock_state: bool = False
|
||||||
|
|
||||||
|
self.linux_platform = linux_platform
|
||||||
|
self.active_platform = None
|
||||||
|
self.active_slot = None
|
||||||
|
|
||||||
|
# self._init_socket()
|
||||||
|
|
||||||
|
def _init_socket(self) -> None:
|
||||||
|
active_platform = "Unknown"
|
||||||
|
if system() == "Windows":
|
||||||
|
socket_family = socket.AF_INET
|
||||||
|
socket_name = ("127.0.0.1", self._slot)
|
||||||
|
|
||||||
|
active_platform = "Windows"
|
||||||
|
elif system() == "Linux":
|
||||||
|
socket_family = socket.AF_UNIX
|
||||||
|
|
||||||
|
socket_name = os.environ.get("XDG_RUNTIME_DIR", "/tmp")
|
||||||
|
|
||||||
|
# Default/AppImage Socket Path
|
||||||
|
if os.access(socket_name + "/pcsx2.sock", os.R_OK) and self.linux_platform != "flatpak":
|
||||||
|
socket_name += "/pcsx2.sock"
|
||||||
|
|
||||||
|
active_platform = "Linux Standard"
|
||||||
|
# Flatpak Socket Path
|
||||||
|
elif self.linux_platform != "standard":
|
||||||
|
socket_name += "/.flatpak/net.pcsx2.PCSX2/xdg-run"
|
||||||
|
socket_name += "/pcsx2.sock"
|
||||||
|
|
||||||
|
active_platform = "Flatpak"
|
||||||
|
|
||||||
|
elif system() == "Darwin":
|
||||||
|
socket_family = socket.AF_UNIX
|
||||||
|
socket_name = os.environ.get("TMPDIR", "/tmp")
|
||||||
|
socket_name += "/pcsx2.sock"
|
||||||
|
|
||||||
|
active_platform = "Darwin"
|
||||||
|
else:
|
||||||
|
socket_family = socket.AF_UNIX
|
||||||
|
socket_name = "/tmp/pcsx2.sock"
|
||||||
|
|
||||||
|
if self.active_platform != "Windows" and self._slot != 28011:
|
||||||
|
socket_name += f".{self._slot}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._sock = socket.socket(socket_family, socket.SOCK_STREAM)
|
||||||
|
self._sock.settimeout(5.0)
|
||||||
|
self._sock.connect(socket_name)
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.active_slot = None
|
||||||
|
self.active_platform = None
|
||||||
|
return
|
||||||
|
except socket.error:
|
||||||
|
self._sock.close()
|
||||||
|
self._sock_state = False
|
||||||
|
|
||||||
|
self.active_slot = None
|
||||||
|
self.active_platform = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self._sock_state = True
|
||||||
|
|
||||||
|
self.active_slot = self._slot
|
||||||
|
self.active_platform = active_platform
|
||||||
|
|
||||||
|
def connect(self,) -> None:
|
||||||
|
if not self._sock_state:
|
||||||
|
self._init_socket()
|
||||||
|
|
||||||
|
def disconnect(self) -> None:
|
||||||
|
if self._sock_state:
|
||||||
|
self._sock.close()
|
||||||
|
|
||||||
|
def set_slot(self, slot: int = 28011) -> None:
|
||||||
|
self._slot = slot
|
||||||
|
|
||||||
|
def set_linux_platform(self, linux_platform: str = "auto") -> None:
|
||||||
|
self.linux_platform = linux_platform
|
||||||
|
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
return self._sock_state
|
||||||
|
|
||||||
|
def read_int8(self, address: int) -> int:
|
||||||
|
request = Pine._create_request(Pine.IPCCommand.READ8, address, 9)
|
||||||
|
return Pine.from_bytes(self._send_request(request)[-1:])
|
||||||
|
|
||||||
|
def read_int16(self, address) -> int:
|
||||||
|
request = Pine._create_request(Pine.IPCCommand.READ16, address, 9)
|
||||||
|
return Pine.from_bytes(self._send_request(request)[-2:])
|
||||||
|
|
||||||
|
def read_int32(self, address) -> int:
|
||||||
|
request = Pine._create_request(Pine.IPCCommand.READ32, address, 9)
|
||||||
|
return Pine.from_bytes(self._send_request(request)[-4:])
|
||||||
|
|
||||||
|
def read_int64(self, address) -> int:
|
||||||
|
request = Pine._create_request(Pine.IPCCommand.READ64, address, 9)
|
||||||
|
return Pine.from_bytes(self._send_request(request)[-8:])
|
||||||
|
|
||||||
|
def read_float(self, address) -> float:
|
||||||
|
request = Pine._create_request(Pine.IPCCommand.READ32, address, 9)
|
||||||
|
return struct.unpack("<f", self._send_request(request)[-4:])[0]
|
||||||
|
|
||||||
|
def read_bytes(self, address: int, length: int) -> bytes:
|
||||||
|
"""Careful! This can be quite slow for large reads"""
|
||||||
|
data = b''
|
||||||
|
while len(data) < length:
|
||||||
|
if length - len(data) >= 8:
|
||||||
|
data += self._send_request(Pine._create_request(Pine.IPCCommand.READ64, address + len(data), 9))[-8:]
|
||||||
|
elif length - len(data) >= 4:
|
||||||
|
data += self._send_request(Pine._create_request(Pine.IPCCommand.READ32, address + len(data), 9))[-4:]
|
||||||
|
elif length - len(data) >= 2:
|
||||||
|
data += self._send_request(Pine._create_request(Pine.IPCCommand.READ16, address + len(data), 9))[-2:]
|
||||||
|
elif length - len(data) >= 1:
|
||||||
|
data += self._send_request(Pine._create_request(Pine.IPCCommand.READ8, address + len(data), 9))[-1:]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def write_int8(self, address: int, value: int) -> None:
|
||||||
|
request = Pine._create_request(Pine.IPCCommand.WRITE8, address, 9 + Pine.DataSize.INT8)
|
||||||
|
request += value.to_bytes(length=1, byteorder="little")
|
||||||
|
self._send_request(request)
|
||||||
|
|
||||||
|
def write_int16(self, address: int, value: int) -> None:
|
||||||
|
request = Pine._create_request(Pine.IPCCommand.WRITE16, address, 9 + Pine.DataSize.INT16)
|
||||||
|
request += value.to_bytes(length=2, byteorder="little")
|
||||||
|
self._send_request(request)
|
||||||
|
|
||||||
|
def write_int32(self, address: int, value: int) -> None:
|
||||||
|
request = Pine._create_request(Pine.IPCCommand.WRITE32, address, 9 + Pine.DataSize.INT32)
|
||||||
|
request += value.to_bytes(length=4, byteorder="little")
|
||||||
|
self._send_request(request)
|
||||||
|
|
||||||
|
def write_int64(self, address: int, value: int) -> None:
|
||||||
|
request = Pine._create_request(Pine.IPCCommand.WRITE64, address, 9 + Pine.DataSize.INT64)
|
||||||
|
request += value.to_bytes(length=8, byteorder="little")
|
||||||
|
self._send_request(request)
|
||||||
|
|
||||||
|
def write_float(self, address: int, value: float) -> None:
|
||||||
|
request = Pine._create_request(Pine.IPCCommand.WRITE32, address, 9 + Pine.DataSize.INT32)
|
||||||
|
request += struct.pack("<f", value)
|
||||||
|
self._send_request(request)
|
||||||
|
|
||||||
|
def write_bytes(self, address: int, data: bytes) -> None:
|
||||||
|
"""Careful! This can be quite slow for large writes"""
|
||||||
|
bytes_written = 0
|
||||||
|
while bytes_written < len(data):
|
||||||
|
if len(data) - bytes_written >= 8:
|
||||||
|
request = self._create_request(Pine.IPCCommand.WRITE64, address + bytes_written, 9 + Pine.DataSize.INT64)
|
||||||
|
request += data[bytes_written:bytes_written + 8]
|
||||||
|
self._send_request(request)
|
||||||
|
bytes_written += 8
|
||||||
|
elif len(data) - bytes_written >= 4:
|
||||||
|
request = self._create_request(Pine.IPCCommand.WRITE32, address + bytes_written, 9 + Pine.DataSize.INT32)
|
||||||
|
request += data[bytes_written:bytes_written + 4]
|
||||||
|
self._send_request(request)
|
||||||
|
bytes_written += 4
|
||||||
|
elif len(data) - bytes_written >= 2:
|
||||||
|
request = self._create_request(Pine.IPCCommand.WRITE16, address + bytes_written, 9 + Pine.DataSize.INT16)
|
||||||
|
request += data[bytes_written:bytes_written + 2]
|
||||||
|
self._send_request(request)
|
||||||
|
bytes_written += 2
|
||||||
|
elif len(data) - bytes_written >= 1:
|
||||||
|
request = self._create_request(Pine.IPCCommand.WRITE8, address + bytes_written, 9 + Pine.DataSize.INT8)
|
||||||
|
request += data[bytes_written:bytes_written + 1]
|
||||||
|
self._send_request(request)
|
||||||
|
bytes_written += 1
|
||||||
|
|
||||||
|
def get_game_id(self) -> str:
|
||||||
|
request = Pine.to_bytes(5, 4) + Pine.to_bytes(Pine.IPCCommand.ID, 1)
|
||||||
|
response = self._send_request(request)
|
||||||
|
return response[9:-1].decode("ascii")
|
||||||
|
|
||||||
|
def save_state(self, slot: int) -> None:
|
||||||
|
request = Pine.to_bytes(6, 4) + Pine.to_bytes(Pine.IPCCommand.SAVE_STATE, 1)
|
||||||
|
request += slot.to_bytes(length=1, byteorder="little")
|
||||||
|
self._send_request(request)
|
||||||
|
|
||||||
|
def load_state(self, slot: int) -> None:
|
||||||
|
request = Pine.to_bytes(6, 4) + Pine.to_bytes(Pine.IPCCommand.LOAD_STATE, 1)
|
||||||
|
request += slot.to_bytes(length=1, byteorder="little")
|
||||||
|
self._send_request(request)
|
||||||
|
|
||||||
|
def _send_request(self, request: bytes) -> bytes:
|
||||||
|
if not self._sock_state:
|
||||||
|
self._init_socket()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._sock.sendall(request)
|
||||||
|
except socket.error:
|
||||||
|
self._sock.close()
|
||||||
|
self._sock_state = False
|
||||||
|
raise ConnectionError("Lost connection to PCSX2.")
|
||||||
|
|
||||||
|
end_length = 4
|
||||||
|
result: bytes = b''
|
||||||
|
while len(result) < end_length:
|
||||||
|
try:
|
||||||
|
response = self._sock.recv(4096)
|
||||||
|
except TimeoutError:
|
||||||
|
raise TimeoutError("Response timed out. "
|
||||||
|
"This might be caused by having two PINE connections open on the same slot")
|
||||||
|
|
||||||
|
if len(response) <= 0:
|
||||||
|
result = b''
|
||||||
|
break
|
||||||
|
|
||||||
|
result += response
|
||||||
|
|
||||||
|
if end_length == 4 and len(response) >= 4:
|
||||||
|
end_length = Pine.from_bytes(result[0:4])
|
||||||
|
if end_length > Pine.MAX_IPC_SIZE:
|
||||||
|
result = b''
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(result) == 0:
|
||||||
|
raise ConnectionError("Invalid response from PCSX2.")
|
||||||
|
if result[4] == Pine.IPCResult.IPC_FAIL:
|
||||||
|
raise ConnectionError("Failure indicated in PCSX2 response.")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_request(command: IPCCommand, address: int, size: int = 0) -> bytes:
|
||||||
|
ipc = Pine.to_bytes(size, 4)
|
||||||
|
ipc += Pine.to_bytes(command, 1)
|
||||||
|
ipc += Pine.to_bytes(address, 4)
|
||||||
|
return ipc
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_bytes(value: int, size: int) -> bytes:
|
||||||
|
return value.to_bytes(length=size, byteorder="little")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_bytes(arr: bytes) -> int:
|
||||||
|
return int.from_bytes(arr, byteorder="little")
|
||||||
1
worlds/armoredcore/.gitignore
vendored
Normal file
1
worlds/armoredcore/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__pycache__/
|
||||||
15
worlds/armoredcore/README.md
Normal file
15
worlds/armoredcore/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# [Archipelago](https://archipelago.gg) Armored Core (PSX)
|
||||||
|
|
||||||
|
Armored Core is a 1997 third-person shooter mecha PSX game developed by FromSoftware.
|
||||||
|
|
||||||
|
APworld being developed by myself and ArmoredKori. Available to download in the releases section!
|
||||||
|
|
||||||
|
## [Setup Guide](docs/setup_en.md)
|
||||||
|
|
||||||
|
## [Randomizer Info](docs/en_Armored%20Core.md)
|
||||||
|
|
||||||
|
Have suggestions or comments?
|
||||||
|
You can interact with this repository here or reach the devs on the Archipelago discord.
|
||||||
|
|
||||||
|
Special Thanks:
|
||||||
|
Vartazian for ripping / doing data entry of Parts data
|
||||||
231
worlds/armoredcore/__init__.py
Normal file
231
worlds/armoredcore/__init__.py
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import typing
|
||||||
|
import warnings
|
||||||
|
import random
|
||||||
|
|
||||||
|
from worlds.AutoWorld import World, WebWorld
|
||||||
|
from BaseClasses import CollectionState, Region, Tutorial, LocationProgressType
|
||||||
|
from worlds.generic.Rules import set_rule
|
||||||
|
|
||||||
|
from .client import ACClient
|
||||||
|
from .utils import Constants
|
||||||
|
from .mission import Mission, all_missions, STARTING_MISSION, DESTROY_FLOATING_MINES, id_to_mission as mission_id_to_mission, missions_that_award_credits
|
||||||
|
from .mail import Mail, all_mail
|
||||||
|
from .items import ACItem, create_item as fabricate_item, item_name_to_item_id, create_victory_event
|
||||||
|
from .locations import ACLocation, MissionLocation, get_location_name_for_mission, location_name_to_id as location_map, MailLocation, ShopLocation
|
||||||
|
from .options import ACOptions
|
||||||
|
from .parts import Part, all_parts, base_starting_parts, all_dummy_parts, all_parts_data_order
|
||||||
|
from .ac_randomizer import randomize_start_parts
|
||||||
|
|
||||||
|
class ACWeb(WebWorld):
|
||||||
|
theme = "dirt"
|
||||||
|
|
||||||
|
setup_en = Tutorial(
|
||||||
|
"Multiworld Setup Guide",
|
||||||
|
f"A guide to playing {Constants.GAME_NAME} with Archipelago.",
|
||||||
|
"English",
|
||||||
|
"setup_en.md",
|
||||||
|
"setup/en",
|
||||||
|
["Jumza"]
|
||||||
|
)
|
||||||
|
|
||||||
|
tutorials = [setup_en]
|
||||||
|
|
||||||
|
class ACWorld(World):
|
||||||
|
"""Armored Core is a 1997 third-person shooter mecha PSX game developed by FromSoftware."""
|
||||||
|
game: str = Constants.GAME_NAME
|
||||||
|
options_dataclass = ACOptions
|
||||||
|
options: ACOptions
|
||||||
|
required_client_version = (0, 5, 0)
|
||||||
|
web = ACWeb()
|
||||||
|
|
||||||
|
location_name_to_id = location_map
|
||||||
|
item_name_to_id = item_name_to_item_id
|
||||||
|
|
||||||
|
mission_unlock_order: typing.List[Mission]
|
||||||
|
shop_listing_unlock_order: typing.List[Part]
|
||||||
|
randomized_valid_parts_rewards: typing.List[Part] # Won't include Dummy or Starting Parts
|
||||||
|
missions_awarding_credits: typing.List[Mission]
|
||||||
|
starting_parts: typing.Tuple[Part, ...]
|
||||||
|
starting_parts_str: typing.List[str]
|
||||||
|
|
||||||
|
def get_available_missions(self, state: CollectionState) -> typing.List[Mission]:
|
||||||
|
available_missions: typing.List[Mission] = [STARTING_MISSION] # Dummy00 is always available
|
||||||
|
if self.options.goal == 0: # Missionsanity
|
||||||
|
for m in self.mission_unlock_order:
|
||||||
|
if m is not STARTING_MISSION:
|
||||||
|
if state.has(m.name, self.player):
|
||||||
|
available_missions.append(m)
|
||||||
|
else: # Progressive Missions
|
||||||
|
for m in self.mission_unlock_order:
|
||||||
|
if m is not STARTING_MISSION:
|
||||||
|
# Progression level - 1 because in this mode we always start with the first 5 missions
|
||||||
|
if state.count(Constants.PROGRESSIVE_MISSION_ITEM_NAME, self.player) >= (m.progression_level - 1):
|
||||||
|
available_missions.append(m)
|
||||||
|
return available_missions
|
||||||
|
""" Unused
|
||||||
|
def get_available_shop_listings(self, state: CollectionState) -> typing.List[Part]:
|
||||||
|
available_listings: typing.List[Part]
|
||||||
|
for count, mission in enumerate(self.mission_unlock_order): # mission unlock order vs all_missions?
|
||||||
|
start_index: int = count * self.options.shopsanity_listings_per_mission
|
||||||
|
end_index: int = (((count + 1) * self.options.shopsanity_listings_per_mission) if ((count + 1) * self.options.shopsanity_listings_per_mission) < len(self.shop_listing_unlock_order)
|
||||||
|
else len(self.shop_listing_unlock_order) - 1)
|
||||||
|
if state.has(mission.name, self.player):
|
||||||
|
for part in self.shop_listing_unlock_order[start_index : end_index]:
|
||||||
|
available_listings.append(part)
|
||||||
|
return available_listings
|
||||||
|
"""
|
||||||
|
def generate_early(self) -> None:
|
||||||
|
self.mission_unlock_order = list(all_missions)
|
||||||
|
if self.options.goal == 1: # Progressive Missions
|
||||||
|
self.mission_unlock_order.sort(key = lambda m: m.progression_level)
|
||||||
|
self.shop_listing_unlock_order = list(all_parts_data_order)
|
||||||
|
# random.shuffle(self.shop_listing_unlock_order) Don't randomize! Need to know what order this will be in
|
||||||
|
|
||||||
|
if self.options.rando_start_parts:
|
||||||
|
self.starting_parts = randomize_start_parts()
|
||||||
|
else:
|
||||||
|
self.starting_parts = base_starting_parts
|
||||||
|
|
||||||
|
self.starting_parts_str = [part.name for part in self.starting_parts if part in all_parts]
|
||||||
|
self.randomized_valid_parts_rewards = list((set(all_parts) - set(all_dummy_parts)) - set(self.starting_parts))
|
||||||
|
random.shuffle(self.randomized_valid_parts_rewards)
|
||||||
|
self.missions_awarding_credits = list(missions_that_award_credits)
|
||||||
|
|
||||||
|
def create_item(self, name: str) -> ACItem:
|
||||||
|
return fabricate_item(name, self.player)
|
||||||
|
|
||||||
|
def create_regions(self) -> None:
|
||||||
|
menu_region = Region("Menu", self.player, self.multiworld)
|
||||||
|
|
||||||
|
mission_list_region = Region("Mission List", self.player, self.multiworld)
|
||||||
|
|
||||||
|
# Define mission locations. Changes slightly based on goal
|
||||||
|
|
||||||
|
# Missionsanity Goal
|
||||||
|
if self.options.goal == 0:
|
||||||
|
for mission in self.mission_unlock_order:
|
||||||
|
mission_location: MissionLocation = MissionLocation(mission_list_region, self.player, mission)
|
||||||
|
set_rule(mission_location, (lambda state, m=mission_location:
|
||||||
|
m.mission in self.get_available_missions(state)))
|
||||||
|
mission_list_region.locations.append(mission_location)
|
||||||
|
else: # Progressive Missions Goal
|
||||||
|
for mission in self.mission_unlock_order:
|
||||||
|
if mission is not DESTROY_FLOATING_MINES:
|
||||||
|
mission_location: MissionLocation = MissionLocation(mission_list_region, self.player, mission)
|
||||||
|
set_rule(mission_location, (lambda state, m=mission_location:
|
||||||
|
m.mission in self.get_available_missions(state)))
|
||||||
|
mission_list_region.locations.append(mission_location)
|
||||||
|
|
||||||
|
self.multiworld.completion_condition[self.player] = lambda state: state.has(
|
||||||
|
Constants.VICTORY_ITEM_NAME, self.player
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define mail locations
|
||||||
|
for mail in all_mail:
|
||||||
|
if mail.mission_unlock_id != -1: # It unlocks by doing a specific mission
|
||||||
|
mail_location: MailLocation = MailLocation(mission_list_region, self.player, mail)
|
||||||
|
set_rule(mail_location, (lambda state, m=mail_location:
|
||||||
|
mission_id_to_mission[m.mail.mission_unlock_id] in self.get_available_missions(state)))
|
||||||
|
mission_list_region.locations.append(mail_location)
|
||||||
|
else: # It has a different unlock requirement. As of right now these can all be done Out Of Logic, but it leads to a better play pattern
|
||||||
|
# Missionsanity Goal
|
||||||
|
if self.options.goal == 0:
|
||||||
|
if mail.name == "New Parts Added (1)": # Unlocks after 10 missions are completed, so the rule is you must have at least 10 missions (should it be 9? Raven Test?)
|
||||||
|
mail_location: MailLocation = MailLocation(mission_list_region, self.player, mail)
|
||||||
|
set_rule(mail_location, lambda state: state.has_from_list([m.name for m in self.mission_unlock_order], self.player, 10))
|
||||||
|
mission_list_region.locations.append(mail_location)
|
||||||
|
elif mail.name == "New Parts Added (2)": # Unlocks after 20 missions are completed, so the rule is you must have at least 20 missions (should it be 19? Raven Test?)
|
||||||
|
mail_location: MailLocation = MailLocation(mission_list_region, self.player, mail)
|
||||||
|
set_rule(mail_location, lambda state: state.has_from_list([m.name for m in self.mission_unlock_order], self.player, 20))
|
||||||
|
mission_list_region.locations.append(mail_location)
|
||||||
|
else: #mail.name == "Human Plus": # Unlocks after 13 missions are completed, so the rule is you must have at least 13 missions (should it be 12? Raven Test?)
|
||||||
|
mail_location: MailLocation = MailLocation(mission_list_region, self.player, mail)
|
||||||
|
set_rule(mail_location, lambda state: state.has_from_list([m.name for m in self.mission_unlock_order], self.player, 13))
|
||||||
|
mission_list_region.locations.append(mail_location)
|
||||||
|
else: # Progressive Missions Goal
|
||||||
|
if mail.name == "New Parts Added (1)": # Unlocks after 10 missions are completed
|
||||||
|
mail_location: MailLocation = MailLocation(mission_list_region, self.player, mail)
|
||||||
|
set_rule(mail_location, lambda state: state.count(Constants.PROGRESSIVE_MISSION_ITEM_NAME, self.player) >= 2)
|
||||||
|
mission_list_region.locations.append(mail_location)
|
||||||
|
elif mail.name == "New Parts Added (2)": # Unlocks after 20 missions are completed
|
||||||
|
mail_location: MailLocation = MailLocation(mission_list_region, self.player, mail)
|
||||||
|
set_rule(mail_location, lambda state: state.count(Constants.PROGRESSIVE_MISSION_ITEM_NAME, self.player) >= 4)
|
||||||
|
mission_list_region.locations.append(mail_location)
|
||||||
|
else: #mail.name == "Human Plus": # Unlocks after 13 missions are completed
|
||||||
|
mail_location: MailLocation = MailLocation(mission_list_region, self.player, mail)
|
||||||
|
set_rule(mail_location, lambda state: state.count(Constants.PROGRESSIVE_MISSION_ITEM_NAME, self.player) >= 3)
|
||||||
|
mission_list_region.locations.append(mail_location)
|
||||||
|
|
||||||
|
# Define Shop Listings locations if shopsanity is on
|
||||||
|
if self.options.shopsanity:
|
||||||
|
for count, mission in enumerate(self.mission_unlock_order):
|
||||||
|
start_index: int = count * self.options.shopsanity_listings_per_mission
|
||||||
|
end_index: int = (((count + 1) * self.options.shopsanity_listings_per_mission) if ((count + 1) * self.options.shopsanity_listings_per_mission) < len(self.shop_listing_unlock_order)
|
||||||
|
else len(self.shop_listing_unlock_order))
|
||||||
|
for part in self.shop_listing_unlock_order[start_index : end_index]:
|
||||||
|
shop_location: ShopLocation = ShopLocation(mission_list_region, self.player, part)
|
||||||
|
# Shop rules are such that the player has the missions needed to unlock them AND at least one mission that awards credits available (which is always true in Progressive Missions Mode)
|
||||||
|
# Missionsanity Goal
|
||||||
|
if self.options.goal == 0:
|
||||||
|
set_rule(shop_location, lambda state, c = count: state.has_from_list([m.name for m in self.mission_unlock_order], self.player, c) and
|
||||||
|
state.has_from_list([m.name for m in self.missions_awarding_credits], self.player, 1))
|
||||||
|
else: # Progressive Missions Goal
|
||||||
|
set_rule(shop_location, lambda state, c = count: state.count(Constants.PROGRESSIVE_MISSION_ITEM_NAME, self.player) >= c // 5)
|
||||||
|
mission_list_region.locations.append(shop_location)
|
||||||
|
|
||||||
|
itempool: typing.List[ACItem] = []
|
||||||
|
if self.options.goal == 0: # Missionsanity
|
||||||
|
for mission in self.mission_unlock_order:
|
||||||
|
if mission is not STARTING_MISSION:
|
||||||
|
itempool.append(self.create_item(mission.name))
|
||||||
|
else: # Progressive Missions
|
||||||
|
for i in range(9): # Hardcoded number of Progressive Mission items currently
|
||||||
|
itempool.append(self.create_item(Constants.PROGRESSIVE_MISSION_ITEM_NAME))
|
||||||
|
|
||||||
|
# Generate filler
|
||||||
|
|
||||||
|
filler_slots: int = len(mission_list_region.locations) - len(itempool)
|
||||||
|
|
||||||
|
# Human+ generates first if the option is on
|
||||||
|
if self.options.include_humanplus:
|
||||||
|
humanplus_slots: int = 3 if filler_slots > 3 else filler_slots
|
||||||
|
itempool += [self.create_item(Constants.PROGRESSIVE_HUMANPLUS_ITEM_NAME) for h in range(humanplus_slots)][:humanplus_slots]
|
||||||
|
filler_slots = filler_slots - humanplus_slots
|
||||||
|
|
||||||
|
# Then parts if shopsanity is on
|
||||||
|
if self.options.shopsanity:
|
||||||
|
valid_parts_rewards_count: int = len(self.randomized_valid_parts_rewards)
|
||||||
|
shopsanity_slots: int = valid_parts_rewards_count if filler_slots > valid_parts_rewards_count else filler_slots
|
||||||
|
itempool += [self.create_item(p.name) for p in self.randomized_valid_parts_rewards[:shopsanity_slots]][:shopsanity_slots]
|
||||||
|
# Note to self, list slice should be redundant, but ensures the added amount of items doesn't exceed the length of shopsanity_slots
|
||||||
|
filler_slots = filler_slots - shopsanity_slots
|
||||||
|
|
||||||
|
# Credit checks
|
||||||
|
itempool += [self.create_item(Constants.CREDIT_ITEM_NAME) for c in range(filler_slots)][:filler_slots]
|
||||||
|
|
||||||
|
# Set Goal item location
|
||||||
|
|
||||||
|
if self.options.goal == 0: # Missionsanity
|
||||||
|
mission_threshold_location: ACLocation = ACLocation(mission_list_region, self.player, Constants.VICTORY_LOCATION_NAME, Constants.VICTORY_LOCATION_ID)
|
||||||
|
# The player wins after beating a number of mission determined in yaml (missionsanity_goal_requirement)
|
||||||
|
set_rule(mission_threshold_location, lambda state: state.has_from_list([m.name for m in self.mission_unlock_order], self.player, self.options.missionsanity_goal_requirement))
|
||||||
|
mission_threshold_location.place_locked_item(create_victory_event(self.player))
|
||||||
|
mission_list_region.locations.append(mission_threshold_location)
|
||||||
|
else: # Progressive Missions
|
||||||
|
destroy_floating_mines_location: MissionLocation = MissionLocation(mission_list_region, self.player, DESTROY_FLOATING_MINES)
|
||||||
|
# Destroy Floating Mines will appear after receiving 9 progressive mission items
|
||||||
|
set_rule(destroy_floating_mines_location, lambda state: state.count(Constants.PROGRESSIVE_MISSION_ITEM_NAME, self.player) == 9)
|
||||||
|
destroy_floating_mines_location.place_locked_item(create_victory_event(self.player))
|
||||||
|
mission_list_region.locations.append(destroy_floating_mines_location)
|
||||||
|
|
||||||
|
self.multiworld.itempool.extend(itempool)
|
||||||
|
|
||||||
|
menu_region.connect(mission_list_region)
|
||||||
|
self.multiworld.regions.append(mission_list_region)
|
||||||
|
self.multiworld.regions.append(menu_region)
|
||||||
|
|
||||||
|
def fill_slot_data(self) -> typing.Dict[str, typing.Any]:
|
||||||
|
return {
|
||||||
|
Constants.GAME_OPTIONS_KEY: self.options.serialize(),
|
||||||
|
Constants.STARTING_PARTS_KEY: self.starting_parts_str
|
||||||
|
}
|
||||||
429
worlds/armoredcore/ac_randomizer.py
Normal file
429
worlds/armoredcore/ac_randomizer.py
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
# Authors: Vartazian, edited by Justin(Jumza)
|
||||||
|
|
||||||
|
from .parts import *
|
||||||
|
import random
|
||||||
|
import typing
|
||||||
|
#Randomizer can generate ACs with weighting on Specific tiers. Check Parts.py to see Tiers of parts.
|
||||||
|
|
||||||
|
def get_random_head_by_tier():
|
||||||
|
tiers = [1, 2, 3, 4]
|
||||||
|
tier_weights = [100, 0, 0, 0] # %chance of each part being selected by the tier. 50, 50, 0 ,0 for example means that you have 50% chance of getting a T1 or T2 Part. No T3 or T4 in Pool.
|
||||||
|
|
||||||
|
selected_tier = random.choices(tiers, weights = tier_weights, k=1)[0]
|
||||||
|
|
||||||
|
if selected_tier == 1:
|
||||||
|
eligible_heads = heads_by_tier[1]
|
||||||
|
elif selected_tier == 2:
|
||||||
|
eligible_heads = heads_by_tier[2]
|
||||||
|
elif selected_tier == 3:
|
||||||
|
eligible_heads = heads_by_tier[3]
|
||||||
|
elif selected_tier == 4:
|
||||||
|
eligible_heads = all_heads
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
return random.choice(eligible_heads)
|
||||||
|
|
||||||
|
def get_random_core_by_tier():
|
||||||
|
tiers = [1, 2]
|
||||||
|
tier_weights = [50, 50]
|
||||||
|
|
||||||
|
selected_tier = random.choices(tiers, weights = tier_weights, k=1)[0]
|
||||||
|
|
||||||
|
if selected_tier == 1:
|
||||||
|
eligible_cores = cores_by_tier[1]
|
||||||
|
elif selected_tier == 2:
|
||||||
|
eligible_cores = all_cores
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return random.choice(eligible_cores)
|
||||||
|
|
||||||
|
def get_random_arms_by_tier():
|
||||||
|
tiers = [1, 2, 3, 4]
|
||||||
|
tier_weights = [100, 0, 0, 0]
|
||||||
|
|
||||||
|
selected_tier = random.choices(tiers, weights = tier_weights, k=1)[0]
|
||||||
|
|
||||||
|
if selected_tier == 1:
|
||||||
|
eligible_Arms = arms_by_tier[1]
|
||||||
|
elif selected_tier == 2:
|
||||||
|
eligible_Arms = arms_by_tier[2]
|
||||||
|
elif selected_tier == 3 or selected_tier == 4:
|
||||||
|
eligible_Arms = all_arms
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return random.choice(eligible_Arms)
|
||||||
|
|
||||||
|
def get_random_legs_by_tier():
|
||||||
|
tiers = [1, 2, 3, 4]
|
||||||
|
tier_weights = [100, 0, 0, 0]
|
||||||
|
|
||||||
|
selected_tier = random.choices(tiers, weights = tier_weights, k=1)[0]
|
||||||
|
|
||||||
|
if selected_tier == 1:
|
||||||
|
eligible_Legs = legs_by_tier[1]
|
||||||
|
elif selected_tier == 2:
|
||||||
|
eligible_Legs = legs_by_tier[2]
|
||||||
|
elif selected_tier == 3:
|
||||||
|
eligible_Legs = legs_by_tier[3]
|
||||||
|
elif selected_tier == 4:
|
||||||
|
eligible_Legs = legs_by_tier[4]
|
||||||
|
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return random.choice(eligible_Legs)
|
||||||
|
|
||||||
|
def get_random_booster_by_tier():
|
||||||
|
tiers = [1, 2, 3]
|
||||||
|
tier_weights = [100, 0, 0]
|
||||||
|
|
||||||
|
selected_tier = random.choices(tiers, weights = tier_weights, k=1)[0]
|
||||||
|
|
||||||
|
if selected_tier == 1:
|
||||||
|
eligible_booster = booster_by_tier[1]
|
||||||
|
elif selected_tier == 2:
|
||||||
|
eligible_booster = booster_by_tier[2]
|
||||||
|
elif selected_tier == 3:
|
||||||
|
eligible_booster = all_boosters
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return random.choice(eligible_booster)
|
||||||
|
|
||||||
|
def get_random_generator_by_tier():
|
||||||
|
tiers = [1, 2, 3, 4]
|
||||||
|
tier_weights = [100, 0, 0, 0]
|
||||||
|
|
||||||
|
selected_tier = random.choices(tiers, weights = tier_weights, k=1)[0]
|
||||||
|
|
||||||
|
if selected_tier == 1:
|
||||||
|
eligible_generators = generator_by_tier[1]
|
||||||
|
elif selected_tier == 2:
|
||||||
|
eligible_generators = generator_by_tier[2]
|
||||||
|
elif selected_tier == 3 or selected_tier == 4:
|
||||||
|
eligible_generators = all_generators
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return random.choice(eligible_generators)
|
||||||
|
|
||||||
|
def get_random_FCS_by_tier():
|
||||||
|
tiers = [1, 2, 3]
|
||||||
|
tier_weights = [100, 0, 0]
|
||||||
|
|
||||||
|
selected_tier = random.choices(tiers, weights = tier_weights, k=1)[0]
|
||||||
|
|
||||||
|
if selected_tier == 1:
|
||||||
|
eligible_FCS = fcs_by_tier[1]
|
||||||
|
elif selected_tier == 2:
|
||||||
|
eligible_FCS = fcs_by_tier[2]
|
||||||
|
elif selected_tier == 3:
|
||||||
|
eligible_FCS = all_fcs
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return random.choice(eligible_FCS)
|
||||||
|
|
||||||
|
def get_random_Arm_Weapon_R_by_tier():
|
||||||
|
tiers = [1, 2, 3, 4]
|
||||||
|
tier_weights = [100, 0, 0, 0]
|
||||||
|
|
||||||
|
selected_tier = random.choices(tiers, weights = tier_weights, k=1)[0]
|
||||||
|
|
||||||
|
if selected_tier == 1:
|
||||||
|
eligible_Arm_r = arm_weapon_r_by_tier[1]
|
||||||
|
elif selected_tier == 2:
|
||||||
|
eligible_Arm_r = arm_weapon_r_by_tier[2]
|
||||||
|
elif selected_tier == 3:
|
||||||
|
eligible_Arm_r = arm_weapon_r_by_tier[3]
|
||||||
|
elif selected_tier == 4:
|
||||||
|
eligible_Arm_r = all_arm_weapon_rs
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return random.choice(eligible_Arm_r)
|
||||||
|
|
||||||
|
def get_random_Arm_Weapon_L_by_tier():
|
||||||
|
tiers = [1, 2, 3]
|
||||||
|
tier_weights = [100, 0, 0]
|
||||||
|
|
||||||
|
selected_tier = random.choices(tiers, weights = tier_weights, k=1)[0]
|
||||||
|
|
||||||
|
if selected_tier == 1:
|
||||||
|
eligible_Arm_l = arm_weapon_l_by_tier[1]
|
||||||
|
elif selected_tier == 2:
|
||||||
|
eligible_Arm_l = arm_weapon_l_by_tier[2]
|
||||||
|
elif selected_tier == 3:
|
||||||
|
eligible_Arm_l = arm_weapon_l_by_tier[3]
|
||||||
|
elif selected_tier == 4:
|
||||||
|
eligible_Arm_l = all_arm_weapon_ls
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return random.choice(eligible_Arm_l)
|
||||||
|
|
||||||
|
def get_Back_Weapon_by_tier(guarantee_unlocked, RADAR):
|
||||||
|
tiers = [1, 2, 3, 4]
|
||||||
|
tier_weights = [100, 0, 0, 0]
|
||||||
|
|
||||||
|
selected_tier = random.choices(tiers, weights = tier_weights, k=1)[0]
|
||||||
|
|
||||||
|
if selected_tier == 1:
|
||||||
|
eligible_Back_Weapon = back_weapon_by_tier[1]
|
||||||
|
elif selected_tier == 2:
|
||||||
|
eligible_Back_Weapon = back_weapon_by_tier[2]
|
||||||
|
elif selected_tier == 3:
|
||||||
|
eligible_Back_Weapon = back_weapon_by_tier[3]
|
||||||
|
elif selected_tier == 4:
|
||||||
|
eligible_Back_Weapon = all_back_weapons
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
back_weapon = random.choice(eligible_Back_Weapon)
|
||||||
|
|
||||||
|
if guarantee_unlocked:
|
||||||
|
if back_weapon.fire_unlocked == False:
|
||||||
|
back_weapon_inelgible = True
|
||||||
|
else:
|
||||||
|
back_weapon_inelgible = False
|
||||||
|
while (back_weapon_inelgible):
|
||||||
|
back_weapon = random.choice(eligible_Back_Weapon)
|
||||||
|
if back_weapon.fire_unlocked == False:
|
||||||
|
back_weapon_inelgible = True
|
||||||
|
else:
|
||||||
|
back_weapon_inelgible = False
|
||||||
|
|
||||||
|
if RADAR:
|
||||||
|
if back_weapon.part_type == "RADAR":
|
||||||
|
back_weapon_inelgible = True
|
||||||
|
else:
|
||||||
|
back_weapon_inelgible = False
|
||||||
|
while (back_weapon_inelgible):
|
||||||
|
back_weapon = random.choice(eligible_Back_Weapon)
|
||||||
|
if back_weapon.part_type == "RADAR":
|
||||||
|
back_weapon_inelgible = True
|
||||||
|
else:
|
||||||
|
back_weapon_inelgible = False
|
||||||
|
|
||||||
|
|
||||||
|
#print(f"Selected tier: {selected_tier}")
|
||||||
|
#print(f"Eligible weapons for this tier: {eligible_Back_Weapon}")
|
||||||
|
#print(guarantee_unlocked)
|
||||||
|
|
||||||
|
return back_weapon
|
||||||
|
|
||||||
|
|
||||||
|
def generate_AC():
|
||||||
|
ac_head = get_random_head_by_tier()
|
||||||
|
ac_core = get_random_core_by_tier()
|
||||||
|
ac_arms = get_random_arms_by_tier()
|
||||||
|
ac_legs = get_random_legs_by_tier()
|
||||||
|
ac_booster = get_random_booster_by_tier()
|
||||||
|
ac_generator = get_random_generator_by_tier()
|
||||||
|
ac_FCS = get_random_FCS_by_tier()
|
||||||
|
ac_arm_weapon_r = get_random_Arm_Weapon_R_by_tier()
|
||||||
|
ac_arm_weapon_l = get_random_Arm_Weapon_L_by_tier()
|
||||||
|
|
||||||
|
ac_arm_weapon_r_status = "VALID"
|
||||||
|
ac_arm_weapon_l_status = "VALID"
|
||||||
|
ac_back_weapon_r_status = "VALID"
|
||||||
|
ac_back_weapon_l_status = "VALID"
|
||||||
|
percentage_chance_r = 80 #Base Percentage chances can me modified here. Logic will still give 100% for specific parts if rolls are unsuccessful.
|
||||||
|
percentage_chance_l = 50
|
||||||
|
percentage_chance_back_r = 50
|
||||||
|
percentage_chance_back_l = 33
|
||||||
|
|
||||||
|
ac_remaining_weight = ac_legs.max_weight - ac_core.weight - ac_generator.weight - ac_arms.weight - ac_booster.weight - ac_head.weight - ac_FCS.weight
|
||||||
|
|
||||||
|
ac_core_weight = ac_core.max_weight - ac_arms.weight
|
||||||
|
|
||||||
|
ac_remaining_energy = ac_generator.energy_output - ac_core.energy_drain - ac_legs.energy_drain - ac_arms.energy_drain - ac_booster.energy_drain - ac_head.energy_drain - ac_FCS.energy_drain
|
||||||
|
|
||||||
|
if ac_arms.part_type == "ARM UNIT":
|
||||||
|
if 0 <= percentage_chance_r <= 100:
|
||||||
|
ac_arm_weapon_r = get_random_Arm_Weapon_R_by_tier()
|
||||||
|
if random.random() < percentage_chance_r / 100:
|
||||||
|
ac_remaining_weight -= ac_arm_weapon_r.weight
|
||||||
|
else:
|
||||||
|
ac_arm_weapon_r_status = "NO EQUIP"
|
||||||
|
|
||||||
|
if 0 <= percentage_chance_l <= 100: #Laser Blade Guaranteed if Arm Weapon R is not generated.
|
||||||
|
if ac_arm_weapon_r_status == "NO EQUIP":
|
||||||
|
percentage_chance_l = 100
|
||||||
|
ac_arm_weapon_l = get_random_Arm_Weapon_L_by_tier()
|
||||||
|
if random.random() < percentage_chance_l / 100:
|
||||||
|
ac_remaining_weight -= ac_arm_weapon_l.weight
|
||||||
|
else:
|
||||||
|
ac_arm_weapon_l_status = "NO EQUIP"
|
||||||
|
|
||||||
|
if ac_arms.part_type != "ARM UNIT": #Flag for Weapon names
|
||||||
|
ac_arm_weapon_r_status = "EQUIPMENT IMPOSSIBLE"
|
||||||
|
ac_arm_weapon_l_status = "EQUIPMENT IMPOSSIBLE"
|
||||||
|
|
||||||
|
|
||||||
|
# Back Weapon is 100% Chance if Weapon arms are present or only one arm is present. Will also guarantee a fire unlocked weapon if AC has none currently.
|
||||||
|
|
||||||
|
if ac_arm_weapon_l_status == "NO EQUIP" or ac_arm_weapon_l_status == "EQUIPMENT IMPOSSIBLE" or ac_arm_weapon_r_status == "NO EQUIP" or ac_arm_weapon_r_status == "EQUIPMENT IMPOSSIBLE":
|
||||||
|
percentage_chance_back_r = 100
|
||||||
|
ac_back_weapon_r = get_Back_Weapon_by_tier(guarantee_unlocked = ac_arms.part_type != "ARM UNIT", RADAR = False)
|
||||||
|
if random.random() < percentage_chance_back_r / 100:
|
||||||
|
ac_remaining_weight -= ac_back_weapon_r.weight
|
||||||
|
|
||||||
|
else :
|
||||||
|
ac_back_weapon_r_status = "NO EQUIP"
|
||||||
|
|
||||||
|
#print (percentage_chance_back_r)
|
||||||
|
|
||||||
|
# If Back Weapon R is generated as a RADAR. 100% chance of getting a back weapon that can deal damage.
|
||||||
|
|
||||||
|
if ac_back_weapon_r.part_type == "RADAR":
|
||||||
|
percentage_chance_back_l = 100
|
||||||
|
ac_back_weapon_l = get_Back_Weapon_by_tier(guarantee_unlocked = False, RADAR = True)
|
||||||
|
if random.random() < percentage_chance_back_l / 100:
|
||||||
|
ac_remaining_weight -= ac_back_weapon_l.weight
|
||||||
|
|
||||||
|
else :
|
||||||
|
ac_back_weapon_l_status = "NO EQUIP"
|
||||||
|
|
||||||
|
if ac_arm_weapon_r_status == "VALID":
|
||||||
|
ac_remaining_energy -= ac_arm_weapon_r.energy_drain
|
||||||
|
if ac_arm_weapon_l_status == "VALID":
|
||||||
|
ac_remaining_energy -= ac_arm_weapon_l.energy_drain
|
||||||
|
if ac_back_weapon_r_status == "VALID":
|
||||||
|
ac_remaining_energy -= ac_back_weapon_r.energy_drain
|
||||||
|
if ac_back_weapon_l_status == "VALID":
|
||||||
|
ac_remaining_energy -= ac_back_weapon_l.energy_drain
|
||||||
|
|
||||||
|
if ac_arms.part_type != "ARM UNIT": #Flag for Weapon names
|
||||||
|
ac_arm_weapon_r_status = "EQUIPMENT IMPOSSIBLE"
|
||||||
|
ac_arm_weapon_l_status = "EQUIPMENT IMPOSSIBLE"
|
||||||
|
|
||||||
|
if ac_arms.part_type == "ARM UNIT":
|
||||||
|
ac_core_weight -= ac_arm_weapon_r.weight - ac_arm_weapon_l.weight
|
||||||
|
|
||||||
|
if (ac_remaining_weight < 0 or ac_core_weight < 0 or ac_remaining_energy < 0):
|
||||||
|
invalid_AC = True
|
||||||
|
else:
|
||||||
|
invalid_AC = False
|
||||||
|
|
||||||
|
# Check for invalid part combinations
|
||||||
|
|
||||||
|
if ac_legs.part_type == "CATERPILLAR":
|
||||||
|
if ac_back_weapon_l.name == "RZ-BBP" or ac_back_weapon_r.name == "RZ-BBP":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "RZ-A1" or ac_back_weapon_r.name == "RZ-A1":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "RXA-99" or ac_back_weapon_r.name == "RXA-99":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "RZ-A0" or ac_back_weapon_r.name == "RZ-A0":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "RXA-01WE" or ac_back_weapon_r.name == "RXA-01WE":
|
||||||
|
invalid_AC = True
|
||||||
|
|
||||||
|
if ac_arm_weapon_r.name == "WG-1-KARASAWA":
|
||||||
|
invalid_AC = True
|
||||||
|
|
||||||
|
# Is it even possible for the ranom generator not generate a booster? That would mean tank legs are never possible
|
||||||
|
if ac_booster != None:
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_legs.part_type == "FOUR LEGS TYPE":
|
||||||
|
if ac_arm_weapon_r.name == "WG-1-KARASAWA":
|
||||||
|
invalid_AC = True
|
||||||
|
if ac_arms.name == "ANKS-1A46J":
|
||||||
|
if ac_back_weapon_l.name == "WM-S40/1" or ac_back_weapon_r.name == "WM-S40/1":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WM-S40/2" or ac_back_weapon_r.name == "WM-S40/2":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WM-S60/4" or ac_back_weapon_r.name == "WM-S60/4":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WM-S60/6" or ac_back_weapon_r.name == "WM-S60/6":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WM-MVG404" or ac_back_weapon_r.name == "WM-MVG404":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WM-MVG802" or ac_back_weapon_r.name == "WM-MVG802":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WM-L201" or ac_back_weapon_r.name == "WM-L201":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WM-X201" or ac_back_weapon_r.name == "WM-X201":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WM-X5-AA" or ac_back_weapon_r.name == "WM-X5-AA":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WM-X10" or ac_back_weapon_r.name == "WM-X10":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WM-P4001" or ac_back_weapon_r.name == "WM-P4001":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WM-PS-2" or ac_back_weapon_r.name == "WM-PS-2":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WR-S50" or ac_back_weapon_r.name == "WR-S50":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WR-S100" or ac_back_weapon_r.name == "WR-S100":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WR-M50" or ac_back_weapon_r.name == "WR-M50":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WR-M70" or ac_back_weapon_r.name == "WR-M70":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WR-L24" or ac_back_weapon_r.name == "WR-L24":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WC-CN35" or ac_back_weapon_r.name == "WC-CN35":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WC-ST120" or ac_back_weapon_r.name == "WC-ST120":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WC-LN350" or ac_back_weapon_r.name == "WC-LN350":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WC-GN230" or ac_back_weapon_r.name == "WC-GN230":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WC-XP4000" or ac_back_weapon_r.name == "WC-XP4000":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WC-XC8000" or ac_back_weapon_r.name == "WC-XC8000":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WC-01QL" or ac_back_weapon_r.name == "WC-01QL":
|
||||||
|
invalid_AC = True
|
||||||
|
elif ac_back_weapon_l.name == "WX-S800-GF" or ac_back_weapon_r.name == "WX-S800-GF":
|
||||||
|
invalid_AC = True
|
||||||
|
|
||||||
|
return (invalid_AC, ac_head, ac_core, ac_arms, ac_legs, ac_booster, ac_generator, ac_FCS, ac_arm_weapon_r, ac_arm_weapon_l, ac_back_weapon_r, ac_back_weapon_l, ac_remaining_weight, ac_core_weight, ac_remaining_energy, ac_arm_weapon_r_status, ac_arm_weapon_l_status, ac_back_weapon_r_status, ac_back_weapon_l_status)
|
||||||
|
|
||||||
|
|
||||||
|
def randomize_start_parts():
|
||||||
|
(invalid_AC, ac_head, ac_core, ac_arms, ac_legs, ac_booster, ac_generator, ac_FCS, ac_arm_weapon_r, ac_arm_weapon_l, ac_back_weapon_r, ac_back_weapon_l, ac_remaining_weight, ac_core_weight, ac_remaining_energy, ac_arm_weapon_r_status, ac_arm_weapon_l_status, ac_back_weapon_r_status, ac_back_weapon_l_status) = generate_AC()
|
||||||
|
|
||||||
|
while invalid_AC == True:
|
||||||
|
print ("Invalid AC Detected: Retrying...")
|
||||||
|
(invalid_AC, ac_head, ac_core, ac_arms, ac_legs, ac_booster, ac_generator, ac_FCS, ac_arm_weapon_r, ac_arm_weapon_l, ac_back_weapon_r, ac_back_weapon_l, ac_remaining_weight, ac_core_weight, ac_remaining_energy, ac_arm_weapon_r_status, ac_arm_weapon_l_status, ac_back_weapon_r_status, ac_back_weapon_l_status) = generate_AC()
|
||||||
|
|
||||||
|
if ac_arm_weapon_l_status == "NO EQUIP":
|
||||||
|
ac_arm_weapon_l = "NO EQUIP"
|
||||||
|
if ac_arm_weapon_l_status == "EQUIPMENT IMPOSSIBLE":
|
||||||
|
ac_arm_weapon_l = "EQUIPMENT IMPOSSIBLE"
|
||||||
|
|
||||||
|
if ac_arm_weapon_r_status == "NO EQUIP":
|
||||||
|
ac_arm_weapon_r = "NO EQUIP"
|
||||||
|
if ac_arm_weapon_r_status == "EQUIPMENT IMPOSSIBLE":
|
||||||
|
ac_arm_weapon_r = "EQUIPMENT IMPOSSIBLE"
|
||||||
|
|
||||||
|
if ac_back_weapon_r_status == "NO EQUIP":
|
||||||
|
ac_back_weapon_r = "NO EQUIP"
|
||||||
|
if ac_back_weapon_l_status == "NO EQUIP":
|
||||||
|
ac_back_weapon_l = "NO EQUIP"
|
||||||
|
|
||||||
|
print (ac_head.name)
|
||||||
|
print (ac_core.name)
|
||||||
|
print (ac_arms.name)
|
||||||
|
print (ac_legs.name)
|
||||||
|
print (ac_booster.name)
|
||||||
|
print (ac_generator.name)
|
||||||
|
print (ac_FCS.name)
|
||||||
|
print (ac_arm_weapon_r)
|
||||||
|
print (ac_arm_weapon_l)
|
||||||
|
print (ac_back_weapon_r)
|
||||||
|
print (ac_back_weapon_l)
|
||||||
|
|
||||||
|
print(f"Remaining Weight:", ac_remaining_weight, f"Remaining Arm Weight:", ac_core_weight, f"Remaining Energy:", ac_remaining_energy)
|
||||||
|
|
||||||
|
new_start_parts: typing.Tuple[Part, ...] = {ac_head, ac_core, ac_arms, ac_legs, ac_booster, ac_generator, ac_FCS, ac_arm_weapon_r, ac_arm_weapon_l, ac_back_weapon_r, ac_back_weapon_l}
|
||||||
|
|
||||||
|
return new_start_parts
|
||||||
6
worlds/armoredcore/archipelago.json
Normal file
6
worlds/armoredcore/archipelago.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"game": "Armored Core",
|
||||||
|
"world_version": "0.3.0",
|
||||||
|
"authors": ["Jumza", "ArmoredKori", "Vartazian"],
|
||||||
|
"compatible_version": 7
|
||||||
|
}
|
||||||
893
worlds/armoredcore/client.py
Normal file
893
worlds/armoredcore/client.py
Normal file
@@ -0,0 +1,893 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from NetUtils import ClientStatus
|
||||||
|
from BaseClasses import ItemClassification
|
||||||
|
from collections import Counter
|
||||||
|
import random
|
||||||
|
import worlds._bizhawk as bizhawk
|
||||||
|
from worlds._bizhawk.client import BizHawkClient
|
||||||
|
|
||||||
|
from .version import __version__
|
||||||
|
from .utils import Constants
|
||||||
|
from .locations import get_location_id_for_mission, is_mission_location_id, mission_from_location_id, get_location_id_for_mail, get_location_id_for_shop_listing
|
||||||
|
from .mission import Mission, all_missions
|
||||||
|
from .mail import Mail, all_mail
|
||||||
|
from .parts import Part, all_parts, id_to_part, all_parts_data_order, base_starting_parts, name_to_part, all_heads, all_cores, all_boosters, all_arms, all_arm_weapon_rs, all_arm_weapon_ls, all_back_weapons, all_generators, all_fcs, all_legs
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from worlds._bizhawk.context import BizHawkClientContext
|
||||||
|
from NetUtils import JSONMessagePart
|
||||||
|
|
||||||
|
MAIN_RAM: typing.Final[str] = "MainRAM"
|
||||||
|
|
||||||
|
class ACClient(BizHawkClient):
|
||||||
|
game: str = Constants.GAME_NAME
|
||||||
|
system: str = "PSX"
|
||||||
|
patch_suffix: str = ".apac"
|
||||||
|
local_checked_locations: typing.Set[int]
|
||||||
|
checked_version_string: bool
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.local_checked_locations = set()
|
||||||
|
|
||||||
|
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||||
|
|
||||||
|
try:
|
||||||
|
# this import down here to prevent circular import issue
|
||||||
|
from CommonClient import logger
|
||||||
|
# Check ROM name/patch version
|
||||||
|
# Unable to locate rom name, verifying based on memory card id instead
|
||||||
|
# If you know what the rom memory domain for PSX is on Bizhawk please let me know!
|
||||||
|
mem_card_id_bytes = ((await bizhawk.read(ctx.bizhawk_ctx, [(0x4B6BD, 12, MAIN_RAM)]))[0])
|
||||||
|
mem_card_id = bytes([byte for byte in mem_card_id_bytes if byte != 0]).decode("ascii")
|
||||||
|
logger.info(f"{mem_card_id} mem_card_id")
|
||||||
|
if not mem_card_id.startswith("BASCUS-94182"):
|
||||||
|
return False
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return False
|
||||||
|
except bizhawk.RequestFailedError:
|
||||||
|
return False # Should verify on the next pass
|
||||||
|
|
||||||
|
ctx.game = self.game
|
||||||
|
ctx.items_handling = 0b111 # Has this been set correctly? A little confusion
|
||||||
|
ctx.want_slot_data = True
|
||||||
|
ctx.watcher_timeout = 0.125
|
||||||
|
logger.info(f"Armored Core 1 Client v{__version__}.")
|
||||||
|
# Add updates section to logger info
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def randomize_starting_ac(self, ctx: "BizHawkClientContext") -> None:
|
||||||
|
# Check to see if this option is on, if it is:
|
||||||
|
# Check to see if the randomize starting ac flag has been turned on, if not:
|
||||||
|
# Remove base starting parts from players inventory
|
||||||
|
# if Shopsanity is off, put the base starting parts into the shop. Doesn't really matter if new randomized starting parts are in the shop or not
|
||||||
|
# Place new starting parts into inventory
|
||||||
|
# Equip new starting parts onto player
|
||||||
|
# set the flag that the starting ac has been randomized to be on
|
||||||
|
|
||||||
|
if ctx.slot_data[Constants.GAME_OPTIONS_KEY]["rando_start_parts"] == False:
|
||||||
|
return
|
||||||
|
already_randomized: int = int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.STARTING_AC_RANDO_TRACKING_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
# Need to make sure the player has selected Scenario Mode in the main menu first before randomizing. Expected value if it has been selected: 0x18
|
||||||
|
scenario_mode_selected: int = int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.SCENARIO_MODE_SELECTED_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
if already_randomized == 1 or scenario_mode_selected != 0x18:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get inventory
|
||||||
|
inventory_bytes = list((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.PARTS_INVENTORY_OFFSET, 147, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
|
||||||
|
for part in base_starting_parts:
|
||||||
|
inventory_bytes[part.id] = 0x00
|
||||||
|
|
||||||
|
if ctx.slot_data[Constants.GAME_OPTIONS_KEY]["shopsanity"] == False:
|
||||||
|
for part in base_starting_parts:
|
||||||
|
# Put one in the store inventory
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.SHOP_INVENTORY_OFFSET + part.id,
|
||||||
|
[0x01],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
for part_name in ctx.slot_data[Constants.STARTING_PARTS_KEY]:
|
||||||
|
inventory_bytes[name_to_part[part_name].id] = 0x01
|
||||||
|
|
||||||
|
# active parts order for your ac unit is: Core, Boosters, Head, Arms, Arm W L, Arm W R, Back W L, Back W R, Generator, FCS, BLANK, Legs
|
||||||
|
active_parts: typing.List[int] = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]
|
||||||
|
for part_name in ctx.slot_data[Constants.STARTING_PARTS_KEY]:
|
||||||
|
# Does this preserve order of parts as defined in init? I don't know that I can rely on that
|
||||||
|
if name_to_part[part_name] in all_cores:
|
||||||
|
active_parts[0] = name_to_part[part_name].id - all_cores[0].id
|
||||||
|
if name_to_part[part_name] in all_boosters:
|
||||||
|
active_parts[1] = name_to_part[part_name].id - all_boosters[0].id
|
||||||
|
if name_to_part[part_name] in all_heads:
|
||||||
|
active_parts[2] = name_to_part[part_name].id - all_heads[0].id
|
||||||
|
if name_to_part[part_name] in all_arms:
|
||||||
|
active_parts[3] = name_to_part[part_name].id - all_arms[0].id
|
||||||
|
if name_to_part[part_name] in all_arm_weapon_ls:
|
||||||
|
active_parts[4] = name_to_part[part_name].id - all_arm_weapon_ls[0].id
|
||||||
|
if name_to_part[part_name] in all_arm_weapon_rs:
|
||||||
|
active_parts[5] = name_to_part[part_name].id - all_arm_weapon_rs[0].id
|
||||||
|
if name_to_part[part_name] in all_back_weapons and active_parts[6] == 0xFF:
|
||||||
|
active_parts[6] = name_to_part[part_name].id - all_back_weapons[0].id
|
||||||
|
elif name_to_part[part_name] in all_back_weapons:
|
||||||
|
active_parts[7] = name_to_part[part_name].id - all_back_weapons[0].id
|
||||||
|
if name_to_part[part_name] in all_generators:
|
||||||
|
active_parts[8] = name_to_part[part_name].id - all_generators[0].id
|
||||||
|
if name_to_part[part_name] in all_fcs:
|
||||||
|
active_parts[9] = name_to_part[part_name].id - all_fcs[0].id
|
||||||
|
if name_to_part[part_name] in all_legs:
|
||||||
|
active_parts[11] = name_to_part[part_name].id - all_legs[0].id
|
||||||
|
|
||||||
|
# Write the inventory, then the players new AC parts, then the AC is randomized flag
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.PARTS_INVENTORY_OFFSET,
|
||||||
|
inventory_bytes,
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.CURRENT_AC_PARTS_OFFSET,
|
||||||
|
active_parts,
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.STARTING_AC_RANDO_TRACKING_OFFSET,
|
||||||
|
[0x01],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
|
||||||
|
async def shopsanity_initialization(self, ctx: "BizHawkClientContext", in_menu) -> None:
|
||||||
|
if not in_menu or ctx.slot_data[Constants.GAME_OPTIONS_KEY]["shopsanity"] == False:
|
||||||
|
return []
|
||||||
|
# Disable the Sell option in game
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.SHOP_SELL_INTERCEPT_OFFSETS[0],
|
||||||
|
[0x00, 0x00, 0x60, 0xA0],
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.SHOP_SELL_INTERCEPT_OFFSETS[0],
|
||||||
|
[0x00, 0x00, 0x62, 0xA0],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.SHOP_SELL_INTERCEPT_OFFSETS[1],
|
||||||
|
[0x00, 0x00, 0x60, 0xA0],
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.SHOP_SELL_INTERCEPT_OFFSETS[1],
|
||||||
|
[0x00, 0x00, 0x62, 0xA0],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.SHOP_SELL_INTERCEPT_OFFSETS[2],
|
||||||
|
[0x00, 0x00, 0x60, 0xA0],
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.SHOP_SELL_INTERCEPT_OFFSETS[2],
|
||||||
|
[0x00, 0x00, 0x62, 0xA0],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.SHOP_SELL_INTERCEPT_OFFSETS[3],
|
||||||
|
[0x00, 0x00, 0x60, 0xA0],
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.SHOP_SELL_INTERCEPT_OFFSETS[3],
|
||||||
|
[0x00, 0x00, 0x62, 0xA0],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
# Change Sell text to BUY2
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.SHOP_SELL_TEXT__OFFSET,
|
||||||
|
[0x42, 0x55, 0x59, 0x32],
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.SHOP_SELL_TEXT__OFFSET,
|
||||||
|
[0x53, 0x45, 0x4C, 0x4C],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
# Prevent shop from removing Optional Parts it mistakenly thinks you don't have
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.SHOP_OVERWRITE_OPTIONAL_PARTS_OFFSET,
|
||||||
|
[0x00, 0x00, 0x00, 0x00],
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.SHOP_OVERWRITE_OPTIONAL_PARTS_OFFSET,
|
||||||
|
[0xA0, 0x9C, 0x22, 0xA4],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
shop_listings: int = int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.SHOPSANITY_TRACKING_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
if shop_listings == 0: # The run has just begun, set listings to 1 and remove everything from the shop
|
||||||
|
shop_listings = 1
|
||||||
|
new_shop_contents: str = "00" * 147
|
||||||
|
shop_contents_hex: typing.List[int] = []
|
||||||
|
for i in range(0, len(new_shop_contents), 2):
|
||||||
|
shop_contents_hex.append(int(new_shop_contents[i:i+2], 16))
|
||||||
|
# Write instead of guarded write based on ravens nest menu check
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.SHOP_INVENTORY_OFFSET,
|
||||||
|
shop_contents_hex,
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
# Write shop listings so this won't happen again
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.SHOPSANITY_TRACKING_OFFSET,
|
||||||
|
shop_listings,
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def read_mission_completion(self, ctx: "BizHawkClientContext", in_menu) -> typing.List[bool]:
|
||||||
|
if not in_menu:
|
||||||
|
return []
|
||||||
|
byte_list_missions: typing.List[bytes] = []
|
||||||
|
for mission_number in range(len(all_missions)):
|
||||||
|
# Don't read mission completion for omitted missions
|
||||||
|
byte_list_missions.append((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.MISSION_COMPLETION_OFFSET + all_missions[mission_number].id, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
mission_completed_flags: typing.List[bool] = []
|
||||||
|
for byte in byte_list_missions:
|
||||||
|
if int.from_bytes(byte) & 0x2 == 0x2:
|
||||||
|
mission_completed_flags.append(True)
|
||||||
|
else:
|
||||||
|
mission_completed_flags.append(False)
|
||||||
|
return mission_completed_flags
|
||||||
|
|
||||||
|
async def read_mail_read_flags(self, ctx: "BizHawkClientContext", in_menu) -> typing.List[bool]:
|
||||||
|
if not in_menu:
|
||||||
|
return []
|
||||||
|
|
||||||
|
byte_list_mail: typing.List[bytes] = []
|
||||||
|
for mail_number in range(len(all_mail)):
|
||||||
|
# Don't read mail read flag for omitted mail
|
||||||
|
byte_list_mail.append((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.MAIL_RECEPTION_OFFSET + all_mail[mail_number].id, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
mail_read_flags: typing.List[bool] = []
|
||||||
|
# There must be a better way! Too tired to think of a better one atm
|
||||||
|
accepted_bytes: typing.List[bytes] = [0x3, 0x7, 0xb, 0xf,
|
||||||
|
0x13, 0x17, 0x1b, 0x1f,
|
||||||
|
0x23, 0x27, 0x2b, 0x2f,
|
||||||
|
0x33, 0x37, 0x3b, 0x3f,
|
||||||
|
0x43, 0x47, 0x4b, 0x4f,
|
||||||
|
0x53, 0x57, 0x5b]
|
||||||
|
for byte in byte_list_mail:
|
||||||
|
if int.from_bytes(byte) in accepted_bytes:
|
||||||
|
mail_read_flags.append(True)
|
||||||
|
else:
|
||||||
|
mail_read_flags.append(False)
|
||||||
|
return mail_read_flags
|
||||||
|
|
||||||
|
# return: True/False if it detects we are in the Ravens Nest Menu
|
||||||
|
async def ravens_nest_menu_check(self, ctx: "BizHawkClientContext") -> bool:
|
||||||
|
MENU_LOADED_BYTES: bytes = bytes([0xC0, 0xDC, 0x04, 0x80])
|
||||||
|
menu_verification: bytes = (await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.MENU_LOADED_VERIFY_OFFSET1, 4, MAIN_RAM)]
|
||||||
|
))[0]
|
||||||
|
if menu_verification == MENU_LOADED_BYTES:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
menu_verification = int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.MENU_LOADED_VERIFY_OFFSET2, 4, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
if menu_verification == MENU_LOADED_BYTES:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# return: 0-5 indicates what part of the ravens nest menu we are hovering / in. -1 means we are not in the ravens nest menu.
|
||||||
|
async def ravens_nest_menu_section_check(self, ctx: "BizHawkClientContext") -> int:
|
||||||
|
menu_verification: int = int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.MENU_CURRENT_SELECTION1_VERIFY_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
if menu_verification == 0x20 or menu_verification == 0xE0:
|
||||||
|
return int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.MENU_CURRENT_SELECTION1_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
else:
|
||||||
|
menu_verification = int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.MENU_CURRENT_SELECTION2_VERIFY_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
if menu_verification == 0x20 or menu_verification == 0xE0:
|
||||||
|
return int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.MENU_CURRENT_SELECTION2_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
else:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
async def update_mission_list_code(self, ctx: "BizHawkClientContext", menu_section) -> None:
|
||||||
|
# Mission list code needs to be updated on the fly by the client
|
||||||
|
|
||||||
|
if menu_section != 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Lock / Unlock if the mission menu is about to be loaded and the Mail data hasn't been overwritten with mission data
|
||||||
|
# I hate fighting race conditions (need to find other free space)
|
||||||
|
|
||||||
|
code_written_check: int = int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.FREESPACE_CODE_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
locked: bool = False
|
||||||
|
|
||||||
|
if code_written_check != 0x1F:
|
||||||
|
await bizhawk.lock(ctx.bizhawk_ctx)
|
||||||
|
locked = True
|
||||||
|
|
||||||
|
await self.set_mission_list_display_all(ctx)
|
||||||
|
|
||||||
|
# Hooks into mission list write routine
|
||||||
|
# OOF hardcoded this jump. If freespace changes this too must change unless you code better
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.MISSION_MENU_HOOK_OFFSET,
|
||||||
|
[0x18, 0xFE, 0x05, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.MISSION_MENU_HOOK_OFFSET,
|
||||||
|
[0x1F, 0x80, 0x01, 0x3C, 0x21, 0x08, 0x30, 0x00, 0xD4, 0x37, 0x23, 0xA0],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
# Freespace we write to to update mission list as new mission checks are received
|
||||||
|
code_as_hex: typing.List[int] = []
|
||||||
|
#lui r1,0x801f
|
||||||
|
#addu r1,r1,r16
|
||||||
|
code_as_string: str = "1F80013C21083000"
|
||||||
|
mission_counter: int = 0
|
||||||
|
number_of_missions: int = 0
|
||||||
|
if ctx.slot_data[Constants.GAME_OPTIONS_KEY]["goal"] == 0: # Missionsanity
|
||||||
|
for item in ctx.items_received:
|
||||||
|
if is_mission_location_id(item.item):
|
||||||
|
number_of_missions += 1
|
||||||
|
for item in ctx.items_received:
|
||||||
|
if is_mission_location_id(item.item):
|
||||||
|
mission: Mission = mission_from_location_id(item.item)
|
||||||
|
code_as_string += self.construct_new_mission_code_entry(mission.id, mission_counter, number_of_missions)
|
||||||
|
mission_counter += 1
|
||||||
|
else: # Progressive Missions
|
||||||
|
progressive_missions_received: int = 0
|
||||||
|
for item in ctx.items_received:
|
||||||
|
if item.item == Constants.PROGRESSIVE_MISSION_ITEM_ID:
|
||||||
|
progressive_missions_received += 1
|
||||||
|
# number_of_missions is 5 * progressive mission items up to 8 times, then the 9th is 1. 46 total in the end.
|
||||||
|
if progressive_missions_received < 9:
|
||||||
|
number_of_missions = 5 * (progressive_missions_received + 1)
|
||||||
|
else:
|
||||||
|
number_of_missions = 46
|
||||||
|
for i in range(1, progressive_missions_received + 2):
|
||||||
|
for mission in all_missions:
|
||||||
|
if mission.progression_level == i:
|
||||||
|
code_as_string += self.construct_new_mission_code_entry(mission.id, mission_counter, number_of_missions)
|
||||||
|
mission_counter += 1
|
||||||
|
code_as_string += "0000000000000324D43723A0891C020800000000"
|
||||||
|
for i in range(0, len(code_as_string), 2):
|
||||||
|
code_as_hex.append(int(code_as_string[i:i+2], 16))
|
||||||
|
# Write instead of guarded write based on ravens nest menu check
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.FREESPACE_CODE_OFFSET,
|
||||||
|
code_as_hex,
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
if locked:
|
||||||
|
await bizhawk.unlock(ctx.bizhawk_ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def construct_new_mission_code_entry(self, mission_id: int, mission_counter: int, number_of_missions: int) -> str:
|
||||||
|
new_code_entry: str = ""
|
||||||
|
if (mission_counter < 16):
|
||||||
|
new_code_entry += "0" + hex(mission_counter)[2:]
|
||||||
|
else:
|
||||||
|
new_code_entry += hex(mission_counter)[2:]
|
||||||
|
new_code_entry += "000224"
|
||||||
|
# Branch by an additional 0xC(12) bytes for every mission entry
|
||||||
|
branch_amount = (number_of_missions - mission_counter) * 3
|
||||||
|
if (branch_amount < 16):
|
||||||
|
new_code_entry += "0" + hex(branch_amount)[2:]
|
||||||
|
else:
|
||||||
|
new_code_entry += hex(branch_amount)[2:]
|
||||||
|
new_code_entry += "005010"
|
||||||
|
if (mission_id < 16):
|
||||||
|
new_code_entry += "0" + hex(mission_id)[2:]
|
||||||
|
else:
|
||||||
|
new_code_entry += hex(mission_id)[2:]
|
||||||
|
new_code_entry += "000324"
|
||||||
|
return new_code_entry
|
||||||
|
|
||||||
|
async def set_mission_list_display_all(self, ctx: "BizHawkClientContext") -> None:
|
||||||
|
# Guarded write ensures we do this only when the menu is open
|
||||||
|
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.MISSION_LIST_MODE_OFFSET,
|
||||||
|
[0x00],
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.MISSION_MENU_HOOK_OFFSET,
|
||||||
|
[0x18, 0xFE, 0x05, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
async def award_credits(self, ctx: "BizHawkClientContext", in_menu) -> None:
|
||||||
|
# We fail to award credits if we are not in the ravens nest menu at all
|
||||||
|
if not in_menu:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Read how many credit items have been received
|
||||||
|
stored_credit_drops: int = int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.CREDIT_ITEMS_RECEIVED_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
|
||||||
|
received_credit_drops: int = 0
|
||||||
|
for item in ctx.items_received:
|
||||||
|
if item.item == Constants.CREDIT_ITEM_ID:
|
||||||
|
received_credit_drops += 1
|
||||||
|
|
||||||
|
from CommonClient import logger
|
||||||
|
|
||||||
|
if received_credit_drops > stored_credit_drops:
|
||||||
|
# Award the difference to the player
|
||||||
|
player_credit_bytes = (await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.PLAYER_CREDITS_OFFSET, 4, MAIN_RAM)]
|
||||||
|
))
|
||||||
|
player_credit: int = int.from_bytes(player_credit_bytes[0], "little", signed = True)
|
||||||
|
logger.info(f"Player credits read as {player_credit}")
|
||||||
|
p1, p2, p3, p4 = (player_credit & 0xFFFFFFFF).to_bytes(4, "little")
|
||||||
|
player_credit = player_credit + ((received_credit_drops - stored_credit_drops) * ctx.slot_data[Constants.GAME_OPTIONS_KEY]["credit_check_amount"])
|
||||||
|
c1, c2, c3, c4 = (player_credit & 0xFFFFFFFF).to_bytes(4, "little")
|
||||||
|
logger.info(f"Attempting to award {(received_credit_drops - stored_credit_drops) * ctx.slot_data[Constants.GAME_OPTIONS_KEY]["shopsanity"]}, new total should be {player_credit}")
|
||||||
|
logger.info(f"bytes {player_credit.to_bytes(4, "little", signed = True)} and {player_credit_bytes} and {player_credit_bytes[0]} and {c1} {c2} {c3} {c4}")
|
||||||
|
# Guarded write based on read in credit amount. Stops things from messing up when the game is updating credit value
|
||||||
|
award_success: bool = await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.PLAYER_CREDITS_OFFSET,
|
||||||
|
[c1, c2, c3, c4],
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.PLAYER_CREDITS_OFFSET,
|
||||||
|
[p1, p2, p3, p4],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
if award_success:
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.CREDIT_ITEMS_RECEIVED_OFFSET,
|
||||||
|
[received_credit_drops],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
async def award_humanplus(self, ctx: "BizHawkClientContext", in_menu) -> None:
|
||||||
|
# We fail to award humanplus levels if we are not in the ravens nest menu at all
|
||||||
|
# Although it should be safe to do during missions as well...
|
||||||
|
if not in_menu:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Read what human+ level we are at
|
||||||
|
# Progressive Human+ checks will overwrite Human+ progress from regular gameplay.
|
||||||
|
# This is probably fine
|
||||||
|
stored_humanplus_level: int = int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.HUMANPLUS_LEVEL_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
|
||||||
|
received_humanplus_drops: int = 0
|
||||||
|
for item in ctx.items_received:
|
||||||
|
if item.item == Constants.PROGRESSIVE_HUMANPLUS_ITEM_ID:
|
||||||
|
received_humanplus_drops += 1
|
||||||
|
|
||||||
|
new_humanplus_level: int
|
||||||
|
if received_humanplus_drops == 1:
|
||||||
|
new_humanplus_level = 0x1
|
||||||
|
elif received_humanplus_drops == 2:
|
||||||
|
new_humanplus_level = 0x4
|
||||||
|
elif received_humanplus_drops == 3:
|
||||||
|
new_humanplus_level = 0x6
|
||||||
|
else:
|
||||||
|
new_humanplus_level = 0x0
|
||||||
|
|
||||||
|
if stored_humanplus_level < new_humanplus_level:
|
||||||
|
# Award new human+ level
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.HUMANPLUS_LEVEL_OFFSET,
|
||||||
|
[new_humanplus_level],
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.HUMANPLUS_LEVEL_OFFSET,
|
||||||
|
[stored_humanplus_level],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
async def award_shop_listings(self, ctx: "BizHawkClientContext", mission_completion_count, in_menu) -> None:
|
||||||
|
# Don't bother if Shopsanity is not on
|
||||||
|
# shop listings are based on the number of completed missions
|
||||||
|
# shop listings are immediately scouted when the player earns them
|
||||||
|
|
||||||
|
if not in_menu or ctx.slot_data[Constants.GAME_OPTIONS_KEY]["shopsanity"] == False:
|
||||||
|
return []
|
||||||
|
|
||||||
|
shop_listings: int = int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.SHOPSANITY_TRACKING_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
|
||||||
|
# Because shop_listings starts at 1 and not 0, offset this by -1
|
||||||
|
shop_listings -= 1
|
||||||
|
|
||||||
|
shop_listings_unlock_order: list[Part] = list(all_parts_data_order)
|
||||||
|
|
||||||
|
if mission_completion_count > shop_listings: # We have listings to award the player
|
||||||
|
for i in range(shop_listings, mission_completion_count):
|
||||||
|
start_index: int = i * ctx.slot_data[Constants.GAME_OPTIONS_KEY]["shopsanity_listings_per_mission"]
|
||||||
|
end_index: int = (((i + 1) * ctx.slot_data[Constants.GAME_OPTIONS_KEY]["shopsanity_listings_per_mission"]) if ((i + 1) * ctx.slot_data[Constants.GAME_OPTIONS_KEY]["shopsanity_listings_per_mission"]) < len(shop_listings_unlock_order)
|
||||||
|
else len(shop_listings_unlock_order) - 1)
|
||||||
|
for part in shop_listings_unlock_order[start_index : end_index]:
|
||||||
|
# Put one in the store inventory
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.SHOP_INVENTORY_OFFSET + part.id,
|
||||||
|
[0x01],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
# Scout the location (a free hint for the player so they can easily tell what is in the shop)
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "LocationScouts",
|
||||||
|
"locations": [
|
||||||
|
get_location_id_for_shop_listing(part)
|
||||||
|
],
|
||||||
|
"create_as_hint": 2
|
||||||
|
}])
|
||||||
|
# Now write mission_completion_count + 1 to shop listings
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.SHOPSANITY_TRACKING_OFFSET,
|
||||||
|
[mission_completion_count + 1],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
async def award_parts(self, ctx: "BizHawkClientContext") -> None:
|
||||||
|
# No menu check required, it's always loaded in memory
|
||||||
|
|
||||||
|
# Shopsanity check
|
||||||
|
if ctx.slot_data[Constants.GAME_OPTIONS_KEY]["shopsanity"] == False:
|
||||||
|
return []
|
||||||
|
|
||||||
|
inventory_bytes = list((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.PARTS_INVENTORY_OFFSET, 147, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
inventory_copy = list(inventory_bytes)
|
||||||
|
for item in ctx.items_received:
|
||||||
|
partID: int = item.item - Constants.PARTS_INVENTORY_OFFSET # Converts from ap itemID back to part id when subtracting
|
||||||
|
if partID in id_to_part:
|
||||||
|
if inventory_bytes[partID] == 0x00:
|
||||||
|
inventory_bytes[partID] = 0x02 # Give 2 of those parts
|
||||||
|
|
||||||
|
# If no new parts have been given, don't perform the gaurded write
|
||||||
|
if inventory_bytes == inventory_copy:
|
||||||
|
return []
|
||||||
|
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.PARTS_INVENTORY_OFFSET,
|
||||||
|
inventory_bytes,
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.PARTS_INVENTORY_OFFSET,
|
||||||
|
inventory_copy,
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
# Checks shop listings received vs that listing still being available for purchase
|
||||||
|
# Also removes the part that was just purchased from the players inventory
|
||||||
|
async def check_purchased_items(self, ctx: "BizHawkClientContext", mission_completion_count, in_menu) -> typing.Dict[Part, bool]:
|
||||||
|
if not in_menu or ctx.slot_data[Constants.GAME_OPTIONS_KEY]["shopsanity"] == False:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
shop_listings_unlock_order: list[Part] = list(all_parts_data_order)
|
||||||
|
purchased_items: typing.Dict[Part, bool] = {}
|
||||||
|
# There are 147 entries
|
||||||
|
purchased_bytes = (await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.SHOP_INVENTORY_OFFSET, 147, MAIN_RAM)]
|
||||||
|
))[0]
|
||||||
|
inventory_bytes = list((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.PARTS_INVENTORY_OFFSET, 147, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
inventory_copy = list(inventory_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
if mission_completion_count > 0:
|
||||||
|
start_index: int = 0
|
||||||
|
end_index: int = (((mission_completion_count) * ctx.slot_data[Constants.GAME_OPTIONS_KEY]["shopsanity_listings_per_mission"]) if ((mission_completion_count) * ctx.slot_data[Constants.GAME_OPTIONS_KEY]["shopsanity_listings_per_mission"]) < len(purchased_bytes)
|
||||||
|
else len(purchased_bytes))
|
||||||
|
for count, byte in enumerate(purchased_bytes[start_index : end_index]):
|
||||||
|
#print(int.from_bytes(byte, "little", signed = True))
|
||||||
|
true_part_index: int = shop_listings_unlock_order[count].id
|
||||||
|
if byte == 0x00:
|
||||||
|
# The player has had a shop listing given and then purchased that item if they also have one or three
|
||||||
|
if inventory_bytes[true_part_index] == 0x01 or inventory_bytes[true_part_index] == 0x03 or (inventory_bytes[true_part_index] == 0x02 and shop_listings_unlock_order[count] in base_starting_parts):
|
||||||
|
purchased_items[shop_listings_unlock_order[true_part_index]] = True
|
||||||
|
# It will be 01 if they have just made the purchase but don't have that part in their inventory
|
||||||
|
if inventory_bytes[true_part_index] == 0x01:
|
||||||
|
inventory_bytes[true_part_index] = 0x00
|
||||||
|
# If they've purchased the item but they've already received that part, it doesn't matter if more are given to them
|
||||||
|
|
||||||
|
# Inequality signifies that the inventory needs updating
|
||||||
|
if inventory_bytes != inventory_copy:
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.PARTS_INVENTORY_OFFSET,
|
||||||
|
inventory_bytes,
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.PARTS_INVENTORY_OFFSET,
|
||||||
|
inventory_copy,
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
return purchased_items
|
||||||
|
|
||||||
|
# Update shop item and description text when shopsanity is active
|
||||||
|
async def shopsanity_update_shop_text(self, ctx: "BizHawkClientContext", menu_section) -> None:
|
||||||
|
if menu_section != 1 or ctx.slot_data[Constants.GAME_OPTIONS_KEY]["shopsanity"] == False: # Shop
|
||||||
|
return
|
||||||
|
|
||||||
|
locations_data = ctx.locations_info
|
||||||
|
|
||||||
|
if len(locations_data) != len(all_parts):
|
||||||
|
from CommonClient import logger
|
||||||
|
# Scout location data for shop text updating purposes!
|
||||||
|
parts_locations_to_scout: typing.List[int] = [get_location_id_for_shop_listing(part) for part in all_parts]
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "LocationScouts",
|
||||||
|
"locations": parts_locations_to_scout,
|
||||||
|
"create_as_hint": 0
|
||||||
|
}])
|
||||||
|
logger.info("Parts location data scouted.\nIf shop names are incorrect, exit and enter shop again.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if the data needs to be overwritten (Write in a junk value for checking)
|
||||||
|
first_part_char_check: int = int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.PARTS_TEXT_CHANGE_VERIFY_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
|
||||||
|
if first_part_char_check != 0x40:
|
||||||
|
return
|
||||||
|
|
||||||
|
first_part_char_check = 0x4A
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.PARTS_TEXT_CHANGE_VERIFY_OFFSET,
|
||||||
|
[first_part_char_check],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
# 12 Character max for new item names
|
||||||
|
# Descriptions can have 36 characters per line and have two lines with '8' point font. ; is newline marker
|
||||||
|
# Description starts with ~^num before text which defines font size. We'll use ~^8 for now (7E 5E 38)
|
||||||
|
# playernames's itemname;itemtype (red colour for progression)
|
||||||
|
|
||||||
|
# Take item name and truncate if necessary
|
||||||
|
for counter, part in enumerate(all_parts_data_order):
|
||||||
|
location_info = locations_data[get_location_id_for_shop_listing(part)]
|
||||||
|
player_name: str = ctx.player_names[location_info.player]
|
||||||
|
item_name: str = ctx.item_names.lookup_in_slot(location_info.item, location_info.player)
|
||||||
|
item_type_flags: int = location_info.flags
|
||||||
|
item_type: str
|
||||||
|
if item_type_flags & ItemClassification.progression == ItemClassification.progression:
|
||||||
|
item_type = "@1(Progression)"
|
||||||
|
elif item_type_flags & ItemClassification.filler == ItemClassification.filler:
|
||||||
|
item_type = "@0(Filler)"
|
||||||
|
elif item_type_flags & ItemClassification.trap == ItemClassification.trap:
|
||||||
|
item_type = "@0(Trap)"
|
||||||
|
else:
|
||||||
|
item_type = "@0(Useful)"
|
||||||
|
|
||||||
|
# Description
|
||||||
|
|
||||||
|
# Maximum length for a slot name in AP is 16 characters (I'm going to truncate to 16 as well)
|
||||||
|
# Then item name needs to be truncated to 36 - (nameLength+3) length
|
||||||
|
player_name = player_name[:16]
|
||||||
|
item_name_length: int = 36 - (len(player_name) + 3)
|
||||||
|
desc_item_name = item_name[:item_name_length]
|
||||||
|
|
||||||
|
description_top: str = f"~^8{player_name}'s {desc_item_name}"
|
||||||
|
description_bottom: str = f";{item_type}"
|
||||||
|
if len(description_bottom) < 36:
|
||||||
|
repeat: int = 36 - len(description_bottom)
|
||||||
|
name_filler: str = "\0" * repeat
|
||||||
|
description_bottom += name_filler
|
||||||
|
description: str = description_top + description_bottom
|
||||||
|
description_as_hex: typing.List[int] = []
|
||||||
|
for i in range(0, len(description)):
|
||||||
|
description_as_hex.append(description[i].encode('utf-8')[0])
|
||||||
|
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.PARTS_DESCRIPTIONS_OFFSET + (0x4E * counter),
|
||||||
|
description_as_hex,
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.PARTS_TEXT_CHANGE_VERIFY_OFFSET,
|
||||||
|
[first_part_char_check],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
# Take item name and truncate if necessary
|
||||||
|
for counter, part in enumerate(all_parts):
|
||||||
|
location_info = locations_data[get_location_id_for_shop_listing(part)]
|
||||||
|
item_name: str = ctx.item_names.lookup_in_slot(location_info.item, location_info.player)
|
||||||
|
|
||||||
|
# Shop Listing Name
|
||||||
|
listing_name: str = item_name[:12]
|
||||||
|
if len(listing_name) < 12:
|
||||||
|
repeat: int = 12 - len(listing_name)
|
||||||
|
name_filler: str = "\0" * repeat
|
||||||
|
listing_name += name_filler
|
||||||
|
listing_as_hex: typing.List[int] = []
|
||||||
|
for i in range(0, len(listing_name)):
|
||||||
|
listing_as_hex.append(listing_name[i].encode('utf-8')[0])
|
||||||
|
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.PARTS_NAMES_OFFSETS[counter],
|
||||||
|
listing_as_hex,
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.PARTS_TEXT_CHANGE_VERIFY_OFFSET,
|
||||||
|
[first_part_char_check],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
# Update shop item and description text when shopsanity is active
|
||||||
|
async def shopsanity_update_garage_text(self, ctx: "BizHawkClientContext", menu_section) -> None:
|
||||||
|
if menu_section != 0 or ctx.slot_data[Constants.GAME_OPTIONS_KEY]["shopsanity"] == False: # Shop
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if the data needs to be overwritten (Write in a junk value for checking)
|
||||||
|
first_part_char_check: int = int.from_bytes((await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.PARTS_TEXT_CHANGE_VERIFY_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0])
|
||||||
|
|
||||||
|
if first_part_char_check != 0x4A:
|
||||||
|
return
|
||||||
|
|
||||||
|
first_part_char_check = 0x40
|
||||||
|
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.PARTS_TEXT_CHANGE_VERIFY_OFFSET,
|
||||||
|
[first_part_char_check],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
# 12 Character max for new item names
|
||||||
|
|
||||||
|
# Take item name and truncate if necessary
|
||||||
|
for counter, part in enumerate(all_parts):
|
||||||
|
item_name: str = part.name
|
||||||
|
|
||||||
|
# Shop Listing Name
|
||||||
|
listing_name: str = item_name[:12]
|
||||||
|
if len(listing_name) < 12:
|
||||||
|
repeat: int = 12 - len(listing_name)
|
||||||
|
name_filler: str = "\0" * repeat
|
||||||
|
listing_name += name_filler
|
||||||
|
listing_as_hex: typing.List[int] = []
|
||||||
|
for i in range(0, len(listing_name)):
|
||||||
|
listing_as_hex.append(listing_name[i].encode('utf-8')[0])
|
||||||
|
|
||||||
|
await bizhawk.guarded_write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.PARTS_NAMES_OFFSETS[counter],
|
||||||
|
listing_as_hex,
|
||||||
|
MAIN_RAM
|
||||||
|
)],[(
|
||||||
|
Constants.PARTS_TEXT_CHANGE_VERIFY_OFFSET,
|
||||||
|
[first_part_char_check],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
# Store the number of successfully completed missions into story progress (For certain Mail's to appear)
|
||||||
|
async def force_update_mission_count(self, ctx: "BizHawkClientContext", in_menu) -> None:
|
||||||
|
if not in_menu:
|
||||||
|
return []
|
||||||
|
completed_sorties_byte: typing.int = (await bizhawk.read(
|
||||||
|
ctx.bizhawk_ctx, [(Constants.SUCCESSFUL_SORTIES_COUNT_OFFSET, 1, MAIN_RAM)]
|
||||||
|
))[0]
|
||||||
|
await bizhawk.write(ctx.bizhawk_ctx, [(
|
||||||
|
Constants.STORY_PROGRESS_OFFSET,
|
||||||
|
[int.from_bytes(completed_sorties_byte)],
|
||||||
|
MAIN_RAM
|
||||||
|
)])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
||||||
|
if ctx.slot_data is not None:
|
||||||
|
|
||||||
|
if not ctx.finished_game and any((item.item == Constants.VICTORY_ITEM_ID) for item in ctx.items_received):
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "StatusUpdate",
|
||||||
|
"status": ClientStatus.CLIENT_GOAL
|
||||||
|
}])
|
||||||
|
ctx.finished_game = True
|
||||||
|
|
||||||
|
# Find out if we are in the Ravens Nest Menu
|
||||||
|
# Timing matters less on this than it does for the menu_section_check
|
||||||
|
# (which is right before the function that needs it)
|
||||||
|
in_menu: bool = await self.ravens_nest_menu_check(ctx)
|
||||||
|
|
||||||
|
# Handles randomizing a players starting AC
|
||||||
|
await self.randomize_starting_ac(ctx)
|
||||||
|
|
||||||
|
# Blanks the entire shop at the start of the run so it can be properly updated (if shopsanity is on)
|
||||||
|
await self.shopsanity_initialization(ctx, in_menu)
|
||||||
|
|
||||||
|
# Force update a value to properly count completed missions
|
||||||
|
await self.force_update_mission_count(ctx, in_menu)
|
||||||
|
|
||||||
|
# Read mission completion locations
|
||||||
|
completed_missions_flags: typing.List[bool] = await self.read_mission_completion(ctx, in_menu)
|
||||||
|
|
||||||
|
# Read mail read locations
|
||||||
|
read_mail_flags: typing.List[bool] = await self.read_mail_read_flags(ctx, in_menu)
|
||||||
|
|
||||||
|
# Items received handling
|
||||||
|
|
||||||
|
# Find out what ravens nest menu section we're in
|
||||||
|
menu_section: int = await self.ravens_nest_menu_section_check(ctx)
|
||||||
|
# Unlock missions based on what has been received
|
||||||
|
await self.update_mission_list_code(ctx, menu_section)
|
||||||
|
# Update shop listings text if we're in the shop
|
||||||
|
await self.shopsanity_update_shop_text(ctx, menu_section)
|
||||||
|
# Update part names in the Garage (fix them from opening shop)
|
||||||
|
await self.shopsanity_update_garage_text(ctx, menu_section)
|
||||||
|
|
||||||
|
# Credits handling
|
||||||
|
await self.award_credits(ctx, in_menu)
|
||||||
|
|
||||||
|
# Human+ handling
|
||||||
|
await self.award_humanplus(ctx, in_menu)
|
||||||
|
|
||||||
|
# Shopsanity handling
|
||||||
|
await self.award_shop_listings(ctx, completed_missions_flags.count(True), in_menu)
|
||||||
|
|
||||||
|
# Parts handling
|
||||||
|
await self.award_parts(ctx)
|
||||||
|
|
||||||
|
# Local checked checks handling
|
||||||
|
|
||||||
|
new_local_check_locations: typing.Set[int]
|
||||||
|
|
||||||
|
missions_to_completed: typing.Dict[Mission, bool] = {
|
||||||
|
m: c for m, c in zip(all_missions, completed_missions_flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
mail_been_read: typing.Dict[Mail, bool] = {
|
||||||
|
m: c for m, c in zip(all_mail, read_mail_flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
items_purchased: typing.Dict[Part, bool] = await self.check_purchased_items(ctx, completed_missions_flags.count(True), in_menu)
|
||||||
|
|
||||||
|
new_local_check_locations = set([
|
||||||
|
get_location_id_for_mission(key) for key, value in missions_to_completed.items() if value
|
||||||
|
])
|
||||||
|
|
||||||
|
new_local_check_locations = new_local_check_locations.union(set([
|
||||||
|
get_location_id_for_mail(key) for key, value in mail_been_read.items() if value
|
||||||
|
]))
|
||||||
|
|
||||||
|
new_local_check_locations = new_local_check_locations.union(set([
|
||||||
|
get_location_id_for_shop_listing(key) for key, value in items_purchased.items()
|
||||||
|
]))
|
||||||
|
|
||||||
|
# Award game completion if in missionsanity mode and you've reached the mission goal threshold
|
||||||
|
if ctx.slot_data[Constants.GAME_OPTIONS_KEY]["goal"] == 0: # Missionsanity
|
||||||
|
if completed_missions_flags.count(True) == ctx.slot_data[Constants.GAME_OPTIONS_KEY]["missionsanity_goal_requirement"] + 1:
|
||||||
|
new_local_check_locations.add(Constants.VICTORY_LOCATION_ID)
|
||||||
|
|
||||||
|
if new_local_check_locations != self.local_checked_locations:
|
||||||
|
self.local_checked_locations = new_local_check_locations
|
||||||
|
if new_local_check_locations is not None:
|
||||||
|
await ctx.send_msgs([{
|
||||||
|
"cmd": "LocationChecks",
|
||||||
|
"locations": list(new_local_check_locations)
|
||||||
|
}])
|
||||||
31
worlds/armoredcore/docs/en_Armored Core.md
Normal file
31
worlds/armoredcore/docs/en_Armored Core.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Armored Core
|
||||||
|
|
||||||
|
## What does randomization do to this game?
|
||||||
|
|
||||||
|
Mission order, acquiring Parts, starting AC, and receiving bonus Human+ levels and Credits are all randomized.
|
||||||
|
|
||||||
|
## What is the goal of Armored Core when randomized?
|
||||||
|
|
||||||
|
In Progressive Missions mode, complete Destroy Floating Mines.
|
||||||
|
In Mission-Sanity mode, complete a number of missions you can set between 1 and 46.
|
||||||
|
|
||||||
|
## What items and locations get shuffled?
|
||||||
|
|
||||||
|
Each mission completed is a location where you can find a check.
|
||||||
|
Each piece of Mail read is a location.
|
||||||
|
With Shopsanity on, each part listing can be purchased for items separate from the ones you already own.
|
||||||
|
|
||||||
|
## Which items can be in another player's world?
|
||||||
|
|
||||||
|
Missions and Progressive Missions depending on how you are playing, as well as filler (Credits, Human+ Levels, Parts, etc).
|
||||||
|
|
||||||
|
## What does another world's item look like in Armored Core?
|
||||||
|
|
||||||
|
Your item acquisitions take the form of having completed a mission, reading mail, or buying a Part from the shop.
|
||||||
|
You'll have to check the BizHawk client to know the actual items that are unlocked from non-shop locations.
|
||||||
|
Note that mission completion isn't checked until you have re-entered the Raven's Nest menu.
|
||||||
|
|
||||||
|
## When the player receives an item, what happens?
|
||||||
|
|
||||||
|
In the Raven's Nest menu the client will update the Mission list with what you have access to. If you are currently in the Mission list when sent a mission, you have to exit and re-enter for it to update.
|
||||||
|
Parts can be equipped in the Garage when sent to you. If you are already in the Garage you will have to exit and re-enter.
|
||||||
65
worlds/armoredcore/docs/setup_en.md
Normal file
65
worlds/armoredcore/docs/setup_en.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Armored Core Setup Guide
|
||||||
|
|
||||||
|
## Required Software
|
||||||
|
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Please use version 0.4.4 or later for integrated
|
||||||
|
BizHawk support.
|
||||||
|
- Armored Core .bin rom.
|
||||||
|
- The MD5 hash of the rom I used is 34A3F5EA63AC6A6E9753E5FD38425D7E
|
||||||
|
- Make sure to launch a "New Game" for each seed you play.
|
||||||
|
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later. Other emulators are not supported.
|
||||||
|
- The latest `armoredcore.apworld` file. You can find this on the [Releases page](https://github.com/JustinMarshall98/Armored-Core-PSX-Archipelago/releases). Put this in your `Archipelago/lib/worlds` folder.
|
||||||
|
|
||||||
|
### Configuring BizHawk
|
||||||
|
|
||||||
|
Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
|
||||||
|
|
||||||
|
- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
|
||||||
|
`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
|
||||||
|
- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
|
||||||
|
tabbed out of EmuHawk.
|
||||||
|
- Open any Playstation 1 game in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
|
||||||
|
`Controllers…`, it's because you need to load a game first.
|
||||||
|
- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to
|
||||||
|
clear it.
|
||||||
|
|
||||||
|
## Generating a Game
|
||||||
|
|
||||||
|
1. Create your options file (YAML). After installing the `armoredcore.apworld` file, you can generate a template within the Archipelago Launcher by clicking `Generate Template Settings`.
|
||||||
|
2. Follow the general Archipelago instructions for [generating a game](https://archipelago.gg/tutorial/Archipelago/setup/en#generating-a-game).
|
||||||
|
3. Open `ArchipelagoLauncher.exe`
|
||||||
|
4. Select "BizHawk Client" in the right-side column. On your first time opening BizHawk Client, you will also be asked to
|
||||||
|
locate `EmuHawk.exe` in your BizHawk install.
|
||||||
|
|
||||||
|
## Connecting to a Server
|
||||||
|
|
||||||
|
1. If EmuHawk didn't launch automatically, open it manually.
|
||||||
|
2. Open your Armored Core .bin file in EmuHawk.
|
||||||
|
3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing. Be careful to avoid clicking "TAStudio" below it in the menu, as this is known to delete your savefile.
|
||||||
|
5. In the Lua Console window, go to `Script > Open Script…`.
|
||||||
|
6. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
|
||||||
|
7. The emulator and client will eventually connect to each other. The BizHawk Client window should indicate that it
|
||||||
|
connected and recognized Armored Core (it should do so once the title screen is reached at the earliest).
|
||||||
|
8. 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.
|
||||||
|
|
||||||
|
You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect.
|
||||||
|
It is possible to make progress offline if you're already loaded into a mission when you drop your connection,
|
||||||
|
but the mission menu restructuring code requires a connection to the Archipelago Bizhawk Client.
|
||||||
|
The shop menu when Shopsanity is on also requires a connection to the Bizhawk Client to function properly,
|
||||||
|
you may notice oddities with Part names and descriptions if your connection is lost.
|
||||||
|
Reconnecting to the server after completing a mission will still send its check out.
|
||||||
|
|
||||||
|
## Notes and Limitations
|
||||||
|
|
||||||
|
1. As mentioned above, the mission menu and shop restructuring code require a connection to the client and a connection to the server.
|
||||||
|
2. Checks are sent and received only in the Raven's Nest menu.
|
||||||
|
3. Missions that are not unlocked yet currently appear in the mission list as Dummy00.
|
||||||
|
This mission is the Raven Test, you won't get anything for completing it again.
|
||||||
|
4. The Missions Menu / Shop Menu (when shopsanity is on) sometimes need a moment to structure themselves properly, hover over it for a second
|
||||||
|
before entering or exit and re-enter to get the properly constructed mission selection options.
|
||||||
|
Failing to do so for the Mission Menu can rarely result in a crash.
|
||||||
|
5. When Shopsanity is on, Hidden Parts can be obtained either by someone sending the part to you or picking it up in the level its in.
|
||||||
|
Once the Hidden Part appears in the shop as a check to be sold however, it will no longer appear in its normal level anymore.
|
||||||
|
6. If you try to start yourself with a part from your pool and you have randomized starting ac on, if you luck into starting with that part your world may fail to generate properly.
|
||||||
|
Try generating again until this doesn't happen. (The same is true for trying to start yourself off with one of the normal starting parts if your starting ac is not randomized).
|
||||||
|
7. In some cases, selling an equipped optional part will keep it equipped until you open the optional parts menu.
|
||||||
46
worlds/armoredcore/items.py
Normal file
46
worlds/armoredcore/items.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
from BaseClasses import Item, ItemClassification
|
||||||
|
from .utils import Constants
|
||||||
|
from .mission import all_missions, name_to_mission
|
||||||
|
from .parts import all_parts, name_to_part
|
||||||
|
|
||||||
|
item_id_to_item_name: typing.Dict[int, str] = {}
|
||||||
|
|
||||||
|
# Mission unlock item ID's are the mission ID + mission completion offset
|
||||||
|
for mission in all_missions:
|
||||||
|
item_id_to_item_name[mission.id + Constants.MISSION_COMPLETION_OFFSET] = mission.name
|
||||||
|
|
||||||
|
# Victory Item
|
||||||
|
item_id_to_item_name[Constants.VICTORY_ITEM_ID] = Constants.VICTORY_ITEM_NAME
|
||||||
|
|
||||||
|
# Progressive Missions
|
||||||
|
item_id_to_item_name[Constants.PROGRESSIVE_MISSION_ITEM_ID] = Constants.PROGRESSIVE_MISSION_ITEM_NAME
|
||||||
|
|
||||||
|
# Credit
|
||||||
|
item_id_to_item_name[Constants.CREDIT_ITEM_ID] = Constants.CREDIT_ITEM_NAME
|
||||||
|
|
||||||
|
# Progressive Human+
|
||||||
|
item_id_to_item_name[Constants.PROGRESSIVE_HUMANPLUS_ITEM_ID] = Constants.PROGRESSIVE_HUMANPLUS_ITEM_NAME
|
||||||
|
|
||||||
|
# Parts
|
||||||
|
for part in all_parts:
|
||||||
|
if part.name != "DUMMY" and part.name != "NO WEAPON":
|
||||||
|
item_id_to_item_name[part.id + Constants.PARTS_INVENTORY_OFFSET] = part.name #f"{part}"
|
||||||
|
|
||||||
|
# Reverse item_id_to_item_name
|
||||||
|
item_name_to_item_id: typing.Dict[str, int] = {value: key for key, value in item_id_to_item_name.items()}
|
||||||
|
|
||||||
|
class ACItem(Item):
|
||||||
|
game: str = Constants.GAME_NAME
|
||||||
|
|
||||||
|
def create_item(name: str, player_id: int) -> ACItem:
|
||||||
|
return ACItem(name, ItemClassification.progression if (name in name_to_mission or
|
||||||
|
name is Constants.PROGRESSIVE_MISSION_ITEM_NAME)
|
||||||
|
else ItemClassification.filler, item_name_to_item_id[name], player_id)
|
||||||
|
|
||||||
|
def create_victory_event(player_id: int) -> ACItem:
|
||||||
|
return ACItem(Constants.VICTORY_ITEM_NAME, ItemClassification.progression, Constants.VICTORY_ITEM_ID, player_id)
|
||||||
|
|
||||||
|
def convert_item_id_to_mission_id(item_id: int) -> int:
|
||||||
|
return item_id - Constants.MISSION_COMPLETION_OFFSET
|
||||||
97
worlds/armoredcore/locations.py
Normal file
97
worlds/armoredcore/locations.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
from BaseClasses import Location, Region, LocationProgressType, Item
|
||||||
|
from .mission import Mission, all_missions, id_to_mission
|
||||||
|
from .utils import Constants
|
||||||
|
from .mail import Mail, all_mail, id_to_mail
|
||||||
|
from .parts import Part, all_parts
|
||||||
|
|
||||||
|
def get_location_name_for_mission(mission: Mission) -> str:
|
||||||
|
return f"{mission.name} Completed"
|
||||||
|
|
||||||
|
def get_location_id_for_mission_id(mission_id: int) -> int:
|
||||||
|
return Constants.MISSION_COMPLETION_OFFSET + mission_id
|
||||||
|
|
||||||
|
def get_location_id_for_mission(mission: Mission) -> int:
|
||||||
|
return get_location_id_for_mission_id(mission.id)
|
||||||
|
|
||||||
|
def is_mission_location_id(location_id: int) -> bool:
|
||||||
|
return (location_id - Constants.MISSION_COMPLETION_OFFSET) in id_to_mission
|
||||||
|
|
||||||
|
def mission_from_location_id(location_id: int) -> Mission:
|
||||||
|
return id_to_mission[location_id - Constants.MISSION_COMPLETION_OFFSET]
|
||||||
|
|
||||||
|
|
||||||
|
def get_location_name_for_mail(mail: Mail) -> str:
|
||||||
|
return f"Mail - {mail.name}"
|
||||||
|
|
||||||
|
def get_location_id_for_mail_id(mail_id: int) -> int:
|
||||||
|
return Constants.MAIL_RECEPTION_OFFSET + mail_id
|
||||||
|
|
||||||
|
def get_location_id_for_mail(mail: Mail) -> int:
|
||||||
|
return get_location_id_for_mail_id(mail.id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_location_name_for_shop_listing(part: Part) -> str:
|
||||||
|
return f"Shop - {part.name}"
|
||||||
|
|
||||||
|
def get_location_id_for_shop_listing_id(part_id: int) -> int:
|
||||||
|
return Constants.SHOP_INVENTORY_OFFSET + part_id
|
||||||
|
|
||||||
|
def get_location_id_for_shop_listing(part: Part) -> int:
|
||||||
|
return get_location_id_for_shop_listing_id(part.id)
|
||||||
|
|
||||||
|
|
||||||
|
class ACLocation(Location):
|
||||||
|
game: str
|
||||||
|
|
||||||
|
def __init__(self, region: Region, player: int, name: str, id: int):
|
||||||
|
super().__init__(player, name, parent=region)
|
||||||
|
self.game = Constants.GAME_NAME
|
||||||
|
self.address = id
|
||||||
|
|
||||||
|
def exclude(self) -> None:
|
||||||
|
self.progress_type = LocationProgressType.EXCLUDED
|
||||||
|
|
||||||
|
def place(self, item: Item) -> None:
|
||||||
|
self.item = item
|
||||||
|
item.location = self
|
||||||
|
|
||||||
|
class MissionLocation(ACLocation):
|
||||||
|
# Location for mission completion
|
||||||
|
mission: Mission
|
||||||
|
|
||||||
|
def __init__(self, region: Region, player: int, mission: Mission):
|
||||||
|
super().__init__(region, player, get_location_name_for_mission(mission), get_location_id_for_mission(mission))
|
||||||
|
self.mission = mission
|
||||||
|
|
||||||
|
mission_location_name_to_id: typing.Dict[str, int] = {}
|
||||||
|
for mission in all_missions:
|
||||||
|
mission_location_name_to_id[get_location_name_for_mission(mission)] = get_location_id_for_mission(mission)
|
||||||
|
|
||||||
|
class MailLocation(ACLocation):
|
||||||
|
# Location for having read Mail
|
||||||
|
mail: Mail
|
||||||
|
def __init__(self, region: Region, player: int, mail: Mail):
|
||||||
|
super().__init__(region, player, get_location_name_for_mail(mail), get_location_id_for_mail(mail))
|
||||||
|
self.mail = mail
|
||||||
|
|
||||||
|
mail_location_name_to_id: typing.Dict[str, int] = {}
|
||||||
|
for mail in all_mail:
|
||||||
|
mail_location_name_to_id[get_location_name_for_mail(mail)] = get_location_id_for_mail(mail)
|
||||||
|
|
||||||
|
class ShopLocation(ACLocation):
|
||||||
|
# Location for purchasing something from the Shop (the shop listing based on a given part)
|
||||||
|
part: Part
|
||||||
|
def __init__(self, region: Region, player: int, part: Part):
|
||||||
|
super().__init__(region, player, get_location_name_for_shop_listing(part), get_location_id_for_shop_listing(part))
|
||||||
|
self.part = part
|
||||||
|
|
||||||
|
shop_listing_location_name_to_id: typing.Dict[str, int] = {}
|
||||||
|
for part in all_parts:
|
||||||
|
shop_listing_location_name_to_id[get_location_name_for_shop_listing(part)] = get_location_id_for_shop_listing(part)
|
||||||
|
|
||||||
|
victory_location_name_to_id: typing.Dict[str, int] = {}
|
||||||
|
victory_location_name_to_id[Constants.VICTORY_LOCATION_NAME] = Constants.VICTORY_LOCATION_ID
|
||||||
|
|
||||||
|
location_name_to_id: typing.Dict[str, int] = {**mission_location_name_to_id, **mail_location_name_to_id, **shop_listing_location_name_to_id, **victory_location_name_to_id}
|
||||||
46
worlds/armoredcore/mail.py
Normal file
46
worlds/armoredcore/mail.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
class Mail:
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
mission_unlock_id: int # Value is -1 if it is a special case (not tied to mission ID)
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, unlock_id: int):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.mission_unlock_id = unlock_id
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"{self.name} "
|
||||||
|
)
|
||||||
|
|
||||||
|
all_mail: typing.Tuple[Mail, ...] = (
|
||||||
|
Mail(0x0, "New Parts Added (1)", -1), # After doing 10 Missions
|
||||||
|
Mail(0x1, "New Parts Added (2)", -1), # After doing 20 Missions
|
||||||
|
Mail(0x2, "Human Plus", -1), # After doing 13 Missions
|
||||||
|
Mail(0x3, "To All New Ravens", 0x0), # Raven Test
|
||||||
|
Mail(0x4, "Reclaiming the Facility", 0x25), # Reclaim Oil Facility
|
||||||
|
Mail(0x5, "Struggle", 0x25), # Reclaim Oil Facility
|
||||||
|
Mail(0x6, "Terrorists in the Shadows", 0x1f), # Eliminate Squatters (2)
|
||||||
|
Mail(0x7, "Chrome and Murakumo", 0x1f), # Eliminate Squatters (2)
|
||||||
|
Mail(0x8, "Thanks", 0x2), # Remove Gun Emplacement
|
||||||
|
Mail(0x9, "Ranking Raven", 0x1c), # Attack Urban Center
|
||||||
|
Mail(0xa, "EERC", 0x3), # Rescue Survey Team
|
||||||
|
Mail(0xb, "Chemical-Dyne Co", 0x9), # Destroy Fuel Depot
|
||||||
|
Mail(0xc, "The Red AC", 0x8), # Guard Freight Train
|
||||||
|
Mail(0xd, "Factory Assault Report", 0x6), # Secret Factory Recon
|
||||||
|
Mail(0xe, "Factory Affair Report", 0x28), # Guard Factory Entrance
|
||||||
|
Mail(0xf, "Giant Organisms", 0x7), # Exterminate Organisms (1)
|
||||||
|
Mail(0x10, "Plus", 0x18), # Destroy Plus Escapee
|
||||||
|
Mail(0x11, "Relics of the Past", 0x26), # Recover Capsules
|
||||||
|
# Mail(0x12, "Above Ground", ???), # Unknown requirement? Seems to appear after Guard Airplane on Chrome route? Might be tied to path progression
|
||||||
|
Mail(0x13, "Struggle's Demise", 0x2c), # Kill "Struggle" Leader
|
||||||
|
Mail(0x14, "???", 0x12), # Remove Base Occupants
|
||||||
|
Mail(0x15, "Biological Weapons", 0x2a), # Release Organisms
|
||||||
|
Mail(0x16, "Imminent Storm's Demise", 0x2b) # Retake Air Cleaner
|
||||||
|
)
|
||||||
|
|
||||||
|
id_to_mail = {mail.id: mail for mail in all_mail}
|
||||||
|
name_to_mail = {mail.name: mail for mail in all_mail}
|
||||||
82
worlds/armoredcore/mission.py
Normal file
82
worlds/armoredcore/mission.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
class Mission:
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
progression_level: int
|
||||||
|
awards_credits: bool
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, progression_level: int, awards_credits: bool):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.progression_level = progression_level
|
||||||
|
self.awards_credits = awards_credits
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"{self.name} "
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unused missions (outside of Dummy00) are omitted from this list
|
||||||
|
all_missions: typing.Tuple[Mission, ...] = (
|
||||||
|
Mission(0x0, "Raven Test", 0, False), # Named Dummy00 in game, name changed for player clarity
|
||||||
|
Mission(0x1, "Stop Terrorist Threat", 2, True),
|
||||||
|
Mission(0x2, "Remove Gun Emplacement", 2, False),
|
||||||
|
Mission(0x3, "Rescue Survey Team", 3, True),
|
||||||
|
Mission(0x4, "Terrorist Pursuit", 3, True),
|
||||||
|
Mission(0x5, "Worker Robot Removal", 2, True),
|
||||||
|
Mission(0x6, "Secret Factory Recon", 4, True),
|
||||||
|
Mission(0x7, "Exterminate Organisms(1)", 5, True),
|
||||||
|
Mission(0x8, "Guard Freight Train", 3, True),
|
||||||
|
Mission(0x9, "Destroy Fuel Depot", 3, True),
|
||||||
|
Mission(0xa, "Prototype MT Test(1)", 4, True),
|
||||||
|
Mission(0xb, "Guard Airplane", 6, True),
|
||||||
|
Mission(0xc, "Stop Gas Exposure", 7, True),
|
||||||
|
Mission(0xd, "Prototype MT Test(2)", 6, False),
|
||||||
|
Mission(0xe, "Repulse Enemy Attack", 5, True),
|
||||||
|
Mission(0xf, "Exterminate Organisms(2)", 7, True),
|
||||||
|
# Mission(0x10, "16 Omitted", 0),
|
||||||
|
Mission(0x11, "Guard Wharf Warehouse", 4, True),
|
||||||
|
Mission(0x12, "Remove Base Occupants", 7, True),
|
||||||
|
Mission(0x13, "Destroy Space Catapult", 8, True),
|
||||||
|
Mission(0x14, "Destroy Base Generator", 8, True),
|
||||||
|
Mission(0x15, "Mop Up Chrome Remnants(1)", 9, True),
|
||||||
|
Mission(0x16, "Destroy \"Justice\"", 9, True),
|
||||||
|
Mission(0x17, "Chrome Uprising", 9, True),
|
||||||
|
Mission(0x18, "Destroy Plus Escapee", 5, True),
|
||||||
|
Mission(0x19, "Destroy Intruders", 8, True),
|
||||||
|
Mission(0x1a, "Destroy Plane Computer", 6, True),
|
||||||
|
Mission(0x1b, "AC Battle(1)", 6, True),
|
||||||
|
Mission(0x1c, "Attack Urban Center", 3, True),
|
||||||
|
# Mission(0x1d, "29 Omitted", 0),
|
||||||
|
Mission(0x1e, "Eliminate Squatters(1)", 1, True),
|
||||||
|
Mission(0x1f, "Eliminate Squatters(2)", 2, True),
|
||||||
|
Mission(0x20, "Destroy Unknown MTs", 1, True),
|
||||||
|
Mission(0x21, "Rescue Transport Truck", 2, True),
|
||||||
|
Mission(0x22, "Eliminate Strikers", 1, True),
|
||||||
|
Mission(0x23, "Stop Security MTs", 1, True),
|
||||||
|
Mission(0x24, "Stop Gang, \"Dark Soul\"", 4, False),
|
||||||
|
Mission(0x25, "Reclaim Oil Facility", 1, True),
|
||||||
|
Mission(0x26, "Recover Capsules", 5, True),
|
||||||
|
# Mission(0x27, "39 Omitted", 0),
|
||||||
|
Mission(0x28, "Guard Factory Entrance", 4, True),
|
||||||
|
Mission(0x29, "Capture Space Station", 8, True),
|
||||||
|
Mission(0x2a, "Release Organisms", 7, True),
|
||||||
|
Mission(0x2b, "Retake Air Cleaner", 8, True),
|
||||||
|
Mission(0x2c, "Kill \"Struggle\" Leader", 7, True),
|
||||||
|
Mission(0x2d, "Stop Security MT", 5, True),
|
||||||
|
Mission(0x2e, "Destroy Base Computer", 6, True),
|
||||||
|
Mission(0x2f, "Mop Up Chrome Remnants(2)", 9, True),
|
||||||
|
Mission(0x30, "Destroy Floating Mines", 10, True),
|
||||||
|
# Mission(0x31, "Destruction of Ravens", 10),
|
||||||
|
Mission(0x32, "AC Battle(2)", 9, True)
|
||||||
|
# Mission(0x33, "~~~~~~~~~", 0) etc
|
||||||
|
)
|
||||||
|
|
||||||
|
STARTING_MISSION = all_missions[0]
|
||||||
|
DESTROY_FLOATING_MINES = all_missions[-2]
|
||||||
|
|
||||||
|
missions_that_award_credits: typing.Tuple[Mission, ...] = {mission for mission in all_missions if mission.awards_credits}
|
||||||
|
|
||||||
|
id_to_mission = {mission.id: mission for mission in all_missions}
|
||||||
|
name_to_mission = {mission.name: mission for mission in all_missions}
|
||||||
83
worlds/armoredcore/options.py
Normal file
83
worlds/armoredcore/options.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import typing
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
from Options import Range, Choice, PerGameCommonOptions, DefaultOnToggle, Toggle as DefaultOffToggle
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
class Goal(Choice):
|
||||||
|
"""
|
||||||
|
Choose what you want your goal to be.
|
||||||
|
In missionsanity all missions are individually added to the pool of checks,
|
||||||
|
you set the number of missions that you must complete in order to complete your goal.
|
||||||
|
In progressive missions you receive 'progressive mission' items that unlock groups of
|
||||||
|
5 missions at a time. Your goal is completing Destroy Floating Mines after collecting
|
||||||
|
all 'progressive mission' items.
|
||||||
|
"""
|
||||||
|
display_name = "Goal"
|
||||||
|
option_missionsanity = 0
|
||||||
|
option_progressive_missions = 1
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
class MissionsanityGoalRequirement(Range):
|
||||||
|
"""
|
||||||
|
This option only matters if your Goal is Missionsanity.
|
||||||
|
Select how many missions it takes to complete your goal.
|
||||||
|
Does not include the tutorial mission.
|
||||||
|
"""
|
||||||
|
display_name = "Missionsanity Goal Requirement"
|
||||||
|
range_start = 1
|
||||||
|
range_end = 46
|
||||||
|
default = 46
|
||||||
|
|
||||||
|
class IncludeHumanPlusFiller(DefaultOnToggle):
|
||||||
|
"""
|
||||||
|
If this option is on, three "Progressive Human+"
|
||||||
|
Enhancement levels will be added as filler to the item pool.
|
||||||
|
"""
|
||||||
|
display_name = "Include Human+ Filler"
|
||||||
|
|
||||||
|
class Shopsanity(DefaultOffToggle):
|
||||||
|
"""
|
||||||
|
Shopsanity turns all parts listings in the shop into locations,
|
||||||
|
and all parts that you don't start with are shuffled into the multiworld.
|
||||||
|
"""
|
||||||
|
display_name = "Shopsanity"
|
||||||
|
|
||||||
|
class ShopsanityListingsPerMission(Range):
|
||||||
|
"""
|
||||||
|
Define how many shop listings open up per mission completion.
|
||||||
|
Higher numbers may require more grinding. Includes Raven Test.
|
||||||
|
"""
|
||||||
|
display_name = "Shopsanity Listings Per Mission"
|
||||||
|
range_start = 4
|
||||||
|
range_end = 146
|
||||||
|
default = 4
|
||||||
|
|
||||||
|
class RandomizeStartingParts(DefaultOffToggle):
|
||||||
|
"""
|
||||||
|
Your starting AC Parts will be randomized but still
|
||||||
|
adhere to weight and energy limits.
|
||||||
|
"""
|
||||||
|
display_name = "Randomize Starting Mech"
|
||||||
|
|
||||||
|
class CreditCheckAmount(Range):
|
||||||
|
"""
|
||||||
|
Define how much you earn from Credit Filler checks you receieve.
|
||||||
|
"""
|
||||||
|
display_name = "Credit Check Amount"
|
||||||
|
range_start = 100
|
||||||
|
range_end = 100000
|
||||||
|
default = 10000
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ACOptions(PerGameCommonOptions):
|
||||||
|
goal: Goal
|
||||||
|
missionsanity_goal_requirement: MissionsanityGoalRequirement
|
||||||
|
include_humanplus: IncludeHumanPlusFiller
|
||||||
|
shopsanity: Shopsanity
|
||||||
|
shopsanity_listings_per_mission: ShopsanityListingsPerMission
|
||||||
|
rando_start_parts: RandomizeStartingParts
|
||||||
|
credit_check_amount: CreditCheckAmount
|
||||||
|
|
||||||
|
def serialize(self) -> typing.Dict[str, int]:
|
||||||
|
return {field.name: getattr(self, field.name).value for field in dataclasses.fields(self)}
|
||||||
597
worlds/armoredcore/parts.py
Normal file
597
worlds/armoredcore/parts.py
Normal file
@@ -0,0 +1,597 @@
|
|||||||
|
# Data ripped by Vartazian
|
||||||
|
import typing
|
||||||
|
|
||||||
|
class Part:
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
part_type: str
|
||||||
|
price: int
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, part_type: str, price: int):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.part_type = part_type
|
||||||
|
self.price = price
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"{self.part_type} - {self.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# For parts that do not have specific parameters, -1 if its an int and they don't have it, "" if its a string and they don't have it
|
||||||
|
# Could pull data right from game... But also I feel like this data will be nice to have documented for posterity
|
||||||
|
# Tier subjective, balancing by Vartazian! Tier default is 1, tier 5 is made for excluding parts from being started with
|
||||||
|
|
||||||
|
class Head(Part):
|
||||||
|
weight: int
|
||||||
|
energy_drain: int
|
||||||
|
armor_point: int
|
||||||
|
def_shell: int
|
||||||
|
def_energy: int
|
||||||
|
computer_type: str
|
||||||
|
map_type: str
|
||||||
|
noise_canceler: str
|
||||||
|
bio_sensor: str
|
||||||
|
radar_function: str
|
||||||
|
radar_range: str
|
||||||
|
radar_type: str
|
||||||
|
tier: int
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, part_type: str, price: int,
|
||||||
|
weight: int, energy_drain: int, armor_point: int,
|
||||||
|
def_shell: int, def_energy: int, computer_type: str,
|
||||||
|
map_type: str, noise_canceler: str, bio_sensor: str,
|
||||||
|
radar_function: str, radar_range: int = -1, radar_type: str = "", tier: int = 1):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.part_type = part_type
|
||||||
|
self.price = price
|
||||||
|
self.weight = weight
|
||||||
|
self.energy_drain = energy_drain
|
||||||
|
self.armor_point = armor_point
|
||||||
|
self.def_shell = def_shell
|
||||||
|
self.def_energy = def_energy
|
||||||
|
self.computer_type = computer_type
|
||||||
|
self.map_type = map_type
|
||||||
|
self.noise_canceler = noise_canceler
|
||||||
|
self.bio_sensor = bio_sensor
|
||||||
|
self.radar_function = radar_function
|
||||||
|
self.radar_range = radar_range
|
||||||
|
self.radar_type = radar_type
|
||||||
|
self.tier = tier # Tier 1 lowest, Tier 4 highest
|
||||||
|
|
||||||
|
all_heads: typing.Tuple[Head, ...] = (
|
||||||
|
Head(0x00, "HD-01-SRVT", "HEAD UNIT", 26500, 122, 350, 816, 154, 149, "DETAILED", "AREA MEMORY", "NONE", "PROVIDED", "NONE", tier = 1), #Head unit with built-in bio sensor.
|
||||||
|
Head(0x01, "HD-2002", "HEAD UNIT", 29000, 156, 457, 787, 140, 154, "STANDARD", "AREA MEMORY", "NONE", "NONE", "PROVIDED", 6000, "STANDARD", tier = 2), #Head unit equipped with radar function.
|
||||||
|
Head(0x02, "HD-X1487", "HEAD UNIT", 19000, 166, 420, 975, 160, 185, "ROUGH", "NO MEMORY", "PROVIDED", "PROVIDED", "NONE", tier = 1), #Full range of sensors but without the auto-map function.
|
||||||
|
Head(0x03, "HD-REDEYE", "HEAD UNIT", 41100, 146, 538, 840, 148, 151, "DETAILED", "AREA&PLACE NAME", "NONE", "NONE", "PROVIDED", 5980, "STANDARD", tier = 3), #Equipped with radar and an enhanced auto-map function.
|
||||||
|
Head(0x04, "HS-D-9066", "HEAD UNIT", 43200, 138, 657, 885, 165, 232, "STANDARD", "AREA MEMORY", "NONE", "PROVIDED", "PROVIDED", 6120, "STANDARD", tier = 2), #Full range of options and good EG shields.
|
||||||
|
Head(0x05, "HD-GRY-NX", "HEAD UNIT", 14700, 232, 218, 1001, 194, 134, "ROUGH", "NO MEMORY", "NONE", "NONE", "NONE", tier = 1), #Economy Unit with good shields but no optional equipment.
|
||||||
|
Head(0x06, "HD-06-RADAR", "HEAD UNIT", 51800, 145, 875, 741, 109, 194, "STANDARD", "AREA&PLACE NAME", "PROVIDED", "NONE", "PROVIDED", 8120, "STANDARD", tier = 3), #Equiped with wide-area radar and various options.
|
||||||
|
Head(0x07, "HD-ONE", "HEAD UNIT", 68100, 161, 304, 800, 132, 129, "DETAILED", "AREA MEMORY", "PROVIDED", "PROVIDED", "PROVIDED", 7980, "STANDARD", tier = 4), #Fully equipped with wide-area radar and all options.
|
||||||
|
Head(0x08, "HD-08-DISH", "HEAD UNIT", 33200, 133, 716, 870, 205, 162, "STANDARD", "AREA&PLACE NAME", "NONE", "NONE", "NONE", tier = 2), #Equipped with an enhanced auto-map function.
|
||||||
|
Head(0x09, "HD-ZERO", "HEAD UNIT", 22500, 185, 431, 925, 221, 149, "ROUGH", "NO MEMORY", "NONE", "NONE", "PROVIDED", 6300, "STANDARD", tier = 3), #Equipped with radar functions and enhanced shock protection.
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
heads_by_tier = {
|
||||||
|
1: [head for head in all_heads if head.tier == 1],
|
||||||
|
2: [head for head in all_heads if head.tier == 2],
|
||||||
|
3: [head for head in all_heads if head.tier == 3],
|
||||||
|
4: [head for head in all_heads if head.tier == 4]
|
||||||
|
}
|
||||||
|
|
||||||
|
class Core(Part):
|
||||||
|
weight: int
|
||||||
|
energy_drain: int
|
||||||
|
armor_point: int
|
||||||
|
def_shell: int
|
||||||
|
def_energy: int
|
||||||
|
max_weight: int
|
||||||
|
anti_missile_response: int
|
||||||
|
anti_missile_angle: int
|
||||||
|
extension_slots: int
|
||||||
|
tier: int
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, part_type: str, price: int,
|
||||||
|
weight: int, energy_drain: int, armor_point: int,
|
||||||
|
def_shell: int, def_energy: int, max_weight: int,
|
||||||
|
anti_missile_response: int, anti_missile_angle: int,
|
||||||
|
extension_slots: int, tier: int = 1):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.part_type = part_type
|
||||||
|
self.price = price
|
||||||
|
self.weight = weight
|
||||||
|
self.energy_drain = energy_drain
|
||||||
|
self.armor_point = armor_point
|
||||||
|
self.def_shell = def_shell
|
||||||
|
self.def_energy = def_energy
|
||||||
|
self.max_weight = max_weight
|
||||||
|
self.anti_missile_response = anti_missile_response
|
||||||
|
self.anti_missile_angle = anti_missile_angle
|
||||||
|
self.extension_slots = extension_slots
|
||||||
|
self.tier = tier
|
||||||
|
|
||||||
|
all_cores: typing.Tuple[Core, ...] = (
|
||||||
|
Core(0x0A, "XCA-00", "CORE UNIT", 61500, 1103, 1046, 2710, 530, 505, 2770, 48, 48, 8, tier = 1), #Standard core unit with average performance overall.
|
||||||
|
Core(0x0B, "XCL-01", "CORE UNIT", 88000, 885, 1380, 2380, 492, 610, 2450, 45, 64, 16, tier = 2), #Electronic warfare core with many slots for special equipment.
|
||||||
|
Core(0x0C, "XCH-01", "CORE UNIT", 72000, 1384, 873, 3015, 615, 543, 3600, 48, 32, 12, tier = 2), #Heavyweight core with an excellent shoulder load and heavy armor.
|
||||||
|
)
|
||||||
|
|
||||||
|
cores_by_tier = {
|
||||||
|
1: [Cores for Cores in all_cores if Cores.tier == 1],
|
||||||
|
2: [Cores for Cores in all_cores if Cores.tier == 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
class Arms(Part):
|
||||||
|
weight: int
|
||||||
|
energy_drain: int
|
||||||
|
armor_point: int
|
||||||
|
def_shell: int
|
||||||
|
def_energy: int
|
||||||
|
weapon_lock: str
|
||||||
|
attack_power: int
|
||||||
|
number_of_ammo: int
|
||||||
|
ammo_type: str
|
||||||
|
ammo_price: int
|
||||||
|
arms_range: int
|
||||||
|
maximum_lock: int
|
||||||
|
reload_time: int
|
||||||
|
tier: int
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, part_type: str, price: int,
|
||||||
|
weight: int, energy_drain: int, armor_point: int,
|
||||||
|
def_shell: int, def_energy: int, weapon_lock: str = "",
|
||||||
|
attack_power: int = -1, number_of_ammo: int = -1,
|
||||||
|
ammo_type: str = "", ammo_price: int = -1, arms_range: int = -1,
|
||||||
|
maximum_lock: int = -1, reload_time: int = -1, tier: int = 1):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.part_type = part_type
|
||||||
|
self.price = price
|
||||||
|
self.weight = weight
|
||||||
|
self.energy_drain = energy_drain
|
||||||
|
self.armor_point = armor_point
|
||||||
|
self.def_shell = def_shell
|
||||||
|
self.def_energy = def_energy
|
||||||
|
self.weapon_lock = weapon_lock
|
||||||
|
self.attack_power = attack_power
|
||||||
|
self.number_of_ammo = number_of_ammo
|
||||||
|
self.ammo_type = ammo_type
|
||||||
|
self.ammo_price = ammo_price
|
||||||
|
self.arms_range = arms_range
|
||||||
|
self.maximum_lock = maximum_lock
|
||||||
|
self.reload_time = reload_time
|
||||||
|
self.tier = tier
|
||||||
|
|
||||||
|
all_arms: typing.Tuple[Arms, ...] = (
|
||||||
|
Arms(0x0D, "AN-101", "ARM UNIT", 19000, 1228, 1006, 1670, 384, 374, tier = 1), #Normal arm units with average performance.
|
||||||
|
Arms(0x0E, "AN-201", "ARM UNIT", 15300, 1054, 877, 1635, 352, 334, tier = 1), #Low energy consumption version of the AN-101.
|
||||||
|
Arms(0x0F, "AN-K1", "ARM UNIT", 49000, 905, 930, 1790, 337, 402, tier = 3), #Reduced-weight arm units with full AP and shields.
|
||||||
|
Arms(0x10, "AN-D-7001", "ARM UNIT", 23000, 1445, 1512, 1743, 306, 453, tier = 1), #Average arm units with enhanced performance.
|
||||||
|
Arms(0x11, "AN-3001", "ARM UNIT", 39500, 1612, 1258, 1935, 487, 353, tier = 2), #Middleweight arms with maximum energy shielding.
|
||||||
|
Arms(0x12, "ANKS-1A46J", "ARM UNIT", 42100, 2120, 1415, 1990, 679, 496, tier = 5), #Offers the maximum AP but interferes with some parts.
|
||||||
|
Arms(0x13, "AN-863-B", "ARM UNIT", 34000, 1726, 1394, 1880, 517, 406, tier = 2), #Weight is increased for added durability.
|
||||||
|
Arms(0x14, "AN-25", "ARM UNIT", 28400, 853, 682, 1826, 344, 284, tier = 3), #Lightweight type arm units with better performance.
|
||||||
|
Arms(0x15, "AW-MG25/2", "MACHINE GUN", 54500, 1193, 78, 812, 0, 0, "SPECIAL", 158, 400, "SOLID", 33, 8800, 1, 2, tier = 1), #Can strafe with 4 rifles at once
|
||||||
|
Arms(0x16, "AW-GT2000", "GATLING GUN", 48600, 1415, 92, 1132, 0, 0, "SPECIAL", 305, 300, "SOLID", 62, 7800, 1, 2, tier = 2), #Dual Gatling guns can concentrate high-speed rounds at a single point.
|
||||||
|
Arms(0x17, "AW-RF105", "CANNON", 77600, 1530, 106, 1280, 0, 0, "NARROW & DEEP", 1530, 100, "SOLID", 220, 9300, 1, 15, tier = 3), #2 cannons with incredible firepower.
|
||||||
|
Arms(0x18, "AW-30/3", "DUAL MISSILE", 56400, 480, 377, 688, 0, 0, "STANDARD", 830, 80, "SOLID", 130, 9000, 3, 10, tier = 2), #Fires 2 rounds of 3 small missiles for a total of 6 missiles.
|
||||||
|
Arms(0x19, "AW-RF120", "CANNON", 67200, 1827, 137, 1420, 0, 0, "NARROW & DEEP", 2120, 50, "SOLID", 300, 9800, 1, 18, tier = 3), #Enhanced dual cannons. Somewhat fewer shots.
|
||||||
|
Arms(0x1A, "AW-S60/2", "DUAL MISSILE", 66600, 762, 420, 725, 0, 0, "STANDARD", 830, 120, "SOLID", 130, 9000, 2, 10, tier = 3), #Fires 2 rounds of 2 missiles at once for extra shots.
|
||||||
|
Arms(0x1B, "AW-XC5500", "PLASMA CANNON", 83600, 1688, 547, 875, 0, 0, "NARROW & DEEP", 1241, 70, "ENERGY", 0, 12000, 1, 7, tier = 4), #Energy weapon. Fires twin bursts of light.
|
||||||
|
Arms(0x1C, "AW-XC65", "LASER CANNON", 98500, 1905, 625, 792, 0, 0, "NARROW & DEEP", 2322, 40, "ENERGY", 0, 8300, 1, 10, tier = 4) #Energy Weapon. Fires two beams.
|
||||||
|
)
|
||||||
|
|
||||||
|
arms_by_tier = {
|
||||||
|
1: [Arms for Arms in all_arms if Arms.tier == 1],
|
||||||
|
2: [Arms for Arms in all_arms if Arms.tier == 2],
|
||||||
|
3: [Arms for Arms in all_arms if Arms.tier == 3],
|
||||||
|
4: [Arms for Arms in all_arms if Arms.tier == 4]
|
||||||
|
}
|
||||||
|
|
||||||
|
class Legs(Part):
|
||||||
|
weight: int
|
||||||
|
energy_drain: int
|
||||||
|
armor_point: int
|
||||||
|
def_shell: int
|
||||||
|
def_energy: int
|
||||||
|
max_weight: int
|
||||||
|
speed: int
|
||||||
|
stability: int
|
||||||
|
jump_function: str
|
||||||
|
tier: int
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, part_type: str, price: int,
|
||||||
|
weight: int, energy_drain: int, armor_point: int,
|
||||||
|
def_shell: int, def_energy: int, max_weight: int,
|
||||||
|
speed: int, stability: int, jump_function: str, tier: int = 1):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.part_type = part_type
|
||||||
|
self.price = price
|
||||||
|
self.weight = weight
|
||||||
|
self.energy_drain = energy_drain
|
||||||
|
self.armor_point = armor_point
|
||||||
|
self.def_shell = def_shell
|
||||||
|
self.def_energy = def_energy
|
||||||
|
self.max_weight = max_weight
|
||||||
|
self.speed = speed
|
||||||
|
self.stability = stability
|
||||||
|
self.jump_function = jump_function
|
||||||
|
self.tier = tier
|
||||||
|
|
||||||
|
all_legs: typing.Tuple[Legs, ...] = (
|
||||||
|
Legs(0x1D, "LN-1001", "HUMANOID LEGS", 28500, 1966, 1725, 3235, 556, 531, 4470, 277, 1018, "PROVIDED", tier = 1), #Balanced, standard humanoid legs.
|
||||||
|
Legs(0x1E, "LN-SSVT", "HUMANOID LEGS", 44000, 1528, 2338, 2795, 482, 507, 3560, 445, 596, "PROVIDED", tier = 2), #Light, fast humanoid legs but with low load capacity and AP.
|
||||||
|
Legs(0x1F, "LN-3001", "HUMANOID LEGS", 52200, 3137, 2206, 3703, 870, 594, 6600, 153, 2518, "PROVIDED", tier = 2), #Heavily armored humanoid legs with high load capacity. Poor speed.
|
||||||
|
Legs(0x20, "LN-1001-PX-0", "HUMANOID LEGS", 25000, 1892, 1844, 3035, 528, 508, 4100, 280, 904, "PROVIDED", tier = 1), #Balanced humanoid legs for combat on all terrain.
|
||||||
|
Legs(0x21, "LN-501", "HUMANOID LEGS", 71800, 1675, 2910, 2947, 508, 535, 3990, 451, 854, "PROVIDED", tier = 3), #Has the shield performance and load capacity of a middleweight.
|
||||||
|
Legs(0x22, "LN-SSVR", "HUMANOID LEGS", 32400, 2750, 2013, 3606, 805, 532, 5400, 148, 2150, "PROVIDED", tier = 1), #Lightest of the heavily armored humanoid legs.
|
||||||
|
Legs(0x23, "LN-1001B", "HUMANOID LEGS", 45200, 2305, 1889, 3383, 585, 543, 4630, 272, 1320, "PROVIDED", tier = 3), #Enhanced variation of the LN-1001.
|
||||||
|
Legs(0x24, "DUMMY1", "DUMMY PARTS", 0, 0, 0, 0, 0, 0, 0, 0, 0, "NONE", tier = 5), #
|
||||||
|
Legs(0x25, "LN-3001C", "HUMANOID LEGS", 64100, 3528, 2418, 3977, 889, 602, 7100, 151, 2977, "PROVIDED", tier = 3), #Best AP and shields among the humanoid legs.
|
||||||
|
Legs(0x26, "DUMMY2", "DUMMY PARTS", 0, 0, 0, 0, 0, 0, 0, 0, 0, "NONE", tier = 5), #
|
||||||
|
Legs(0x27, "LN-502", "HUMANOID LEGS", 35800, 1790, 2466, 3343, 538, 592, 3800, 275, 843, "PROVIDED", tier = 3), #This middleweight has reduced weight without sacrificing performance.
|
||||||
|
Legs(0x28, "LN-D-8000R", "HUMANOID LEGS", 49000, 2426, 2350, 3532, 510, 656, 4720, 269, 1200, "PROVIDED", tier = 2), #Humanoid legs with special anti-energy weapon armor.
|
||||||
|
Legs(0x29, "DUMMY3", "DUMMY PARTS", 0, 0, 0, 0, 0, 0, 0, 0, 0, "NONE", tier = 5), #
|
||||||
|
Legs(0x2A, "LNKS-1B46J", "HUMANOID LEGS", 48000, 3065, 2304, 3788, 822, 618, 6100, 146, 3802, "PROVIDED", tier = 2), #Shock absorbing structure reduces recoil from shell hits.
|
||||||
|
Legs(0x2B, "LB-4400", "REVERSE JOINT", 17300, 2520, 1400, 3560, 617, 451, 4020, 294, 2084, "PROVIDED", tier = 1), #Standard reverse joint type. Good maneuverability and inexpensive.
|
||||||
|
Legs(0x2C, "DUMMY4", "DUMMY PARTS", 0, 0, 0, 0, 0, 0, 0, 0, 0, "NONE", tier = 5), #
|
||||||
|
Legs(0x2D, "LB-4401", "REVERSE JOINT", 31800, 2910, 1456, 3810, 672, 468, 4510, 287, 2713, "PROVIDED", tier = 2), #Best overall performance of the reverse joint types.
|
||||||
|
Legs(0x2E, "LB-4303", "REVERSE JOINT", 24000, 2647, 1585, 3575, 643, 488, 4180, 291, 2505, "PROVIDED", tier = 1), #Increased ground contact area for enhanced shock absorbing capacity.
|
||||||
|
Legs(0x2F, "LB-1000-P", "REVERSE JOINT", 20500, 2095, 1228, 3514, 609, 444, 3775, 286, 2310, "PROVIDED", tier = 1), #Phenomenal maneuverability but low load carrying capacity.
|
||||||
|
Legs(0x30, "LBKS-2B45A", "REVERSE JOINT", 27000, 2480, 1703, 3731, 584, 515, 3990, 299, 1985, "PROVIDED", tier = 2), #Deluxe type with enhanced shielding against energy weapons.
|
||||||
|
Legs(0x31, "DUMMY5", "DUMMY PARTS", 0, 0, 0, 0, 0, 0, 0, 0, 0, "NONE", tier = 5), #
|
||||||
|
Legs(0x32, "LF-205-SF", "FOUR LEGS TYPE", 42600, 2137, 2810, 2841, 446, 654, 3450, 483, 580, "PROVIDED", tier = 2), #Standard four-leg type. Top-class maneuverability.
|
||||||
|
Legs(0x33, "LFH-X3", "FOUR LEGS TYPE", 56000, 2400, 2988, 3100, 468, 610, 3810, 421, 710, "PROVIDED", tier = 3), #Energy gauge recovers quickly when halted.
|
||||||
|
Legs(0x34, "LF-DEX-1", "FOUR LEGS TYPE", 69000, 2650, 4016, 3179, 557, 553, 4450, 360, 820, "PROVIDED", tier = 3), #Increased load carrying capacity requires vast amounts of power.
|
||||||
|
Legs(0x35, "DUMMY6", "DUMMY PARTS", 0, 0, 0, 0, 0, 0, 0, 0, 0, "NONE", tier = 5), #
|
||||||
|
Legs(0x36, "DUMMY7", "DUMMY PARTS", 0, 0, 0, 0, 0, 0, 0, 0, 0, "NONE", tier = 5), #
|
||||||
|
Legs(0x37, "LFH-X5X", "FOUR LEGS TYPE", 82000, 2880, 3584, 3328, 497, 700, 5000, 442, 1110, "PROVIDED", tier = 4), #New four-leg type pushes the specs to the limit.
|
||||||
|
Legs(0x38, "LC-MOS18", "CATERPILLAR", 16000, 4182, 978, 3928, 858, 572, 8000, 105, 4245, "NONE", tier = 1), #Maximum load carrying capacity but poor speed and weight.
|
||||||
|
Legs(0x39, "LC-UKI60", "CATERPILLAR", 25500, 3860, 1104, 3822, 812, 589, 6950, 138, 3710, "NONE", tier = 1), #Economy wheeled truck type with finely adjusted performance.
|
||||||
|
Legs(0x3A, "DUMMY8", "DUMMY PARTS", 0, 0, 0, 0, 0, 0, 0, 0, 0, "NONE", tier = 5), #
|
||||||
|
Legs(0x3B, "LC-HTP-AAA", "CATERPILLAR", 38500, 2915, 2877, 3688, 728, 694, 4130, 250, 630, "NONE", tier = 2), #Has performance near that of a four-legged type.
|
||||||
|
Legs(0x3C, "LC-MOS4545", "CATERPILLAR", 59000, 3610, 2609, 3990, 905, 753, 7400, 211, 5101, "NONE", tier = 3) #A dreadfully durable monster machine.
|
||||||
|
)
|
||||||
|
|
||||||
|
legs_by_tier = {
|
||||||
|
1: [Legs for Legs in all_legs if Legs.tier == 1],
|
||||||
|
2: [Legs for Legs in all_legs if Legs.tier == 2],
|
||||||
|
3: [Legs for Legs in all_legs if Legs.tier == 3],
|
||||||
|
4: [Legs for Legs in all_legs if Legs.tier == 4]
|
||||||
|
}
|
||||||
|
|
||||||
|
class Generator(Part):
|
||||||
|
weight: int
|
||||||
|
energy_output: int
|
||||||
|
maximum_charge: int
|
||||||
|
charge_redzone: int
|
||||||
|
tier: int
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, part_type: str, price: int,
|
||||||
|
weight: int, energy_output: int, maximum_charge: int,
|
||||||
|
charge_redzone: int, tier: int = 1):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.part_type = part_type
|
||||||
|
self.price = price
|
||||||
|
self.weight = weight
|
||||||
|
self.energy_output = energy_output
|
||||||
|
self.maximum_charge = maximum_charge
|
||||||
|
self.charge_redzone = charge_redzone
|
||||||
|
self.tier = tier
|
||||||
|
|
||||||
|
all_generators: typing.Tuple[Generator, ...] = (
|
||||||
|
Generator(0x3D, "GPS-VVA", "PULSE GENERATOR", 19500, 308, 4728, 28000, 7800, tier = 1), #Low in both power and capacity. Wide red zone.
|
||||||
|
Generator(0x3E, "GPS-V6", "PULSE GENERATOR", 32000, 363, 4728, 43000, 5000, tier = 1), #Load increased to nearly twice that of the GPS-VVA.
|
||||||
|
Generator(0x3F, "GRD-RX5", "PULSE GENERATOR", 23300, 225, 5300, 38000, 4000, tier = 1), #Balanced-performance generator.
|
||||||
|
Generator(0x40, "GRD-RX6", "PULSE GENERATOR", 27800, 286, 6000, 33000, 4000, tier = 1), #Performance not bad, but the equipment is so-so.
|
||||||
|
Generator(0x41, "GRD-RX7", "PULSE GENERATOR", 38700, 348, 6810, 31500, 5000, tier = 2), #Very good power but poor stamina.
|
||||||
|
Generator(0x42, "GBG-10000", "PULSE GENERATOR", 43500, 398, 9988, 34000, 2980, tier = 3), #High Power provides a wide selection of equipment.
|
||||||
|
Generator(0x43, "GBG-XR", "PULSE GENERATOR", 56000, 452, 8207, 48000, 3250, tier = 3) #Custom-made unit having both power and capacity.
|
||||||
|
)
|
||||||
|
|
||||||
|
generator_by_tier = {
|
||||||
|
1: [gen for gen in all_generators if gen.tier == 1],
|
||||||
|
2: [gen for gen in all_generators if gen.tier == 2],
|
||||||
|
3: [gen for gen in all_generators if gen.tier == 3]
|
||||||
|
}
|
||||||
|
|
||||||
|
class FCS(Part):
|
||||||
|
weight: int
|
||||||
|
energy_drain: int
|
||||||
|
maximum_lock: int
|
||||||
|
lock_type: str
|
||||||
|
tier: int
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, part_type: str, price: int,
|
||||||
|
weight: int, energy_drain: int, maximum_lock: int,
|
||||||
|
lock_type: str, tier: int = 1):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.part_type = part_type
|
||||||
|
self.price = price
|
||||||
|
self.weight = weight
|
||||||
|
self.energy_drain = energy_drain
|
||||||
|
self.maximum_lock = maximum_lock
|
||||||
|
self.lock_type = lock_type
|
||||||
|
self.tier = tier
|
||||||
|
|
||||||
|
all_fcs: typing.Tuple[FCS, ...] = (
|
||||||
|
FCS(0x44, "COMDEX-C7", "FCS", 11100, 14, 24, 4, "STANDARD", tier = 1), #Maximum of 4 lock-ons, average performance
|
||||||
|
FCS(0x45, "COMDEX-G0", "FCS", 22500, 14, 24, 4, "STANDARD", tier = 1), #Maximum of 4 lock-ons, fast lock-on.
|
||||||
|
FCS(0x46, "COMDEX-G8", "FCS", 16400, 14, 24, 6, "STANDARD", tier = 1), #Maximum of 6 lock-ons, long-distance lock-on.
|
||||||
|
FCS(0x47, "QX-21", "FCS", 20300, 8, 12, 1, "WIDE & SHALLOW", tier = 2), #Maximum of 1 lock-on, short lock over a wide area.
|
||||||
|
FCS(0x48, "QX-AF", "FCS", 35700, 10, 16, 2, "WIDE & SHALLOW", tier = 2), #Maximum of 2 lock-ons, short lock.
|
||||||
|
FCS(0x49, "TRYX-BOXER", "FCS", 48100, 10, 19, 3, "TALL", tier = 3), #Maximum of 3 lock-ons, vertical sight.
|
||||||
|
FCS(0x4A, "TRYX-QUAD", "FCS", 63000, 18, 38, 6, "WIDE", tier = 3), #Maximum of 6 lock-ons, horizontal sight.
|
||||||
|
FCS(0x4B, "QX-9009", "FCS", 96000, 24, 55, 6, "NARROW & DEEP", tier = 3) #Maximum of 6 lock-ons, longest lock distance.
|
||||||
|
)
|
||||||
|
|
||||||
|
fcs_by_tier = {
|
||||||
|
1: [FC for FC in all_fcs if FC.tier == 1],
|
||||||
|
2: [FC for FC in all_fcs if FC.tier == 2],
|
||||||
|
3: [FC for FC in all_fcs if FC.tier == 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Option_Part(Part):
|
||||||
|
slot_spend: int
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, part_type: str, price: int,
|
||||||
|
slot_spend: int):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.part_type = part_type
|
||||||
|
self.price = price
|
||||||
|
self.slot_spend = slot_spend
|
||||||
|
|
||||||
|
all_options_parts: typing.Tuple[Option_Part, ...] = (
|
||||||
|
Option_Part(0x4C, "SP-MAW", "RADAR OPTION", 14200, 1), #Adds a missile display function to the radar.
|
||||||
|
Option_Part(0x4D, "SP-JAM", "MISSILE JAMMER", 26000, 3), #Regularly generates pulses that disable missile lock-ons.
|
||||||
|
Option_Part(0x4E, "SP-M/AUTO", "AUTO LAUNCHER", 12900, 1), #Fires a missile automatically on full lock-on.
|
||||||
|
Option_Part(0x4F, "SP-ABS", "BALANCER OPTION", 29600, 1), #Reduces the recoil of shell hits.
|
||||||
|
Option_Part(0x50, "SP-SAP", "ABSORBER OPTION", 31800, 1), #Reduces the recoil of cannon fire.
|
||||||
|
Option_Part(0x51, "SP-CND-K", "CHARGE EXPANDER", 21000, 4), #Increases the number of capacitors in the generator.
|
||||||
|
Option_Part(0x52, "SP-AXL", "FCS ACCELERATOR", 24000, 2), #Shortens the lock-on time.
|
||||||
|
Option_Part(0x53, "SP-S/SCR", "SHELL SCREEN", 33000, 2), #Reduces the damage from solid rounds.
|
||||||
|
Option_Part(0x54, "SP-E/SCR", "ENERGY SCREEN", 38500, 1), #Reduces damage from energy rounds.
|
||||||
|
Option_Part(0x55, "SP-EH", "RAPID CHARGE", 45000, 1), #Increases the burst fire rate of energy weapons.
|
||||||
|
Option_Part(0x56, "SP-E+", "ENERGY AMPLIFIER", 45000, 1) #Increases the firepower of energy weapons.
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Booster(Part):
|
||||||
|
weight: int
|
||||||
|
energy_drain: int
|
||||||
|
boost_power: int
|
||||||
|
charge_drain: int
|
||||||
|
tier: int
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, part_type: str, price: int,
|
||||||
|
weight: int, energy_drain: int, boost_power: int,
|
||||||
|
charge_drain: int, tier: int = 1):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.part_type = part_type
|
||||||
|
self.price = price
|
||||||
|
self.weight = weight
|
||||||
|
self.energy_drain = energy_drain
|
||||||
|
self.boost_power = boost_power
|
||||||
|
self.charge_drain = charge_drain
|
||||||
|
self.tier = tier
|
||||||
|
|
||||||
|
all_boosters: typing.Tuple[Booster, ...] = (
|
||||||
|
Booster(0x57, "B-P320", "BOOST UNIT", 10800, 208, 28, 9800, 4360, tier = 1), #Low priced but seems a bit underpowered.
|
||||||
|
Booster(0x58, "B-P350", "BOOST UNIT", 13700, 162, 33, 12800, 4410, tier = 1), #Economy type with high power but high energy consumption.
|
||||||
|
Booster(0x59, "B-T001", "BOOST UNIT", 34000, 149, 30, 17300, 4600, tier = 2), #Achieves both enhanced power and low weight at the same time.
|
||||||
|
Booster(0x5A, "B-T2", "BOOST UNIT", 31500, 235, 38, 14800, 3850, tier = 2), #Power itself is low but offers the highest efficiency.
|
||||||
|
Booster(0x5B, "B-P351", "BOOST UNIT", 25500, 288, 41, 21000, 6980, tier = 2), #High-Performance model with both high power and energy consumption.
|
||||||
|
Booster(0x5C, "B-VR-33", "BOOST UNIT", 48500, 255, 35, 19000, 5070, tier = 3) #Maintains the top-class power to achieve good efficiency.
|
||||||
|
)
|
||||||
|
|
||||||
|
booster_by_tier = {
|
||||||
|
1: [Boost for Boost in all_boosters if Boost.tier == 1],
|
||||||
|
2: [Boost for Boost in all_boosters if Boost.tier == 2],
|
||||||
|
3: [Boost for Boost in all_boosters if Boost.tier == 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
class Back_Weapon(Part):
|
||||||
|
weight: int
|
||||||
|
energy_drain: int
|
||||||
|
radar_function: str
|
||||||
|
weapon_lock: str
|
||||||
|
attack_power: int
|
||||||
|
number_of_ammo: int
|
||||||
|
ammo_type: str
|
||||||
|
ammo_price: int
|
||||||
|
arms_range: int
|
||||||
|
maximum_lock: int
|
||||||
|
reload_time: int
|
||||||
|
fire_unlocked: bool
|
||||||
|
tier: int
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, part_type: str, price: int,
|
||||||
|
weight: int, energy_drain: int, radar_function: str,
|
||||||
|
weapon_lock: str, attack_power: int, number_of_ammo: int = -1,
|
||||||
|
ammo_type: str = "", ammo_price: int = -1, arms_range: int = -1,
|
||||||
|
maximum_lock: int = -1, reload_time: int = -1, fire_unlocked: bool = False, tier: int = 1):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.part_type = part_type
|
||||||
|
self.price = price
|
||||||
|
self.weight = weight
|
||||||
|
self.energy_drain = energy_drain
|
||||||
|
self.radar_function = radar_function
|
||||||
|
self.weapon_lock = weapon_lock
|
||||||
|
self.attack_power = attack_power
|
||||||
|
self.number_of_ammo = number_of_ammo
|
||||||
|
self.ammo_type = ammo_type
|
||||||
|
self.ammo_price = ammo_price
|
||||||
|
self.arms_range = arms_range
|
||||||
|
self.maximum_lock = maximum_lock
|
||||||
|
self.reload_time = reload_time
|
||||||
|
self.fire_unlocked = fire_unlocked
|
||||||
|
self.tier = tier
|
||||||
|
|
||||||
|
all_back_weapons: typing.Tuple[Back_Weapon, ...] = (
|
||||||
|
Back_Weapon(0x5D, "WM-S40/1", "SMALL MISSILE", 18700, 245, 245, "NONE", "STANDARD", 830, 40, "SOLID", 130, 9000, 1, 10, fire_unlocked = False, tier = 1), #Pod that fires single small missiles.
|
||||||
|
Back_Weapon(0x5E, "WM-S40/2", "SMALL MISSILE", 23000, 337, 320, "NONE", "STANDARD", 830, 40, "SOLID", 130, 9000, 2, 10, fire_unlocked = False, tier = 1), #Fires up to 2 small missiles at once.
|
||||||
|
Back_Weapon(0x5F, "WM-S60/4", "SMALL MISSILE", 28800, 520, 349, "NONE", "STANDARD", 830, 60, "SOLID", 130, 9000, 4, 10, fire_unlocked = False, tier = 1), #Fires up to 4 small missiles at once.
|
||||||
|
Back_Weapon(0x60, "WM-S60/6", "SMALL MISSILE", 38100, 583, 353, "NONE", "STANDARD", 830, 60, "SOLID", 130, 9000, 6, 10, fire_unlocked = False, tier = 2), #Fires up to 6 small missiles at once.
|
||||||
|
Back_Weapon(0x61, "WM-MVG404", "MISSILE", 31000, 620, 280, "NONE", "STANDARD", 1560, 24, "SOLID", 252, 10000, 1, 10, fire_unlocked = False, tier = 2), #Pod that fires singles missiles.
|
||||||
|
Back_Weapon(0x62, "WM-MVG802", "MISSILE", 44000, 718, 220, "NONE", "STANDARD", 1560, 32, "SOLID", 252, 10000, 2, 10, fire_unlocked = False, tier = 2), #Fires up to 2 missiles at once.
|
||||||
|
Back_Weapon(0x63, "WM-L201", "LARGE MISSILE", 46200, 835, 180, "NONE", "STANDARD", 4300, 12, "SOLID", 897, 12500, 1, 10, fire_unlocked = False, tier = 3), #Powerful large missiles fired singly
|
||||||
|
Back_Weapon(0x64, "WM-X201", "MULTI MISSILE", 62250, 720, 250, "NONE", "STANDARD", 980, 18, "SOLID", 1125, 12000, 1, 15, fire_unlocked = False, tier = 3), #Multi-warhead missile that scatters warheads in flight.
|
||||||
|
Back_Weapon(0x65, "WM-X5-AA", "BOMB DISPENSER", 19300, 616, 85, "NONE", "NONE", 675, 10, "SOLID", 270, 0, 0, 50, fire_unlocked = True, tier = 2), #Drops 8 ground-attack mines. For experts.
|
||||||
|
Back_Weapon(0x66, "WM-X10", "BOMB DISPENSER", 24800, 939, 105, "NONE", "NONE", 675, 10, "SOLID", 560, 0, 0, 50, fire_unlocked = True, tier = 2), #Drops 16 powerful ground-attack mines.
|
||||||
|
Back_Weapon(0x67, "WM-P40001", "DUAL MISSILE", 43800, 755, 320, "NONE", "STANDARD", 830, 60, "SOLID", 130, 9000, 1, 10, fire_unlocked = False, tier = 2), #Fires 2 left or right curving indirect attack missiles.
|
||||||
|
Back_Weapon(0x68, "WM-PS-2", "TRIPLE MISSILE", 66700, 1125, 360, "NONE", "STANDARD", 830, 90, "SOLID", 130, 9000, 1, 10, fire_unlocked = False, tier = 2), #Fires 3 up-curving indirect attack missiles.
|
||||||
|
Back_Weapon(0x69, "WR-S50", "SMALL ROCKET", 15900, 218, 8, "NONE", "NONE", 1310, 50, "SOLID", 110, 12500, 0, 8, fire_unlocked = True, tier = 1), #Carries 50 small rockets.
|
||||||
|
Back_Weapon(0x6A, "WR-S100", "SMALL ROCKET", 32400, 846, 15, "NONE", "NONE", 1310, 100, "SOLID", 110, 12500, 0, 12, fire_unlocked = True, tier = 2), #Carries 100 small rockets.
|
||||||
|
Back_Weapon(0x6B, "WR-M50", "ROCKET", 27600, 677, 13, "NONE", "NONE", 2240, 50, "SOLID", 220, 14000, 0, 12, fire_unlocked = True, tier = 2), #Carries 50 rockets.
|
||||||
|
Back_Weapon(0x6C, "WR-M70", "ROCKET", 36500, 718, 24, "NONE", "NONE", 2240, 70, "SOLID", 220, 14000, 0, 16, fire_unlocked = True, tier = 2), #Carries 70 rockets.
|
||||||
|
Back_Weapon(0x6D, "WR-L24", "LARGE ROCKET", 29460, 805, 18, "NONE", "NONE", 3980, 24, "SOLID", 417, 17700, 0, 16, fire_unlocked = True, tier = 3), #This Rocket has the greatest firepower of any single weapon.
|
||||||
|
Back_Weapon(0x6E, "WC-CN35", "CHAIN GUN", 32750, 593, 11, "NONE", "SPECIAL", 338, 250, "SOLID", 52, 10000, 1, 2, fire_unlocked = True, tier = 3), #Fast reloading rifle. Easy to use.
|
||||||
|
Back_Weapon(0x6F, "WC-ST120", "SLUG GUN", 56000, 827, 6, "NONE", "SPECIAL", 183, 80, "SOLID", 156, 8100, 1, 22, fire_unlocked = True, tier = 2), #Fires 7 simultaneous shots that scatter over a wide range.
|
||||||
|
Back_Weapon(0x70, "WC-LN350", "LINEAR GUN", 41800, 425, 8, "NONE", "SPECIAL", 690, 120, "SOLID", 108, 9000, 1, 6, fire_unlocked = True, tier = 3), #Burst-fire type weapon emphasizing firepower over number of shots.
|
||||||
|
Back_Weapon(0x71, "WC-GN230", "GRENADE LAUNCHER", 75200, 1230, 8, "NONE", "NARROW & DEEP", 3520, 15, "SOLID", 985, 12000, 1, 32, fire_unlocked = True, tier = 4), #An AC’s symbolic weapon that mows down enemies in a firestorm.
|
||||||
|
Back_Weapon(0x72, "WC-XP4000", "PULSE CANNON", 61000, 318, 364, "NONE", "NARROW & DEEP", 770, 100, "ENERGY", 0, 9000, 1, 5, fire_unlocked = True, tier = 3), #Energy weapon. Reloading ion cannon.
|
||||||
|
Back_Weapon(0x73, "WC-XC8000", "LASER CANNON", 78700, 1110, 455, "NONE", "NARROW & DEEP", 2065, 50, "ENERGY", 0, 8500, 1, 10, fire_unlocked = True, tier = 4), #Energy weapon. Fires laser rounds.
|
||||||
|
Back_Weapon(0x74, "WC-01QL", "PLASMA CANNON", 69500, 273, 618, "NONE", "NARROW & DEEP", 1531, 80, "ENERGY", 0, 12000, 1, 7, fire_unlocked = True, tier = 4), #Energy weapon. Beam cuts down enemies.
|
||||||
|
Back_Weapon(0x75, "RXA-01WE", "RADAR", 12100, 210, 243, "PROVIDED", 8650, "STANDARD", fire_unlocked = False, tier = 1), #Old-style antenna but still holds up well in use.
|
||||||
|
Back_Weapon(0x76, "RZ-A0", "RADAR", 17900, 480, 387, "PROVIDED", 11500, "CIRCLE", fire_unlocked = False, tier = 1), #This radar uses 2 dishes for enhanced enemy-search capability.
|
||||||
|
Back_Weapon(0x77, "RXA-99", "RADAR", 14500, 160, 267, "PROVIDED", 8800, "STANDARD", fire_unlocked = False, tier = 1), #New-type radar permits even wider area to be searched.
|
||||||
|
Back_Weapon(0x78, "RXA-77", "RADAR", 23000, 125, 274, "PROVIDED", 8700, "STANDARD", fire_unlocked = False, tier = 1), #This radar can detect the approach of homing missiles.
|
||||||
|
Back_Weapon(0x79, "RZ-A1", "RADAR", 33000, 433, 403, "PROVIDED", 15700, "CIRCLE", fire_unlocked = False, tier = 2), #Expands the enemy-search range up to the current technological limit.
|
||||||
|
Back_Weapon(0x7A, "RZT-333", "RADAR", 27700, 343, 451, "PROVIDED", 11700, "OCTAGON", fire_unlocked = False, tier = 2), #Combines both missile detection and wide-range search capability.
|
||||||
|
Back_Weapon(0x7B, "RZ-BBP", "RADAR", 40900, 454, 566, "PROVIDED", 16300, "CIRCLE", fire_unlocked = False, tier = 2), #Highest-quality radar with highest-class performance.
|
||||||
|
Back_Weapon(0x7C, "WX-S800/2", "DUAL MISSILE", 69400, 1650, 415, "NONE", "STANDARD", 1120, 60, "SOLID", 515, 11000, 1, 12, fire_unlocked = False, tier = 3), #Fires 2 missiles with 1 lock-on
|
||||||
|
Back_Weapon(0x7D, "WX-S800-GF", "DUAL MISSILE", 90900, 1110, 656, "NONE", "STANDARD", 1120, 60, "SOLID", 515, 11000, 1, 10, fire_unlocked = False, tier = 3), #Fires 6 missiles with 1 lock-on
|
||||||
|
Back_Weapon(0x7E, "XCS-9900", "MULTI MISSILE", 94500, 1480, 310, "NONE", "STANDARD", 980, 20, "SOLID", 1125, 12000, 1, 15, fire_unlocked = False, tier = 3) #Fires 2 multi-warhead missiles simultaneously.
|
||||||
|
)
|
||||||
|
|
||||||
|
back_weapon_by_tier = {
|
||||||
|
1: [Back_Weapon for Back_Weapon in all_back_weapons if Back_Weapon.tier == 1],
|
||||||
|
2: [Back_Weapon for Back_Weapon in all_back_weapons if Back_Weapon.tier == 2],
|
||||||
|
3: [Back_Weapon for Back_Weapon in all_back_weapons if Back_Weapon.tier == 3],
|
||||||
|
4: [Back_Weapon for Back_Weapon in all_back_weapons if Back_Weapon.tier == 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
class Arm_Weapon_R(Part): #guns
|
||||||
|
weight: int
|
||||||
|
energy_drain: int
|
||||||
|
weapon_lock: str
|
||||||
|
attack_power: int
|
||||||
|
number_of_ammo: int
|
||||||
|
ammo_type: str
|
||||||
|
ammo_price: int
|
||||||
|
arms_range: int
|
||||||
|
maximum_lock: int
|
||||||
|
reload_time: int
|
||||||
|
tier: int
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, part_type: str, price: int,
|
||||||
|
weight: int, energy_drain: int, weapon_lock: str,
|
||||||
|
attack_power: int, number_of_ammo: int, ammo_type: str,
|
||||||
|
ammo_price: int, arms_range: int, maximum_lock: int,
|
||||||
|
reload_time: int, tier: int = 1):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.part_type = part_type
|
||||||
|
self.price = price
|
||||||
|
self.weight = weight
|
||||||
|
self.energy_drain = energy_drain
|
||||||
|
self.weapon_lock = weapon_lock
|
||||||
|
self.attack_power = attack_power
|
||||||
|
self.number_of_ammo = number_of_ammo
|
||||||
|
self.ammo_type = ammo_type
|
||||||
|
self.ammo_price = ammo_price
|
||||||
|
self.arms_range = arms_range
|
||||||
|
self.maximum_lock = maximum_lock
|
||||||
|
self.reload_time = reload_time
|
||||||
|
self.tier = tier
|
||||||
|
|
||||||
|
all_arm_weapon_rs: typing.Tuple[Arm_Weapon_R, ...] = (
|
||||||
|
Arm_Weapon_R(0x7F, "NO WEAPON", "- - - - - - - -", 0, 0, 0, "WIDE & SHALLOW", 0, 0, "- - - - - -", 0, 0, 0, 0, tier = 5), #
|
||||||
|
Arm_Weapon_R(0x80, "WG-RF35", "RIFLE", 11400, 415, 6, "WIDE & SHALLOW", 218, 200, "SOLID", 18, 8500, 1, 5, tier = 1), #Standard portable rifle. Suitable for various missions.
|
||||||
|
Arm_Weapon_R(0x81, "WG-MGA1", "MACHINE GUN", 14000, 370, 4, "WIDE & SHALLOW", 85, 500, "SOLID", 9, 6300, 1, 1, tier = 1), #Fast-reloading solid round machine gun. Low single-round firepower.
|
||||||
|
Arm_Weapon_R(0x82, "WG-MG500", "MACHINE GUN", 28400, 458, 4, "WIDE & SHALLOW", 135, 500, "SOLID", 15, 7800, 1, 2, tier = 2), #Enhanced version of the machine gun with higher firepower.
|
||||||
|
Arm_Weapon_R(0x83, "WG-AR1000", "MACHINE GUN", 42300, 516, 8, "SPECIAL", 105, 1000, "SOLID", 12, 7000, 1, 1, tier = 3), #Most powerful portable type machine gun.
|
||||||
|
Arm_Weapon_R(0x84, "WG-HG235", "HAND GUN", 19000, 170, 22, "WIDE & SHALLOW", 226, 100, "SOLID", 68, 4800, 1, 5, tier = 1), #Wide scatter-shot pistol. Very short range.
|
||||||
|
Arm_Weapon_R(0x85, "WG-RF/5", "SNIPER RIFLE", 41500, 295, 5, "SPECIAL", 530, 80, "SOLID", 83, 20000, 1, 10, tier = 1), #Long-barrel sniper rifle.
|
||||||
|
Arm_Weapon_R(0x86, "WG-RF/P", "SNIPER RIFLE", 33100, 308, 4, "SPECIAL", 612, 60, "SOLID", 95, 16000, 1, 12, tier = 1), #Superior firepower and range, but low reload rate.
|
||||||
|
Arm_Weapon_R(0x87, "WG-HG512", "HAND GUN", 26200, 324, 10, "WIDE & SHALLOW", 437, 120, "SOLID", 48, 5800, 1, 8, tier = 3), #Lower performance, but inexpensive.
|
||||||
|
Arm_Weapon_R(0x88, "WG-FG99", "FLAMETHROWER", 58300, 352, 9, "NONE", 512, 500 , "SOLID", 41, 900, 1, 1, tier = 2), #Close-in combat gun shows off its true worth in hand-to-hand combat.
|
||||||
|
Arm_Weapon_R(0x89, "WG-B2120", "BAZOOKA", 59740, 778, 13, "NARROW & DEEP", 1250, 80, "SOLID", 163, 8200, 1, 16, tier = 2), #High firepower but slow-moving bazooka fire is easily avoidable.
|
||||||
|
Arm_Weapon_R(0x8A, "WG-B2180", "BAZOOKA", 75900, 905, 16, "NARROW & DEEP", 1930, 50, "SOLID", 348, 7800, 1, 22, tier = 3), #Ultra-attack bazooka for betting it all on one shot.
|
||||||
|
Arm_Weapon_R(0x8B, "WG-XP1000", "PULSE RIFLE", 46000, 188, 246, "SPECIAL", 302, 180, "ENERGY", 0, 15000, 1, 3, tier = 2), #Energy weapon. Noted for its long range and reload speed.
|
||||||
|
Arm_Weapon_R(0x8C, "WG-XP2000", "PULSE RIFLE", 61500, 265, 285, "SPECIAL", 435, 200, "ENERGY", 0, 18000, 1, 6, tier = 2), #Energy weapon. Emphasizes its long range and number of shots.
|
||||||
|
Arm_Weapon_R(0x8D, "WG-XC4", "LASER RIFLE", 51000, 686, 308, "SPECIAL", 820, 100, "ENERGY", 0, 8000, 1, 10, tier = 3), #Energy weapon. High firepower and energy consumption.
|
||||||
|
Arm_Weapon_R(0x8E, "WG-1-KARASAWA", "LASER RIFLE", 75000, 1000, 422, "SPECIAL", 1550, 50, "ENERGY", 0, 10000, 1, 8, tier = 5) #Energy Weapon. Powerful but heavy.
|
||||||
|
)
|
||||||
|
|
||||||
|
arm_weapon_r_by_tier = {
|
||||||
|
1: [Arm_Weapon_R for Arm_Weapon_R in all_arm_weapon_rs if Arm_Weapon_R.tier == 1],
|
||||||
|
2: [Arm_Weapon_R for Arm_Weapon_R in all_arm_weapon_rs if Arm_Weapon_R.tier == 2],
|
||||||
|
3: [Arm_Weapon_R for Arm_Weapon_R in all_arm_weapon_rs if Arm_Weapon_R.tier == 3],
|
||||||
|
4: [Arm_Weapon_R for Arm_Weapon_R in all_arm_weapon_rs if Arm_Weapon_R.tier == 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
class Arm_Weapon_L(Part): #laserblades
|
||||||
|
weight: int
|
||||||
|
energy_drain: int
|
||||||
|
charge_drain: int
|
||||||
|
attack_power: int
|
||||||
|
tier: int
|
||||||
|
|
||||||
|
def __init__(self, _id: int, name: str, part_type: str, price: int,
|
||||||
|
weight: int, energy_drain: int, charge_drain: int,
|
||||||
|
attack_power: int, tier: int = 1):
|
||||||
|
self.id = _id
|
||||||
|
self.name = name
|
||||||
|
self.part_type = part_type
|
||||||
|
self.price = price
|
||||||
|
self.weight = weight
|
||||||
|
self.energy_drain = energy_drain
|
||||||
|
self.charge_drain = charge_drain
|
||||||
|
self.attack_power = attack_power
|
||||||
|
self.tier = tier
|
||||||
|
|
||||||
|
all_arm_weapon_ls: typing.Tuple[Arm_Weapon_L, ...] = (
|
||||||
|
Arm_Weapon_L(0x8F, "LS-2001", "LASERBLADE", 11500, 123, 28, 2050, 738, tier = 1), #Infinitely reusable laser blade.
|
||||||
|
Arm_Weapon_L(0x90, "LS-200G", "LASERBLADE", 29000, 181, 45, 1700, 950, tier = 1), #Powerful weapon exclusively for close-in combat.
|
||||||
|
Arm_Weapon_L(0x91, "LS-3303", "LASERBLADE", 37200, 224, 43, 2630, 1210, tier = 2), #Enhanced blade weapon. Both power and energy consumption are higher.
|
||||||
|
Arm_Weapon_L(0x92, "LS-99-MOONLIGHT", "LASERBLADE", 54000, 336, 93, 810, 2801, tier = 3) #Blade weapon with more than twice the power of conventional blades.
|
||||||
|
)
|
||||||
|
|
||||||
|
arm_weapon_l_by_tier = {
|
||||||
|
1: [Arm_Weapon_L for Arm_Weapon_L in all_arm_weapon_ls if Arm_Weapon_L.tier == 1],
|
||||||
|
2: [Arm_Weapon_L for Arm_Weapon_L in all_arm_weapon_ls if Arm_Weapon_L.tier == 2],
|
||||||
|
3: [Arm_Weapon_L for Arm_Weapon_L in all_arm_weapon_ls if Arm_Weapon_L.tier == 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
all_parts: typing.Tuple[Part, ...] = all_heads + all_cores + all_legs + all_arms + all_arm_weapon_rs + all_back_weapons + all_arm_weapon_ls + all_boosters + all_fcs + all_generators + all_options_parts
|
||||||
|
all_parts_data_order: typing.Tuple[Part, ...] = all_heads + all_cores + all_arms + all_legs + all_generators + all_fcs + all_options_parts + all_boosters + all_back_weapons + all_arm_weapon_rs + all_arm_weapon_ls
|
||||||
|
|
||||||
|
all_dummy_parts: typing.Tuple[Part, ...] = {part for part in all_parts if "DUMMY" in part.name or part.name == "NO WEAPON"}
|
||||||
|
|
||||||
|
base_starting_parts: typing.Tuple[Part, ...] = {part for part in all_parts if
|
||||||
|
part.name == "HD-GRY-NX" or
|
||||||
|
part.name == "XCA-00" or
|
||||||
|
part.name == "AN-201" or
|
||||||
|
part.name == "LN-1001-PX-0" or
|
||||||
|
part.name == "GPS-VVA" or
|
||||||
|
part.name == "COMDEX-C7" or
|
||||||
|
part.name == "B-P320" or
|
||||||
|
part.name == "WM-S40/1" or
|
||||||
|
part.name == "RXA-01WE" or
|
||||||
|
part.name == "WG-RF35" or
|
||||||
|
part.name == "LS-2001"}
|
||||||
|
|
||||||
|
id_to_part = {part.id: part for part in all_parts}
|
||||||
|
name_to_part = {part.name: part for part in all_parts}
|
||||||
205
worlds/armoredcore/utils.py
Normal file
205
worlds/armoredcore/utils.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Constants:
|
||||||
|
# Armored Core constants
|
||||||
|
GAME_NAME: str = "Armored Core"
|
||||||
|
MISSION_LIST_MODE_OFFSET: int = 0x1F3754 # Data is 1 Byte
|
||||||
|
MISSION_COMPLETION_OFFSET: int = 0x1F3788 # Data is 1 Byte each
|
||||||
|
VICTORY_ITEM_NAME: str = "Armored Core Goal"
|
||||||
|
VICTORY_LOCATION_NAME: str = "Armored Core Completed"
|
||||||
|
VICTORY_ITEM_ID: int = 0x1
|
||||||
|
VICTORY_LOCATION_ID: int = 0x1
|
||||||
|
PROGRESSIVE_MISSION_ITEM_NAME: str = "Progressive Mission"
|
||||||
|
PROGRESSIVE_MISSION_ITEM_ID: int = 0x2
|
||||||
|
CREDIT_ITEM_NAME: str = "Bonus Credits"
|
||||||
|
CREDIT_ITEM_ID: int = 0x3
|
||||||
|
CREDIT_ITEMS_RECEIVED_OFFSET: int = 0x48664 # We are commandeering the VS Time Limit option memory for this! Sticks in save file
|
||||||
|
SHOPSANITY_TRACKING_OFFSET: int = 0x48663 # We are comandeering the VS Stage option memory for this!
|
||||||
|
STARTING_AC_RANDO_TRACKING_OFFSET: int = 0x48665 # Comandeering this hopefully useless byte* in the players save file memory for this! 01 = randomized
|
||||||
|
PLAYER_CREDITS_OFFSET: int = 0x39CA4 # Data is 4 byte size, signed integer. Min max should be +/- 99,999,999.
|
||||||
|
HUMANPLUS_LEVEL_OFFSET: int = 0x039D20 # Data is 1 byte size. The three levels are 01/04/06
|
||||||
|
PROGRESSIVE_HUMANPLUS_ITEM_NAME: str = "Progressive Human+"
|
||||||
|
PROGRESSIVE_HUMANPLUS_ITEM_ID: int = 0x4
|
||||||
|
MAIL_RECEPTION_OFFSET: int = 0x17F6D0 # Data is 1 Byte
|
||||||
|
STORY_PROGRESS_OFFSET: int = 0x1F3752 # Data is 1 Byte
|
||||||
|
SUCCESSFUL_SORTIES_COUNT_OFFSET: int = 0x1F3780 # Data is 1 Byte
|
||||||
|
SHOP_INVENTORY_OFFSET: int = 0x031B34 # Data is 1 Byte
|
||||||
|
PARTS_INVENTORY_OFFSET: int = 0x031A94 # Data is 1 Byte
|
||||||
|
CURRENT_AC_PARTS_OFFSET: int = 0x31A58 # Data is 12 bytes long, 1 byte for each part and 1 blank
|
||||||
|
SCENARIO_MODE_SELECTED_OFFSET: int = 0x31A5F # This is technically the players equipped Back Weapon R, but when you select Scenario Mode on the main menu
|
||||||
|
# The value changes from 0x13 to 0x18. That's what we'll be using as a failsafe for our starting AC rando check
|
||||||
|
|
||||||
|
SHOP_OVERWRITE_OPTIONAL_PARTS_OFFSET: int = 0x05b270 # The instruction here performs the check for if optional parts should be removed (if you don't have them in inventory)
|
||||||
|
FREESPACE_CODE_OFFSET: int = 0x17f860 # Used for Mission List modification, it's actually where Mail names are stored lol (but we guarded write to prevent Mail from getting messed up)
|
||||||
|
MISSION_MENU_HOOK_OFFSET: int = 0x87218 # Used to hack into mission list display routine
|
||||||
|
MENU_CURRENT_SELECTION1_OFFSET: int = 0x1A2836 # 1A2837. IE Mail is 0x04, 1A2766,1A2767. 20 or E0 in second values must be checked? WHY is this location moving around ugh
|
||||||
|
MENU_CURRENT_SELECTION1_VERIFY_OFFSET: int = 0x1A2837 # 20 or E0 to verify we can use the above offset
|
||||||
|
MENU_CURRENT_SELECTION2_OFFSET: int = 0x1A2766 # This data moves around, this is the second possible location for it (plus its verifier)
|
||||||
|
MENU_CURRENT_SELECTION2_VERIFY_OFFSET: int = 0x1A2767
|
||||||
|
MENU_LOADED_VERIFY_OFFSET1: int = 0x1A2728
|
||||||
|
MENU_LOADED_VERIFY_OFFSET2: int = 0x1A27F8
|
||||||
|
SHOP_SELL_INTERCEPT_OFFSETS: typing.Tuple[int, ...] = (
|
||||||
|
0x0952B4,
|
||||||
|
0x0952CC,
|
||||||
|
0x095220,
|
||||||
|
0x095230,
|
||||||
|
) # For removing the Sell option
|
||||||
|
SHOP_SELL_TEXT__OFFSET: int = 0x04CC25
|
||||||
|
PARTS_DESCRIPTIONS_OFFSET: int = 0x19C360 # Each entry is 0x4E apart
|
||||||
|
PARTS_TEXT_CHANGE_VERIFY_OFFSET: int = 0x19F02B # Expected value is 0x40
|
||||||
|
PARTS_NAMES_OFFSETS: typing.Tuple[int, ...] = ( # This is so awful lol. all_parts changed to match this order
|
||||||
|
0x0B91F4, # Head
|
||||||
|
0x0B9230,
|
||||||
|
0x0B926C,
|
||||||
|
0x0B92A8,
|
||||||
|
0x0B92E4,
|
||||||
|
0x0B9320,
|
||||||
|
0x0B935C,
|
||||||
|
0x0B9398,
|
||||||
|
0x0B93D4,
|
||||||
|
0x0B9410,
|
||||||
|
0x0B944C, # Core
|
||||||
|
0x0B9484,
|
||||||
|
0x0B94BC,
|
||||||
|
0x0B94F4, # Leg
|
||||||
|
0x0B953C,
|
||||||
|
0x0B9584,
|
||||||
|
0x0B95CC,
|
||||||
|
0x0B9614,
|
||||||
|
0x0B965C,
|
||||||
|
0x0B96A4,
|
||||||
|
0x0B96EC,
|
||||||
|
0x0B9734,
|
||||||
|
0x0B977C,
|
||||||
|
0x0B97C4,
|
||||||
|
0x0B980C,
|
||||||
|
0x0B9854,
|
||||||
|
0x0B989C,
|
||||||
|
0x0B98E4,
|
||||||
|
0x0B992C,
|
||||||
|
0x0B9974,
|
||||||
|
0x0B99BC,
|
||||||
|
0x0B9A04,
|
||||||
|
0x0B9A4C,
|
||||||
|
0x0B9A94,
|
||||||
|
0x0B9ADC,
|
||||||
|
0x0B9B24,
|
||||||
|
0x0B9B6C,
|
||||||
|
0x0B9BB4,
|
||||||
|
0x0B9BFC,
|
||||||
|
0x0B9C44,
|
||||||
|
0x0B9C8C,
|
||||||
|
0x0B9CD4,
|
||||||
|
0x0B9D1C,
|
||||||
|
0x0B9D64,
|
||||||
|
0x0B9DAC,
|
||||||
|
0x0B9DF4, # Arms
|
||||||
|
0x0B9E28,
|
||||||
|
0x0B9E5C,
|
||||||
|
0x0B9E90,
|
||||||
|
0x0B9EC4,
|
||||||
|
0x0B9EF8,
|
||||||
|
0x0B9F2C,
|
||||||
|
0x0B9F60,
|
||||||
|
0x0B9F94,
|
||||||
|
0x0B9FC8,
|
||||||
|
0x0B9FFC,
|
||||||
|
0x0BA030,
|
||||||
|
0x0BA064,
|
||||||
|
0x0BA098,
|
||||||
|
0x0BA0CC,
|
||||||
|
0x0BA100,
|
||||||
|
0x0BA134, #Arm Weapon R
|
||||||
|
0x0BA160,
|
||||||
|
0x0BA18C,
|
||||||
|
0x0BA1B8,
|
||||||
|
0x0BA1E4,
|
||||||
|
0x0BA210,
|
||||||
|
0x0BA23C,
|
||||||
|
0x0BA268,
|
||||||
|
0x0BA294,
|
||||||
|
0x0BA2C0,
|
||||||
|
0x0BA2EC,
|
||||||
|
0x0BA318,
|
||||||
|
0x0BA344,
|
||||||
|
0x0BA370,
|
||||||
|
0x0BA39C,
|
||||||
|
0x0BA3C8,
|
||||||
|
0x0BA3F4, # Back Weapon
|
||||||
|
0x0BA428,
|
||||||
|
0x0BA45C,
|
||||||
|
0x0BA490,
|
||||||
|
0x0BA4C4,
|
||||||
|
0x0BA4F8,
|
||||||
|
0x0BA52C,
|
||||||
|
0x0BA560,
|
||||||
|
0x0BA594,
|
||||||
|
0x0BA5C8,
|
||||||
|
0x0BA5FC,
|
||||||
|
0x0BA630,
|
||||||
|
0x0BA664,
|
||||||
|
0x0BA698,
|
||||||
|
0x0BA6CC,
|
||||||
|
0x0BA700,
|
||||||
|
0x0BA734,
|
||||||
|
0x0BA768,
|
||||||
|
0x0BA79C,
|
||||||
|
0x0BA7D0,
|
||||||
|
0x0BA804,
|
||||||
|
0x0BA838,
|
||||||
|
0x0BA86C,
|
||||||
|
0x0BA8A0,
|
||||||
|
0x0BA8D4,
|
||||||
|
0x0BA908,
|
||||||
|
0x0BA93C,
|
||||||
|
0x0BA970,
|
||||||
|
0x0BA9A4,
|
||||||
|
0x0BA9D8,
|
||||||
|
0x0BAA0C,
|
||||||
|
0x0BAA40,
|
||||||
|
0x0BAA74,
|
||||||
|
0x0BAAA8,
|
||||||
|
0x0BAADC, #Arm Weapon L's
|
||||||
|
0x0BAB10,
|
||||||
|
0x0BAB44,
|
||||||
|
0x0BAB78,
|
||||||
|
0x0BABAC,
|
||||||
|
0x0BABDC, #Boosters
|
||||||
|
0x0BAC0C,
|
||||||
|
0x0BAC3C,
|
||||||
|
0x0BAC6C,
|
||||||
|
0x0BAC9C,
|
||||||
|
0x0BACCC, #fcs
|
||||||
|
0x0BAD2C,
|
||||||
|
0x0BAD8C,
|
||||||
|
0x0BADEC,
|
||||||
|
0x0BAE4C,
|
||||||
|
0x0BAEAC,
|
||||||
|
0x0BAF0C,
|
||||||
|
0x0BAF6C,
|
||||||
|
0x0BAFCC, #Generator
|
||||||
|
0x0BAFFC,
|
||||||
|
0x0BB02C,
|
||||||
|
0x0BB05C,
|
||||||
|
0x0BB08C,
|
||||||
|
0x0BB0BC,
|
||||||
|
0x0BB0EC,
|
||||||
|
0x0BB11C, #option parts
|
||||||
|
0x0BB148,
|
||||||
|
0x0BB174,
|
||||||
|
0x0BB1A0,
|
||||||
|
0x0BB1CC,
|
||||||
|
0x0BB1F8,
|
||||||
|
0x0BB224,
|
||||||
|
0x0BB250,
|
||||||
|
0x0BB27C,
|
||||||
|
0x0BB2A8,
|
||||||
|
0x0BB2D4
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
GAME_OPTIONS_KEY: str = "g"
|
||||||
|
STARTING_PARTS_KEY: str = "p"
|
||||||
1
worlds/armoredcore/version.py
Normal file
1
worlds/armoredcore/version.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = "0.3.0"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user