mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 08:33:22 -07:00
Merge branch 'main' into allow_collect
This commit is contained in:
34
.github/workflows/build.yml
vendored
34
.github/workflows/build.yml
vendored
@@ -2,10 +2,20 @@
|
||||
|
||||
name: Build
|
||||
|
||||
on: workflow_dispatch
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.github/workflows/build.yml'
|
||||
- 'setup.py'
|
||||
- 'requirements.txt'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/build.yml'
|
||||
- 'setup.py'
|
||||
- 'requirements.txt'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
SNI_VERSION: v0.0.88
|
||||
ENEMIZER_VERSION: 7.1
|
||||
APPIMAGETOOL_VERSION: 13
|
||||
|
||||
@@ -15,15 +25,13 @@ jobs:
|
||||
build-win-py38: # RCs will still be built and signed by hand
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.8'
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/${Env:SNI_VERSION}/sni-${Env:SNI_VERSION}-windows-amd64.zip -OutFile sni.zip
|
||||
Expand-Archive -Path sni.zip -DestinationPath SNI -Force
|
||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
|
||||
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
|
||||
- name: Build
|
||||
@@ -39,7 +47,7 @@ jobs:
|
||||
Rename-Item exe.$NAME Archipelago
|
||||
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
|
||||
- name: Store 7z
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.ZIP_NAME }}
|
||||
path: dist/${{ env.ZIP_NAME }}
|
||||
@@ -49,14 +57,14 @@ jobs:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
# - copy code below to release.yml -
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install base dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
||||
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Install build-time dependencies
|
||||
@@ -69,10 +77,6 @@ jobs:
|
||||
chmod a+rx appimagetool
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
|
||||
tar xf sni-*.tar.xz
|
||||
rm sni-*.tar.xz
|
||||
mv sni-* SNI
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
|
||||
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
|
||||
- name: Build
|
||||
@@ -93,13 +97,13 @@ jobs:
|
||||
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
||||
# - copy code above to release.yml -
|
||||
- name: Store AppImage
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.APPIMAGE_NAME }}
|
||||
path: dist/${{ env.APPIMAGE_NAME }}
|
||||
retention-days: 7
|
||||
- name: Store .tar.gz
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.TAR_NAME }}
|
||||
path: dist/${{ env.TAR_NAME }}
|
||||
|
||||
16
.github/workflows/codeql-analysis.yml
vendored
16
.github/workflows/codeql-analysis.yml
vendored
@@ -14,9 +14,17 @@ name: "CodeQL"
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- '**.py'
|
||||
- '**.js'
|
||||
- '.github/workflows/codeql-analysis.yml'
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- '**.py'
|
||||
- '**.js'
|
||||
- '.github/workflows/codeql-analysis.yml'
|
||||
schedule:
|
||||
- cron: '44 8 * * 1'
|
||||
|
||||
@@ -35,11 +43,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -50,7 +58,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -64,4 +72,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
@@ -3,7 +3,13 @@
|
||||
|
||||
name: lint
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.py'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.py'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -11,9 +17,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install dependencies
|
||||
|
||||
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@@ -8,7 +8,6 @@ on:
|
||||
- '*.*.*'
|
||||
|
||||
env:
|
||||
SNI_VERSION: v0.0.88
|
||||
ENEMIZER_VERSION: 7.1
|
||||
APPIMAGETOOL_VERSION: 13
|
||||
|
||||
@@ -36,14 +35,14 @@ jobs:
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
# - code below copied from build.yml -
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install base dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
|
||||
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Install build-time dependencies
|
||||
@@ -56,10 +55,6 @@ jobs:
|
||||
chmod a+rx appimagetool
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz
|
||||
tar xf sni-*.tar.xz
|
||||
rm sni-*.tar.xz
|
||||
mv sni-* SNI
|
||||
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
|
||||
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
|
||||
- name: Build
|
||||
|
||||
26
.github/workflows/unittests.yml
vendored
26
.github/workflows/unittests.yml
vendored
@@ -3,7 +3,25 @@
|
||||
|
||||
name: unittests
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '!setup.py'
|
||||
- '!*.iss'
|
||||
- '!.gitignore'
|
||||
- '!.github/workflows/**'
|
||||
- '.github/workflows/unittests.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**'
|
||||
- '!docs/**'
|
||||
- '!setup.py'
|
||||
- '!*.iss'
|
||||
- '!.gitignore'
|
||||
- '!.github/workflows/**'
|
||||
- '.github/workflows/unittests.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -23,11 +41,13 @@ jobs:
|
||||
os: windows-latest
|
||||
- python: {version: '3.10'} # current
|
||||
os: windows-latest
|
||||
- python: {version: '3.10'} # current
|
||||
os: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python.version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python.version }}
|
||||
- name: Install dependencies
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,6 +8,7 @@
|
||||
*.apm3
|
||||
*.apmc
|
||||
*.apz5
|
||||
*.aptloz
|
||||
*.pyc
|
||||
*.pyd
|
||||
*.sfc
|
||||
@@ -138,6 +139,7 @@ ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.code-workspace
|
||||
shell.nix
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
@@ -167,6 +169,7 @@ cython_debug/
|
||||
jdk*/
|
||||
minecraft*/
|
||||
minecraft_versions.json
|
||||
!worlds/minecraft/
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
390
BaseClasses.py
390
BaseClasses.py
@@ -2,14 +2,13 @@ from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import secrets
|
||||
import typing # this can go away when Python 3.8 support is dropped
|
||||
from argparse import Namespace
|
||||
from collections import OrderedDict, Counter, deque
|
||||
from enum import unique, IntEnum, IntFlag
|
||||
from collections import OrderedDict, Counter, deque, ChainMap
|
||||
from enum import IntEnum, IntFlag
|
||||
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple
|
||||
|
||||
import NetUtils
|
||||
@@ -73,6 +72,11 @@ class MultiWorld():
|
||||
completion_condition: Dict[int, Callable[[CollectionState], bool]]
|
||||
indirect_connections: Dict[Region, Set[Entrance]]
|
||||
exclude_locations: Dict[int, Options.ExcludeLocations]
|
||||
priority_locations: Dict[int, Options.PriorityLocations]
|
||||
start_inventory: Dict[int, Options.StartInventory]
|
||||
start_hints: Dict[int, Options.StartHints]
|
||||
start_location_hints: Dict[int, Options.StartLocationHints]
|
||||
item_links: Dict[int, Options.ItemLinks]
|
||||
|
||||
game: Dict[int, str]
|
||||
|
||||
@@ -761,169 +765,9 @@ class CollectionState():
|
||||
found += self.prog_items[item_name, player]
|
||||
return found
|
||||
|
||||
def can_buy_unlimited(self, item: str, player: int) -> bool:
|
||||
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
|
||||
shop in self.multiworld.shops)
|
||||
|
||||
def can_buy(self, item: str, player: int) -> bool:
|
||||
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(self) for
|
||||
shop in self.multiworld.shops)
|
||||
|
||||
def item_count(self, item: str, player: int) -> int:
|
||||
return self.prog_items[item, player]
|
||||
|
||||
def has_triforce_pieces(self, count: int, player: int) -> bool:
|
||||
return self.item_count('Triforce Piece', player) + self.item_count('Power Star', player) >= count
|
||||
|
||||
def has_crystals(self, count: int, player: int) -> bool:
|
||||
found: int = 0
|
||||
for crystalnumber in range(1, 8):
|
||||
found += self.prog_items[f"Crystal {crystalnumber}", player]
|
||||
if found >= count:
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_lift_rocks(self, player: int):
|
||||
return self.has('Power Glove', player) or self.has('Titans Mitts', player)
|
||||
|
||||
def bottle_count(self, player: int) -> int:
|
||||
return min(self.multiworld.difficulty_requirements[player].progressive_bottle_limit,
|
||||
self.count_group("Bottles", player))
|
||||
|
||||
def has_hearts(self, player: int, count: int) -> int:
|
||||
# Warning: This only considers items that are marked as advancement items
|
||||
return self.heart_count(player) >= count
|
||||
|
||||
def heart_count(self, player: int) -> int:
|
||||
# Warning: This only considers items that are marked as advancement items
|
||||
diff = self.multiworld.difficulty_requirements[player]
|
||||
return min(self.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \
|
||||
+ self.item_count('Sanctuary Heart Container', player) \
|
||||
+ min(self.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
|
||||
+ 3 # starting hearts
|
||||
|
||||
def can_lift_heavy_rocks(self, player: int) -> bool:
|
||||
return self.has('Titans Mitts', player)
|
||||
|
||||
def can_extend_magic(self, player: int, smallmagic: int = 16,
|
||||
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
|
||||
basemagic = 8
|
||||
if self.has('Magic Upgrade (1/4)', player):
|
||||
basemagic = 32
|
||||
elif self.has('Magic Upgrade (1/2)', player):
|
||||
basemagic = 16
|
||||
if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player):
|
||||
if self.multiworld.item_functionality[player] == 'hard' and not fullrefill:
|
||||
basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count(player))
|
||||
elif self.multiworld.item_functionality[player] == 'expert' and not fullrefill:
|
||||
basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count(player))
|
||||
else:
|
||||
basemagic = basemagic + basemagic * self.bottle_count(player)
|
||||
return basemagic >= smallmagic
|
||||
|
||||
def can_kill_most_things(self, player: int, enemies: int = 5) -> bool:
|
||||
return (self.has_melee_weapon(player)
|
||||
or self.has('Cane of Somaria', player)
|
||||
or (self.has('Cane of Byrna', player) and (enemies < 6 or self.can_extend_magic(player)))
|
||||
or self.can_shoot_arrows(player)
|
||||
or self.has('Fire Rod', player)
|
||||
or (self.has('Bombs (10)', player) and enemies < 6))
|
||||
|
||||
def can_shoot_arrows(self, player: int) -> bool:
|
||||
if self.multiworld.retro_bow[player]:
|
||||
return (self.has('Bow', player) or self.has('Silver Bow', player)) and self.can_buy('Single Arrow', player)
|
||||
return self.has('Bow', player) or self.has('Silver Bow', player)
|
||||
|
||||
def can_get_good_bee(self, player: int) -> bool:
|
||||
cave = self.multiworld.get_region('Good Bee Cave', player)
|
||||
return (
|
||||
self.has_group("Bottles", player) and
|
||||
self.has('Bug Catching Net', player) and
|
||||
(self.has('Pegasus Boots', player) or (self.has_sword(player) and self.has('Quake', player))) and
|
||||
cave.can_reach(self) and
|
||||
self.is_not_bunny(cave, player)
|
||||
)
|
||||
|
||||
def can_retrieve_tablet(self, player: int) -> bool:
|
||||
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
|
||||
(self.multiworld.swordless[player] and
|
||||
self.has("Hammer", player)))
|
||||
|
||||
def has_sword(self, player: int) -> bool:
|
||||
return self.has('Fighter Sword', player) \
|
||||
or self.has('Master Sword', player) \
|
||||
or self.has('Tempered Sword', player) \
|
||||
or self.has('Golden Sword', player)
|
||||
|
||||
def has_beam_sword(self, player: int) -> bool:
|
||||
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword',
|
||||
player)
|
||||
|
||||
def has_melee_weapon(self, player: int) -> bool:
|
||||
return self.has_sword(player) or self.has('Hammer', player)
|
||||
|
||||
def has_fire_source(self, player: int) -> bool:
|
||||
return self.has('Fire Rod', player) or self.has('Lamp', player)
|
||||
|
||||
def can_melt_things(self, player: int) -> bool:
|
||||
return self.has('Fire Rod', player) or \
|
||||
(self.has('Bombos', player) and
|
||||
(self.multiworld.swordless[player] or
|
||||
self.has_sword(player)))
|
||||
|
||||
def can_avoid_lasers(self, player: int) -> bool:
|
||||
return self.has('Mirror Shield', player) or self.has('Cane of Byrna', player) or self.has('Cape', player)
|
||||
|
||||
def is_not_bunny(self, region: Region, player: int) -> bool:
|
||||
if self.has('Moon Pearl', player):
|
||||
return True
|
||||
|
||||
return region.is_light_world if self.multiworld.mode[player] != 'inverted' else region.is_dark_world
|
||||
|
||||
def can_reach_light_world(self, player: int) -> bool:
|
||||
if True in [i.is_light_world for i in self.reachable_regions[player]]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_reach_dark_world(self, player: int) -> bool:
|
||||
if True in [i.is_dark_world for i in self.reachable_regions[player]]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_misery_mire_medallion(self, player: int) -> bool:
|
||||
return self.has(self.multiworld.required_medallions[player][0], player)
|
||||
|
||||
def has_turtle_rock_medallion(self, player: int) -> bool:
|
||||
return self.has(self.multiworld.required_medallions[player][1], player)
|
||||
|
||||
def can_boots_clip_lw(self, player: int) -> bool:
|
||||
if self.multiworld.mode[player] == 'inverted':
|
||||
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
|
||||
return self.has('Pegasus Boots', player)
|
||||
|
||||
def can_boots_clip_dw(self, player: int) -> bool:
|
||||
if self.multiworld.mode[player] != 'inverted':
|
||||
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
|
||||
return self.has('Pegasus Boots', player)
|
||||
|
||||
def can_get_glitched_speed_lw(self, player: int) -> bool:
|
||||
rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])]
|
||||
if self.multiworld.mode[player] == 'inverted':
|
||||
rules.append(self.has('Moon Pearl', player))
|
||||
return all(rules)
|
||||
|
||||
def can_superbunny_mirror_with_sword(self, player: int) -> bool:
|
||||
return self.has('Magic Mirror', player) and self.has_sword(player)
|
||||
|
||||
def can_get_glitched_speed_dw(self, player: int) -> bool:
|
||||
rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])]
|
||||
if self.multiworld.mode[player] != 'inverted':
|
||||
rules.append(self.has('Moon Pearl', player))
|
||||
return all(rules)
|
||||
|
||||
def can_bomb_clip(self, region: Region, player: int) -> bool:
|
||||
return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player)
|
||||
|
||||
def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool:
|
||||
if location:
|
||||
self.locations_checked.add(location)
|
||||
@@ -959,12 +803,6 @@ class Region:
|
||||
exits: List[Entrance]
|
||||
locations: List[Location]
|
||||
dungeon: Optional[Dungeon] = None
|
||||
shop: Optional = None
|
||||
|
||||
# LttP specific. TODO: move to a LttPRegion
|
||||
# will be set after making connections.
|
||||
is_light_world: bool = False
|
||||
is_dark_world: bool = False
|
||||
|
||||
def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
|
||||
self.name = name
|
||||
@@ -1129,7 +967,7 @@ class Location:
|
||||
self.parent_region = parent
|
||||
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||
return (self.always_allow(state, item)
|
||||
return ((self.always_allow(state, item) and item.name not in state.multiworld.non_local_items[item.player])
|
||||
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
||||
and self.item_rule(item)
|
||||
and (not check_access or self.can_reach(state))))
|
||||
@@ -1261,13 +1099,9 @@ class Spoiler():
|
||||
self.multiworld = world
|
||||
self.hashes = {}
|
||||
self.entrances = OrderedDict()
|
||||
self.medallions = {}
|
||||
self.playthrough = {}
|
||||
self.unreachables = set()
|
||||
self.locations = {}
|
||||
self.paths = {}
|
||||
self.shops = []
|
||||
self.bosses = OrderedDict()
|
||||
|
||||
def set_entrance(self, entrance: str, exit_: str, direction: str, player: int):
|
||||
if self.multiworld.players == 1:
|
||||
@@ -1277,126 +1111,6 @@ class Spoiler():
|
||||
self.entrances[(entrance, direction, player)] = OrderedDict(
|
||||
[('player', player), ('entrance', entrance), ('exit', exit_), ('direction', direction)])
|
||||
|
||||
def parse_data(self):
|
||||
from worlds.alttp.SubClasses import LTTPRegionType
|
||||
self.medallions = OrderedDict()
|
||||
for player in self.multiworld.get_game_players("A Link to the Past"):
|
||||
self.medallions[f'Misery Mire ({self.multiworld.get_player_name(player)})'] = \
|
||||
self.multiworld.required_medallions[player][0]
|
||||
self.medallions[f'Turtle Rock ({self.multiworld.get_player_name(player)})'] = \
|
||||
self.multiworld.required_medallions[player][1]
|
||||
|
||||
self.locations = OrderedDict()
|
||||
listed_locations = set()
|
||||
lw_locations = []
|
||||
dw_locations = []
|
||||
cave_locations = []
|
||||
for loc in self.multiworld.get_locations():
|
||||
if loc.game == "A Link to the Past":
|
||||
if loc not in listed_locations and loc.parent_region and \
|
||||
loc.parent_region.type == LTTPRegionType.LightWorld and loc.show_in_spoiler:
|
||||
lw_locations.append(loc)
|
||||
elif loc not in listed_locations and loc.parent_region and \
|
||||
loc.parent_region.type == LTTPRegionType.DarkWorld and loc.show_in_spoiler:
|
||||
dw_locations.append(loc)
|
||||
elif loc not in listed_locations and loc.parent_region and \
|
||||
loc.parent_region.type == LTTPRegionType.Cave and loc.show_in_spoiler:
|
||||
cave_locations.append(loc)
|
||||
|
||||
self.locations['Light World'] = OrderedDict(
|
||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
||||
lw_locations])
|
||||
listed_locations.update(lw_locations)
|
||||
|
||||
self.locations['Dark World'] = OrderedDict(
|
||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
||||
dw_locations])
|
||||
listed_locations.update(dw_locations)
|
||||
|
||||
self.locations['Caves'] = OrderedDict(
|
||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
||||
cave_locations])
|
||||
listed_locations.update(cave_locations)
|
||||
|
||||
for dungeon in self.multiworld.dungeons.values():
|
||||
dungeon_locations = [loc for loc in self.multiworld.get_locations() if
|
||||
loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and loc.show_in_spoiler]
|
||||
self.locations[str(dungeon)] = OrderedDict(
|
||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
||||
dungeon_locations])
|
||||
listed_locations.update(dungeon_locations)
|
||||
|
||||
other_locations = [loc for loc in self.multiworld.get_locations() if
|
||||
loc not in listed_locations and loc.show_in_spoiler]
|
||||
if other_locations:
|
||||
self.locations['Other Locations'] = OrderedDict(
|
||||
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
|
||||
other_locations])
|
||||
listed_locations.update(other_locations)
|
||||
|
||||
self.shops = []
|
||||
from worlds.alttp.Shops import ShopType, price_type_display_name, price_rate_display
|
||||
for shop in self.multiworld.shops:
|
||||
if not shop.custom:
|
||||
continue
|
||||
shopdata = {
|
||||
'location': str(shop.region),
|
||||
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
|
||||
}
|
||||
for index, item in enumerate(shop.inventory):
|
||||
if item is None:
|
||||
continue
|
||||
my_price = item['price'] // price_rate_display.get(item['price_type'], 1)
|
||||
shopdata['item_{}'.format(
|
||||
index)] = f"{item['item']} — {my_price} {price_type_display_name[item['price_type']]}"
|
||||
|
||||
if item['player'] > 0:
|
||||
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('—',
|
||||
'(Player {}) — '.format(
|
||||
item['player']))
|
||||
|
||||
if item['max'] == 0:
|
||||
continue
|
||||
shopdata['item_{}'.format(index)] += " x {}".format(item['max'])
|
||||
|
||||
if item['replacement'] is None:
|
||||
continue
|
||||
shopdata['item_{}'.format(
|
||||
index)] += f", {item['replacement']} - {item['replacement_price']} {price_type_display_name[item['replacement_price_type']]}"
|
||||
self.shops.append(shopdata)
|
||||
|
||||
for player in self.multiworld.get_game_players("A Link to the Past"):
|
||||
self.bosses[str(player)] = OrderedDict()
|
||||
self.bosses[str(player)]["Eastern Palace"] = self.multiworld.get_dungeon("Eastern Palace", player).boss.name
|
||||
self.bosses[str(player)]["Desert Palace"] = self.multiworld.get_dungeon("Desert Palace", player).boss.name
|
||||
self.bosses[str(player)]["Tower Of Hera"] = self.multiworld.get_dungeon("Tower of Hera", player).boss.name
|
||||
self.bosses[str(player)]["Hyrule Castle"] = "Agahnim"
|
||||
self.bosses[str(player)]["Palace Of Darkness"] = self.multiworld.get_dungeon("Palace of Darkness",
|
||||
player).boss.name
|
||||
self.bosses[str(player)]["Swamp Palace"] = self.multiworld.get_dungeon("Swamp Palace", player).boss.name
|
||||
self.bosses[str(player)]["Skull Woods"] = self.multiworld.get_dungeon("Skull Woods", player).boss.name
|
||||
self.bosses[str(player)]["Thieves Town"] = self.multiworld.get_dungeon("Thieves Town", player).boss.name
|
||||
self.bosses[str(player)]["Ice Palace"] = self.multiworld.get_dungeon("Ice Palace", player).boss.name
|
||||
self.bosses[str(player)]["Misery Mire"] = self.multiworld.get_dungeon("Misery Mire", player).boss.name
|
||||
self.bosses[str(player)]["Turtle Rock"] = self.multiworld.get_dungeon("Turtle Rock", player).boss.name
|
||||
if self.multiworld.mode[player] != 'inverted':
|
||||
self.bosses[str(player)]["Ganons Tower Basement"] = \
|
||||
self.multiworld.get_dungeon('Ganons Tower', player).bosses['bottom'].name
|
||||
self.bosses[str(player)]["Ganons Tower Middle"] = self.multiworld.get_dungeon('Ganons Tower', player).bosses[
|
||||
'middle'].name
|
||||
self.bosses[str(player)]["Ganons Tower Top"] = self.multiworld.get_dungeon('Ganons Tower', player).bosses[
|
||||
'top'].name
|
||||
else:
|
||||
self.bosses[str(player)]["Ganons Tower Basement"] = \
|
||||
self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name
|
||||
self.bosses[str(player)]["Ganons Tower Middle"] = \
|
||||
self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name
|
||||
self.bosses[str(player)]["Ganons Tower Top"] = \
|
||||
self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name
|
||||
|
||||
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
|
||||
self.bosses[str(player)]["Ganon"] = "Ganon"
|
||||
|
||||
def create_playthrough(self, create_paths: bool = True):
|
||||
"""Destructive to the world while it is run, damage gets repaired afterwards."""
|
||||
from itertools import chain
|
||||
@@ -1548,35 +1262,12 @@ class Spoiler():
|
||||
self.paths[str(multiworld.get_region('Inverted Big Bomb Shop', player))] = \
|
||||
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))
|
||||
|
||||
def to_json(self):
|
||||
self.parse_data()
|
||||
out = OrderedDict()
|
||||
out['Entrances'] = list(self.entrances.values())
|
||||
out.update(self.locations)
|
||||
out['Special'] = self.medallions
|
||||
if self.hashes:
|
||||
out['Hashes'] = self.hashes
|
||||
if self.shops:
|
||||
out['Shops'] = self.shops
|
||||
out['playthrough'] = self.playthrough
|
||||
out['paths'] = self.paths
|
||||
out['Bosses'] = self.bosses
|
||||
|
||||
return json.dumps(out)
|
||||
|
||||
def to_file(self, filename: str):
|
||||
self.parse_data()
|
||||
|
||||
def bool_to_text(variable: Union[bool, str]) -> str:
|
||||
if type(variable) == str:
|
||||
return variable
|
||||
return 'Yes' if variable else 'No'
|
||||
|
||||
def write_option(option_key: str, option_obj: type(Options.Option)):
|
||||
res = getattr(self.multiworld, option_key)[player]
|
||||
display_name = getattr(option_obj, "display_name", option_key)
|
||||
try:
|
||||
outfile.write(f'{display_name + ":":33}{res.get_current_option_name()}\n')
|
||||
outfile.write(f'{display_name + ":":33}{res.current_option_name}\n')
|
||||
except:
|
||||
raise Exception
|
||||
|
||||
@@ -1593,46 +1284,13 @@ class Spoiler():
|
||||
if self.multiworld.players > 1:
|
||||
outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player)))
|
||||
outfile.write('Game: %s\n' % self.multiworld.game[player])
|
||||
for f_option, option in Options.per_game_common_options.items():
|
||||
|
||||
options = ChainMap(Options.per_game_common_options, self.multiworld.worlds[player].option_definitions)
|
||||
for f_option, option in options.items():
|
||||
write_option(f_option, option)
|
||||
options = self.multiworld.worlds[player].option_definitions
|
||||
if options:
|
||||
for f_option, option in options.items():
|
||||
write_option(f_option, option)
|
||||
|
||||
AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile)
|
||||
|
||||
if player in self.multiworld.get_game_players("A Link to the Past"):
|
||||
outfile.write('%s%s\n' % ('Hash: ', self.hashes[player]))
|
||||
|
||||
outfile.write('Logic: %s\n' % self.multiworld.logic[player])
|
||||
outfile.write('Dark Room Logic: %s\n' % self.multiworld.dark_room_logic[player])
|
||||
outfile.write('Mode: %s\n' % self.multiworld.mode[player])
|
||||
outfile.write('Goal: %s\n' % self.multiworld.goal[player])
|
||||
if "triforce" in self.multiworld.goal[player]: # triforce hunt
|
||||
outfile.write("Pieces available for Triforce: %s\n" %
|
||||
self.multiworld.triforce_pieces_available[player])
|
||||
outfile.write("Pieces required for Triforce: %s\n" %
|
||||
self.multiworld.triforce_pieces_required[player])
|
||||
outfile.write('Difficulty: %s\n' % self.multiworld.difficulty[player])
|
||||
outfile.write('Item Functionality: %s\n' % self.multiworld.item_functionality[player])
|
||||
outfile.write('Entrance Shuffle: %s\n' % self.multiworld.shuffle[player])
|
||||
if self.multiworld.shuffle[player] != "vanilla":
|
||||
outfile.write('Entrance Shuffle Seed %s\n' % self.multiworld.worlds[player].er_seed)
|
||||
outfile.write('Shop inventory shuffle: %s\n' %
|
||||
bool_to_text("i" in self.multiworld.shop_shuffle[player]))
|
||||
outfile.write('Shop price shuffle: %s\n' %
|
||||
bool_to_text("p" in self.multiworld.shop_shuffle[player]))
|
||||
outfile.write('Shop upgrade shuffle: %s\n' %
|
||||
bool_to_text("u" in self.multiworld.shop_shuffle[player]))
|
||||
outfile.write('New Shop inventory: %s\n' %
|
||||
bool_to_text("g" in self.multiworld.shop_shuffle[player] or
|
||||
"f" in self.multiworld.shop_shuffle[player]))
|
||||
outfile.write('Custom Potion Shop: %s\n' %
|
||||
bool_to_text("w" in self.multiworld.shop_shuffle[player]))
|
||||
outfile.write('Enemy health: %s\n' % self.multiworld.enemy_health[player])
|
||||
outfile.write('Enemy damage: %s\n' % self.multiworld.enemy_damage[player])
|
||||
outfile.write('Prize shuffle %s\n' %
|
||||
self.multiworld.shuffle_prizes[player])
|
||||
if self.entrances:
|
||||
outfile.write('\n\nEntrances:\n\n')
|
||||
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(entry["player"])}: '
|
||||
@@ -1641,30 +1299,14 @@ class Spoiler():
|
||||
'<=' if entry['direction'] == 'exit' else '=>',
|
||||
entry['exit']) for entry in self.entrances.values()]))
|
||||
|
||||
if self.medallions:
|
||||
outfile.write('\n\nMedallions:\n')
|
||||
for dungeon, medallion in self.medallions.items():
|
||||
outfile.write(f'\n{dungeon}: {medallion}')
|
||||
|
||||
AutoWorld.call_all(self.multiworld, "write_spoiler", outfile)
|
||||
|
||||
locations = [(str(location), str(location.item) if location.item is not None else "Nothing")
|
||||
for location in self.multiworld.get_locations() if location.show_in_spoiler]
|
||||
outfile.write('\n\nLocations:\n\n')
|
||||
outfile.write('\n'.join(
|
||||
['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in
|
||||
grouping.items()]))
|
||||
['%s: %s' % (location, item) for location, item in locations]))
|
||||
|
||||
if self.shops:
|
||||
outfile.write('\n\nShops:\n\n')
|
||||
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(
|
||||
item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if
|
||||
item)) for shop in self.shops))
|
||||
|
||||
for player in self.multiworld.get_game_players("A Link to the Past"):
|
||||
if self.multiworld.boss_shuffle[player] != 'none':
|
||||
bossmap = self.bosses[str(player)] if self.multiworld.players > 1 else self.bosses
|
||||
outfile.write(
|
||||
f'\n\nBosses{(f" ({self.multiworld.get_player_name(player)})" if self.multiworld.players > 1 else "")}:\n')
|
||||
outfile.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
|
||||
outfile.write('\n\nPlaythrough:\n\n')
|
||||
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join(
|
||||
[' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [
|
||||
|
||||
@@ -63,7 +63,7 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
|
||||
def _cmd_received(self) -> bool:
|
||||
"""List all received items"""
|
||||
logger.info(f'{len(self.ctx.items_received)} received items:')
|
||||
self.output(f'{len(self.ctx.items_received)} received items:')
|
||||
for index, item in enumerate(self.ctx.items_received, 1):
|
||||
self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}")
|
||||
return True
|
||||
|
||||
3
Fill.py
3
Fill.py
@@ -840,8 +840,7 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
maxcount = placement['count']['target']
|
||||
from_pool = placement['from_pool']
|
||||
|
||||
candidates = list(location for location in world.get_unfilled_locations_for_players(locations,
|
||||
worlds))
|
||||
candidates = list(world.get_unfilled_locations_for_players(locations, sorted(worlds)))
|
||||
world.random.shuffle(candidates)
|
||||
world.random.shuffle(items)
|
||||
count = 0
|
||||
|
||||
@@ -107,7 +107,7 @@ def main(args=None, callback=ERmain):
|
||||
player_files = {}
|
||||
for file in os.scandir(args.player_files_path):
|
||||
fname = file.name
|
||||
if file.is_file() and not file.name.startswith(".") and \
|
||||
if file.is_file() and not fname.startswith(".") and \
|
||||
os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
|
||||
path = os.path.join(args.player_files_path, fname)
|
||||
try:
|
||||
|
||||
@@ -132,7 +132,8 @@ components: Iterable[Component] = (
|
||||
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient'),
|
||||
# SNI
|
||||
Component('SNI Client', 'SNIClient',
|
||||
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw')),
|
||||
file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3',
|
||||
'.apsmw', '.apl2ac')),
|
||||
Component('LttP Adjuster', 'LttPAdjuster'),
|
||||
# Factorio
|
||||
Component('Factorio Client', 'FactorioClient'),
|
||||
@@ -147,10 +148,14 @@ components: Iterable[Component] = (
|
||||
Component('FF1 Client', 'FF1Client'),
|
||||
# Pokémon
|
||||
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
|
||||
# TLoZ
|
||||
Component('Zelda 1 Client', 'Zelda1Client'),
|
||||
# ChecksFinder
|
||||
Component('ChecksFinder Client', 'ChecksFinderClient'),
|
||||
# Starcraft 2
|
||||
Component('Starcraft 2 Client', 'Starcraft2Client'),
|
||||
# Wargroove
|
||||
Component('Wargroove Client', 'WargrooveClient'),
|
||||
# Zillion
|
||||
Component('Zillion Client', 'ZillionClient',
|
||||
file_identifier=SuffixIdentifier('.apzl')),
|
||||
|
||||
6
Main.py
6
Main.py
@@ -38,7 +38,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
world = MultiWorld(args.multi)
|
||||
|
||||
logger = logging.getLogger()
|
||||
world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed))
|
||||
world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
|
||||
world.plando_options = args.plando_options
|
||||
|
||||
world.shuffle = args.shuffle.copy()
|
||||
@@ -53,7 +53,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
world.enemy_damage = args.enemy_damage.copy()
|
||||
world.beemizer_total_chance = args.beemizer_total_chance.copy()
|
||||
world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
|
||||
world.timer = args.timer.copy()
|
||||
world.countdown_start_time = args.countdown_start_time.copy()
|
||||
world.red_clock_time = args.red_clock_time.copy()
|
||||
world.blue_clock_time = args.blue_clock_time.copy()
|
||||
@@ -79,7 +78,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
world.state = CollectionState(world)
|
||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
||||
|
||||
logger.info("Found World Types:")
|
||||
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
|
||||
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
|
||||
|
||||
max_item = 0
|
||||
@@ -362,6 +361,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
if game_world.data_version == 0 and game_world.game not in datapackage:
|
||||
datapackage[game_world.game] = worlds.network_data_package["games"][game_world.game]
|
||||
datapackage[game_world.game]["item_name_groups"] = game_world.item_name_groups
|
||||
datapackage[game_world.game]["location_name_groups"] = game_world.location_name_groups
|
||||
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
|
||||
@@ -163,7 +163,9 @@ class Context:
|
||||
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
|
||||
item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
|
||||
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
||||
location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
|
||||
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||
all_location_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||
non_hintable_names: typing.Dict[str, typing.Set[str]]
|
||||
|
||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||
@@ -233,7 +235,9 @@ class Context:
|
||||
# init empty to satisfy linter, I suppose
|
||||
self.gamespackage = {}
|
||||
self.item_name_groups = {}
|
||||
self.location_name_groups = {}
|
||||
self.all_item_and_group_names = {}
|
||||
self.all_location_and_group_names = {}
|
||||
self.non_hintable_names = collections.defaultdict(frozenset)
|
||||
|
||||
self._load_game_data()
|
||||
@@ -245,6 +249,8 @@ class Context:
|
||||
|
||||
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
|
||||
worlds.AutoWorldRegister.world_types.items()}
|
||||
self.location_name_groups = {world_name: world.location_name_groups for world_name, world in
|
||||
worlds.AutoWorldRegister.world_types.items()}
|
||||
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
||||
self.non_hintable_names[world_name] = world.hint_blacklist
|
||||
|
||||
@@ -256,6 +262,8 @@ class Context:
|
||||
self.location_names[location_id] = location_name
|
||||
self.all_item_and_group_names[game_name] = \
|
||||
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
|
||||
self.all_location_and_group_names[game_name] = \
|
||||
set(game_package["location_name_to_id"]) | set(self.location_name_groups[game_name])
|
||||
|
||||
def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
|
||||
return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None
|
||||
@@ -431,10 +439,14 @@ class Context:
|
||||
logging.info(f"Loading custom datapackage for game {game_name}")
|
||||
self.gamespackage[game_name] = data
|
||||
self.item_name_groups[game_name] = data["item_name_groups"]
|
||||
self.location_name_groups[game_name] = data["location_name_groups"]
|
||||
del data["item_name_groups"] # remove from datapackage, but keep in self.item_name_groups
|
||||
del data["location_name_groups"]
|
||||
self._init_game_data()
|
||||
for game_name, data in self.item_name_groups.items():
|
||||
self.read_data[f"item_name_groups_{game_name}"] = lambda lgame=game_name: self.item_name_groups[lgame]
|
||||
for game_name, data in self.location_name_groups.items():
|
||||
self.read_data[f"location_name_groups_{game_name}"] = lambda lgame=game_name: self.location_name_groups[lgame]
|
||||
|
||||
# saving
|
||||
|
||||
@@ -1408,7 +1420,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
if game not in self.ctx.all_item_and_group_names:
|
||||
self.output("Can't look up item/location for unknown game. Hint for ID instead.")
|
||||
return False
|
||||
names = self.ctx.location_names_for_game(game) \
|
||||
names = self.ctx.all_location_and_group_names[game] \
|
||||
if for_location else \
|
||||
self.ctx.all_item_and_group_names[game]
|
||||
hint_name, usable, response = get_intended_text(input_text, names)
|
||||
@@ -1424,6 +1436,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
|
||||
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
elif hint_name in self.ctx.location_name_groups[game]: # location group name
|
||||
hints = []
|
||||
for loc_name in self.ctx.location_name_groups[game][hint_name]:
|
||||
if loc_name in self.ctx.location_names_for_game(game):
|
||||
hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name))
|
||||
else: # location name
|
||||
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
|
||||
|
||||
71
Options.py
71
Options.py
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
import abc
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
import math
|
||||
import numbers
|
||||
@@ -9,6 +10,10 @@ import random
|
||||
from schema import Schema, And, Or, Optional
|
||||
from Utils import get_fuzzy_results
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from BaseClasses import PlandoOptions
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
|
||||
class AssembleOptions(abc.ABCMeta):
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
@@ -95,11 +100,11 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
supports_weighting = True
|
||||
|
||||
# filled by AssembleOptions:
|
||||
name_lookup: typing.Dict[int, str]
|
||||
name_lookup: typing.Dict[T, str]
|
||||
options: typing.Dict[str, int]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.get_current_option_name()})"
|
||||
return f"{self.__class__.__name__}({self.current_option_name})"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.value)
|
||||
@@ -109,7 +114,14 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
return self.name_lookup[self.value]
|
||||
|
||||
def get_current_option_name(self) -> str:
|
||||
"""For display purposes."""
|
||||
"""Deprecated. use current_option_name instead. TODO remove around 0.4"""
|
||||
logging.warning(DeprecationWarning(f"get_current_option_name for {self.__class__.__name__} is deprecated."
|
||||
f" use current_option_name instead. Worlds should use {self}.current_key"))
|
||||
return self.current_option_name
|
||||
|
||||
@property
|
||||
def current_option_name(self) -> str:
|
||||
"""For display purposes. Worlds should be using current_key."""
|
||||
return self.get_option_name(self.value)
|
||||
|
||||
@classmethod
|
||||
@@ -131,17 +143,14 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
...
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from Generate import PlandoOptions
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
def verify(self, world: World, player_name: str, plando_options: PlandoOptions) -> None:
|
||||
def verify(self, world: typing.Type[World], player_name: str, plando_options: PlandoOptions) -> None:
|
||||
pass
|
||||
else:
|
||||
def verify(self, *args, **kwargs) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class FreeText(Option):
|
||||
class FreeText(Option[str]):
|
||||
"""Text option that allows users to enter strings.
|
||||
Needs to be validated by the world or option definition."""
|
||||
|
||||
@@ -162,7 +171,7 @@ class FreeText(Option):
|
||||
return cls.from_text(str(data))
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: T) -> str:
|
||||
def get_option_name(cls, value: str) -> str:
|
||||
return value
|
||||
|
||||
|
||||
@@ -424,6 +433,7 @@ class Choice(NumericOption):
|
||||
|
||||
class TextChoice(Choice):
|
||||
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
|
||||
value: typing.Union[str, int]
|
||||
|
||||
def __init__(self, value: typing.Union[str, int]):
|
||||
assert isinstance(value, str) or isinstance(value, int), \
|
||||
@@ -434,8 +444,7 @@ class TextChoice(Choice):
|
||||
def current_key(self) -> str:
|
||||
if isinstance(self.value, str):
|
||||
return self.value
|
||||
else:
|
||||
return self.name_lookup[self.value]
|
||||
return super().current_key
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> TextChoice:
|
||||
@@ -450,7 +459,7 @@ class TextChoice(Choice):
|
||||
def get_option_name(cls, value: T) -> str:
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return cls.name_lookup[value]
|
||||
return super().get_option_name(value)
|
||||
|
||||
def __eq__(self, other: typing.Any):
|
||||
if isinstance(other, self.__class__):
|
||||
@@ -573,12 +582,11 @@ class PlandoBosses(TextChoice, metaclass=BossMeta):
|
||||
def valid_location_name(cls, value: str) -> bool:
|
||||
return value in cls.locations
|
||||
|
||||
def verify(self, world, player_name: str, plando_options) -> None:
|
||||
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
||||
if isinstance(self.value, int):
|
||||
return
|
||||
from Generate import PlandoOptions
|
||||
from BaseClasses import PlandoOptions
|
||||
if not(PlandoOptions.bosses & plando_options):
|
||||
import logging
|
||||
# plando is disabled but plando options were given so pull the option and change it to an int
|
||||
option = self.value.split(";")[-1]
|
||||
self.value = self.options[option]
|
||||
@@ -716,7 +724,7 @@ class VerifyKeys:
|
||||
value: typing.Any
|
||||
|
||||
@classmethod
|
||||
def verify_keys(cls, data):
|
||||
def verify_keys(cls, data: typing.List[str]):
|
||||
if cls.valid_keys:
|
||||
data = set(data)
|
||||
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
|
||||
@@ -725,12 +733,17 @@ class VerifyKeys:
|
||||
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
|
||||
f"Allowed keys: {cls.valid_keys}.")
|
||||
|
||||
def verify(self, world, player_name: str, plando_options) -> None:
|
||||
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
||||
if self.convert_name_groups and self.verify_item_name:
|
||||
new_value = type(self.value)() # empty container of whatever value is
|
||||
for item_name in self.value:
|
||||
new_value |= world.item_name_groups.get(item_name, {item_name})
|
||||
self.value = new_value
|
||||
elif self.convert_name_groups and self.verify_location_name:
|
||||
new_value = type(self.value)()
|
||||
for loc_name in self.value:
|
||||
new_value |= world.location_name_groups.get(loc_name, {loc_name})
|
||||
self.value = new_value
|
||||
if self.verify_item_name:
|
||||
for item_name in self.value:
|
||||
if item_name not in world.item_names:
|
||||
@@ -830,7 +843,9 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||
return item in self.value
|
||||
|
||||
|
||||
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
||||
class ItemSet(OptionSet):
|
||||
verify_item_name = True
|
||||
convert_name_groups = True
|
||||
|
||||
|
||||
class Accessibility(Choice):
|
||||
@@ -872,11 +887,6 @@ common_options = {
|
||||
}
|
||||
|
||||
|
||||
class ItemSet(OptionSet):
|
||||
verify_item_name = True
|
||||
convert_name_groups = True
|
||||
|
||||
|
||||
class LocalItems(ItemSet):
|
||||
"""Forces these items to be in their native world."""
|
||||
display_name = "Local Items"
|
||||
@@ -898,22 +908,23 @@ class StartHints(ItemSet):
|
||||
display_name = "Start Hints"
|
||||
|
||||
|
||||
class StartLocationHints(OptionSet):
|
||||
class LocationSet(OptionSet):
|
||||
verify_location_name = True
|
||||
|
||||
|
||||
class StartLocationHints(LocationSet):
|
||||
"""Start with these locations and their item prefilled into the !hint command"""
|
||||
display_name = "Start Location Hints"
|
||||
verify_location_name = True
|
||||
|
||||
|
||||
class ExcludeLocations(OptionSet):
|
||||
class ExcludeLocations(LocationSet):
|
||||
"""Prevent these locations from having an important item"""
|
||||
display_name = "Excluded Locations"
|
||||
verify_location_name = True
|
||||
|
||||
|
||||
class PriorityLocations(OptionSet):
|
||||
class PriorityLocations(LocationSet):
|
||||
"""Prevent these locations from having an unimportant item"""
|
||||
display_name = "Priority Locations"
|
||||
verify_location_name = True
|
||||
|
||||
|
||||
class DeathLink(Toggle):
|
||||
@@ -954,7 +965,7 @@ class ItemLinks(OptionList):
|
||||
pool |= {item_name}
|
||||
return pool
|
||||
|
||||
def verify(self, world, player_name: str, plando_options) -> None:
|
||||
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
||||
link: dict
|
||||
super(ItemLinks, self).verify(world, player_name, plando_options)
|
||||
existing_links = set()
|
||||
|
||||
@@ -17,7 +17,7 @@ from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandP
|
||||
from worlds.pokemon_rb.locations import location_data
|
||||
from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch
|
||||
|
||||
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}}
|
||||
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}}
|
||||
location_bytes_bits = {}
|
||||
for location in location_data:
|
||||
if location.ram_address is not None:
|
||||
@@ -40,7 +40,7 @@ CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||
|
||||
DISPLAY_MSGS = True
|
||||
|
||||
SCRIPT_VERSION = 1
|
||||
SCRIPT_VERSION = 3
|
||||
|
||||
|
||||
class GBCommandProcessor(ClientCommandProcessor):
|
||||
@@ -70,6 +70,8 @@ class GBContext(CommonContext):
|
||||
self.set_deathlink = False
|
||||
self.client_compatibility_mode = 0
|
||||
self.items_handling = 0b001
|
||||
self.sent_release = False
|
||||
self.sent_collect = False
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -124,7 +126,8 @@ def get_payload(ctx: GBContext):
|
||||
"items": [item.item for item in ctx.items_received],
|
||||
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
|
||||
if key[0] > current_time - 10},
|
||||
"deathlink": ctx.deathlink_pending
|
||||
"deathlink": ctx.deathlink_pending,
|
||||
"options": ((ctx.permissions['release'] in ('goal', 'enabled')) * 2) + (ctx.permissions['collect'] in ('goal', 'enabled'))
|
||||
}
|
||||
)
|
||||
ctx.deathlink_pending = False
|
||||
@@ -134,10 +137,13 @@ def get_payload(ctx: GBContext):
|
||||
async def parse_locations(data: List, ctx: GBContext):
|
||||
locations = []
|
||||
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
|
||||
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], "Rod": data[0x140 + 0x20 + 0x0E:]}
|
||||
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E],
|
||||
"Rod": data[0x140 + 0x20 + 0x0E:0x140 + 0x20 + 0x0E + 0x01]}
|
||||
|
||||
if len(flags['Rod']) > 1:
|
||||
return
|
||||
if len(data) > 0x140 + 0x20 + 0x0E + 0x01:
|
||||
flags["DexSanityFlag"] = data[0x140 + 0x20 + 0x0E + 0x01:]
|
||||
else:
|
||||
flags["DexSanityFlag"] = [0] * 19
|
||||
|
||||
for flag_type, loc_map in location_map.items():
|
||||
for flag, loc_id in loc_map.items():
|
||||
@@ -207,6 +213,16 @@ async def gb_sync_task(ctx: GBContext):
|
||||
async_start(parse_locations(data_decoded['locations'], ctx))
|
||||
if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags:
|
||||
await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!")
|
||||
if 'options' in data_decoded:
|
||||
msgs = []
|
||||
if data_decoded['options'] & 4 and not ctx.sent_release:
|
||||
ctx.sent_release = True
|
||||
msgs.append({"cmd": "Say", "text": "!release"})
|
||||
if data_decoded['options'] & 8 and not ctx.sent_collect:
|
||||
ctx.sent_collect = True
|
||||
msgs.append({"cmd": "Say", "text": "!collect"})
|
||||
if msgs:
|
||||
await ctx.send_msgs(msgs)
|
||||
if ctx.set_deathlink:
|
||||
await ctx.update_death_link(True)
|
||||
except asyncio.TimeoutError:
|
||||
|
||||
@@ -34,6 +34,11 @@ Currently, the following games are supported:
|
||||
* Overcooked! 2
|
||||
* Zillion
|
||||
* Lufia II Ancient Cave
|
||||
* Blasphemous
|
||||
* Wargroove
|
||||
* Stardew Valley
|
||||
* The Legend of Zelda
|
||||
* The Messenger
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
@@ -52,9 +52,9 @@ class StarcraftClientProcessor(ClientCommandProcessor):
|
||||
"""Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal"""
|
||||
options = difficulty.split()
|
||||
num_options = len(options)
|
||||
difficulty_choice = options[0].lower()
|
||||
|
||||
if num_options > 0:
|
||||
difficulty_choice = options[0].lower()
|
||||
if difficulty_choice == "casual":
|
||||
self.ctx.difficulty_override = 0
|
||||
elif difficulty_choice == "normal":
|
||||
@@ -71,7 +71,11 @@ class StarcraftClientProcessor(ClientCommandProcessor):
|
||||
return True
|
||||
|
||||
else:
|
||||
self.output("Difficulty needs to be specified in the command.")
|
||||
if self.ctx.difficulty == -1:
|
||||
self.output("Please connect to a seed before checking difficulty.")
|
||||
else:
|
||||
self.output("Current difficulty: " + ["Casual", "Normal", "Hard", "Brutal"][self.ctx.difficulty])
|
||||
self.output("To change the difficulty, add the name of the difficulty after the command.")
|
||||
return False
|
||||
|
||||
def _cmd_disable_mission_check(self) -> bool:
|
||||
|
||||
14
Utils.py
14
Utils.py
@@ -195,11 +195,11 @@ def get_public_ipv4() -> str:
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
ctx = get_cert_none_ssl_context()
|
||||
try:
|
||||
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx).read().decode("utf8").strip()
|
||||
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip()
|
||||
except Exception as e:
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
ip = urllib.request.urlopen("https://v4.ident.me", context=ctx).read().decode("utf8").strip()
|
||||
ip = urllib.request.urlopen("https://v4.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
|
||||
except Exception:
|
||||
logging.exception(e)
|
||||
pass # we could be offline, in a local game, so no point in erroring out
|
||||
@@ -213,7 +213,7 @@ def get_public_ipv6() -> str:
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
ctx = get_cert_none_ssl_context()
|
||||
try:
|
||||
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx).read().decode("utf8").strip()
|
||||
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
pass # we could be offline, in a local game, or ipv6 may not be available
|
||||
@@ -310,6 +310,14 @@ def get_default_options() -> OptionsType:
|
||||
"lufia2ac_options": {
|
||||
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
|
||||
},
|
||||
"tloz_options": {
|
||||
"rom_file": "Legend of Zelda, The (U) (PRG0) [!].nes",
|
||||
"rom_start": True,
|
||||
"display_msgs": True,
|
||||
},
|
||||
"wargroove_options": {
|
||||
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
||||
}
|
||||
}
|
||||
return options
|
||||
|
||||
|
||||
445
WargrooveClient.py
Normal file
445
WargrooveClient.py
Normal file
@@ -0,0 +1,445 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import random
|
||||
import shutil
|
||||
from typing import Tuple, List, Iterable, Dict
|
||||
|
||||
from worlds.wargroove import WargrooveWorld
|
||||
from worlds.wargroove.Items import item_table, faction_table, CommanderData, ItemData
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
import Utils
|
||||
import json
|
||||
import logging
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("WargrooveClient", exception_logger="Client")
|
||||
|
||||
from NetUtils import NetworkItem, ClientStatus
|
||||
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
|
||||
CommonContext, server_loop
|
||||
|
||||
wg_logger = logging.getLogger("WG")
|
||||
|
||||
|
||||
class WargrooveClientCommandProcessor(ClientCommandProcessor):
|
||||
def _cmd_resync(self):
|
||||
"""Manually trigger a resync."""
|
||||
self.output(f"Syncing items.")
|
||||
self.ctx.syncing = True
|
||||
|
||||
def _cmd_commander(self, *commander_name: Iterable[str]):
|
||||
"""Set the current commander to the given commander."""
|
||||
if commander_name:
|
||||
self.ctx.set_commander(' '.join(commander_name))
|
||||
else:
|
||||
if self.ctx.can_choose_commander:
|
||||
commanders = self.ctx.get_commanders()
|
||||
wg_logger.info('Unlocked commanders: ' +
|
||||
', '.join((commander.name for commander, unlocked in commanders if unlocked)))
|
||||
wg_logger.info('Locked commanders: ' +
|
||||
', '.join((commander.name for commander, unlocked in commanders if not unlocked)))
|
||||
else:
|
||||
wg_logger.error('Cannot set commanders in this game mode.')
|
||||
|
||||
|
||||
class WargrooveContext(CommonContext):
|
||||
command_processor: int = WargrooveClientCommandProcessor
|
||||
game = "Wargroove"
|
||||
items_handling = 0b111 # full remote
|
||||
current_commander: CommanderData = faction_table["Starter"][0]
|
||||
can_choose_commander: bool = False
|
||||
commander_defense_boost_multiplier: int = 0
|
||||
income_boost_multiplier: int = 0
|
||||
starting_groove_multiplier: float
|
||||
faction_item_ids = {
|
||||
'Starter': 0,
|
||||
'Cherrystone': 52025,
|
||||
'Felheim': 52026,
|
||||
'Floran': 52027,
|
||||
'Heavensong': 52028,
|
||||
'Requiem': 52029,
|
||||
'Outlaw': 52030
|
||||
}
|
||||
buff_item_ids = {
|
||||
'Income Boost': 52023,
|
||||
'Commander Defense Boost': 52024,
|
||||
}
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super(WargrooveContext, self).__init__(server_address, password)
|
||||
self.send_index: int = 0
|
||||
self.syncing = False
|
||||
self.awaiting_bridge = False
|
||||
# self.game_communication_path: files go in this path to pass data between us and the actual game
|
||||
if "appdata" in os.environ:
|
||||
options = Utils.get_options()
|
||||
root_directory = os.path.join(options["wargroove_options"]["root_directory"])
|
||||
data_directory = os.path.join("lib", "worlds", "wargroove", "data")
|
||||
dev_data_directory = os.path.join("worlds", "wargroove", "data")
|
||||
appdata_wargroove = os.path.expandvars(os.path.join("%APPDATA%", "Chucklefish", "Wargroove"))
|
||||
if not os.path.isfile(os.path.join(root_directory, "win64_bin", "wargroove64.exe")):
|
||||
print_error_and_close("WargrooveClient couldn't find wargroove64.exe. "
|
||||
"Unable to infer required game_communication_path")
|
||||
self.game_communication_path = os.path.join(root_directory, "AP")
|
||||
if not os.path.exists(self.game_communication_path):
|
||||
os.makedirs(self.game_communication_path)
|
||||
self.remove_communication_files()
|
||||
atexit.register(self.remove_communication_files)
|
||||
if not os.path.isdir(appdata_wargroove):
|
||||
print_error_and_close("WargrooveClient couldn't find Wargoove in appdata!"
|
||||
"Boot Wargroove and then close it to attempt to fix this error")
|
||||
if not os.path.isdir(data_directory):
|
||||
data_directory = dev_data_directory
|
||||
if not os.path.isdir(data_directory):
|
||||
print_error_and_close("WargrooveClient couldn't find Wargoove mod and save files in install!")
|
||||
shutil.copytree(data_directory, appdata_wargroove, dirs_exist_ok=True)
|
||||
else:
|
||||
print_error_and_close("WargrooveClient couldn't detect system type. "
|
||||
"Unable to infer required game_communication_path")
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(WargrooveContext, self).server_auth(password_requested)
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
|
||||
async def connection_closed(self):
|
||||
await super(WargrooveContext, self).connection_closed()
|
||||
self.remove_communication_files()
|
||||
|
||||
@property
|
||||
def endpoints(self):
|
||||
if self.server:
|
||||
return [self.server]
|
||||
else:
|
||||
return []
|
||||
|
||||
async def shutdown(self):
|
||||
await super(WargrooveContext, self).shutdown()
|
||||
self.remove_communication_files()
|
||||
|
||||
def remove_communication_files(self):
|
||||
for root, dirs, files in os.walk(self.game_communication_path):
|
||||
for file in files:
|
||||
os.remove(root + "/" + file)
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd in {"Connected"}:
|
||||
filename = f"AP_settings.json"
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
slot_data = args["slot_data"]
|
||||
json.dump(args["slot_data"], f)
|
||||
self.can_choose_commander = slot_data["can_choose_commander"]
|
||||
print('can choose commander:', self.can_choose_commander)
|
||||
self.starting_groove_multiplier = slot_data["starting_groove_multiplier"]
|
||||
self.income_boost_multiplier = slot_data["income_boost"]
|
||||
self.commander_defense_boost_multiplier = slot_data["commander_defense_boost"]
|
||||
f.close()
|
||||
for ss in self.checked_locations:
|
||||
filename = f"send{ss}"
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
f.close()
|
||||
self.update_commander_data()
|
||||
self.ui.update_tracker()
|
||||
|
||||
random.seed(self.seed_name + str(self.slot))
|
||||
# Our indexes start at 1 and we have 24 levels
|
||||
for i in range(1, 25):
|
||||
filename = f"seed{i}"
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
f.write(str(random.randint(0, 4294967295)))
|
||||
f.close()
|
||||
|
||||
if cmd in {"RoomInfo"}:
|
||||
self.seed_name = args["seed_name"]
|
||||
|
||||
if cmd in {"ReceivedItems"}:
|
||||
received_ids = [item.item for item in self.items_received]
|
||||
for network_item in self.items_received:
|
||||
filename = f"AP_{str(network_item.item)}.item"
|
||||
path = os.path.join(self.game_communication_path, filename)
|
||||
|
||||
# Newly-obtained items
|
||||
if not os.path.isfile(path):
|
||||
open(path, 'w').close()
|
||||
# Announcing commander unlocks
|
||||
item_name = self.item_names[network_item.item]
|
||||
if item_name in faction_table.keys():
|
||||
for commander in faction_table[item_name]:
|
||||
logger.info(f"{commander.name} has been unlocked!")
|
||||
|
||||
with open(path, 'w') as f:
|
||||
item_count = received_ids.count(network_item.item)
|
||||
if self.buff_item_ids["Income Boost"] == network_item.item:
|
||||
f.write(f"{item_count * self.income_boost_multiplier}")
|
||||
elif self.buff_item_ids["Commander Defense Boost"] == network_item.item:
|
||||
f.write(f"{item_count * self.commander_defense_boost_multiplier}")
|
||||
else:
|
||||
f.write(f"{item_count}")
|
||||
f.close()
|
||||
|
||||
print_filename = f"AP_{str(network_item.item)}.item.print"
|
||||
print_path = os.path.join(self.game_communication_path, print_filename)
|
||||
if not os.path.isfile(print_path):
|
||||
open(print_path, 'w').close()
|
||||
with open(print_path, 'w') as f:
|
||||
f.write("Received " +
|
||||
self.item_names[network_item.item] +
|
||||
" from " +
|
||||
self.player_names[network_item.player])
|
||||
f.close()
|
||||
self.update_commander_data()
|
||||
self.ui.update_tracker()
|
||||
|
||||
if cmd in {"RoomUpdate"}:
|
||||
if "checked_locations" in args:
|
||||
for ss in self.checked_locations:
|
||||
filename = f"send{ss}"
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
f.close()
|
||||
|
||||
def run_gui(self):
|
||||
"""Import kivy UI system and start running it as self.ui_task."""
|
||||
from kvui import GameManager, HoverBehavior, ServerToolTip
|
||||
from kivy.uix.tabbedpanel import TabbedPanelItem
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.togglebutton import ToggleButton
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.image import AsyncImage, Image
|
||||
from kivy.uix.stacklayout import StackLayout
|
||||
from kivy.uix.label import Label
|
||||
from kivy.properties import ColorProperty
|
||||
from kivy.uix.image import Image
|
||||
import pkgutil
|
||||
|
||||
class TrackerLayout(BoxLayout):
|
||||
pass
|
||||
|
||||
class CommanderSelect(BoxLayout):
|
||||
pass
|
||||
|
||||
class CommanderButton(ToggleButton):
|
||||
pass
|
||||
|
||||
class FactionBox(BoxLayout):
|
||||
pass
|
||||
|
||||
class CommanderGroup(BoxLayout):
|
||||
pass
|
||||
|
||||
class ItemTracker(BoxLayout):
|
||||
pass
|
||||
|
||||
class ItemLabel(Label):
|
||||
pass
|
||||
|
||||
class WargrooveManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
("WG", "WG Console"),
|
||||
]
|
||||
base_title = "Archipelago Wargroove Client"
|
||||
ctx: WargrooveContext
|
||||
unit_tracker: ItemTracker
|
||||
trigger_tracker: BoxLayout
|
||||
boost_tracker: BoxLayout
|
||||
commander_buttons: Dict[int, List[CommanderButton]]
|
||||
tracker_items = {
|
||||
"Swordsman": ItemData(None, "Unit", False),
|
||||
"Dog": ItemData(None, "Unit", False),
|
||||
**item_table
|
||||
}
|
||||
|
||||
def build(self):
|
||||
container = super().build()
|
||||
panel = TabbedPanelItem(text="Wargroove")
|
||||
panel.content = self.build_tracker()
|
||||
self.tabs.add_widget(panel)
|
||||
return container
|
||||
|
||||
def build_tracker(self) -> TrackerLayout:
|
||||
try:
|
||||
tracker = TrackerLayout(orientation="horizontal")
|
||||
commander_select = CommanderSelect(orientation="vertical")
|
||||
self.commander_buttons = {}
|
||||
|
||||
for faction, commanders in faction_table.items():
|
||||
faction_box = FactionBox(size_hint=(None, None), width=100 * len(commanders), height=70)
|
||||
commander_group = CommanderGroup()
|
||||
commander_buttons = []
|
||||
for commander in commanders:
|
||||
commander_button = CommanderButton(text=commander.name, group="commanders")
|
||||
if faction == "Starter":
|
||||
commander_button.disabled = False
|
||||
commander_button.bind(on_press=lambda instance: self.ctx.set_commander(instance.text))
|
||||
commander_buttons.append(commander_button)
|
||||
commander_group.add_widget(commander_button)
|
||||
self.commander_buttons[faction] = commander_buttons
|
||||
faction_box.add_widget(Label(text=faction, size_hint_x=None, pos_hint={'left': 1}, size_hint_y=None, height=10))
|
||||
faction_box.add_widget(commander_group)
|
||||
commander_select.add_widget(faction_box)
|
||||
item_tracker = ItemTracker(padding=[0,20])
|
||||
self.unit_tracker = BoxLayout(orientation="vertical")
|
||||
other_tracker = BoxLayout(orientation="vertical")
|
||||
self.trigger_tracker = BoxLayout(orientation="vertical")
|
||||
self.boost_tracker = BoxLayout(orientation="vertical")
|
||||
other_tracker.add_widget(self.trigger_tracker)
|
||||
other_tracker.add_widget(self.boost_tracker)
|
||||
item_tracker.add_widget(self.unit_tracker)
|
||||
item_tracker.add_widget(other_tracker)
|
||||
tracker.add_widget(commander_select)
|
||||
tracker.add_widget(item_tracker)
|
||||
self.update_tracker()
|
||||
return tracker
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def update_tracker(self):
|
||||
received_ids = [item.item for item in self.ctx.items_received]
|
||||
for faction, item_id in self.ctx.faction_item_ids.items():
|
||||
for commander_button in self.commander_buttons[faction]:
|
||||
commander_button.disabled = not (faction == "Starter" or item_id in received_ids)
|
||||
self.unit_tracker.clear_widgets()
|
||||
self.trigger_tracker.clear_widgets()
|
||||
for name, item in self.tracker_items.items():
|
||||
if item.type in ("Unit", "Trigger"):
|
||||
status_color = (1, 1, 1, 1) if item.code is None or item.code in received_ids else (0.6, 0.2, 0.2, 1)
|
||||
label = ItemLabel(text=name, color=status_color)
|
||||
if item.type == "Unit":
|
||||
self.unit_tracker.add_widget(label)
|
||||
else:
|
||||
self.trigger_tracker.add_widget(label)
|
||||
self.boost_tracker.clear_widgets()
|
||||
extra_income = received_ids.count(52023) * self.ctx.income_boost_multiplier
|
||||
extra_defense = received_ids.count(52024) * self.ctx.commander_defense_boost_multiplier
|
||||
income_boost = ItemLabel(text="Extra Income: " + str(extra_income))
|
||||
defense_boost = ItemLabel(text="Comm Defense: " + str(100 + extra_defense))
|
||||
self.boost_tracker.add_widget(income_boost)
|
||||
self.boost_tracker.add_widget(defense_boost)
|
||||
|
||||
self.ui = WargrooveManager(self)
|
||||
data = pkgutil.get_data(WargrooveWorld.__module__, "Wargroove.kv").decode()
|
||||
Builder.load_string(data)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
def update_commander_data(self):
|
||||
if self.can_choose_commander:
|
||||
faction_items = 0
|
||||
faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()]
|
||||
for network_item in self.items_received:
|
||||
if self.item_names[network_item.item] in faction_item_names:
|
||||
faction_items += 1
|
||||
starting_groove = (faction_items - 1) * self.starting_groove_multiplier
|
||||
# Must be an integer larger than 0
|
||||
starting_groove = int(max(starting_groove, 0))
|
||||
data = {
|
||||
"commander": self.current_commander.internal_name,
|
||||
"starting_groove": starting_groove
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"commander": "seed",
|
||||
"starting_groove": 0
|
||||
}
|
||||
filename = 'commander.json'
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
json.dump(data, f)
|
||||
if self.ui:
|
||||
self.ui.update_tracker()
|
||||
|
||||
def set_commander(self, commander_name: str) -> bool:
|
||||
"""Sets the current commander to the given one, if possible"""
|
||||
if not self.can_choose_commander:
|
||||
wg_logger.error("Cannot set commanders in this game mode.")
|
||||
return
|
||||
match_name = commander_name.lower()
|
||||
for commander, unlocked in self.get_commanders():
|
||||
if commander.name.lower() == match_name or commander.alt_name and commander.alt_name.lower() == match_name:
|
||||
if unlocked:
|
||||
self.current_commander = commander
|
||||
self.syncing = True
|
||||
wg_logger.info(f"Commander set to {commander.name}.")
|
||||
self.update_commander_data()
|
||||
return True
|
||||
else:
|
||||
wg_logger.error(f"Commander {commander.name} has not been unlocked.")
|
||||
return False
|
||||
else:
|
||||
wg_logger.error(f"{commander_name} is not a recognized Wargroove commander.")
|
||||
|
||||
def get_commanders(self) -> List[Tuple[CommanderData, bool]]:
|
||||
"""Gets a list of commanders with their unlocked status"""
|
||||
commanders = []
|
||||
received_ids = [item.item for item in self.items_received]
|
||||
for faction in faction_table.keys():
|
||||
unlocked = faction == 'Starter' or self.faction_item_ids[faction] in received_ids
|
||||
commanders += [(commander, unlocked) for commander in faction_table[faction]]
|
||||
return commanders
|
||||
|
||||
|
||||
async def game_watcher(ctx: WargrooveContext):
|
||||
from worlds.wargroove.Locations import location_table
|
||||
while not ctx.exit_event.is_set():
|
||||
if ctx.syncing == True:
|
||||
sync_msg = [{'cmd': 'Sync'}]
|
||||
if ctx.locations_checked:
|
||||
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
||||
await ctx.send_msgs(sync_msg)
|
||||
ctx.syncing = False
|
||||
sending = []
|
||||
victory = False
|
||||
for root, dirs, files in os.walk(ctx.game_communication_path):
|
||||
for file in files:
|
||||
if file.find("send") > -1:
|
||||
st = file.split("send", -1)[1]
|
||||
sending = sending+[(int(st))]
|
||||
if file.find("victory") > -1:
|
||||
victory = True
|
||||
ctx.locations_checked = sending
|
||||
message = [{"cmd": 'LocationChecks', "locations": sending}]
|
||||
await ctx.send_msgs(message)
|
||||
if not ctx.finished_game and victory:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
def print_error_and_close(msg):
|
||||
logger.error("Error: " + msg)
|
||||
Utils.messagebox("Error", msg, error=True)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
async def main(args):
|
||||
ctx = WargrooveContext(args.connect, args.password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
progression_watcher = asyncio.create_task(
|
||||
game_watcher(ctx), name="WargrooveProgressionWatcher")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
|
||||
await progression_watcher
|
||||
|
||||
await ctx.shutdown()
|
||||
|
||||
import colorama
|
||||
|
||||
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
|
||||
|
||||
args, rest = parser.parse_known_args()
|
||||
colorama.init()
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
@@ -33,6 +33,11 @@ def get_app():
|
||||
import yaml
|
||||
app.config.from_file(configpath, yaml.safe_load)
|
||||
logging.info(f"Updated config from {configpath}")
|
||||
if not app.config["HOST_ADDRESS"]:
|
||||
logging.info("Getting public IP, as HOST_ADDRESS is empty.")
|
||||
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
|
||||
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
|
||||
|
||||
db.bind(**app.config["PONY"])
|
||||
db.generate_mapping(create_tables=True)
|
||||
return app
|
||||
|
||||
@@ -51,7 +51,7 @@ app.config["PONY"] = {
|
||||
app.config["MAX_ROLL"] = 20
|
||||
app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache"
|
||||
app.config["JSON_AS_ASCII"] = False
|
||||
app.config["PATCH_TARGET"] = "archipelago.gg"
|
||||
app.config["HOST_ADDRESS"] = ""
|
||||
|
||||
cache = Cache(app)
|
||||
Compress(app)
|
||||
|
||||
@@ -179,6 +179,7 @@ class MultiworldInstance():
|
||||
self.ponyconfig = config["PONY"]
|
||||
self.cert = config["SELFLAUNCHCERT"]
|
||||
self.key = config["SELFLAUNCHKEY"]
|
||||
self.host = config["HOST_ADDRESS"]
|
||||
|
||||
def start(self):
|
||||
if self.process and self.process.is_alive():
|
||||
@@ -187,7 +188,7 @@ class MultiworldInstance():
|
||||
logging.info(f"Spinning up {self.room_id}")
|
||||
process = multiprocessing.Process(group=None, target=run_server_process,
|
||||
args=(self.room_id, self.ponyconfig, get_static_server_data(),
|
||||
self.cert, self.key),
|
||||
self.cert, self.key, self.host),
|
||||
name="MultiHost")
|
||||
process.start()
|
||||
# bind after start to prevent thread sync issues with guardian.
|
||||
|
||||
@@ -131,6 +131,8 @@ def get_static_server_data() -> dict:
|
||||
"gamespackage": worlds.network_data_package["games"],
|
||||
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
|
||||
worlds.AutoWorldRegister.world_types.items()},
|
||||
"location_name_groups": {world_name: world.location_name_groups for world_name, world in
|
||||
worlds.AutoWorldRegister.world_types.items()},
|
||||
}
|
||||
|
||||
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
||||
@@ -140,7 +142,8 @@ def get_static_server_data() -> dict:
|
||||
|
||||
|
||||
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str]):
|
||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||
host: str):
|
||||
# establish DB connection for multidata and multisave
|
||||
db.bind(**ponyconfig)
|
||||
db.generate_mapping(check_tables=False)
|
||||
@@ -165,17 +168,18 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||
for wssocket in ctx.server.ws_server.sockets:
|
||||
socketname = wssocket.getsockname()
|
||||
if wssocket.family == socket.AF_INET6:
|
||||
logging.info(f'Hosting game at [{get_public_ipv6()}]:{socketname[1]}')
|
||||
# Prefer IPv4, as most users seem to not have working ipv6 support
|
||||
if not port:
|
||||
port = socketname[1]
|
||||
elif wssocket.family == socket.AF_INET:
|
||||
logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}')
|
||||
port = socketname[1]
|
||||
if port:
|
||||
logging.info(f'Hosting game at {host}:{port}')
|
||||
with db_session:
|
||||
room = Room.get(id=ctx.room_id)
|
||||
room.last_port = port
|
||||
else:
|
||||
logging.exception("Could not determine port. Likely hosting failure.")
|
||||
with db_session:
|
||||
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
||||
|
||||
@@ -26,7 +26,7 @@ def download_patch(room_id, patch_id):
|
||||
with zipfile.ZipFile(filelike, "a") as zf:
|
||||
with zf.open("archipelago.json", "r") as f:
|
||||
manifest = json.load(f)
|
||||
manifest["server"] = f"{app.config['PATCH_TARGET']}:{last_port}" if last_port else None
|
||||
manifest["server"] = f"{app.config['HOST_ADDRESS']}:{last_port}" if last_port else None
|
||||
with zipfile.ZipFile(new_file, "w") as new_zip:
|
||||
for file in zf.infolist():
|
||||
if file.filename == "archipelago.json":
|
||||
@@ -64,7 +64,7 @@ def download_slot_file(room_id, player_id: int):
|
||||
if slot_data.game == "Minecraft":
|
||||
from worlds.minecraft import mc_update_output
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
|
||||
data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port)
|
||||
data = mc_update_output(slot_data.data, server=app.config['HOST_ADDRESS'], port=room.last_port)
|
||||
return send_file(io.BytesIO(data), as_attachment=True, download_name=fname)
|
||||
elif slot_data.game == "Factorio":
|
||||
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
flask>=2.2.2
|
||||
flask>=2.2.3
|
||||
pony>=0.7.16
|
||||
waitress>=2.1.2
|
||||
Flask-Caching>=2.0.1
|
||||
Flask-Caching>=2.0.2
|
||||
Flask-Compress>=1.13
|
||||
Flask-Limiter>=2.8.1
|
||||
bokeh>=3.0.2
|
||||
Flask-Limiter>=3.3.0
|
||||
bokeh>=3.1.0
|
||||
|
||||
18
WebHostLib/static/assets/baseHeader.js
Normal file
18
WebHostLib/static/assets/baseHeader.js
Normal file
@@ -0,0 +1,18 @@
|
||||
window.addEventListener('load', () => {
|
||||
const menuButton = document.getElementById('base-header-mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('base-header-mobile-menu');
|
||||
|
||||
menuButton.addEventListener('click', (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
if (!mobileMenu.style.display || mobileMenu.style.display === 'none') {
|
||||
return mobileMenu.style.display = 'flex';
|
||||
}
|
||||
|
||||
mobileMenu.style.display = 'none';
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
mobileMenu.style.display = 'none';
|
||||
});
|
||||
});
|
||||
49
WebHostLib/static/assets/checksfinderTracker.js
Normal file
49
WebHostLib/static/assets/checksfinderTracker.js
Normal file
@@ -0,0 +1,49 @@
|
||||
window.addEventListener('load', () => {
|
||||
// Reload tracker every 60 seconds
|
||||
const url = window.location;
|
||||
setInterval(() => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
|
||||
// Create a fake DOM using the returned HTML
|
||||
const domParser = new DOMParser();
|
||||
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
|
||||
|
||||
// Update item tracker
|
||||
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
|
||||
// Update only counters in the location-table
|
||||
let counters = document.getElementsByClassName('counter');
|
||||
const fakeCounters = fakeDOM.getElementsByClassName('counter');
|
||||
for (let i = 0; i < counters.length; i++) {
|
||||
counters[i].innerHTML = fakeCounters[i].innerHTML;
|
||||
}
|
||||
};
|
||||
ajax.open('GET', url);
|
||||
ajax.send();
|
||||
}, 60000)
|
||||
|
||||
// Collapsible advancement sections
|
||||
const categories = document.getElementsByClassName("location-category");
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
let hide_id = categories[i].id.split('-')[0];
|
||||
if (hide_id == 'Total') {
|
||||
continue;
|
||||
}
|
||||
categories[i].addEventListener('click', function() {
|
||||
// Toggle the advancement list
|
||||
document.getElementById(hide_id).classList.toggle("hide");
|
||||
// Change text of the header
|
||||
const tab_header = document.getElementById(hide_id+'-header').children[0];
|
||||
const orig_text = tab_header.innerHTML;
|
||||
let new_text;
|
||||
if (orig_text.includes("▼")) {
|
||||
new_text = orig_text.replace("▼", "▲");
|
||||
}
|
||||
else {
|
||||
new_text = orig_text.replace("▲", "▼");
|
||||
}
|
||||
tab_header.innerHTML = new_text;
|
||||
});
|
||||
}
|
||||
});
|
||||
6
WebHostLib/static/assets/lttpMultiTracker.js
Normal file
6
WebHostLib/static/assets/lttpMultiTracker.js
Normal file
@@ -0,0 +1,6 @@
|
||||
window.addEventListener('load', () => {
|
||||
$(".table-wrapper").scrollsync({
|
||||
y_sync: true,
|
||||
x_sync: true
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
const adjustTableHeight = () => {
|
||||
const tablesContainer = document.getElementById('tables-container');
|
||||
if (!tablesContainer)
|
||||
return;
|
||||
const upperDistance = tablesContainer.getBoundingClientRect().top;
|
||||
|
||||
const containerHeight = window.innerHeight - upperDistance;
|
||||
@@ -108,7 +110,7 @@ window.addEventListener('load', () => {
|
||||
const update = () => {
|
||||
const target = $("<div></div>");
|
||||
console.log("Updating Tracker...");
|
||||
target.load("/tracker/" + tracker, function (response, status) {
|
||||
target.load(location.href, function (response, status) {
|
||||
if (status === "success") {
|
||||
target.find(".table").each(function (i, new_table) {
|
||||
const new_trs = $(new_table).find("tbody>tr");
|
||||
@@ -135,10 +137,5 @@ window.addEventListener('load', () => {
|
||||
tables.draw();
|
||||
});
|
||||
|
||||
$(".table-wrapper").scrollsync({
|
||||
y_sync: true,
|
||||
x_sync: true
|
||||
});
|
||||
|
||||
adjustTableHeight();
|
||||
});
|
||||
BIN
WebHostLib/static/static/button-images/hamburger-menu-icon.png
Normal file
BIN
WebHostLib/static/static/button-images/hamburger-menu-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
30
WebHostLib/static/styles/checksfinderTracker.css
Normal file
30
WebHostLib/static/styles/checksfinderTracker.css
Normal file
@@ -0,0 +1,30 @@
|
||||
#player-tracker-wrapper{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#inventory-table{
|
||||
padding: 8px 10px 2px 6px;
|
||||
background-color: #42b149;
|
||||
border-radius: 4px;
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
#inventory-table tr.column-headers td {
|
||||
font-size: 1rem;
|
||||
padding: 0 5rem 0 0;
|
||||
}
|
||||
|
||||
#inventory-table td{
|
||||
padding: 0 0.5rem 0.5rem;
|
||||
font-family: LexendDeca-Light, monospace;
|
||||
font-size: 2.5rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#inventory-table td img{
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
@@ -42,7 +42,7 @@ html{
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#base-header a{
|
||||
#base-header a, #base-header-mobile-menu a{
|
||||
color: #2f6b83;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
@@ -51,3 +51,64 @@ html{
|
||||
font-family: LondrinaSolid-Light, sans-serif;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#base-header-right-mobile{
|
||||
display: none;
|
||||
margin-top: 2rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
#base-header-mobile-menu{
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
background-color: #ffffff;
|
||||
text-align: center;
|
||||
overflow-y: auto;
|
||||
z-index: 10000;
|
||||
width: 100vw;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
|
||||
position: absolute;
|
||||
top: 7rem;
|
||||
right: 0;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
#base-header-mobile-menu a{
|
||||
padding: 4rem 2rem;
|
||||
font-size: 5rem;
|
||||
line-height: 5rem;
|
||||
color: #699ca8;
|
||||
border-top: 1px solid #d3d3d3;
|
||||
}
|
||||
|
||||
#base-header-right-mobile img{
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1580px){
|
||||
html{
|
||||
padding-top: 260px;
|
||||
scroll-padding-top: 230px;
|
||||
}
|
||||
|
||||
#base-header{
|
||||
height: 200px;
|
||||
background-size: auto 200px;
|
||||
}
|
||||
|
||||
#base-header #site-title img{
|
||||
height: calc(38px * 2);
|
||||
margin-top: 30px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
#base-header-right{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#base-header-right-mobile{
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,33 @@ img.alttp-sprite {
|
||||
background-color: #d3c97d;
|
||||
}
|
||||
|
||||
#tracker-navigation {
|
||||
display: inline-flex;
|
||||
background-color: #b0a77d;
|
||||
margin: 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tracker-navigation-button {
|
||||
display: block;
|
||||
margin: 4px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
.tracker-navigation-button:hover {
|
||||
background-color: #e2eabb !important;
|
||||
}
|
||||
|
||||
.tracker-navigation-button.selected {
|
||||
background-color: rgb(220, 226, 189);
|
||||
}
|
||||
|
||||
@media all and (max-width: 1700px) {
|
||||
table.dataTable thead th.upper-row{
|
||||
position: -webkit-sticky;
|
||||
|
||||
35
WebHostLib/templates/checksfinderTracker.html
Normal file
35
WebHostLib/templates/checksfinderTracker.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/checksfinderTracker.css') }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/checksfinderTracker.js') }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr class="column-headers">
|
||||
<td colspan="2">Checks Available:</td>
|
||||
<td colspan="2">Map Bombs:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img alt="Checks Available" src="{{ icons['Checks Available'] }}" /></td>
|
||||
<td>{{ checks_available }}</td>
|
||||
<td><img alt="Bombs Remaining" src="{{ icons['Map Bombs'] }}" /></td>
|
||||
<td>{{ bombs_display }}/20</td>
|
||||
</tr>
|
||||
<tr class="column-headers">
|
||||
<td colspan="2">Map Width:</td>
|
||||
<td colspan="2">Map Height:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img alt="Map Width" src="{{ icons['Map Width'] }}" /></td>
|
||||
<td>{{ width_display }}/10</td>
|
||||
<td><img alt="Map Height" src="{{ icons['Map Height'] }}" /></td>
|
||||
<td>{{ height_display }}/10</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,7 +4,7 @@
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% block head %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/themes/base.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/baseHeader.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
@@ -16,5 +17,17 @@
|
||||
<a href="/faq/en">f.a.q.</a>
|
||||
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
||||
</div>
|
||||
<div id="base-header-right-mobile">
|
||||
<a id="base-header-mobile-menu-button" href="#">
|
||||
<img src="/static/static/button-images/hamburger-menu-icon.png" alt="Menu" />
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<div id="base-header-mobile-menu">
|
||||
<a href="/games">supported games</a>
|
||||
<a href="/tutorial">setup guides</a>
|
||||
<a href="/start-playing">start playing</a>
|
||||
<a href="/faq/en">f.a.q.</a>
|
||||
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if room.tracker %}
|
||||
This room has a <a href="{{ url_for("getTracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.
|
||||
This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.
|
||||
<br />
|
||||
{% endif %}
|
||||
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
|
||||
@@ -25,8 +25,8 @@
|
||||
The most likely failure reason is that the multiworld is too old to be loaded now.
|
||||
{% elif room.last_port %}
|
||||
You can connect to this room by using <span class="interactive"
|
||||
data-tooltip="This means address/ip is {{ config['PATCH_TARGET'] }} and port is {{ room.last_port }}.">
|
||||
'/connect {{ config['PATCH_TARGET'] }}:{{ room.last_port }}'
|
||||
data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}.">
|
||||
'/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}'
|
||||
</span>
|
||||
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
{% extends 'tablepage.html' %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Multiworld Tracker</title>
|
||||
<title>ALttP Multiworld Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/lttpMultiTracker.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/dirtHeader.html' %}
|
||||
{% include 'multiTrackerNavigation.html' %}
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search"/>
|
||||
@@ -22,7 +22,7 @@
|
||||
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
||||
<tr>
|
||||
<td>{{ patch.player_id }}</td>
|
||||
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:@{{ config['PATCH_TARGET'] }}:{{ room.last_port }}">{{ patch.player_name }}<a/></td>
|
||||
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
|
||||
<td>{{ patch.game }}</td>
|
||||
<td>
|
||||
{% if patch.game == "Minecraft" %}
|
||||
|
||||
44
WebHostLib/templates/multiFactorioTracker.html
Normal file
44
WebHostLib/templates/multiFactorioTracker.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends "multiTracker.html" %}
|
||||
{% block custom_table_headers %}
|
||||
<th class="center-column">
|
||||
<img src="https://wiki.factorio.com/images/thumb/Logistic_science_pack.png/32px-Logistic_science_pack.png"
|
||||
alt="Logistic Science Pack">
|
||||
</th>
|
||||
<th class="center-column">
|
||||
<img src="https://wiki.factorio.com/images/thumb/Military_science_pack.png/32px-Military_science_pack.png"
|
||||
alt="Military Science Pack">
|
||||
</th>
|
||||
<th class="center-column">
|
||||
<img src="https://wiki.factorio.com/images/thumb/Chemical_science_pack.png/32px-Chemical_science_pack.png"
|
||||
alt="Chemical Science Pack">
|
||||
</th>
|
||||
<th class="center-column">
|
||||
<img src="https://wiki.factorio.com/images/thumb/Production_science_pack.png/32px-Production_science_pack.png"
|
||||
alt="Production Science Pack">
|
||||
</th>
|
||||
<th class="center-column">
|
||||
<img src="https://wiki.factorio.com/images/thumb/Utility_science_pack.png/32px-Utility_science_pack.png"
|
||||
alt="Utility Science Pack">
|
||||
</th>
|
||||
<th class="center-column">
|
||||
<img src="https://wiki.factorio.com/images/thumb/Space_science_pack.png/32px-Space_science_pack.png"
|
||||
alt="Space Science Pack">
|
||||
</th>
|
||||
{% endblock %}
|
||||
{% block custom_table_row scoped %}
|
||||
{% if games[player] == "Factorio" %}
|
||||
<td class="center-column">{% if inventory[team][player][131161] or inventory[team][player][131281] %}✔{% endif %}</td>
|
||||
<td class="center-column">{% if inventory[team][player][131172] or inventory[team][player][131281] > 1%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if inventory[team][player][131195] or inventory[team][player][131281] > 2%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if inventory[team][player][131240] or inventory[team][player][131281] > 3%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if inventory[team][player][131240] or inventory[team][player][131281] > 4%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if inventory[team][player][131220] or inventory[team][player][131281] > 5%}✔{% endif %}</td>
|
||||
{% else %}
|
||||
<td class="center-column">❌</td>
|
||||
<td class="center-column">❌</td>
|
||||
<td class="center-column">❌</td>
|
||||
<td class="center-column">❌</td>
|
||||
<td class="center-column">❌</td>
|
||||
<td class="center-column">❌</td>
|
||||
{% endif %}
|
||||
{% endblock%}
|
||||
95
WebHostLib/templates/multiTracker.html
Normal file
95
WebHostLib/templates/multiTracker.html
Normal file
@@ -0,0 +1,95 @@
|
||||
{% extends 'tablepage.html' %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Multiworld Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/dirtHeader.html' %}
|
||||
{% include 'multiTrackerNavigation.html' %}
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search"/>
|
||||
<span{% if not video %} hidden{% endif %} id="multi-stream-link">
|
||||
<a target="_blank" href="https://multistream.me/
|
||||
{%- for platform, link in video.values()|unique(False, 1)-%}
|
||||
{%- if platform == "Twitch" -%}t{%- else -%}yt{%- endif -%}:{{- link -}}/
|
||||
{%- endfor -%}">
|
||||
Multistream
|
||||
</a>
|
||||
</span>
|
||||
<span class="info">Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically.</span>
|
||||
</div>
|
||||
<div id="tables-container">
|
||||
{% for team, players in checks_done.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="checks-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Game</th>
|
||||
{% block custom_table_headers %}
|
||||
{# implement this block in game-specific multi trackers #}
|
||||
{% endblock %}
|
||||
<th class="center-column">Checks</th>
|
||||
<th class="center-column">%</th>
|
||||
<th class="center-column hours">Last<br>Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for player, checks in players.items() -%}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||
<td>{{ player_names[(team, loop.index)]|e }}</td>
|
||||
<td>{{ games[player] }}</td>
|
||||
{% block custom_table_row scoped %}
|
||||
{# implement this block in game-specific multi trackers #}
|
||||
{% endblock %}
|
||||
<td class="center-column">{{ checks["Total"] }}/{{ checks_in_area[player]["Total"] }}</td>
|
||||
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
|
||||
{%- if activity_timers[(team, player)] -%}
|
||||
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">None</td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for team, hints in hints.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="hints-table" class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Finder</th>
|
||||
<th>Receiver</th>
|
||||
<th>Item</th>
|
||||
<th>Location</th>
|
||||
<th>Entrance</th>
|
||||
<th>Found</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for hint in hints -%}
|
||||
<tr>
|
||||
<td>{{ long_player_names[team, hint.finding_player] }}</td>
|
||||
<td>{{ long_player_names[team, hint.receiving_player] }}</td>
|
||||
<td>{{ hint.item|item_name }}</td>
|
||||
<td>{{ hint.location|location_name }}</td>
|
||||
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
|
||||
<td>{% if hint.found %}✔{% endif %}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
9
WebHostLib/templates/multiTrackerNavigation.html
Normal file
9
WebHostLib/templates/multiTrackerNavigation.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{%- if enabled_multiworld_trackers|length > 1 -%}
|
||||
<div id="tracker-navigation">
|
||||
{% for enabled_tracker in enabled_multiworld_trackers %}
|
||||
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %}
|
||||
<a class="tracker-navigation-button{%- if enabled_tracker.current -%} selected{% endif %}"
|
||||
href="{{ tracker_url }}">{{ enabled_tracker.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
@@ -5,6 +5,7 @@ from typing import Counter, Optional, Dict, Any, Tuple
|
||||
from uuid import UUID
|
||||
|
||||
from flask import render_template
|
||||
from jinja2 import pass_context, runtime
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
from MultiServer import Context, get_saving_second
|
||||
@@ -83,9 +84,6 @@ def get_alttp_id(item_name):
|
||||
return Items.item_table[item_name][2]
|
||||
|
||||
|
||||
app.jinja_env.filters["location_name"] = lambda location: lookup_any_location_id_to_name.get(location, location)
|
||||
app.jinja_env.filters['item_name'] = lambda id: lookup_any_item_id_to_name.get(id, id)
|
||||
|
||||
links = {"Bow": "Progressive Bow",
|
||||
"Silver Arrows": "Progressive Bow",
|
||||
"Silver Bow": "Progressive Bow",
|
||||
@@ -212,14 +210,6 @@ del data
|
||||
del item
|
||||
|
||||
|
||||
def attribute_item(inventory, team, recipient, item):
|
||||
target_item = links.get(item, item)
|
||||
if item in levels: # non-progressive
|
||||
inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item])
|
||||
else:
|
||||
inventory[team][recipient][target_item] += 1
|
||||
|
||||
|
||||
def attribute_item_solo(inventory, item):
|
||||
"""Adds item to inventory counter, converts everything to progressive."""
|
||||
target_item = links.get(item, item)
|
||||
@@ -237,6 +227,22 @@ def render_timedelta(delta: datetime.timedelta):
|
||||
return f"{hours}:{minutes}"
|
||||
|
||||
|
||||
@pass_context
|
||||
def get_location_name(context: runtime.Context, loc: int) -> str:
|
||||
context_locations = context.get("custom_locations", {})
|
||||
return collections.ChainMap(lookup_any_location_id_to_name, context_locations).get(loc, loc)
|
||||
|
||||
|
||||
@pass_context
|
||||
def get_item_name(context: runtime.Context, item: int) -> str:
|
||||
context_items = context.get("custom_items", {})
|
||||
return collections.ChainMap(lookup_any_item_id_to_name, context_items).get(item, item)
|
||||
|
||||
|
||||
app.jinja_env.filters["location_name"] = get_location_name
|
||||
app.jinja_env.filters["item_name"] = get_item_name
|
||||
|
||||
|
||||
_multidata_cache = {}
|
||||
|
||||
|
||||
@@ -258,10 +264,23 @@ def get_static_room_data(room: Room):
|
||||
# in > 100 players this can take a bit of time and is the main reason for the cache
|
||||
locations: Dict[int, Dict[int, Tuple[int, int, int]]] = multidata['locations']
|
||||
names: Dict[int, Dict[int, str]] = multidata["names"]
|
||||
games = {}
|
||||
groups = {}
|
||||
custom_locations = {}
|
||||
custom_items = {}
|
||||
if "slot_info" in multidata:
|
||||
games = {slot: slot_info.game for slot, slot_info in multidata["slot_info"].items()}
|
||||
groups = {slot: slot_info.group_members for slot, slot_info in multidata["slot_info"].items()
|
||||
if slot_info.type == SlotType.group}
|
||||
|
||||
for game in games.values():
|
||||
if game in multidata["datapackage"]:
|
||||
custom_locations.update(
|
||||
{id: name for name, id in multidata["datapackage"][game]["location_name_to_id"].items()})
|
||||
custom_items.update(
|
||||
{id: name for name, id in multidata["datapackage"][game]["item_name_to_id"].items()})
|
||||
elif "games" in multidata:
|
||||
games = multidata["games"]
|
||||
seed_checks_in_area = checks_in_area.copy()
|
||||
|
||||
use_door_tracker = False
|
||||
@@ -282,7 +301,8 @@ def get_static_room_data(room: Room):
|
||||
if playernumber not in groups}
|
||||
saving_second = get_saving_second(multidata["seed_name"])
|
||||
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
|
||||
multidata["precollected_items"], multidata["games"], multidata["slot_data"], groups, saving_second
|
||||
multidata["precollected_items"], games, multidata["slot_data"], groups, saving_second, \
|
||||
custom_locations, custom_items
|
||||
_multidata_cache[room.seed.id] = result
|
||||
return result
|
||||
|
||||
@@ -309,7 +329,8 @@ def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, w
|
||||
|
||||
# Collect seed information and pare it down to a single player
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
||||
precollected_items, games, slot_data, groups, saving_second = get_static_room_data(room)
|
||||
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
|
||||
get_static_room_data(room)
|
||||
player_name = names[tracked_team][tracked_player - 1]
|
||||
location_to_area = player_location_to_area[tracked_player]
|
||||
inventory = collections.Counter()
|
||||
@@ -351,7 +372,7 @@ def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, w
|
||||
seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second)
|
||||
else:
|
||||
tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
|
||||
seed_checks_in_area, checks_done, saving_second)
|
||||
seed_checks_in_area, checks_done, saving_second, custom_locations, custom_items)
|
||||
|
||||
return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker
|
||||
|
||||
@@ -457,7 +478,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
|
||||
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
|
||||
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
|
||||
"Dragon Egg Shard": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/38/Dragon_Egg_JE4.png",
|
||||
"Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png",
|
||||
"Lead": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/1/1f/Lead_JE2_BE2.png",
|
||||
"Saddle": "https://i.imgur.com/2QtDyR0.png",
|
||||
"Channeling Book": "https://i.imgur.com/J3WsYZw.png",
|
||||
"Silk Touch Book": "https://i.imgur.com/iqERxHQ.png",
|
||||
@@ -465,7 +486,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
|
||||
}
|
||||
|
||||
minecraft_location_ids = {
|
||||
"Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070,
|
||||
"Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070,
|
||||
42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077],
|
||||
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
|
||||
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42104, 42014],
|
||||
@@ -627,7 +648,7 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
|
||||
|
||||
if base_name == "hookshot":
|
||||
display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level)
|
||||
if base_name == "wallet":
|
||||
if base_name == "wallet":
|
||||
display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level)
|
||||
|
||||
# Determine display for bottles. Show letter if it's obtained, determine bottle count
|
||||
@@ -645,7 +666,6 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
|
||||
}
|
||||
for item_name, item_id in multi_items.items():
|
||||
base_name = item_name.split()[-1].lower()
|
||||
count = inventory[item_id]
|
||||
display_data[base_name+"_count"] = inventory[item_id]
|
||||
|
||||
# Gather dungeon locations
|
||||
@@ -775,7 +795,7 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
|
||||
}
|
||||
|
||||
timespinner_location_ids = {
|
||||
"Present": [
|
||||
"Present": [
|
||||
1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009,
|
||||
1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019,
|
||||
1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029,
|
||||
@@ -796,20 +816,20 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
|
||||
1337150, 1337151, 1337152, 1337153, 1337154, 1337155,
|
||||
1337171, 1337172, 1337173, 1337174, 1337175],
|
||||
"Ancient Pyramid": [
|
||||
1337236,
|
||||
1337236,
|
||||
1337246, 1337247, 1337248, 1337249]
|
||||
}
|
||||
|
||||
if(slot_data["DownloadableItems"]):
|
||||
timespinner_location_ids["Present"] += [
|
||||
1337156, 1337157, 1337159,
|
||||
1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
|
||||
1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
|
||||
1337170]
|
||||
if(slot_data["Cantoran"]):
|
||||
timespinner_location_ids["Past"].append(1337176)
|
||||
if(slot_data["LoreChecks"]):
|
||||
timespinner_location_ids["Present"] += [
|
||||
1337177, 1337178, 1337179,
|
||||
1337177, 1337178, 1337179,
|
||||
1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187]
|
||||
timespinner_location_ids["Past"] += [
|
||||
1337188, 1337189,
|
||||
@@ -1190,11 +1210,89 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
**display_data)
|
||||
|
||||
def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||
inventory: Counter, team: int, player: int, playerName: str,
|
||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict, saving_second: int) -> str:
|
||||
|
||||
icons = {
|
||||
"Checks Available": "https://0rganics.org/archipelago/cf/spr_tiles_3.png",
|
||||
"Map Width": "https://0rganics.org/archipelago/cf/spr_tiles_4.png",
|
||||
"Map Height": "https://0rganics.org/archipelago/cf/spr_tiles_5.png",
|
||||
"Map Bombs": "https://0rganics.org/archipelago/cf/spr_tiles_6.png",
|
||||
|
||||
"Nothing": "",
|
||||
}
|
||||
|
||||
checksfinder_location_ids = {
|
||||
"Tile 1": 81000,
|
||||
"Tile 2": 81001,
|
||||
"Tile 3": 81002,
|
||||
"Tile 4": 81003,
|
||||
"Tile 5": 81004,
|
||||
"Tile 6": 81005,
|
||||
"Tile 7": 81006,
|
||||
"Tile 8": 81007,
|
||||
"Tile 9": 81008,
|
||||
"Tile 10": 81009,
|
||||
"Tile 11": 81010,
|
||||
"Tile 12": 81011,
|
||||
"Tile 13": 81012,
|
||||
"Tile 14": 81013,
|
||||
"Tile 15": 81014,
|
||||
"Tile 16": 81015,
|
||||
"Tile 17": 81016,
|
||||
"Tile 18": 81017,
|
||||
"Tile 19": 81018,
|
||||
"Tile 20": 81019,
|
||||
"Tile 21": 81020,
|
||||
"Tile 22": 81021,
|
||||
"Tile 23": 81022,
|
||||
"Tile 24": 81023,
|
||||
"Tile 25": 81024,
|
||||
}
|
||||
|
||||
display_data = {}
|
||||
|
||||
# Multi-items
|
||||
multi_items = {
|
||||
"Map Width": 80000,
|
||||
"Map Height": 80001,
|
||||
"Map Bombs": 80002
|
||||
}
|
||||
for item_name, item_id in multi_items.items():
|
||||
base_name = item_name.split()[-1].lower()
|
||||
count = inventory[item_id]
|
||||
display_data[base_name + "_count"] = count
|
||||
display_data[base_name + "_display"] = count + 5
|
||||
|
||||
# Get location info
|
||||
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
|
||||
lookup_name = lambda id: lookup_any_location_id_to_name[id]
|
||||
location_info = {tile_name: {lookup_name(tile_location): (tile_location in checked_locations)} for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in set(locations[player])}
|
||||
checks_done = {tile_name: len([tile_location]) for tile_name, tile_location in checksfinder_location_ids.items() if tile_location in checked_locations and tile_location in set(locations[player])}
|
||||
checks_done['Total'] = len(checked_locations)
|
||||
checks_in_area = checks_done
|
||||
|
||||
# Calculate checks available
|
||||
display_data["checks_unlocked"] = min(display_data["width_count"] + display_data["height_count"] + display_data["bombs_count"] + 5, 25)
|
||||
display_data["checks_available"] = max(display_data["checks_unlocked"] - len(checked_locations), 0)
|
||||
|
||||
# Victory condition
|
||||
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||
display_data['game_finished'] = game_state == 30
|
||||
|
||||
return render_template("checksfinderTracker.html",
|
||||
inventory=inventory, icons=icons,
|
||||
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
|
||||
id in lookup_any_item_id_to_name},
|
||||
player=player, team=team, room=room, player_name=playerName,
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
**display_data)
|
||||
|
||||
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||
inventory: Counter, team: int, player: int, playerName: str,
|
||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int],
|
||||
saving_second: int) -> str:
|
||||
saving_second: int, custom_locations: Dict[int, str], custom_items: Dict[int, str]) -> str:
|
||||
|
||||
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
|
||||
player_received_items = {}
|
||||
@@ -1212,21 +1310,36 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic
|
||||
player=player, team=team, room=room, player_name=playerName,
|
||||
checked_locations=checked_locations,
|
||||
not_checked_locations=set(locations[player]) - checked_locations,
|
||||
received_items=player_received_items,
|
||||
saving_second=saving_second)
|
||||
received_items=player_received_items, saving_second=saving_second,
|
||||
custom_items=custom_items, custom_locations=custom_locations)
|
||||
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>')
|
||||
@cache.memoize(timeout=1) # multisave is currently created at most every minute
|
||||
def getTracker(tracker: UUID):
|
||||
def get_enabled_multiworld_trackers(room: Room, current: str):
|
||||
enabled = [
|
||||
{
|
||||
"name": "Generic",
|
||||
"endpoint": "get_multiworld_tracker",
|
||||
"current": current == "Generic"
|
||||
}
|
||||
]
|
||||
for game_name, endpoint in multi_trackers.items():
|
||||
if any(slot.game == game_name for slot in room.seed.slots) or current == game_name:
|
||||
enabled.append({
|
||||
"name": game_name,
|
||||
"endpoint": endpoint.__name__,
|
||||
"current": current == game_name}
|
||||
)
|
||||
return enabled
|
||||
|
||||
|
||||
def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]:
|
||||
room: Room = Room.get(tracker=tracker)
|
||||
if not room:
|
||||
abort(404)
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
||||
precollected_items, games, slot_data, groups, saving_second = get_static_room_data(room)
|
||||
return None
|
||||
|
||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
locations, names, use_door_tracker, checks_in_area, player_location_to_area, \
|
||||
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
|
||||
get_static_room_data(room)
|
||||
|
||||
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
|
||||
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||
@@ -1236,7 +1349,6 @@ def getTracker(tracker: UUID):
|
||||
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
|
||||
|
||||
hints = {team: set() for team in range(len(names))}
|
||||
if room.multisave:
|
||||
multisave = restricted_loads(room.multisave)
|
||||
@@ -1246,6 +1358,126 @@ def getTracker(tracker: UUID):
|
||||
for (team, slot), slot_hints in multisave["hints"].items():
|
||||
hints[team] |= set(slot_hints)
|
||||
|
||||
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
|
||||
if player in groups:
|
||||
continue
|
||||
player_locations = locations[player]
|
||||
checks_done[team][player]["Total"] = sum(1 for loc in locations_checked if loc in player_locations)
|
||||
percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] /
|
||||
checks_in_area[player]["Total"] * 100) \
|
||||
if checks_in_area[player]["Total"] else 100
|
||||
|
||||
activity_timers = {}
|
||||
now = datetime.datetime.utcnow()
|
||||
for (team, player), timestamp in multisave.get("client_activity_timers", []):
|
||||
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
||||
|
||||
player_names = {}
|
||||
for team, names in enumerate(names):
|
||||
for player, name in enumerate(names, 1):
|
||||
player_names[(team, player)] = name
|
||||
long_player_names = player_names.copy()
|
||||
for (team, player), alias in multisave.get("name_aliases", {}).items():
|
||||
player_names[(team, player)] = alias
|
||||
long_player_names[(team, player)] = f"{alias} ({long_player_names[(team, player)]})"
|
||||
|
||||
video = {}
|
||||
for (team, player), data in multisave.get("video", []):
|
||||
video[(team, player)] = data
|
||||
|
||||
return dict(player_names=player_names, room=room, checks_done=checks_done,
|
||||
percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area,
|
||||
activity_timers=activity_timers, video=video, hints=hints,
|
||||
long_player_names=long_player_names,
|
||||
multisave=multisave, precollected_items=precollected_items, groups=groups,
|
||||
locations=locations, games=games)
|
||||
|
||||
|
||||
def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]:
|
||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
|
||||
for teamnumber, team_data in data["checks_done"].items()}
|
||||
|
||||
groups = data["groups"]
|
||||
|
||||
for (team, player), locations_checked in data["multisave"].get("location_checks", {}).items():
|
||||
if player in data["groups"]:
|
||||
continue
|
||||
player_locations = data["locations"][player]
|
||||
precollected = data["precollected_items"][player]
|
||||
for item_id in precollected:
|
||||
inventory[team][player][item_id] += 1
|
||||
for location in locations_checked:
|
||||
item_id, recipient, flags = player_locations[location]
|
||||
recipients = groups.get(recipient, [recipient])
|
||||
for recipient in recipients:
|
||||
inventory[team][recipient][item_id] += 1
|
||||
return inventory
|
||||
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>')
|
||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||
def get_multiworld_tracker(tracker: UUID):
|
||||
data = _get_multiworld_tracker_data(tracker)
|
||||
if not data:
|
||||
abort(404)
|
||||
|
||||
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic")
|
||||
|
||||
return render_template("multiTracker.html", **data)
|
||||
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>/Factorio')
|
||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||
def get_Factorio_multiworld_tracker(tracker: UUID):
|
||||
data = _get_multiworld_tracker_data(tracker)
|
||||
if not data:
|
||||
abort(404)
|
||||
|
||||
data["inventory"] = _get_inventory_data(data)
|
||||
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio")
|
||||
|
||||
return render_template("multiFactorioTracker.html", **data)
|
||||
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>/A Link to the Past')
|
||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||
def get_LttP_multiworld_tracker(tracker: UUID):
|
||||
room: Room = Room.get(tracker=tracker)
|
||||
if not room:
|
||||
abort(404)
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
||||
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
|
||||
get_static_room_data(room)
|
||||
|
||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if
|
||||
playernumber not in groups}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
|
||||
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
|
||||
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
|
||||
percent_total_checks_done = {teamnumber: {playernumber: 0
|
||||
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||
for teamnumber, team in enumerate(names)}
|
||||
|
||||
hints = {team: set() for team in range(len(names))}
|
||||
if room.multisave:
|
||||
multisave = restricted_loads(room.multisave)
|
||||
else:
|
||||
multisave = {}
|
||||
if "hints" in multisave:
|
||||
for (team, slot), slot_hints in multisave["hints"].items():
|
||||
hints[team] |= set(slot_hints)
|
||||
|
||||
def attribute_item(team: int, recipient: int, item: int):
|
||||
nonlocal inventory
|
||||
target_item = links.get(item, item)
|
||||
if item in levels: # non-progressive
|
||||
inventory[team][recipient][target_item] = max(inventory[team][recipient][target_item], levels[item])
|
||||
else:
|
||||
inventory[team][recipient][target_item] += 1
|
||||
|
||||
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
|
||||
if player in groups:
|
||||
continue
|
||||
@@ -1253,18 +1485,19 @@ def getTracker(tracker: UUID):
|
||||
if precollected_items:
|
||||
precollected = precollected_items[player]
|
||||
for item_id in precollected:
|
||||
attribute_item(inventory, team, player, item_id)
|
||||
attribute_item(team, player, item_id)
|
||||
for location in locations_checked:
|
||||
if location not in player_locations or location not in player_location_to_area[player]:
|
||||
continue
|
||||
|
||||
item, recipient, flags = player_locations[location]
|
||||
|
||||
if recipient in names:
|
||||
attribute_item(inventory, team, recipient, item)
|
||||
checks_done[team][player][player_location_to_area[player][location]] += 1
|
||||
checks_done[team][player]["Total"] += 1
|
||||
percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if seed_checks_in_area[player]["Total"] else 100
|
||||
recipients = groups.get(recipient, [recipient])
|
||||
for recipient in recipients:
|
||||
attribute_item(team, recipient, item)
|
||||
checks_done[team][player][player_location_to_area[player][location]] += 1
|
||||
checks_done[team][player]["Total"] += 1
|
||||
percent_total_checks_done[team][player] = int(
|
||||
checks_done[team][player]["Total"] / seed_checks_in_area[player]["Total"] * 100) if \
|
||||
seed_checks_in_area[player]["Total"] else 100
|
||||
|
||||
for (team, player), game_state in multisave.get("client_game_state", {}).items():
|
||||
if player in groups:
|
||||
@@ -1306,14 +1539,19 @@ def getTracker(tracker: UUID):
|
||||
for (team, player), data in multisave.get("video", []):
|
||||
video[(team, player)] = data
|
||||
|
||||
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
|
||||
enabled_multiworld_trackers = get_enabled_multiworld_trackers(room, "A Link to the Past")
|
||||
|
||||
return render_template("lttpMultiTracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
|
||||
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
|
||||
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
|
||||
multi_items=multi_items, checks_done=checks_done, percent_total_checks_done=percent_total_checks_done,
|
||||
ordered_areas=ordered_areas, checks_in_area=seed_checks_in_area, activity_timers=activity_timers,
|
||||
multi_items=multi_items, checks_done=checks_done,
|
||||
percent_total_checks_done=percent_total_checks_done,
|
||||
ordered_areas=ordered_areas, checks_in_area=seed_checks_in_area,
|
||||
activity_timers=activity_timers,
|
||||
key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids,
|
||||
video=video, big_key_locations=group_big_key_locations,
|
||||
hints=hints, long_player_names=long_player_names)
|
||||
hints=hints, long_player_names=long_player_names,
|
||||
enabled_multiworld_trackers=enabled_multiworld_trackers)
|
||||
|
||||
|
||||
game_specific_trackers: typing.Dict[str, typing.Callable] = {
|
||||
@@ -1321,6 +1559,12 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
|
||||
"Ocarina of Time": __renderOoTTracker,
|
||||
"Timespinner": __renderTimespinnerTracker,
|
||||
"A Link to the Past": __renderAlttpTracker,
|
||||
"ChecksFinder": __renderChecksfinder,
|
||||
"Super Metroid": __renderSuperMetroidTracker,
|
||||
"Starcraft 2 Wings of Liberty": __renderSC2WoLTracker
|
||||
}
|
||||
|
||||
multi_trackers: typing.Dict[str, typing.Callable] = {
|
||||
"A Link to the Past": get_LttP_multiworld_tracker,
|
||||
"Factorio": get_Factorio_multiworld_tracker,
|
||||
}
|
||||
|
||||
393
Zelda1Client.py
Normal file
393
Zelda1Client.py
Normal file
@@ -0,0 +1,393 @@
|
||||
# Based (read: copied almost wholesale and edited) off the FF1 Client.
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import typing
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from typing import List
|
||||
|
||||
import Utils
|
||||
from Utils import async_start
|
||||
from worlds import lookup_any_location_id_to_name
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
|
||||
get_base_parser
|
||||
|
||||
from worlds.tloz.Items import item_game_ids
|
||||
from worlds.tloz.Locations import location_ids
|
||||
from worlds.tloz import Items, Locations, Rom
|
||||
|
||||
SYSTEM_MESSAGE_ID = 0
|
||||
|
||||
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart Zelda_connector.lua"
|
||||
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure Zelda_connector.lua is running"
|
||||
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart Zelda_connector.lua"
|
||||
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
||||
CONNECTION_CONNECTED_STATUS = "Connected"
|
||||
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||
|
||||
DISPLAY_MSGS = True
|
||||
|
||||
item_ids = item_game_ids
|
||||
location_ids = location_ids
|
||||
items_by_id = {id: item for item, id in item_ids.items()}
|
||||
locations_by_id = {id: location for location, id in location_ids.items()}
|
||||
|
||||
|
||||
class ZeldaCommandProcessor(ClientCommandProcessor):
|
||||
|
||||
def _cmd_nes(self):
|
||||
"""Check NES Connection State"""
|
||||
if isinstance(self.ctx, ZeldaContext):
|
||||
logger.info(f"NES Status: {self.ctx.nes_status}")
|
||||
|
||||
def _cmd_toggle_msgs(self):
|
||||
"""Toggle displaying messages in bizhawk"""
|
||||
global DISPLAY_MSGS
|
||||
DISPLAY_MSGS = not DISPLAY_MSGS
|
||||
logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}")
|
||||
|
||||
|
||||
class ZeldaContext(CommonContext):
|
||||
command_processor = ZeldaCommandProcessor
|
||||
items_handling = 0b101 # get sent remote and starting items
|
||||
# Infinite Hyrule compatibility
|
||||
overworld_item = 0x5F
|
||||
armos_item = 0x24
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super().__init__(server_address, password)
|
||||
self.bonus_items = []
|
||||
self.nes_streams: (StreamReader, StreamWriter) = None
|
||||
self.nes_sync_task = None
|
||||
self.messages = {}
|
||||
self.locations_array = None
|
||||
self.nes_status = CONNECTION_INITIAL_STATUS
|
||||
self.game = 'The Legend of Zelda'
|
||||
self.awaiting_rom = False
|
||||
self.shop_slots_left = 0
|
||||
self.shop_slots_middle = 0
|
||||
self.shop_slots_right = 0
|
||||
self.shop_slots = [self.shop_slots_left, self.shop_slots_middle, self.shop_slots_right]
|
||||
self.slot_data = dict()
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(ZeldaContext, self).server_auth(password_requested)
|
||||
if not self.auth:
|
||||
self.awaiting_rom = True
|
||||
logger.info('Awaiting connection to NES to get Player information')
|
||||
return
|
||||
|
||||
await self.send_connect()
|
||||
|
||||
def _set_message(self, msg: str, msg_id: int):
|
||||
if DISPLAY_MSGS:
|
||||
self.messages[(time.time(), msg_id)] = msg
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == 'Connected':
|
||||
self.slot_data = args.get("slot_data", {})
|
||||
asyncio.create_task(parse_locations(self.locations_array, self, True))
|
||||
elif cmd == 'Print':
|
||||
msg = args['text']
|
||||
if ': !' not in msg:
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
if self.ui:
|
||||
self.ui.print_json(copy.deepcopy(args["data"]))
|
||||
else:
|
||||
text = self.jsontotextparser(copy.deepcopy(args["data"]))
|
||||
logger.info(text)
|
||||
relevant = args.get("type", None) in {"Hint", "ItemSend"}
|
||||
if relevant:
|
||||
item = args["item"]
|
||||
# goes to this world
|
||||
if self.slot_concerns_self(args["receiving"]):
|
||||
relevant = True
|
||||
# found in this world
|
||||
elif self.slot_concerns_self(item.player):
|
||||
relevant = True
|
||||
# not related
|
||||
else:
|
||||
relevant = False
|
||||
if relevant:
|
||||
item = args["item"]
|
||||
msg = self.raw_text_parser(copy.deepcopy(args["data"]))
|
||||
self._set_message(msg, item.item)
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager
|
||||
|
||||
class ZeldaManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago Zelda 1 Client"
|
||||
|
||||
self.ui = ZeldaManager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
|
||||
def get_payload(ctx: ZeldaContext):
|
||||
current_time = time.time()
|
||||
bonus_items = [item for item in ctx.bonus_items]
|
||||
return json.dumps(
|
||||
{
|
||||
"items": [item.item for item in ctx.items_received],
|
||||
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
|
||||
if key[0] > current_time - 10},
|
||||
"shops": {
|
||||
"left": ctx.shop_slots_left,
|
||||
"middle": ctx.shop_slots_middle,
|
||||
"right": ctx.shop_slots_right
|
||||
},
|
||||
"bonusItems": bonus_items
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def reconcile_shops(ctx: ZeldaContext):
|
||||
checked_location_names = [lookup_any_location_id_to_name[location] for location in ctx.checked_locations]
|
||||
shops = [location for location in checked_location_names if "Shop" in location]
|
||||
left_slots = [shop for shop in shops if "Left" in shop]
|
||||
middle_slots = [shop for shop in shops if "Middle" in shop]
|
||||
right_slots = [shop for shop in shops if "Right" in shop]
|
||||
for shop in left_slots:
|
||||
ctx.shop_slots_left |= get_shop_bit_from_name(shop)
|
||||
for shop in middle_slots:
|
||||
ctx.shop_slots_middle |= get_shop_bit_from_name(shop)
|
||||
for shop in right_slots:
|
||||
ctx.shop_slots_right |= get_shop_bit_from_name(shop)
|
||||
|
||||
|
||||
def get_shop_bit_from_name(location_name):
|
||||
if "Potion" in location_name:
|
||||
return Rom.potion_shop
|
||||
elif "Arrow" in location_name:
|
||||
return Rom.arrow_shop
|
||||
elif "Shield" in location_name:
|
||||
return Rom.shield_shop
|
||||
elif "Ring" in location_name:
|
||||
return Rom.ring_shop
|
||||
elif "Candle" in location_name:
|
||||
return Rom.candle_shop
|
||||
elif "Take" in location_name:
|
||||
return Rom.take_any
|
||||
return 0 # this should never be hit
|
||||
|
||||
|
||||
async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone="None"):
|
||||
if locations_array == ctx.locations_array and not force:
|
||||
return
|
||||
else:
|
||||
# print("New values")
|
||||
ctx.locations_array = locations_array
|
||||
locations_checked = []
|
||||
location = None
|
||||
for location in ctx.missing_locations:
|
||||
location_name = lookup_any_location_id_to_name[location]
|
||||
|
||||
if location_name in Locations.overworld_locations and zone == "overworld":
|
||||
status = locations_array[Locations.major_location_offsets[location_name]]
|
||||
if location_name == "Ocean Heart Container":
|
||||
status = locations_array[ctx.overworld_item]
|
||||
if location_name == "Armos Knights":
|
||||
status = locations_array[ctx.armos_item]
|
||||
if status & 0x10:
|
||||
ctx.locations_checked.add(location)
|
||||
locations_checked.append(location)
|
||||
elif location_name in Locations.underworld1_locations and zone == "underworld1":
|
||||
status = locations_array[Locations.floor_location_game_offsets_early[location_name]]
|
||||
if status & 0x10:
|
||||
ctx.locations_checked.add(location)
|
||||
locations_checked.append(location)
|
||||
elif location_name in Locations.underworld2_locations and zone == "underworld2":
|
||||
status = locations_array[Locations.floor_location_game_offsets_late[location_name]]
|
||||
if status & 0x10:
|
||||
ctx.locations_checked.add(location)
|
||||
locations_checked.append(location)
|
||||
elif (location_name in Locations.shop_locations or "Take" in location_name) and zone == "caves":
|
||||
shop_bit = get_shop_bit_from_name(location_name)
|
||||
slot = 0
|
||||
context_slot = 0
|
||||
if "Left" in location_name:
|
||||
slot = "slot1"
|
||||
context_slot = 0
|
||||
elif "Middle" in location_name:
|
||||
slot = "slot2"
|
||||
context_slot = 1
|
||||
elif "Right" in location_name:
|
||||
slot = "slot3"
|
||||
context_slot = 2
|
||||
if locations_array[slot] & shop_bit > 0:
|
||||
locations_checked.append(location)
|
||||
ctx.shop_slots[context_slot] |= shop_bit
|
||||
if locations_array["takeAnys"] and locations_array["takeAnys"] >= 4:
|
||||
if "Take Any" in location_name:
|
||||
short_name = None
|
||||
if "Left" in location_name:
|
||||
short_name = "TakeAnyLeft"
|
||||
elif "Middle" in location_name:
|
||||
short_name = "TakeAnyMiddle"
|
||||
elif "Right" in location_name:
|
||||
short_name = "TakeAnyRight"
|
||||
if short_name is not None:
|
||||
item_code = ctx.slot_data[short_name]
|
||||
if item_code > 0:
|
||||
ctx.bonus_items.append(item_code)
|
||||
locations_checked.append(location)
|
||||
if locations_checked:
|
||||
await ctx.send_msgs([
|
||||
{"cmd": "LocationChecks",
|
||||
"locations": locations_checked}
|
||||
])
|
||||
|
||||
|
||||
async def nes_sync_task(ctx: ZeldaContext):
|
||||
logger.info("Starting nes connector. Use /nes for status information")
|
||||
while not ctx.exit_event.is_set():
|
||||
error_status = None
|
||||
if ctx.nes_streams:
|
||||
(reader, writer) = ctx.nes_streams
|
||||
msg = get_payload(ctx).encode()
|
||||
writer.write(msg)
|
||||
writer.write(b'\n')
|
||||
try:
|
||||
await asyncio.wait_for(writer.drain(), timeout=1.5)
|
||||
try:
|
||||
# Data will return a dict with up to two fields:
|
||||
# 1. A keepalive response of the Players Name (always)
|
||||
# 2. An array representing the memory values of the locations area (if in game)
|
||||
data = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
data_decoded = json.loads(data.decode())
|
||||
if data_decoded["overworldHC"] is not None:
|
||||
ctx.overworld_item = data_decoded["overworldHC"]
|
||||
if data_decoded["overworldPB"] is not None:
|
||||
ctx.armos_item = data_decoded["overworldPB"]
|
||||
if data_decoded['gameMode'] == 19 and ctx.finished_game == False:
|
||||
await ctx.send_msgs([
|
||||
{"cmd": "StatusUpdate",
|
||||
"status": 30}
|
||||
])
|
||||
ctx.finished_game = True
|
||||
if ctx.game is not None and 'overworld' in data_decoded:
|
||||
# Not just a keep alive ping, parse
|
||||
asyncio.create_task(parse_locations(data_decoded['overworld'], ctx, False, "overworld"))
|
||||
if ctx.game is not None and 'underworld1' in data_decoded:
|
||||
asyncio.create_task(parse_locations(data_decoded['underworld1'], ctx, False, "underworld1"))
|
||||
if ctx.game is not None and 'underworld2' in data_decoded:
|
||||
asyncio.create_task(parse_locations(data_decoded['underworld2'], ctx, False, "underworld2"))
|
||||
if ctx.game is not None and 'caves' in data_decoded:
|
||||
asyncio.create_task(parse_locations(data_decoded['caves'], ctx, False, "caves"))
|
||||
if not ctx.auth:
|
||||
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
|
||||
if ctx.auth == '':
|
||||
logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate"
|
||||
"the ROM using the same link but adding your slot name")
|
||||
if ctx.awaiting_rom:
|
||||
await ctx.server_auth(False)
|
||||
reconcile_shops(ctx)
|
||||
except asyncio.TimeoutError:
|
||||
logger.debug("Read Timed Out, Reconnecting")
|
||||
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||
writer.close()
|
||||
ctx.nes_streams = None
|
||||
except ConnectionResetError as e:
|
||||
logger.debug("Read failed due to Connection Lost, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.nes_streams = None
|
||||
except TimeoutError:
|
||||
logger.debug("Connection Timed Out, Reconnecting")
|
||||
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||
writer.close()
|
||||
ctx.nes_streams = None
|
||||
except ConnectionResetError:
|
||||
logger.debug("Connection Lost, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.nes_streams = None
|
||||
if ctx.nes_status == CONNECTION_TENTATIVE_STATUS:
|
||||
if not error_status:
|
||||
logger.info("Successfully Connected to NES")
|
||||
ctx.nes_status = CONNECTION_CONNECTED_STATUS
|
||||
else:
|
||||
ctx.nes_status = f"Was tentatively connected but error occured: {error_status}"
|
||||
elif error_status:
|
||||
ctx.nes_status = error_status
|
||||
logger.info("Lost connection to nes and attempting to reconnect. Use /nes for status updates")
|
||||
else:
|
||||
try:
|
||||
logger.debug("Attempting to connect to NES")
|
||||
ctx.nes_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 52980), timeout=10)
|
||||
ctx.nes_status = CONNECTION_TENTATIVE_STATUS
|
||||
except TimeoutError:
|
||||
logger.debug("Connection Timed Out, Trying Again")
|
||||
ctx.nes_status = CONNECTION_TIMING_OUT_STATUS
|
||||
continue
|
||||
except ConnectionRefusedError:
|
||||
logger.debug("Connection Refused, Trying Again")
|
||||
ctx.nes_status = CONNECTION_REFUSED_STATUS
|
||||
continue
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Text Mode to use !hint and such with games that have no text entry
|
||||
Utils.init_logging("ZeldaClient")
|
||||
|
||||
options = Utils.get_options()
|
||||
DISPLAY_MSGS = options["tloz_options"]["display_msgs"]
|
||||
|
||||
|
||||
async def run_game(romfile: str) -> None:
|
||||
auto_start = typing.cast(typing.Union[bool, str],
|
||||
Utils.get_options()["tloz_options"].get("rom_start", True))
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
elif isinstance(auto_start, str) and os.path.isfile(auto_start):
|
||||
subprocess.Popen([auto_start, romfile],
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
async def main(args):
|
||||
if args.diff_file:
|
||||
import Patch
|
||||
logging.info("Patch file was supplied. Creating nes rom..")
|
||||
meta, romfile = Patch.create_rom_file(args.diff_file)
|
||||
if "server" in meta:
|
||||
args.connect = meta["server"]
|
||||
logging.info(f"Wrote rom file to {romfile}")
|
||||
async_start(run_game(romfile))
|
||||
ctx = ZeldaContext(args.connect, args.password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
ctx.nes_sync_task = asyncio.create_task(nes_sync_task(ctx), name="NES Sync")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
|
||||
await ctx.shutdown()
|
||||
|
||||
if ctx.nes_sync_task:
|
||||
await ctx.nes_sync_task
|
||||
|
||||
|
||||
import colorama
|
||||
|
||||
parser = get_base_parser()
|
||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||
help='Path to a Archipelago Binary Patch file')
|
||||
args = parser.parse_args()
|
||||
colorama.init()
|
||||
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
@@ -7,7 +7,7 @@ local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
||||
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
||||
local STATE_UNINITIALIZED = "Uninitialized"
|
||||
|
||||
local SCRIPT_VERSION = 1
|
||||
local SCRIPT_VERSION = 3
|
||||
|
||||
local APIndex = 0x1A6E
|
||||
local APDeathLinkAddress = 0x00FD
|
||||
@@ -16,7 +16,8 @@ local EventFlagAddress = 0x1735
|
||||
local MissableAddress = 0x161A
|
||||
local HiddenItemsAddress = 0x16DE
|
||||
local RodAddress = 0x1716
|
||||
local InGame = 0x1A71
|
||||
local DexSanityAddress = 0x1A71
|
||||
local InGameAddress = 0x1A84
|
||||
local ClientCompatibilityAddress = 0xFF00
|
||||
|
||||
local ItemsReceived = nil
|
||||
@@ -34,6 +35,7 @@ local frame = 0
|
||||
local u8 = nil
|
||||
local wU8 = nil
|
||||
local u16
|
||||
local compat = nil
|
||||
|
||||
local function defineMemoryFunctions()
|
||||
local memDomain = {}
|
||||
@@ -70,18 +72,6 @@ function slice (tbl, s, e)
|
||||
return new
|
||||
end
|
||||
|
||||
function processBlock(block)
|
||||
if block == nil then
|
||||
return
|
||||
end
|
||||
local itemsBlock = block["items"]
|
||||
memDomain.wram()
|
||||
if itemsBlock ~= nil then
|
||||
ItemsReceived = itemsBlock
|
||||
end
|
||||
deathlink_rec = block["deathlink"]
|
||||
end
|
||||
|
||||
function difference(a, b)
|
||||
local aa = {}
|
||||
for k,v in pairs(a) do aa[v]=true end
|
||||
@@ -99,6 +89,7 @@ function generateLocationsChecked()
|
||||
events = uRange(EventFlagAddress, 0x140)
|
||||
missables = uRange(MissableAddress, 0x20)
|
||||
hiddenitems = uRange(HiddenItemsAddress, 0x0E)
|
||||
dexsanity = uRange(DexSanityAddress, 19)
|
||||
rod = u8(RodAddress)
|
||||
|
||||
data = {}
|
||||
@@ -108,6 +99,9 @@ function generateLocationsChecked()
|
||||
table.foreach(hiddenitems, function(k, v) table.insert(data, v) end)
|
||||
table.insert(data, rod)
|
||||
|
||||
if compat > 1 then
|
||||
table.foreach(dexsanity, function(k, v) table.insert(data, v) end)
|
||||
end
|
||||
return data
|
||||
end
|
||||
|
||||
@@ -141,7 +135,15 @@ function receive()
|
||||
return
|
||||
end
|
||||
if l ~= nil then
|
||||
processBlock(json.decode(l))
|
||||
block = json.decode(l)
|
||||
if block ~= nil then
|
||||
local itemsBlock = block["items"]
|
||||
if itemsBlock ~= nil then
|
||||
ItemsReceived = itemsBlock
|
||||
end
|
||||
deathlink_rec = block["deathlink"]
|
||||
|
||||
end
|
||||
end
|
||||
-- Determine Message to send back
|
||||
memDomain.rom()
|
||||
@@ -156,15 +158,31 @@ function receive()
|
||||
seedName = newSeedName
|
||||
local retTable = {}
|
||||
retTable["scriptVersion"] = SCRIPT_VERSION
|
||||
retTable["clientCompatibilityVersion"] = u8(ClientCompatibilityAddress)
|
||||
|
||||
if compat == nil then
|
||||
compat = u8(ClientCompatibilityAddress)
|
||||
if compat < 2 then
|
||||
InGameAddress = 0x1A71
|
||||
end
|
||||
end
|
||||
|
||||
retTable["clientCompatibilityVersion"] = compat
|
||||
retTable["playerName"] = playerName
|
||||
retTable["seedName"] = seedName
|
||||
memDomain.wram()
|
||||
if u8(InGame) == 0xAC then
|
||||
|
||||
in_game = u8(InGameAddress)
|
||||
if in_game == 0x2A or in_game == 0xAC then
|
||||
retTable["locations"] = generateLocationsChecked()
|
||||
elseif in_game ~= 0 then
|
||||
print("Game may have crashed")
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
end
|
||||
|
||||
retTable["deathLink"] = deathlink_send
|
||||
deathlink_send = false
|
||||
|
||||
msg = json.encode(retTable).."\n"
|
||||
local ret, error = gbSocket:send(msg)
|
||||
if ret == nil then
|
||||
@@ -193,16 +211,23 @@ function main()
|
||||
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
|
||||
if (frame % 5 == 0) then
|
||||
receive()
|
||||
if u8(InGame) == 0xAC and u8(APItemAddress) == 0x00 then
|
||||
ItemIndex = u16(APIndex)
|
||||
if deathlink_rec == true then
|
||||
wU8(APDeathLinkAddress, 1)
|
||||
elseif u8(APDeathLinkAddress) == 3 then
|
||||
wU8(APDeathLinkAddress, 0)
|
||||
deathlink_send = true
|
||||
end
|
||||
if ItemsReceived[ItemIndex + 1] ~= nil then
|
||||
wU8(APItemAddress, ItemsReceived[ItemIndex + 1] - 172000000)
|
||||
in_game = u8(InGameAddress)
|
||||
if in_game == 0x2A or in_game == 0xAC then
|
||||
if u8(APItemAddress) == 0x00 then
|
||||
ItemIndex = u16(APIndex)
|
||||
if deathlink_rec == true then
|
||||
wU8(APDeathLinkAddress, 1)
|
||||
elseif u8(APDeathLinkAddress) == 3 then
|
||||
wU8(APDeathLinkAddress, 0)
|
||||
deathlink_send = true
|
||||
end
|
||||
if ItemsReceived[ItemIndex + 1] ~= nil then
|
||||
item_id = ItemsReceived[ItemIndex + 1] - 172000000
|
||||
if item_id > 255 then
|
||||
item_id = item_id - 256
|
||||
end
|
||||
wU8(APItemAddress, item_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
702
data/lua/TLoZ/TheLegendOfZeldaConnector.lua
Normal file
702
data/lua/TLoZ/TheLegendOfZeldaConnector.lua
Normal file
@@ -0,0 +1,702 @@
|
||||
--Shamelessly based off the FF1 lua
|
||||
|
||||
local socket = require("socket")
|
||||
local json = require('json')
|
||||
local math = require('math')
|
||||
|
||||
local STATE_OK = "Ok"
|
||||
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
||||
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
||||
local STATE_UNINITIALIZED = "Uninitialized"
|
||||
|
||||
local itemMessages = {}
|
||||
local consumableStacks = nil
|
||||
local prevstate = ""
|
||||
local curstate = STATE_UNINITIALIZED
|
||||
local zeldaSocket = nil
|
||||
local frame = 0
|
||||
local gameMode = 0
|
||||
|
||||
local cave_index
|
||||
local triforce_byte
|
||||
local game_state
|
||||
|
||||
local u8 = nil
|
||||
local wU8 = nil
|
||||
local isNesHawk = false
|
||||
|
||||
local shopsChecked = {}
|
||||
local shopSlotLeft = 0x0628
|
||||
local shopSlotMiddle = 0x0629
|
||||
local shopSlotRight = 0x062A
|
||||
|
||||
--N.B.: you won't find these in a RAM map. They're flag values that the base patch derives from the cave ID.
|
||||
local blueRingShopBit = 0x40
|
||||
local potionShopBit = 0x02
|
||||
local arrowShopBit = 0x08
|
||||
local candleShopBit = 0x10
|
||||
local shieldShopBit = 0x20
|
||||
local takeAnyCaveBit = 0x01
|
||||
|
||||
|
||||
local sword = 0x0657
|
||||
local bombs = 0x0658
|
||||
local maxBombs = 0x067C
|
||||
local keys = 0x066E
|
||||
local arrow = 0x0659
|
||||
local bow = 0x065A
|
||||
local candle = 0x065B
|
||||
local recorder = 0x065C
|
||||
local food = 0x065D
|
||||
local waterOfLife = 0x065E
|
||||
local magicalRod = 0x065F
|
||||
local raft = 0x0660
|
||||
local bookOfMagic = 0x0661
|
||||
local ring = 0x0662
|
||||
local stepladder = 0x0663
|
||||
local magicalKey = 0x0664
|
||||
local powerBracelet = 0x0665
|
||||
local letter = 0x0666
|
||||
local clockItem = 0x066C
|
||||
local heartContainers = 0x066F
|
||||
local partialHearts = 0x0670
|
||||
local triforceFragments = 0x0671
|
||||
local boomerang = 0x0674
|
||||
local magicalBoomerang = 0x0675
|
||||
local magicalShield = 0x0676
|
||||
local rupeesToAdd = 0x067D
|
||||
local rupeesToSubtract = 0x067E
|
||||
local itemsObtained = 0x0677
|
||||
local takeAnyCavesChecked = 0x0678
|
||||
local localTriforce = 0x0679
|
||||
local bonusItemsObtained = 0x067A
|
||||
|
||||
itemAPids = {
|
||||
["Boomerang"] = 7100,
|
||||
["Bow"] = 7101,
|
||||
["Magical Boomerang"] = 7102,
|
||||
["Raft"] = 7103,
|
||||
["Stepladder"] = 7104,
|
||||
["Recorder"] = 7105,
|
||||
["Magical Rod"] = 7106,
|
||||
["Red Candle"] = 7107,
|
||||
["Book of Magic"] = 7108,
|
||||
["Magical Key"] = 7109,
|
||||
["Red Ring"] = 7110,
|
||||
["Silver Arrow"] = 7111,
|
||||
["Sword"] = 7112,
|
||||
["White Sword"] = 7113,
|
||||
["Magical Sword"] = 7114,
|
||||
["Heart Container"] = 7115,
|
||||
["Letter"] = 7116,
|
||||
["Magical Shield"] = 7117,
|
||||
["Candle"] = 7118,
|
||||
["Arrow"] = 7119,
|
||||
["Food"] = 7120,
|
||||
["Water of Life (Blue)"] = 7121,
|
||||
["Water of Life (Red)"] = 7122,
|
||||
["Blue Ring"] = 7123,
|
||||
["Triforce Fragment"] = 7124,
|
||||
["Power Bracelet"] = 7125,
|
||||
["Small Key"] = 7126,
|
||||
["Bomb"] = 7127,
|
||||
["Recovery Heart"] = 7128,
|
||||
["Five Rupees"] = 7129,
|
||||
["Rupee"] = 7130,
|
||||
["Clock"] = 7131,
|
||||
["Fairy"] = 7132
|
||||
}
|
||||
|
||||
itemCodes = {
|
||||
["Boomerang"] = 0x1D,
|
||||
["Bow"] = 0x0A,
|
||||
["Magical Boomerang"] = 0x1E,
|
||||
["Raft"] = 0x0C,
|
||||
["Stepladder"] = 0x0D,
|
||||
["Recorder"] = 0x05,
|
||||
["Magical Rod"] = 0x10,
|
||||
["Red Candle"] = 0x07,
|
||||
["Book of Magic"] = 0x11,
|
||||
["Magical Key"] = 0x0B,
|
||||
["Red Ring"] = 0x13,
|
||||
["Silver Arrow"] = 0x09,
|
||||
["Sword"] = 0x01,
|
||||
["White Sword"] = 0x02,
|
||||
["Magical Sword"] = 0x03,
|
||||
["Heart Container"] = 0x1A,
|
||||
["Letter"] = 0x15,
|
||||
["Magical Shield"] = 0x1C,
|
||||
["Candle"] = 0x06,
|
||||
["Arrow"] = 0x08,
|
||||
["Food"] = 0x04,
|
||||
["Water of Life (Blue)"] = 0x1F,
|
||||
["Water of Life (Red)"] = 0x20,
|
||||
["Blue Ring"] = 0x12,
|
||||
["Triforce Fragment"] = 0x1B,
|
||||
["Power Bracelet"] = 0x14,
|
||||
["Small Key"] = 0x19,
|
||||
["Bomb"] = 0x00,
|
||||
["Recovery Heart"] = 0x22,
|
||||
["Five Rupees"] = 0x0F,
|
||||
["Rupee"] = 0x18,
|
||||
["Clock"] = 0x21,
|
||||
["Fairy"] = 0x23
|
||||
}
|
||||
|
||||
|
||||
--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded
|
||||
local function defineMemoryFunctions()
|
||||
local memDomain = {}
|
||||
local domains = memory.getmemorydomainlist()
|
||||
if domains[1] == "System Bus" then
|
||||
--NesHawk
|
||||
isNesHawk = true
|
||||
memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
|
||||
memDomain["ram"] = function() memory.usememorydomain("RAM") end
|
||||
memDomain["saveram"] = function() memory.usememorydomain("Battery RAM") end
|
||||
memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
|
||||
elseif domains[1] == "WRAM" then
|
||||
--QuickNES
|
||||
memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
|
||||
memDomain["ram"] = function() memory.usememorydomain("RAM") end
|
||||
memDomain["saveram"] = function() memory.usememorydomain("WRAM") end
|
||||
memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
|
||||
end
|
||||
return memDomain
|
||||
end
|
||||
|
||||
local memDomain = defineMemoryFunctions()
|
||||
u8 = memory.read_u8
|
||||
wU8 = memory.write_u8
|
||||
uRange = memory.readbyterange
|
||||
|
||||
itemIDNames = {}
|
||||
|
||||
for key, value in pairs(itemAPids) do
|
||||
itemIDNames[value] = key
|
||||
end
|
||||
|
||||
|
||||
|
||||
local function determineItem(array)
|
||||
memdomain.ram()
|
||||
currentItemsObtained = u8(itemsObtained)
|
||||
|
||||
end
|
||||
|
||||
local function gotSword()
|
||||
local currentSword = u8(sword)
|
||||
wU8(sword, math.max(currentSword, 1))
|
||||
end
|
||||
|
||||
local function gotWhiteSword()
|
||||
local currentSword = u8(sword)
|
||||
wU8(sword, math.max(currentSword, 2))
|
||||
end
|
||||
|
||||
local function gotMagicalSword()
|
||||
wU8(sword, 3)
|
||||
end
|
||||
|
||||
local function gotBomb()
|
||||
local currentBombs = u8(bombs)
|
||||
local currentMaxBombs = u8(maxBombs)
|
||||
wU8(bombs, math.min(currentBombs + 4, currentMaxBombs))
|
||||
wU8(0x505, 0x29) -- Fake bomb to show item get.
|
||||
end
|
||||
|
||||
local function gotArrow()
|
||||
local currentArrow = u8(arrow)
|
||||
wU8(arrow, math.max(currentArrow, 1))
|
||||
end
|
||||
|
||||
local function gotSilverArrow()
|
||||
wU8(arrow, 2)
|
||||
end
|
||||
|
||||
local function gotBow()
|
||||
wU8(bow, 1)
|
||||
end
|
||||
|
||||
local function gotCandle()
|
||||
local currentCandle = u8(candle)
|
||||
wU8(candle, math.max(currentCandle, 1))
|
||||
end
|
||||
|
||||
local function gotRedCandle()
|
||||
wU8(candle, 2)
|
||||
end
|
||||
|
||||
local function gotRecorder()
|
||||
wU8(recorder, 1)
|
||||
end
|
||||
|
||||
local function gotFood()
|
||||
wU8(food, 1)
|
||||
end
|
||||
|
||||
local function gotWaterOfLifeBlue()
|
||||
local currentWaterOfLife = u8(waterOfLife)
|
||||
wU8(waterOfLife, math.max(currentWaterOfLife, 1))
|
||||
end
|
||||
|
||||
local function gotWaterOfLifeRed()
|
||||
wU8(waterOfLife, 2)
|
||||
end
|
||||
|
||||
local function gotMagicalRod()
|
||||
wU8(magicalRod, 1)
|
||||
end
|
||||
|
||||
local function gotBookOfMagic()
|
||||
wU8(bookOfMagic, 1)
|
||||
end
|
||||
|
||||
local function gotRaft()
|
||||
wU8(raft, 1)
|
||||
end
|
||||
|
||||
local function gotBlueRing()
|
||||
local currentRing = u8(ring)
|
||||
wU8(ring, math.max(currentRing, 1))
|
||||
memDomain.saveram()
|
||||
local currentTunicColor = u8(0x0B92)
|
||||
if currentTunicColor == 0x29 then
|
||||
wU8(0x0B92, 0x32)
|
||||
wU8(0x0804, 0x32)
|
||||
end
|
||||
end
|
||||
|
||||
local function gotRedRing()
|
||||
wU8(ring, 2)
|
||||
memDomain.saveram()
|
||||
wU8(0x0B92, 0x16)
|
||||
wU8(0x0804, 0x16)
|
||||
end
|
||||
|
||||
local function gotStepladder()
|
||||
wU8(stepladder, 1)
|
||||
end
|
||||
|
||||
local function gotMagicalKey()
|
||||
wU8(magicalKey, 1)
|
||||
end
|
||||
|
||||
local function gotPowerBracelet()
|
||||
wU8(powerBracelet, 1)
|
||||
end
|
||||
|
||||
local function gotLetter()
|
||||
wU8(letter, 1)
|
||||
end
|
||||
|
||||
local function gotHeartContainer()
|
||||
local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4)
|
||||
if currentHeartContainers < 16 then
|
||||
currentHeartContainers = math.min(currentHeartContainers + 1, 16)
|
||||
local currentHearts = bit.band(u8(heartContainers), 0x0F) + 1
|
||||
wU8(heartContainers, bit.lshift(currentHeartContainers, 4) + currentHearts)
|
||||
end
|
||||
end
|
||||
|
||||
local function gotTriforceFragment()
|
||||
local triforceByte = 0xFF
|
||||
local newTriforceCount = u8(localTriforce) + 1
|
||||
wU8(localTriforce, newTriforceCount)
|
||||
end
|
||||
|
||||
local function gotBoomerang()
|
||||
wU8(boomerang, 1)
|
||||
end
|
||||
|
||||
local function gotMagicalBoomerang()
|
||||
wU8(magicalBoomerang, 1)
|
||||
end
|
||||
|
||||
local function gotMagicalShield()
|
||||
wU8(magicalShield, 1)
|
||||
end
|
||||
|
||||
local function gotRecoveryHeart()
|
||||
local currentHearts = bit.band(u8(heartContainers), 0x0F)
|
||||
local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4)
|
||||
if currentHearts < currentHeartContainers then
|
||||
currentHearts = currentHearts + 1
|
||||
else
|
||||
wU8(partialHearts, 0xFF)
|
||||
end
|
||||
currentHearts = bit.bor(bit.band(u8(heartContainers), 0xF0), currentHearts)
|
||||
wU8(heartContainers, currentHearts)
|
||||
end
|
||||
|
||||
local function gotFairy()
|
||||
local currentHearts = bit.band(u8(heartContainers), 0x0F)
|
||||
local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4)
|
||||
if currentHearts < currentHeartContainers then
|
||||
currentHearts = currentHearts + 3
|
||||
if currentHearts > currentHeartContainers then
|
||||
currentHearts = currentHeartContainers
|
||||
wU8(partialHearts, 0xFF)
|
||||
end
|
||||
else
|
||||
wU8(partialHearts, 0xFF)
|
||||
end
|
||||
currentHearts = bit.bor(bit.band(u8(heartContainers), 0xF0), currentHearts)
|
||||
wU8(heartContainers, currentHearts)
|
||||
end
|
||||
|
||||
local function gotClock()
|
||||
wU8(clockItem, 1)
|
||||
end
|
||||
|
||||
local function gotFiveRupees()
|
||||
local currentRupeesToAdd = u8(rupeesToAdd)
|
||||
wU8(rupeesToAdd, math.min(currentRupeesToAdd + 5, 255))
|
||||
end
|
||||
|
||||
local function gotSmallKey()
|
||||
wU8(keys, math.min(u8(keys) + 1, 9))
|
||||
end
|
||||
|
||||
local function gotItem(item)
|
||||
--Write itemCode to itemToLift
|
||||
--Write 128 to itemLiftTimer
|
||||
--Write 4 to sound effect queue
|
||||
itemName = itemIDNames[item]
|
||||
itemCode = itemCodes[itemName]
|
||||
wU8(0x505, itemCode)
|
||||
wU8(0x506, 128)
|
||||
wU8(0x602, 4)
|
||||
numberObtained = u8(itemsObtained) + 1
|
||||
wU8(itemsObtained, numberObtained)
|
||||
if itemName == "Boomerang" then gotBoomerang() end
|
||||
if itemName == "Bow" then gotBow() end
|
||||
if itemName == "Magical Boomerang" then gotMagicalBoomerang() end
|
||||
if itemName == "Raft" then gotRaft() end
|
||||
if itemName == "Stepladder" then gotStepladder() end
|
||||
if itemName == "Recorder" then gotRecorder() end
|
||||
if itemName == "Magical Rod" then gotMagicalRod() end
|
||||
if itemName == "Red Candle" then gotRedCandle() end
|
||||
if itemName == "Book of Magic" then gotBookOfMagic() end
|
||||
if itemName == "Magical Key" then gotMagicalKey() end
|
||||
if itemName == "Red Ring" then gotRedRing() end
|
||||
if itemName == "Silver Arrow" then gotSilverArrow() end
|
||||
if itemName == "Sword" then gotSword() end
|
||||
if itemName == "White Sword" then gotWhiteSword() end
|
||||
if itemName == "Magical Sword" then gotMagicalSword() end
|
||||
if itemName == "Heart Container" then gotHeartContainer() end
|
||||
if itemName == "Letter" then gotLetter() end
|
||||
if itemName == "Magical Shield" then gotMagicalShield() end
|
||||
if itemName == "Candle" then gotCandle() end
|
||||
if itemName == "Arrow" then gotArrow() end
|
||||
if itemName == "Food" then gotFood() end
|
||||
if itemName == "Water of Life (Blue)" then gotWaterOfLifeBlue() end
|
||||
if itemName == "Water of Life (Red)" then gotWaterOfLifeRed() end
|
||||
if itemName == "Blue Ring" then gotBlueRing() end
|
||||
if itemName == "Triforce Fragment" then gotTriforceFragment() end
|
||||
if itemName == "Power Bracelet" then gotPowerBracelet() end
|
||||
if itemName == "Small Key" then gotSmallKey() end
|
||||
if itemName == "Bomb" then gotBomb() end
|
||||
if itemName == "Recovery Heart" then gotRecoveryHeart() end
|
||||
if itemName == "Five Rupees" then gotFiveRupees() end
|
||||
if itemName == "Fairy" then gotFairy() end
|
||||
if itemName == "Clock" then gotClock() end
|
||||
end
|
||||
|
||||
|
||||
local function StateOKForMainLoop()
|
||||
memDomain.ram()
|
||||
local gameMode = u8(0x12)
|
||||
return gameMode == 5
|
||||
end
|
||||
|
||||
local function checkCaveItemObtained()
|
||||
memDomain.ram()
|
||||
local returnTable = {}
|
||||
returnTable["slot1"] = u8(shopSlotLeft)
|
||||
returnTable["slot2"] = u8(shopSlotMiddle)
|
||||
returnTable["slot3"] = u8(shopSlotRight)
|
||||
returnTable["takeAnys"] = u8(takeAnyCavesChecked)
|
||||
return returnTable
|
||||
end
|
||||
|
||||
function table.empty (self)
|
||||
for _, _ in pairs(self) do
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function slice (tbl, s, e)
|
||||
local pos, new = 1, {}
|
||||
for i = s + 1, e do
|
||||
new[pos] = tbl[i]
|
||||
pos = pos + 1
|
||||
end
|
||||
return new
|
||||
end
|
||||
|
||||
local bizhawk_version = client.getversion()
|
||||
local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5")
|
||||
local is26To28 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7") or (bizhawk_version:sub(1,3)=="2.8")
|
||||
|
||||
local function getMaxMessageLength()
|
||||
if is23Or24Or25 then
|
||||
return client.screenwidth()/11
|
||||
elseif is26To28 then
|
||||
return client.screenwidth()/12
|
||||
end
|
||||
end
|
||||
|
||||
local function drawText(x, y, message, color)
|
||||
if is23Or24Or25 then
|
||||
gui.addmessage(message)
|
||||
elseif is26To28 then
|
||||
gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", "middle", "bottom", nil, "client")
|
||||
end
|
||||
end
|
||||
|
||||
local function clearScreen()
|
||||
if is23Or24Or25 then
|
||||
return
|
||||
elseif is26To28 then
|
||||
drawText(0, 0, "", "black")
|
||||
end
|
||||
end
|
||||
|
||||
local function drawMessages()
|
||||
if table.empty(itemMessages) then
|
||||
clearScreen()
|
||||
return
|
||||
end
|
||||
local y = 10
|
||||
found = false
|
||||
maxMessageLength = getMaxMessageLength()
|
||||
for k, v in pairs(itemMessages) do
|
||||
if v["TTL"] > 0 then
|
||||
message = v["message"]
|
||||
while true do
|
||||
drawText(5, y, message:sub(1, maxMessageLength), v["color"])
|
||||
y = y + 16
|
||||
|
||||
message = message:sub(maxMessageLength + 1, message:len())
|
||||
if message:len() == 0 then
|
||||
break
|
||||
end
|
||||
end
|
||||
newTTL = 0
|
||||
if is26To28 then
|
||||
newTTL = itemMessages[k]["TTL"] - 1
|
||||
end
|
||||
itemMessages[k]["TTL"] = newTTL
|
||||
found = true
|
||||
end
|
||||
end
|
||||
if found == false then
|
||||
clearScreen()
|
||||
end
|
||||
end
|
||||
|
||||
function generateOverworldLocationChecked()
|
||||
memDomain.ram()
|
||||
data = uRange(0x067E, 0x81)
|
||||
data[0] = nil
|
||||
return data
|
||||
end
|
||||
|
||||
function getHCLocation()
|
||||
memDomain.rom()
|
||||
data = u8(0x1789A)
|
||||
return data
|
||||
end
|
||||
|
||||
function getPBLocation()
|
||||
memDomain.rom()
|
||||
data = u8(0x10CB2)
|
||||
return data
|
||||
end
|
||||
|
||||
function generateUnderworld16LocationChecked()
|
||||
memDomain.ram()
|
||||
data = uRange(0x06FE, 0x81)
|
||||
data[0] = nil
|
||||
return data
|
||||
end
|
||||
|
||||
function generateUnderworld79LocationChecked()
|
||||
memDomain.ram()
|
||||
data = uRange(0x077E, 0x81)
|
||||
data[0] = nil
|
||||
return data
|
||||
end
|
||||
|
||||
function updateTriforceFragments()
|
||||
memDomain.ram()
|
||||
local triforceByte = 0xFF
|
||||
totalTriforceCount = u8(localTriforce)
|
||||
local currentPieces = bit.rshift(triforceByte, 8 - math.min(8, totalTriforceCount))
|
||||
wU8(triforceFragments, currentPieces)
|
||||
end
|
||||
|
||||
function processBlock(block)
|
||||
if block ~= nil then
|
||||
local msgBlock = block['messages']
|
||||
if msgBlock ~= nil then
|
||||
for i, v in pairs(msgBlock) do
|
||||
if itemMessages[i] == nil then
|
||||
local msg = {TTL=450, message=v, color=0xFFFF0000}
|
||||
itemMessages[i] = msg
|
||||
end
|
||||
end
|
||||
end
|
||||
local bonusItems = block["bonusItems"]
|
||||
if bonusItems ~= nil and isInGame then
|
||||
for i, item in ipairs(bonusItems) do
|
||||
memDomain.ram()
|
||||
if i > u8(bonusItemsObtained) then
|
||||
if u8(0x505) == 0 then
|
||||
gotItem(item)
|
||||
wU8(itemsObtained, u8(itemsObtained) - 1)
|
||||
wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local itemsBlock = block["items"]
|
||||
memDomain.saveram()
|
||||
isInGame = StateOKForMainLoop()
|
||||
updateTriforceFragments()
|
||||
if itemsBlock ~= nil and isInGame then
|
||||
memDomain.ram()
|
||||
--get item from item code
|
||||
--get function from item
|
||||
--do function
|
||||
for i, item in ipairs(itemsBlock) do
|
||||
memDomain.ram()
|
||||
if u8(0x505) == 0 then
|
||||
if i > u8(itemsObtained) then
|
||||
gotItem(item)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local shopsBlock = block["shops"]
|
||||
if shopsBlock ~= nil then
|
||||
wU8(shopSlotLeft, bit.bor(u8(shopSlotLeft), shopsBlock["left"]))
|
||||
wU8(shopSlotMiddle, bit.bor(u8(shopSlotMiddle), shopsBlock["middle"]))
|
||||
wU8(shopSlotRight, bit.bor(u8(shopSlotRight), shopsBlock["right"]))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function difference(a, b)
|
||||
local aa = {}
|
||||
for k,v in pairs(a) do aa[v]=true end
|
||||
for k,v in pairs(b) do aa[v]=nil end
|
||||
local ret = {}
|
||||
local n = 0
|
||||
for k,v in pairs(a) do
|
||||
if aa[v] then n=n+1 ret[n]=v end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function receive()
|
||||
l, e = zeldaSocket:receive()
|
||||
if e == 'closed' then
|
||||
if curstate == STATE_OK then
|
||||
print("Connection closed")
|
||||
end
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
elseif e == 'timeout' then
|
||||
print("timeout")
|
||||
return
|
||||
elseif e ~= nil then
|
||||
print(e)
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
end
|
||||
processBlock(json.decode(l))
|
||||
|
||||
-- Determine Message to send back
|
||||
memDomain.rom()
|
||||
local playerName = uRange(0x1F, 0x10)
|
||||
playerName[0] = nil
|
||||
local retTable = {}
|
||||
retTable["playerName"] = playerName
|
||||
if StateOKForMainLoop() then
|
||||
retTable["overworld"] = generateOverworldLocationChecked()
|
||||
retTable["underworld1"] = generateUnderworld16LocationChecked()
|
||||
retTable["underworld2"] = generateUnderworld79LocationChecked()
|
||||
end
|
||||
retTable["caves"] = checkCaveItemObtained()
|
||||
memDomain.ram()
|
||||
if gameMode ~= 19 then
|
||||
gameMode = u8(0x12)
|
||||
end
|
||||
retTable["gameMode"] = gameMode
|
||||
retTable["overworldHC"] = getHCLocation()
|
||||
retTable["overworldPB"] = getPBLocation()
|
||||
retTable["itemsObtained"] = u8(itemsObtained)
|
||||
msg = json.encode(retTable).."\n"
|
||||
local ret, error = zeldaSocket:send(msg)
|
||||
if ret == nil then
|
||||
print(error)
|
||||
elseif curstate == STATE_INITIAL_CONNECTION_MADE then
|
||||
curstate = STATE_TENTATIVELY_CONNECTED
|
||||
elseif curstate == STATE_TENTATIVELY_CONNECTED then
|
||||
print("Connected!")
|
||||
itemMessages["(0,0)"] = {TTL=240, message="Connected", color="green"}
|
||||
curstate = STATE_OK
|
||||
end
|
||||
end
|
||||
|
||||
function main()
|
||||
if (is23Or24Or25 or is26To28) == false then
|
||||
print("Must use a version of bizhawk 2.3.1 or higher")
|
||||
return
|
||||
end
|
||||
server, error = socket.bind('localhost', 52980)
|
||||
|
||||
while true do
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
|
||||
frame = frame + 1
|
||||
drawMessages()
|
||||
if not (curstate == prevstate) then
|
||||
-- console.log("Current state: "..curstate)
|
||||
prevstate = curstate
|
||||
end
|
||||
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
|
||||
if (frame % 60 == 0) then
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "Blue")
|
||||
receive()
|
||||
else
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "Green")
|
||||
end
|
||||
elseif (curstate == STATE_UNINITIALIZED) then
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "White")
|
||||
if (frame % 60 == 0) then
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
|
||||
|
||||
drawText(5, 8, "Waiting for client", 0xFFFF0000)
|
||||
drawText(5, 32, "Please start Zelda1Client.exe", 0xFFFF0000)
|
||||
|
||||
-- Advance so the messages are drawn
|
||||
emu.frameadvance()
|
||||
server:settimeout(2)
|
||||
print("Attempting to connect")
|
||||
local client, timeout = server:accept()
|
||||
if timeout == nil then
|
||||
-- print('Initial Connection Made')
|
||||
curstate = STATE_INITIAL_CONNECTION_MADE
|
||||
zeldaSocket = client
|
||||
zeldaSocket:settimeout(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
emu.frameadvance()
|
||||
end
|
||||
end
|
||||
|
||||
main()
|
||||
BIN
data/lua/TLoZ/core.dll
Normal file
BIN
data/lua/TLoZ/core.dll
Normal file
Binary file not shown.
380
data/lua/TLoZ/json.lua
Normal file
380
data/lua/TLoZ/json.lua
Normal file
@@ -0,0 +1,380 @@
|
||||
--
|
||||
-- json.lua
|
||||
--
|
||||
-- Copyright (c) 2015 rxi
|
||||
--
|
||||
-- This library is free software; you can redistribute it and/or modify it
|
||||
-- under the terms of the MIT license. See LICENSE for details.
|
||||
--
|
||||
|
||||
local json = { _version = "0.1.0" }
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Encode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local encode
|
||||
|
||||
local escape_char_map = {
|
||||
[ "\\" ] = "\\\\",
|
||||
[ "\"" ] = "\\\"",
|
||||
[ "\b" ] = "\\b",
|
||||
[ "\f" ] = "\\f",
|
||||
[ "\n" ] = "\\n",
|
||||
[ "\r" ] = "\\r",
|
||||
[ "\t" ] = "\\t",
|
||||
}
|
||||
|
||||
local escape_char_map_inv = { [ "\\/" ] = "/" }
|
||||
for k, v in pairs(escape_char_map) do
|
||||
escape_char_map_inv[v] = k
|
||||
end
|
||||
|
||||
|
||||
local function escape_char(c)
|
||||
return escape_char_map[c] or string.format("\\u%04x", c:byte())
|
||||
end
|
||||
|
||||
|
||||
local function encode_nil(val)
|
||||
return "null"
|
||||
end
|
||||
|
||||
|
||||
local function encode_table(val, stack)
|
||||
local res = {}
|
||||
stack = stack or {}
|
||||
|
||||
-- Circular reference?
|
||||
if stack[val] then error("circular reference") end
|
||||
|
||||
stack[val] = true
|
||||
|
||||
if val[1] ~= nil or next(val) == nil then
|
||||
-- Treat as array -- check keys are valid and it is not sparse
|
||||
local n = 0
|
||||
for k in pairs(val) do
|
||||
if type(k) ~= "number" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
n = n + 1
|
||||
end
|
||||
if n ~= #val then
|
||||
error("invalid table: sparse array")
|
||||
end
|
||||
-- Encode
|
||||
for i, v in ipairs(val) do
|
||||
table.insert(res, encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "[" .. table.concat(res, ",") .. "]"
|
||||
|
||||
else
|
||||
-- Treat as an object
|
||||
for k, v in pairs(val) do
|
||||
if type(k) ~= "string" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "{" .. table.concat(res, ",") .. "}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function encode_string(val)
|
||||
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
||||
end
|
||||
|
||||
|
||||
local function encode_number(val)
|
||||
-- Check for NaN, -inf and inf
|
||||
if val ~= val or val <= -math.huge or val >= math.huge then
|
||||
error("unexpected number value '" .. tostring(val) .. "'")
|
||||
end
|
||||
return string.format("%.14g", val)
|
||||
end
|
||||
|
||||
|
||||
local type_func_map = {
|
||||
[ "nil" ] = encode_nil,
|
||||
[ "table" ] = encode_table,
|
||||
[ "string" ] = encode_string,
|
||||
[ "number" ] = encode_number,
|
||||
[ "boolean" ] = tostring,
|
||||
}
|
||||
|
||||
|
||||
encode = function(val, stack)
|
||||
local t = type(val)
|
||||
local f = type_func_map[t]
|
||||
if f then
|
||||
return f(val, stack)
|
||||
end
|
||||
error("unexpected type '" .. t .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.encode(val)
|
||||
return ( encode(val) )
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Decode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local parse
|
||||
|
||||
local function create_set(...)
|
||||
local res = {}
|
||||
for i = 1, select("#", ...) do
|
||||
res[ select(i, ...) ] = true
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local space_chars = create_set(" ", "\t", "\r", "\n")
|
||||
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
|
||||
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
|
||||
local literals = create_set("true", "false", "null")
|
||||
|
||||
local literal_map = {
|
||||
[ "true" ] = true,
|
||||
[ "false" ] = false,
|
||||
[ "null" ] = nil,
|
||||
}
|
||||
|
||||
|
||||
local function next_char(str, idx, set, negate)
|
||||
for i = idx, #str do
|
||||
if set[str:sub(i, i)] ~= negate then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return #str + 1
|
||||
end
|
||||
|
||||
|
||||
local function decode_error(str, idx, msg)
|
||||
--local line_count = 1
|
||||
--local col_count = 1
|
||||
--for i = 1, idx - 1 do
|
||||
-- col_count = col_count + 1
|
||||
-- if str:sub(i, i) == "\n" then
|
||||
-- line_count = line_count + 1
|
||||
-- col_count = 1
|
||||
-- end
|
||||
-- end
|
||||
-- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
||||
end
|
||||
|
||||
|
||||
local function codepoint_to_utf8(n)
|
||||
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
||||
local f = math.floor
|
||||
if n <= 0x7f then
|
||||
return string.char(n)
|
||||
elseif n <= 0x7ff then
|
||||
return string.char(f(n / 64) + 192, n % 64 + 128)
|
||||
elseif n <= 0xffff then
|
||||
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
elseif n <= 0x10ffff then
|
||||
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
||||
f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
end
|
||||
error( string.format("invalid unicode codepoint '%x'", n) )
|
||||
end
|
||||
|
||||
|
||||
local function parse_unicode_escape(s)
|
||||
local n1 = tonumber( s:sub(3, 6), 16 )
|
||||
local n2 = tonumber( s:sub(9, 12), 16 )
|
||||
-- Surrogate pair?
|
||||
if n2 then
|
||||
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
||||
else
|
||||
return codepoint_to_utf8(n1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function parse_string(str, i)
|
||||
local has_unicode_escape = false
|
||||
local has_surrogate_escape = false
|
||||
local has_escape = false
|
||||
local last
|
||||
for j = i + 1, #str do
|
||||
local x = str:byte(j)
|
||||
|
||||
if x < 32 then
|
||||
decode_error(str, j, "control character in string")
|
||||
end
|
||||
|
||||
if last == 92 then -- "\\" (escape char)
|
||||
if x == 117 then -- "u" (unicode escape sequence)
|
||||
local hex = str:sub(j + 1, j + 5)
|
||||
if not hex:find("%x%x%x%x") then
|
||||
decode_error(str, j, "invalid unicode escape in string")
|
||||
end
|
||||
if hex:find("^[dD][89aAbB]") then
|
||||
has_surrogate_escape = true
|
||||
else
|
||||
has_unicode_escape = true
|
||||
end
|
||||
else
|
||||
local c = string.char(x)
|
||||
if not escape_chars[c] then
|
||||
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
|
||||
end
|
||||
has_escape = true
|
||||
end
|
||||
last = nil
|
||||
|
||||
elseif x == 34 then -- '"' (end of string)
|
||||
local s = str:sub(i + 1, j - 1)
|
||||
if has_surrogate_escape then
|
||||
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_unicode_escape then
|
||||
s = s:gsub("\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_escape then
|
||||
s = s:gsub("\\.", escape_char_map_inv)
|
||||
end
|
||||
return s, j + 1
|
||||
|
||||
else
|
||||
last = x
|
||||
end
|
||||
end
|
||||
decode_error(str, i, "expected closing quote for string")
|
||||
end
|
||||
|
||||
|
||||
local function parse_number(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local s = str:sub(i, x - 1)
|
||||
local n = tonumber(s)
|
||||
if not n then
|
||||
decode_error(str, i, "invalid number '" .. s .. "'")
|
||||
end
|
||||
return n, x
|
||||
end
|
||||
|
||||
|
||||
local function parse_literal(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local word = str:sub(i, x - 1)
|
||||
if not literals[word] then
|
||||
decode_error(str, i, "invalid literal '" .. word .. "'")
|
||||
end
|
||||
return literal_map[word], x
|
||||
end
|
||||
|
||||
|
||||
local function parse_array(str, i)
|
||||
local res = {}
|
||||
local n = 1
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local x
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of array?
|
||||
if str:sub(i, i) == "]" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read token
|
||||
x, i = parse(str, i)
|
||||
res[n] = x
|
||||
n = n + 1
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "]" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local function parse_object(str, i)
|
||||
local res = {}
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local key, val
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of object?
|
||||
if str:sub(i, i) == "}" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read key
|
||||
if str:sub(i, i) ~= '"' then
|
||||
decode_error(str, i, "expected string for key")
|
||||
end
|
||||
key, i = parse(str, i)
|
||||
-- Read ':' delimiter
|
||||
i = next_char(str, i, space_chars, true)
|
||||
if str:sub(i, i) ~= ":" then
|
||||
decode_error(str, i, "expected ':' after key")
|
||||
end
|
||||
i = next_char(str, i + 1, space_chars, true)
|
||||
-- Read value
|
||||
val, i = parse(str, i)
|
||||
-- Set
|
||||
res[key] = val
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "}" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local char_func_map = {
|
||||
[ '"' ] = parse_string,
|
||||
[ "0" ] = parse_number,
|
||||
[ "1" ] = parse_number,
|
||||
[ "2" ] = parse_number,
|
||||
[ "3" ] = parse_number,
|
||||
[ "4" ] = parse_number,
|
||||
[ "5" ] = parse_number,
|
||||
[ "6" ] = parse_number,
|
||||
[ "7" ] = parse_number,
|
||||
[ "8" ] = parse_number,
|
||||
[ "9" ] = parse_number,
|
||||
[ "-" ] = parse_number,
|
||||
[ "t" ] = parse_literal,
|
||||
[ "f" ] = parse_literal,
|
||||
[ "n" ] = parse_literal,
|
||||
[ "[" ] = parse_array,
|
||||
[ "{" ] = parse_object,
|
||||
}
|
||||
|
||||
|
||||
parse = function(str, idx)
|
||||
local chr = str:sub(idx, idx)
|
||||
local f = char_func_map[chr]
|
||||
if f then
|
||||
return f(str, idx)
|
||||
end
|
||||
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.decode(str)
|
||||
if type(str) ~= "string" then
|
||||
error("expected argument of type string, got " .. type(str))
|
||||
end
|
||||
return ( parse(str, next_char(str, 1, space_chars, true)) )
|
||||
end
|
||||
|
||||
|
||||
return json
|
||||
132
data/lua/TLoZ/socket.lua
Normal file
132
data/lua/TLoZ/socket.lua
Normal file
@@ -0,0 +1,132 @@
|
||||
-----------------------------------------------------------------------------
|
||||
-- LuaSocket helper module
|
||||
-- Author: Diego Nehab
|
||||
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Declare module and import dependencies
|
||||
-----------------------------------------------------------------------------
|
||||
local base = _G
|
||||
local string = require("string")
|
||||
local math = require("math")
|
||||
local socket = require("socket.core")
|
||||
module("socket")
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Exported auxiliar functions
|
||||
-----------------------------------------------------------------------------
|
||||
function connect(address, port, laddress, lport)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
if laddress then
|
||||
local res, err = sock:bind(laddress, lport, -1)
|
||||
if not res then return nil, err end
|
||||
end
|
||||
local res, err = sock:connect(address, port)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
function bind(host, port, backlog)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
sock:setoption("reuseaddr", true)
|
||||
local res, err = sock:bind(host, port)
|
||||
if not res then return nil, err end
|
||||
res, err = sock:listen(backlog)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
try = newtry()
|
||||
|
||||
function choose(table)
|
||||
return function(name, opt1, opt2)
|
||||
if base.type(name) ~= "string" then
|
||||
name, opt1, opt2 = "default", name, opt1
|
||||
end
|
||||
local f = table[name or "nil"]
|
||||
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
|
||||
else return f(opt1, opt2) end
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Socket sources and sinks, conforming to LTN12
|
||||
-----------------------------------------------------------------------------
|
||||
-- create namespaces inside LuaSocket namespace
|
||||
sourcet = {}
|
||||
sinkt = {}
|
||||
|
||||
BLOCKSIZE = 2048
|
||||
|
||||
sinkt["close-when-done"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if not chunk then
|
||||
sock:close()
|
||||
return 1
|
||||
else return sock:send(chunk) end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["keep-open"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if chunk then return sock:send(chunk)
|
||||
else return 1 end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["default"] = sinkt["keep-open"]
|
||||
|
||||
sink = choose(sinkt)
|
||||
|
||||
sourcet["by-length"] = function(sock, length)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if length <= 0 then return nil end
|
||||
local size = math.min(socket.BLOCKSIZE, length)
|
||||
local chunk, err = sock:receive(size)
|
||||
if err then return nil, err end
|
||||
length = length - string.len(chunk)
|
||||
return chunk
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sourcet["until-closed"] = function(sock)
|
||||
local done
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if done then return nil end
|
||||
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
|
||||
if not err then return chunk
|
||||
elseif err == "closed" then
|
||||
sock:close()
|
||||
done = 1
|
||||
return partial
|
||||
else return nil, err end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
sourcet["default"] = sourcet["until-closed"]
|
||||
|
||||
source = choose(sourcet)
|
||||
188
docs/options api.md
Normal file
188
docs/options api.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Archipelago Options API
|
||||
|
||||
This document covers some of the generic options available using Archipelago's options handling system.
|
||||
|
||||
For more information on where these options go in your world please refer to:
|
||||
- [world api.md](/docs/world%20api.md)
|
||||
|
||||
Archipelago will be abbreviated as "AP" from now on.
|
||||
|
||||
## Option Definitions
|
||||
Option parsing in AP is done using different Option classes. For each option you would like to have in your game, you
|
||||
need to create:
|
||||
- A new option class with a docstring detailing what the option will do to your user.
|
||||
- A `display_name` to be displayed on the webhost.
|
||||
- A new entry in the `option_definitions` dict for your World.
|
||||
By style and convention, the internal names should be snake_case. If the option supports having multiple sub_options
|
||||
such as Choice options, these can be defined with `option_my_sub_option`, where the preceding `option_` is required and
|
||||
stripped for users, so will show as `my_sub_option` in yaml files and if `auto_display_name` is True `My Sub Option`
|
||||
on the webhost. All options support `random` as a generic option. `random` chooses from any of the available
|
||||
values for that option, and is reserved by AP. You can set this as your default value but you cannot define your own
|
||||
new `option_random`.
|
||||
|
||||
### Option Creation
|
||||
As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's
|
||||
create our option class (with a docstring), give it a `display_name`, and add it to a dictionary that keeps track of our
|
||||
options:
|
||||
|
||||
```python
|
||||
# Options.py
|
||||
class StartingSword(Toggle):
|
||||
"""Adds a sword to your starting inventory."""
|
||||
display_name = "Start With Sword"
|
||||
|
||||
|
||||
example_options = {
|
||||
"starting_sword": StartingSword
|
||||
}
|
||||
```
|
||||
|
||||
This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it
|
||||
to our world's `__init__.py`:
|
||||
|
||||
```python
|
||||
from worlds.AutoWorld import World
|
||||
from .Options import options
|
||||
|
||||
|
||||
class ExampleWorld(World):
|
||||
option_definitions = options
|
||||
```
|
||||
|
||||
### Option Checking
|
||||
Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after
|
||||
world instantiation. These are created as attributes on the MultiWorld and can be accessed with
|
||||
`self.multiworld.my_option_name[self.player]`. This is the option class, which supports direct comparison methods to
|
||||
relevant objects (like comparing a Toggle class to a `bool`). If you need to access the option result directly, this is
|
||||
the option class's `value` attribute. For our example above we can do a simple check:
|
||||
```python
|
||||
if self.multiworld.starting_sword[self.player]:
|
||||
do_some_things()
|
||||
```
|
||||
|
||||
or if I need a boolean object, such as in my slot_data I can access it as:
|
||||
```python
|
||||
start_with_sword = bool(self.multiworld.starting_sword[self.player].value)
|
||||
```
|
||||
|
||||
## Generic Option Classes
|
||||
These options are generically available to every game automatically, but can be overridden for slightly different
|
||||
behavior, if desired. See `worlds/soe/Options.py` for an example.
|
||||
|
||||
### Accessibility
|
||||
Sets rules for availability of locations for the player. `Items` is for all items available but not necessarily all
|
||||
locations, such as self-locking keys, but needs to be set by the world for this to be different from locations access.
|
||||
|
||||
### ProgressionBalancing
|
||||
Algorithm for moving progression items into earlier spheres to make the gameplay experience a bit smoother. Can be
|
||||
overridden if you want a different default value.
|
||||
|
||||
### LocalItems
|
||||
Forces the players' items local to their world.
|
||||
|
||||
### NonLocalItems
|
||||
Forces the players' items outside their world.
|
||||
|
||||
### StartInventory
|
||||
Allows the player to define a dictionary of starting items with item name and quantity.
|
||||
|
||||
### StartHints
|
||||
Gives the player starting hints for where the items defined here are.
|
||||
|
||||
### StartLocationHints
|
||||
Gives the player starting hints for the items on locations defined here.
|
||||
|
||||
### ExcludeLocations
|
||||
Marks locations given here as `LocationProgressType.Excluded` so that progression items can't be placed on them.
|
||||
|
||||
### PriorityLocations
|
||||
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them.
|
||||
|
||||
### ItemLinks
|
||||
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
|
||||
two players will combine their items in the link into a single item, which then gets replaced with `World.create_filler()`.
|
||||
|
||||
## Basic Option Classes
|
||||
### Toggle
|
||||
The example above. This simply has 0 and 1 as its available results with 0 (false) being the default value. Cannot be
|
||||
compared to strings but can be directly compared to True and False.
|
||||
|
||||
### DefaultOnToggle
|
||||
Like Toggle, but 1 (true) is the default value.
|
||||
|
||||
### Choice
|
||||
A numeric option allowing you to define different sub options. Values are stored as integers, but you can also do
|
||||
comparison methods with the class and strings, so if you have an `option_early_sword`, this can be compared with:
|
||||
```python
|
||||
if self.multiworld.sword_availability[self.player] == "early_sword":
|
||||
do_early_sword_things()
|
||||
```
|
||||
|
||||
or:
|
||||
```python
|
||||
from .Options import SwordAvailability
|
||||
|
||||
if self.multiworld.sword_availability[self.player] == SwordAvailability.option_early_sword:
|
||||
do_early_sword_things()
|
||||
```
|
||||
|
||||
### Range
|
||||
A numeric option allowing a variety of integers including the endpoints. Has a default `range_start` of 0 and default
|
||||
`range_end` of 1. Allows for negative values as well. This will always be an integer and has no methods for string
|
||||
comparisons.
|
||||
|
||||
### SpecialRange
|
||||
Like range but also allows you to define a dictionary of special names the user can use to equate to a specific value.
|
||||
For example:
|
||||
```python
|
||||
special_range_names: {
|
||||
"normal": 20,
|
||||
"extreme": 99,
|
||||
}
|
||||
```
|
||||
|
||||
will let users use the names "normal" or "extreme" in their options selections, but will still return those as integers
|
||||
to you. Useful if you want special handling regarding those specified values.
|
||||
|
||||
## More Advanced Options
|
||||
### FreeText
|
||||
This is an option that allows the user to enter any possible string value. Can only be compared with strings, and has
|
||||
no validation step, so if this needs to be validated, you can either add a validation step to the option class or
|
||||
within the world.
|
||||
|
||||
### TextChoice
|
||||
Like choice allows you to predetermine options and has all of the same comparison methods and handling. Also accepts any
|
||||
user defined string as a valid option, so will either need to be validated by adding a validation step to the option
|
||||
class or within world, if necessary. Value for this class is `Union[str, int]` so if you need the value at a specified
|
||||
point, `self.multiworld.my_option[self.player].current_key` will always return a string.
|
||||
|
||||
### PlandoBosses
|
||||
An option specifically built for handling boss rando, if your game can use it. Is a subclass of TextChoice so supports
|
||||
everything it does, as well as having multiple validation steps to automatically support boss plando from users. If
|
||||
using this class, you must define `bosses`, a set of valid boss names, and `locations`, a set of valid boss location
|
||||
names, and `def can_place_boss`, which passes a boss and location, allowing you to check if that placement is valid for
|
||||
your game. When this function is called, `bosses`, `locations`, and the passed strings will all be lowercase. There is
|
||||
also a `duplicate_bosses` attribute allowing you to define if a boss can be placed multiple times in your world. False
|
||||
by default, and will reject duplicate boss names from the user. For an example of using this class, refer to
|
||||
`worlds.alttp.options.py`
|
||||
|
||||
### OptionDict
|
||||
This option returns a dictionary. Setting a default here is recommended as it will output the dictionary to the
|
||||
template. If you set a [Schema](https://pypi.org/project/schema/) on the class with `schema = Schema()`, then the
|
||||
options system will automatically validate the user supplied data against the schema to ensure it's in the correct
|
||||
format.
|
||||
|
||||
### ItemDict
|
||||
Like OptionDict, except this will verify that every key in the dictionary is a valid name for an item for your world.
|
||||
|
||||
### OptionList
|
||||
This option defines a List, where the user can add any number of strings to said list, allowing duplicate values. You
|
||||
can define a set of keys in `valid_keys`, and a default list if you want certain options to be available without editing
|
||||
for this. If `valid_keys_casefold` is true, the verification will be case-insensitive; `verify_item_name` will check
|
||||
that each value is a valid item name; and`verify_location_name` will check that each value is a valid location name.
|
||||
|
||||
### OptionSet
|
||||
Like OptionList, but returns a set, preventing duplicates.
|
||||
|
||||
### ItemSet
|
||||
Like OptionSet, but will verify that all the items in the set are a valid name for an item for your world.
|
||||
@@ -48,5 +48,5 @@
|
||||
# TODO
|
||||
#JSON_AS_ASCII: false
|
||||
|
||||
# Patch target. This is the address encoded into the patch that will be used for client auto-connect.
|
||||
#PATCH_TARGET: archipelago.gg
|
||||
# Host Address. This is the address encoded into the patch that will be used for client auto-connect.
|
||||
#HOST_ADDRESS: archipelago.gg
|
||||
|
||||
17
host.yaml
17
host.yaml
@@ -107,7 +107,7 @@ factorio_options:
|
||||
filter_item_sends: false
|
||||
# Whether to send chat messages from players on the Factorio server to Archipelago.
|
||||
bridge_chat_out: true
|
||||
minecraft_options:
|
||||
minecraft_options:
|
||||
forge_directory: "Minecraft Forge server"
|
||||
max_heap_size: "2G"
|
||||
# release channel, currently "release", or "beta"
|
||||
@@ -125,6 +125,15 @@ soe_options:
|
||||
rom_file: "Secret of Evermore (USA).sfc"
|
||||
ffr_options:
|
||||
display_msgs: true
|
||||
tloz_options:
|
||||
# File name of the Zelda 1
|
||||
rom_file: "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
|
||||
dkc3_options:
|
||||
# File name of the DKC3 US rom
|
||||
rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
|
||||
@@ -139,6 +148,12 @@ pokemon_rb_options:
|
||||
# True for operating system default program
|
||||
# Alternatively, a path to a program to open the .gb file with
|
||||
rom_start: true
|
||||
|
||||
wargroove_options:
|
||||
# Locate 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"
|
||||
|
||||
zillion_options:
|
||||
# File name of the Zillion US rom
|
||||
rom_file: "Zillion (UE) [!].sms"
|
||||
|
||||
@@ -25,9 +25,9 @@ OutputBaseFilename=Setup {#MyAppName} {#MyAppVersionText}
|
||||
Compression=lzma2
|
||||
SolidCompression=yes
|
||||
LZMANumBlockThreads=8
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
ArchitecturesInstallIn64BitMode=x64 arm64
|
||||
ChangesAssociations=yes
|
||||
ArchitecturesAllowed=x64
|
||||
ArchitecturesAllowed=x64 arm64
|
||||
AllowNoIcons=yes
|
||||
SetupIconFile={#MyAppIcon}
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
|
||||
86
setup.py
86
setup.py
@@ -7,15 +7,21 @@ import sys
|
||||
import sysconfig
|
||||
import typing
|
||||
import zipfile
|
||||
from collections.abc import Iterable
|
||||
from hashlib import sha3_512
|
||||
from pathlib import Path
|
||||
import urllib.request
|
||||
import io
|
||||
import json
|
||||
import threading
|
||||
import subprocess
|
||||
import pkg_resources
|
||||
|
||||
from collections.abc import Iterable
|
||||
from hashlib import sha3_512
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||
try:
|
||||
requirement = 'cx-Freeze>=6.14.1'
|
||||
requirement = 'cx-Freeze>=6.14.7'
|
||||
pkg_resources.require(requirement)
|
||||
import cx_Freeze
|
||||
except pkg_resources.ResolutionError:
|
||||
@@ -45,9 +51,74 @@ apworlds: set = {
|
||||
"Rogue Legacy",
|
||||
"Donkey Kong Country 3",
|
||||
"Super Mario World",
|
||||
"Stardew Valley",
|
||||
"Timespinner",
|
||||
"Minecraft",
|
||||
"The Messenger",
|
||||
}
|
||||
|
||||
|
||||
def download_SNI():
|
||||
print("Updating SNI")
|
||||
machine_to_go = {
|
||||
"x86_64": "amd64",
|
||||
"aarch64": "arm64",
|
||||
"armv7l": "arm"
|
||||
}
|
||||
platform_name = platform.system().lower()
|
||||
machine_name = platform.machine().lower()
|
||||
# force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH
|
||||
machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name)
|
||||
with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request:
|
||||
data = json.load(request)
|
||||
files = data["assets"]
|
||||
|
||||
source_url = None
|
||||
|
||||
for file in files:
|
||||
download_url: str = file["browser_download_url"]
|
||||
machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name
|
||||
if platform_name in download_url and machine_match:
|
||||
# prefer "many" builds
|
||||
if "many" in download_url:
|
||||
source_url = download_url
|
||||
break
|
||||
source_url = download_url
|
||||
|
||||
if source_url and source_url.endswith(".zip"):
|
||||
with urllib.request.urlopen(source_url) as download:
|
||||
with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf:
|
||||
for member in zf.infolist():
|
||||
zf.extract(member, path="SNI")
|
||||
print(f"Downloaded SNI from {source_url}")
|
||||
|
||||
elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")):
|
||||
import tarfile
|
||||
mode = "r:xz" if source_url.endswith(".tar.xz") else "r:gz"
|
||||
with urllib.request.urlopen(source_url) as download:
|
||||
sni_dir = None
|
||||
with tarfile.open(fileobj=io.BytesIO(download.read()), mode=mode) as tf:
|
||||
for member in tf.getmembers():
|
||||
if member.name.startswith("/") or "../" in member.name:
|
||||
raise ValueError(f"Unexpected file '{member.name}' in {source_url}")
|
||||
elif member.isdir() and not sni_dir:
|
||||
sni_dir = member.name
|
||||
elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir):
|
||||
raise ValueError(f"Expected folder before '{member.name}' in {source_url}")
|
||||
elif member.isfile() and sni_dir:
|
||||
tf.extract(member)
|
||||
# sadly SNI is in its own folder on non-windows, so we need to rename
|
||||
shutil.rmtree("SNI", True)
|
||||
os.rename(sni_dir, "SNI")
|
||||
print(f"Downloaded SNI from {source_url}")
|
||||
|
||||
elif source_url:
|
||||
print(f"Don't know how to extract SNI from {source_url}")
|
||||
|
||||
else:
|
||||
print(f"No SNI found for system spec {platform_name} {machine_name}")
|
||||
|
||||
|
||||
if os.path.exists("X:/pw.txt"):
|
||||
print("Using signtool")
|
||||
with open("X:/pw.txt", encoding="utf-8-sig") as f:
|
||||
@@ -173,6 +244,10 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
print("Created Manifest")
|
||||
|
||||
def run(self):
|
||||
# start downloading sni asap
|
||||
sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader")
|
||||
sni_thread.start()
|
||||
|
||||
# pre build steps
|
||||
print(f"Outputting to: {self.buildfolder}")
|
||||
os.makedirs(self.buildfolder, exist_ok=True)
|
||||
@@ -184,6 +259,9 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
self.buildtime = datetime.datetime.utcnow()
|
||||
super().run()
|
||||
|
||||
# need to finish download before copying
|
||||
sni_thread.join()
|
||||
|
||||
# include_files seems to not be done automatically. implement here
|
||||
for src, dst in self.include_files:
|
||||
print(f"copying {src} -> {self.buildfolder / dst}")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pathlib
|
||||
import typing
|
||||
import unittest
|
||||
import pathlib
|
||||
from argparse import Namespace
|
||||
|
||||
import Utils
|
||||
@@ -112,6 +112,12 @@ class WorldTestBase(unittest.TestCase):
|
||||
self.world_setup()
|
||||
|
||||
def world_setup(self, seed: typing.Optional[int] = None) -> None:
|
||||
if type(self) is WorldTestBase or \
|
||||
(hasattr(WorldTestBase, self._testMethodName)
|
||||
and not self.run_default_tests and
|
||||
getattr(self, self._testMethodName).__code__ is
|
||||
getattr(WorldTestBase, self._testMethodName, None).__code__):
|
||||
return # setUp gets called for tests defined in the base class. We skip world_setup here.
|
||||
if not hasattr(self, "game"):
|
||||
raise NotImplementedError("didn't define game name")
|
||||
self.multiworld = MultiWorld(1)
|
||||
@@ -128,7 +134,9 @@ class WorldTestBase(unittest.TestCase):
|
||||
for step in gen_steps:
|
||||
call_all(self.multiworld, step)
|
||||
|
||||
# methods that can be called within tests
|
||||
def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]) -> None:
|
||||
"""Collects all pre-placed items and items in the multiworld itempool except those provided"""
|
||||
if isinstance(item_names, str):
|
||||
item_names = (item_names,)
|
||||
for item in self.multiworld.get_items():
|
||||
@@ -136,12 +144,14 @@ class WorldTestBase(unittest.TestCase):
|
||||
self.multiworld.state.collect(item)
|
||||
|
||||
def get_item_by_name(self, item_name: str) -> Item:
|
||||
"""Returns the first item found in placed items, or in the itempool with the matching name"""
|
||||
for item in self.multiworld.get_items():
|
||||
if item.name == item_name:
|
||||
return item
|
||||
raise ValueError("No such item")
|
||||
|
||||
def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
||||
"""Returns actual items from the itempool that match the provided name(s)"""
|
||||
if isinstance(item_names, str):
|
||||
item_names = (item_names,)
|
||||
return [item for item in self.multiworld.itempool if item.name in item_names]
|
||||
@@ -153,12 +163,14 @@ class WorldTestBase(unittest.TestCase):
|
||||
return items
|
||||
|
||||
def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
||||
"""Collects the provided item(s) into state"""
|
||||
if isinstance(items, Item):
|
||||
items = (items,)
|
||||
for item in items:
|
||||
self.multiworld.state.collect(item)
|
||||
|
||||
def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
||||
"""Removes the provided item(s) from state"""
|
||||
if isinstance(items, Item):
|
||||
items = (items,)
|
||||
for item in items:
|
||||
@@ -167,17 +179,22 @@ class WorldTestBase(unittest.TestCase):
|
||||
self.multiworld.state.remove(item)
|
||||
|
||||
def can_reach_location(self, location: str) -> bool:
|
||||
"""Determines if the current state can reach the provide location name"""
|
||||
return self.multiworld.state.can_reach(location, "Location", 1)
|
||||
|
||||
def can_reach_entrance(self, entrance: str) -> bool:
|
||||
"""Determines if the current state can reach the provided entrance name"""
|
||||
return self.multiworld.state.can_reach(entrance, "Entrance", 1)
|
||||
|
||||
def count(self, item_name: str) -> int:
|
||||
"""Returns the amount of an item currently in state"""
|
||||
return self.multiworld.state.count(item_name, 1)
|
||||
|
||||
def assertAccessDependency(self,
|
||||
locations: typing.List[str],
|
||||
possible_items: typing.Iterable[typing.Iterable[str]]) -> None:
|
||||
"""Asserts that the provided locations can't be reached without the listed items but can be reached with any
|
||||
one of the provided combinations"""
|
||||
all_items = [item_name for item_names in possible_items for item_name in item_names]
|
||||
|
||||
self.collect_all_but(all_items)
|
||||
@@ -190,4 +207,43 @@ class WorldTestBase(unittest.TestCase):
|
||||
self.remove(items)
|
||||
|
||||
def assertBeatable(self, beatable: bool):
|
||||
"""Asserts that the game can be beaten with the current state"""
|
||||
self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable)
|
||||
|
||||
# following tests are automatically run
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
"""Not possible or identical to the base test that's always being run already"""
|
||||
return (self.options
|
||||
or self.setUp.__code__ is not WorldTestBase.setUp.__code__
|
||||
or self.world_setup.__code__ is not WorldTestBase.world_setup.__code__)
|
||||
|
||||
@property
|
||||
def constructed(self) -> bool:
|
||||
"""A multiworld has been constructed by this point"""
|
||||
return hasattr(self, "game") and hasattr(self, "multiworld")
|
||||
|
||||
def testAllStateCanReachEverything(self):
|
||||
"""Ensure all state can reach everything and complete the game with the defined options"""
|
||||
if not (self.run_default_tests and self.constructed):
|
||||
return
|
||||
with self.subTest("Game", game=self.game):
|
||||
excluded = self.multiworld.exclude_locations[1].value
|
||||
state = self.multiworld.get_all_state(False)
|
||||
for location in self.multiworld.get_locations():
|
||||
if location.name not in excluded:
|
||||
with self.subTest("Location should be reached", location=location):
|
||||
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
||||
with self.subTest("Beatable"):
|
||||
self.multiworld.state = state
|
||||
self.assertBeatable(True)
|
||||
|
||||
def testEmptyStateCanReachSomething(self):
|
||||
"""Ensure empty state can reach at least one location with the defined options"""
|
||||
if not (self.run_default_tests and self.constructed):
|
||||
return
|
||||
with self.subTest("Game", game=self.game):
|
||||
state = CollectionState(self.multiworld)
|
||||
locations = self.multiworld.get_reachable_locations(state, 1)
|
||||
self.assertGreater(len(locations), 0,
|
||||
"Need to be able to reach at least one location to get started.")
|
||||
|
||||
@@ -8,7 +8,7 @@ class TestImplemented(unittest.TestCase):
|
||||
def testCompletionCondition(self):
|
||||
"""Ensure a completion condition is set that has requirements."""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if not world_type.hidden and game_name not in {"ArchipIDLE", "Sudoku"}:
|
||||
if not world_type.hidden and game_name not in {"Sudoku"}:
|
||||
with self.subTest(game_name):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
self.assertFalse(multiworld.completion_condition[1](multiworld.state))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
from collections import Counter
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
|
||||
@@ -23,3 +23,33 @@ class TestBase(unittest.TestCase):
|
||||
for location in locations:
|
||||
self.assertIn(location.name, world_type.location_name_to_id)
|
||||
self.assertEqual(location.address, world_type.location_name_to_id[location.name])
|
||||
|
||||
def testLocationCreationSteps(self):
|
||||
"""Tests that Regions and Locations aren't created after `create_items`."""
|
||||
gen_steps = ("generate_early", "create_regions", "create_items")
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game_name=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type, gen_steps)
|
||||
multiworld._recache()
|
||||
region_count = len(multiworld.get_regions())
|
||||
location_count = len(multiworld.get_locations())
|
||||
|
||||
call_all(multiworld, "set_rules")
|
||||
self.assertEqual(region_count, len(multiworld.get_regions()),
|
||||
f"{game_name} modified region count during rule creation")
|
||||
self.assertEqual(location_count, len(multiworld.get_locations()),
|
||||
f"{game_name} modified locations count during rule creation")
|
||||
|
||||
multiworld._recache()
|
||||
call_all(multiworld, "generate_basic")
|
||||
self.assertEqual(region_count, len(multiworld.get_regions()),
|
||||
f"{game_name} modified region count during generate_basic")
|
||||
self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
|
||||
f"{game_name} modified locations count during generate_basic")
|
||||
|
||||
multiworld._recache()
|
||||
call_all(multiworld, "pre_fill")
|
||||
self.assertEqual(region_count, len(multiworld.get_regions()),
|
||||
f"{game_name} modified region count during pre_fill")
|
||||
self.assertGreaterEqual(location_count, len(multiworld.get_locations()),
|
||||
f"{game_name} modified locations count during pre_fill")
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from argparse import Namespace
|
||||
from typing import Type, Tuple
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from worlds.AutoWorld import call_all
|
||||
from worlds.AutoWorld import call_all, World
|
||||
|
||||
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
|
||||
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
|
||||
|
||||
|
||||
def setup_solo_multiworld(world_type) -> MultiWorld:
|
||||
def setup_solo_multiworld(world_type: Type[World], steps: Tuple[str, ...] = gen_steps) -> MultiWorld:
|
||||
multiworld = MultiWorld(1)
|
||||
multiworld.game[1] = world_type.game
|
||||
multiworld.player_name = {1: "Tester"}
|
||||
@@ -16,6 +17,6 @@ def setup_solo_multiworld(world_type) -> MultiWorld:
|
||||
setattr(args, name, {1: option.from_any(option.default)})
|
||||
multiworld.set_options(args)
|
||||
multiworld.set_default_common_options()
|
||||
for step in gen_steps:
|
||||
for step in steps:
|
||||
call_all(multiworld, step)
|
||||
return multiworld
|
||||
|
||||
@@ -131,54 +131,72 @@ class World(metaclass=AutoWorldRegister):
|
||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||
A Game should have its own subclass of World in which it defines the required data structures."""
|
||||
|
||||
option_definitions: ClassVar[Dict[str, AssembleOptions]] = {} # link your Options mapping
|
||||
game: ClassVar[str] # name the game
|
||||
topology_present: ClassVar[bool] = False # indicate if world type has any meaningful layout/pathing
|
||||
option_definitions: ClassVar[Dict[str, AssembleOptions]] = {}
|
||||
"""link your Options mapping"""
|
||||
game: ClassVar[str]
|
||||
"""name the game"""
|
||||
topology_present: ClassVar[bool] = False
|
||||
"""indicate if world type has any meaningful layout/pathing"""
|
||||
|
||||
# gets automatically populated with all item and item group names
|
||||
all_item_and_group_names: ClassVar[FrozenSet[str]] = frozenset()
|
||||
"""gets automatically populated with all item and item group names"""
|
||||
|
||||
# map names to their IDs
|
||||
item_name_to_id: ClassVar[Dict[str, int]] = {}
|
||||
"""map item names to their IDs"""
|
||||
location_name_to_id: ClassVar[Dict[str, int]] = {}
|
||||
"""map location names to their IDs"""
|
||||
|
||||
# maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"}
|
||||
item_name_groups: ClassVar[Dict[str, Set[str]]] = {}
|
||||
"""maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}"""
|
||||
|
||||
location_name_groups: ClassVar[Dict[str, Set[str]]] = {}
|
||||
"""maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}"""
|
||||
|
||||
# increment this every time something in your world's names/id mappings changes.
|
||||
# While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
|
||||
# retrieved by clients on every connection.
|
||||
data_version: ClassVar[int] = 1
|
||||
"""
|
||||
increment this every time something in your world's names/id mappings changes.
|
||||
While this is set to 0, this world's DataPackage is considered in testing mode and will be inserted to the multidata
|
||||
and retrieved by clients on every connection.
|
||||
"""
|
||||
|
||||
# override this if changes to a world break forward-compatibility of the client
|
||||
# The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the
|
||||
# future. Protocol level compatibility check moved to MultiServer.min_client_version.
|
||||
required_client_version: Tuple[int, int, int] = (0, 1, 6)
|
||||
"""
|
||||
override this if changes to a world break forward-compatibility of the client
|
||||
The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the
|
||||
future. Protocol level compatibility check moved to MultiServer.min_client_version.
|
||||
"""
|
||||
|
||||
# update this if the resulting multidata breaks forward-compatibility of the server
|
||||
required_server_version: Tuple[int, int, int] = (0, 2, 4)
|
||||
"""update this if the resulting multidata breaks forward-compatibility of the server"""
|
||||
|
||||
hint_blacklist: ClassVar[FrozenSet[str]] = frozenset() # any names that should not be hintable
|
||||
hint_blacklist: ClassVar[FrozenSet[str]] = frozenset()
|
||||
"""any names that should not be hintable"""
|
||||
|
||||
# Hide World Type from various views. Does not remove functionality.
|
||||
hidden: ClassVar[bool] = False
|
||||
"""Hide World Type from various views. Does not remove functionality."""
|
||||
|
||||
# see WebWorld for options
|
||||
web: ClassVar[WebWorld] = WebWorld()
|
||||
"""see WebWorld for options"""
|
||||
|
||||
# autoset on creation:
|
||||
multiworld: "MultiWorld"
|
||||
"""autoset on creation. The MultiWorld object for the currently generating multiworld."""
|
||||
player: int
|
||||
"""autoset on creation. The player number for this World"""
|
||||
|
||||
# automatically generated
|
||||
item_id_to_name: ClassVar[Dict[int, str]]
|
||||
"""automatically generated reverse lookup of item id to name"""
|
||||
location_id_to_name: ClassVar[Dict[int, str]]
|
||||
"""automatically generated reverse lookup of location id to name"""
|
||||
|
||||
item_names: ClassVar[Set[str]] # set of all potential item names
|
||||
location_names: ClassVar[Set[str]] # set of all potential location names
|
||||
item_names: ClassVar[Set[str]]
|
||||
"""set of all potential item names"""
|
||||
location_names: ClassVar[Set[str]]
|
||||
"""set of all potential location names"""
|
||||
|
||||
zip_path: ClassVar[Optional[pathlib.Path]] = None # If loaded from a .apworld, this is the Path to it.
|
||||
__file__: ClassVar[str] # path it was loaded from
|
||||
zip_path: ClassVar[Optional[pathlib.Path]] = None
|
||||
"""If loaded from a .apworld, this is the Path to it."""
|
||||
__file__: ClassVar[str]
|
||||
"""path it was loaded from"""
|
||||
|
||||
def __init__(self, multiworld: "MultiWorld", player: int):
|
||||
self.multiworld = multiworld
|
||||
@@ -196,18 +214,32 @@ class World(metaclass=AutoWorldRegister):
|
||||
pass
|
||||
|
||||
def generate_early(self) -> None:
|
||||
"""
|
||||
Run before any general steps of the MultiWorld other than options. Useful for getting and adjusting option
|
||||
results and determining layouts for entrance rando etc. start inventory gets pushed after this step.
|
||||
"""
|
||||
pass
|
||||
|
||||
def create_regions(self) -> None:
|
||||
"""Method for creating and connecting regions for the World."""
|
||||
pass
|
||||
|
||||
def create_items(self) -> None:
|
||||
"""
|
||||
Method for creating and submitting items to the itempool. Items and Regions should *not* be created and submitted
|
||||
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`.
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_rules(self) -> None:
|
||||
"""Method for setting the rules on the World's regions and locations."""
|
||||
pass
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
"""
|
||||
Useful for randomizing things that don't affect logic but are better to be determined before the output stage.
|
||||
i.e. checking what the player has marked as priority or randomizing enemies
|
||||
"""
|
||||
pass
|
||||
|
||||
def pre_fill(self) -> None:
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Optional, Union, List, Tuple, Callable, Dict
|
||||
from BaseClasses import Boss
|
||||
from Fill import FillError
|
||||
from .Options import LTTPBosses as Bosses
|
||||
|
||||
from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, has_melee_weapon, has_fire_source
|
||||
|
||||
def BossFactory(boss: str, player: int) -> Optional[Boss]:
|
||||
if boss in boss_table:
|
||||
@@ -16,33 +16,33 @@ def BossFactory(boss: str, player: int) -> Optional[Boss]:
|
||||
def ArmosKnightsDefeatRule(state, player: int) -> bool:
|
||||
# Magic amounts are probably a bit overkill
|
||||
return (
|
||||
state.has_melee_weapon(player) or
|
||||
state.can_shoot_arrows(player) or
|
||||
(state.has('Cane of Somaria', player) and state.can_extend_magic(player, 10)) or
|
||||
(state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or
|
||||
(state.has('Ice Rod', player) and state.can_extend_magic(player, 32)) or
|
||||
(state.has('Fire Rod', player) and state.can_extend_magic(player, 32)) or
|
||||
has_melee_weapon(state, player) or
|
||||
can_shoot_arrows(state, player) or
|
||||
(state.has('Cane of Somaria', player) and can_extend_magic(state, player, 10)) or
|
||||
(state.has('Cane of Byrna', player) and can_extend_magic(state, player, 16)) or
|
||||
(state.has('Ice Rod', player) and can_extend_magic(state, player, 32)) or
|
||||
(state.has('Fire Rod', player) and can_extend_magic(state, player, 32)) or
|
||||
state.has('Blue Boomerang', player) or
|
||||
state.has('Red Boomerang', player))
|
||||
|
||||
|
||||
def LanmolasDefeatRule(state, player: int) -> bool:
|
||||
return (
|
||||
state.has_melee_weapon(player) or
|
||||
has_melee_weapon(state, player) or
|
||||
state.has('Fire Rod', player) or
|
||||
state.has('Ice Rod', player) or
|
||||
state.has('Cane of Somaria', player) or
|
||||
state.has('Cane of Byrna', player) or
|
||||
state.can_shoot_arrows(player))
|
||||
can_shoot_arrows(state, player))
|
||||
|
||||
|
||||
def MoldormDefeatRule(state, player: int) -> bool:
|
||||
return state.has_melee_weapon(player)
|
||||
return has_melee_weapon(state, player)
|
||||
|
||||
|
||||
def HelmasaurKingDefeatRule(state, player: int) -> bool:
|
||||
# TODO: technically possible with the hammer
|
||||
return state.has_sword(player) or state.can_shoot_arrows(player)
|
||||
return has_sword(state, player) or can_shoot_arrows(state, player)
|
||||
|
||||
|
||||
def ArrghusDefeatRule(state, player: int) -> bool:
|
||||
@@ -51,28 +51,28 @@ def ArrghusDefeatRule(state, player: int) -> bool:
|
||||
# TODO: ideally we would have a check for bow and silvers, which combined with the
|
||||
# hookshot is enough. This is not coded yet because the silvers that only work in pyramid feature
|
||||
# makes this complicated
|
||||
if state.has_melee_weapon(player):
|
||||
if has_melee_weapon(state, player):
|
||||
return True
|
||||
|
||||
return ((state.has('Fire Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player,
|
||||
return ((state.has('Fire Rod', player) and (can_shoot_arrows(state, player) or can_extend_magic(state, player,
|
||||
12))) or # assuming mostly gitting two puff with one shot
|
||||
(state.has('Ice Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16))))
|
||||
(state.has('Ice Rod', player) and (can_shoot_arrows(state, player) or can_extend_magic(state, player, 16))))
|
||||
|
||||
|
||||
def MothulaDefeatRule(state, player: int) -> bool:
|
||||
return (
|
||||
state.has_melee_weapon(player) or
|
||||
(state.has('Fire Rod', player) and state.can_extend_magic(player, 10)) or
|
||||
has_melee_weapon(state, player) or
|
||||
(state.has('Fire Rod', player) and can_extend_magic(state, player, 10)) or
|
||||
# TODO: Not sure how much (if any) extend magic is needed for these two, since they only apply
|
||||
# to non-vanilla locations, so are harder to test, so sticking with what VT has for now:
|
||||
(state.has('Cane of Somaria', player) and state.can_extend_magic(player, 16)) or
|
||||
(state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) or
|
||||
state.can_get_good_bee(player)
|
||||
(state.has('Cane of Somaria', player) and can_extend_magic(state, player, 16)) or
|
||||
(state.has('Cane of Byrna', player) and can_extend_magic(state, player, 16)) or
|
||||
can_get_good_bee(state, player)
|
||||
)
|
||||
|
||||
|
||||
def BlindDefeatRule(state, player: int) -> bool:
|
||||
return state.has_melee_weapon(player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player)
|
||||
return has_melee_weapon(state, player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player)
|
||||
|
||||
|
||||
def KholdstareDefeatRule(state, player: int) -> bool:
|
||||
@@ -81,56 +81,56 @@ def KholdstareDefeatRule(state, player: int) -> bool:
|
||||
state.has('Fire Rod', player) or
|
||||
(
|
||||
state.has('Bombos', player) and
|
||||
(state.has_sword(player) or state.multiworld.swordless[player])
|
||||
(has_sword(state, player) or state.multiworld.swordless[player])
|
||||
)
|
||||
) and
|
||||
(
|
||||
state.has_melee_weapon(player) or
|
||||
(state.has('Fire Rod', player) and state.can_extend_magic(player, 20)) or
|
||||
has_melee_weapon(state, player) or
|
||||
(state.has('Fire Rod', player) and can_extend_magic(state, player, 20)) or
|
||||
(
|
||||
state.has('Fire Rod', player) and
|
||||
state.has('Bombos', player) and
|
||||
state.multiworld.swordless[player] and
|
||||
state.can_extend_magic(player, 16)
|
||||
can_extend_magic(state, player, 16)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def VitreousDefeatRule(state, player: int) -> bool:
|
||||
return state.can_shoot_arrows(player) or state.has_melee_weapon(player)
|
||||
return can_shoot_arrows(state, player) or has_melee_weapon(state, player)
|
||||
|
||||
|
||||
def TrinexxDefeatRule(state, player: int) -> bool:
|
||||
if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)):
|
||||
return False
|
||||
return state.has('Hammer', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', player) or \
|
||||
(state.has('Master Sword', player) and state.can_extend_magic(player, 16)) or \
|
||||
(state.has_sword(player) and state.can_extend_magic(player, 32))
|
||||
(state.has('Master Sword', player) and can_extend_magic(state, player, 16)) or \
|
||||
(has_sword(state, player) and can_extend_magic(state, player, 32))
|
||||
|
||||
|
||||
def AgahnimDefeatRule(state, player: int) -> bool:
|
||||
return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player)
|
||||
return has_sword(state, player) or state.has('Hammer', player) or state.has('Bug Catching Net', player)
|
||||
|
||||
|
||||
def GanonDefeatRule(state, player: int) -> bool:
|
||||
if state.multiworld.swordless[player]:
|
||||
return state.has('Hammer', player) and \
|
||||
state.has_fire_source(player) and \
|
||||
has_fire_source(state, player) and \
|
||||
state.has('Silver Bow', player) and \
|
||||
state.can_shoot_arrows(player)
|
||||
can_shoot_arrows(state, player)
|
||||
|
||||
can_hurt = state.has_beam_sword(player)
|
||||
common = can_hurt and state.has_fire_source(player)
|
||||
can_hurt = has_beam_sword(state, player)
|
||||
common = can_hurt and has_fire_source(state, player)
|
||||
# silverless ganon may be needed in anything higher than no glitches
|
||||
if state.multiworld.logic[player] != 'noglitches':
|
||||
# need to light torch a sufficient amount of times
|
||||
return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (
|
||||
state.has('Silver Bow', player) and state.can_shoot_arrows(player)) or
|
||||
state.has('Lamp', player) or state.can_extend_magic(player, 12))
|
||||
state.has('Silver Bow', player) and can_shoot_arrows(state, player)) or
|
||||
state.has('Lamp', player) or can_extend_magic(state, player, 12))
|
||||
|
||||
else:
|
||||
return common and state.has('Silver Bow', player) and state.can_shoot_arrows(player)
|
||||
return common and state.has('Silver Bow', player) and can_shoot_arrows(state, player)
|
||||
|
||||
|
||||
boss_table: Dict[str, Tuple[str, Optional[Callable]]] = {
|
||||
|
||||
@@ -9,7 +9,8 @@ from worlds.alttp.Dungeons import get_dungeon_item_pool_player
|
||||
from worlds.alttp.EntranceShuffle import connect_entrance
|
||||
from Fill import FillError
|
||||
from worlds.alttp.Items import ItemFactory, GetBeemizerItem
|
||||
from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle
|
||||
from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses
|
||||
from .StateHelpers import has_triforce_pieces, has_melee_weapon
|
||||
|
||||
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
|
||||
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
|
||||
@@ -249,8 +250,10 @@ def generate_itempool(world):
|
||||
world.push_item(loc, ItemFactory('Triforce Piece', player), False)
|
||||
world.treasure_hunt_count[player] = 1
|
||||
if world.boss_shuffle[player] != 'none':
|
||||
if 'turtle rock-' not in world.boss_shuffle[player]:
|
||||
world.boss_shuffle[player] = f'Turtle Rock-Trinexx;{world.boss_shuffle[player]}'
|
||||
if isinstance(world.boss_shuffle[player].value, str) and 'turtle rock-' not in world.boss_shuffle[player].value:
|
||||
world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}')
|
||||
elif isinstance(world.boss_shuffle[player].value, int):
|
||||
world.boss_shuffle[player] = LTTPBosses.from_text(f'Turtle Rock-Trinexx;{world.boss_shuffle[player].current_key}')
|
||||
else:
|
||||
logging.warning(f'Cannot guarantee that Trinexx is the boss of Turtle Rock for player {player}')
|
||||
loc.event = True
|
||||
@@ -286,7 +289,7 @@ def generate_itempool(world):
|
||||
region = world.get_region('Light World', player)
|
||||
|
||||
loc = ALttPLocation(player, "Murahdahla", parent=region)
|
||||
loc.access_rule = lambda state: state.has_triforce_pieces(state.multiworld.treasure_hunt_count[player], player)
|
||||
loc.access_rule = lambda state: has_triforce_pieces(state, player)
|
||||
|
||||
region.locations.append(loc)
|
||||
world.clear_location_cache()
|
||||
@@ -327,7 +330,7 @@ def generate_itempool(world):
|
||||
for item in precollected_items:
|
||||
world.push_precollected(ItemFactory(item, player))
|
||||
|
||||
if world.mode[player] == 'standard' and not world.state.has_melee_weapon(player):
|
||||
if world.mode[player] == 'standard' and not has_melee_weapon(world.state, player):
|
||||
if "Link's Uncle" not in placed_items:
|
||||
found_sword = False
|
||||
found_bow = False
|
||||
|
||||
@@ -107,10 +107,14 @@ class Crystals(Range):
|
||||
|
||||
|
||||
class CrystalsTower(Crystals):
|
||||
"""Number of crystals needed to open Ganon's Tower"""
|
||||
display_name = "Crystals for GT"
|
||||
default = 7
|
||||
|
||||
|
||||
class CrystalsGanon(Crystals):
|
||||
"""Number of crystals needed to damage Ganon"""
|
||||
display_name = "Crystals for Ganon"
|
||||
default = 7
|
||||
|
||||
|
||||
@@ -121,12 +125,15 @@ class TriforcePieces(Range):
|
||||
|
||||
|
||||
class ShopItemSlots(Range):
|
||||
"""Number of slots in all shops available to have items from the multiworld"""
|
||||
display_name = "Available Shop Slots"
|
||||
range_start = 0
|
||||
range_end = 30
|
||||
|
||||
|
||||
class ShopPriceModifier(Range):
|
||||
"""Percentage modifier for shuffled item prices in shops"""
|
||||
display_name = "Shop Price Cost Percent"
|
||||
range_start = 0
|
||||
default = 100
|
||||
range_end = 400
|
||||
@@ -144,7 +151,7 @@ class LTTPBosses(PlandoBosses):
|
||||
Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur.
|
||||
Chaos allows any boss to appear any number of times.
|
||||
Singularity places a single boss in as many places as possible, and a second boss in any remaining locations.
|
||||
Supports plando placement. Formatting here: https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/plando/en"""
|
||||
Supports plando placement."""
|
||||
display_name = "Boss Shuffle"
|
||||
option_none = 0
|
||||
option_basic = 1
|
||||
@@ -202,6 +209,7 @@ class Enemies(Choice):
|
||||
|
||||
|
||||
class Progressive(Choice):
|
||||
"""How item types that have multiple tiers (armor, bows, gloves, shields, and swords) should be rewarded"""
|
||||
display_name = "Progressive Items"
|
||||
option_off = 0
|
||||
option_grouped_random = 1
|
||||
@@ -305,22 +313,27 @@ class Palette(Choice):
|
||||
|
||||
|
||||
class OWPalette(Palette):
|
||||
"""The type of palette shuffle to use for the overworld"""
|
||||
display_name = "Overworld Palette"
|
||||
|
||||
|
||||
class UWPalette(Palette):
|
||||
"""The type of palette shuffle to use for the underworld (caves, dungeons, etc.)"""
|
||||
display_name = "Underworld Palette"
|
||||
|
||||
|
||||
class HUDPalette(Palette):
|
||||
"""The type of palette shuffle to use for the HUD"""
|
||||
display_name = "Menu Palette"
|
||||
|
||||
|
||||
class SwordPalette(Palette):
|
||||
"""The type of palette shuffle to use for the sword"""
|
||||
display_name = "Sword Palette"
|
||||
|
||||
|
||||
class ShieldPalette(Palette):
|
||||
"""The type of palette shuffle to use for the shield"""
|
||||
display_name = "Shield Palette"
|
||||
|
||||
|
||||
@@ -329,6 +342,7 @@ class ShieldPalette(Palette):
|
||||
|
||||
|
||||
class HeartBeep(Choice):
|
||||
"""How quickly the heart beep sound effect will play"""
|
||||
display_name = "Heart Beep Rate"
|
||||
option_normal = 0
|
||||
option_double = 1
|
||||
@@ -338,6 +352,7 @@ class HeartBeep(Choice):
|
||||
|
||||
|
||||
class HeartColor(Choice):
|
||||
"""The color of hearts in the HUD"""
|
||||
display_name = "Heart Color"
|
||||
option_red = 0
|
||||
option_blue = 1
|
||||
@@ -346,10 +361,12 @@ class HeartColor(Choice):
|
||||
|
||||
|
||||
class QuickSwap(DefaultOnToggle):
|
||||
"""Allows you to quickly swap items while playing with L/R"""
|
||||
display_name = "L/R Quickswapping"
|
||||
|
||||
|
||||
class MenuSpeed(Choice):
|
||||
"""How quickly the menu appears/disappears"""
|
||||
display_name = "Menu Speed"
|
||||
option_normal = 0
|
||||
option_instant = 1,
|
||||
@@ -360,14 +377,17 @@ class MenuSpeed(Choice):
|
||||
|
||||
|
||||
class Music(DefaultOnToggle):
|
||||
"""Whether background music will play in game"""
|
||||
display_name = "Play music"
|
||||
|
||||
|
||||
class ReduceFlashing(DefaultOnToggle):
|
||||
"""Reduces flashing for certain scenes such as the Misery Mire and Ganon's Tower opening cutscenes"""
|
||||
display_name = "Reduce Screen Flashes"
|
||||
|
||||
|
||||
class TriforceHud(Choice):
|
||||
"""When and how the triforce hunt HUD should display"""
|
||||
display_name = "Display Method for Triforce Hunt"
|
||||
option_normal = 0
|
||||
option_hide_goal = 1
|
||||
@@ -375,6 +395,11 @@ class TriforceHud(Choice):
|
||||
option_hide_both = 3
|
||||
|
||||
|
||||
class GlitchBoots(DefaultOnToggle):
|
||||
"""If this is enabled, the player will start with Pegasus Boots when playing with overworld glitches or harder logic."""
|
||||
display_name = "Glitched Starting Boots"
|
||||
|
||||
|
||||
class BeemizerRange(Range):
|
||||
value: int
|
||||
range_start = 0
|
||||
@@ -437,7 +462,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"music": Music,
|
||||
"reduceflashing": ReduceFlashing,
|
||||
"triforcehud": TriforceHud,
|
||||
"glitch_boots": DefaultOnToggle,
|
||||
"glitch_boots": GlitchBoots,
|
||||
"beemizer_total_chance": BeemizerTotalChance,
|
||||
"beemizer_trap_chance": BeemizerTrapChance,
|
||||
"death_link": DeathLink,
|
||||
|
||||
@@ -4,6 +4,7 @@ Helper functions to deliver entrance/exit/region sets to OWG rules.
|
||||
|
||||
from BaseClasses import Entrance
|
||||
|
||||
from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw
|
||||
|
||||
def get_sword_required_superbunny_mirror_regions():
|
||||
"""
|
||||
@@ -169,7 +170,7 @@ def get_boots_clip_exits_dw(inverted, player):
|
||||
yield ('Ganons Tower Ascent', 'Dark Death Mountain (West Bottom)', 'Dark Death Mountain (Top)') # This only gets you to the GT entrance
|
||||
yield ('Dark Death Mountain Glitched Bridge', 'Dark Death Mountain (West Bottom)', 'Dark Death Mountain (Top)')
|
||||
yield ('Turtle Rock (Top) Clip Spot', 'Dark Death Mountain (Top)', 'Turtle Rock (Top)')
|
||||
yield ('Ice Palace Clip', 'South Dark World', 'Dark Lake Hylia Central Island', lambda state: state.can_boots_clip_dw(player) and state.has('Flippers', player))
|
||||
yield ('Ice Palace Clip', 'South Dark World', 'Dark Lake Hylia Central Island', lambda state: can_boots_clip_dw(state, player) and state.has('Flippers', player))
|
||||
else:
|
||||
yield ('Dark Desert Teleporter Clip Spot', 'Dark Desert', 'Dark Desert Ledge')
|
||||
|
||||
@@ -203,7 +204,7 @@ def get_mirror_offset_spots_lw(player):
|
||||
Mirror shenanigans placing a mirror portal with a broken camera
|
||||
"""
|
||||
yield ('Death Mountain Offset Mirror', 'Death Mountain', 'Light World')
|
||||
yield ('Death Mountain Offset Mirror (Houlihan Exit)', 'Death Mountain', 'Hyrule Castle Ledge', lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player) and state.has('Moon Pearl', player))
|
||||
yield ('Death Mountain Offset Mirror (Houlihan Exit)', 'Death Mountain', 'Hyrule Castle Ledge', lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player) and state.has('Moon Pearl', player))
|
||||
|
||||
|
||||
|
||||
@@ -255,11 +256,11 @@ def overworld_glitch_connections(world, player):
|
||||
def overworld_glitches_rules(world, player):
|
||||
|
||||
# Boots-accessible locations.
|
||||
set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: state.can_boots_clip_lw(player))
|
||||
set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: state.can_boots_clip_dw(player))
|
||||
set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: can_boots_clip_lw(state, player))
|
||||
set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
|
||||
|
||||
# Glitched speed drops.
|
||||
set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: state.can_get_glitched_speed_dw(player))
|
||||
set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
|
||||
# Dark Death Mountain Ledge Clip Spot also accessible with mirror.
|
||||
if world.mode[player] != 'inverted':
|
||||
add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
@@ -267,20 +268,20 @@ def overworld_glitches_rules(world, player):
|
||||
# Mirror clip spots.
|
||||
if world.mode[player] != 'inverted':
|
||||
set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
|
||||
set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_lw(player))
|
||||
set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player))
|
||||
else:
|
||||
set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player))
|
||||
set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player))
|
||||
|
||||
# Regions that require the boots and some other stuff.
|
||||
if world.mode[player] != 'inverted':
|
||||
world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (state.can_boots_clip_lw(player) or state.can_lift_heavy_rocks(player)) and state.has('Hammer', player)
|
||||
world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player)
|
||||
add_alternate_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player))
|
||||
else:
|
||||
add_alternate_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Moon Pearl', player))
|
||||
|
||||
world.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and state.can_lift_heavy_rocks(player)
|
||||
add_alternate_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: state.can_boots_clip_dw(player))
|
||||
add_alternate_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.can_boots_clip_dw(player))
|
||||
world.get_entrance('Dark Desert Teleporter', player).access_rule = lambda state: (state.has('Flute', player) or state.has('Pegasus Boots', player)) and can_lift_heavy_rocks(state, player)
|
||||
add_alternate_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_boots_clip_dw(state, player))
|
||||
add_alternate_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: can_boots_clip_dw(state, player))
|
||||
|
||||
# Zora's Ledge via waterwalk setup.
|
||||
add_alternate_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Pegasus Boots', player))
|
||||
|
||||
@@ -2,17 +2,24 @@ import collections
|
||||
import logging
|
||||
from typing import Iterator, Set
|
||||
|
||||
from worlds.alttp import OverworldGlitchRules
|
||||
from BaseClasses import MultiWorld, Entrance
|
||||
from worlds.alttp.Items import ItemFactory, progression_items, item_name_groups, item_table
|
||||
from worlds.alttp.OverworldGlitchRules import overworld_glitches_rules, no_logic_rules
|
||||
from worlds.alttp.Regions import location_table
|
||||
from worlds.alttp.UnderworldGlitchRules import underworld_glitches_rules
|
||||
from worlds.alttp.Bosses import GanonDefeatRule
|
||||
from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \
|
||||
item_name
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
from worlds.alttp.Regions import LTTPRegionType
|
||||
from BaseClasses import Entrance, MultiWorld
|
||||
from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
|
||||
item_in_locations, location_item_name, set_rule, allow_self_locking_items)
|
||||
|
||||
from . import OverworldGlitchRules
|
||||
from .Bosses import GanonDefeatRule
|
||||
from .Items import ItemFactory, item_name_groups, item_table, progression_items
|
||||
from .Options import smallkey_shuffle
|
||||
from .OverworldGlitchRules import no_logic_rules, overworld_glitches_rules
|
||||
from .Regions import LTTPRegionType, location_table
|
||||
from .StateHelpers import (can_extend_magic, can_kill_most_things,
|
||||
can_lift_heavy_rocks, can_lift_rocks,
|
||||
can_melt_things, can_retrieve_tablet,
|
||||
can_shoot_arrows, has_beam_sword, has_crystals,
|
||||
has_fire_source, has_hearts,
|
||||
has_misery_mire_medallion, has_sword, has_turtle_rock_medallion,
|
||||
has_triforce_pieces)
|
||||
from .UnderworldGlitchRules import underworld_glitches_rules
|
||||
|
||||
|
||||
def set_rules(world):
|
||||
@@ -76,7 +83,7 @@ def set_rules(world):
|
||||
|
||||
if world.goal[player] == 'bosses':
|
||||
# require all bosses to beat ganon
|
||||
add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and state.has_crystals(7, player))
|
||||
add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and has_crystals(state, 7, player))
|
||||
elif world.goal[player] == 'ganon':
|
||||
# require aga2 to beat ganon
|
||||
add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
|
||||
@@ -101,7 +108,7 @@ def set_rules(world):
|
||||
|
||||
set_trock_key_rules(world, player)
|
||||
|
||||
set_rule(ganons_tower, lambda state: state.has_crystals(state.multiworld.crystals_needed_for_gt[player], player))
|
||||
set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_gt[player], player))
|
||||
if world.mode[player] != 'inverted' and world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']:
|
||||
add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
|
||||
|
||||
@@ -199,7 +206,7 @@ def global_rules(world, player):
|
||||
set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player))
|
||||
set_rule(world.get_location('Purple Chest', player),
|
||||
lambda state: state.has('Pick Up Purple Chest', player)) # Can S&Q with chest
|
||||
set_rule(world.get_location('Ether Tablet', player), lambda state: state.can_retrieve_tablet(player))
|
||||
set_rule(world.get_location('Ether Tablet', player), lambda state: can_retrieve_tablet(state, player))
|
||||
set_rule(world.get_location('Master Sword Pedestal', player), lambda state: state.has('Red Pendant', player) and state.has('Blue Pendant', player) and state.has('Green Pendant', player))
|
||||
|
||||
set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith
|
||||
@@ -212,11 +219,11 @@ def global_rules(world, player):
|
||||
|
||||
|
||||
set_rule(world.get_location('Spike Cave', player), lambda state:
|
||||
state.has('Hammer', player) and state.can_lift_rocks(player) and
|
||||
((state.has('Cape', player) and state.can_extend_magic(player, 16, True)) or
|
||||
state.has('Hammer', player) and can_lift_rocks(state, player) and
|
||||
((state.has('Cape', player) and can_extend_magic(state, player, 16, True)) or
|
||||
(state.has('Cane of Byrna', player) and
|
||||
(state.can_extend_magic(player, 12, True) or
|
||||
(state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or state.has_hearts(player, 4))))))
|
||||
(can_extend_magic(state, player, 12, True) or
|
||||
(state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or has_hearts(state, player, 4))))))
|
||||
)
|
||||
|
||||
set_rule(world.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player))
|
||||
@@ -232,11 +239,11 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('Sewers Back Door', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
|
||||
set_rule(world.get_entrance('Agahnim 1', player),
|
||||
lambda state: state.has_sword(player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
lambda state: has_sword(state, player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
|
||||
set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: state.can_kill_most_things(player, 8))
|
||||
set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: can_kill_most_things(state, player, 8))
|
||||
set_rule(world.get_location('Castle Tower - Dark Maze', player),
|
||||
lambda state: state.can_kill_most_things(player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)',
|
||||
lambda state: can_kill_most_things(state, player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)',
|
||||
player))
|
||||
|
||||
set_rule(world.get_location('Eastern Palace - Big Chest', player),
|
||||
@@ -248,62 +255,62 @@ def global_rules(world, player):
|
||||
set_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and
|
||||
ep_prize.parent_region.dungeon.boss.can_defeat(state))
|
||||
if not world.enemy_shuffle[player]:
|
||||
add_rule(ep_boss, lambda state: state.can_shoot_arrows(player))
|
||||
add_rule(ep_prize, lambda state: state.can_shoot_arrows(player))
|
||||
add_rule(ep_boss, lambda state: can_shoot_arrows(state, player))
|
||||
add_rule(ep_prize, lambda state: can_shoot_arrows(state, player))
|
||||
|
||||
set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player))
|
||||
set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player))
|
||||
set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
|
||||
# logic patch to prevent placing a crystal in Desert that's required to reach the required keys
|
||||
if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]):
|
||||
add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state))
|
||||
|
||||
set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player))
|
||||
set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or location_item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player))
|
||||
set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||
set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||
set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player))
|
||||
set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
|
||||
|
||||
set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
|
||||
set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player))
|
||||
set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player) or item_name(state, 'Swamp Palace - Big Chest', player) == ('Big Key (Swamp Palace)', player))
|
||||
set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Swamp Palace - Big Chest', player), lambda state, item: item.name == 'Big Key (Swamp Palace)' and item.player == player)
|
||||
allow_self_locking_items(world.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
|
||||
set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player))
|
||||
if not world.smallkey_shuffle[player] and world.logic[player] not in ['hybridglitches', 'nologic']:
|
||||
forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
|
||||
|
||||
set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
|
||||
set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player) or item_name(state, 'Thieves\' Town - Big Chest', player) == ('Small Key (Thieves Town)', player)) and state.has('Hammer', player))
|
||||
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player)) and state.has('Hammer', player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player and state.has('Hammer', player))
|
||||
allow_self_locking_items(world.get_location('Thieves\' Town - Big Chest', player), 'Small Key (Thieves Town)')
|
||||
set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||
|
||||
set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
|
||||
set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
|
||||
set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section
|
||||
set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2))
|
||||
set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) or item_name(state, 'Skull Woods - Big Chest', player) == ('Big Key (Skull Woods)', player))
|
||||
set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Skull Woods - Big Chest', player), lambda state, item: item.name == 'Big Key (Skull Woods)' and item.player == player)
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and state.has_sword(player)) # sword required for curtain
|
||||
allow_self_locking_items(world.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
|
||||
|
||||
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_melt_things(player))
|
||||
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_melt_things(state, player))
|
||||
set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
|
||||
set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1))))
|
||||
set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1))))
|
||||
set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or (
|
||||
item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
|
||||
set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player))
|
||||
|
||||
set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player))) # need to defeat wizzrobes, bombs don't work ...
|
||||
set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ...
|
||||
set_rule(world.get_location('Misery Mire - Big Chest', player), lambda state: state.has('Big Key (Misery Mire)', player))
|
||||
set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.multiworld.can_take_damage[player] and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
|
||||
set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.multiworld.can_take_damage[player] and has_hearts(state, player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
|
||||
set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player))
|
||||
# you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ...
|
||||
# big key gives backdoor access to that from the teleporter in the north west
|
||||
@@ -311,11 +318,11 @@ def global_rules(world, player):
|
||||
set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state._lttp_has_key('Big Key (Misery Mire)', player))
|
||||
# we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet
|
||||
set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2) if ((
|
||||
item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or
|
||||
location_item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or
|
||||
(
|
||||
item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3))
|
||||
set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: state.has_fire_source(player))
|
||||
set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: state.has_fire_source(player))
|
||||
location_item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3))
|
||||
set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: has_fire_source(state, player))
|
||||
set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: has_fire_source(state, player))
|
||||
set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
|
||||
set_rule(world.get_entrance('Turtle Rock Entrance Gap', player), lambda state: state.has('Cane of Somaria', player))
|
||||
@@ -335,20 +342,20 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
|
||||
|
||||
if not world.enemy_shuffle[player]:
|
||||
set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: state.can_shoot_arrows(player))
|
||||
set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_shoot_arrows(state, player))
|
||||
set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and state.can_shoot_arrows(player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and can_shoot_arrows(state, player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
|
||||
set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player))
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3)))
|
||||
location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3)))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
|
||||
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||
item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
|
||||
location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||
|
||||
@@ -362,7 +369,7 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
|
||||
set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
|
||||
item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
|
||||
location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
|
||||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
|
||||
|
||||
@@ -399,9 +406,9 @@ def global_rules(world, player):
|
||||
lambda state: state.has('Big Key (Ganons Tower)', player))
|
||||
else:
|
||||
set_rule(world.get_entrance('Ganons Tower Big Key Door', player),
|
||||
lambda state: state.has('Big Key (Ganons Tower)', player) and state.can_shoot_arrows(player))
|
||||
lambda state: state.has('Big Key (Ganons Tower)', player) and can_shoot_arrows(state, player))
|
||||
set_rule(world.get_entrance('Ganons Tower Torch Rooms', player),
|
||||
lambda state: state.has_fire_source(player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
|
||||
lambda state: has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
|
||||
set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
|
||||
set_rule(world.get_entrance('Ganons Tower Moldorm Door', player),
|
||||
@@ -412,12 +419,12 @@ def global_rules(world, player):
|
||||
ganon = world.get_location('Ganon', player)
|
||||
set_rule(ganon, lambda state: GanonDefeatRule(state, player))
|
||||
if world.goal[player] in ['ganontriforcehunt', 'localganontriforcehunt']:
|
||||
add_rule(ganon, lambda state: state.has_triforce_pieces(state.multiworld.treasure_hunt_count[player], player))
|
||||
add_rule(ganon, lambda state: has_triforce_pieces(state, player))
|
||||
elif world.goal[player] == 'ganonpedestal':
|
||||
add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player))
|
||||
else:
|
||||
add_rule(ganon, lambda state: state.has_crystals(state.multiworld.crystals_needed_for_ganon[player], player))
|
||||
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop
|
||||
add_rule(ganon, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_ganon[player], player))
|
||||
set_rule(world.get_entrance('Ganon Drop', player), lambda state: has_beam_sword(state, player)) # need to damage ganon to get tiles to drop
|
||||
|
||||
set_rule(world.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player))
|
||||
|
||||
@@ -426,51 +433,51 @@ def default_rules(world, player):
|
||||
"""Default world rules when world state is not inverted."""
|
||||
# overworld requirements
|
||||
set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('Kings Grave Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player))
|
||||
# Caution: If king's grave is releaxed at all to account for reaching it via a two way cave's exit in insanity mode, then the bomb shop logic will need to be updated (that would involve create a small ledge-like Region for it)
|
||||
set_rule(world.get_entrance('Bonk Fairy (Light)', player), lambda state: state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Beat Agahnim 1', player))
|
||||
set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player))
|
||||
set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player))
|
||||
set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player))
|
||||
set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player))
|
||||
set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player))
|
||||
set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Flute Spot 1', player), lambda state: state.has('Activated Flute', player))
|
||||
set_rule(world.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
|
||||
set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
|
||||
set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and state.can_lift_rocks(player)) or state.can_lift_heavy_rocks(player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes
|
||||
set_rule(world.get_entrance('Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('East Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
|
||||
set_rule(world.get_entrance('South Hyrule Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
|
||||
set_rule(world.get_entrance('Kakariko Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player)) # bunny cannot lift bushes
|
||||
set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player))
|
||||
set_rule(world.get_entrance('Bat Cave Drop Ledge', player), lambda state: state.has('Hammer', player))
|
||||
|
||||
set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player))
|
||||
set_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Flippers', player))
|
||||
set_rule(world.get_location('Frog', player), lambda state: state.can_lift_heavy_rocks(player)) # will get automatic moon pearl requirement
|
||||
set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player)) # will get automatic moon pearl requirement
|
||||
set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player))
|
||||
set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: state.can_lift_rocks(player)) # should we decide to place something that is not a dungeon end up there at some point
|
||||
set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle
|
||||
set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player))
|
||||
set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player)) # should we decide to place something that is not a dungeon end up there at some point
|
||||
set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player))
|
||||
set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle
|
||||
set_rule(world.get_entrance('Top of Pyramid', player), lambda state: state.has('Beat Agahnim 1', player))
|
||||
set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up
|
||||
set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block
|
||||
set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
|
||||
|
||||
set_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (state.can_lift_rocks(player) or state.has('Hammer', player) or state.has('Flippers', player)))
|
||||
set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (state.can_lift_rocks(player) or state.has('Hammer', player)))
|
||||
set_rule(world.get_entrance('Catfish Exit Rock', player), lambda state: can_lift_rocks(state, player))
|
||||
set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player))
|
||||
set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player) or state.has('Flippers', player)))
|
||||
set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: state.has('Moon Pearl', player) and (can_lift_rocks(state, player) or state.has('Hammer', player)))
|
||||
set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Moon Pearl', player) and state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Moon Pearl', player) and state.has('Hookshot', player))
|
||||
@@ -478,12 +485,12 @@ def default_rules(world, player):
|
||||
set_rule(world.get_entrance('Hyrule Castle Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Hyrule Castle Main Gate', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: (state.has('Moon Pearl', player) and state.has('Flippers', player) or state.has('Magic Mirror', player))) # Overworld Bunny Revival
|
||||
set_rule(world.get_location('Bombos Tablet', player), lambda state: state.can_retrieve_tablet(player))
|
||||
set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # ToDo any fake flipper set up?
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Ledge Fairy', player), lambda state: state.has('Moon Pearl', player)) # bomb required
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('Hype Cave', player), lambda state: state.has('Moon Pearl', player)) # bomb required
|
||||
set_rule(world.get_entrance('Brewery', player), lambda state: state.has('Moon Pearl', player)) # bomb required
|
||||
set_rule(world.get_entrance('Thieves Town', player), lambda state: state.has('Moon Pearl', player)) # bunny cannot pull
|
||||
@@ -497,26 +504,26 @@ def default_rules(world, player):
|
||||
set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
|
||||
set_rule(world.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has('Moon Pearl', player) and state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has('Moon Pearl', player) and can_lift_rocks(state, player))
|
||||
set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Bat Cave Drop Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has('Moon Pearl', player) and can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Moon Pearl', player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player))
|
||||
set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player))
|
||||
|
||||
set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has('Moon Pearl', player)) # bunny cannot use fire rod
|
||||
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and state.has_sword(player) and state.has_misery_mire_medallion(player)) # sword required to cast magic (!)
|
||||
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!)
|
||||
set_rule(world.get_entrance('Desert Ledge (Northeast) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
|
||||
set_rule(world.get_entrance('Desert Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Desert Palace Stairs Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Desert Palace Entrance (North) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Spectacle Rock Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Hookshot Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
|
||||
|
||||
set_rule(world.get_entrance('East Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Mimic Cave Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
@@ -525,7 +532,7 @@ def default_rules(world, player):
|
||||
set_rule(world.get_entrance('Isolated Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Superbunny Cave Exit (Bottom)', player), lambda state: False) # Cannot get to bottom exit from top. Just exists for shuffling
|
||||
set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and state.has_sword(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
|
||||
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
|
||||
|
||||
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player].to_bool(world, player))
|
||||
|
||||
@@ -545,12 +552,12 @@ def inverted_rules(world, player):
|
||||
set_rule(world.get_entrance('Potion Shop Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Light World Pier', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Kings Grave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Kings Grave Outer Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Kings Grave Inner Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Potion Shop Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Potion Shop Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Potion Shop Outer Rock', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Potion Shop Inner Rock', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Potion Shop Outer Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Potion Shop Inner Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Graveyard Cave Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Graveyard Cave Outer Bushes', player), lambda state: state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Secret Passage Inner Bushes', player), lambda state: state.has('Moon Pearl', player))
|
||||
@@ -560,23 +567,23 @@ def inverted_rules(world, player):
|
||||
set_rule(world.get_entrance('Lumberjack Tree Tree', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) and state.has('Beat Agahnim 1', player))
|
||||
set_rule(world.get_entrance('Bonk Rock Cave', player), lambda state: state.has('Pegasus Boots', player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Desert Palace Stairs', player), lambda state: state.has('Book of Mudora', player)) # bunny can use book
|
||||
set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Sanctuary Grave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('20 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('50 Rupee Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Death Mountain Entrance Rock', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Bumper Cave Entrance Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
|
||||
set_rule(world.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
|
||||
set_rule(world.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and state.can_lift_rocks(player)) or state.can_lift_heavy_rocks(player)) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Central Island Teleporter', player), lambda state: can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('Dark Desert Teleporter', player), lambda state: state.has('Activated Flute', player) and can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('East Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
|
||||
set_rule(world.get_entrance('South Dark World Teleporter', player), lambda state: state.has('Hammer', player) and can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
|
||||
set_rule(world.get_entrance('West Dark World Teleporter', player), lambda state: ((state.has('Hammer', player) and can_lift_rocks(state, player)) or can_lift_heavy_rocks(state, player)) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_location('Flute Spot', player), lambda state: state.has('Shovel', player) and state.has('Moon Pearl', player))
|
||||
|
||||
set_rule(world.get_location('Zora\'s Ledge', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Waterfall of Wishing Cave', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Northeast Light World Return', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_location('Frog', player), lambda state: state.can_lift_heavy_rocks(player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal
|
||||
set_rule(world.get_location('Frog', player), lambda state: can_lift_heavy_rocks(state, player) and (state.has('Moon Pearl', player) or state.has('Beat Agahnim 1', player)) or (state.can_reach('Light World', 'Region', player) and state.has('Magic Mirror', player))) # Need LW access using Mirror or Portal
|
||||
set_rule(world.get_location('Missing Smith', player), lambda state: state.has('Get Frog', player) and state.can_reach('Blacksmiths Hut', 'Region', player)) # Can't S&Q with smith
|
||||
set_rule(world.get_location('Blacksmith', player), lambda state: state.has('Return Smith', player))
|
||||
set_rule(world.get_location('Magic Bat', player), lambda state: state.has('Magic Powder', player) and state.has('Moon Pearl', player))
|
||||
@@ -591,52 +598,52 @@ def inverted_rules(world, player):
|
||||
set_rule(world.get_entrance('North Fairy Cave Drop', player), lambda state: state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Lost Woods Hideout Drop', player), lambda state: state.has('Moon Pearl', player))
|
||||
set_rule(world.get_location('Potion Shop', player), lambda state: state.has('Mushroom', player) and (state.can_reach('Potion Shop Area', 'Region', player))) # new inverted region, need pearl for bushes or access to potion shop door/waterfall fairy
|
||||
set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point
|
||||
set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: state.can_lift_rocks(player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Desert Palace Entrance (North) Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Desert Ledge Return Rocks', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player)) # should we decide to place something that is not a dungeon end up there at some point
|
||||
set_rule(world.get_entrance('Checkerboard Cave', player), lambda state: can_lift_rocks(state, player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Hyrule Castle Secret Entrance Drop', player), lambda state: state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Old Man Cave Exit (West)', player), lambda state: False) # drop cannot be climbed up
|
||||
set_rule(world.get_entrance('Broken Bridge (West)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block
|
||||
set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player))
|
||||
set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
|
||||
set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: can_lift_heavy_rocks(state, player) and state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny cannot use hammer
|
||||
set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has('Moon Pearl', player)) # bunny can not use hammer
|
||||
|
||||
set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((state.can_lift_rocks(player) or state.has('Hammer', player)) or state.has('Flippers', player)))
|
||||
set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (state.can_lift_rocks(player) or state.has('Hammer', player)))
|
||||
set_rule(world.get_entrance('Catfish Entrance Rock', player), lambda state: can_lift_rocks(state, player))
|
||||
set_rule(world.get_entrance('Northeast Dark World Broken Bridge Pass', player), lambda state: ((can_lift_rocks(state, player) or state.has('Hammer', player)) or state.has('Flippers', player)))
|
||||
set_rule(world.get_entrance('East Dark World Broken Bridge Pass', player), lambda state: (can_lift_rocks(state, player) or state.has('Hammer', player)))
|
||||
set_rule(world.get_entrance('South Dark World Bridge', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Bonk Fairy (Dark)', player), lambda state: state.has('Pegasus Boots', player))
|
||||
set_rule(world.get_entrance('West Dark World Gap', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Flippers', player))
|
||||
set_rule(world.get_location('Bombos Tablet', player), lambda state: state.can_retrieve_tablet(player))
|
||||
set_rule(world.get_location('Bombos Tablet', player), lambda state: can_retrieve_tablet(state, player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Drop (South)', player), lambda state: state.has('Flippers', player)) # ToDo any fake flipper set up?
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Ledge Pier', player), lambda state: state.has('Flippers', player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Ledge Spike Cave', player), lambda state: can_lift_rocks(state, player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Teleporter', player), lambda state: state.has('Flippers', player)) # Fake Flippers
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Shallows', player), lambda state: state.has('Flippers', player))
|
||||
set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Village of Outcasts Heavy Rock', player), lambda state: can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('East Dark World Bridge', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Lake Hylia Central Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('East Dark World River Pier', player), lambda state: state.has('Flippers', player))
|
||||
set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: can_lift_rocks(state, player))
|
||||
set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Hammer Peg Area Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: can_lift_heavy_rocks(state, player))
|
||||
set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player))
|
||||
set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player))
|
||||
|
||||
set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player))
|
||||
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) # sword required to cast magic (!)
|
||||
set_rule(world.get_entrance('Misery Mire', player), lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # sword required to cast magic (!)
|
||||
|
||||
set_rule(world.get_entrance('Hookshot Cave', player), lambda state: state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Hookshot Cave', player), lambda state: can_lift_rocks(state, player))
|
||||
|
||||
set_rule(world.get_entrance('East Death Mountain Mirror Spot (Top)', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Death Mountain (Top) Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
@@ -646,7 +653,7 @@ def inverted_rules(world, player):
|
||||
set_rule(world.get_entrance('Dark Death Mountain Ledge Mirror Spot (West)', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Laser Bridge Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_sword(player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
|
||||
set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
|
||||
|
||||
# new inverted spots
|
||||
set_rule(world.get_entrance('Post Aga Teleporter', player), lambda state: state.has('Beat Agahnim 1', player))
|
||||
@@ -687,7 +694,7 @@ def inverted_rules(world, player):
|
||||
def no_glitches_rules(world, player):
|
||||
""""""
|
||||
if world.mode[player] == 'inverted':
|
||||
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or state.can_lift_rocks(player)))
|
||||
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or can_lift_rocks(state, player)))
|
||||
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
|
||||
set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
|
||||
set_rule(world.get_entrance('Lake Hylia Warp', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
|
||||
@@ -698,7 +705,7 @@ def no_glitches_rules(world, player):
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Ledge Drop', player), lambda state: state.has('Flippers', player))
|
||||
set_rule(world.get_entrance('East Dark World Pier', player), lambda state: state.has('Flippers', player))
|
||||
else:
|
||||
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Flippers', player) or can_lift_rocks(state, player))
|
||||
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Flippers', player)) # can be fake flippered to
|
||||
set_rule(world.get_entrance('Hobo Bridge', player), lambda state: state.has('Flippers', player))
|
||||
set_rule(world.get_entrance('Dark Lake Hylia Drop (East)', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player))
|
||||
@@ -820,19 +827,19 @@ def open_rules(world, player):
|
||||
|
||||
|
||||
def swordless_rules(world, player):
|
||||
set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or can_shoot_arrows(state, player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
|
||||
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
|
||||
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace
|
||||
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle
|
||||
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
|
||||
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!)
|
||||
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
|
||||
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!)
|
||||
else:
|
||||
# only need ddm access for aga tower in inverted
|
||||
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has_turtle_rock_medallion(player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
|
||||
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_misery_mire_medallion(player)) # sword not required to use medallion for opening in swordless (!)
|
||||
set_rule(world.get_entrance('Turtle Rock', player), lambda state: has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
|
||||
set_rule(world.get_entrance('Misery Mire', player), lambda state: has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!)
|
||||
|
||||
|
||||
def add_connection(parent_name, target_name, entrance_name, world, player):
|
||||
@@ -904,7 +911,7 @@ def set_trock_key_rules(world, player):
|
||||
# Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we
|
||||
# might open all the locked doors in any order so we need maximally restrictive rules.
|
||||
if can_reach_back:
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
|
||||
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
|
||||
# Only consider wasting the key on the Trinexx door for going from the front entrance to middle section. If other key doors are accessible, then these doors can be avoided
|
||||
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
|
||||
@@ -924,7 +931,7 @@ def set_trock_key_rules(world, player):
|
||||
def tr_big_key_chest_keys_needed(state):
|
||||
# This function handles the key requirements for the TR Big Chest in the situations it having the Big Key should logically require 2 keys, small key
|
||||
# should logically require no keys, and anything else should logically require 4 keys.
|
||||
item = item_name(state, 'Turtle Rock - Big Key Chest', player)
|
||||
item = location_item_name(state, 'Turtle Rock - Big Key Chest', player)
|
||||
if item in [('Small Key (Turtle Rock)', player)]:
|
||||
return 0
|
||||
if item in [('Big Key (Turtle Rock)', player)]:
|
||||
@@ -1083,7 +1090,7 @@ def set_big_bomb_rules(world, player):
|
||||
# returning via the eastern and southern teleporters needs the same items, so we use the southern teleporter for out routing.
|
||||
# crossing preg bridge already requires hammer so we just add the gloves to the requirement
|
||||
def southern_teleporter(state):
|
||||
return state.can_lift_rocks(player) and cross_peg_bridge(state)
|
||||
return can_lift_rocks(state, player) and cross_peg_bridge(state)
|
||||
|
||||
# the basic routes assume you can reach eastern light world with the bomb.
|
||||
# you can then use the southern teleporter, or (if you have beaten Aga1) the hyrule castle gate warp
|
||||
@@ -1110,13 +1117,13 @@ def set_big_bomb_rules(world, player):
|
||||
#1. Mirror and basic routes
|
||||
#2. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl
|
||||
# -> (Mitts and CPB) or (M and BR)
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state)))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (state.has('Magic Mirror', player) and basic_routes(state)))
|
||||
elif bombshop_entrance.name == 'Bumper Cave (Bottom)':
|
||||
#1. Mirror and Lift rock and basic_routes
|
||||
#2. Mirror and Flute and basic routes (can make difference if accessed via insanity or w/ mirror from connector, and then via hyrule castle gate, because no gloves are needed in that case)
|
||||
#3. Go to south DW and then cross peg bridge: Need Mitts and hammer and moon pearl
|
||||
# -> (Mitts and CPB) or (((G or Flute) and M) and BR))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and cross_peg_bridge(state)) or (((state.can_lift_rocks(player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state)))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and cross_peg_bridge(state)) or (((can_lift_rocks(state, player) or state.has('Flute', player)) and state.has('Magic Mirror', player)) and basic_routes(state)))
|
||||
elif bombshop_entrance.name in Southern_DW_entrances:
|
||||
#1. Mirror and enter via gate: Need mirror and Aga1
|
||||
#2. cross peg bridge: Need hammer and moon pearl
|
||||
@@ -1144,7 +1151,7 @@ def set_big_bomb_rules(world, player):
|
||||
elif bombshop_entrance.name == 'Fairy Ascension Cave (Bottom)':
|
||||
# Same as East_LW_DM_entrances except navigation without BR requires Mitts
|
||||
# -> Flute and ((M and Hookshot and Mitts) or BR)
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and state.can_lift_heavy_rocks(player)) or basic_routes(state)))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) and ((state.has('Magic Mirror', player) and state.has('Hookshot', player) and can_lift_heavy_rocks(state, player)) or basic_routes(state)))
|
||||
elif bombshop_entrance.name in Castle_ledge_entrances:
|
||||
# 1. mirror on pyramid to castle ledge, grab bomb, return through mirror spot: Needs mirror
|
||||
# 2. flute then basic routes
|
||||
@@ -1160,7 +1167,7 @@ def set_big_bomb_rules(world, player):
|
||||
# 1. Lift rock then basic_routes
|
||||
# 2. flute then basic_routes
|
||||
# -> (Flute or G) and BR
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.can_lift_rocks(player)) and basic_routes(state))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_rocks(state, player)) and basic_routes(state))
|
||||
elif bombshop_entrance.name == 'Graveyard Cave':
|
||||
# 1. flute then basic routes
|
||||
# 2. (has west dark world access) use existing mirror spot (required Pearl), mirror again off ledge
|
||||
@@ -1176,13 +1183,13 @@ def set_big_bomb_rules(world, player):
|
||||
# 2. walk down by hammering peg: needs hammer and pearl
|
||||
# 3. mirror and basic routes
|
||||
# -> (P and (H or Gloves)) or (M and BR)
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or state.can_lift_rocks(player))) or (state.has('Magic Mirror', player) and basic_routes(state)))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Moon Pearl', player) and (state.has('Hammer', player) or can_lift_rocks(state, player))) or (state.has('Magic Mirror', player) and basic_routes(state)))
|
||||
elif bombshop_entrance.name == 'Kings Grave':
|
||||
# same as the Normal_LW_entrances case except that the pre-existing mirror is only possible if you have mitts
|
||||
# (because otherwise mirror was used to reach the grave, so would cancel a pre-existing mirror spot)
|
||||
# to account for insanity, must consider a way to escape without a cave for basic_routes
|
||||
# -> (M and Mitts) or ((Mitts or Flute or (M and P and West Dark World access)) and BR)
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.can_lift_heavy_rocks(player) and state.has('Magic Mirror', player)) or ((state.can_lift_heavy_rocks(player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state)))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (can_lift_heavy_rocks(state, player) and state.has('Magic Mirror', player)) or ((can_lift_heavy_rocks(state, player) or state.has('Activated Flute', player) or (state.can_reach('West Dark World', 'Region', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))) and basic_routes(state)))
|
||||
elif bombshop_entrance.name == 'Waterfall of Wishing':
|
||||
# same as the Normal_LW_entrances case except in insanity it's possible you could be here without Flippers which
|
||||
# means you need an escape route of either Flippers or Flute
|
||||
@@ -1329,7 +1336,7 @@ def set_inverted_big_bomb_rules(world, player):
|
||||
elif bombshop_entrance.name in Northern_DW_entrances:
|
||||
# You can just fly with the Flute, you can take a long walk with Mitts and Hammer,
|
||||
# or you can leave a Mirror portal nearby and then walk to the castle to Mirror again.
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player)))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player)))
|
||||
elif bombshop_entrance.name in Southern_DW_entrances:
|
||||
# This is the same as north DW without the Mitts rock present.
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Hammer', player) or state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player)))
|
||||
@@ -1341,22 +1348,22 @@ def set_inverted_big_bomb_rules(world, player):
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player)))
|
||||
elif bombshop_entrance.name in LW_bush_entrances:
|
||||
# These entrances are behind bushes in LW so you need either Pearl or the tools to solve NDW bomb shop locations.
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player))))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player))))
|
||||
elif bombshop_entrance.name == 'Village of Outcasts Shop':
|
||||
# This is mostly the same as NDW but the Mirror path requires the Pearl, or using the Hammer
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player))))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player))))
|
||||
elif bombshop_entrance.name == 'Bumper Cave (Bottom)':
|
||||
# This is mostly the same as NDW but the Mirror path requires being able to lift a rock.
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_lift_rocks(player) and state.can_reach('Light World', 'Region', player)))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and can_lift_rocks(state, player) and state.can_reach('Light World', 'Region', player)))
|
||||
elif bombshop_entrance.name == 'Old Man Cave (West)':
|
||||
# The three paths back are Mirror and DW walk, Mirror and Flute, or LW walk and then Mirror.
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.can_lift_rocks(player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player)))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and ((can_lift_heavy_rocks(state, player) and state.has('Hammer', player)) or (can_lift_rocks(state, player) and state.has('Moon Pearl', player)) or state.has('Activated Flute', player)))
|
||||
elif bombshop_entrance.name == 'Dark World Potion Shop':
|
||||
# You either need to Flute to 5 or cross the rock/hammer choice pass to the south.
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or state.can_lift_rocks(player))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or state.has('Hammer', player) or can_lift_rocks(state, player))
|
||||
elif bombshop_entrance.name == 'Kings Grave':
|
||||
# Either lift the rock and walk to the castle to Mirror or Mirror immediately and Flute.
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or state.can_lift_heavy_rocks(player)) and state.has('Magic Mirror', player))
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: (state.has('Activated Flute', player) or can_lift_heavy_rocks(state, player)) and state.has('Magic Mirror', player))
|
||||
elif bombshop_entrance.name == 'Waterfall of Wishing':
|
||||
# You absolutely must be able to swim to return it from here.
|
||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Flippers', player) and state.has('Moon Pearl', player) and state.has('Magic Mirror', player))
|
||||
@@ -1421,7 +1428,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
|
||||
if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic
|
||||
return lambda state: state.has('Moon Pearl', player)
|
||||
if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch
|
||||
return lambda state: state.has('Magic Mirror', player) and state.has_sword(player) or state.has('Moon Pearl', player)
|
||||
return lambda state: state.has('Magic Mirror', player) and has_sword(state, player) or state.has('Moon Pearl', player)
|
||||
if region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
|
||||
return lambda state: state.has('Magic Mirror', player) or state.has('Moon Pearl', player)
|
||||
if region.type == LTTPRegionType.Dungeon:
|
||||
@@ -1459,7 +1466,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
|
||||
# For glitch rulesets, establish superbunny and revival rules.
|
||||
if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
|
||||
if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions():
|
||||
possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and state.has_sword(player))
|
||||
possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and has_sword(state, player))
|
||||
elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions()
|
||||
or location is not None and location.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_locations()):
|
||||
possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and state.has('Pegasus Boots', player))
|
||||
@@ -1507,4 +1514,4 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
|
||||
continue
|
||||
if location.name in bunny_accessible_locations:
|
||||
continue
|
||||
add_rule(location, get_rule_to_add(entrance.connected_region, location))
|
||||
add_rule(location, get_rule_to_add(entrance.connected_region, location))
|
||||
|
||||
137
worlds/alttp/StateHelpers.py
Normal file
137
worlds/alttp/StateHelpers.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from .SubClasses import LTTPRegion
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
def is_not_bunny(state: CollectionState, region: LTTPRegion, player: int) -> bool:
|
||||
if state.has('Moon Pearl', player):
|
||||
return True
|
||||
|
||||
return region.is_light_world if state.multiworld.mode[player] != 'inverted' else region.is_dark_world
|
||||
|
||||
def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bool:
|
||||
return is_not_bunny(state, region, player) and state.has('Pegasus Boots', player)
|
||||
|
||||
def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool:
|
||||
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(state) for
|
||||
shop in state.multiworld.shops)
|
||||
|
||||
def can_buy(state: CollectionState, item: str, player: int) -> bool:
|
||||
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(state) for
|
||||
shop in state.multiworld.shops)
|
||||
|
||||
def can_shoot_arrows(state: CollectionState, player: int) -> bool:
|
||||
if state.multiworld.retro_bow[player]:
|
||||
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player)
|
||||
return state.has('Bow', player) or state.has('Silver Bow', player)
|
||||
|
||||
def has_triforce_pieces(state: CollectionState, player: int) -> bool:
|
||||
count = state.multiworld.treasure_hunt_count[player]
|
||||
return state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= count
|
||||
|
||||
def has_crystals(state: CollectionState, count: int, player: int) -> bool:
|
||||
found = state.count_group("Crystals", player)
|
||||
return found >= count
|
||||
|
||||
def can_lift_rocks(state: CollectionState, player: int):
|
||||
return state.has('Power Glove', player) or state.has('Titans Mitts', player)
|
||||
|
||||
def can_lift_heavy_rocks(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Titans Mitts', player)
|
||||
|
||||
def bottle_count(state: CollectionState, player: int) -> int:
|
||||
return min(state.multiworld.difficulty_requirements[player].progressive_bottle_limit,
|
||||
state.count_group("Bottles", player))
|
||||
|
||||
def has_hearts(state: CollectionState, player: int, count: int) -> int:
|
||||
# Warning: This only considers items that are marked as advancement items
|
||||
return heart_count(state, player) >= count
|
||||
|
||||
def heart_count(state: CollectionState, player: int) -> int:
|
||||
# Warning: This only considers items that are marked as advancement items
|
||||
diff = state.multiworld.difficulty_requirements[player]
|
||||
return min(state.item_count('Boss Heart Container', player), diff.boss_heart_container_limit) \
|
||||
+ state.item_count('Sanctuary Heart Container', player) \
|
||||
+ min(state.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
|
||||
+ 3 # starting hearts
|
||||
|
||||
def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
|
||||
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
|
||||
basemagic = 8
|
||||
if state.has('Magic Upgrade (1/4)', player):
|
||||
basemagic = 32
|
||||
elif state.has('Magic Upgrade (1/2)', player):
|
||||
basemagic = 16
|
||||
if can_buy_unlimited(state, 'Green Potion', player) or can_buy_unlimited(state, 'Blue Potion', player):
|
||||
if state.multiworld.item_functionality[player] == 'hard' and not fullrefill:
|
||||
basemagic = basemagic + int(basemagic * 0.5 * bottle_count(state, player))
|
||||
elif state.multiworld.item_functionality[player] == 'expert' and not fullrefill:
|
||||
basemagic = basemagic + int(basemagic * 0.25 * bottle_count(state, player))
|
||||
else:
|
||||
basemagic = basemagic + basemagic * bottle_count(state, player)
|
||||
return basemagic >= smallmagic
|
||||
|
||||
def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool:
|
||||
return (has_melee_weapon(state, player)
|
||||
or state.has('Cane of Somaria', player)
|
||||
or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player)))
|
||||
or can_shoot_arrows(state, player)
|
||||
or state.has('Fire Rod', player)
|
||||
or (state.has('Bombs (10)', player) and enemies < 6))
|
||||
|
||||
def can_get_good_bee(state: CollectionState, player: int) -> bool:
|
||||
cave = state.multiworld.get_region('Good Bee Cave', player)
|
||||
return (
|
||||
state.has_group("Bottles", player) and
|
||||
state.has('Bug Catching Net', player) and
|
||||
(state.has('Pegasus Boots', player) or (has_sword(state, player) and state.has('Quake', player))) and
|
||||
cave.can_reach(state) and
|
||||
is_not_bunny(state, cave, player)
|
||||
)
|
||||
|
||||
def can_retrieve_tablet(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or
|
||||
(state.multiworld.swordless[player] and
|
||||
state.has("Hammer", player)))
|
||||
|
||||
def has_sword(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Fighter Sword', player) \
|
||||
or state.has('Master Sword', player) \
|
||||
or state.has('Tempered Sword', player) \
|
||||
or state.has('Golden Sword', player)
|
||||
|
||||
def has_beam_sword(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Master Sword', player) or state.has('Tempered Sword', player) or state.has('Golden Sword',
|
||||
player)
|
||||
|
||||
def has_melee_weapon(state: CollectionState, player: int) -> bool:
|
||||
return has_sword(state, player) or state.has('Hammer', player)
|
||||
|
||||
def has_fire_source(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Fire Rod', player) or state.has('Lamp', player)
|
||||
|
||||
def can_melt_things(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Fire Rod', player) or \
|
||||
(state.has('Bombos', player) and
|
||||
(state.multiworld.swordless[player] or
|
||||
has_sword(state, player)))
|
||||
|
||||
def has_misery_mire_medallion(state: CollectionState, player: int) -> bool:
|
||||
return state.has(state.multiworld.required_medallions[player][0], player)
|
||||
|
||||
def has_turtle_rock_medallion(state: CollectionState, player: int) -> bool:
|
||||
return state.has(state.multiworld.required_medallions[player][1], player)
|
||||
|
||||
def can_boots_clip_lw(state: CollectionState, player: int) -> bool:
|
||||
if state.multiworld.mode[player] == 'inverted':
|
||||
return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)
|
||||
return state.has('Pegasus Boots', player)
|
||||
|
||||
def can_boots_clip_dw(state: CollectionState, player: int) -> bool:
|
||||
if state.multiworld.mode[player] != 'inverted':
|
||||
return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)
|
||||
return state.has('Pegasus Boots', player)
|
||||
|
||||
def can_get_glitched_speed_dw(state: CollectionState, player: int) -> bool:
|
||||
rules = [state.has('Pegasus Boots', player), any([state.has('Hookshot', player), has_sword(state, player)])]
|
||||
if state.multiworld.mode[player] != 'inverted':
|
||||
rules.append(state.has('Moon Pearl', player))
|
||||
return all(rules)
|
||||
@@ -4,7 +4,6 @@ from enum import IntEnum
|
||||
|
||||
from BaseClasses import Location, Item, ItemClassification, Region, MultiWorld
|
||||
|
||||
|
||||
class ALttPLocation(Location):
|
||||
game: str = "A Link to the Past"
|
||||
crystal: bool
|
||||
@@ -81,6 +80,12 @@ class LTTPRegionType(IntEnum):
|
||||
class LTTPRegion(Region):
|
||||
type: LTTPRegionType
|
||||
|
||||
# will be set after making connections.
|
||||
is_light_world: bool = False
|
||||
is_dark_world: bool = False
|
||||
|
||||
shop: Optional = None
|
||||
|
||||
def __init__(self, name: str, type_: LTTPRegionType, hint: str, player: int, multiworld: MultiWorld):
|
||||
super().__init__(name, player, multiworld, hint)
|
||||
self.type = type_
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
|
||||
from BaseClasses import Entrance
|
||||
from .SubClasses import LTTPRegion
|
||||
from worlds.generic.Rules import set_rule, add_rule
|
||||
from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion
|
||||
|
||||
# We actually need the logic to properly "mark" these regions as Light or Dark world.
|
||||
# Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules.
|
||||
@@ -46,9 +48,9 @@ def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, du
|
||||
if dungeon_entrance.name == 'Skull Woods Final Section':
|
||||
set_rule(clip, lambda state: False) # entrance doesn't exist until you fire rod it from the other side
|
||||
elif dungeon_entrance.name == 'Misery Mire':
|
||||
add_rule(clip, lambda state: state.has_sword(player) and state.has_misery_mire_medallion(player)) # open the dungeon
|
||||
add_rule(clip, lambda state: has_sword(state, player) and has_misery_mire_medallion(state, player)) # open the dungeon
|
||||
elif dungeon_entrance.name == 'Agahnims Tower':
|
||||
add_rule(clip, lambda state: state.has('Cape', player) or state.has_beam_sword(player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier
|
||||
add_rule(clip, lambda state: state.has('Cape', player) or has_beam_sword(state, player) or state.has('Beat Agahnim 1', player)) # kill/bypass barrier
|
||||
# Then we set a restriction on exiting the dungeon, so you can't leave unless you got in normally.
|
||||
add_rule(world.get_entrance(dungeon_exit, player), lambda state: dungeon_entrance.can_reach(state))
|
||||
elif not fix_fake_worlds: # full, dungeonsfull; fixed dungeon exits, but no fake worlds fix
|
||||
@@ -66,21 +68,21 @@ def underworld_glitches_rules(world, player):
|
||||
|
||||
# Ice Palace Entrance Clip
|
||||
# This is the easiest one since it's a simple internal clip. Just need to also add melting to freezor chest since it's otherwise assumed.
|
||||
add_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_bomb_clip(world.get_region('Ice Palace (Entrance)', player), player), combine='or')
|
||||
add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.can_melt_things(player))
|
||||
add_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: can_bomb_clip(state, world.get_region('Ice Palace (Entrance)', player), player), combine='or')
|
||||
add_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: can_melt_things(state, player))
|
||||
|
||||
|
||||
# Kiki Skip
|
||||
kikiskip = world.get_entrance('Kiki Skip', player)
|
||||
set_rule(kikiskip, lambda state: state.can_bomb_clip(kikiskip.parent_region, player))
|
||||
set_rule(kikiskip, lambda state: can_bomb_clip(state, kikiskip.parent_region, player))
|
||||
dungeon_reentry_rules(world, player, kikiskip, 'Palace of Darkness (Entrance)', 'Palace of Darkness Exit')
|
||||
|
||||
|
||||
# Mire -> Hera -> Swamp
|
||||
# Using mire keys on other dungeon doors
|
||||
mire = world.get_region('Misery Mire (West)', player)
|
||||
mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and state.can_bomb_clip(mire, player) and state.has_fire_source(player)
|
||||
hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and state.can_bomb_clip(world.get_region('Tower of Hera (Top)', player), player)
|
||||
mire_clip = lambda state: state.can_reach('Misery Mire (West)', 'Region', player) and can_bomb_clip(state, mire, player) and has_fire_source(state, player)
|
||||
hera_clip = lambda state: state.can_reach('Tower of Hera (Top)', 'Region', player) and can_bomb_clip(state, world.get_region('Tower of Hera (Top)', player), player)
|
||||
add_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: mire_clip(state) and state.has('Big Key (Misery Mire)', player), combine='or')
|
||||
add_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: mire_clip(state), combine='or')
|
||||
add_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: mire_clip(state) or hera_clip(state), combine='or')
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import random
|
||||
import threading
|
||||
import typing
|
||||
from collections import OrderedDict
|
||||
|
||||
import Utils
|
||||
from BaseClasses import Item, CollectionState, Tutorial, MultiWorld
|
||||
@@ -19,9 +20,10 @@ from .Client import ALTTPSNIClient
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
|
||||
get_hash_string, get_base_rom_path, LttPDeltaPatch
|
||||
from .Rules import set_rules
|
||||
from .Shops import create_shops, ShopSlotFill
|
||||
from .SubClasses import ALttPItem
|
||||
from .Shops import create_shops, Shop, ShopSlotFill, ShopType, price_rate_display, price_type_display_name
|
||||
from .SubClasses import ALttPItem, LTTPRegionType
|
||||
from worlds.AutoWorld import World, WebWorld, LogicMixin
|
||||
from .StateHelpers import can_buy_unlimited
|
||||
|
||||
lttp_logger = logging.getLogger("A Link to the Past")
|
||||
|
||||
@@ -115,6 +117,75 @@ class ALTTPWorld(World):
|
||||
option_definitions = alttp_options
|
||||
topology_present = True
|
||||
item_name_groups = item_name_groups
|
||||
location_name_groups = {
|
||||
"Blind's Hideout": {"Blind's Hideout - Top", "Blind's Hideout - Left", "Blind's Hideout - Right",
|
||||
"Blind's Hideout - Far Left", "Blind's Hideout - Far Right"},
|
||||
"Kakariko Well": {"Kakariko Well - Top", "Kakariko Well - Left", "Kakariko Well - Middle",
|
||||
"Kakariko Well - Right", "Kakariko Well - Bottom"},
|
||||
"Mini Moldorm Cave": {"Mini Moldorm Cave - Far Left", "Mini Moldorm Cave - Left", "Mini Moldorm Cave - Right",
|
||||
"Mini Moldorm Cave - Far Right", "Mini Moldorm Cave - Generous Guy"},
|
||||
"Paradox Cave": {"Paradox Cave Lower - Far Left", "Paradox Cave Lower - Left", "Paradox Cave Lower - Right",
|
||||
"Paradox Cave Lower - Far Right", "Paradox Cave Lower - Middle", "Paradox Cave Upper - Left",
|
||||
"Paradox Cave Upper - Right"},
|
||||
"Hype Cave": {"Hype Cave - Top", "Hype Cave - Middle Right", "Hype Cave - Middle Left",
|
||||
"Hype Cave - Bottom", "Hype Cave - Generous Guy"},
|
||||
"Hookshot Cave": {"Hookshot Cave - Top Right", "Hookshot Cave - Top Left", "Hookshot Cave - Bottom Right",
|
||||
"Hookshot Cave - Bottom Left"},
|
||||
"Hyrule Castle": {"Hyrule Castle - Boomerang Chest", "Hyrule Castle - Map Chest",
|
||||
"Hyrule Castle - Zelda's Chest", "Sewers - Dark Cross", "Sewers - Secret Room - Left",
|
||||
"Sewers - Secret Room - Middle", "Sewers - Secret Room - Right"},
|
||||
"Eastern Palace": {"Eastern Palace - Compass Chest", "Eastern Palace - Big Chest",
|
||||
"Eastern Palace - Cannonball Chest", "Eastern Palace - Big Key Chest",
|
||||
"Eastern Palace - Map Chest", "Eastern Palace - Boss"},
|
||||
"Desert Palace": {"Desert Palace - Big Chest", "Desert Palace - Torch", "Desert Palace - Map Chest",
|
||||
"Desert Palace - Compass Chest", "Desert Palace Big Key Chest", "Desert Palace - Boss"},
|
||||
"Tower of Hera": {"Tower of Hera - Basement Cage", "Tower of Hera - Map Chest", "Tower of Hera - Big Key Chest",
|
||||
"Tower of Hera - Compass Chest", "Tower of Hera - Big Chest", "Tower of Hera - Boss"},
|
||||
"Palace of Darkness": {"Palace of Darkness - Shooter Room", "Palace of Darkness - The Arena - Bridge",
|
||||
"Palace of Darkness - Stalfos Basement", "Palace of Darkness - Big Key Chest",
|
||||
"Palace of Darkness - The Arena - Ledge", "Palace of Darkness - Map Chest",
|
||||
"Palace of Darkness - Compass Chest", "Palace of Darkness - Dark Basement - Left",
|
||||
"Palace of Darkness - Dark Basement - Right", "Palace of Darkness - Dark Maze - Top",
|
||||
"Palace of Darkness - Dark Maze - Bottom", "Palace of Darkness - Big Chest",
|
||||
"Palace of Darkness - Harmless Hellway", "Palace of Darkness - Boss"},
|
||||
"Swamp Palace": {"Swamp Palace - Entrance", "Swamp Palace - Swamp Palace - Map Chest",
|
||||
"Swamp Palace - Big Chest", "Swamp Palace - Compass Chest", "Swamp Palace - Big Key Chest",
|
||||
"Swamp Palace - West Chest", "Swamp Palace - Flooded Room - Left",
|
||||
"Swamp Palace - Flooded Room - Right", "Swamp Palace - Waterfall Room", "Swamp Palace - Boss"},
|
||||
"Thieves' Town": {"Thieves' Town - Big Key Chest", "Thieves' Town - Map Chest", "Thieves' Town - Compass Chest",
|
||||
"Thieves' Town - Ambush Chest", "Thieves' Town - Attic", "Thieves' Town - Big Chest",
|
||||
"Thieves' Town - Blind's Cell", "Thieves' Town - Boss"},
|
||||
"Skull Woods": {"Skull Woods - Map Chest", "Skull Woods - Pinball Room", "Skull Woods - Compass Chest",
|
||||
"Skull Woods - Pot Prison", "Skull Woods - Big Chest", "Skull Woods - Big Key Chest",
|
||||
"Skull Woods - Bridge Room", "Skull Woods - Boss"},
|
||||
"Ice Palace": {"Ice Palace - Compass Chest", "Ice Palace - Freezor Chest", "Ice Palace - Big Chest",
|
||||
"Ice Palace - Freezor Chest", "Ice Palace - Big Chest", "Ice Palace - Iced T Room",
|
||||
"Ice Palace - Spike Room", "Ice Palace - Big Key Chest", "Ice Palace - Map Chest",
|
||||
"Ice Palace - Boss"},
|
||||
"Misery Mire": {"Misery Mire - Big Chest", "Misery Mire - Map Chest", "Misery Mire - Main Lobby",
|
||||
"Misery Mire - Bridge Chest", "Misery Mire - Spike Chest", "Misery Mire - Compass Chest",
|
||||
"Misery Mire - Big Key Chest", "Misery Mire - Boss"},
|
||||
"Turtle Rock": {"Turtle Rock - Compass Chest", "Turtle Rock - Roller Room - Left",
|
||||
"Turtle Rock - Roller Room - Right", "Turtle Room - Chain Chomps", "Turtle Rock - Big Key Chest",
|
||||
"Turtle Rock - Big Chest", "Turtle Rock - Crystaroller Room",
|
||||
"Turtle Rock - Eye Bridge - Bottom Left", "Turtle Rock - Eye Bridge - Bottom Right",
|
||||
"Turtle Rock - Eye Bridge - Top Left", "Turtle Rock - Eye Bridge - Top Right", "Turtle Rock - Boss"},
|
||||
"Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganon's Tower - Hope Room - Left",
|
||||
"Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room",
|
||||
"Ganons Tower - Compass Room - Top Left", "Ganons Tower - Compass Room - Top Right",
|
||||
"Ganons Tower - Compass Room - Bottom Left", "Ganons Tower - Compass Room - Bottom Left",
|
||||
"Ganons Tower - DMs Room - Top Left", "Ganons Tower - DMs Room - Top Right",
|
||||
"Ganons Tower - DMs Room - Bottom Left", "Ganons Tower - DMs Room - Bottom Right",
|
||||
"Ganons Tower - Map Chest", "Ganons Tower - Firesnake Room",
|
||||
"Ganons Tower - Randomizer Room - Top Left", "Ganons Tower - Randomizer Room - Top Right",
|
||||
"Ganons Tower - Randomizer Room - Bottom Left", "Ganons Tower - Randomizer Room - Bottom Right",
|
||||
"Ganons Tower - Bob's Chest", "Ganons Tower - Big Chest", "Ganons Tower - Big Key Room - Left",
|
||||
"Ganons Tower - Big Key Room - Right", "Ganons Tower - Big Key Chest",
|
||||
"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right",
|
||||
"Ganons Tower - Pre-Moldorm Room", "Ganons Tower - Validation Chest"},
|
||||
"Ganons Tower Climb": {"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right",
|
||||
"Ganons Tower - Pre-Moldorm Room", "Ganons Tower - Validation Chest"},
|
||||
}
|
||||
hint_blacklist = {"Triforce"}
|
||||
|
||||
item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int}
|
||||
@@ -520,6 +591,122 @@ class ALTTPWorld(World):
|
||||
else:
|
||||
logging.warning(f"Could not trash fill Ganon's Tower for player {player}.")
|
||||
|
||||
def write_spoiler_header(self, spoiler_handle: typing.TextIO) -> None:
|
||||
def bool_to_text(variable: typing.Union[bool, str]) -> str:
|
||||
if type(variable) == str:
|
||||
return variable
|
||||
return "Yes" if variable else "No"
|
||||
|
||||
spoiler_handle.write('Logic: %s\n' % self.multiworld.logic[self.player])
|
||||
spoiler_handle.write('Dark Room Logic: %s\n' % self.multiworld.dark_room_logic[self.player])
|
||||
spoiler_handle.write('Mode: %s\n' % self.multiworld.mode[self.player])
|
||||
spoiler_handle.write('Goal: %s\n' % self.multiworld.goal[self.player])
|
||||
if "triforce" in self.multiworld.goal[self.player]: # triforce hunt
|
||||
spoiler_handle.write("Pieces available for Triforce: %s\n" %
|
||||
self.multiworld.triforce_pieces_available[self.player])
|
||||
spoiler_handle.write("Pieces required for Triforce: %s\n" %
|
||||
self.multiworld.triforce_pieces_required[self.player])
|
||||
spoiler_handle.write('Difficulty: %s\n' % self.multiworld.difficulty[self.player])
|
||||
spoiler_handle.write('Item Functionality: %s\n' % self.multiworld.item_functionality[self.player])
|
||||
spoiler_handle.write('Entrance Shuffle: %s\n' % self.multiworld.shuffle[self.player])
|
||||
if self.multiworld.shuffle[self.player] != "vanilla":
|
||||
spoiler_handle.write('Entrance Shuffle Seed %s\n' % self.er_seed)
|
||||
spoiler_handle.write('Shop inventory shuffle: %s\n' %
|
||||
bool_to_text("i" in self.multiworld.shop_shuffle[self.player]))
|
||||
spoiler_handle.write('Shop price shuffle: %s\n' %
|
||||
bool_to_text("p" in self.multiworld.shop_shuffle[self.player]))
|
||||
spoiler_handle.write('Shop upgrade shuffle: %s\n' %
|
||||
bool_to_text("u" in self.multiworld.shop_shuffle[self.player]))
|
||||
spoiler_handle.write('New Shop inventory: %s\n' %
|
||||
bool_to_text("g" in self.multiworld.shop_shuffle[self.player] or
|
||||
"f" in self.multiworld.shop_shuffle[self.player]))
|
||||
spoiler_handle.write('Custom Potion Shop: %s\n' %
|
||||
bool_to_text("w" in self.multiworld.shop_shuffle[self.player]))
|
||||
spoiler_handle.write('Enemy health: %s\n' % self.multiworld.enemy_health[self.player])
|
||||
spoiler_handle.write('Enemy damage: %s\n' % self.multiworld.enemy_damage[self.player])
|
||||
spoiler_handle.write('Prize shuffle %s\n' % self.multiworld.shuffle_prizes[self.player])
|
||||
|
||||
def write_spoiler(self, spoiler_handle: typing.TextIO) -> None:
|
||||
spoiler_handle.write("\n\nMedallions:\n")
|
||||
spoiler_handle.write(f"\nMisery Mire ({self.multiworld.get_player_name(self.player)}):"
|
||||
f" {self.multiworld.required_medallions[self.player][0]}")
|
||||
spoiler_handle.write(
|
||||
f"\nTurtle Rock ({self.multiworld.get_player_name(self.player)}):"
|
||||
f" {self.multiworld.required_medallions[self.player][1]}")
|
||||
|
||||
if self.multiworld.boss_shuffle[self.player] != "none":
|
||||
def create_boss_map() -> typing.Dict:
|
||||
boss_map = {
|
||||
"Eastern Palace": self.multiworld.get_dungeon("Eastern Palace", self.player).boss.name,
|
||||
"Desert Palace": self.multiworld.get_dungeon("Desert Palace", self.player).boss.name,
|
||||
"Tower Of Hera": self.multiworld.get_dungeon("Tower of Hera", self.player).boss.name,
|
||||
"Hyrule Castle": "Agahnim",
|
||||
"Palace Of Darkness": self.multiworld.get_dungeon("Palace of Darkness",
|
||||
self.player).boss.name,
|
||||
"Swamp Palace": self.multiworld.get_dungeon("Swamp Palace", self.player).boss.name,
|
||||
"Skull Woods": self.multiworld.get_dungeon("Skull Woods", self.player).boss.name,
|
||||
"Thieves Town": self.multiworld.get_dungeon("Thieves Town", self.player).boss.name,
|
||||
"Ice Palace": self.multiworld.get_dungeon("Ice Palace", self.player).boss.name,
|
||||
"Misery Mire": self.multiworld.get_dungeon("Misery Mire", self.player).boss.name,
|
||||
"Turtle Rock": self.multiworld.get_dungeon("Turtle Rock", self.player).boss.name,
|
||||
"Ganons Tower": "Agahnim 2",
|
||||
"Ganon": "Ganon"
|
||||
}
|
||||
if self.multiworld.mode[self.player] != 'inverted':
|
||||
boss_map.update({
|
||||
"Ganons Tower Basement":
|
||||
self.multiworld.get_dungeon("Ganons Tower", self.player).bosses["bottom"].name,
|
||||
"Ganons Tower Middle": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[
|
||||
"middle"].name,
|
||||
"Ganons Tower Top": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[
|
||||
"top"].name
|
||||
})
|
||||
else:
|
||||
boss_map.update({
|
||||
"Ganons Tower Basement": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["bottom"].name,
|
||||
"Ganons Tower Middle": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["middle"].name,
|
||||
"Ganons Tower Top": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["top"].name
|
||||
})
|
||||
return boss_map
|
||||
|
||||
bossmap = create_boss_map()
|
||||
spoiler_handle.write(
|
||||
f'\n\nBosses{(f" ({self.multiworld.get_player_name(self.player)})" if self.multiworld.players > 1 else "")}:\n')
|
||||
spoiler_handle.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
|
||||
|
||||
def build_shop_info(shop: Shop) -> typing.Dict[str, str]:
|
||||
shop_data = {
|
||||
"location": str(shop.region),
|
||||
"type": "Take Any" if shop.type == ShopType.TakeAny else "Shop"
|
||||
}
|
||||
|
||||
for index, item in enumerate(shop.inventory):
|
||||
if item is None:
|
||||
continue
|
||||
price = item["price"] // price_rate_display.get(item["price_type"], 1)
|
||||
shop_data["item_{}".format(index)] = f"{item['item']} - {price} {price_type_display_name[item['price_type']]}"
|
||||
if item["player"]:
|
||||
shop_data["item_{}".format(index)] =\
|
||||
shop_data["item_{}".format(index)].replace("—", "(Player {}) — ".format(item["player"]))
|
||||
|
||||
if item["max"] == 0:
|
||||
continue
|
||||
shop_data["item_{}".format(index)] += " x {}".format(item["max"])
|
||||
if item["replacement"] is None:
|
||||
continue
|
||||
shop_data["item_{}".format(index)] +=\
|
||||
f", {item['replacement']} - {item['replacement_price']}" \
|
||||
f" {price_type_display_name[item['replacement_price_type']]}"
|
||||
|
||||
return shop_data
|
||||
|
||||
if shop_info := [build_shop_info(shop) for shop in self.multiworld.shops if shop.custom]:
|
||||
spoiler_handle.write('\n\nShops:\n\n')
|
||||
for shop_data in shop_info:
|
||||
spoiler_handle.write("{} [{}]\n {}\n".format(shop_data['location'], shop_data['type'], "\n ".join(
|
||||
item for item in [shop_data.get('item_0', None), shop_data.get('item_1', None), shop_data.get('item_2', None)] if
|
||||
item)))
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
if self.multiworld.goal[self.player] == "icerodhunt":
|
||||
item = "Nothing"
|
||||
@@ -552,5 +739,5 @@ class ALttPLogic(LogicMixin):
|
||||
if self.multiworld.logic[player] == 'nologic':
|
||||
return True
|
||||
if self.multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
return self.can_buy_unlimited('Small Key (Universal)', player)
|
||||
return can_buy_unlimited(self, 'Small Key (Universal)', player)
|
||||
return self.prog_items[item, player] >= count
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
import Generate
|
||||
from BaseClasses import PlandoOptions
|
||||
from Options import PlandoBosses
|
||||
|
||||
|
||||
@@ -123,14 +123,14 @@ class TestPlandoBosses(unittest.TestCase):
|
||||
regular = MultiBosses.from_any(regular_string)
|
||||
|
||||
# plando should work with boss plando
|
||||
plandoed.verify(None, "Player", Generate.PlandoOptions.bosses)
|
||||
plandoed.verify(None, "Player", PlandoOptions.bosses)
|
||||
self.assertTrue(plandoed.value.startswith(plandoed_string))
|
||||
# plando should fall back to default without boss plando
|
||||
plandoed.verify(None, "Player", Generate.PlandoOptions.items)
|
||||
plandoed.verify(None, "Player", PlandoOptions.items)
|
||||
self.assertEqual(plandoed, MultiBosses.option_vanilla)
|
||||
# mixed should fall back to mode
|
||||
mixed.verify(None, "Player", Generate.PlandoOptions.items) # should produce a warning and still work
|
||||
mixed.verify(None, "Player", PlandoOptions.items) # should produce a warning and still work
|
||||
self.assertEqual(mixed, MultiBosses.option_shuffle)
|
||||
# mode stuff should just work
|
||||
regular.verify(None, "Player", Generate.PlandoOptions.items)
|
||||
regular.verify(None, "Player", PlandoOptions.items)
|
||||
self.assertEqual(regular, MultiBosses.option_shuffle)
|
||||
|
||||
@@ -31,3 +31,7 @@ def set_rules(world: MultiWorld, player: int):
|
||||
world.get_location(f"IDLE for at least {int(i / 2)} minutes {30 if (i % 2) else 0} seconds", player),
|
||||
lambda state: state._archipidle_location_is_accessible(player, 20)
|
||||
)
|
||||
|
||||
world.completion_condition[player] =\
|
||||
lambda state:\
|
||||
state.can_reach(world.get_location("IDLE for at least 50 minutes 0 seconds", player), "Location", player)
|
||||
|
||||
135
worlds/blasphemous/Exits.py
Normal file
135
worlds/blasphemous/Exits.py
Normal file
@@ -0,0 +1,135 @@
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
region_exit_table: Dict[str, List[str]] = {
|
||||
"menu" : ["New Game"],
|
||||
|
||||
"albero" : ["To The Holy Line",
|
||||
"To Desecrated Cistern",
|
||||
"To Wasteland of the Buried Churches",
|
||||
"To Dungeons"],
|
||||
|
||||
"attots" : ["To Mother of Mothers"],
|
||||
|
||||
"ar" : ["To Mother of Mothers",
|
||||
"To Wall of the Holy Prohibitions",
|
||||
"To Deambulatory of His Holiness"],
|
||||
|
||||
"bottc" : ["To Wasteland of the Buried Churches",
|
||||
"To Ferrous Tree"],
|
||||
|
||||
"botss" : ["To The Holy Line",
|
||||
"To Mountains of the Endless Dusk"],
|
||||
|
||||
"coolotcv" : ["To Graveyard of the Peaks",
|
||||
"To Wall of the Holy Prohibitions"],
|
||||
|
||||
"dohh" : ["To Archcathedral Rooftops"],
|
||||
|
||||
"dc" : ["To Albero",
|
||||
"To Mercy Dreams",
|
||||
"To Mountains of the Endless Dusk",
|
||||
"To Echoes of Salt",
|
||||
"To Grievance Ascends"],
|
||||
|
||||
"eos" : ["To Jondo",
|
||||
"To Mountains of the Endless Dusk",
|
||||
"To Desecrated Cistern",
|
||||
"To The Resting Place of the Sister",
|
||||
"To Mourning and Havoc"],
|
||||
|
||||
"ft" : ["To Bridge of the Three Cavalries",
|
||||
"To Hall of the Dawning",
|
||||
"To Patio of the Silent Steps"],
|
||||
|
||||
"gotp" : ["To Where Olive Trees Wither",
|
||||
"To Convent of Our Lady of the Charred Visage"],
|
||||
|
||||
"ga" : ["To Jondo",
|
||||
"To Desecrated Cistern"],
|
||||
|
||||
"hotd" : ["To Ferrous Tree"],
|
||||
|
||||
"jondo" : ["To Mountains of the Endless Dusk",
|
||||
"To Grievance Ascends"],
|
||||
|
||||
"kottw" : ["To Mother of Mothers"],
|
||||
|
||||
"lotnw" : ["To Mother of Mothers",
|
||||
"To The Sleeping Canvases"],
|
||||
|
||||
"md" : ["To Wasteland of the Buried Churches",
|
||||
"To Desecrated Cistern",
|
||||
"To The Sleeping Canvases"],
|
||||
|
||||
"mom" : ["To Patio of the Silent Steps",
|
||||
"To Archcathedral Rooftops",
|
||||
"To Knot of the Three Words",
|
||||
"To Library of the Negated Words",
|
||||
"To All the Tears of the Sea"],
|
||||
|
||||
"moted" : ["To Brotherhood of the Silent Sorrow",
|
||||
"To Jondo",
|
||||
"To Desecrated Cistern"],
|
||||
|
||||
"mah" : ["To Echoes of Salt",
|
||||
"To Mother of Mothers"],
|
||||
|
||||
"potss" : ["To Ferrous Tree",
|
||||
"To Mother of Mothers",
|
||||
"To Wall of the Holy Prohibitions"],
|
||||
|
||||
"petrous" : ["To The Holy Line"],
|
||||
|
||||
"thl" : ["To Brotherhood of the Silent Sorrow",
|
||||
"To Petrous",
|
||||
"To Albero"],
|
||||
|
||||
"trpots" : ["To Echoes of Salt"],
|
||||
|
||||
"tsc" : ["To Library of the Negated Words",
|
||||
"To Mercy Dreams"],
|
||||
|
||||
"wothp" : ["To Archcathedral Rooftops",
|
||||
"To Convent of Our Lady of the Charred Visage"],
|
||||
|
||||
"wotbc" : ["To Albero",
|
||||
"To Where Olive Trees Wither",
|
||||
"To Mercy Dreams"],
|
||||
|
||||
"wotw" : ["To Wasteland of the Buried Churches",
|
||||
"To Graveyard of the Peaks"]
|
||||
}
|
||||
|
||||
exit_lookup_table: Dict[str, str] = {
|
||||
"New Game": "botss",
|
||||
"To Albero": "albero",
|
||||
"To All the Tears of the Sea": "attots",
|
||||
"To Archcathedral Rooftops": "ar",
|
||||
"To Bridge of the Three Cavalries": "bottc",
|
||||
"To Brotherhood of the Silent Sorrow": "botss",
|
||||
"To Convent of Our Lady of the Charred Visage": "coolotcv",
|
||||
"To Deambulatory of His Holiness": "dohh",
|
||||
"To Desecrated Cistern": "dc",
|
||||
"To Echoes of Salt": "eos",
|
||||
"To Ferrous Tree": "ft",
|
||||
"To Graveyard of the Peaks": "gotp",
|
||||
"To Grievance Ascends": "ga",
|
||||
"To Hall of the Dawning": "hotd",
|
||||
"To Jondo": "jondo",
|
||||
"To Knot of the Three Words": "kottw",
|
||||
"To Library of the Negated Words": "lotnw",
|
||||
"To Mercy Dreams": "md",
|
||||
"To Mother of Mothers": "mom",
|
||||
"To Mountains of the Endless Dusk": "moted",
|
||||
"To Mourning and Havoc": "mah",
|
||||
"To Patio of the Silent Steps": "potss",
|
||||
"To Petrous": "petrous",
|
||||
"To The Holy Line": "thl",
|
||||
"To The Resting Place of the Sister": "trpots",
|
||||
"To The Sleeping Canvases": "tsc",
|
||||
"To Wall of the Holy Prohibitions": "wothp",
|
||||
"To Wasteland of the Buried Churches": "wotbc",
|
||||
"To Where Olive Trees Wither": "wotw",
|
||||
"To Dungeons": "dungeon"
|
||||
}
|
||||
754
worlds/blasphemous/Items.py
Normal file
754
worlds/blasphemous/Items.py
Normal file
@@ -0,0 +1,754 @@
|
||||
from BaseClasses import ItemClassification
|
||||
from typing import TypedDict, Dict, List, Set
|
||||
|
||||
|
||||
class ItemDict(TypedDict):
|
||||
name: str
|
||||
count: int
|
||||
classification: ItemClassification
|
||||
|
||||
base_id = 1909000
|
||||
|
||||
item_table: List[ItemDict] = [
|
||||
# Rosary Beads
|
||||
{'name': "Dove Skull",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Ember of the Holy Cremation",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Silver Grape",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Uvula of Proclamation",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Hollow Pearl",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Knot of Hair",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Painted Wood Bead",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Piece of a Golden Mask",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Moss Preserved in Glass",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Frozen Olive",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Quirce's Scorched Bead",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Wicker Knot",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Perpetva's Protection",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Thorned Symbol",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Piece of a Tombstone",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Sphere of the Sacred Smoke",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Bead of Red Wax",
|
||||
'count': 3,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Little Toe made of Limestone",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Big Toe made of Limestone",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Fourth Toe made of Limestone",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Bead of Blue Wax",
|
||||
'count': 3,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Pelican Effigy",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Drop of Coagulated Ink",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Amber Eye",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Muted Bell",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Consecrated Amethyst",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Embers of a Broken Star",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Scaly Coin",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Seashell of the Inverted Spiral",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Calcified Eye of Erudition",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Weight of True Guilt",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Reliquary of the Fervent Heart",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Reliquary of the Suffering Heart",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Reliquary of the Sorrowful Heart",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Token of Appreciation",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Cloistered Ruby",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Bead of Gold Thread",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Cloistered Sapphire",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Fire Enclosed in Enamel",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Light of the Lady of the Lamp",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Scale of Burnished Alabaster",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "The Young Mason's Wheel",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Crown of Gnawed Iron",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Crimson Heart of a Miura",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
|
||||
# Prayers
|
||||
{'name': "Seguiriya to your Eyes like Stars",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Debla of the Lights",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Saeta Dolorosa",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Campanillero to the Sons of the Aurora",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Lorquiana",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Zarabanda of the Safe Haven",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Taranto to my Sister",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Solea of Excommunication",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Tiento to your Thorned Hairs",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Cante Jondo of the Three Sisters",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Verdiales of the Forsaken Hamlet",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Romance to the Crimson Mist",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Zambra to the Resplendent Crown",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Aubade of the Nameless Guardian",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Cantina of the Blue Rose",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Mirabras of the Return to Port",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Tirana of the Celestial Bastion",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
|
||||
# Relics
|
||||
{'name': "Blood Perpetuated in Sand",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Incorrupt Hand of the Fraternal Master",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Nail Uprooted from Dirt",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Shroud of Dreamt Sins",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Linen of Golden Thread",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Silvered Lung of Dolphos",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Three Gnarled Tongues",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
|
||||
# Mea Culpa Hearts
|
||||
{'name': "Smoking Heart of Incense",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Heart of the Virtuous Pain",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Heart of Saltpeter Blood",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Heart of Oils",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Heart of Cerulean Incense",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Heart of the Holy Purge",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Molten Heart of Boiling Blood",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Heart of the Single Tone",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Heart of the Unnamed Minstrel",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Brilliant Heart of Dawn",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Apodictic Heart of Mea Culpa",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
|
||||
# Quest Items
|
||||
{'name': "Cord of the True Burying",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Mark of the First Refuge",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Mark of the Second Refuge",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Mark of the Third Refuge",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Tentudia's Carnal Remains",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Remains of Tentudia's Hair",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Tentudia's Skeletal Remains",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Melted Golden Coins",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Torn Bridal Ribbon",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Black Grieving Veil",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Egg of Deformity",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Hatched Egg of Deformity",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Bouquet of Rosemary",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Incense Garlic",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Thorn Upgrade",
|
||||
'count': 8,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Olive Seeds",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Holy Wound of Attrition",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Holy Wound of Contrition",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Holy Wound of Compunction",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Empty Bile Vessel",
|
||||
'count': 8,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Knot of Rosary Rope",
|
||||
'count': 6,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Golden Thimble Filled with Burning Oil",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Key to the Chamber of the Eldest Brother",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Empty Golden Thimble",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Deformed Mask of Orestes",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Mirrored Mask of Dolphos",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Embossed Mask of Crescente",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Dried Clove",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Sooty Garlic",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Bouquet of Thyme",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Linen Cloth",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Severed Hand",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Dried Flowers bathed in Tears",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Key of the Secular",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Key of the Scribe",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Key of the Inquisitor",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Key of the High Peaks",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Chalice of Inverted Verses",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Quicksilver",
|
||||
'count': 5,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Petrified Bell",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Verses Spun from Gold",
|
||||
'count': 4,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Severed Right Eye of the Traitor",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Broken Left Eye of the Traitor",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Incomplete Scapular",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Key Grown from Twisted Wood",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Holy Wound of Abnegation",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
|
||||
# Skills
|
||||
{'name': "Combo Skill",
|
||||
'count': 3,
|
||||
'classification': ItemClassification.useful},
|
||||
{'name': "Charged Skill",
|
||||
'count': 3,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Ranged Skill",
|
||||
'count': 3,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Dive Skill",
|
||||
'count': 3,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Lunge Skill",
|
||||
'count': 3,
|
||||
'classification': ItemClassification.useful},
|
||||
|
||||
# Other
|
||||
{'name': "Parietal bone of Lasser, the Inquisitor",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Jaw of Ashgan, the Inquisitor",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Cervical vertebra of Zicher, the Brewmaster",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Clavicle of Dalhuisen, the Schoolchild",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Sternum of Vitas, the Performer",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Ribs of Sabnock, the Guardian",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Vertebra of John, the Gambler",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Scapula of Carlos, the Executioner",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Humerus of McMittens, the Nurse",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Ulna of Koke, the Troubadour",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Radius of Helzer, the Poet",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Frontal of Martinus, the Ropemaker",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Metacarpus of Hodges, the Blacksmith",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Phalanx of Arthur, the Sailor",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Phalanx of Miriam, the Counsellor",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Phalanx of Brannon, the Gravedigger",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Coxal of June, the Prostitute",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Sacrum of the Dark Warlock",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Coccyx of Daniel, the Possessed",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Femur of Karpow, the Bounty Hunter",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Kneecap of Sebastien, the Puppeteer",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Tibia of Alsahli, the Mystic",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Fibula of Rysp, the Ranger",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Temporal of Joel, the Thief",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Metatarsus of Rikusyo, the Traveller",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Phalanx of Zeth, the Prisoner",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Phalanx of William, the Sceptic",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Phalanx of Aralcarim, the Archivist",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Occipital of Tequila, the Metalsmith",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Maxilla of Tarradax, the Cleric",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Nasal bone of Charles, the Artist",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Hyoid bone of Senex, the Beggar",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Vertebra of Lindquist, the Forger",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Trapezium of Jeremiah, the Hangman",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Trapezoid of Yeager, the Jeweller",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Capitate of Barock, the Herald",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Hamate of Vukelich, the Copyist",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Pisiform of Hernandez, the Explorer",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Triquetral of Luca, the Tailor",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Lunate of Keiya, the Butcher",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Scaphoid of Fierce, the Leper",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Anklebone of Weston, the Pilgrim",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Calcaneum of Persian, the Bandit",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Navicular of Kahnnyhoo, the Murderer",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Child of Moonlight",
|
||||
'count': 38,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Life Upgrade",
|
||||
'count': 6,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Fervour Upgrade",
|
||||
'count': 6,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Mea Culpa Upgrade",
|
||||
'count': 7,
|
||||
'classification': ItemClassification.progression},
|
||||
{'name': "Tears of Atonement (250)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (300)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (500)",
|
||||
'count': 3,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (625)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (750)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (1000)",
|
||||
'count': 4,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (1250)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (1500)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (1750)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (2000)",
|
||||
'count': 2,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (2100)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (2500)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (2600)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (3000)",
|
||||
'count': 2,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (4300)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (5000)",
|
||||
'count': 4,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (5500)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (9000)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (10000)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (11250)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (18000)",
|
||||
'count': 5,
|
||||
'classification': ItemClassification.filler},
|
||||
{'name': "Tears of Atonement (30000)",
|
||||
'count': 1,
|
||||
'classification': ItemClassification.filler}
|
||||
]
|
||||
|
||||
group_table: Dict[str, Set[str]] = {
|
||||
"wounds" : ["Holy Wound of Attrition",
|
||||
"Holy Wound of Contrition",
|
||||
"Holy Wound of Compunction"],
|
||||
|
||||
"masks" : ["Deformed Mask of Orestes",
|
||||
"Mirrored Mask of Dolphos",
|
||||
"Embossed Mask of Crescente"],
|
||||
|
||||
"tirso" : ["Bouquet of Rosemary",
|
||||
"Incense Garlic",
|
||||
"Olive Seeds",
|
||||
"Dried Clove",
|
||||
"Sooty Garlic",
|
||||
"Bouquet of Thyme"],
|
||||
|
||||
"tentudia": ["Tentudia's Carnal Remains",
|
||||
"Remains of Tentudia's Hair",
|
||||
"Tentudia's Skeletal Remains"],
|
||||
|
||||
"egg" : ["Melted Golden Coins",
|
||||
"Torn Bridal Ribbon",
|
||||
"Black Grieving Veil"],
|
||||
|
||||
"bones" : ["Parietal bone of Lasser, the Inquisitor",
|
||||
"Jaw of Ashgan, the Inquisitor",
|
||||
"Cervical vertebra of Zicher, the Brewmaster",
|
||||
"Clavicle of Dalhuisen, the Schoolchild",
|
||||
"Sternum of Vitas, the Performer",
|
||||
"Ribs of Sabnock, the Guardian",
|
||||
"Vertebra of John, the Gambler",
|
||||
"Scapula of Carlos, the Executioner",
|
||||
"Humerus of McMittens, the Nurse",
|
||||
"Ulna of Koke, the Troubadour",
|
||||
"Radius of Helzer, the Poet",
|
||||
"Frontal of Martinus, the Ropemaker",
|
||||
"Metacarpus of Hodges, the Blacksmith",
|
||||
"Phalanx of Arthur, the Sailor",
|
||||
"Phalanx of Miriam, the Counsellor",
|
||||
"Phalanx of Brannon, the Gravedigger",
|
||||
"Coxal of June, the Prostitute",
|
||||
"Sacrum of the Dark Warlock",
|
||||
"Coccyx of Daniel, the Possessed",
|
||||
"Femur of Karpow, the Bounty Hunter",
|
||||
"Kneecap of Sebastien, the Puppeteer",
|
||||
"Tibia of Alsahli, the Mystic",
|
||||
"Fibula of Rysp, the Ranger",
|
||||
"Temporal of Joel, the Thief",
|
||||
"Metatarsus of Rikusyo, the Traveller",
|
||||
"Phalanx of Zeth, the Prisoner",
|
||||
"Phalanx of William, the Sceptic",
|
||||
"Phalanx of Aralcarim, the Archivist",
|
||||
"Occipital of Tequila, the Metalsmith",
|
||||
"Maxilla of Tarradax, the Cleric",
|
||||
"Nasal bone of Charles, the Artist",
|
||||
"Hyoid bone of Senex, the Beggar",
|
||||
"Vertebra of Lindquist, the Forger",
|
||||
"Trapezium of Jeremiah, the Hangman",
|
||||
"Trapezoid of Yeager, the Jeweller",
|
||||
"Capitate of Barock, the Herald",
|
||||
"Hamate of Vukelich, the Copyist",
|
||||
"Pisiform of Hernandez, the Explorer",
|
||||
"Triquetral of Luca, the Tailor",
|
||||
"Lunate of Keiya, the Butcher",
|
||||
"Scaphoid of Fierce, the Leper",
|
||||
"Anklebone of Weston, the Pilgrim",
|
||||
"Calcaneum of Persian, the Bandit",
|
||||
"Navicular of Kahnnyhoo, the Murderer"],
|
||||
|
||||
"power" : ["Life Upgrade",
|
||||
"Fervour Upgrade",
|
||||
"Empty Bile Vessel",
|
||||
"Quicksilver"],
|
||||
|
||||
"prayer" : ["Seguiriya to your Eyes like Stars",
|
||||
"Debla of the Lights",
|
||||
"Saeta Dolorosa",
|
||||
"Campanillero to the Sons of the Aurora",
|
||||
"Lorquiana",
|
||||
"Zarabanda of the Safe Haven",
|
||||
"Taranto to my Sister",
|
||||
"Solea of Excommunication",
|
||||
"Tiento to your Thorned Hairs",
|
||||
"Cante Jondo of the Three Sisters",
|
||||
"Verdiales of the Forsaken Hamlet",
|
||||
"Romance to the Crimson Mist",
|
||||
"Zambra to the Resplendent Crown",
|
||||
"Cantina of the Blue Rose",
|
||||
"Mirabras of the Return to Port"]
|
||||
}
|
||||
|
||||
tears_set: Set[str] = [
|
||||
"Tears of Atonement (500)",
|
||||
"Tears of Atonement (625)",
|
||||
"Tears of Atonement (750)",
|
||||
"Tears of Atonement (1000)",
|
||||
"Tears of Atonement (1250)",
|
||||
"Tears of Atonement (1500)",
|
||||
"Tears of Atonement (1750)",
|
||||
"Tears of Atonement (2000)",
|
||||
"Tears of Atonement (2100)",
|
||||
"Tears of Atonement (2500)",
|
||||
"Tears of Atonement (2600)",
|
||||
"Tears of Atonement (3000)",
|
||||
"Tears of Atonement (4300)",
|
||||
"Tears of Atonement (5000)",
|
||||
"Tears of Atonement (5500)",
|
||||
"Tears of Atonement (9000)",
|
||||
"Tears of Atonement (10000)",
|
||||
"Tears of Atonement (11250)",
|
||||
"Tears of Atonement (18000)",
|
||||
"Tears of Atonement (30000)"
|
||||
]
|
||||
|
||||
reliquary_set: Set[str] = [
|
||||
"Reliquary of the Fervent Heart",
|
||||
"Reliquary of the Suffering Heart",
|
||||
"Reliquary of the Sorrowful Heart"
|
||||
]
|
||||
|
||||
skill_set: Set[str] = [
|
||||
"Combo Skill",
|
||||
"Charged Skill",
|
||||
"Ranged Skill",
|
||||
"Dive Skill",
|
||||
"Lunge Skill"
|
||||
]
|
||||
1295
worlds/blasphemous/Locations.py
Normal file
1295
worlds/blasphemous/Locations.py
Normal file
File diff suppressed because it is too large
Load Diff
257
worlds/blasphemous/Options.py
Normal file
257
worlds/blasphemous/Options.py
Normal file
@@ -0,0 +1,257 @@
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink
|
||||
|
||||
|
||||
class PrieDieuWarp(DefaultOnToggle):
|
||||
"""Automatically unlocks the ability to warp between Prie Dieu shrines."""
|
||||
display_name = "Unlock Fast Travel"
|
||||
|
||||
|
||||
class SkipCutscenes(DefaultOnToggle):
|
||||
"""Automatically skips most cutscenes."""
|
||||
display_name = "Auto Skip Cutscenes"
|
||||
|
||||
|
||||
class CorpseHints(DefaultOnToggle):
|
||||
"""Changes the 34 corpses in game to give various hints about item locations."""
|
||||
display_name = "Corpse Hints"
|
||||
|
||||
|
||||
class Difficulty(Choice):
|
||||
"""Adjusts the logic required to defeat bosses.
|
||||
Impossible: Removes all logic requirements for bosses. Good luck."""
|
||||
display_name = "Difficulty"
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
option_impossible = 3
|
||||
default = 1
|
||||
|
||||
|
||||
class Penitence(Toggle):
|
||||
"""Allows one of the three Penitences to be chosen at the beginning of the game."""
|
||||
display_name = "Penitence"
|
||||
|
||||
|
||||
class ExpertLogic(Toggle):
|
||||
"""Expands the logic used by the randomizer to allow for some difficult and/or lesser known tricks."""
|
||||
display_name = "Expert Logic"
|
||||
|
||||
|
||||
class Ending(Choice):
|
||||
"""Choose which ending is required to complete the game."""
|
||||
display_name = "Ending"
|
||||
option_any_ending = 0
|
||||
option_ending_b = 1
|
||||
option_ending_c = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class ThornShuffle(Choice):
|
||||
"""Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool."""
|
||||
display_name = "Shuffle Thorn"
|
||||
option_anywhere = 0
|
||||
option_local_only = 1
|
||||
option_vanilla = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class ReliquaryShuffle(DefaultOnToggle):
|
||||
"""Adds the True Torment exclusive Reliquary rosary beads into the item pool."""
|
||||
display_name = "Shuffle Penitence Rewards"
|
||||
|
||||
|
||||
class CherubShuffle(DefaultOnToggle):
|
||||
"""Shuffles Children of Moonlight into the item pool."""
|
||||
display_name = "Shuffle Children of Moonlight"
|
||||
|
||||
|
||||
class LifeShuffle(DefaultOnToggle):
|
||||
"""Shuffles life upgrades from the Lady of the Six Sorrows into the item pool."""
|
||||
display_name = "Shuffle Life Upgrades"
|
||||
|
||||
|
||||
class FervourShuffle(DefaultOnToggle):
|
||||
"""Shuffles fervour upgrades from the Oil of the Pilgrims into the item pool."""
|
||||
display_name = "Shuffle Fervour Upgrades"
|
||||
|
||||
|
||||
class SwordShuffle(DefaultOnToggle):
|
||||
"""Shuffles Mea Culpa upgrades from the Mea Culpa Altars into the item pool."""
|
||||
display_name = "Shuffle Mea Culpa Upgrades"
|
||||
|
||||
|
||||
class BlessingShuffle(DefaultOnToggle):
|
||||
"""Shuffles blessings from the Lake of Silent Pilgrims into the item pool."""
|
||||
display_name = "Shuffle Blessings"
|
||||
|
||||
|
||||
class DungeonShuffle(DefaultOnToggle):
|
||||
"""Shuffles rewards from completing Confessor Dungeons into the item pool."""
|
||||
display_name = "Shuffle Dungeon Rewards"
|
||||
|
||||
|
||||
class TirsoShuffle(DefaultOnToggle):
|
||||
"""Shuffles rewards from delivering herbs to Tirso into the item pool."""
|
||||
display_name = "Shuffle Tirso's Rewards"
|
||||
|
||||
|
||||
class MiriamShuffle(DefaultOnToggle):
|
||||
"""Shuffles the prayer given by Miriam into the item pool."""
|
||||
display_name = "Shuffle Miriram's Reward"
|
||||
|
||||
|
||||
class RedentoShuffle(DefaultOnToggle):
|
||||
"""Shuffles rewards from assisting Redento into the item pool."""
|
||||
display_name = "Shuffle Redento's Rewards"
|
||||
|
||||
|
||||
class JocineroShuffle(DefaultOnToggle):
|
||||
"""Shuffles rewards from rescuing 20 and 38 Children of Moonlight into the item pool."""
|
||||
display_name = "Shuffle Jocinero's Rewards"
|
||||
|
||||
|
||||
class AltasgraciasShuffle(DefaultOnToggle):
|
||||
"""Shuffles the reward given by Altasgracias and the item left behind by them into the item pool."""
|
||||
display_name = "Shuffle Altasgracias' Rewards"
|
||||
|
||||
|
||||
class TentudiaShuffle(DefaultOnToggle):
|
||||
"""Shuffles the rewards from delivering Tentudia's remains to Lvdovico into the item pool."""
|
||||
display_name = "Shuffle Lvdovico's Rewards"
|
||||
|
||||
|
||||
class GeminoShuffle(DefaultOnToggle):
|
||||
"""Shuffles the rewards from Gemino's quest and the hidden tomb into the item pool."""
|
||||
display_name = "Shuffle Gemino's Rewards"
|
||||
|
||||
|
||||
class GuiltShuffle(DefaultOnToggle):
|
||||
"""Shuffles the Weight of True Guilt into the item pool."""
|
||||
display_name = "Shuffle Immaculate Bead"
|
||||
|
||||
|
||||
class OssuaryShuffle(DefaultOnToggle):
|
||||
"""Shuffles the rewards from delivering bones to the Ossuary into the item pool."""
|
||||
display_name = "Shuffle Ossuary Rewards"
|
||||
|
||||
|
||||
class BossShuffle(DefaultOnToggle):
|
||||
"""Shuffles the Tears of Atonement from defeating bosses into the item pool."""
|
||||
display_name = "Shuffle Boss Tears"
|
||||
|
||||
|
||||
class WoundShuffle(DefaultOnToggle):
|
||||
"""Shuffles the Holy Wounds required to pass the Bridge of the Three Cavalries into the item pool."""
|
||||
display_name = "Shuffle Holy Wounds"
|
||||
|
||||
|
||||
class MaskShuffle(DefaultOnToggle):
|
||||
"""Shuffles the masks required to use the elevator in Archcathedral Rooftops into the item pool."""
|
||||
display_name = "Shuffle Masks"
|
||||
|
||||
|
||||
class EyeShuffle(DefaultOnToggle):
|
||||
"""Shuffles the Eyes of the Traitor from defeating Isidora and Sierpes into the item pool."""
|
||||
display_name = "Shuffle Traitor's Eyes"
|
||||
|
||||
|
||||
class HerbShuffle(DefaultOnToggle):
|
||||
"""Shuffles the herbs required for Tirso's quest into the item pool."""
|
||||
display_name = "Shuffle Herbs"
|
||||
|
||||
|
||||
class ChurchShuffle(DefaultOnToggle):
|
||||
"""Shuffles the rewards from donating 5,000 and 50,000 Tears of Atonement to the Church in Albero into the item pool."""
|
||||
display_name = "Shuffle Donation Rewards"
|
||||
|
||||
|
||||
class ShopShuffle(DefaultOnToggle):
|
||||
"""Shuffles the items sold in Candelaria's shops into the item pool."""
|
||||
display_name = "Shuffle Shop Items"
|
||||
|
||||
|
||||
class CandleShuffle(DefaultOnToggle):
|
||||
"""Shuffles the Beads of Wax and their upgrades into the item pool."""
|
||||
display_name = "Shuffle Candles"
|
||||
|
||||
|
||||
class StartWheel(Toggle):
|
||||
"""Changes the beginning gift to The Young Mason's Wheel."""
|
||||
display_name = "Start with Wheel"
|
||||
|
||||
|
||||
class SkillRando(Toggle):
|
||||
"""Randomizes the abilities from the skill tree into the item pool."""
|
||||
display_name = "Skill Randomizer"
|
||||
|
||||
|
||||
class EnemyRando(Choice):
|
||||
"""Randomizes the enemies that appear in each room.
|
||||
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in a standard game.
|
||||
Randomized: Every enemy is completely random, and can appear any number of times.
|
||||
Some enemies will never be randomized."""
|
||||
display_name = "Enemy Randomizer"
|
||||
option_disabled = 0
|
||||
option_shuffled = 1
|
||||
option_randomized = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class EnemyGroups(DefaultOnToggle):
|
||||
"""Randomized enemies will chosen from sets of specific groups.
|
||||
(Weak, normal, large, flying)
|
||||
Has no effect if Enemy Randomizer is disabled."""
|
||||
display_name = "Enemy Groups"
|
||||
|
||||
|
||||
class EnemyScaling(DefaultOnToggle):
|
||||
"""Randomized enemies will have their stats increased or decreased depending on the area they appear in.
|
||||
Has no effect if Enemy Randomizer is disabled."""
|
||||
display_name = "Enemy Scaling"
|
||||
|
||||
|
||||
class BlasphemousDeathLink(DeathLink):
|
||||
"""When you die, everyone dies. The reverse is also true.
|
||||
Note that Guilt Fragments will not appear when killed by Death Link."""
|
||||
|
||||
|
||||
blasphemous_options = {
|
||||
"prie_dieu_warp": PrieDieuWarp,
|
||||
"skip_cutscenes": SkipCutscenes,
|
||||
"corpse_hints": CorpseHints,
|
||||
"difficulty": Difficulty,
|
||||
"penitence": Penitence,
|
||||
"expert_logic": ExpertLogic,
|
||||
"ending": Ending,
|
||||
"thorn_shuffle" : ThornShuffle,
|
||||
"reliquary_shuffle": ReliquaryShuffle,
|
||||
"cherub_shuffle" : CherubShuffle,
|
||||
"life_shuffle" : LifeShuffle,
|
||||
"fervour_shuffle" : FervourShuffle,
|
||||
"sword_shuffle" : SwordShuffle,
|
||||
"blessing_shuffle" : BlessingShuffle,
|
||||
"dungeon_shuffle" : DungeonShuffle,
|
||||
"tirso_shuffle" : TirsoShuffle,
|
||||
"miriam_shuffle" : MiriamShuffle,
|
||||
"redento_shuffle" : RedentoShuffle,
|
||||
"jocinero_shuffle" : JocineroShuffle,
|
||||
"altasgracias_shuffle" : AltasgraciasShuffle,
|
||||
"tentudia_shuffle" : TentudiaShuffle,
|
||||
"gemino_shuffle" : GeminoShuffle,
|
||||
"guilt_shuffle" : GuiltShuffle,
|
||||
"ossuary_shuffle" : OssuaryShuffle,
|
||||
"boss_shuffle" : BossShuffle,
|
||||
"wound_shuffle" : WoundShuffle,
|
||||
"mask_shuffle" : MaskShuffle,
|
||||
"eye_shuffle": EyeShuffle,
|
||||
"herb_shuffle" : HerbShuffle,
|
||||
"church_shuffle" : ChurchShuffle,
|
||||
"shop_shuffle" : ShopShuffle,
|
||||
"candle_shuffle" : CandleShuffle,
|
||||
"start_wheel": StartWheel,
|
||||
"skill_randomizer": SkillRando,
|
||||
"enemy_randomizer": EnemyRando,
|
||||
"enemy_groups": EnemyGroups,
|
||||
"enemy_scaling": EnemyScaling,
|
||||
"death_link": BlasphemousDeathLink
|
||||
}
|
||||
1455
worlds/blasphemous/Rules.py
Normal file
1455
worlds/blasphemous/Rules.py
Normal file
File diff suppressed because it is too large
Load Diff
246
worlds/blasphemous/Vanilla.py
Normal file
246
worlds/blasphemous/Vanilla.py
Normal file
@@ -0,0 +1,246 @@
|
||||
from typing import Set, Dict
|
||||
|
||||
unrandomized_dict: Dict[str, str] = {
|
||||
"CoOLotCV: Fountain of burning oil": "Golden Thimble Filled with Burning Oil",
|
||||
"MotED: Egg hatching": "Hatched Egg of Deformity",
|
||||
"BotSS: Crisanta's gift": "Holy Wound of Abnegation",
|
||||
"DC: Chalice room": "Chalice of Inverted Verses"
|
||||
}
|
||||
|
||||
cherub_set: Set[str] = [
|
||||
"Albero: Child of Moonlight",
|
||||
"AR: Upper west shaft Child of Moonlight",
|
||||
"BotSS: Starting room Child of Moonlight",
|
||||
"DC: Child of Moonlight, above water",
|
||||
"DC: Upper east Child of Moonlight",
|
||||
"DC: Child of Moonlight, miasma room",
|
||||
"DC: Child of Moonlight, behind pillar",
|
||||
"DC: Top of elevator Child of Moonlight",
|
||||
"DC: Elevator shaft Child of Moonlight",
|
||||
"GotP: Shop cave Child of Moonlight",
|
||||
"GotP: Elevator shaft Child of Moonlight",
|
||||
"GotP: West shaft Child of Moonlight",
|
||||
"GotP: Center shaft Child of Moonlight",
|
||||
"GA: Miasma room Child of Moonlight",
|
||||
"GA: Blood bridge Child of Moonlight",
|
||||
"GA: Lower east Child of Moonlight",
|
||||
"Jondo: Upper east Child of Moonlight",
|
||||
"Jondo: Spike tunnel Child of Moonlight",
|
||||
"Jondo: Upper west Child of Moonlight",
|
||||
"LotNW: Platform room Child of Moonlight",
|
||||
"LotNW: Lowest west Child of Moonlight",
|
||||
"LotNW: Elevator Child of Moonlight",
|
||||
"MD: Second area Child of Moonlight",
|
||||
"MD: Cave Child of Moonlight",
|
||||
"MoM: Lower west Child of Moonlight",
|
||||
"MoM: Upper center Child of Moonlight",
|
||||
"MotED: Child of Moonlight, above chasm",
|
||||
"PotSS: First area Child of Moonlight",
|
||||
"PotSS: Third area Child of Moonlight",
|
||||
"THL: Child of Moonlight",
|
||||
"WotHP: Upper east room, top bronze cell",
|
||||
"WotHP: Upper west room, top silver cell",
|
||||
"WotHP: Lower east room, bottom silver cell",
|
||||
"WotHP: Outside Child of Moonlight",
|
||||
"WotBC: Outside Child of Moonlight",
|
||||
"WotBC: Cliffside Child of Moonlight",
|
||||
"WOTW: Underground Child of Moonlight",
|
||||
"WOTW: Upper east Child of Moonlight",
|
||||
]
|
||||
|
||||
life_set: Set[str] = [
|
||||
"AR: Lady of the Six Sorrows",
|
||||
"CoOLotCV: Lady of the Six Sorrows",
|
||||
"DC: Lady of the Six Sorrows, from MD",
|
||||
"DC: Lady of the Six Sorrows, elevator shaft",
|
||||
"GotP: Lady of the Six Sorrows",
|
||||
"LotNW: Lady of the Six Sorrows"
|
||||
]
|
||||
|
||||
fervour_set: Set[str] = [
|
||||
"DC: Oil of the Pilgrims",
|
||||
"GotP: Oil of the Pilgrims",
|
||||
"GA: Oil of the Pilgrims",
|
||||
"LotNW: Oil of the Pilgrims",
|
||||
"MoM: Oil of the Pilgrims",
|
||||
"WotHP: Oil of the Pilgrims"
|
||||
]
|
||||
|
||||
sword_set: Set[str] = [
|
||||
"Albero: Mea Culpa altar",
|
||||
"AR: Mea Culpa altar",
|
||||
"BotSS: Mea Culpa altar",
|
||||
"CoOLotCV: Mea Culpa altar",
|
||||
"DC: Mea Culpa altar",
|
||||
"LotNW: Mea Culpa altar",
|
||||
"MoM: Mea Culpa altar"
|
||||
]
|
||||
|
||||
blessing_dict: Dict[str, str] = {
|
||||
"Albero: Bless Severed Hand": "Incorrupt Hand of the Fraternal Master",
|
||||
"Albero: Bless Linen Cloth": "Shroud of Dreamt Sins",
|
||||
"Albero: Bless Hatched Egg": "Three Gnarled Tongues"
|
||||
}
|
||||
|
||||
dungeon_dict: Dict[str, str] = {
|
||||
"Confessor Dungeon 1 extra": "Tears of Atonement (1000)",
|
||||
"Confessor Dungeon 2 extra": "Heart of the Single Tone",
|
||||
"Confessor Dungeon 3 extra": "Tears of Atonement (3000)",
|
||||
"Confessor Dungeon 4 extra": "Embers of a Broken Star",
|
||||
"Confessor Dungeon 5 extra": "Tears of Atonement (5000)",
|
||||
"Confessor Dungeon 6 extra": "Scaly Coin",
|
||||
"Confessor Dungeon 7 extra": "Seashell of the Inverted Spiral"
|
||||
}
|
||||
|
||||
tirso_dict: Dict[str, str] = {
|
||||
"Albero: Tirso's 1st reward": "Linen Cloth",
|
||||
"Albero: Tirso's 2nd reward": "Tears of Atonement (500)",
|
||||
"Albero: Tirso's 3rd reward": "Tears of Atonement (1000)",
|
||||
"Albero: Tirso's 4th reward": "Tears of Atonement (2000)",
|
||||
"Albero: Tirso's 5th reward": "Tears of Atonement (5000)",
|
||||
"Albero: Tirso's 6th reward": "Tears of Atonement (10000)",
|
||||
"Albero: Tirso's final reward": "Knot of Rosary Rope"
|
||||
}
|
||||
|
||||
redento_dict: Dict[str, str] = {
|
||||
"MoM: Redento's treasure": "Nail Uprooted from Dirt",
|
||||
"MoM: Final meeting with Redento": "Knot of Rosary Rope",
|
||||
"MotED: 1st meeting with Redento": "Fourth Toe made of Limestone",
|
||||
"PotSS: 4th meeting with Redento": "Big Toe made of Limestone",
|
||||
"WotBC: 3rd meeting with Redento": "Little Toe made of Limestone"
|
||||
}
|
||||
|
||||
jocinero_dict: Dict[str, str] = {
|
||||
"TSC: Jocinero's 1st reward": "Linen of Golden Thread",
|
||||
"TSC: Jocinero's final reward": "Campanillero to the Sons of the Aurora"
|
||||
}
|
||||
|
||||
altasgracias_dict: Dict[str, str] = {
|
||||
"GA: Altasgracias' gift": "Egg of Deformity",
|
||||
"GA: Empty giant egg": "Knot of Hair"
|
||||
}
|
||||
|
||||
tentudia_dict: Dict[str, str] = {
|
||||
"Albero: Lvdovico's 1st reward": "Tears of Atonement (500)",
|
||||
"Albero: Lvdovico's 2nd reward": "Tears of Atonement (1000)",
|
||||
"Albero: Lvdovico's 3rd reward": "Debla of the Lights"
|
||||
}
|
||||
|
||||
gemino_dict: Dict[str, str] = {
|
||||
"WOTW: Gift for the tomb": "Dried Flowers bathed in Tears",
|
||||
"WOTW: Underground tomb": "Saeta Dolorosa",
|
||||
"WOTW: Gemino's gift": "Empty Golden Thimble",
|
||||
"WOTW: Gemino's reward": "Frozen Olive"
|
||||
}
|
||||
|
||||
ossuary_dict: Dict[str, str] = {
|
||||
"Ossuary: 1st reward": "Tears of Atonement (250)",
|
||||
"Ossuary: 2nd reward": "Tears of Atonement (500)",
|
||||
"Ossuary: 3rd reward": "Tears of Atonement (750)",
|
||||
"Ossuary: 4th reward": "Tears of Atonement (1000)",
|
||||
"Ossuary: 5th reward": "Tears of Atonement (1250)",
|
||||
"Ossuary: 6th reward": "Tears of Atonement (1500)",
|
||||
"Ossuary: 7th reward": "Tears of Atonement (1750)",
|
||||
"Ossuary: 8th reward": "Tears of Atonement (2000)",
|
||||
"Ossuary: 9th reward": "Tears of Atonement (2500)",
|
||||
"Ossuary: 10th reward": "Tears of Atonement (3000)",
|
||||
"Ossuary: 11th reward": "Tears of Atonement (5000)",
|
||||
}
|
||||
|
||||
boss_dict: Dict[str, str] = {
|
||||
"BotTC: Esdras, of the Anointed Legion": "Tears of Atonement (4300)",
|
||||
"BotSS: Warden of the Silent Sorrow": "Tears of Atonement (300)",
|
||||
"CoOLotCV: Our Lady of the Charred Visage": "Tears of Atonement (2600)",
|
||||
"HotD: Laudes, the First of the Amanecidas": "Tears of Atonement (30000)",
|
||||
"GotP: Amanecida of the Bejeweled Arrow": "Tears of Atonement (18000)",
|
||||
"GA: Tres Angustias": "Tears of Atonement (2100)",
|
||||
"MD: Ten Piedad": "Tears of Atonement (625)",
|
||||
"MoM: Melquiades, The Exhumed Archbishop": "Tears of Atonement (5500)",
|
||||
"MotED: Amanecida of the Golden Blades": "Tears of Atonement (18000)",
|
||||
"MaH: Sierpes": "Tears of Atonement (5000)",
|
||||
"PotSS: Amanecida of the Chiselled Steel": "Tears of Atonement (18000)",
|
||||
"TSC: Exposito, Scion of Abjuration": "Tears of Atonement (9000)",
|
||||
"WotHP: Quirce, Returned By The Flames": "Tears of Atonement (11250)",
|
||||
"WotHP: Amanecida of the Molten Thorn": "Tears of Atonement (18000)"
|
||||
}
|
||||
|
||||
wound_dict: Dict[str, str] = {
|
||||
"CoOLotCV: Visage of Compunction": "Holy Wound of Compunction",
|
||||
"GA: Visage of Contrition": "Holy Wound of Contrition",
|
||||
"MD: Visage of Attrition": "Holy Wound of Attrition"
|
||||
}
|
||||
|
||||
mask_dict: Dict[str, str] = {
|
||||
"CoOLotCV: Mask room": "Mirrored Mask of Dolphos",
|
||||
"LotNW: Mask room": "Embossed Mask of Crescente",
|
||||
"MoM: Mask room": "Deformed Mask of Orestes"
|
||||
}
|
||||
|
||||
eye_dict: Dict[str, str] = {
|
||||
"Ossuary: Isidora, Voice of the Dead": "Severed Right Eye of the Traitor",
|
||||
"MaH: Sierpes' eye": "Broken Left Eye of the Traitor"
|
||||
}
|
||||
|
||||
herb_dict: Dict[str, str] = {
|
||||
"Albero: Gate of Travel room": "Bouquet of Thyme",
|
||||
"Jondo: Lower east bell trap": "Bouquet of Rosemary",
|
||||
"MotED: Blood platform alcove": "Dried Clove",
|
||||
"PotSS: Third area lower ledge": "Olive Seeds",
|
||||
"TSC: Painting ladder ledge": "Sooty Garlic",
|
||||
"WOTW: Entrance to tomb": "Incense Garlic"
|
||||
}
|
||||
|
||||
church_dict: Dict[str, str] = {
|
||||
"Albero: Donate 5000 Tears": "Token of Appreciation",
|
||||
"Albero: Donate 50000 Tears": "Cloistered Ruby"
|
||||
}
|
||||
|
||||
shop_dict: Dict[str, str] = {
|
||||
"GotP: Shop item 1": "Torn Bridal Ribbon",
|
||||
"GotP: Shop item 2": "Calcified Eye of Erudition",
|
||||
"GotP: Shop item 3": "Ember of the Holy Cremation",
|
||||
"MD: Shop item 1": "Key to the Chamber of the Eldest Brother",
|
||||
"MD: Shop item 2": "Hollow Pearl",
|
||||
"MD: Shop item 3": "Moss Preserved in Glass",
|
||||
"TSC: Shop item 1": "Wicker Knot",
|
||||
"TSC: Shop item 2": "Empty Bile Vessel",
|
||||
"TSC: Shop item 3": "Key of the Inquisitor"
|
||||
}
|
||||
|
||||
thorn_set: Set[str] = {
|
||||
"THL: Deogracias' gift",
|
||||
"Confessor Dungeon 1 main",
|
||||
"Confessor Dungeon 2 main",
|
||||
"Confessor Dungeon 3 main",
|
||||
"Confessor Dungeon 4 main",
|
||||
"Confessor Dungeon 5 main",
|
||||
"Confessor Dungeon 6 main",
|
||||
"Confessor Dungeon 7 main",
|
||||
}
|
||||
|
||||
candle_dict: Dict[str, str] = {
|
||||
"CoOLotCV: Red candle": "Bead of Red Wax",
|
||||
"LotNW: Red candle": "Bead of Red Wax",
|
||||
"MD: Red candle": "Bead of Red Wax",
|
||||
"BotSS: Blue candle": "Bead of Blue Wax",
|
||||
"CoOLotCV: Blue candle": "Bead of Blue Wax",
|
||||
"MD: Blue candle": "Bead of Blue Wax"
|
||||
}
|
||||
|
||||
skill_dict: Dict[str, str] = {
|
||||
"Skill 1, Tier 1": "Combo Skill",
|
||||
"Skill 1, Tier 2": "Combo Skill",
|
||||
"Skill 1, Tier 3": "Combo Skill",
|
||||
"Skill 2, Tier 1": "Charged Skill",
|
||||
"Skill 2, Tier 2": "Charged Skill",
|
||||
"Skill 2, Tier 3": "Charged Skill",
|
||||
"Skill 3, Tier 1": "Ranged Skill",
|
||||
"Skill 3, Tier 2": "Ranged Skill",
|
||||
"Skill 3, Tier 3": "Ranged Skill",
|
||||
"Skill 4, Tier 1": "Dive Skill",
|
||||
"Skill 4, Tier 2": "Dive Skill",
|
||||
"Skill 4, Tier 3": "Dive Skill",
|
||||
"Skill 5, Tier 1": "Lunge Skill",
|
||||
"Skill 5, Tier 2": "Lunge Skill",
|
||||
"Skill 5, Tier 3": "Lunge Skill",
|
||||
}
|
||||
413
worlds/blasphemous/__init__.py
Normal file
413
worlds/blasphemous/__init__.py
Normal file
@@ -0,0 +1,413 @@
|
||||
from typing import Dict, Set, Any
|
||||
from collections import Counter
|
||||
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from .Items import base_id, item_table, group_table, tears_set, reliquary_set, skill_set
|
||||
from .Locations import location_table, shop_set
|
||||
from .Exits import region_exit_table, exit_lookup_table
|
||||
from .Rules import rules
|
||||
from worlds.generic.Rules import set_rule
|
||||
from .Options import blasphemous_options
|
||||
from . import Vanilla
|
||||
|
||||
|
||||
class BlasphemousWeb(WebWorld):
|
||||
theme = "stone"
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Blasphemous randomizer connected to an Archipelago Multiworld",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["TRPG"]
|
||||
)]
|
||||
|
||||
|
||||
class BlasphemousWorld(World):
|
||||
"""
|
||||
Blasphemous is a challenging Metroidvania set in the cursed land of Cvstodia. Play as the Penitent One, trapped
|
||||
in an endless cycle of death and rebirth, and free the world from it's terrible fate in your quest to break
|
||||
your eternal damnation!
|
||||
"""
|
||||
|
||||
game: str = "Blasphemous"
|
||||
web = BlasphemousWeb()
|
||||
data_version: 1
|
||||
|
||||
item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
|
||||
location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)}
|
||||
location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table}
|
||||
|
||||
item_name_groups = group_table
|
||||
option_definitions = blasphemous_options
|
||||
|
||||
|
||||
def set_rules(self):
|
||||
rules(self)
|
||||
|
||||
|
||||
def create_item(self, name: str) -> "BlasphemousItem":
|
||||
item_id: int = self.item_name_to_id[name]
|
||||
id = item_id - base_id
|
||||
|
||||
return BlasphemousItem(name, item_table[id]["classification"], item_id, player=self.player)
|
||||
|
||||
|
||||
def create_event(self, event: str):
|
||||
return BlasphemousItem(event, ItemClassification.progression_skip_balancing, None, self.player)
|
||||
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.multiworld.random.choice(tears_set)
|
||||
|
||||
|
||||
def generate_basic(self):
|
||||
placed_items = []
|
||||
|
||||
placed_items.extend(Vanilla.unrandomized_dict.values())
|
||||
|
||||
if not self.multiworld.reliquary_shuffle[self.player]:
|
||||
placed_items.extend(reliquary_set)
|
||||
elif self.multiworld.reliquary_shuffle[self.player]:
|
||||
placed_items.append("Tears of Atonement (250)")
|
||||
placed_items.append("Tears of Atonement (300)")
|
||||
placed_items.append("Tears of Atonement (500)")
|
||||
|
||||
if not self.multiworld.cherub_shuffle[self.player]:
|
||||
for i in range(38):
|
||||
placed_items.append("Child of Moonlight")
|
||||
|
||||
if not self.multiworld.life_shuffle[self.player]:
|
||||
for i in range(6):
|
||||
placed_items.append("Life Upgrade")
|
||||
|
||||
if not self.multiworld.fervour_shuffle[self.player]:
|
||||
for i in range(6):
|
||||
placed_items.append("Fervour Upgrade")
|
||||
|
||||
if not self.multiworld.sword_shuffle[self.player]:
|
||||
for i in range(7):
|
||||
placed_items.append("Mea Culpa Upgrade")
|
||||
|
||||
if not self.multiworld.blessing_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.blessing_dict.values())
|
||||
|
||||
if not self.multiworld.dungeon_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.dungeon_dict.values())
|
||||
|
||||
if not self.multiworld.tirso_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.tirso_dict.values())
|
||||
|
||||
if not self.multiworld.miriam_shuffle[self.player]:
|
||||
placed_items.append("Cantina of the Blue Rose")
|
||||
|
||||
if not self.multiworld.redento_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.redento_dict.values())
|
||||
|
||||
if not self.multiworld.jocinero_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.jocinero_dict.values())
|
||||
|
||||
if not self.multiworld.altasgracias_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.altasgracias_dict.values())
|
||||
|
||||
if not self.multiworld.tentudia_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.tentudia_dict.values())
|
||||
|
||||
if not self.multiworld.gemino_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.gemino_dict.values())
|
||||
|
||||
if not self.multiworld.guilt_shuffle[self.player]:
|
||||
placed_items.append("Weight of True Guilt")
|
||||
|
||||
if not self.multiworld.ossuary_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.ossuary_dict.values())
|
||||
|
||||
if not self.multiworld.boss_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.boss_dict.values())
|
||||
|
||||
if not self.multiworld.wound_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.wound_dict.values())
|
||||
|
||||
if not self.multiworld.mask_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.mask_dict.values())
|
||||
|
||||
if not self.multiworld.eye_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.eye_dict.values())
|
||||
|
||||
if not self.multiworld.herb_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.herb_dict.values())
|
||||
|
||||
if not self.multiworld.church_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.church_dict.values())
|
||||
|
||||
if not self.multiworld.shop_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.shop_dict.values())
|
||||
|
||||
if self.multiworld.thorn_shuffle[self.player] == 2:
|
||||
for i in range(8):
|
||||
placed_items.append("Thorn Upgrade")
|
||||
|
||||
if not self.multiworld.candle_shuffle[self.player]:
|
||||
placed_items.extend(Vanilla.candle_dict.values())
|
||||
|
||||
if self.multiworld.start_wheel[self.player]:
|
||||
placed_items.append("The Young Mason's Wheel")
|
||||
|
||||
if not self.multiworld.skill_randomizer[self.player]:
|
||||
placed_items.extend(Vanilla.skill_dict.values())
|
||||
|
||||
counter = Counter(placed_items)
|
||||
|
||||
pool = []
|
||||
|
||||
for item in item_table:
|
||||
count = item["count"] - counter[item["name"]]
|
||||
|
||||
if count <= 0:
|
||||
continue
|
||||
else:
|
||||
for i in range(count):
|
||||
pool.append(self.create_item(item["name"]))
|
||||
|
||||
self.multiworld.itempool += pool
|
||||
|
||||
|
||||
def pre_fill(self):
|
||||
self.place_items_from_dict(Vanilla.unrandomized_dict)
|
||||
|
||||
if not self.multiworld.cherub_shuffle[self.player]:
|
||||
self.place_items_from_set(Vanilla.cherub_set, "Child of Moonlight")
|
||||
|
||||
if not self.multiworld.life_shuffle[self.player]:
|
||||
self.place_items_from_set(Vanilla.life_set, "Life Upgrade")
|
||||
|
||||
if not self.multiworld.fervour_shuffle[self.player]:
|
||||
self.place_items_from_set(Vanilla.fervour_set, "Fervour Upgrade")
|
||||
|
||||
if not self.multiworld.sword_shuffle[self.player]:
|
||||
self.place_items_from_set(Vanilla.sword_set, "Mea Culpa Upgrade")
|
||||
|
||||
if not self.multiworld.blessing_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.blessing_dict)
|
||||
|
||||
if not self.multiworld.dungeon_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.dungeon_dict)
|
||||
|
||||
if not self.multiworld.tirso_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.tirso_dict)
|
||||
|
||||
if not self.multiworld.miriam_shuffle[self.player]:
|
||||
self.multiworld.get_location("AtTotS: Miriam's gift", self.player)\
|
||||
.place_locked_item(self.create_item("Cantina of the Blue Rose"))
|
||||
|
||||
if not self.multiworld.redento_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.redento_dict)
|
||||
|
||||
if not self.multiworld.jocinero_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.jocinero_dict)
|
||||
|
||||
if not self.multiworld.altasgracias_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.altasgracias_dict)
|
||||
|
||||
if not self.multiworld.tentudia_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.tentudia_dict)
|
||||
|
||||
if not self.multiworld.gemino_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.gemino_dict)
|
||||
|
||||
if not self.multiworld.guilt_shuffle[self.player]:
|
||||
self.multiworld.get_location("GotP: Confessor Dungeon room", self.player)\
|
||||
.place_locked_item(self.create_item("Weight of True Guilt"))
|
||||
|
||||
if not self.multiworld.ossuary_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.ossuary_dict)
|
||||
|
||||
if not self.multiworld.boss_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.boss_dict)
|
||||
|
||||
if not self.multiworld.wound_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.wound_dict)
|
||||
|
||||
if not self.multiworld.mask_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.mask_dict)
|
||||
|
||||
if not self.multiworld.eye_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.eye_dict)
|
||||
|
||||
if not self.multiworld.herb_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.herb_dict)
|
||||
|
||||
if not self.multiworld.church_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.church_dict)
|
||||
|
||||
if not self.multiworld.shop_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.shop_dict)
|
||||
|
||||
if self.multiworld.thorn_shuffle[self.player] == 2:
|
||||
self.place_items_from_set(Vanilla.thorn_set, "Thorn Upgrade")
|
||||
|
||||
if not self.multiworld.candle_shuffle[self.player]:
|
||||
self.place_items_from_dict(Vanilla.candle_dict)
|
||||
|
||||
if self.multiworld.start_wheel[self.player]:
|
||||
self.multiworld.get_location("BotSS: Beginning gift", self.player)\
|
||||
.place_locked_item(self.create_item("The Young Mason's Wheel"))
|
||||
|
||||
if not self.multiworld.skill_randomizer[self.player]:
|
||||
self.place_items_from_dict(Vanilla.skill_dict)
|
||||
|
||||
if self.multiworld.thorn_shuffle[self.player] == 1:
|
||||
self.multiworld.local_items[self.player].value.add("Thorn Upgrade")
|
||||
|
||||
|
||||
def place_items_from_set(self, location_set: Set[str], name: str):
|
||||
for loc in location_set:
|
||||
self.multiworld.get_location(loc, self.player)\
|
||||
.place_locked_item(self.create_item(name))
|
||||
|
||||
|
||||
def place_items_from_dict(self, option_dict: Dict[str, str]):
|
||||
for loc, item in option_dict.items():
|
||||
self.multiworld.get_location(loc, self.player)\
|
||||
.place_locked_item(self.create_item(item))
|
||||
|
||||
|
||||
def create_regions(self) -> None:
|
||||
|
||||
player = self.player
|
||||
world = self.multiworld
|
||||
|
||||
region_table: Dict[str, Region] = {
|
||||
"menu" : Region("Menu", player, world),
|
||||
"albero" : Region("Albero", player, world),
|
||||
"attots" : Region("All the Tears of the Sea", player, world),
|
||||
"ar" : Region("Archcathedral Rooftops", player, world),
|
||||
"bottc" : Region("Bridge of the Three Cavalries", player, world),
|
||||
"botss" : Region("Brotherhood of the Silent Sorrow", player, world),
|
||||
"coolotcv": Region("Convent of Our Lady of the Charred Visage", player, world),
|
||||
"dohh" : Region("Deambulatory of His Holiness", player, world),
|
||||
"dc" : Region("Desecrated Cistern", player, world),
|
||||
"eos" : Region("Echoes of Salt", player, world),
|
||||
"ft" : Region("Ferrous Tree", player, world),
|
||||
"gotp" : Region("Graveyard of the Peaks", player, world),
|
||||
"ga" : Region("Grievance Ascends", player, world),
|
||||
"hotd" : Region("Hall of the Dawning", player, world),
|
||||
"jondo" : Region("Jondo", player, world),
|
||||
"kottw" : Region("Knot of the Three Words", player, world),
|
||||
"lotnw" : Region("Library of the Negated Words", player, world),
|
||||
"md" : Region("Mercy Dreams", player, world),
|
||||
"mom" : Region("Mother of Mothers", player, world),
|
||||
"moted" : Region("Mountains of the Endless Dusk", player, world),
|
||||
"mah" : Region("Mourning and Havoc", player, world),
|
||||
"potss" : Region("Patio of the Silent Steps", player, world),
|
||||
"petrous" : Region("Petrous", player, world),
|
||||
"thl" : Region("The Holy Line", player, world),
|
||||
"trpots" : Region("The Resting Place of the Sister", player, world),
|
||||
"tsc" : Region("The Sleeping Canvases", player, world),
|
||||
"wothp" : Region("Wall of the Holy Prohibitions", player, world),
|
||||
"wotbc" : Region("Wasteland of the Buried Churches", player, world),
|
||||
"wotw" : Region("Where Olive Trees Wither", player, world),
|
||||
"dungeon" : Region("Dungeons", player, world)
|
||||
}
|
||||
|
||||
for rname, reg in region_table.items():
|
||||
world.regions.append(reg)
|
||||
|
||||
for ename, exits in region_exit_table.items():
|
||||
if ename == rname:
|
||||
for i in exits:
|
||||
ent = Entrance(player, i, reg)
|
||||
reg.exits.append(ent)
|
||||
|
||||
for e, r in exit_lookup_table.items():
|
||||
if i == e:
|
||||
ent.connect(region_table[r])
|
||||
|
||||
for loc in location_table:
|
||||
id = base_id + location_table.index(loc)
|
||||
region_table[loc["region"]].locations\
|
||||
.append(BlasphemousLocation(self.player, loc["name"], id, region_table[loc["region"]]))
|
||||
|
||||
victory = Location(self.player, "His Holiness Escribar", None, self.multiworld.get_region("Deambulatory of His Holiness", self.player))
|
||||
victory.place_locked_item(self.create_event("Victory"))
|
||||
self.multiworld.get_region("Deambulatory of His Holiness", self.player).locations.append(victory)
|
||||
|
||||
if self.multiworld.ending[self.player].value == 1:
|
||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
|
||||
elif self.multiworld.ending[self.player].value == 2:
|
||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and \
|
||||
state.has("Holy Wound of Abnegation", player))
|
||||
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
slot_data: Dict[str, Any] = {}
|
||||
locations = []
|
||||
|
||||
for loc in self.multiworld.get_filled_locations(self.player):
|
||||
if loc.name == "His Holiness Escribar":
|
||||
continue
|
||||
else:
|
||||
data = {
|
||||
"id": self.location_name_to_game_id[loc.name],
|
||||
"ap_id": loc.address,
|
||||
"name": loc.item.name,
|
||||
"player_name": self.multiworld.player_name[loc.item.player]
|
||||
}
|
||||
|
||||
if loc.name in shop_set:
|
||||
data["type"] = loc.item.classification.name
|
||||
|
||||
locations.append(data)
|
||||
|
||||
config = {
|
||||
"versionCreated": "AP",
|
||||
"general": {
|
||||
"teleportationAlwaysUnlocked": bool(self.multiworld.prie_dieu_warp[self.player].value),
|
||||
"skipCutscenes": bool(self.multiworld.skip_cutscenes[self.player].value),
|
||||
"enablePenitence": bool(self.multiworld.penitence[self.player].value),
|
||||
"hardMode": False,
|
||||
"customSeed": 0,
|
||||
"allowHints": bool(self.multiworld.corpse_hints[self.player].value)
|
||||
},
|
||||
"items": {
|
||||
"type": 1,
|
||||
"lungDamage": False,
|
||||
"disableNPCDeath": True,
|
||||
"startWithWheel": bool(self.multiworld.start_wheel[self.player].value),
|
||||
"shuffleReliquaries": bool(self.multiworld.reliquary_shuffle[self.player].value)
|
||||
},
|
||||
"enemies": {
|
||||
"type": self.multiworld.enemy_randomizer[self.player].value,
|
||||
"maintainClass": bool(self.multiworld.enemy_groups[self.player].value),
|
||||
"areaScaling": bool(self.multiworld.enemy_scaling[self.player].value)
|
||||
},
|
||||
"prayers": {
|
||||
"type": 0,
|
||||
"removeMirabis": False
|
||||
},
|
||||
"doors": {
|
||||
"type": 0
|
||||
},
|
||||
"debug": {
|
||||
"type": 0
|
||||
}
|
||||
}
|
||||
|
||||
slot_data = {
|
||||
"locations": locations,
|
||||
"cfg": config,
|
||||
"ending": self.multiworld.ending[self.player].value,
|
||||
"death_link": bool(self.multiworld.death_link[self.player].value)
|
||||
}
|
||||
|
||||
return slot_data
|
||||
|
||||
|
||||
class BlasphemousItem(Item):
|
||||
game: str = "Blasphemous"
|
||||
|
||||
|
||||
class BlasphemousLocation(Location):
|
||||
game: str = "Blasphemous"
|
||||
64
worlds/blasphemous/docs/en_Blasphemous.md
Normal file
64
worlds/blasphemous/docs/en_Blasphemous.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Blasphemous
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
All items that appear on the ground are randomized, and there are options to randomize more of the game, such as stat upgrades, enemies, skills, and more.
|
||||
|
||||
In addition, there are other changes to the game that make it better optimized for a randomizer:
|
||||
|
||||
- Some items and enemies are never randomized.
|
||||
- Teleportation between Prie Dieus can be unlocked from the beginning.
|
||||
- New save files are created in True Torment mode (NG+), but enemies and bosses will use their regular attack & defense values. A penitence can be chosen if the option is enabled.
|
||||
- Save files can not be ascended - the randomizer is meant to be completed in a single playthrough.
|
||||
- The Ossuary will give a reward for every four bones collected.
|
||||
- Side quests have been modified so that the items received from them cannot be missed.
|
||||
- The Apodictic Heart of Mea Culpa can be unequipped.
|
||||
- Dying with the Immaculate Bead is unnecessary, it is automatically upgraded to the Weight of True Guilt.
|
||||
- If the option is enabled, the 34 corpses in game will have their messages changed to give hints about certain items and locations. The Shroud of Dreamt Sins is not required to hear them.
|
||||
|
||||
## What has been changed about the side quests?
|
||||
|
||||
Tirso:
|
||||
- Tirso's helpers will never die. Herbs can be given to him at any time.
|
||||
|
||||
Gemino:
|
||||
- Gemino will never freeze. The thimble can be given to him at any time.
|
||||
|
||||
Viridiana:
|
||||
- Viridiana will never die. The player can ask for her assistance at all 5 boss fights, and she will still appear at the rooftops.
|
||||
|
||||
Redento:
|
||||
- No changes.
|
||||
|
||||
Cleofas:
|
||||
- The choice to end Socorro's suffering has been removed.
|
||||
- Cleofas will not jump off the rooftops, even after talking to him without the Cord of the True Burying.
|
||||
|
||||
Crisanta / Ending C:
|
||||
- The Incomplete Scapular will not skip the fight with Esdras. Instead, it is required to open the door to the church in Brotherhood of the Silent Sorrow.
|
||||
- Perpetva's item from The Resting Place of the Sister is always accessible, even after defeating Esdras.
|
||||
- Crisanta's gift in Brotherhood of the Silent Sorrow will always be the Holy Wound of Abnegation.
|
||||
- When fighting Crisanta, it is no longer required to have the Apodictic Heart of Mea Culpa equipped to continue with Ending C, it just needs to be in the player's inventory.
|
||||
|
||||
## Which items and enemies are never randomized?
|
||||
|
||||
Items:
|
||||
- Golden Thimble Filled with Burning Oil - from the fountain of burning oil in Convent of Our Lady of the Charred Visage
|
||||
- Hatched Egg of Deformity - from the tree in Mountains of the Endless Dusk
|
||||
- Chalice of Inverted Verses - from the statue in Desecrated Cistern
|
||||
- Holy Wound of Abnegation - given by Crisanta in Brotherhood of the Silent Sorrow
|
||||
|
||||
Enemies:
|
||||
- The Charging Knell in Mountains of the Endless Dusk
|
||||
- The bell ringer in lower east Jondo
|
||||
- The first Phalaris, Lionheart, and Sleepless Tomb in their respective areas (Chalice of Inverted Verses quest)
|
||||
|
||||
In addition, any enemies that appear in some kind of arena (such as the Confessor Dungeons, or the bridges in Archcathedral Rooftops or Grievance Ascends) will not be randomized to prevent softlocking.
|
||||
|
||||
## What does another world's item look like in Blasphemous?
|
||||
|
||||
Items retain their original appearance. You won't know if an item is for another player until you collect it. The only exception to this is the shops, where items that belong to other players are represented by the Archipelago logo.
|
||||
21
worlds/blasphemous/docs/setup_en.md
Normal file
21
worlds/blasphemous/docs/setup_en.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Blasphemous Multiworld Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- Blasphemous from: [Steam](https://store.steampowered.com/app/774361/Blasphemous/)
|
||||
- Blasphemous Modding API from: [GitHub](https://github.com/BrandenEK/Blasphemous-Modding-API)
|
||||
- Blasphemous Randomizer from: [GitHub](https://github.com/BrandenEK/Blasphemous-Randomizer)
|
||||
- Blasphemous Multiworld from: [GitHub](https://github.com/BrandenEK/Blasphemous-Multiworld)
|
||||
|
||||
## Instructions (Windows)
|
||||
|
||||
1. Download the [Modding API](https://github.com/BrandenEK/Blasphemous-Modding-API/releases), and follow the [installation instructions](https://github.com/BrandenEK/Blasphemous-Modding-API#installation) on the GitHub page.
|
||||
|
||||
2. After the Modding API has been installed, download the [Randomizer](https://github.com/BrandenEK/Blasphemous-Randomizer/releases) and [Multiworld](https://github.com/BrandenEK/Blasphemous-Multiworld/releases) archives, and extract the contents of both into the `Modding` folder.
|
||||
|
||||
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both the Randomizer and Multiworld on the title screen.
|
||||
|
||||
## Connecting
|
||||
|
||||
To connect to an Archipelago server, open the in-game console by pressing backslash `\` and use the command `multiworld connect [address:port] [name] [password]`. The port and password are both optional - if no port is provided then the default port of 38281 is used.
|
||||
**Make sure to connect to the server before attempting to start a new save file.**
|
||||
@@ -1,10 +1,18 @@
|
||||
import sys
|
||||
|
||||
from BaseClasses import Item
|
||||
from worlds.dark_souls_3.data.items_data import item_tables
|
||||
from worlds.dark_souls_3.data.items_data import item_tables, dlc_shields_table, dlc_weapons_upgrade_10_table, \
|
||||
dlc_weapons_upgrade_5_table, dlc_goods_table, dlc_spells_table, dlc_armor_table, dlc_ring_table, dlc_misc_table, dlc_goods_2_table
|
||||
|
||||
|
||||
class DarkSouls3Item(Item):
|
||||
game: str = "Dark Souls III"
|
||||
|
||||
dlc_set = {**dlc_shields_table, **dlc_weapons_upgrade_10_table, **dlc_weapons_upgrade_5_table,
|
||||
**dlc_goods_table, **dlc_spells_table, **dlc_armor_table, **dlc_ring_table, **dlc_misc_table}
|
||||
|
||||
dlc_progressive = {**dlc_goods_2_table}
|
||||
|
||||
@staticmethod
|
||||
def get_name_to_id() -> dict:
|
||||
base_id = 100000
|
||||
@@ -12,6 +20,17 @@ class DarkSouls3Item(Item):
|
||||
|
||||
output = {}
|
||||
for i, table in enumerate(item_tables):
|
||||
if len(table) > table_offset:
|
||||
raise Exception("An item table has {} entries, that is more than {} entries (table #{})".format(len(table), table_offset, i))
|
||||
output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))})
|
||||
|
||||
return output
|
||||
|
||||
@staticmethod
|
||||
def is_dlc_item(name) -> bool:
|
||||
return name in DarkSouls3Item.dlc_set
|
||||
|
||||
@staticmethod
|
||||
def is_dlc_progressive(name) -> bool:
|
||||
return name in DarkSouls3Item.dlc_progressive
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import sys
|
||||
|
||||
from BaseClasses import Location
|
||||
from worlds.dark_souls_3.data.locations_data import location_tables
|
||||
from worlds.dark_souls_3.data.locations_data import location_tables, painted_world_table, dreg_heap_table, \
|
||||
ringed_city_table
|
||||
|
||||
|
||||
class DarkSouls3Location(Location):
|
||||
@@ -12,6 +15,8 @@ class DarkSouls3Location(Location):
|
||||
|
||||
output = {}
|
||||
for i, table in enumerate(location_tables):
|
||||
if len(table) > table_offset:
|
||||
raise Exception("A location table has {} entries, that is more than {} entries (table #{})".format(len(table), table_offset, i))
|
||||
output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))})
|
||||
|
||||
return output
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import typing
|
||||
from Options import Toggle, Option, DeathLink
|
||||
from Options import Toggle, Option, Range, Choice, DeathLink
|
||||
|
||||
|
||||
class AutoEquipOption(Toggle):
|
||||
@@ -29,10 +29,57 @@ class NoEquipLoadOption(Toggle):
|
||||
display_name = "No Equip load"
|
||||
|
||||
|
||||
class RandomizeWeaponsLevelOption(Toggle):
|
||||
"""Enable this option to upgrade 33% ( based on the probability chance ) of the pool of weapons to a random value
|
||||
between +1 and +5/+10"""
|
||||
class RandomizeWeaponsLevelOption(Choice):
|
||||
"""Enable this option to upgrade a percentage of the pool of weapons to a random value between the minimum and
|
||||
maximum levels defined.
|
||||
all: All weapons are eligible, both basic and epic
|
||||
basic: Only weapons that can be upgraded to +10
|
||||
epic: Only weapons that can be upgraded to +5"""
|
||||
display_name = "Randomize weapons level"
|
||||
option_none = 0
|
||||
option_all = 1
|
||||
option_basic = 2
|
||||
option_epic = 3
|
||||
|
||||
|
||||
class RandomizeWeaponsLevelPercentageOption(Range):
|
||||
"""The percentage of weapons in the pool to be upgraded if randomize weapons level is toggled"""
|
||||
display_name = "Percentage of randomized weapons"
|
||||
range_start = 1
|
||||
range_end = 100
|
||||
default = 33
|
||||
|
||||
|
||||
class MinLevelsIn5WeaponPoolOption(Range):
|
||||
"""The minimum upgraded value of a weapon in the pool of weapons that can only reach +5"""
|
||||
display_name = "Minimum level of +5 weapons"
|
||||
range_start = 1
|
||||
range_end = 5
|
||||
default = 1
|
||||
|
||||
|
||||
class MaxLevelsIn5WeaponPoolOption(Range):
|
||||
"""The maximum upgraded value of a weapon in the pool of weapons that can only reach +5"""
|
||||
display_name = "Maximum level of +5 weapons"
|
||||
range_start = 1
|
||||
range_end = 5
|
||||
default = 5
|
||||
|
||||
|
||||
class MinLevelsIn10WeaponPoolOption(Range):
|
||||
"""The minimum upgraded value of a weapon in the pool of weapons that can reach +10"""
|
||||
display_name = "Minimum level of +10 weapons"
|
||||
range_start = 1
|
||||
range_end = 10
|
||||
default = 1
|
||||
|
||||
|
||||
class MaxLevelsIn10WeaponPoolOption(Range):
|
||||
"""The maximum upgraded value of a weapon in the pool of weapons that can reach +10"""
|
||||
display_name = "Maximum level of +10 weapons"
|
||||
range_start = 1
|
||||
range_end = 10
|
||||
default = 10
|
||||
|
||||
|
||||
class LateBasinOfVowsOption(Toggle):
|
||||
@@ -41,14 +88,31 @@ class LateBasinOfVowsOption(Toggle):
|
||||
display_name = "Late Basin of Vows"
|
||||
|
||||
|
||||
class EnableProgressiveLocationsOption(Toggle):
|
||||
"""Randomize upgrade materials such as the titanite shards, the estus shards and the consumables"""
|
||||
display_name = "Randomize materials, Estus shards and consumables (+196 checks/items)"
|
||||
|
||||
|
||||
class EnableDLCOption(Toggle):
|
||||
"""To use this option, you must own both the ASHES OF ARIANDEL and the RINGED CITY DLC"""
|
||||
display_name = "Add the DLC Items and Locations to the pool (+81 checks/items)"
|
||||
|
||||
|
||||
dark_souls_options: typing.Dict[str, type(Option)] = {
|
||||
"auto_equip": AutoEquipOption,
|
||||
"lock_equip": LockEquipOption,
|
||||
"no_weapon_requirements": NoWeaponRequirementsOption,
|
||||
"randomize_weapons_level": RandomizeWeaponsLevelOption,
|
||||
"randomize_weapons_percentage": RandomizeWeaponsLevelPercentageOption,
|
||||
"min_levels_in_5": MinLevelsIn5WeaponPoolOption,
|
||||
"max_levels_in_5": MaxLevelsIn5WeaponPoolOption,
|
||||
"min_levels_in_10": MinLevelsIn10WeaponPoolOption,
|
||||
"max_levels_in_10": MaxLevelsIn10WeaponPoolOption,
|
||||
"late_basin_of_vows": LateBasinOfVowsOption,
|
||||
"no_spell_requirements": NoSpellRequirementsOption,
|
||||
"no_equip_load": NoEquipLoadOption,
|
||||
"death_link": DeathLink,
|
||||
"enable_progressive_locations": EnableProgressiveLocationsOption,
|
||||
"enable_dlc": EnableDLCOption,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# world/dark_souls_3/__init__.py
|
||||
import json
|
||||
import os
|
||||
from typing import Dict
|
||||
|
||||
from .Items import DarkSouls3Item
|
||||
from .Locations import DarkSouls3Location
|
||||
from .Options import dark_souls_options
|
||||
from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary, key_items_list
|
||||
from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary, key_items_list, \
|
||||
dlc_weapons_upgrade_5_table, dlc_weapons_upgrade_10_table
|
||||
from .data.locations_data import location_dictionary, fire_link_shrine_table, \
|
||||
high_wall_of_lothric, \
|
||||
undead_settlement_table, road_of_sacrifice_table, consumed_king_garden_table, cathedral_of_the_deep_table, \
|
||||
farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, \
|
||||
irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, grand_archives_table, \
|
||||
untended_graves_table, archdragon_peak_table, firelink_shrine_bell_tower_table, progressive_locations
|
||||
untended_graves_table, archdragon_peak_table, firelink_shrine_bell_tower_table, progressive_locations, \
|
||||
progressive_locations_2, progressive_locations_3, painted_world_table, dreg_heap_table, ringed_city_table, dlc_progressive_locations
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from BaseClasses import MultiWorld, Region, Item, Entrance, Tutorial, ItemClassification
|
||||
from ..generic.Rules import set_rule, add_item_rule
|
||||
@@ -52,9 +52,9 @@ class DarkSouls3World(World):
|
||||
option_definitions = dark_souls_options
|
||||
topology_present: bool = True
|
||||
web = DarkSouls3Web()
|
||||
data_version = 4
|
||||
data_version = 5
|
||||
base_id = 100000
|
||||
required_client_version = (0, 3, 6)
|
||||
required_client_version = (0, 3, 7)
|
||||
item_name_to_id = DarkSouls3Item.get_name_to_id()
|
||||
location_name_to_id = DarkSouls3Location.get_name_to_id()
|
||||
|
||||
@@ -77,7 +77,15 @@ class DarkSouls3World(World):
|
||||
return DarkSouls3Item(name, item_classification, data, self.player)
|
||||
|
||||
def create_regions(self):
|
||||
menu_region = self.create_region("Menu", progressive_locations)
|
||||
|
||||
if self.multiworld.enable_progressive_locations[self.player].value and self.multiworld.enable_dlc[self.player].value:
|
||||
menu_region = self.create_region("Menu", {**progressive_locations, **progressive_locations_2,
|
||||
**progressive_locations_3, **dlc_progressive_locations})
|
||||
elif self.multiworld.enable_progressive_locations[self.player].value:
|
||||
menu_region = self.create_region("Menu", {**progressive_locations, **progressive_locations_2,
|
||||
**progressive_locations_3})
|
||||
else:
|
||||
menu_region = self.create_region("Menu", None)
|
||||
|
||||
# Create all Vanilla regions of Dark Souls III
|
||||
firelink_shrine_region = self.create_region("Firelink Shrine", fire_link_shrine_table)
|
||||
@@ -101,6 +109,11 @@ class DarkSouls3World(World):
|
||||
untended_graves_region = self.create_region("Untended Graves", untended_graves_table)
|
||||
archdragon_peak_region = self.create_region("Archdragon Peak", archdragon_peak_table)
|
||||
kiln_of_the_first_flame_region = self.create_region("Kiln Of The First Flame", None)
|
||||
# DLC Down here
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
painted_world_of_ariandel_region = self.create_region("Painted World of Ariandel", painted_world_table)
|
||||
dreg_heap_region = self.create_region("Dreg Heap", dreg_heap_table)
|
||||
ringed_city_region = self.create_region("Ringed City", ringed_city_table)
|
||||
|
||||
# Create the entrance to connect those regions
|
||||
menu_region.exits.append(Entrance(self.player, "New Game", menu_region))
|
||||
@@ -112,7 +125,8 @@ class DarkSouls3World(World):
|
||||
firelink_shrine_region.exits.append(Entrance(self.player, "Goto Bell Tower",
|
||||
firelink_shrine_region))
|
||||
self.multiworld.get_entrance("Goto High Wall of Lothric", self.player).connect(high_wall_of_lothric_region)
|
||||
self.multiworld.get_entrance("Goto Kiln Of The First Flame", self.player).connect(kiln_of_the_first_flame_region)
|
||||
self.multiworld.get_entrance("Goto Kiln Of The First Flame", self.player).connect(
|
||||
kiln_of_the_first_flame_region)
|
||||
self.multiworld.get_entrance("Goto Bell Tower", self.player).connect(firelink_shrine_bell_tower_region)
|
||||
high_wall_of_lothric_region.exits.append(Entrance(self.player, "Goto Undead Settlement",
|
||||
high_wall_of_lothric_region))
|
||||
@@ -133,7 +147,7 @@ class DarkSouls3World(World):
|
||||
catacombs_of_carthus_region))
|
||||
catacombs_of_carthus_region.exits.append(Entrance(self.player, "Goto Smouldering Lake",
|
||||
catacombs_of_carthus_region))
|
||||
self.multiworld.get_entrance("Goto Irithyll of the boreal", self.player).\
|
||||
self.multiworld.get_entrance("Goto Irithyll of the boreal", self.player). \
|
||||
connect(irithyll_of_the_boreal_valley_region)
|
||||
self.multiworld.get_entrance("Goto Smouldering Lake", self.player).connect(smouldering_lake_region)
|
||||
irithyll_of_the_boreal_valley_region.exits.append(Entrance(self.player, "Goto Irithyll dungeon",
|
||||
@@ -153,6 +167,16 @@ class DarkSouls3World(World):
|
||||
consumed_king_garden_region.exits.append(Entrance(self.player, "Goto Untended Graves",
|
||||
consumed_king_garden_region))
|
||||
self.multiworld.get_entrance("Goto Untended Graves", self.player).connect(untended_graves_region)
|
||||
# DLC Connectors Below
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
cathedral_of_the_deep_region.exits.append(Entrance(self.player, "Goto Painted World of Ariandel",
|
||||
cathedral_of_the_deep_region))
|
||||
self.multiworld.get_entrance("Goto Painted World of Ariandel", self.player).connect(painted_world_of_ariandel_region)
|
||||
painted_world_of_ariandel_region.exits.append(Entrance(self.player, "Goto Dreg Heap",
|
||||
painted_world_of_ariandel_region))
|
||||
self.multiworld.get_entrance("Goto Dreg Heap", self.player).connect(dreg_heap_region)
|
||||
dreg_heap_region.exits.append(Entrance(self.player, "Goto Ringed City", dreg_heap_region))
|
||||
self.multiworld.get_entrance("Goto Ringed City", self.player).connect(ringed_city_region)
|
||||
|
||||
# For each region, add the associated locations retrieved from the corresponding location_table
|
||||
def create_region(self, region_name, location_table) -> Region:
|
||||
@@ -169,8 +193,18 @@ class DarkSouls3World(World):
|
||||
def create_items(self):
|
||||
for name, address in self.item_name_to_id.items():
|
||||
# Specific items will be included in the item pool under certain conditions. See generate_basic
|
||||
if name != "Basin of Vows":
|
||||
self.multiworld.itempool += [self.create_item(name)]
|
||||
if name == "Basin of Vows":
|
||||
continue
|
||||
# Do not add progressive_items ( containing "#" ) to the itempool if the option is disabled
|
||||
if (not self.multiworld.enable_progressive_locations[self.player]) and "#" in name:
|
||||
continue
|
||||
# Do not add DLC items if the option is disabled
|
||||
if (not self.multiworld.enable_dlc[self.player]) and DarkSouls3Item.is_dlc_item(name):
|
||||
continue
|
||||
# Do not add DLC Progressives if both options are disabled
|
||||
if ((not self.multiworld.enable_progressive_locations[self.player]) or (not self.multiworld.enable_dlc[self.player])) and DarkSouls3Item.is_dlc_progressive(name):
|
||||
continue
|
||||
self.multiworld.itempool += [self.create_item(name)]
|
||||
|
||||
def generate_early(self):
|
||||
pass
|
||||
@@ -194,15 +228,23 @@ class DarkSouls3World(World):
|
||||
lambda state: state.has("Grand Archives Key", self.player))
|
||||
set_rule(self.multiworld.get_entrance("Goto Kiln Of The First Flame", self.player),
|
||||
lambda state: state.has("Cinders of a Lord - Abyss Watcher", self.player) and
|
||||
state.has("Cinders of a Lord - Yhorm the Giant", self.player) and
|
||||
state.has("Cinders of a Lord - Aldrich", self.player) and
|
||||
state.has("Cinders of a Lord - Lothric Prince", self.player))
|
||||
state.has("Cinders of a Lord - Yhorm the Giant", self.player) and
|
||||
state.has("Cinders of a Lord - Aldrich", self.player) and
|
||||
state.has("Cinders of a Lord - Lothric Prince", self.player))
|
||||
# DLC Access Rules Below
|
||||
if self.multiworld.enable_dlc[self.player]:
|
||||
set_rule(self.multiworld.get_entrance("Goto Painted World of Ariandel", self.player),
|
||||
lambda state: state.has("Contraption Key", self.player))
|
||||
set_rule(self.multiworld.get_entrance("Goto Ringed City", self.player),
|
||||
lambda state: state.has("Small Envoy Banner", self.player))
|
||||
|
||||
# Define the access rules to some specific locations
|
||||
set_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player),
|
||||
lambda state: state.has("Basin of Vows", self.player))
|
||||
set_rule(self.multiworld.get_location("HWL: Greirat's Ashes", self.player),
|
||||
lambda state: state.has("Cell Key", self.player))
|
||||
set_rule(self.multiworld.get_location("HWL: Blue Tearstone Ring", self.player),
|
||||
lambda state: state.has("Cell Key", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Bellowing Dragoncrest Ring", self.player),
|
||||
lambda state: state.has("Jailbreaker's Key", self.player))
|
||||
set_rule(self.multiworld.get_location("ID: Prisoner Chief's Ashes", self.player),
|
||||
@@ -242,17 +284,38 @@ class DarkSouls3World(World):
|
||||
|
||||
# Depending on the specified option, modify items hexadecimal value to add an upgrade level
|
||||
item_dictionary_copy = item_dictionary.copy()
|
||||
if self.multiworld.randomize_weapons_level[self.player]:
|
||||
# Randomize some weapons upgrades
|
||||
for name in weapons_upgrade_5_table.keys():
|
||||
if self.multiworld.random.randint(0, 100) < 33:
|
||||
value = self.multiworld.random.randint(1, 5)
|
||||
item_dictionary_copy[name] += value
|
||||
if self.multiworld.randomize_weapons_level[self.player] > 0:
|
||||
# if the user made an error and set a min higher than the max we default to the max
|
||||
max_5 = self.multiworld.max_levels_in_5[self.player]
|
||||
min_5 = min(self.multiworld.min_levels_in_5[self.player], max_5)
|
||||
max_10 = self.multiworld.max_levels_in_10[self.player]
|
||||
min_10 = min(self.multiworld.min_levels_in_10[self.player], max_10)
|
||||
weapons_percentage = self.multiworld.randomize_weapons_percentage[self.player]
|
||||
|
||||
for name in weapons_upgrade_10_table.keys():
|
||||
if self.multiworld.random.randint(0, 100) < 33:
|
||||
value = self.multiworld.random.randint(1, 10)
|
||||
item_dictionary_copy[name] += value
|
||||
# Randomize some weapons upgrades
|
||||
if self.multiworld.randomize_weapons_level[self.player] in [1, 3]: # Options are either all or +5
|
||||
for name in weapons_upgrade_5_table.keys():
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage:
|
||||
value = self.multiworld.per_slot_randoms[self.player].randint(min_5, max_5)
|
||||
item_dictionary_copy[name] += value
|
||||
|
||||
if self.multiworld.randomize_weapons_level[self.player] in [1, 2]: # Options are either all or +10
|
||||
for name in weapons_upgrade_10_table.keys():
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage:
|
||||
value = self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10)
|
||||
item_dictionary_copy[name] += value
|
||||
|
||||
if self.multiworld.randomize_weapons_level[self.player] in [1, 3]:
|
||||
for name in dlc_weapons_upgrade_5_table.keys():
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage:
|
||||
value = self.multiworld.per_slot_randoms[self.player].randint(min_5, max_5)
|
||||
item_dictionary_copy[name] += value
|
||||
|
||||
if self.multiworld.randomize_weapons_level[self.player] in [1, 2]:
|
||||
for name in dlc_weapons_upgrade_10_table.keys():
|
||||
if self.multiworld.per_slot_randoms[self.player].randint(1, 100) < weapons_percentage:
|
||||
value = self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10)
|
||||
item_dictionary_copy[name] += value
|
||||
|
||||
# Create the mandatory lists to generate the player's output file
|
||||
items_id = []
|
||||
@@ -281,6 +344,7 @@ class DarkSouls3World(World):
|
||||
"death_link": self.multiworld.death_link[self.player].value,
|
||||
"no_spell_requirements": self.multiworld.no_spell_requirements[self.player].value,
|
||||
"no_equip_load": self.multiworld.no_equip_load[self.player].value,
|
||||
"enable_dlc": self.multiworld.enable_dlc[self.player].value
|
||||
},
|
||||
"seed": self.multiworld.seed_name, # to verify the server's multiworld
|
||||
"slot": self.multiworld.player_name[self.player], # to connect to server
|
||||
|
||||
@@ -13,7 +13,6 @@ weapons_upgrade_5_table = {
|
||||
"Izalith Staff": 0x00C96A80,
|
||||
"Fume Ultra Greatsword": 0x0060E4B0,
|
||||
"Black Knight Sword": 0x005F5E10,
|
||||
|
||||
"Yorshka's Spear": 0x008C3A70,
|
||||
"Smough's Great Hammer": 0x007E30B0,
|
||||
"Dragonslayer Greatbow": 0x00CF8500,
|
||||
@@ -25,7 +24,6 @@ weapons_upgrade_5_table = {
|
||||
"Dragonslayer Spear": 0x008CAFA0,
|
||||
"Caitha's Chime": 0x00CA06C0,
|
||||
"Sunlight Straight Sword": 0x00203230,
|
||||
|
||||
"Firelink Greatsword": 0x0060BDA0,
|
||||
"Hollowslayer Greatsword": 0x00604870,
|
||||
"Arstor's Spear": 0x008BEC50,
|
||||
@@ -37,7 +35,6 @@ weapons_upgrade_5_table = {
|
||||
"Wolnir's Holy Sword": 0x005FFA50,
|
||||
"Demon's Greataxe": 0x006CA480,
|
||||
"Demon's Fist": 0x00A84DF0,
|
||||
|
||||
"Old King's Great Hammer": 0x007CF830,
|
||||
"Greatsword of Judgment": 0x005E2590,
|
||||
"Profaned Greatsword": 0x005E4CA0,
|
||||
@@ -55,6 +52,29 @@ weapons_upgrade_5_table = {
|
||||
"Irithyll Rapier": 0x002E8A10
|
||||
}
|
||||
|
||||
dlc_weapons_upgrade_5_table = {
|
||||
"Friede's Great Scythe": 0x009B55A0,
|
||||
"Rose of Ariandel": 0x00B82C70,
|
||||
"Demon's Scar": 0x003F04D0, # Assigned to "RC: Church Guardian Shiv"
|
||||
"Frayed Blade": 0x004D35A0, # Assigned to "RC: Ritual Spear Fragment"
|
||||
"Gael's Greatsword": 0x00227C20, # Assigned to "RC: Violet Wrappings"
|
||||
"Repeating Crossbow": 0x00D885B0, # Assigned to "RC: Blood of the Dark Souls"
|
||||
"Onyx Blade": 0x00222E00, # VILHELM FIGHT
|
||||
"Earth Seeker": 0x006D8EE0,
|
||||
"Quakestone Hammer": 0x007ECCF0,
|
||||
"Millwood Greatbow": 0x00D85EA0,
|
||||
"Valorheart": 0x00F646E0,
|
||||
"Aquamarine Dagger": 0x00116520,
|
||||
"Ringed Knight Straight Sword": 0x00225510,
|
||||
"Ledo's Great Hammer": 0x007EF400, # INVADER FIGHT
|
||||
"Ringed Knight Spear": 0x008CFDC0,
|
||||
"Crucifix of the Mad King": 0x008D4BE0,
|
||||
"Sacred Chime of Filianore": 0x00CCECF0,
|
||||
"Preacher's Right Arm": 0x00CD1400,
|
||||
"White Birch Bow": 0x00D77440,
|
||||
"Ringed Knight Paired Greatswords": 0x00F69500
|
||||
}
|
||||
|
||||
weapons_upgrade_10_table = {
|
||||
"Broken Straight Sword": 0x001EF9B0,
|
||||
"Deep Battle Axe": 0x0006AFA54,
|
||||
@@ -73,7 +93,6 @@ weapons_upgrade_10_table = {
|
||||
"Red Hilted Halberd": 0x009AB960,
|
||||
"Saint's Talisman": 0x00CACA10,
|
||||
"Large Club": 0x007AFC60,
|
||||
|
||||
"Brigand Twindaggers": 0x00F50E60,
|
||||
"Butcher Knife": 0x006BE130,
|
||||
"Brigand Axe": 0x006B1DE0,
|
||||
@@ -104,9 +123,24 @@ weapons_upgrade_10_table = {
|
||||
"Drakeblood Greatsword": 0x00609690,
|
||||
"Greatlance": 0x008A8CC0,
|
||||
"Sniper Crossbow": 0x00D83790,
|
||||
|
||||
"Claw": 0x00A7D8C0,
|
||||
"Drang Twinspears": 0x00F5AAA0,
|
||||
"Pyromancy Flame": 0x00CC77C0 #given/dropped by Cornyx
|
||||
}
|
||||
|
||||
dlc_weapons_upgrade_10_table = {
|
||||
"Follower Sabre": 0x003EDDC0,
|
||||
"Millwood Battle Axe": 0x006D67D0,
|
||||
"Follower Javelin": 0x008CD6B0,
|
||||
"Crow Talons": 0x00A89C10,
|
||||
"Pyromancer's Parting Flame": 0x00CC9ED0,
|
||||
"Crow Quills": 0x00F66DF0,
|
||||
"Follower Torch": 0x015F1AD0,
|
||||
"Murky Hand Scythe": 0x00118C30,
|
||||
"Herald Curved Greatsword": 0x006159E0,
|
||||
"Lothric War Banner": 0x008D24D0,
|
||||
"Splitleaf Greatsword": 0x009B2E90, # SHOP ITEM
|
||||
"Murky Longstaff": 0x00CCC5E0,
|
||||
}
|
||||
|
||||
shields_table = {
|
||||
@@ -132,7 +166,15 @@ shields_table = {
|
||||
"Golden Wing Crest Shield": 0x0143CAA0,
|
||||
"Ancient Dragon Greatshield": 0x013599D0,
|
||||
"Spirit Tree Crest Shield": 0x014466E0,
|
||||
"Blessed Red and White Shield": 0x01343FB9,
|
||||
"Blessed Red and White Shield": 0x01343FB9
|
||||
}
|
||||
|
||||
dlc_shields_table = {
|
||||
"Followers Shield": 0x0135C0E0,
|
||||
"Ethereal Oak Shield": 0x01450320,
|
||||
"Giant Door Shield": 0x00F5F8C0,
|
||||
"Dragonhead Shield": 0x0135E7F0,
|
||||
"Dragonhead Greatshield": 0x01452A30
|
||||
}
|
||||
|
||||
goods_table = {
|
||||
@@ -167,7 +209,55 @@ goods_table = {
|
||||
**{"Soul of a Great Champion #"+str(i): 0x400001A4 for i in range(1, 3)},
|
||||
**{"Soul of a Champion #"+str(i): 0x400001A3 for i in range(1, 5)},
|
||||
**{"Soul of a Venerable Old Hand #"+str(i): 0x400001A2 for i in range(1, 5)},
|
||||
**{"Soul of a Crestfallen Knight #"+str(i): 0x40000199 for i in range(1, 11)},
|
||||
**{"Soul of a Crestfallen Knight #"+str(i): 0x40000199 for i in range(1, 11)}
|
||||
}
|
||||
|
||||
goods_2_table = { # Added by Br00ty
|
||||
"HWL: Gold Pine Resin #": 0x4000014B,
|
||||
"US: Charcoal Pine Resin #": 0x4000014A,
|
||||
"FK: Gold Pine Bundle #": 0x40000155,
|
||||
"CC: Carthus Rouge #": 0x4000014F,
|
||||
"ID: Pale Pine Resin #": 0x40000150,
|
||||
**{"Ember #"+str(i): 0x400001F4 for i in range(1, 45)},
|
||||
**{"Titanite Shard #"+str(i): 0x400003E8 for i in range(11, 16)},
|
||||
**{"Large Titanite Shard #"+str(i): 0x400003E9 for i in range(11, 16)},
|
||||
**{"Titanite Scale #" + str(i): 0x400003FC for i in range(1, 25)}
|
||||
}
|
||||
|
||||
goods_3_table = { # Added by Br00ty
|
||||
**{"Fading Soul #" + str(i): 0x40000190 for i in range(1, 4)},
|
||||
**{"Ring of Sacrifice #"+str(i): 0x20004EF2 for i in range(1, 5)},
|
||||
**{"Homeward Bone #"+str(i): 0x4000015E for i in range(1, 17)},
|
||||
**{"Green Blossom #"+str(i): 0x40000104 for i in range(1, 7)},
|
||||
**{"Human Pine Resin #"+str(i): 0x4000014E for i in range(1, 3)},
|
||||
**{"Charcoal Pine Bundle #"+str(i): 0x40000154 for i in range(1, 3)},
|
||||
**{"Rotten Pine Resin #"+str(i): 0x40000157 for i in range(1, 3)},
|
||||
**{"Alluring Skull #"+str(i): 0x40000126 for i in range(1, 9)},
|
||||
**{"Rusted Coin #"+str(i): 0x400001C7 for i in range(1, 3)},
|
||||
**{"Rusted Gold Coin #"+str(i): 0x400001C9 for i in range(1, 3)},
|
||||
**{"Titanite Chunk #"+str(i): 0x400003EA for i in range(1, 17)},
|
||||
**{"Twinkling Titanite #"+str(i): 0x40000406 for i in range(1, 8)}
|
||||
}
|
||||
|
||||
dlc_goods_table = {
|
||||
"Soul of Sister Friede": 0x400002E8,
|
||||
"Soul of the Demon Prince": 0x400002EA,
|
||||
"Soul of Darkeater Midir": 0x400002EB,
|
||||
"Soul of Slave Knight Gael": 0x400002E9
|
||||
}
|
||||
|
||||
dlc_goods_2_table = { #71
|
||||
**{"Large Soul of an Unknown Traveler $"+str(i): 0x40000194 for i in range(1, 10)},
|
||||
**{"Soul of a Weary Warrior $"+str(i): 0x40000197 for i in range(1, 6)},
|
||||
**{"Large Soul of a Weary Warrior $"+str(i): 0x40000198 for i in range(1, 7)},
|
||||
**{"Soul of a Crestfallen Knight $"+str(i): 0x40000199 for i in range(1, 7)},
|
||||
**{"Large Soul of a Crestfallen Knight $"+str(i): 0x4000019A for i in range(1, 4)},
|
||||
**{"Homeward Bone $"+str(i): 0x4000015E for i in range(1, 7)},
|
||||
**{"Large Titanite Shard $"+str(i): 0x400003E9 for i in range(1, 4)},
|
||||
**{"Titanite Chunk $"+str(i): 0x400003EA for i in range(1, 16)},
|
||||
**{"Twinkling Titanite $"+str(i): 0x40000406 for i in range(1, 6)},
|
||||
**{"Rusted Coin $"+str(i): 0x400001C7 for i in range(1, 4)},
|
||||
**{"Ember $"+str(i): 0x400001F4 for i in range(1, 11)}
|
||||
}
|
||||
|
||||
armor_table = {
|
||||
@@ -265,6 +355,69 @@ armor_table = {
|
||||
"Outrider Knight Armor": 0x1328BB28,
|
||||
"Outrider Knight Gauntlets": 0x1328BF10,
|
||||
"Outrider Knight Leggings": 0x1328C2F8,
|
||||
|
||||
"Cornyx's Wrap": 0x11946370,
|
||||
"Cornyx's Garb": 0x11945F88,
|
||||
"Cornyx's Skirt": 0x11946758
|
||||
}
|
||||
|
||||
dlc_armor_table = {
|
||||
"Slave Knight Hood": 0x134EDCE0,
|
||||
"Slave Knight Armor": 0x134EE0C8,
|
||||
"Slave Knight Gauntlets": 0x134EE4B0,
|
||||
"Slave Knight Leggings": 0x134EE898,
|
||||
"Vilhelm's Helm": 0x11312D00,
|
||||
"Vilhelm's Armor": 0x113130E8,
|
||||
"Vilhelm's Gauntlets": 0x113134D0,
|
||||
"Vilhelm's Leggings": 0x113138B8,
|
||||
#"Millwood Knight Helm": 0x139B2820, # SHOP ITEM
|
||||
#"Millwood Knight Armor": 0x139B2C08, # SHOP ITEM
|
||||
#"Millwood Knight Gauntlets": 0x139B2FF0, # SHOP ITEM
|
||||
#"Millwood Knight Leggings": 0x139B33D8, # SHOP ITEM
|
||||
|
||||
"Shira's Crown": 0x11C22260,
|
||||
"Shira's Armor": 0x11C22648,
|
||||
"Shira's Gloves": 0x11C22A30,
|
||||
"Shira's Trousers": 0x11C22E18,
|
||||
#"Lapp's Helm": 0x11E84800, # SHOP ITEM
|
||||
#"Lapp's Armor": 0x11E84BE8, # SHOP ITEM
|
||||
#"Lapp's Gauntlets": 0x11E84FD0, # SHOP ITEM
|
||||
#"Lapp's Leggings": 0x11E853B8, # SHOP ITEM
|
||||
#"Ringed Knight Hood": 0x13C8EEE0, # RANDOM ENEMY DROP
|
||||
#"Ringed Knight Armor": 0x13C8F2C8, # RANDOM ENEMY DROP
|
||||
#"Ringed Knight Gauntlets": 0x13C8F6B0, # RANDOM ENEMY DROP
|
||||
#"Ringed Knight Leggings": 0x13C8FA98, # RANDOM ENEMY DROP
|
||||
#"Harald Legion Armor": 0x13D83508, # RANDOM ENEMY DROP
|
||||
#"Harald Legion Gauntlets": 0x13D838F0, # RANDOM ENEMY DROP
|
||||
#"Harald Legion Leggings": 0x13D83CD8, # RANDOM ENEMY DROP
|
||||
"Iron Dragonslayer Helm": 0x1405F7E0,
|
||||
"Iron Dragonslayer Armor": 0x1405FBC8,
|
||||
"Iron Dragonslayer Gauntlets": 0x1405FFB0,
|
||||
"Iron Dragonslayer Leggings": 0x14060398,
|
||||
|
||||
"Ruin Sentinel Helm": 0x14CC5520,
|
||||
"Ruin Sentinel Armor": 0x14CC5908,
|
||||
"Ruin Sentinel Gauntlets": 0x14CC5CF0,
|
||||
"Ruin Sentinel Leggings": 0x14CC60D8,
|
||||
"Desert Pyromancer Hood": 0x14DB9760,
|
||||
"Desert Pyromancer Garb": 0x14DB9B48,
|
||||
"Desert Pyromancer Gloves": 0x14DB9F30,
|
||||
"Desert Pyromancer Skirt": 0x14DBA318,
|
||||
|
||||
#"Follower Helm": 0x137CA3A0, # RANDOM ENEMY DROP
|
||||
#"Follower Armor": 0x137CA788, # RANDOM ENEMY DROP
|
||||
#"Follower Gloves": 0x137CAB70, # RANDOM ENEMY DROP
|
||||
#"Follower Boots": 0x137CAF58, # RANDOM ENEMY DROP
|
||||
#"Ordained Hood": 0x135E1F20, # SHOP ITEM
|
||||
#"Ordained Dress": 0x135E2308, # SHOP ITEM
|
||||
#"Ordained Trousers": 0x135E2AD8, # SHOP ITEM
|
||||
"Black Witch Veil": 0x14FA1BE0,
|
||||
"Black Witch Hat": 0x14EAD9A0,
|
||||
"Black Witch Garb": 0x14EADD88,
|
||||
"Black Witch Wrappings": 0x14EAE170,
|
||||
"Black Witch Trousers": 0x14EAE558,
|
||||
"White Preacher Head": 0x14153A20,
|
||||
"Antiquated Plain Garb": 0x11B2E408
|
||||
}
|
||||
|
||||
rings_table = {
|
||||
@@ -314,6 +467,12 @@ rings_table = {
|
||||
"Dragonscale Ring": 0x2000515E,
|
||||
"Knight Slayer's Ring": 0x20005000,
|
||||
"Magic Stoneplate Ring": 0x20004E66,
|
||||
"Blue Tearstone Ring": 0x20004ED4 #given/dropped by Greirat
|
||||
}
|
||||
|
||||
dlc_ring_table = {
|
||||
"Havel's Ring": 0x20004E34,
|
||||
"Chillbite Ring": 0x20005208
|
||||
}
|
||||
|
||||
spells_table = {
|
||||
@@ -335,7 +494,21 @@ spells_table = {
|
||||
"Divine Pillars of Light": 0x4038C340,
|
||||
"Great Magic Barrier": 0x40365628,
|
||||
"Great Magic Shield": 0x40144F38,
|
||||
"Crystal Scroll": 0x40000856,
|
||||
"Crystal Scroll": 0x40000856
|
||||
}
|
||||
|
||||
dlc_spells_table = {
|
||||
#"Boulder Heave": 0x40282170, # KILN STRAY DEMON
|
||||
#"Seething Chaos": 0x402896A0, # KILN DEMON PRINCES
|
||||
#"Old Moonlight": 0x4014FF00, # KILN MIDIR
|
||||
"Frozen Weapon": 0x401408E8,
|
||||
"Snap Freeze": 0x401A90C8,
|
||||
"Great Soul Dregs": 0x401879A0,
|
||||
"Flame Fan": 0x40258190,
|
||||
"Lightning Arrow": 0x40358B08,
|
||||
"Way of White Corona": 0x403642A0,
|
||||
"Projected Heal": 0x40364688,
|
||||
"Floating Chaos": 0x40257DA8
|
||||
}
|
||||
|
||||
misc_items_table = {
|
||||
@@ -347,7 +520,7 @@ misc_items_table = {
|
||||
"Braille Divine Tome of Carim": 0x40000847, # Shop
|
||||
"Great Swamp Pyromancy Tome": 0x4000084F, # Shop
|
||||
"Farron Coal ": 0x40000837, # Shop
|
||||
"Paladin's Ashes": 0x4000083D, #Shop
|
||||
"Paladin's Ashes": 0x4000083D, # Shop
|
||||
"Deep Braille Divine Tome": 0x40000860, # Shop
|
||||
"Small Doll": 0x400007D5,
|
||||
"Golden Scroll": 0x4000085C,
|
||||
@@ -388,6 +561,12 @@ misc_items_table = {
|
||||
"Orbeck's Ashes": 0x40000840
|
||||
}
|
||||
|
||||
dlc_misc_table = {
|
||||
"Captains Ashes": 0x4000086A,
|
||||
"Contraption Key": 0x4000086B, # Needed for Painted World
|
||||
"Small Envoy Banner": 0x4000086C # Needed to get to Ringed City from Dreg Heap
|
||||
}
|
||||
|
||||
key_items_list = {
|
||||
"Small Lothric Banner",
|
||||
"Basin of Vows",
|
||||
@@ -405,8 +584,17 @@ key_items_list = {
|
||||
"Prisoner Chief's Ashes",
|
||||
"Old Cell Key",
|
||||
"Jailer's Key Ring",
|
||||
"Contraption Key",
|
||||
"Small Envoy Banner"
|
||||
}
|
||||
|
||||
item_tables = [weapons_upgrade_5_table, weapons_upgrade_10_table, shields_table, armor_table, rings_table, spells_table, misc_items_table, goods_table]
|
||||
item_tables = [weapons_upgrade_5_table, weapons_upgrade_10_table, shields_table,
|
||||
armor_table, rings_table, spells_table, misc_items_table, goods_table, goods_2_table, goods_3_table,
|
||||
dlc_weapons_upgrade_5_table, dlc_weapons_upgrade_10_table, dlc_shields_table, dlc_goods_table,
|
||||
dlc_armor_table, dlc_spells_table, dlc_ring_table, dlc_misc_table, dlc_goods_2_table]
|
||||
|
||||
item_dictionary = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table,
|
||||
**armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table, **goods_2_table,
|
||||
**goods_3_table, **dlc_weapons_upgrade_5_table, **dlc_weapons_upgrade_10_table, **dlc_shields_table,
|
||||
**dlc_goods_table, **dlc_armor_table, **dlc_spells_table, **dlc_ring_table, **dlc_misc_table, **dlc_goods_2_table}
|
||||
|
||||
item_dictionary = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table}
|
||||
|
||||
@@ -42,6 +42,7 @@ high_wall_of_lothric = {
|
||||
"HWL: Soul of the Dancer": 0x400002CA,
|
||||
"HWL: Way of Blue Covenant": 0x2000274C,
|
||||
"HWL: Greirat's Ashes": 0x4000083F,
|
||||
"HWL: Blue Tearstone Ring": 0x20004ED4 #given/dropped by Greirat
|
||||
}
|
||||
|
||||
undead_settlement_table = {
|
||||
@@ -91,7 +92,11 @@ undead_settlement_table = {
|
||||
"US: Warrior of Sunlight Covenant": 0x20002738,
|
||||
"US: Blessed Red and White Shield": 0x01343FB9,
|
||||
"US: Irina's Ashes": 0x40000843,
|
||||
"US: Cornyx's Ashes": 0x40000841
|
||||
"US: Cornyx's Ashes": 0x40000841,
|
||||
"US: Cornyx's Wrap": 0x11946370,
|
||||
"US: Cornyx's Garb": 0x11945F88,
|
||||
"US: Cornyx's Skirt": 0x11946758,
|
||||
"US: Pyromancy Flame": 0x00CC77C0 #given/dropped by Cornyx
|
||||
}
|
||||
|
||||
road_of_sacrifice_table = {
|
||||
@@ -437,6 +442,101 @@ archdragon_peak_table = {
|
||||
"AP: Havel's Greatshield": 0x013376F0,
|
||||
}
|
||||
|
||||
painted_world_table = { # DLC
|
||||
"PW: Follower Javelin": 0x008CD6B0,
|
||||
"PW: Frozen Weapon": 0x401408E8,
|
||||
"PW: Millwood Greatbow": 0x00D85EA0,
|
||||
"PW: Captains Ashes": 0x4000086A,
|
||||
"PW: Millwood Battle Axe": 0x006D67D0,
|
||||
"PW: Ethereal Oak Shield": 0x01450320,
|
||||
"PW: Crow Quills": 0x00F66DF0,
|
||||
"PW: Slave Knight Hood": 0x134EDCE0,
|
||||
"PW: Slave Knight Armor": 0x134EE0C8,
|
||||
"PW: Slave Knight Gauntlets": 0x134EE4B0,
|
||||
"PW: Slave Knight Leggings": 0x134EE898,
|
||||
"PW: Way of White Corona": 0x403642A0,
|
||||
"PW: Crow Talons": 0x00A89C10,
|
||||
"PW: Quakestone Hammer": 0x007ECCF0,
|
||||
"PW: Earth Seeker": 0x006D8EE0,
|
||||
"PW: Follower Torch": 0x015F1AD0,
|
||||
"PW: Follower Shield": 0x0135C0E0,
|
||||
"PW: Follower Sabre": 0x003EDDC0,
|
||||
"PW: Snap Freeze": 0x401A90C8,
|
||||
"PW: Floating Chaos": 0x40257DA8,
|
||||
"PW: Pyromancer's Parting Flame": 0x00CC9ED0,
|
||||
"PW: Vilhelm's Helm": 0x11312D00,
|
||||
"PW: Vilhelm's Armor": 0x113130E8,
|
||||
"PW: Vilhelm's Gauntlets": 0x113134D0,
|
||||
"PW: Vilhelm's Leggings": 0x113138B8,
|
||||
"PW: Vilhelm's Leggings": 0x113138B8,
|
||||
"PW: Valorheart": 0x00F646E0, # GRAVETENDER FIGHT
|
||||
"PW: Champions Bones": 0x40000869, # GRAVETENDER FIGHT
|
||||
"PW: Onyx Blade": 0x00222E00, # VILHELM FIGHT
|
||||
"PW: Soul of Sister Friede": 0x400002E8,
|
||||
"PW: Titanite Slab": 0x400003EB,
|
||||
"PW: Chillbite Ring": 0x20005208,
|
||||
"PW: Contraption Key": 0x4000086B # VILHELM FIGHT/NEEDED TO PROGRESS THROUGH PW
|
||||
}
|
||||
|
||||
dreg_heap_table = { # DLC
|
||||
"DH: Loincloth": 0x11B2EBD8,
|
||||
"DH: Aquamarine Dagger": 0x00116520,
|
||||
"DH: Murky Hand Scythe": 0x00118C30,
|
||||
"DH: Murky Longstaff": 0x00CCC5E0,
|
||||
"DH: Great Soul Dregs": 0x401879A0,
|
||||
"DH: Lothric War Banner": 0x00CCC5E0,
|
||||
"DH: Projected Heal": 0x40364688,
|
||||
"DH: Desert Pyromancer Hood": 0x14DB9760,
|
||||
"DH: Desert Pyromancer Garb": 0x14DB9B48,
|
||||
"DH: Desert Pyromancer Gloves": 0x14DB9F30,
|
||||
"DH: Desert Pyromancer Skirt": 0x14DBA318,
|
||||
"DH: Giant Door Shield": 0x00F5F8C0,
|
||||
"DH: Herald Curved Greatsword": 0x006159E0,
|
||||
"DH: Flame Fan": 0x40258190,
|
||||
"DH: Soul of the Demon Prince": 0x400002EA,
|
||||
"DH: Small Envoy Banner": 0x4000086C # NEEDED TO TRAVEL TO RINGED CITY
|
||||
}
|
||||
|
||||
ringed_city_table = { # DLC
|
||||
"RC: Ruin Sentinel Helm": 0x14CC5520,
|
||||
"RC: Ruin Sentinel Armor": 0x14CC5908,
|
||||
"RC: Ruin Sentinel Gauntlets": 0x14CC5CF0,
|
||||
"RC: Ruin Sentinel Leggings": 0x14CC60D8,
|
||||
"RC: Black Witch Veil": 0x14FA1BE0,
|
||||
"RC: Black Witch Hat": 0x14EAD9A0,
|
||||
"RC: Black Witch Garb": 0x14EADD88,
|
||||
"RC: Black Witch Wrappings": 0x14EAE170,
|
||||
"RC: Black Witch Trousers": 0x14EAE558,
|
||||
"RC: White Preacher Head": 0x14153A20,
|
||||
"RC: Havel's Ring": 0x20004E34,
|
||||
"RC: Ringed Knight Spear": 0x008CFDC0,
|
||||
"RC: Dragonhead Shield": 0x0135E7F0,
|
||||
"RC: Ringed Knight Straight Sword": 0x00225510,
|
||||
"RC: Preacher's Right Arm": 0x00CD1400,
|
||||
"RC: White Birch Bow": 0x00D77440,
|
||||
"RC: Church Guardian Shiv": 0x4000013B, # Assigned to "Demon's Scar"
|
||||
"RC: Dragonhead Greatshield": 0x01452A30,
|
||||
"RC: Ringed Knight Paired Greatswords": 0x00F69500,
|
||||
"RC: Shira's Crown": 0x11C22260,
|
||||
"RC: Shira's Armor": 0x11C22648,
|
||||
"RC: Shira's Gloves": 0x11C22A30,
|
||||
"RC: Shira's Trousers": 0x11C22E18,
|
||||
"RC: Titanite Slab": 0x400003EB, # SHIRA DROP
|
||||
"RC: Crucifix of the Mad King": 0x008D4BE0, # SHIRA DROP
|
||||
"RC: Sacred Chime of Filianore": 0x00CCECF0, # SHIRA DROP
|
||||
"RC: Iron Dragonslayer Helm": 0x1405F7E0,
|
||||
"RC: Iron Dragonslayer Armor": 0x1405FBC8,
|
||||
"RC: Iron Dragonslayer Gauntlets": 0x1405FFB0,
|
||||
"RC: Iron Dragonslayer Leggings": 0x14060398,
|
||||
"RC: Lightning Arrow": 0x40358B08,
|
||||
"RC: Ritual Spear Fragment": 0x4000028A, # Assigned to "Frayed Blade"
|
||||
"RC: Antiquated Plain Garb": 0x11B2E408,
|
||||
"RC: Violet Wrappings": 0x11B2E7F0, # Assigned to "Gael's Greatsword"
|
||||
"RC: Soul of Darkeater Midir": 0x400002EB,
|
||||
"RC: Soul of Slave Knight Gael": 0x400002E9,
|
||||
"RC: Blood of the Dark Souls": 0x4000086E, # Assigned to "Repeating Crossbow"
|
||||
}
|
||||
|
||||
progressive_locations = {
|
||||
# Upgrade materials
|
||||
**{"Titanite Shard #"+str(i): 0x400003E8 for i in range(1, 11)},
|
||||
@@ -456,15 +556,60 @@ progressive_locations = {
|
||||
**{"Soul of a Deserted Corpse #" + str(i): 0x40000191 for i in range(1, 6)},
|
||||
**{"Large Soul of a Deserted Corpse #" + str(i): 0x40000192 for i in range(1, 6)},
|
||||
**{"Soul of an Unknown Traveler #" + str(i): 0x40000193 for i in range(1, 6)},
|
||||
**{"Large Soul of an Unknown Traveler #" + str(i): 0x40000194 for i in range(1, 6)},
|
||||
**{"Large Soul of an Unknown Traveler #" + str(i): 0x40000194 for i in range(1, 6)}
|
||||
}
|
||||
|
||||
progressive_locations_2 = {
|
||||
##Added by Br00ty
|
||||
"HWL: Gold Pine Resin #": 0x4000014B,
|
||||
"US: Charcoal Pine Resin #": 0x4000014A,
|
||||
"FK: Gold Pine Bundle #": 0x40000155,
|
||||
"CC: Carthus Rouge #": 0x4000014F,
|
||||
"ID: Pale Pine Resin #": 0x40000150,
|
||||
**{"Titanite Scale #" + str(i): 0x400003FC for i in range(1, 27)},
|
||||
**{"Fading Soul #" + str(i): 0x40000190 for i in range(1, 4)},
|
||||
**{"Ring of Sacrifice #"+str(i): 0x20004EF2 for i in range(1, 5)},
|
||||
**{"Homeward Bone #"+str(i): 0x4000015E for i in range(1, 17)},
|
||||
**{"Ember #"+str(i): 0x400001F4 for i in range(1, 46)},
|
||||
}
|
||||
|
||||
progressive_locations_3 = {
|
||||
**{"Green Blossom #" + str(i): 0x40000104 for i in range(1, 7)},
|
||||
**{"Human Pine Resin #" + str(i): 0x4000014E for i in range(1, 3)},
|
||||
**{"Charcoal Pine Bundle #" + str(i): 0x40000154 for i in range(1, 3)},
|
||||
**{"Rotten Pine Resin #" + str(i): 0x40000157 for i in range(1, 3)},
|
||||
**{"Pale Tongue #" + str(i): 0x40000175 for i in range(1, 3)},
|
||||
**{"Alluring Skull #" + str(i): 0x40000126 for i in range(1, 3)},
|
||||
**{"Undead Hunter Charm #" + str(i): 0x40000128 for i in range(1, 3)},
|
||||
**{"Duel Charm #" + str(i): 0x40000130 for i in range(1, 3)},
|
||||
**{"Rusted Coin #" + str(i): 0x400001C7 for i in range(1, 3)},
|
||||
**{"Rusted Gold Coin #" + str(i): 0x400001C9 for i in range(1, 4)},
|
||||
**{"Titanite Chunk #"+str(i): 0x400003EA for i in range(1, 17)},
|
||||
**{"Twinkling Titanite #"+str(i): 0x40000406 for i in range(1, 8)}
|
||||
}
|
||||
|
||||
dlc_progressive_locations = { #71
|
||||
**{"Large Soul of an Unknown Traveler $"+str(i): 0x40000194 for i in range(1, 10)},
|
||||
**{"Soul of a Weary Warrior $"+str(i): 0x40000197 for i in range(1, 6)},
|
||||
**{"Large Soul of a Weary Warrior $"+str(i): 0x40000198 for i in range(1, 7)},
|
||||
**{"Soul of a Crestfallen Knight $"+str(i): 0x40000199 for i in range(1, 7)},
|
||||
**{"Large Soul of a Crestfallen Knight $"+str(i): 0x4000019A for i in range(1, 4)},
|
||||
**{"Homeward Bone $"+str(i): 0x4000015E for i in range(1, 7)},
|
||||
**{"Large Titanite Shard $"+str(i): 0x400003E9 for i in range(1, 4)},
|
||||
**{"Titanite Chunk $"+str(i): 0x400003EA for i in range(1, 16)},
|
||||
**{"Twinkling Titanite $"+str(i): 0x40000406 for i in range(1, 6)},
|
||||
**{"Rusted Coin $"+str(i): 0x400001C7 for i in range(1, 4)},
|
||||
**{"Ember $"+str(i): 0x400001F4 for i in range(1, 11)}
|
||||
}
|
||||
|
||||
location_tables = [fire_link_shrine_table, firelink_shrine_bell_tower_table, high_wall_of_lothric, undead_settlement_table, road_of_sacrifice_table,
|
||||
cathedral_of_the_deep_table, farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table,
|
||||
irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, consumed_king_garden_table,
|
||||
grand_archives_table, untended_graves_table, archdragon_peak_table, progressive_locations]
|
||||
grand_archives_table, untended_graves_table, archdragon_peak_table, progressive_locations, progressive_locations_2, progressive_locations_3,
|
||||
painted_world_table, dreg_heap_table, ringed_city_table, dlc_progressive_locations]
|
||||
|
||||
location_dictionary = {**fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table,
|
||||
**cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table,
|
||||
**irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table,
|
||||
**grand_archives_table, **untended_graves_table, **archdragon_peak_table, **progressive_locations}
|
||||
**grand_archives_table, **untended_graves_table, **archdragon_peak_table, **progressive_locations, **progressive_locations_2, **progressive_locations_3,
|
||||
**painted_world_table, **dreg_heap_table, **ringed_city_table, **dlc_progressive_locations}
|
||||
|
||||
@@ -7,19 +7,20 @@ config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are randomized.
|
||||
This exclude the upgrade materials such as the titanite shards, the estus shards and the consumables which remain at
|
||||
the same location. I also added an option available from the settings page to randomize the level of the generated
|
||||
weapons(from +0 to +10/+5)
|
||||
In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are
|
||||
randomized.
|
||||
An option is available from the settings page to also randomize the upgrade materials, the Estus shards and the
|
||||
consumables.
|
||||
Another option is available to randomize the level of the generated weapons(from +0 to +10/+5)
|
||||
|
||||
To beat the game you need to collect the 4 "Cinders of a Lord" randomized in the multiworld
|
||||
To beat the game you need to collect the 4 "Cinders of a Lord" randomized in the multiworld
|
||||
and kill the final boss "Soul of Cinder"
|
||||
|
||||
## What Dark Souls III items can appear in other players' worlds?
|
||||
|
||||
Every unique items from Dark Souls III can appear in other player's worlds, such as a piece of armor, an upgraded weapon
|
||||
Every unique item from Dark Souls III can appear in other player's worlds, such as a piece of armor, an upgraded weapon,
|
||||
or a key item.
|
||||
|
||||
## What does another world's item look like in Dark Souls III?
|
||||
|
||||
In Dark Souls III, items which need to be sent to other worlds appear as a Prism Stone.
|
||||
In Dark Souls III, items which need to be sent to other worlds appear as a Prism Stone.
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
|
||||
- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases)
|
||||
|
||||
## Optional Software
|
||||
|
||||
- [Dark Souls III Maptracker Pack](https://github.com/Br00ty/DS3_AP_Maptracker/releases/latest), for use with [Poptracker](https://github.com/black-sliver/PopTracker/releases)
|
||||
|
||||
## General Concept
|
||||
|
||||
The Dark Souls III AP Client is a dinput8.dll triggered when launching Dark Souls III. This .dll file will launch a command
|
||||
|
||||
@@ -740,5 +740,5 @@ def get_base_rom_path(file_name: str = "") -> str:
|
||||
if not file_name:
|
||||
file_name = options["dkc3_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.local_path(file_name)
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
@@ -422,7 +422,7 @@ for root in sorted_rows:
|
||||
progressive = progressive_rows[root]
|
||||
assert all(tech in tech_table for tech in progressive), "declared a progressive technology without base technology"
|
||||
factorio_id += 1
|
||||
progressive_technology = Technology(root, technology_table[progressive_rows[root][0]].ingredients, factorio_id,
|
||||
progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_id,
|
||||
progressive,
|
||||
has_modifier=any(technology_table[tech].has_modifier for tech in progressive),
|
||||
unlocks=any(technology_table[tech].unlocks for tech in progressive))
|
||||
|
||||
@@ -4,14 +4,17 @@ from Options import OptionDict
|
||||
|
||||
|
||||
class Locations(OptionDict):
|
||||
"""to roll settings go to https://finalfantasyrandomizer.com/"""
|
||||
display_name = "locations"
|
||||
|
||||
|
||||
class Items(OptionDict):
|
||||
"""to roll settings go to https://finalfantasyrandomizer.com/"""
|
||||
display_name = "items"
|
||||
|
||||
|
||||
class Rules(OptionDict):
|
||||
"""to roll settings go to https://finalfantasyrandomizer.com/"""
|
||||
display_name = "rules"
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import collections
|
||||
import typing
|
||||
|
||||
from BaseClasses import LocationProgressType, MultiWorld
|
||||
from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import BaseClasses
|
||||
@@ -143,14 +143,41 @@ def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str
|
||||
def item_in_locations(state: "BaseClasses.CollectionState", item: str, player: int,
|
||||
locations: typing.Sequence["BaseClasses.Location"]) -> bool:
|
||||
for location in locations:
|
||||
if item_name(state, location[0], location[1]) == (item, player):
|
||||
if location_item_name(state, location[0], location[1]) == (item, player):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \
|
||||
def location_item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \
|
||||
typing.Optional[typing.Tuple[str, int]]:
|
||||
location = state.multiworld.get_location(location, player)
|
||||
if location.item is None:
|
||||
return None
|
||||
return location.item.name, location.item.player
|
||||
|
||||
|
||||
def allow_self_locking_items(spot: typing.Union[Location, Region], *item_names: str) -> None:
|
||||
"""
|
||||
This function sets rules on the supplied spot, such that the supplied item_name(s) can possibly be placed there.
|
||||
|
||||
spot: Location or Region that the item(s) are allowed to be placed in
|
||||
item_names: item name or names that are allowed to be placed in the Location or Region
|
||||
"""
|
||||
player = spot.player
|
||||
|
||||
def add_allowed_rules(area: typing.Union[Location, Entrance], location: Location) -> None:
|
||||
def set_always_allow(location: Location, rule: typing.Callable) -> None:
|
||||
location.always_allow = rule
|
||||
|
||||
for item_name in item_names:
|
||||
add_rule(area, lambda state, item_name=item_name:
|
||||
location_item_name(state, location.name, player) == (item_name, player), "or")
|
||||
set_always_allow(location, lambda state, item:
|
||||
item.player == player and item.name in [item_name for item_name in item_names])
|
||||
|
||||
if isinstance(spot, Region):
|
||||
for entrance in spot.entrances:
|
||||
for location in spot.locations:
|
||||
add_allowed_rules(entrance, location)
|
||||
else:
|
||||
add_allowed_rules(spot, spot)
|
||||
|
||||
153
worlds/messenger/Constants.py
Normal file
153
worlds/messenger/Constants.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# items
|
||||
# listing individual groups first for easy lookup
|
||||
NOTES = [
|
||||
"Key of Hope",
|
||||
"Key of Chaos",
|
||||
"Key of Courage",
|
||||
"Key of Love",
|
||||
"Key of Strength",
|
||||
"Key of Symbiosis"
|
||||
]
|
||||
|
||||
PROG_ITEMS = [
|
||||
"Wingsuit",
|
||||
"Rope Dart",
|
||||
"Ninja Tabi",
|
||||
"Power Thistle",
|
||||
"Demon King Crown",
|
||||
"Ruxxtin's Amulet",
|
||||
"Fairy Bottle",
|
||||
"Sun Crest",
|
||||
"Moon Crest",
|
||||
# "Astral Seed",
|
||||
# "Astral Tea Leaves"
|
||||
]
|
||||
|
||||
PHOBEKINS = [
|
||||
"Necro",
|
||||
"Pyro",
|
||||
"Claustro",
|
||||
"Acro"
|
||||
]
|
||||
|
||||
USEFUL_ITEMS = [
|
||||
"Windmill Shuriken"
|
||||
]
|
||||
|
||||
# item_name_to_id needs to be deterministic and match upstream
|
||||
ALL_ITEMS = [
|
||||
*NOTES,
|
||||
"Windmill Shuriken",
|
||||
"Wingsuit",
|
||||
"Rope Dart",
|
||||
"Ninja Tabi",
|
||||
# "Astral Seed",
|
||||
# "Astral Tea Leaves",
|
||||
"Candle",
|
||||
"Seashell",
|
||||
"Power Thistle",
|
||||
"Demon King Crown",
|
||||
"Ruxxtin's Amulet",
|
||||
"Fairy Bottle",
|
||||
"Sun Crest",
|
||||
"Moon Crest",
|
||||
*PHOBEKINS,
|
||||
"Power Seal",
|
||||
"Time Shard" # there's 45 separate instances of this in the client lookup, but hopefully we don't care?
|
||||
]
|
||||
|
||||
# locations
|
||||
# the names of these don't actually matter, but using the upstream's names for now
|
||||
# order must be exactly the same as upstream
|
||||
ALWAYS_LOCATIONS = [
|
||||
# notes
|
||||
"Key of Love",
|
||||
"Key of Courage",
|
||||
"Key of Chaos",
|
||||
"Key of Symbiosis",
|
||||
"Key of Strength",
|
||||
"Key of Hope",
|
||||
# upgrades
|
||||
"Wingsuit",
|
||||
"Rope Dart",
|
||||
"Ninja Tabi",
|
||||
"Climbing Claws",
|
||||
# quest items
|
||||
"Astral Seed",
|
||||
"Astral Tea Leaves",
|
||||
"Candle",
|
||||
"Seashell",
|
||||
"Power Thistle",
|
||||
"Demon King Crown",
|
||||
"Ruxxtin's Amulet",
|
||||
"Fairy Bottle",
|
||||
"Sun Crest",
|
||||
"Moon Crest",
|
||||
# phobekins
|
||||
"Necro",
|
||||
"Pyro",
|
||||
"Claustro",
|
||||
"Acro"
|
||||
]
|
||||
|
||||
SEALS = [
|
||||
"Ninja Village Seal - Tree House",
|
||||
|
||||
"Autumn Hills Seal - Trip Saws",
|
||||
"Autumn Hills Seal - Double Swing Saws",
|
||||
"Autumn Hills Seal - Spike Ball Swing",
|
||||
"Autumn Hills Seal - Spike Ball Darts",
|
||||
|
||||
"Catacombs Seal - Triple Spike Crushers",
|
||||
"Catacombs Seal - Crusher Gauntlet",
|
||||
"Catacombs Seal - Dirty Pond",
|
||||
|
||||
"Bamboo Creek Seal - Spike Crushers and Doors",
|
||||
"Bamboo Creek Seal - Spike Ball Pits",
|
||||
"Bamboo Creek Seal - Spike Crushers and Doors v2",
|
||||
|
||||
"Howling Grotto Seal - Windy Saws and Balls",
|
||||
"Howling Grotto Seal - Crushing Pits",
|
||||
"Howling Grotto Seal - Breezy Crushers",
|
||||
|
||||
"Quillshroom Marsh Seal - Spikey Window",
|
||||
"Quillshroom Marsh Seal - Sand Trap",
|
||||
"Quillshroom Marsh Seal - Do the Spike Wave",
|
||||
|
||||
"Searing Crags Seal - Triple Ball Spinner",
|
||||
"Searing Crags Seal - Raining Rocks",
|
||||
"Searing Crags Seal - Rhythm Rocks",
|
||||
|
||||
"Glacial Peak Seal - Ice Climbers",
|
||||
"Glacial Peak Seal - Projectile Spike Pit",
|
||||
"Glacial Peak Seal - Glacial Air Swag",
|
||||
|
||||
"Tower of Time Seal - Time Waster Seal",
|
||||
"Tower of Time Seal - Lantern Climb",
|
||||
"Tower of Time Seal - Arcane Orbs",
|
||||
|
||||
"Cloud Ruins Seal - Ghost Pit",
|
||||
"Cloud Ruins Seal - Toothbrush Alley",
|
||||
"Cloud Ruins Seal - Saw Pit",
|
||||
"Cloud Ruins Seal - Money Farm Room",
|
||||
|
||||
"Underworld Seal - Sharp and Windy Climb",
|
||||
"Underworld Seal - Spike Wall",
|
||||
"Underworld Seal - Fireball Wave",
|
||||
"Underworld Seal - Rising Fanta",
|
||||
|
||||
"Forlorn Temple Seal - Rocket Maze",
|
||||
"Forlorn Temple Seal - Rocket Sunset",
|
||||
|
||||
"Sunken Shrine Seal - Ultra Lifeguard",
|
||||
"Sunken Shrine Seal - Waterfall Paradise",
|
||||
"Sunken Shrine Seal - Tabi Gauntlet",
|
||||
|
||||
"Riviere Turquoise Seal - Bounces and Balls",
|
||||
"Riviere Turquoise Seal - Launch of Faith",
|
||||
"Riviere Turquoise Seal - Flower Power",
|
||||
|
||||
"Elemental Skylands Seal - Air",
|
||||
"Elemental Skylands Seal - Water",
|
||||
"Elemental Skylands Seal - Fire"
|
||||
]
|
||||
66
worlds/messenger/Options.py
Normal file
66
worlds/messenger/Options.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice
|
||||
|
||||
|
||||
class MessengerAccessibility(Accessibility):
|
||||
default = Accessibility.option_locations
|
||||
# defaulting to locations accessibility since items makes certain items self-locking
|
||||
__doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}")
|
||||
|
||||
|
||||
class Logic(DefaultOnToggle):
|
||||
"""Whether the seed should be guaranteed completable."""
|
||||
display_name = "Use Logic"
|
||||
|
||||
|
||||
class PowerSeals(DefaultOnToggle):
|
||||
"""Whether power seal locations should be randomized."""
|
||||
display_name = "Shuffle Seals"
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
"""Requirement to finish the game. Power Seal Hunt will force power seal locations to be shuffled."""
|
||||
display_name = "Goal"
|
||||
option_open_music_box = 0
|
||||
option_power_seal_hunt = 1
|
||||
|
||||
|
||||
class MusicBox(DefaultOnToggle):
|
||||
"""Whether the music box gauntlet needs to be done."""
|
||||
display_name = "Music Box Gauntlet"
|
||||
|
||||
|
||||
class NotesNeeded(Range):
|
||||
"""How many notes are needed to access the Music Box."""
|
||||
display_name = "Notes Needed"
|
||||
range_start = 1
|
||||
range_end = 6
|
||||
default = range_end
|
||||
|
||||
|
||||
class AmountSeals(Range):
|
||||
"""Number of power seals that exist in the item pool when power seal hunt is the goal."""
|
||||
display_name = "Total Power Seals"
|
||||
range_start = 1
|
||||
range_end = 45
|
||||
default = range_end
|
||||
|
||||
|
||||
class RequiredSeals(Range):
|
||||
"""Percentage of total seals required to open the shop chest."""
|
||||
display_name = "Percent Seals Required"
|
||||
range_start = 10
|
||||
range_end = 100
|
||||
default = range_end
|
||||
|
||||
|
||||
messenger_options = {
|
||||
"accessibility": MessengerAccessibility,
|
||||
"enable_logic": Logic,
|
||||
"shuffle_seals": PowerSeals,
|
||||
"goal": Goal,
|
||||
"music_box": MusicBox,
|
||||
"notes_needed": NotesNeeded,
|
||||
"total_seals": AmountSeals,
|
||||
"percent_seals_required": RequiredSeals,
|
||||
"death_link": DeathLink,
|
||||
}
|
||||
52
worlds/messenger/Regions.py
Normal file
52
worlds/messenger/Regions.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from typing import Dict, Set, List
|
||||
|
||||
REGIONS: Dict[str, List[str]] = {
|
||||
"Menu": [],
|
||||
"Tower HQ": [],
|
||||
"The Shop": [],
|
||||
"Tower of Time": [],
|
||||
"Ninja Village": ["Candle", "Astral Seed"],
|
||||
"Autumn Hills": ["Climbing Claws", "Key of Hope"],
|
||||
"Forlorn Temple": ["Demon King Crown"],
|
||||
"Catacombs": ["Necro", "Ruxxtin's Amulet"],
|
||||
"Bamboo Creek": ["Claustro"],
|
||||
"Howling Grotto": ["Wingsuit"],
|
||||
"Quillshroom Marsh": ["Seashell"],
|
||||
"Searing Crags": ["Rope Dart"],
|
||||
"Searing Crags Upper": ["Power Thistle", "Key of Strength", "Astral Tea Leaves"],
|
||||
"Glacial Peak": [],
|
||||
"Cloud Ruins": ["Acro"],
|
||||
"Underworld": ["Pyro", "Key of Chaos"],
|
||||
"Dark Cave": [],
|
||||
"Riviere Turquoise": ["Fairy Bottle"],
|
||||
"Sunken Shrine": ["Ninja Tabi", "Sun Crest", "Moon Crest", "Key of Love"],
|
||||
"Elemental Skylands": ["Key of Symbiosis"],
|
||||
"Corrupted Future": ["Key of Courage"],
|
||||
"Music Box": ["Rescue Phantom"]
|
||||
}
|
||||
"""seal locations have the region in their name and may not need to be created so skip them here"""
|
||||
|
||||
|
||||
REGION_CONNECTIONS: Dict[str, Set[str]] = {
|
||||
"Menu": {"Tower HQ"},
|
||||
"Tower HQ": {"Autumn Hills", "Howling Grotto", "Searing Crags", "Glacial Peak", "Tower of Time", "Riviere Turquoise",
|
||||
"Sunken Shrine", "Corrupted Future", "The Shop", "Music Box"},
|
||||
"Tower of Time": set(),
|
||||
"Ninja Village": set(),
|
||||
"Autumn Hills": {"Ninja Village", "Forlorn Temple", "Catacombs"},
|
||||
"Forlorn Temple": {"Catacombs", "Bamboo Creek"},
|
||||
"Catacombs": {"Autumn Hills", "Bamboo Creek", "Dark Cave"},
|
||||
"Bamboo Creek": {"Catacombs", "Howling Grotto"},
|
||||
"Howling Grotto": {"Bamboo Creek", "Quillshroom Marsh", "Sunken Shrine"},
|
||||
"Quillshroom Marsh": {"Howling Grotto", "Searing Crags"},
|
||||
"Searing Crags": {"Searing Crags Upper", "Quillshroom Marsh", "Underworld"},
|
||||
"Searing Crags Upper": {"Searing Crags", "Glacial Peak"},
|
||||
"Glacial Peak": {"Searing Crags Upper", "Tower HQ", "Cloud Ruins", "Elemental Skylands"},
|
||||
"Cloud Ruins": {"Underworld"},
|
||||
"Underworld": set(),
|
||||
"Dark Cave": {"Catacombs", "Riviere Turquoise"},
|
||||
"Riviere Turquoise": set(),
|
||||
"Sunken Shrine": {"Howling Grotto"},
|
||||
"Elemental Skylands": set()
|
||||
}
|
||||
"""Vanilla layout mapping with all Tower HQ portals open. from -> to"""
|
||||
125
worlds/messenger/Rules.py
Normal file
125
worlds/messenger/Rules.py
Normal file
@@ -0,0 +1,125 @@
|
||||
from typing import Dict, Callable, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
from worlds.generic.Rules import set_rule, allow_self_locking_items
|
||||
from .Options import MessengerAccessibility, Goal
|
||||
from .Constants import NOTES, PHOBEKINS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MessengerWorld
|
||||
else:
|
||||
MessengerWorld = object
|
||||
|
||||
|
||||
class MessengerRules:
|
||||
player: int
|
||||
world: MessengerWorld
|
||||
|
||||
def __init__(self, world: MessengerWorld):
|
||||
self.player = world.player
|
||||
self.world = world
|
||||
|
||||
self.region_rules: Dict[str, Callable[[CollectionState], bool]] = {
|
||||
"Ninja Village": self.has_wingsuit,
|
||||
"Autumn Hills": self.has_wingsuit,
|
||||
"Catacombs": self.has_wingsuit,
|
||||
"Bamboo Creek": self.has_wingsuit,
|
||||
"Searing Crags Upper": self.has_vertical,
|
||||
"Cloud Ruins": lambda state: self.has_wingsuit(state) and state.has("Ruxxtin's Amulet", self.player),
|
||||
"Underworld": self.has_tabi,
|
||||
"Forlorn Temple": lambda state: state.has_all(PHOBEKINS, self.player) and self.has_wingsuit(state),
|
||||
"Glacial Peak": self.has_vertical,
|
||||
"Elemental Skylands": lambda state: state.has("Fairy Bottle", self.player),
|
||||
"Music Box": lambda state: state.has_all(NOTES, self.player)
|
||||
}
|
||||
|
||||
self.location_rules: Dict[str, Callable[[CollectionState], bool]] = {
|
||||
# ninja village
|
||||
"Ninja Village Seal - Tree House": self.has_dart,
|
||||
# autumn hills
|
||||
"Key of Hope": self.has_dart,
|
||||
# howling grotto
|
||||
"Howling Grotto Seal - Windy Saws and Balls": self.has_wingsuit,
|
||||
"Howling Grotto Seal - Crushing Pits": lambda state: self.has_wingsuit(state) and self.has_dart(state),
|
||||
# searing crags
|
||||
"Key of Strength": lambda state: state.has("Power Thistle", self.player),
|
||||
# glacial peak
|
||||
"Glacial Peak Seal - Ice Climbers": self.has_dart,
|
||||
"Glacial Peak Seal - Projectile Spike Pit": self.has_vertical,
|
||||
"Glacial Peak Seal - Glacial Air Swag": self.has_vertical,
|
||||
# tower of time
|
||||
"Tower of Time Seal - Time Waster Seal": self.has_dart,
|
||||
"Tower of Time Seal - Lantern Climb": self.has_wingsuit,
|
||||
"Tower of Time Seal - Arcane Orbs": lambda state: self.has_wingsuit(state) and self.has_dart(state),
|
||||
# underworld
|
||||
"Underworld Seal - Sharp and Windy Climb": self.has_wingsuit,
|
||||
"Underworld Seal - Fireball Wave": self.has_wingsuit,
|
||||
"Underworld Seal - Rising Fanta": self.has_dart,
|
||||
# sunken shrine
|
||||
"Sun Crest": self.has_tabi,
|
||||
"Moon Crest": self.has_tabi,
|
||||
"Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player),
|
||||
"Sunken Shrine Seal - Waterfall Paradise": self.has_tabi,
|
||||
"Sunken Shrine Seal - Tabi Gauntlet": self.has_tabi,
|
||||
# riviere turquoise
|
||||
"Fairy Bottle": self.has_vertical,
|
||||
"Riviere Turquoise Seal - Flower Power": self.has_vertical,
|
||||
# elemental skylands
|
||||
"Key of Symbiosis": self.has_dart,
|
||||
"Elemental Skylands Seal - Air": self.has_wingsuit,
|
||||
"Elemental Skylands Seal - Water": self.has_dart,
|
||||
"Elemental Skylands Seal - Fire": self.has_dart,
|
||||
# corrupted future
|
||||
"Key of Courage": lambda state: state.has_all({"Demon King Crown", "Fairy Bottle"}, self.player),
|
||||
# the shop
|
||||
"Shop Chest": self.has_enough_seals
|
||||
}
|
||||
|
||||
def has_wingsuit(self, state: CollectionState) -> bool:
|
||||
return state.has("Wingsuit", self.player)
|
||||
|
||||
def has_dart(self, state: CollectionState) -> bool:
|
||||
return state.has("Rope Dart", self.player)
|
||||
|
||||
def has_tabi(self, state: CollectionState) -> bool:
|
||||
return state.has("Ninja Tabi", self.player)
|
||||
|
||||
def has_vertical(self, state: CollectionState) -> bool:
|
||||
return self.has_wingsuit(state) or self.has_dart(state)
|
||||
|
||||
def has_enough_seals(self, state: CollectionState) -> bool:
|
||||
required_seals = state.multiworld.worlds[self.player].required_seals
|
||||
return state.has("Power Seal", self.player, required_seals)
|
||||
|
||||
def set_messenger_rules(self) -> None:
|
||||
multiworld = self.world.multiworld
|
||||
|
||||
for region in multiworld.get_regions(self.player):
|
||||
if region.name in self.region_rules:
|
||||
for entrance in region.entrances:
|
||||
entrance.access_rule = self.region_rules[region.name]
|
||||
for loc in region.locations:
|
||||
if loc.name in self.location_rules:
|
||||
loc.access_rule = self.location_rules[loc.name]
|
||||
if multiworld.goal[self.player] == Goal.option_power_seal_hunt:
|
||||
set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player),
|
||||
lambda state: state.has("Shop Chest", self.player))
|
||||
|
||||
if multiworld.enable_logic[self.player]:
|
||||
multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player)
|
||||
else:
|
||||
multiworld.accessibility[self.player].value = MessengerAccessibility.option_minimal
|
||||
if multiworld.accessibility[self.player] > MessengerAccessibility.option_locations:
|
||||
set_self_locking_items(multiworld, self.player)
|
||||
|
||||
|
||||
def set_self_locking_items(multiworld: MultiWorld, player: int) -> None:
|
||||
# do the ones for seal shuffle on and off first
|
||||
allow_self_locking_items(multiworld.get_location("Key of Strength", player), "Power Thistle")
|
||||
allow_self_locking_items(multiworld.get_location("Key of Love", player), "Sun Crest", "Moon Crest")
|
||||
allow_self_locking_items(multiworld.get_location("Key of Courage", player), "Demon King Crown")
|
||||
|
||||
# add these locations when seals aren't shuffled
|
||||
if not multiworld.shuffle_seals[player]:
|
||||
allow_self_locking_items(multiworld.get_region("Cloud Ruins", player), "Ruxxtin's Amulet")
|
||||
allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS)
|
||||
58
worlds/messenger/SubClasses.py
Normal file
58
worlds/messenger/SubClasses.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from typing import Set, TYPE_CHECKING, Optional, Dict
|
||||
|
||||
from BaseClasses import Region, Location, Item, ItemClassification, Entrance
|
||||
from .Constants import SEALS, NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS
|
||||
from .Options import Goal
|
||||
from .Regions import REGIONS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MessengerWorld
|
||||
else:
|
||||
MessengerWorld = object
|
||||
|
||||
|
||||
class MessengerRegion(Region):
|
||||
def __init__(self, name: str, world: MessengerWorld):
|
||||
super().__init__(name, world.player, world.multiworld)
|
||||
self.add_locations(self.multiworld.worlds[self.player].location_name_to_id)
|
||||
world.multiworld.regions.append(self)
|
||||
|
||||
def add_locations(self, name_to_id: Dict[str, int]) -> None:
|
||||
for loc in REGIONS[self.name]:
|
||||
self.locations.append(MessengerLocation(loc, self, name_to_id.get(loc, None)))
|
||||
if self.name == "The Shop" and self.multiworld.goal[self.player] > Goal.option_open_music_box:
|
||||
self.locations.append(MessengerLocation("Shop Chest", self, name_to_id.get("Shop Chest", None)))
|
||||
# putting some dumb special case for searing crags and ToT so i can split them into 2 regions
|
||||
if self.multiworld.shuffle_seals[self.player] and self.name not in {"Searing Crags", "Tower HQ"}:
|
||||
for seal_loc in SEALS:
|
||||
if seal_loc.startswith(self.name.split(" ")[0]):
|
||||
self.locations.append(MessengerLocation(seal_loc, self, name_to_id.get(seal_loc, None)))
|
||||
|
||||
def add_exits(self, exits: Set[str]) -> None:
|
||||
for exit in exits:
|
||||
ret = Entrance(self.player, f"{self.name} -> {exit}", self)
|
||||
self.exits.append(ret)
|
||||
ret.connect(self.multiworld.get_region(exit, self.player))
|
||||
|
||||
|
||||
class MessengerLocation(Location):
|
||||
game = "The Messenger"
|
||||
|
||||
def __init__(self, name: str, parent: MessengerRegion, loc_id: Optional[int]):
|
||||
super().__init__(parent.player, name, loc_id, parent)
|
||||
if loc_id is None:
|
||||
self.place_locked_item(MessengerItem(name, parent.player, None))
|
||||
|
||||
|
||||
class MessengerItem(Item):
|
||||
game = "The Messenger"
|
||||
|
||||
def __init__(self, name: str, player: int, item_id: Optional[int] = None):
|
||||
if name in {*NOTES, *PROG_ITEMS, *PHOBEKINS} or item_id is None:
|
||||
item_class = ItemClassification.progression
|
||||
elif name in USEFUL_ITEMS:
|
||||
item_class = ItemClassification.useful
|
||||
else:
|
||||
item_class = ItemClassification.filler
|
||||
super().__init__(name, item_class, item_id, player)
|
||||
|
||||
125
worlds/messenger/__init__.py
Normal file
125
worlds/messenger/__init__.py
Normal file
@@ -0,0 +1,125 @@
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from BaseClasses import Tutorial, ItemClassification
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS, ALWAYS_LOCATIONS, SEALS, ALL_ITEMS
|
||||
from .Options import messenger_options, NotesNeeded, Goal, PowerSeals
|
||||
from .Regions import REGIONS, REGION_CONNECTIONS
|
||||
from .Rules import MessengerRules
|
||||
from .SubClasses import MessengerRegion, MessengerItem
|
||||
|
||||
|
||||
class MessengerWeb(WebWorld):
|
||||
theme = "ocean"
|
||||
|
||||
bug_report_page = "https://github.com/minous27/TheMessengerRandomizerMod/issues"
|
||||
|
||||
tut_en = Tutorial(
|
||||
"Multiworld Setup Tutorial",
|
||||
"A guide to setting up The Messenger randomizer on your computer.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["alwaysintreble"]
|
||||
)
|
||||
|
||||
tutorials = [tut_en]
|
||||
|
||||
|
||||
class MessengerWorld(World):
|
||||
"""
|
||||
As a demon army besieges his village, a young ninja ventures through a cursed world, to deliver a scroll paramount
|
||||
to his clan’s survival. What begins as a classic action platformer soon unravels into an expansive time-traveling
|
||||
adventure full of thrills, surprises, and humor.
|
||||
"""
|
||||
game = "The Messenger"
|
||||
|
||||
item_name_groups = {
|
||||
"Notes": set(NOTES),
|
||||
"Keys": set(NOTES),
|
||||
"Crest": {"Sun Crest", "Moon Crest"},
|
||||
"Phobe": set(PHOBEKINS),
|
||||
"Phobekin": set(PHOBEKINS),
|
||||
"Shuriken": {"Windmill Shuriken"},
|
||||
}
|
||||
|
||||
option_definitions = messenger_options
|
||||
|
||||
base_offset = 0xADD_000
|
||||
item_name_to_id = {item: item_id
|
||||
for item_id, item in enumerate(ALL_ITEMS, base_offset)}
|
||||
location_name_to_id = {location: location_id
|
||||
for location_id, location in enumerate([*ALWAYS_LOCATIONS, *SEALS], base_offset)}
|
||||
|
||||
data_version = 1
|
||||
|
||||
web = MessengerWeb()
|
||||
|
||||
total_seals: Optional[int] = None
|
||||
required_seals: Optional[int] = None
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
|
||||
self.multiworld.shuffle_seals[self.player].value = PowerSeals.option_true
|
||||
self.total_seals = self.multiworld.total_seals[self.player].value
|
||||
self.required_seals = int(self.multiworld.percent_seals_required[self.player].value / 100 * self.total_seals)
|
||||
|
||||
def create_regions(self) -> None:
|
||||
for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]:
|
||||
if region.name in REGION_CONNECTIONS:
|
||||
region.add_exits(REGION_CONNECTIONS[region.name])
|
||||
|
||||
def create_items(self) -> None:
|
||||
itempool: List[MessengerItem] = []
|
||||
if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt:
|
||||
seals = [self.create_item("Power Seal") for _ in range(self.total_seals)]
|
||||
for i in range(self.required_seals):
|
||||
seals[i].classification = ItemClassification.progression_skip_balancing
|
||||
itempool += seals
|
||||
else:
|
||||
notes = self.multiworld.random.sample(NOTES, k=len(NOTES))
|
||||
precollected_notes_amount = NotesNeeded.range_end - self.multiworld.notes_needed[self.player]
|
||||
if precollected_notes_amount:
|
||||
for note in notes[:precollected_notes_amount]:
|
||||
self.multiworld.push_precollected(self.create_item(note))
|
||||
itempool += [self.create_item(note) for note in notes[precollected_notes_amount:]]
|
||||
|
||||
itempool += [self.create_item(item)
|
||||
for item in self.item_name_to_id
|
||||
if item not in
|
||||
{
|
||||
"Power Seal", "Time Shard", *NOTES,
|
||||
*{collected_item.name for collected_item in self.multiworld.precollected_items[self.player]}
|
||||
# this is a set and currently won't create items for anything that appears in here at all
|
||||
# if we get in a position where this can have duplicates of items that aren't Power Seals
|
||||
# or Time shards, this will need to be redone.
|
||||
}]
|
||||
itempool += [self.create_filler()
|
||||
for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool))]
|
||||
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
def set_rules(self) -> None:
|
||||
MessengerRules(self).set_messenger_rules()
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
locations: Dict[int, List[str]] = {}
|
||||
for loc in self.multiworld.get_filled_locations(self.player):
|
||||
if loc.item.code:
|
||||
locations[loc.address] = [loc.item.name, self.multiworld.player_name[loc.item.player]]
|
||||
|
||||
return {
|
||||
"deathlink": self.multiworld.death_link[self.player].value,
|
||||
"goal": self.multiworld.goal[self.player].current_key,
|
||||
"music_box": self.multiworld.music_box[self.player].value,
|
||||
"required_seals": self.required_seals,
|
||||
"locations": locations,
|
||||
"settings": {"Difficulty": "Basic" if not self.multiworld.shuffle_seals[self.player] else "Advanced"}
|
||||
}
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return "Time Shard"
|
||||
|
||||
def create_item(self, name: str) -> MessengerItem:
|
||||
item_id: Optional[int] = self.item_name_to_id.get(name, None)
|
||||
return MessengerItem(name, self.player, item_id)
|
||||
73
worlds/messenger/docs/en_The Messenger.md
Normal file
73
worlds/messenger/docs/en_The Messenger.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# The Messenger
|
||||
|
||||
## Quick Links
|
||||
- [Setup](../../../../tutorial/The%20Messenger/setup/en)
|
||||
- [Settings Page](../../../../games/The%20Messenger/player-settings)
|
||||
- [Courier Github](https://github.com/Brokemia/Courier)
|
||||
- [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod)
|
||||
- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker)
|
||||
- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack)
|
||||
|
||||
## What does randomization do in this game?
|
||||
|
||||
All items and upgrades that can be picked up by the player in the game are randomized. The player starts in the Tower of
|
||||
Time HQ with the past section finished, all area portals open, and with the cloud step, and climbing claws already
|
||||
obtained. You'll be forced to do sections of the game in different ways with your current abilities. Currently, logic
|
||||
assumes you already have all shop upgrades.
|
||||
|
||||
## What items can appear in other players' worlds?
|
||||
|
||||
* The player's movement items
|
||||
* Quest and pedestal items
|
||||
* Music Box notes
|
||||
* The Phobekins
|
||||
* Time shards
|
||||
* Power Seals
|
||||
|
||||
## Where can I find items?
|
||||
|
||||
You can find items wherever items can be picked up in the original game. This includes:
|
||||
* Shopkeeper dialog where the player originally gains movement items
|
||||
* Quest Item pickups
|
||||
* Music Box notes
|
||||
* Phobekins
|
||||
* Power seals
|
||||
|
||||
## What are the item name groups?
|
||||
|
||||
When you attempt to hint for items in Archipelago you can use either the name for the specific item, or the name of a
|
||||
group of items. Hinting for a group will choose a random item from the group that you do not currently have and hint
|
||||
for it. The groups you can use for The Messenger are:
|
||||
* Notes - This covers the music notes
|
||||
* Keys - An alternative name for the music notes
|
||||
* Crest - The Sun and Moon Crests
|
||||
* Phobekin - Any of the Phobekins
|
||||
* Phobe - An alternative name for the Phobekins
|
||||
* Shuriken - The windmill shuriken
|
||||
|
||||
## Other changes
|
||||
|
||||
* The player can return to the Tower of Time HQ at any point by selecting the button from the options menu
|
||||
* This can cause issues if used at specific times. Current known:
|
||||
* During Boss fights
|
||||
* After Courage Note collection (Corrupted Future chase)
|
||||
* This is currently an expected action in logic. If you do need to teleport during this chase sequence, it
|
||||
is recommended to quit to title and reload the save
|
||||
* After reaching ninja village a teleport option is added to the menu to reach it quickly
|
||||
* Toggle Windmill Shuriken button is added to option menu once the item is received
|
||||
|
||||
## Currently known issues
|
||||
* Necro cutscene will sometimes not play correctly, but will still reward the item
|
||||
* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item
|
||||
* If you receive the Fairy Bottle while in Quillshroom Marsh, The Decurse Queen cutscene will not play. You can exit
|
||||
to Searing Crags and re-enter to get it to play correctly.
|
||||
* If you defeat Barma'thazël, the cutscene afterward will not play correctly since that is what normally transitions
|
||||
you to 2nd quest. The game will not kill you if you fall here, so you can teleport to HQ at any point after defeating him.
|
||||
* Sometimes upon teleporting back to HQ, Ninja will run left and enter a different portal than the one entered by the
|
||||
player.
|
||||
* Text entry menus don't accept controller input
|
||||
|
||||
## What do I do if I have a problem?
|
||||
|
||||
If you believe something happened that isn't intended, please get the `log.txt`from the folder of your game installation
|
||||
and send a bug report either on github or the [Archipelago Discord Server](http://archipelago.gg/discord)
|
||||
50
worlds/messenger/docs/setup_en.md
Normal file
50
worlds/messenger/docs/setup_en.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# The Messenger Randomizer Setup Guide
|
||||
|
||||
## Quick Links
|
||||
- [Game Info](../../../../games/The%20Messenger/info/en)
|
||||
- [Settings Page](../../../../games/The%20Messenger/player-settings)
|
||||
- [Courier Github](https://github.com/Brokemia/Courier)
|
||||
- [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod)
|
||||
- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker)
|
||||
- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download and install Courier Mod Loader using the instructions on the release page
|
||||
* [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases)
|
||||
2. Download and install the randomizer mod
|
||||
1. Download the latest TheMessengerRandomizer.zip from the [The Messenger Randomizer Mod releases page](https://github.com/minous27/TheMessengerRandomizerMod/releases)
|
||||
2. Extract the zip file to `TheMessenger/Mods/` of your game's install location
|
||||
3. Optionally, Backup your save game
|
||||
* On Windows
|
||||
1. Press `Windows Key + R` to open run
|
||||
2. Type `%appdata%` to access AppData
|
||||
3. Navigate to `AppData/locallow/SabotageStudios/The Messenger`
|
||||
4. Rename `SaveGame.txt` to any name of your choice
|
||||
* On Linux
|
||||
1. Navigate to `steamapps/compatdata/764790/pfx/drive_c/users/steamuser/AppData/LocalLow/Sabotage Studio/The Messenger`
|
||||
2. Rename `SaveGame.txt` to any name of your choice
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Launch the game
|
||||
2. Navigate to `Options > Third Party Mod Options`
|
||||
3. Select `Reset Randomizer File Slots`
|
||||
* This will set up all of your save slots with new randomizer save files. You can have up to 3 randomizer files at a
|
||||
time, but must do this step again to start new runs afterwards.
|
||||
4. Enter connection info using the relevant option buttons
|
||||
* **The game is limited to alphanumerical characters and `-` so when entering the host name replace `.` with ` ` and
|
||||
ensure that your player name when generating a settings file follows these constrictions**
|
||||
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
|
||||
website.
|
||||
5. Select the `Connect to Archipelago` button
|
||||
6. Navigate to save file selection
|
||||
7. Select a new valid randomizer save
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you launch the game, and it hangs on the splash screen for more than 30 seconds try these steps:
|
||||
1. Close the game and remove `TheMessengerRandomizer` from the `Mods` folder.
|
||||
2. Launch The Messenger
|
||||
3. Delete any save slot
|
||||
4. Reinstall the randomizer mod following step 2 of the installation.
|
||||
149
worlds/messenger/test/TestAccess.py
Normal file
149
worlds/messenger/test/TestAccess.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from . import MessengerTestBase
|
||||
from ..Constants import NOTES, PHOBEKINS
|
||||
from ..Options import MessengerAccessibility
|
||||
|
||||
|
||||
class AccessTest(MessengerTestBase):
|
||||
|
||||
def testTabi(self) -> None:
|
||||
"""locations that hard require the Ninja Tabi"""
|
||||
locations = ["Pyro", "Key of Chaos", "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Spike Wall",
|
||||
"Underworld Seal - Fireball Wave", "Underworld Seal - Rising Fanta", "Sun Crest", "Moon Crest",
|
||||
"Sunken Shrine Seal - Waterfall Paradise", "Sunken Shrine Seal - Tabi Gauntlet"]
|
||||
items = [["Ninja Tabi"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def testDart(self) -> None:
|
||||
"""locations that hard require the Rope Dart"""
|
||||
locations = ["Ninja Village Seal - Tree House", "Key of Hope", "Howling Grotto Seal - Crushing Pits",
|
||||
"Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster Seal",
|
||||
"Tower of Time Seal - Arcane Orbs", "Underworld Seal - Rising Fanta", "Key of Symbiosis",
|
||||
"Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire"]
|
||||
items = [["Rope Dart"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def testWingsuit(self) -> None:
|
||||
"""locations that hard require the Wingsuit"""
|
||||
locations = ["Candle", "Ninja Village Seal - Tree House", "Climbing Claws", "Key of Hope",
|
||||
"Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
|
||||
"Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Necro",
|
||||
"Ruxxtin's Amulet", "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet",
|
||||
"Catacombs Seal - Dirty Pond", "Claustro", "Acro", "Bamboo Creek Seal - Spike Crushers and Doors",
|
||||
"Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2",
|
||||
"Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Windy Saws and Balls",
|
||||
"Tower of Time Seal - Lantern Climb", "Demon King Crown", "Cloud Ruins Seal - Ghost Pit",
|
||||
"Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
|
||||
"Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs",
|
||||
"Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave",
|
||||
"Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze",
|
||||
"Forlorn Temple Seal - Rocket Sunset", "Astral Seed"]
|
||||
items = [["Wingsuit"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def testVertical(self) -> None:
|
||||
"""locations that require either the Rope Dart or the Wingsuit"""
|
||||
locations = ["Ninja Village Seal - Tree House", "Key of Hope", "Howling Grotto Seal - Crushing Pits",
|
||||
"Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster Seal",
|
||||
"Underworld Seal - Rising Fanta", "Key of Symbiosis",
|
||||
"Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Candle",
|
||||
"Ninja Village Seal - Tree House", "Climbing Claws", "Key of Hope",
|
||||
"Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws",
|
||||
"Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", "Necro",
|
||||
"Ruxxtin's Amulet", "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet",
|
||||
"Catacombs Seal - Dirty Pond", "Claustro", "Acro", "Bamboo Creek Seal - Spike Crushers and Doors",
|
||||
"Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2",
|
||||
"Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Windy Saws and Balls",
|
||||
"Tower of Time Seal - Lantern Climb", "Demon King Crown", "Cloud Ruins Seal - Ghost Pit",
|
||||
"Cloud Ruins Seal - Toothbrush Alley", "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room",
|
||||
"Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs",
|
||||
"Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave",
|
||||
"Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset",
|
||||
"Power Thistle", "Key of Strength", "Glacial Peak Seal - Projectile Spike Pit",
|
||||
"Glacial Peak Seal - Glacial Air Swag", "Fairy Bottle", "Riviere Turquoise Seal - Flower Power",
|
||||
"Searing Crags Seal - Triple Ball Spinner", "Searing Crags Seal - Raining Rocks",
|
||||
"Searing Crags Seal - Rhythm Rocks", "Astral Seed", "Astral Tea Leaves"]
|
||||
items = [["Wingsuit", "Rope Dart"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def testAmulet(self) -> None:
|
||||
"""Locations that require Ruxxtin's Amulet"""
|
||||
locations = ["Acro", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley",
|
||||
"Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room"]
|
||||
# Cloud Ruins requires Ruxxtin's Amulet
|
||||
items = [["Ruxxtin's Amulet"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def testBottle(self) -> None:
|
||||
"""Elemental Skylands and Corrupted Future require the Fairy Bottle"""
|
||||
locations = ["Key of Symbiosis", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Fire",
|
||||
"Elemental Skylands Seal - Water", "Key of Courage"]
|
||||
items = [["Fairy Bottle"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def testCrests(self) -> None:
|
||||
"""Test Key of Love nonsense"""
|
||||
locations = ["Key of Love"]
|
||||
items = [["Sun Crest", "Moon Crest"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
self.collect_all_but("Sun Crest")
|
||||
self.assertEqual(self.can_reach_location("Key of Love"), False)
|
||||
self.remove(self.get_item_by_name("Moon Crest"))
|
||||
self.collect_by_name("Sun Crest")
|
||||
self.assertEqual(self.can_reach_location("Key of Love"), False)
|
||||
|
||||
def testThistle(self) -> None:
|
||||
"""I'm a chuckster!"""
|
||||
locations = ["Key of Strength"]
|
||||
items = [["Power Thistle"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def testCrown(self) -> None:
|
||||
"""Crocomire but not"""
|
||||
locations = ["Key of Courage"]
|
||||
items = [["Demon King Crown"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def testGoal(self) -> None:
|
||||
"""Test some different states to verify goal requires the correct items"""
|
||||
self.collect_all_but([*NOTES, "Rescue Phantom"])
|
||||
self.assertEqual(self.can_reach_location("Rescue Phantom"), False)
|
||||
self.collect_all_but(["Key of Love", "Rescue Phantom"])
|
||||
self.assertBeatable(False)
|
||||
self.collect_by_name(["Key of Love"])
|
||||
self.assertEqual(self.can_reach_location("Rescue Phantom"), True)
|
||||
self.assertBeatable(True)
|
||||
|
||||
|
||||
class ItemsAccessTest(MessengerTestBase):
|
||||
options = {
|
||||
"shuffle_seals": False,
|
||||
"accessibility": MessengerAccessibility.option_items
|
||||
}
|
||||
|
||||
def testSelfLockingItems(self) -> None:
|
||||
"""Force items that can be self locked to ensure it's valid placement."""
|
||||
location_lock_pairs = {
|
||||
"Key of Strength": ["Power Thistle"],
|
||||
"Key of Love": ["Sun Crest", "Moon Crest"],
|
||||
"Key of Courage": ["Demon King Crown"],
|
||||
"Acro": ["Ruxxtin's Amulet"],
|
||||
"Demon King Crown": PHOBEKINS
|
||||
}
|
||||
|
||||
for loc in location_lock_pairs:
|
||||
for item_name in location_lock_pairs[loc]:
|
||||
item = self.get_item_by_name(item_name)
|
||||
with self.subTest("Fulfills Accessibility", location=loc, item=item_name):
|
||||
self.assertTrue(self.multiworld.get_location(loc, self.player).can_fill(self.multiworld.state, item, True))
|
||||
|
||||
|
||||
class NoLogicTest(MessengerTestBase):
|
||||
options = {
|
||||
"enable_logic": "false"
|
||||
}
|
||||
|
||||
def testNoLogic(self) -> None:
|
||||
"""Test some funny locations to make sure they aren't reachable but we can still win"""
|
||||
self.assertEqual(self.can_reach_location("Pyro"), False)
|
||||
self.assertEqual(self.can_reach_location("Rescue Phantom"), False)
|
||||
self.assertBeatable(True)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user