mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-11 10:03:44 -07:00
Compare commits
2 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ead8efbc24 | ||
|
|
aa1180e0aa |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -132,7 +132,7 @@ jobs:
|
||||
# charset-normalizer was somehow incomplete in the github runner
|
||||
"${{ env.PYTHON }}" -m venv venv
|
||||
source venv/bin/activate
|
||||
"${{ env.PYTHON }}" -m pip install --upgrade pip "PyGObject<3.51.0" charset-normalizer
|
||||
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
|
||||
python setup.py build_exe --yes bdist_appimage --yes
|
||||
echo -e "setup.py build output:\n `ls build`"
|
||||
echo -e "setup.py dist output:\n `ls dist`"
|
||||
|
||||
4
.github/workflows/ctest.yml
vendored
4
.github/workflows/ctest.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
- '**.hh?'
|
||||
- '**.hpp'
|
||||
- '**.hxx'
|
||||
- '**/CMakeLists.txt'
|
||||
- '**.CMakeLists'
|
||||
- '.github/workflows/ctest.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
@@ -21,7 +21,7 @@ on:
|
||||
- '**.hh?'
|
||||
- '**.hpp'
|
||||
- '**.hxx'
|
||||
- '**/CMakeLists.txt'
|
||||
- '**.CMakeLists'
|
||||
- '.github/workflows/ctest.yml'
|
||||
|
||||
jobs:
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
# charset-normalizer was somehow incomplete in the github runner
|
||||
"${{ env.PYTHON }}" -m venv venv
|
||||
source venv/bin/activate
|
||||
"${{ env.PYTHON }}" -m pip install --upgrade pip "PyGObject<3.51.0" charset-normalizer
|
||||
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
|
||||
python setup.py build_exe --yes bdist_appimage --yes
|
||||
echo -e "setup.py build output:\n `ls build`"
|
||||
echo -e "setup.py dist output:\n `ls dist`"
|
||||
|
||||
@@ -28,7 +28,6 @@ from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
||||
from NetUtils import ClientStatus
|
||||
from worlds.ladx.Common import BASE_ID as LABaseID
|
||||
from worlds.ladx.GpsTracker import GpsTracker
|
||||
from worlds.ladx.TrackerConsts import storage_key
|
||||
from worlds.ladx.ItemTracker import ItemTracker
|
||||
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
|
||||
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
|
||||
@@ -101,23 +100,19 @@ class LAClientConstants:
|
||||
WRamCheckSize = 0x4
|
||||
WRamSafetyValue = bytearray([0]*WRamCheckSize)
|
||||
|
||||
wRamStart = 0xC000
|
||||
hRamStart = 0xFF80
|
||||
hRamSize = 0x80
|
||||
|
||||
MinGameplayValue = 0x06
|
||||
MaxGameplayValue = 0x1A
|
||||
VictoryGameplayAndSub = 0x0102
|
||||
|
||||
|
||||
class RAGameboy():
|
||||
cache = []
|
||||
cache_start = 0
|
||||
cache_size = 0
|
||||
last_cache_read = None
|
||||
socket = None
|
||||
|
||||
def __init__(self, address, port) -> None:
|
||||
self.cache_start = LAClientConstants.wRamStart
|
||||
self.cache_size = LAClientConstants.hRamStart + LAClientConstants.hRamSize - LAClientConstants.wRamStart
|
||||
|
||||
self.address = address
|
||||
self.port = port
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
@@ -136,14 +131,9 @@ class RAGameboy():
|
||||
async def get_retroarch_status(self):
|
||||
return await self.send_command("GET_STATUS")
|
||||
|
||||
def set_checks_range(self, checks_start, checks_size):
|
||||
self.checks_start = checks_start
|
||||
self.checks_size = checks_size
|
||||
|
||||
def set_location_range(self, location_start, location_size, critical_addresses):
|
||||
self.location_start = location_start
|
||||
self.location_size = location_size
|
||||
self.critical_location_addresses = critical_addresses
|
||||
def set_cache_limits(self, cache_start, cache_size):
|
||||
self.cache_start = cache_start
|
||||
self.cache_size = cache_size
|
||||
|
||||
def send(self, b):
|
||||
if type(b) is str:
|
||||
@@ -198,57 +188,21 @@ class RAGameboy():
|
||||
if not await self.check_safe_gameplay():
|
||||
return
|
||||
|
||||
attempts = 0
|
||||
while True:
|
||||
# RA doesn't let us do an atomic read of a large enough block of RAM
|
||||
# Some bytes can't change in between reading location_block and hram_block
|
||||
location_block = await self.read_memory_block(self.location_start, self.location_size)
|
||||
hram_block = await self.read_memory_block(LAClientConstants.hRamStart, LAClientConstants.hRamSize)
|
||||
verification_block = await self.read_memory_block(self.location_start, self.location_size)
|
||||
|
||||
valid = True
|
||||
for address in self.critical_location_addresses:
|
||||
if location_block[address - self.location_start] != verification_block[address - self.location_start]:
|
||||
valid = False
|
||||
|
||||
if valid:
|
||||
break
|
||||
|
||||
attempts += 1
|
||||
|
||||
# Shouldn't really happen, but keep it from choking
|
||||
if attempts > 5:
|
||||
return
|
||||
|
||||
checks_block = await self.read_memory_block(self.checks_start, self.checks_size)
|
||||
cache = []
|
||||
remaining_size = self.cache_size
|
||||
while remaining_size:
|
||||
block = await self.async_read_memory(self.cache_start + len(cache), remaining_size)
|
||||
remaining_size -= len(block)
|
||||
cache += block
|
||||
|
||||
if not await self.check_safe_gameplay():
|
||||
return
|
||||
|
||||
self.cache = bytearray(self.cache_size)
|
||||
|
||||
start = self.checks_start - self.cache_start
|
||||
self.cache[start:start + len(checks_block)] = checks_block
|
||||
|
||||
start = self.location_start - self.cache_start
|
||||
self.cache[start:start + len(location_block)] = location_block
|
||||
|
||||
start = LAClientConstants.hRamStart - self.cache_start
|
||||
self.cache[start:start + len(hram_block)] = hram_block
|
||||
|
||||
self.cache = cache
|
||||
self.last_cache_read = time.time()
|
||||
|
||||
async def read_memory_block(self, address: int, size: int):
|
||||
block = bytearray()
|
||||
remaining_size = size
|
||||
while remaining_size:
|
||||
chunk = await self.async_read_memory(address + len(block), remaining_size)
|
||||
remaining_size -= len(chunk)
|
||||
block += chunk
|
||||
|
||||
return block
|
||||
|
||||
async def read_memory_cache(self, addresses):
|
||||
# TODO: can we just update once per frame?
|
||||
if not self.last_cache_read or self.last_cache_read + 0.1 < time.time():
|
||||
await self.update_cache()
|
||||
if not self.cache:
|
||||
@@ -405,12 +359,11 @@ class LinksAwakeningClient():
|
||||
auth = binascii.hexlify(await self.gameboy.async_read_memory(0x0134, 12)).decode()
|
||||
self.auth = auth
|
||||
|
||||
async def wait_and_init_tracker(self, magpie: MagpieBridge):
|
||||
async def wait_and_init_tracker(self):
|
||||
await self.wait_for_game_ready()
|
||||
self.tracker = LocationTracker(self.gameboy)
|
||||
self.item_tracker = ItemTracker(self.gameboy)
|
||||
self.gps_tracker = GpsTracker(self.gameboy)
|
||||
magpie.gps_tracker = self.gps_tracker
|
||||
|
||||
async def recved_item_from_ap(self, item_id, from_player, next_index):
|
||||
# Don't allow getting an item until you've got your first check
|
||||
@@ -452,11 +405,9 @@ class LinksAwakeningClient():
|
||||
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
|
||||
|
||||
async def main_tick(self, item_get_cb, win_cb, deathlink_cb):
|
||||
await self.gameboy.update_cache()
|
||||
await self.tracker.readChecks(item_get_cb)
|
||||
await self.item_tracker.readItems()
|
||||
await self.gps_tracker.read_location()
|
||||
await self.gps_tracker.read_entrances()
|
||||
|
||||
current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth]
|
||||
if self.deathlink_debounce and current_health != 0:
|
||||
@@ -514,10 +465,6 @@ class LinksAwakeningContext(CommonContext):
|
||||
magpie_task = None
|
||||
won = False
|
||||
|
||||
@property
|
||||
def slot_storage_key(self):
|
||||
return f"{self.slot_info[self.slot].name}_{storage_key}"
|
||||
|
||||
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
|
||||
self.client = LinksAwakeningClient()
|
||||
self.slot_data = {}
|
||||
@@ -560,19 +507,7 @@ class LinksAwakeningContext(CommonContext):
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
async def send_checks(self):
|
||||
message = [{"cmd": "LocationChecks", "locations": self.found_checks}]
|
||||
await self.send_msgs(message)
|
||||
|
||||
async def send_new_entrances(self, entrances: typing.Dict[str, str]):
|
||||
# Store the entrances we find on the server for future sessions
|
||||
message = [{
|
||||
"cmd": "Set",
|
||||
"key": self.slot_storage_key,
|
||||
"default": {},
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "update", "value": entrances}],
|
||||
}]
|
||||
|
||||
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
|
||||
await self.send_msgs(message)
|
||||
|
||||
had_invalid_slot_data = None
|
||||
@@ -601,12 +536,6 @@ class LinksAwakeningContext(CommonContext):
|
||||
logger.info("victory!")
|
||||
await self.send_msgs(message)
|
||||
self.won = True
|
||||
|
||||
async def request_found_entrances(self):
|
||||
await self.send_msgs([{"cmd": "Get", "keys": [self.slot_storage_key]}])
|
||||
|
||||
# Ask for updates so that players can co-op entrances in a seed
|
||||
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
|
||||
|
||||
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
|
||||
if self.ENABLE_DEATHLINK:
|
||||
@@ -647,12 +576,6 @@ class LinksAwakeningContext(CommonContext):
|
||||
if cmd == "ReceivedItems":
|
||||
for index, item in enumerate(args["items"], start=args["index"]):
|
||||
self.client.recvd_checks[index] = item
|
||||
|
||||
if cmd == "Retrieved" and self.magpie_enabled and self.slot_storage_key in args["keys"]:
|
||||
self.client.gps_tracker.receive_found_entrances(args["keys"][self.slot_storage_key])
|
||||
|
||||
if cmd == "SetReply" and self.magpie_enabled and args["key"] == self.slot_storage_key:
|
||||
self.client.gps_tracker.receive_found_entrances(args["value"])
|
||||
|
||||
async def sync(self):
|
||||
sync_msg = [{'cmd': 'Sync'}]
|
||||
@@ -666,12 +589,6 @@ class LinksAwakeningContext(CommonContext):
|
||||
checkMetadataTable[check.id])] for check in ladxr_checks]
|
||||
self.new_checks(checks, [check.id for check in ladxr_checks])
|
||||
|
||||
for check in ladxr_checks:
|
||||
if check.value and check.linkedItem:
|
||||
linkedItem = check.linkedItem
|
||||
if 'condition' not in linkedItem or linkedItem['condition'](self.slot_data):
|
||||
self.client.item_tracker.setExtraItem(check.linkedItem['item'], check.linkedItem['qty'])
|
||||
|
||||
async def victory():
|
||||
await self.send_victory()
|
||||
|
||||
@@ -705,20 +622,12 @@ class LinksAwakeningContext(CommonContext):
|
||||
if not self.client.recvd_checks:
|
||||
await self.sync()
|
||||
|
||||
await self.client.wait_and_init_tracker(self.magpie)
|
||||
await self.client.wait_and_init_tracker()
|
||||
|
||||
min_tick_duration = 0.1
|
||||
last_tick = time.time()
|
||||
while True:
|
||||
await self.client.main_tick(on_item_get, victory, deathlink)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
now = time.time()
|
||||
tick_duration = now - last_tick
|
||||
sleep_duration = max(min_tick_duration - tick_duration, 0)
|
||||
await asyncio.sleep(sleep_duration)
|
||||
|
||||
last_tick = now
|
||||
|
||||
if self.last_resend + 5.0 < now:
|
||||
self.last_resend = now
|
||||
await self.send_checks()
|
||||
@@ -726,15 +635,8 @@ class LinksAwakeningContext(CommonContext):
|
||||
try:
|
||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||
await self.magpie.set_item_tracker(self.client.item_tracker)
|
||||
await self.magpie.send_gps(self.client.gps_tracker)
|
||||
self.magpie.slot_data = self.slot_data
|
||||
|
||||
if self.client.gps_tracker.needs_found_entrances:
|
||||
await self.request_found_entrances()
|
||||
self.client.gps_tracker.needs_found_entrances = False
|
||||
|
||||
new_entrances = await self.magpie.send_gps(self.client.gps_tracker)
|
||||
if new_entrances:
|
||||
await self.send_new_entrances(new_entrances)
|
||||
except Exception:
|
||||
# Don't let magpie errors take out the client
|
||||
pass
|
||||
|
||||
3
Utils.py
3
Utils.py
@@ -443,8 +443,7 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
else:
|
||||
mod = importlib.import_module(module)
|
||||
obj = getattr(mod, name)
|
||||
if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection,
|
||||
self.options_module.PlandoText)):
|
||||
if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection)):
|
||||
return obj
|
||||
# Forbid everything else.
|
||||
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
|
||||
|
||||
@@ -117,7 +117,6 @@ class WebHostContext(Context):
|
||||
self.gamespackage = {"Archipelago": static_gamespackage.get("Archipelago", {})} # this may be modified by _load
|
||||
self.item_name_groups = {"Archipelago": static_item_name_groups.get("Archipelago", {})}
|
||||
self.location_name_groups = {"Archipelago": static_location_name_groups.get("Archipelago", {})}
|
||||
missing_checksum = False
|
||||
|
||||
for game in list(multidata.get("datapackage", {})):
|
||||
game_data = multidata["datapackage"][game]
|
||||
@@ -133,13 +132,11 @@ class WebHostContext(Context):
|
||||
continue
|
||||
else:
|
||||
self.logger.warning(f"Did not find game_data_package for {game}: {game_data['checksum']}")
|
||||
else:
|
||||
missing_checksum = True # Game rolled on old AP and will load data package from multidata
|
||||
self.gamespackage[game] = static_gamespackage.get(game, {})
|
||||
self.item_name_groups[game] = static_item_name_groups.get(game, {})
|
||||
self.location_name_groups[game] = static_location_name_groups.get(game, {})
|
||||
|
||||
if not game_data_packages and not missing_checksum:
|
||||
if not game_data_packages:
|
||||
# all static -> use the static dicts directly
|
||||
self.gamespackage = static_gamespackage
|
||||
self.item_name_groups = static_item_name_groups
|
||||
|
||||
@@ -75,27 +75,6 @@
|
||||
#inventory-table img.acquired.green{ /*32CD32*/
|
||||
filter: hue-rotate(84deg) saturate(10) brightness(0.7);
|
||||
}
|
||||
#inventory-table img.acquired.hotpink{ /*FF69B4*/
|
||||
filter: sepia(100%) hue-rotate(300deg) saturate(10);
|
||||
}
|
||||
#inventory-table img.acquired.lightsalmon{ /*FFA07A*/
|
||||
filter: sepia(100%) hue-rotate(347deg) saturate(10);
|
||||
}
|
||||
#inventory-table img.acquired.crimson{ /*DB143B*/
|
||||
filter: sepia(100%) hue-rotate(318deg) saturate(10) brightness(0.86);
|
||||
}
|
||||
|
||||
#inventory-table span{
|
||||
color: #B4B4A0;
|
||||
font-size: 40px;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
filter: grayscale(100%) contrast(75%) brightness(30%);
|
||||
}
|
||||
|
||||
#inventory-table span.acquired{
|
||||
filter: none;
|
||||
}
|
||||
|
||||
#inventory-table div.image-stack{
|
||||
display: grid;
|
||||
|
||||
@@ -99,52 +99,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if 'PrismBreak' in options or 'LockKeyAmadeus' in options or 'GateKeep' in options %}
|
||||
<div class="table-row">
|
||||
{% if 'PrismBreak' in options %}
|
||||
<div class="C1">
|
||||
<div class="image-stack">
|
||||
<div class="stack-front">
|
||||
<div class="stack-top-left">
|
||||
<img src="{{ icons['Laser Access'] }}" class="hotpink {{ 'acquired' if 'Laser Access A' in acquired_items }}" title="Laser Access A" />
|
||||
</div>
|
||||
<div class="stack-top-right">
|
||||
<img src="{{ icons['Laser Access'] }}" class="lightsalmon {{ 'acquired' if 'Laser Access I' in acquired_items }}" title="Laser Access I" />
|
||||
</div>
|
||||
<div class="stack-bottum-left">
|
||||
<img src="{{ icons['Laser Access'] }}" class="crimson {{ 'acquired' if 'Laser Access M' in acquired_items }}" title="Laser Access M" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if 'LockKeyAmadeus' in options %}
|
||||
<div class="C2">
|
||||
<div class="image-stack">
|
||||
<div class="stack-front">
|
||||
<div class="stack-top-left">
|
||||
<img src="{{ icons['Lab Glasses'] }}" class="{{ 'acquired' if 'Lab Access Genza' in acquired_items }}" title="Lab Access Genza" />
|
||||
</div>
|
||||
<div class="stack-top-right">
|
||||
<img src="{{ icons['Eye Orb'] }}" class="{{ 'acquired' if 'Lab Access Dynamo' in acquired_items }}" title="Lab Access Dynamo" />
|
||||
</div>
|
||||
<div class="stack-bottum-left">
|
||||
<img src="{{ icons['Lab Coat'] }}" class="{{ 'acquired' if 'Lab Access Research' in acquired_items }}" title="Lab Access Research" />
|
||||
</div>
|
||||
<div class="stack-bottum-right">
|
||||
<img src="{{ icons['Demon'] }}" class="{{ 'acquired' if 'Lab Access Experiment' in acquired_items }}" title="Lab Access Experiment" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if 'GateKeep' in options %}
|
||||
<div class="C3">
|
||||
<span class="{{ 'acquired' if 'Drawbridge Key' in acquired_items }}" title="Drawbridge Key">❖</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<table id="location-table">
|
||||
|
||||
@@ -1071,11 +1071,6 @@ if "Timespinner" in network_data_package["games"]:
|
||||
"Plasma Orb": "https://timespinnerwiki.com/mediawiki/images/4/44/Plasma_Orb.png",
|
||||
"Kobo": "https://timespinnerwiki.com/mediawiki/images/c/c6/Familiar_Kobo.png",
|
||||
"Merchant Crow": "https://timespinnerwiki.com/mediawiki/images/4/4e/Familiar_Crow.png",
|
||||
"Laser Access": "https://timespinnerwiki.com/mediawiki/images/9/99/Historical_Documents.png",
|
||||
"Lab Glasses": "https://timespinnerwiki.com/mediawiki/images/4/4a/Lab_Glasses.png",
|
||||
"Eye Orb": "https://timespinnerwiki.com/mediawiki/images/a/a4/Eye_Orb.png",
|
||||
"Lab Coat": "https://timespinnerwiki.com/mediawiki/images/5/51/Lab_Coat.png",
|
||||
"Demon": "https://timespinnerwiki.com/mediawiki/images/f/f8/Familiar_Demon.png",
|
||||
}
|
||||
|
||||
timespinner_location_ids = {
|
||||
@@ -1123,9 +1118,6 @@ if "Timespinner" in network_data_package["games"]:
|
||||
timespinner_location_ids["Ancient Pyramid"] += [
|
||||
1337237, 1337238, 1337239,
|
||||
1337240, 1337241, 1337242, 1337243, 1337244, 1337245]
|
||||
if (slot_data["PyramidStart"]):
|
||||
timespinner_location_ids["Ancient Pyramid"] += [
|
||||
1337233, 1337234, 1337235]
|
||||
|
||||
display_data = {}
|
||||
|
||||
|
||||
@@ -73,11 +73,11 @@ When tests are run, this class will create a multiworld with a single player hav
|
||||
generic tests, as well as the new custom test. Each test method definition will create its own separate solo multiworld
|
||||
that will be cleaned up after. If you don't want to run the generic tests on a base, `run_default_tests` can be
|
||||
overridden. For more information on what methods are available to your class, check the
|
||||
[WorldTestBase definition](/test/bases.py#L106).
|
||||
[WorldTestBase definition](/test/bases.py#L104).
|
||||
|
||||
#### Alternatives to WorldTestBase
|
||||
|
||||
Unit tests can also be created using [TestBase](/test/bases.py#L16) or
|
||||
Unit tests can also be created using [TestBase](/test/bases.py#L14) or
|
||||
[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) depending on your use case. These
|
||||
may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for
|
||||
testing portions of your code that can be tested without relying on a multiworld to be created first.
|
||||
|
||||
@@ -291,7 +291,7 @@ like entrance randomization in logic.
|
||||
|
||||
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
|
||||
|
||||
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)),
|
||||
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295-L296)),
|
||||
from which the logic unfolds. AP assumes that a player will always be able to return to this starting region by resetting the game ("Save and quit").
|
||||
|
||||
### Entrances
|
||||
@@ -331,7 +331,7 @@ Even doing `state.can_reach_location` or `state.can_reach_entrance` is problemat
|
||||
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
|
||||
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
|
||||
|
||||
Alternatively, you can set [world.explicit_indirect_conditions = False](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L301-L304),
|
||||
Alternatively, you can set [world.explicit_indirect_conditions = False](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L301),
|
||||
avoiding the need for indirect conditions at the expense of performance.
|
||||
|
||||
### Item Rules
|
||||
|
||||
@@ -157,16 +157,17 @@ class ERPlacementState:
|
||||
def placed_regions(self) -> set[Region]:
|
||||
return self.collection_state.reachable_regions[self.world.player]
|
||||
|
||||
def find_placeable_exits(self, check_validity: bool, usable_exits: list[Entrance]) -> list[Entrance]:
|
||||
def find_placeable_exits(self, check_validity: bool) -> list[Entrance]:
|
||||
if check_validity:
|
||||
blocked_connections = self.collection_state.blocked_connections[self.world.player]
|
||||
placeable_randomized_exits = [ex for ex in usable_exits
|
||||
if not ex.connected_region
|
||||
and ex in blocked_connections
|
||||
and ex.is_valid_source_transition(self)]
|
||||
blocked_connections = sorted(blocked_connections, key=lambda x: x.name)
|
||||
placeable_randomized_exits = [connection for connection in blocked_connections
|
||||
if not connection.connected_region
|
||||
and connection.is_valid_source_transition(self)]
|
||||
else:
|
||||
# this is on a beaten minimal attempt, so any exit anywhere is fair game
|
||||
placeable_randomized_exits = [ex for ex in usable_exits if not ex.connected_region]
|
||||
placeable_randomized_exits = [ex for region in self.world.multiworld.get_regions(self.world.player)
|
||||
for ex in region.exits if not ex.connected_region]
|
||||
self.world.random.shuffle(placeable_randomized_exits)
|
||||
return placeable_randomized_exits
|
||||
|
||||
@@ -180,8 +181,7 @@ class ERPlacementState:
|
||||
self.placements.append(source_exit)
|
||||
self.pairings.append((source_exit.name, target_entrance.name))
|
||||
|
||||
def test_speculative_connection(self, source_exit: Entrance, target_entrance: Entrance,
|
||||
usable_exits: set[Entrance]) -> bool:
|
||||
def test_speculative_connection(self, source_exit: Entrance, target_entrance: Entrance) -> bool:
|
||||
copied_state = self.collection_state.copy()
|
||||
# simulated connection. A real connection is unsafe because the region graph is shallow-copied and would
|
||||
# propagate back to the real multiworld.
|
||||
@@ -198,9 +198,6 @@ class ERPlacementState:
|
||||
# ignore the source exit, and, if coupled, the reverse exit. They're not actually new
|
||||
if _exit.name == source_exit.name or (self.coupled and _exit.name == target_entrance.name):
|
||||
continue
|
||||
# make sure we are only paying attention to usable exits
|
||||
if _exit not in usable_exits:
|
||||
continue
|
||||
# technically this should be is_valid_source_transition, but that may rely on side effects from
|
||||
# on_connect, which have not happened here (because we didn't do a real connection, and if we did, we would
|
||||
# not want them to persist). can_reach is a close enough approximation most of the time.
|
||||
@@ -329,24 +326,6 @@ def randomize_entrances(
|
||||
# similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility
|
||||
perform_validity_check = True
|
||||
|
||||
if not er_targets:
|
||||
er_targets = sorted([entrance for region in world.multiworld.get_regions(world.player)
|
||||
for entrance in region.entrances if not entrance.parent_region], key=lambda x: x.name)
|
||||
if not exits:
|
||||
exits = sorted([ex for region in world.multiworld.get_regions(world.player)
|
||||
for ex in region.exits if not ex.connected_region], key=lambda x: x.name)
|
||||
if len(er_targets) != len(exits):
|
||||
raise EntranceRandomizationError(f"Unable to randomize entrances due to a mismatched count of "
|
||||
f"entrances ({len(er_targets)}) and exits ({len(exits)}.")
|
||||
|
||||
# used when membership checks are needed on the exit list, e.g. speculative sweep
|
||||
exits_set = set(exits)
|
||||
for entrance in er_targets:
|
||||
entrance_lookup.add(entrance)
|
||||
|
||||
# place the menu region and connected start region(s)
|
||||
er_state.collection_state.update_reachable_regions(world.player)
|
||||
|
||||
def do_placement(source_exit: Entrance, target_entrance: Entrance) -> None:
|
||||
placed_exits, removed_entrances = er_state.connect(source_exit, target_entrance)
|
||||
# remove the placed targets from consideration
|
||||
@@ -360,7 +339,7 @@ def randomize_entrances(
|
||||
|
||||
def find_pairing(dead_end: bool, require_new_exits: bool) -> bool:
|
||||
nonlocal perform_validity_check
|
||||
placeable_exits = er_state.find_placeable_exits(perform_validity_check, exits)
|
||||
placeable_exits = er_state.find_placeable_exits(perform_validity_check)
|
||||
for source_exit in placeable_exits:
|
||||
target_groups = target_group_lookup[source_exit.randomization_group]
|
||||
for target_entrance in entrance_lookup.get_targets(target_groups, dead_end, preserve_group_order):
|
||||
@@ -376,7 +355,7 @@ def randomize_entrances(
|
||||
and len(placeable_exits) == 1)
|
||||
if exit_requirement_satisfied and source_exit.can_connect_to(target_entrance, dead_end, er_state):
|
||||
if (needs_speculative_sweep
|
||||
and not er_state.test_speculative_connection(source_exit, target_entrance, exits_set)):
|
||||
and not er_state.test_speculative_connection(source_exit, target_entrance)):
|
||||
continue
|
||||
do_placement(source_exit, target_entrance)
|
||||
return True
|
||||
@@ -428,6 +407,21 @@ def randomize_entrances(
|
||||
f"All unplaced entrances: {unplaced_entrances}\n"
|
||||
f"All unplaced exits: {unplaced_exits}")
|
||||
|
||||
if not er_targets:
|
||||
er_targets = sorted([entrance for region in world.multiworld.get_regions(world.player)
|
||||
for entrance in region.entrances if not entrance.parent_region], key=lambda x: x.name)
|
||||
if not exits:
|
||||
exits = sorted([ex for region in world.multiworld.get_regions(world.player)
|
||||
for ex in region.exits if not ex.connected_region], key=lambda x: x.name)
|
||||
if len(er_targets) != len(exits):
|
||||
raise EntranceRandomizationError(f"Unable to randomize entrances due to a mismatched count of "
|
||||
f"entrances ({len(er_targets)}) and exits ({len(exits)}.")
|
||||
for entrance in er_targets:
|
||||
entrance_lookup.add(entrance)
|
||||
|
||||
# place the menu region and connected start region(s)
|
||||
er_state.collection_state.update_reachable_regions(world.player)
|
||||
|
||||
# stage 1 - try to place all the non-dead-end entrances
|
||||
while entrance_lookup.others:
|
||||
if not find_pairing(dead_end=False, require_new_exits=True):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
project(ap-cpp-tests)
|
||||
|
||||
enable_testing()
|
||||
@@ -7,8 +7,8 @@ find_package(GTest REQUIRED)
|
||||
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
|
||||
add_definitions("/source-charset:utf-8")
|
||||
# set(CMAKE_CXX_FLAGS_DEBUG "/MDd") # this is the default
|
||||
# set(CMAKE_CXX_FLAGS_RELEASE "/MD") # this is the default
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "/MTd")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "/MT")
|
||||
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
# enable static analysis for gcc
|
||||
add_compile_options(-fanalyzer -Werror)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import unittest
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
||||
from . import setup_solo_multiworld
|
||||
|
||||
@@ -9,31 +8,12 @@ class TestBase(unittest.TestCase):
|
||||
def test_create_item(self):
|
||||
"""Test that a world can successfully create all items in its datapackage"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
multiworld = setup_solo_multiworld(world_type, steps=("generate_early", "create_regions", "create_items"))
|
||||
proxy_world = multiworld.worlds[1]
|
||||
proxy_world = setup_solo_multiworld(world_type, ()).worlds[1]
|
||||
for item_name in world_type.item_name_to_id:
|
||||
test_state = CollectionState(multiworld)
|
||||
with self.subTest("Create Item", item_name=item_name, game_name=game_name):
|
||||
item = proxy_world.create_item(item_name)
|
||||
|
||||
with self.subTest("Item Name", item_name=item_name, game_name=game_name):
|
||||
self.assertEqual(item.name, item_name)
|
||||
|
||||
if item.advancement:
|
||||
with self.subTest("Item State Collect", item_name=item_name, game_name=game_name):
|
||||
test_state.collect(item, True)
|
||||
|
||||
with self.subTest("Item State Remove", item_name=item_name, game_name=game_name):
|
||||
test_state.remove(item)
|
||||
|
||||
self.assertEqual(test_state.prog_items, multiworld.state.prog_items,
|
||||
"Item Collect -> Remove should restore empty state.")
|
||||
else:
|
||||
with self.subTest("Item State Collect No Change", item_name=item_name, game_name=game_name):
|
||||
# Non-Advancement should not modify state.
|
||||
test_state.collect(item)
|
||||
self.assertEqual(test_state.prog_items, multiworld.state.prog_items)
|
||||
|
||||
def test_item_name_group_has_valid_item(self):
|
||||
"""Test that all item name groups contain valid items. """
|
||||
# This cannot test for Event names that you may have declared for logic, only sendable Items.
|
||||
|
||||
@@ -41,7 +41,6 @@ class BizHawkClientCommandProcessor(ClientCommandProcessor):
|
||||
|
||||
class BizHawkClientContext(CommonContext):
|
||||
command_processor = BizHawkClientCommandProcessor
|
||||
server_seed_name: str | None = None
|
||||
auth_status: AuthStatus
|
||||
password_requested: bool
|
||||
client_handler: BizHawkClient | None
|
||||
@@ -69,8 +68,6 @@ class BizHawkClientContext(CommonContext):
|
||||
if cmd == "Connected":
|
||||
self.slot_data = args.get("slot_data", None)
|
||||
self.auth_status = AuthStatus.AUTHENTICATED
|
||||
elif cmd == "RoomInfo":
|
||||
self.server_seed_name = args.get("seed_name", None)
|
||||
|
||||
if self.client_handler is not None:
|
||||
self.client_handler.on_package(self, cmd, args)
|
||||
@@ -103,7 +100,6 @@ class BizHawkClientContext(CommonContext):
|
||||
|
||||
async def disconnect(self, allow_autoreconnect: bool=False):
|
||||
self.auth_status = AuthStatus.NOT_AUTHENTICATED
|
||||
self.server_seed_name = None
|
||||
await super().disconnect(allow_autoreconnect)
|
||||
|
||||
|
||||
|
||||
@@ -1547,9 +1547,9 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld
|
||||
|
||||
# compasses showing dungeon count
|
||||
if local_world.clock_mode or world.dungeon_counters[player] == 'off':
|
||||
if local_world.clock_mode or not world.dungeon_counters[player]:
|
||||
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
|
||||
elif world.dungeon_counters[player] == 'on':
|
||||
elif world.dungeon_counters[player] is True:
|
||||
rom.write_byte(0x18003C, 0x02) # always on
|
||||
elif world.compass_shuffle[player] or world.dungeon_counters[player] == 'pickup':
|
||||
rom.write_byte(0x18003C, 0x01) # show on pickup
|
||||
|
||||
@@ -89,7 +89,7 @@ location_names: Dict[str, str] = {
|
||||
"RESCUED_CHERUB_15": "DC: Top of elevator Child of Moonlight",
|
||||
"Lady[D01Z05S22]": "DC: Lady of the Six Sorrows, from MD",
|
||||
"QI75": "DC: Chalice room",
|
||||
"Sword[D01Z05S24]": "DC: Mea Culpa altar",
|
||||
"Sword[D01Z05S24]": "DC: Mea culpa altar",
|
||||
"CO44": "DC: Elevator shaft ledge",
|
||||
"RESCUED_CHERUB_22": "DC: Elevator shaft Child of Moonlight",
|
||||
"Lady[D01Z05S26]": "DC: Lady of the Six Sorrows, elevator shaft",
|
||||
|
||||
@@ -67,8 +67,7 @@ class BlasphemousWorld(World):
|
||||
|
||||
def generate_early(self):
|
||||
if not self.options.starting_location.randomized:
|
||||
if (self.options.starting_location == "knot_of_words" or self.options.starting_location == "rooftops" \
|
||||
or self.options.starting_location == "mourning_havoc") and self.options.difficulty < 2:
|
||||
if self.options.starting_location == "mourning_havoc" and self.options.difficulty < 2:
|
||||
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
||||
f"{self.options.starting_location} cannot be chosen if Difficulty is lower than Hard.")
|
||||
|
||||
@@ -84,8 +83,6 @@ class BlasphemousWorld(World):
|
||||
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
|
||||
|
||||
if self.options.difficulty < 2:
|
||||
locations.remove(4)
|
||||
locations.remove(5)
|
||||
locations.remove(6)
|
||||
|
||||
if self.options.dash_shuffle:
|
||||
|
||||
@@ -85,7 +85,20 @@ class TestGrievanceHard(BlasphemousTestBase):
|
||||
}
|
||||
|
||||
|
||||
# knot of the three words, rooftops, and mourning and havoc can't be selected on easy or normal. hard only
|
||||
class TestKnotOfWordsEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "knot_of_words",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestKnotOfWordsNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "knot_of_words",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestKnotOfWordsHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "knot_of_words",
|
||||
@@ -93,6 +106,20 @@ class TestKnotOfWordsHard(BlasphemousTestBase):
|
||||
}
|
||||
|
||||
|
||||
class TestRooftopsEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "rooftops",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestRooftopsNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "rooftops",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestRooftopsHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "rooftops",
|
||||
@@ -100,6 +127,7 @@ class TestRooftopsHard(BlasphemousTestBase):
|
||||
}
|
||||
|
||||
|
||||
# mourning and havoc can't be selected on easy or normal. hard only
|
||||
class TestMourningHavocHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "mourning_havoc",
|
||||
|
||||
@@ -25,10 +25,19 @@ class DarkSouls3Web(WebWorld):
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Natalie", "Marech"]
|
||||
["Marech"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en]
|
||||
setup_fr = Tutorial(
|
||||
setup_en.tutorial_name,
|
||||
setup_en.description,
|
||||
"Français",
|
||||
"setup_fr.md",
|
||||
"setup/fr",
|
||||
["Marech"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_fr]
|
||||
option_groups = option_groups
|
||||
item_descriptions = item_descriptions
|
||||
rich_text_options_doc = True
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
## Required Software
|
||||
|
||||
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
|
||||
- [Dark Souls III AP Client]
|
||||
|
||||
[Dark Souls III AP Client]: https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest
|
||||
- [Dark Souls III AP Client](https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest)
|
||||
|
||||
## Optional Software
|
||||
|
||||
- [Map tracker](https://github.com/TVV1GK/DS3_AP_Maptracker)
|
||||
- Map tracker not yet updated for 3.0.0
|
||||
|
||||
## Setting Up
|
||||
|
||||
@@ -75,65 +73,3 @@ things to keep in mind:
|
||||
|
||||
[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0
|
||||
[WINE]: https://www.winehq.org/
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Enemy randomizer issues
|
||||
|
||||
The DS3 Archipelago randomizer uses [thefifthmatt's DS3 enemy randomizer],
|
||||
essentially unchanged. Unfortunately, this randomizer has a few known issues,
|
||||
including enemy AI not working, enemies spawning in places they can't be killed,
|
||||
and, in a few rare cases, enemies spawning in ways that crash the game when they
|
||||
load. These bugs should be [reported upstream], but unfortunately the
|
||||
Archipelago devs can't help much with them.
|
||||
|
||||
[thefifthmatt's DS3 enemy randomizer]: https://www.nexusmods.com/darksouls3/mods/484
|
||||
[reported upstream]: https://github.com/thefifthmatt/SoulsRandomizers/issues
|
||||
|
||||
Because in rare cases the enemy randomizer can cause seeds to be impossible to
|
||||
complete, we recommend disabling it for large async multiworlds for safety
|
||||
purposes.
|
||||
|
||||
### `launchmod_darksouls3.bat` isn't working
|
||||
|
||||
Sometimes `launchmod_darksouls3.bat` will briefly flash a terminal on your
|
||||
screen and then terminate without actually starting the game. This is usually
|
||||
caused by some issue communicating with Steam either to find `DarkSoulsIII.exe`
|
||||
or to launch it properly. If this is happening to you, make sure:
|
||||
|
||||
* You have DS3 1.15.2 installed. This is the latest patch as of January 2025.
|
||||
(Note that older versions of Archipelago required an older patch, but that
|
||||
_will not work_ with the current version.)
|
||||
|
||||
* You own the DS3 DLC if your randomizer config has DLC enabled. (It's possible,
|
||||
but unconfirmed, that you need the DLC even when it's disabled in your config).
|
||||
|
||||
* Steam is not running in administrator mode. To fix this, right-click
|
||||
`steam.exe` (by default this is in `C:\Program Files\Steam`), select
|
||||
"Properties", open the "Compatiblity" tab, and uncheck "Run this program as an
|
||||
administrator".
|
||||
|
||||
* There is no `dinput8.dll` file in your DS3 game directory. This is the old way
|
||||
of installing mods, and it can interfere with the new ModEngine2 workflow.
|
||||
|
||||
If you've checked all of these, you can also try:
|
||||
|
||||
* Running `launchmod_darksouls3.bat` as an administrator.
|
||||
|
||||
* Reinstalling DS3 or even reinstalling Steam itself.
|
||||
|
||||
* Making sure DS3 is installed on the same drive as Steam and as the randomizer.
|
||||
(A number of users are able to run these on different drives, but this has
|
||||
helped some users.)
|
||||
|
||||
If none of this works, unfortunately there's not much we can do. We use
|
||||
ModEngine2 to launch DS3 with the Archipelago mod enabled, but unfortunately
|
||||
it's no longer maintained and its successor, ModEngine3, isn't usable yet.
|
||||
|
||||
### `DS3Randomizer.exe` isn't working
|
||||
|
||||
This is almost always caused by using a version of the randomizer client that's
|
||||
not compatible with the version used to generate the multiworld. If you're
|
||||
generating your multiworld on archipelago.gg, you *must* use the latest [Dark
|
||||
Souls III AP Client]. If you want to use a different client version, you *must*
|
||||
generate the multiworld locally using the apworld bundled with the client.
|
||||
|
||||
33
worlds/dark_souls_3/docs/setup_fr.md
Normal file
33
worlds/dark_souls_3/docs/setup_fr.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Guide d'installation de Dark Souls III Randomizer
|
||||
|
||||
## Logiciels requis
|
||||
|
||||
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
|
||||
- [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases)
|
||||
|
||||
## Concept général
|
||||
|
||||
Le client Archipelago de Dark Souls III est un fichier dinput8.dll. Cette .dll va lancer une invite de commande Windows
|
||||
permettant de lire des informations de la partie et écrire des commandes pour intéragir avec le serveur Archipelago.
|
||||
|
||||
## Procédures d'installation
|
||||
|
||||
<span style="color:#ff7800">
|
||||
**Il y a des risques de bannissement permanent des serveurs FromSoftware si ce mod est utilisé en ligne.**
|
||||
</span>
|
||||
Ce client a été testé sur la version Steam officielle du jeu (v1.15/1.35), peu importe les DLCs actuellement installés.
|
||||
|
||||
Télécharger le fichier dinput8.dll disponible dans le [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) et
|
||||
placez-le à la racine du jeu (ex: "SteamLibrary\steamapps\common\DARK SOULS III\Game")
|
||||
|
||||
## Rejoindre une partie Multiworld
|
||||
|
||||
1. Lancer DarkSoulsIII.exe ou lancer le jeu depuis Steam
|
||||
2. Ecrire "/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}" dans l'invite de commande Windows ouverte au lancement du jeu
|
||||
3. Une fois connecté, créez une nouvelle partie, choisissez une classe et attendez que les autres soient prêts avant de lancer
|
||||
4. Vous pouvez quitter et lancer le jeu n'importe quand pendant une partie
|
||||
|
||||
## Où trouver le fichier de configuration ?
|
||||
|
||||
La [Page de configuration](/games/Dark%20Souls%20III/player-options) sur le site vous permez de configurer vos
|
||||
paramètres et de les exporter sous la forme d'un fichier.
|
||||
@@ -650,8 +650,8 @@ item_table: Dict[int, ItemDict] = {
|
||||
'doom_type': 2006,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
350106: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
350106: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Backpack',
|
||||
'doom_type': 8,
|
||||
'episode': -1,
|
||||
@@ -1160,30 +1160,6 @@ item_table: Dict[int, ItemDict] = {
|
||||
'doom_type': 2026,
|
||||
'episode': 4,
|
||||
'map': 9},
|
||||
350191: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Bullet capacity',
|
||||
'doom_type': 65001,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
350192: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Shell capacity',
|
||||
'doom_type': 65002,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
350193: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Energy cell capacity',
|
||||
'doom_type': 65003,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
350194: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Rocket capacity',
|
||||
'doom_type': 65004,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from Options import PerGameCommonOptions, Range, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -144,84 +144,6 @@ class Episode4(Toggle):
|
||||
display_name = "Episode 4"
|
||||
|
||||
|
||||
class SplitBackpack(Toggle):
|
||||
"""Split the Backpack into four individual items, each one increasing ammo capacity for one type of weapon only."""
|
||||
display_name = "Split Backpack"
|
||||
|
||||
|
||||
class BackpackCount(Range):
|
||||
"""How many Backpacks will be available.
|
||||
If Split Backpack is set, this will be the number of each capacity upgrade available."""
|
||||
display_name = "Backpack Count"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 1
|
||||
|
||||
|
||||
class MaxAmmoBullets(Range):
|
||||
"""Set the starting ammo capacity for bullets."""
|
||||
display_name = "Max Ammo - Bullets"
|
||||
range_start = 200
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class MaxAmmoShells(Range):
|
||||
"""Set the starting ammo capacity for shotgun shells."""
|
||||
display_name = "Max Ammo - Shells"
|
||||
range_start = 50
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class MaxAmmoRockets(Range):
|
||||
"""Set the starting ammo capacity for rockets."""
|
||||
display_name = "Max Ammo - Rockets"
|
||||
range_start = 50
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class MaxAmmoEnergyCells(Range):
|
||||
"""Set the starting ammo capacity for energy cells."""
|
||||
display_name = "Max Ammo - Energy Cells"
|
||||
range_start = 300
|
||||
range_end = 999
|
||||
default = 300
|
||||
|
||||
|
||||
class AddedAmmoBullets(Range):
|
||||
"""Set the amount of bullet capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Bullets"
|
||||
range_start = 20
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class AddedAmmoShells(Range):
|
||||
"""Set the amount of shotgun shell capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Shells"
|
||||
range_start = 5
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class AddedAmmoRockets(Range):
|
||||
"""Set the amount of rocket capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Rockets"
|
||||
range_start = 5
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class AddedAmmoEnergyCells(Range):
|
||||
"""Set the amount of energy cell capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Energy Cells"
|
||||
range_start = 30
|
||||
range_end = 999
|
||||
default = 300
|
||||
|
||||
|
||||
@dataclass
|
||||
class DOOM1993Options(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
@@ -241,14 +163,3 @@ class DOOM1993Options(PerGameCommonOptions):
|
||||
episode3: Episode3
|
||||
episode4: Episode4
|
||||
|
||||
split_backpack: SplitBackpack
|
||||
backpack_count: BackpackCount
|
||||
max_ammo_bullets: MaxAmmoBullets
|
||||
max_ammo_shells: MaxAmmoShells
|
||||
max_ammo_rockets: MaxAmmoRockets
|
||||
max_ammo_energy_cells: MaxAmmoEnergyCells
|
||||
added_ammo_bullets: AddedAmmoBullets
|
||||
added_ammo_shells: AddedAmmoShells
|
||||
added_ammo_rockets: AddedAmmoRockets
|
||||
added_ammo_energy_cells: AddedAmmoEnergyCells
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class DOOM1993World(World):
|
||||
options: DOOM1993Options
|
||||
game = "DOOM 1993"
|
||||
web = DOOM1993Web()
|
||||
required_client_version = (0, 5, 0) # 1.2.0-prerelease or higher
|
||||
required_client_version = (0, 3, 9)
|
||||
|
||||
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
|
||||
item_name_groups = Items.item_name_groups
|
||||
@@ -204,15 +204,6 @@ class DOOM1993World(World):
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
# Backpack(s) based on options
|
||||
if self.options.split_backpack.value:
|
||||
itempool += [self.create_item("Bullet capacity") for _ in range(self.options.backpack_count.value)]
|
||||
itempool += [self.create_item("Shell capacity") for _ in range(self.options.backpack_count.value)]
|
||||
itempool += [self.create_item("Energy cell capacity") for _ in range(self.options.backpack_count.value)]
|
||||
itempool += [self.create_item("Rocket capacity") for _ in range(self.options.backpack_count.value)]
|
||||
else:
|
||||
itempool += [self.create_item("Backpack") for _ in range(self.options.backpack_count.value)]
|
||||
|
||||
# Place end level items in locked locations
|
||||
for map_name in Maps.map_names:
|
||||
loc_name = map_name + " - Exit"
|
||||
@@ -274,7 +265,7 @@ class DOOM1993World(World):
|
||||
# Was balanced for 3 episodes (We added 4th episode, but keep same ratio)
|
||||
count = min(remaining_loc, max(1, int(round(self.items_ratio[item_name] * ep_count / 3))))
|
||||
if count == 0:
|
||||
logger.warning(f"Warning, no {item_name} will be placed.")
|
||||
logger.warning("Warning, no ", item_name, " will be placed.")
|
||||
return
|
||||
|
||||
for i in range(count):
|
||||
@@ -290,14 +281,4 @@ class DOOM1993World(World):
|
||||
# an older version, the player would end up stuck.
|
||||
slot_data["two_ways_keydoors"] = True
|
||||
|
||||
# Send slot data for ammo capacity values; this must be generic because Heretic uses it too
|
||||
slot_data["ammo1start"] = self.options.max_ammo_bullets.value
|
||||
slot_data["ammo2start"] = self.options.max_ammo_shells.value
|
||||
slot_data["ammo3start"] = self.options.max_ammo_energy_cells.value
|
||||
slot_data["ammo4start"] = self.options.max_ammo_rockets.value
|
||||
slot_data["ammo1add"] = self.options.added_ammo_bullets.value
|
||||
slot_data["ammo2add"] = self.options.added_ammo_shells.value
|
||||
slot_data["ammo3add"] = self.options.added_ammo_energy_cells.value
|
||||
slot_data["ammo4add"] = self.options.added_ammo_rockets.value
|
||||
|
||||
return slot_data
|
||||
|
||||
@@ -56,8 +56,8 @@ item_table: Dict[int, ItemDict] = {
|
||||
'doom_type': 82,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
360007: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
360007: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Backpack',
|
||||
'doom_type': 8,
|
||||
'episode': -1,
|
||||
@@ -1058,30 +1058,6 @@ item_table: Dict[int, ItemDict] = {
|
||||
'doom_type': 2026,
|
||||
'episode': 4,
|
||||
'map': 2},
|
||||
360600: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Bullet capacity',
|
||||
'doom_type': 65001,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
360601: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Shell capacity',
|
||||
'doom_type': 65002,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
360602: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Energy cell capacity',
|
||||
'doom_type': 65003,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
360603: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Rocket capacity',
|
||||
'doom_type': 65004,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import typing
|
||||
|
||||
from Options import PerGameCommonOptions, Range, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -136,84 +136,6 @@ class SecretLevels(Toggle):
|
||||
display_name = "Secret Levels"
|
||||
|
||||
|
||||
class SplitBackpack(Toggle):
|
||||
"""Split the Backpack into four individual items, each one increasing ammo capacity for one type of weapon only."""
|
||||
display_name = "Split Backpack"
|
||||
|
||||
|
||||
class BackpackCount(Range):
|
||||
"""How many Backpacks will be available.
|
||||
If Split Backpack is set, this will be the number of each capacity upgrade available."""
|
||||
display_name = "Backpack Count"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 1
|
||||
|
||||
|
||||
class MaxAmmoBullets(Range):
|
||||
"""Set the starting ammo capacity for bullets."""
|
||||
display_name = "Max Ammo - Bullets"
|
||||
range_start = 200
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class MaxAmmoShells(Range):
|
||||
"""Set the starting ammo capacity for shotgun shells."""
|
||||
display_name = "Max Ammo - Shells"
|
||||
range_start = 50
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class MaxAmmoRockets(Range):
|
||||
"""Set the starting ammo capacity for rockets."""
|
||||
display_name = "Max Ammo - Rockets"
|
||||
range_start = 50
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class MaxAmmoEnergyCells(Range):
|
||||
"""Set the starting ammo capacity for energy cells."""
|
||||
display_name = "Max Ammo - Energy Cells"
|
||||
range_start = 300
|
||||
range_end = 999
|
||||
default = 300
|
||||
|
||||
|
||||
class AddedAmmoBullets(Range):
|
||||
"""Set the amount of bullet capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Bullets"
|
||||
range_start = 20
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class AddedAmmoShells(Range):
|
||||
"""Set the amount of shotgun shell capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Shells"
|
||||
range_start = 5
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class AddedAmmoRockets(Range):
|
||||
"""Set the amount of rocket capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Rockets"
|
||||
range_start = 5
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class AddedAmmoEnergyCells(Range):
|
||||
"""Set the amount of energy cell capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Energy Cells"
|
||||
range_start = 30
|
||||
range_end = 999
|
||||
default = 300
|
||||
|
||||
|
||||
@dataclass
|
||||
class DOOM2Options(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
@@ -231,14 +153,3 @@ class DOOM2Options(PerGameCommonOptions):
|
||||
episode2: Episode2
|
||||
episode3: Episode3
|
||||
episode4: SecretLevels
|
||||
|
||||
split_backpack: SplitBackpack
|
||||
backpack_count: BackpackCount
|
||||
max_ammo_bullets: MaxAmmoBullets
|
||||
max_ammo_shells: MaxAmmoShells
|
||||
max_ammo_rockets: MaxAmmoRockets
|
||||
max_ammo_energy_cells: MaxAmmoEnergyCells
|
||||
added_ammo_bullets: AddedAmmoBullets
|
||||
added_ammo_shells: AddedAmmoShells
|
||||
added_ammo_rockets: AddedAmmoRockets
|
||||
added_ammo_energy_cells: AddedAmmoEnergyCells
|
||||
|
||||
@@ -43,7 +43,7 @@ class DOOM2World(World):
|
||||
options: DOOM2Options
|
||||
game = "DOOM II"
|
||||
web = DOOM2Web()
|
||||
required_client_version = (0, 5, 0) # 1.2.0-prerelease or higher
|
||||
required_client_version = (0, 3, 9)
|
||||
|
||||
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
|
||||
item_name_groups = Items.item_name_groups
|
||||
@@ -196,15 +196,6 @@ class DOOM2World(World):
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
# Backpack(s) based on options
|
||||
if self.options.split_backpack.value:
|
||||
itempool += [self.create_item("Bullet capacity") for _ in range(self.options.backpack_count.value)]
|
||||
itempool += [self.create_item("Shell capacity") for _ in range(self.options.backpack_count.value)]
|
||||
itempool += [self.create_item("Energy cell capacity") for _ in range(self.options.backpack_count.value)]
|
||||
itempool += [self.create_item("Rocket capacity") for _ in range(self.options.backpack_count.value)]
|
||||
else:
|
||||
itempool += [self.create_item("Backpack") for _ in range(self.options.backpack_count.value)]
|
||||
|
||||
# Place end level items in locked locations
|
||||
for map_name in Maps.map_names:
|
||||
loc_name = map_name + " - Exit"
|
||||
@@ -267,23 +258,11 @@ class DOOM2World(World):
|
||||
# Was balanced based on DOOM 1993's first 3 episodes
|
||||
count = min(remaining_loc, max(1, int(round(self.items_ratio[item_name] * ep_count / 3))))
|
||||
if count == 0:
|
||||
logger.warning(f"Warning, no {item_name} will be placed.")
|
||||
logger.warning("Warning, no ", item_name, " will be placed.")
|
||||
return
|
||||
|
||||
for i in range(count):
|
||||
itempool.append(self.create_item(item_name))
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
slot_data = self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "flip_levels", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "episode1", "episode2", "episode3", "episode4")
|
||||
|
||||
# Send slot data for ammo capacity values; this must be generic because Heretic uses it too
|
||||
slot_data["ammo1start"] = self.options.max_ammo_bullets.value
|
||||
slot_data["ammo2start"] = self.options.max_ammo_shells.value
|
||||
slot_data["ammo3start"] = self.options.max_ammo_energy_cells.value
|
||||
slot_data["ammo4start"] = self.options.max_ammo_rockets.value
|
||||
slot_data["ammo1add"] = self.options.added_ammo_bullets.value
|
||||
slot_data["ammo2add"] = self.options.added_ammo_shells.value
|
||||
slot_data["ammo3add"] = self.options.added_ammo_energy_cells.value
|
||||
slot_data["ammo4add"] = self.options.added_ammo_rockets.value
|
||||
|
||||
return slot_data
|
||||
return self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "flip_levels", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "episode1", "episode2", "episode3", "episode4")
|
||||
|
||||
@@ -235,12 +235,6 @@ class FactorioStartItems(OptionDict):
|
||||
"""Mapping of Factorio internal item-name to amount granted on start."""
|
||||
display_name = "Starting Items"
|
||||
default = {"burner-mining-drill": 4, "stone-furnace": 4, "raw-fish": 50}
|
||||
schema = Schema(
|
||||
{
|
||||
str: And(int, lambda n: n > 0,
|
||||
error="amount of starting items has to be a positive integer"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class FactorioFreeSampleBlacklist(OptionSet):
|
||||
@@ -263,8 +257,7 @@ class AttackTrapCount(TrapCount):
|
||||
|
||||
|
||||
class TeleportTrapCount(TrapCount):
|
||||
"""Trap items that when received trigger a random teleport.
|
||||
It is ensured the player can walk back to where they got teleported from."""
|
||||
"""Trap items that when received trigger a random teleport."""
|
||||
display_name = "Teleport Traps"
|
||||
|
||||
|
||||
|
||||
@@ -49,73 +49,6 @@ function fire_entity_at_entities(entity_name, entities, speed)
|
||||
end
|
||||
end
|
||||
|
||||
local teleport_requests = {}
|
||||
local teleport_attempts = {}
|
||||
local max_attempts = 100
|
||||
|
||||
function attempt_teleport_player(player, attempt)
|
||||
-- global attempt storage as metadata can't be stored
|
||||
if attempt == nil then
|
||||
attempt = teleport_attempts[player.index]
|
||||
else
|
||||
teleport_attempts[player.index] = attempt
|
||||
end
|
||||
|
||||
if attempt > max_attempts then
|
||||
player.print("Teleport failed: No valid position found after " .. max_attempts .. " attempts!")
|
||||
teleport_attempts[player.index] = 0
|
||||
return
|
||||
end
|
||||
|
||||
local surface = player.character.surface
|
||||
local prototype_name = player.character.prototype.name
|
||||
local original_position = player.character.position
|
||||
local candidate_position = random_offset_position(original_position, 1024)
|
||||
|
||||
local non_colliding_position = surface.find_non_colliding_position(
|
||||
prototype_name, candidate_position, 0, 1
|
||||
)
|
||||
|
||||
if non_colliding_position then
|
||||
-- Request pathfinding asynchronously
|
||||
local path_id = surface.request_path{
|
||||
bounding_box = player.character.prototype.collision_box,
|
||||
collision_mask = { layers = { ["player"] = true } },
|
||||
start = original_position,
|
||||
goal = non_colliding_position,
|
||||
force = player.force.name,
|
||||
radius = 1,
|
||||
pathfind_flags = {cache = true, low_priority = true, allow_paths_through_own_entities = true},
|
||||
}
|
||||
|
||||
-- Store the request with the player index as the key
|
||||
teleport_requests[player.index] = path_id
|
||||
else
|
||||
attempt_teleport_player(player, attempt + 1)
|
||||
end
|
||||
end
|
||||
|
||||
function handle_teleport_attempt(event)
|
||||
for player_index, path_id in pairs(teleport_requests) do
|
||||
-- Check if the event matches the stored path_id
|
||||
if path_id == event.id then
|
||||
local player = game.players[player_index]
|
||||
|
||||
if event.path then
|
||||
if player.character then
|
||||
player.character.teleport(event.path[#event.path].position) -- Teleport to the last point in the path
|
||||
-- Clear the attempts for this player
|
||||
teleport_attempts[player_index] = 0
|
||||
return
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
attempt_teleport_player(player, nil)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
function spill_character_inventory(character)
|
||||
if not (character and character.valid) then
|
||||
return false
|
||||
|
||||
@@ -134,9 +134,6 @@ end
|
||||
|
||||
script.on_event(defines.events.on_player_changed_position, on_player_changed_position)
|
||||
{% endif %}
|
||||
-- Handle the pathfinding result of teleport traps
|
||||
script.on_event(defines.events.on_script_path_request_finished, handle_teleport_attempt)
|
||||
|
||||
function count_energy_bridges()
|
||||
local count = 0
|
||||
for i, bridge in pairs(storage.energy_link_bridges) do
|
||||
@@ -146,11 +143,9 @@ function count_energy_bridges()
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
function get_energy_increment(bridge)
|
||||
return ENERGY_INCREMENT + (ENERGY_INCREMENT * 0.3 * bridge.quality.level)
|
||||
end
|
||||
|
||||
function on_check_energy_link(event)
|
||||
--- assuming 1 MJ increment and 5MJ battery:
|
||||
--- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing
|
||||
@@ -727,10 +722,12 @@ end,
|
||||
game.forces["enemy"].set_evolution_factor(new_factor, "nauvis")
|
||||
game.print({"", "New evolution factor:", new_factor})
|
||||
end,
|
||||
["Teleport Trap"] = function()
|
||||
["Teleport Trap"] = function ()
|
||||
for _, player in ipairs(game.forces["player"].players) do
|
||||
if player.character then
|
||||
attempt_teleport_player(player, 1)
|
||||
current_character = player.character
|
||||
if current_character ~= nil then
|
||||
current_character.teleport(current_character.surface.find_non_colliding_position(
|
||||
current_character.prototype.name, random_offset_position(current_character.position, 1024), 0, 1))
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
@@ -50,8 +50,8 @@ item_table: Dict[int, ItemDict] = {
|
||||
'doom_type': 2004,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
370006: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
370006: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Bag of Holding',
|
||||
'doom_type': 8,
|
||||
'episode': -1,
|
||||
@@ -1592,42 +1592,6 @@ item_table: Dict[int, ItemDict] = {
|
||||
'doom_type': 35,
|
||||
'episode': 5,
|
||||
'map': 9},
|
||||
370600: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Crystal Capacity',
|
||||
'doom_type': 65001,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
370601: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Ethereal Arrow Capacity',
|
||||
'doom_type': 65002,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
370602: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Claw Orb Capacity',
|
||||
'doom_type': 65003,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
370603: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Rune Capacity',
|
||||
'doom_type': 65004,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
370604: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Flame Orb Capacity',
|
||||
'doom_type': 65005,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
370605: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Mace Sphere Capacity',
|
||||
'doom_type': 65006,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from Options import PerGameCommonOptions, Range, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -144,116 +144,6 @@ class Episode5(Toggle):
|
||||
display_name = "Episode 5"
|
||||
|
||||
|
||||
class SplitBagOfHolding(Toggle):
|
||||
"""Split the Bag of Holding into six individual items, each one increasing ammo capacity for one type of weapon only."""
|
||||
display_name = "Split Bag of Holding"
|
||||
|
||||
|
||||
class BagOfHoldingCount(Range):
|
||||
"""How many Bags of Holding will be available.
|
||||
If Split Bag of Holding is set, this will be the number of each capacity upgrade available."""
|
||||
display_name = "Bag of Holding Count"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 1
|
||||
|
||||
|
||||
class MaxAmmoCrystals(Range):
|
||||
"""Set the starting ammo capacity for crystals (Elven Wand ammo)."""
|
||||
display_name = "Max Ammo - Crystals"
|
||||
range_start = 100
|
||||
range_end = 999
|
||||
default = 100
|
||||
|
||||
|
||||
class MaxAmmoArrows(Range):
|
||||
"""Set the starting ammo capacity for arrows (Ethereal Crossbow ammo)."""
|
||||
display_name = "Max Ammo - Arrows"
|
||||
range_start = 50
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class MaxAmmoClawOrbs(Range):
|
||||
"""Set the starting ammo capacity for claw orbs (Dragon Claw ammo)."""
|
||||
display_name = "Max Ammo - Claw Orbs"
|
||||
range_start = 200
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class MaxAmmoRunes(Range):
|
||||
"""Set the starting ammo capacity for runes (Hellstaff ammo)."""
|
||||
display_name = "Max Ammo - Runes"
|
||||
range_start = 200
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class MaxAmmoFlameOrbs(Range):
|
||||
"""Set the starting ammo capacity for flame orbs (Phoenix Rod ammo)."""
|
||||
display_name = "Max Ammo - Flame Orbs"
|
||||
range_start = 20
|
||||
range_end = 999
|
||||
default = 20
|
||||
|
||||
|
||||
class MaxAmmoSpheres(Range):
|
||||
"""Set the starting ammo capacity for spheres (Firemace ammo)."""
|
||||
display_name = "Max Ammo - Spheres"
|
||||
range_start = 150
|
||||
range_end = 999
|
||||
default = 150
|
||||
|
||||
|
||||
class AddedAmmoCrystals(Range):
|
||||
"""Set the amount of crystal capacity gained when collecting a bag of holding or a capacity upgrade."""
|
||||
display_name = "Added Ammo - Crystals"
|
||||
range_start = 10
|
||||
range_end = 999
|
||||
default = 100
|
||||
|
||||
|
||||
class AddedAmmoArrows(Range):
|
||||
"""Set the amount of arrow capacity gained when collecting a bag of holding or a capacity upgrade."""
|
||||
display_name = "Added Ammo - Arrows"
|
||||
range_start = 5
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class AddedAmmoClawOrbs(Range):
|
||||
"""Set the amount of claw orb capacity gained when collecting a bag of holding or a capacity upgrade."""
|
||||
display_name = "Added Ammo - Claw Orbs"
|
||||
range_start = 20
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class AddedAmmoRunes(Range):
|
||||
"""Set the amount of rune capacity gained when collecting a bag of holding or a capacity upgrade."""
|
||||
display_name = "Added Ammo - Runes"
|
||||
range_start = 20
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class AddedAmmoFlameOrbs(Range):
|
||||
"""Set the amount of flame orb capacity gained when collecting a bag of holding or a capacity upgrade."""
|
||||
display_name = "Added Ammo - Flame Orbs"
|
||||
range_start = 2
|
||||
range_end = 999
|
||||
default = 20
|
||||
|
||||
|
||||
class AddedAmmoSpheres(Range):
|
||||
"""Set the amount of sphere capacity gained when collecting a bag of holding or a capacity upgrade."""
|
||||
display_name = "Added Ammo - Spheres"
|
||||
range_start = 15
|
||||
range_end = 999
|
||||
default = 150
|
||||
|
||||
|
||||
@dataclass
|
||||
class HereticOptions(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
@@ -273,18 +163,3 @@ class HereticOptions(PerGameCommonOptions):
|
||||
episode3: Episode3
|
||||
episode4: Episode4
|
||||
episode5: Episode5
|
||||
|
||||
split_bag_of_holding: SplitBagOfHolding
|
||||
bag_of_holding_count: BagOfHoldingCount
|
||||
max_ammo_crystals: MaxAmmoCrystals
|
||||
max_ammo_arrows: MaxAmmoArrows
|
||||
max_ammo_claw_orbs: MaxAmmoClawOrbs
|
||||
max_ammo_runes: MaxAmmoRunes
|
||||
max_ammo_flame_orbs: MaxAmmoFlameOrbs
|
||||
max_ammo_spheres: MaxAmmoSpheres
|
||||
added_ammo_crystals: AddedAmmoCrystals
|
||||
added_ammo_arrows: AddedAmmoArrows
|
||||
added_ammo_claw_orbs: AddedAmmoClawOrbs
|
||||
added_ammo_runes: AddedAmmoRunes
|
||||
added_ammo_flame_orbs: AddedAmmoFlameOrbs
|
||||
added_ammo_spheres: AddedAmmoSpheres
|
||||
|
||||
@@ -695,11 +695,13 @@ def set_episode5_rules(player, multiworld, pro):
|
||||
state.has("Phoenix Rod", player, 1) and
|
||||
state.has("Firemace", player, 1) and
|
||||
state.has("Hellstaff", player, 1) and
|
||||
state.has("Gauntlets of the Necromancer", player, 1))
|
||||
state.has("Gauntlets of the Necromancer", player, 1) and
|
||||
state.has("Bag of Holding", player, 1))
|
||||
|
||||
# Skein of D'Sparil (E5M9)
|
||||
set_rule(multiworld.get_entrance("Hub -> Skein of D'Sparil (E5M9) Main", player), lambda state:
|
||||
state.has("Skein of D'Sparil (E5M9)", player, 1) and
|
||||
state.has("Bag of Holding", player, 1) and
|
||||
state.has("Hellstaff", player, 1) and
|
||||
state.has("Phoenix Rod", player, 1) and
|
||||
state.has("Dragon Claw", player, 1) and
|
||||
|
||||
@@ -41,7 +41,7 @@ class HereticWorld(World):
|
||||
options: HereticOptions
|
||||
game = "Heretic"
|
||||
web = HereticWeb()
|
||||
required_client_version = (0, 5, 0) # 1.2.0-prerelease or higher
|
||||
required_client_version = (0, 3, 9)
|
||||
|
||||
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
|
||||
item_name_groups = Items.item_name_groups
|
||||
@@ -206,17 +206,6 @@ class HereticWorld(World):
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
# Bag(s) of Holding based on options
|
||||
if self.options.split_bag_of_holding.value:
|
||||
itempool += [self.create_item("Crystal Capacity") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
itempool += [self.create_item("Ethereal Arrow Capacity") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
itempool += [self.create_item("Claw Orb Capacity") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
itempool += [self.create_item("Rune Capacity") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
itempool += [self.create_item("Flame Orb Capacity") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
itempool += [self.create_item("Mace Sphere Capacity") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
else:
|
||||
itempool += [self.create_item("Bag of Holding") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
|
||||
# Place end level items in locked locations
|
||||
for map_name in Maps.map_names:
|
||||
loc_name = map_name + " - Exit"
|
||||
@@ -285,7 +274,7 @@ class HereticWorld(World):
|
||||
episode_count = self.get_episode_count()
|
||||
count = min(remaining_loc, max(1, self.items_ratio[item_name] * episode_count))
|
||||
if count == 0:
|
||||
logger.warning(f"Warning, no {item_name} will be placed.")
|
||||
logger.warning("Warning, no " + item_name + " will be placed.")
|
||||
return
|
||||
|
||||
for i in range(count):
|
||||
@@ -301,18 +290,4 @@ class HereticWorld(World):
|
||||
slot_data["episode4"] = self.included_episodes[3]
|
||||
slot_data["episode5"] = self.included_episodes[4]
|
||||
|
||||
# Send slot data for ammo capacity values; this must be generic because Doom uses it too
|
||||
slot_data["ammo1start"] = self.options.max_ammo_crystals.value
|
||||
slot_data["ammo2start"] = self.options.max_ammo_arrows.value
|
||||
slot_data["ammo3start"] = self.options.max_ammo_claw_orbs.value
|
||||
slot_data["ammo4start"] = self.options.max_ammo_runes.value
|
||||
slot_data["ammo5start"] = self.options.max_ammo_flame_orbs.value
|
||||
slot_data["ammo6start"] = self.options.max_ammo_spheres.value
|
||||
slot_data["ammo1add"] = self.options.added_ammo_crystals.value
|
||||
slot_data["ammo2add"] = self.options.added_ammo_arrows.value
|
||||
slot_data["ammo3add"] = self.options.added_ammo_claw_orbs.value
|
||||
slot_data["ammo4add"] = self.options.added_ammo_runes.value
|
||||
slot_data["ammo5add"] = self.options.added_ammo_flame_orbs.value
|
||||
slot_data["ammo6add"] = self.options.added_ammo_spheres.value
|
||||
|
||||
return slot_data
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import ModuleUpdate
|
||||
import Utils
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
@@ -24,7 +23,6 @@ class KH2Context(CommonContext):
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super(KH2Context, self).__init__(server_address, password)
|
||||
|
||||
self.goofy_ability_to_slot = dict()
|
||||
self.donald_ability_to_slot = dict()
|
||||
self.all_weapon_location_id = None
|
||||
@@ -37,7 +35,6 @@ class KH2Context(CommonContext):
|
||||
self.serverconneced = False
|
||||
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
|
||||
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
|
||||
self.kh2_data_package = {}
|
||||
self.kh2_loc_name_to_id = None
|
||||
self.kh2_item_name_to_id = None
|
||||
self.lookup_id_to_item = None
|
||||
@@ -86,8 +83,6 @@ class KH2Context(CommonContext):
|
||||
},
|
||||
}
|
||||
self.kh2seedname = None
|
||||
self.kh2_seed_save_path_join = None
|
||||
|
||||
self.kh2slotdata = None
|
||||
self.mem_json = None
|
||||
self.itemamount = {}
|
||||
@@ -119,18 +114,26 @@ class KH2Context(CommonContext):
|
||||
# 255: {}, # starting screen
|
||||
}
|
||||
self.last_world_int = -1
|
||||
# 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room
|
||||
# self.sveroom = 0x2A09C00 + 0x41
|
||||
# 0 not in battle 1 in yellow battle 2 red battle #short
|
||||
# self.inBattle = 0x2A0EAC4 + 0x40
|
||||
# self.onDeath = 0xAB9078
|
||||
# PC Address anchors
|
||||
# epic .10 addresses
|
||||
# self.Now = 0x0714DB8 old address
|
||||
# epic addresses
|
||||
self.Now = 0x0716DF8
|
||||
self.Save = 0x9A9330
|
||||
self.Save = 0x09A92F0
|
||||
self.Journal = 0x743260
|
||||
self.Shop = 0x743350
|
||||
self.Slot1 = 0x2A23018
|
||||
self.Slot1 = 0x2A22FD8
|
||||
# self.Sys3 = 0x2A59DF0
|
||||
# self.Bt10 = 0x2A74880
|
||||
# self.BtlEnd = 0x2A0D3E0
|
||||
# self.Slot1 = 0x2A20C98 old address
|
||||
|
||||
self.kh2_game_version = None # can be egs or steam
|
||||
|
||||
self.kh2_seed_save_path = None
|
||||
|
||||
self.chest_set = set(exclusion_table["Chests"])
|
||||
self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"])
|
||||
self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"])
|
||||
@@ -191,7 +194,8 @@ class KH2Context(CommonContext):
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
if self.kh2seedname is not None and self.auth is not None:
|
||||
with open(self.kh2_seed_save_path_join, 'w') as f:
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
|
||||
'w') as f:
|
||||
f.write(json.dumps(self.kh2_seed_save, indent=4))
|
||||
await super(KH2Context, self).connection_closed()
|
||||
|
||||
@@ -199,7 +203,8 @@ class KH2Context(CommonContext):
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
if self.kh2seedname not in {None} and self.auth not in {None}:
|
||||
with open(self.kh2_seed_save_path_join, 'w') as f:
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
|
||||
'w') as f:
|
||||
f.write(json.dumps(self.kh2_seed_save, indent=4))
|
||||
await super(KH2Context, self).disconnect()
|
||||
|
||||
@@ -212,7 +217,8 @@ class KH2Context(CommonContext):
|
||||
|
||||
async def shutdown(self):
|
||||
if self.kh2seedname not in {None} and self.auth not in {None}:
|
||||
with open(self.kh2_seed_save_path_join, 'w') as f:
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
|
||||
'w') as f:
|
||||
f.write(json.dumps(self.kh2_seed_save, indent=4))
|
||||
await super(KH2Context, self).shutdown()
|
||||
|
||||
@@ -226,7 +232,7 @@ class KH2Context(CommonContext):
|
||||
return self.kh2.write_bytes(self.kh2.base_address + address, value.to_bytes(1, 'big'), 1)
|
||||
|
||||
def kh2_read_byte(self, address):
|
||||
return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1))
|
||||
return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1), "big")
|
||||
|
||||
def kh2_read_int(self, address):
|
||||
return self.kh2.read_int(self.kh2.base_address + address)
|
||||
@@ -238,14 +244,11 @@ class KH2Context(CommonContext):
|
||||
return self.kh2.read_string(self.kh2.base_address + address, length)
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "RoomInfo":
|
||||
if cmd in {"RoomInfo"}:
|
||||
self.kh2seedname = args['seed_name']
|
||||
self.kh2_seed_save_path = f"kh2save2{self.kh2seedname}{self.auth}.json"
|
||||
self.kh2_seed_save_path_join = os.path.join(self.game_communication_path, self.kh2_seed_save_path)
|
||||
|
||||
if not os.path.exists(self.game_communication_path):
|
||||
os.makedirs(self.game_communication_path)
|
||||
if not os.path.exists(self.kh2_seed_save_path_join):
|
||||
if not os.path.exists(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json"):
|
||||
self.kh2_seed_save = {
|
||||
"Levels": {
|
||||
"SoraLevel": 0,
|
||||
@@ -258,11 +261,12 @@ class KH2Context(CommonContext):
|
||||
},
|
||||
"SoldEquipment": [],
|
||||
}
|
||||
with open(self.kh2_seed_save_path_join, 'wt') as f:
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
|
||||
'wt') as f:
|
||||
pass
|
||||
# self.locations_checked = set()
|
||||
elif os.path.exists(self.kh2_seed_save_path_join):
|
||||
with open(self.kh2_seed_save_path_join) as f:
|
||||
elif os.path.exists(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json"):
|
||||
with open(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json", 'r') as f:
|
||||
self.kh2_seed_save = json.load(f)
|
||||
if self.kh2_seed_save is None:
|
||||
self.kh2_seed_save = {
|
||||
@@ -280,22 +284,13 @@ class KH2Context(CommonContext):
|
||||
# self.locations_checked = set(self.kh2_seed_save_cache["LocationsChecked"])
|
||||
# self.serverconneced = True
|
||||
|
||||
if cmd == "Connected":
|
||||
if cmd in {"Connected"}:
|
||||
asyncio.create_task(self.send_msgs([{"cmd": "GetDataPackage", "games": ["Kingdom Hearts 2"]}]))
|
||||
self.kh2slotdata = args['slot_data']
|
||||
|
||||
self.kh2_data_package = Utils.load_data_package_for_checksum(
|
||||
"Kingdom Hearts 2", self.checksums["Kingdom Hearts 2"])
|
||||
|
||||
if "location_name_to_id" in self.kh2_data_package:
|
||||
self.data_package_kh2_cache(
|
||||
self.kh2_data_package["location_name_to_id"], self.kh2_data_package["item_name_to_id"])
|
||||
self.connect_to_game()
|
||||
else:
|
||||
asyncio.create_task(self.send_msgs([{"cmd": "GetDataPackage", "games": ["Kingdom Hearts 2"]}]))
|
||||
|
||||
# self.kh2_local_items = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()}
|
||||
self.locations_checked = set(args["checked_locations"])
|
||||
|
||||
if cmd == "ReceivedItems":
|
||||
if cmd in {"ReceivedItems"}:
|
||||
# 0x2546
|
||||
# 0x2658
|
||||
# 0x276A
|
||||
@@ -343,44 +338,42 @@ class KH2Context(CommonContext):
|
||||
for item in args['items']:
|
||||
asyncio.create_task(self.give_item(item.item, item.location))
|
||||
|
||||
if cmd == "RoomUpdate":
|
||||
if cmd in {"RoomUpdate"}:
|
||||
if "checked_locations" in args:
|
||||
new_locations = set(args["checked_locations"])
|
||||
self.locations_checked |= new_locations
|
||||
|
||||
if cmd == "DataPackage":
|
||||
if cmd in {"DataPackage"}:
|
||||
if "Kingdom Hearts 2" in args["data"]["games"]:
|
||||
self.data_package_kh2_cache(
|
||||
args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"],
|
||||
args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"])
|
||||
self.connect_to_game()
|
||||
asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}]))
|
||||
self.data_package_kh2_cache(args)
|
||||
if "KeybladeAbilities" in self.kh2slotdata.keys():
|
||||
# sora ability to slot
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"])
|
||||
# itemid:[slots that are available for that item]
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["StaffAbilities"])
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["ShieldAbilities"])
|
||||
|
||||
def connect_to_game(self):
|
||||
if "KeybladeAbilities" in self.kh2slotdata.keys():
|
||||
# sora ability to slot
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"])
|
||||
# itemid:[slots that are available for that item]
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["StaffAbilities"])
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["ShieldAbilities"])
|
||||
all_weapon_location_id = []
|
||||
for weapon_location in all_weapon_slot:
|
||||
all_weapon_location_id.append(self.kh2_loc_name_to_id[weapon_location])
|
||||
self.all_weapon_location_id = set(all_weapon_location_id)
|
||||
|
||||
self.all_weapon_location_id = {self.kh2_loc_name_to_id[loc] for loc in all_weapon_slot}
|
||||
try:
|
||||
if not self.kh2:
|
||||
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
self.get_addresses()
|
||||
|
||||
try:
|
||||
if not self.kh2:
|
||||
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
self.get_addresses()
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
self.kh2connected = False
|
||||
logger.info("Game is not open.")
|
||||
self.serverconneced = True
|
||||
asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}]))
|
||||
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
self.kh2connected = False
|
||||
logger.info("Game is not open.")
|
||||
self.serverconneced = True
|
||||
|
||||
def data_package_kh2_cache(self, loc_to_id, item_to_id):
|
||||
self.kh2_loc_name_to_id = loc_to_id
|
||||
def data_package_kh2_cache(self, args):
|
||||
self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"]
|
||||
self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()}
|
||||
self.kh2_item_name_to_id = item_to_id
|
||||
self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"]
|
||||
self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()}
|
||||
self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]]
|
||||
|
||||
@@ -749,8 +742,7 @@ class KH2Context(CommonContext):
|
||||
for item_name in master_stat:
|
||||
amount_of_items = 0
|
||||
amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][item_name]
|
||||
# checking if they talked to the computer to give them these
|
||||
if self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and (self.kh2_read_byte(self.Save + 0x1D27) & 0x1 << 3) > 0:
|
||||
if self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5:
|
||||
if item_name == ItemName.MaxHPUp:
|
||||
if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical
|
||||
Bonus = 5
|
||||
@@ -816,33 +808,34 @@ class KH2Context(CommonContext):
|
||||
def get_addresses(self):
|
||||
if not self.kh2connected and self.kh2 is not None:
|
||||
if self.kh2_game_version is None:
|
||||
# current verions is .10 then runs the get from github stuff
|
||||
if self.kh2_read_string(0x9A98B0, 4) == "KH2J":
|
||||
|
||||
if self.kh2_read_string(0x09A9830, 4) == "KH2J":
|
||||
self.kh2_game_version = "STEAM"
|
||||
self.Now = 0x0717008
|
||||
self.Save = 0x09A98B0
|
||||
self.Slot1 = 0x2A23598
|
||||
self.Save = 0x09A9830
|
||||
self.Slot1 = 0x2A23518
|
||||
self.Journal = 0x7434E0
|
||||
self.Shop = 0x7435D0
|
||||
elif self.kh2_read_string(0x9A9330, 4) == "KH2J":
|
||||
elif self.kh2_read_string(0x09A92F0, 4) == "KH2J":
|
||||
self.kh2_game_version = "EGS"
|
||||
else:
|
||||
if self.game_communication_path:
|
||||
logger.info("Checking with most up to date addresses from the addresses json.")
|
||||
logger.info("Checking with most up to date addresses of github. If file is not found will be downloading datafiles. This might take a moment")
|
||||
#if mem addresses file is found then check version and if old get new one
|
||||
kh2memaddresses_path = os.path.join(self.game_communication_path, "kh2memaddresses.json")
|
||||
kh2memaddresses_path = os.path.join(self.game_communication_path, f"kh2memaddresses.json")
|
||||
if not os.path.exists(kh2memaddresses_path):
|
||||
logger.info("File is not found. Downloading json with memory addresses. This might take a moment")
|
||||
mem_resp = requests.get("https://raw.githubusercontent.com/JaredWeakStrike/KH2APMemoryValues/master/kh2memaddresses.json")
|
||||
if mem_resp.status_code == 200:
|
||||
self.mem_json = json.loads(mem_resp.content)
|
||||
with open(kh2memaddresses_path, 'w') as f:
|
||||
with open(kh2memaddresses_path,
|
||||
'w') as f:
|
||||
f.write(json.dumps(self.mem_json, indent=4))
|
||||
else:
|
||||
with open(kh2memaddresses_path) as f:
|
||||
with open(kh2memaddresses_path, 'r') as f:
|
||||
self.mem_json = json.load(f)
|
||||
if self.mem_json:
|
||||
for key in self.mem_json.keys():
|
||||
|
||||
if self.kh2_read_string(int(self.mem_json[key]["GameVersionCheck"], 0), 4) == "KH2J":
|
||||
self.Now = int(self.mem_json[key]["Now"], 0)
|
||||
self.Save = int(self.mem_json[key]["Save"], 0)
|
||||
|
||||
@@ -368,37 +368,6 @@ def patch_kh2(self, output_directory):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'name': 'msg/us/he.bar',
|
||||
'multi': [
|
||||
{
|
||||
'name': 'msg/fr/he.bar'
|
||||
},
|
||||
{
|
||||
'name': 'msg/gr/he.bar'
|
||||
},
|
||||
{
|
||||
'name': 'msg/it/he.bar'
|
||||
},
|
||||
{
|
||||
'name': 'msg/sp/he.bar'
|
||||
}
|
||||
],
|
||||
'method': 'binarc',
|
||||
'source': [
|
||||
{
|
||||
'name': 'he',
|
||||
'type': 'list',
|
||||
'method': 'kh2msg',
|
||||
'source': [
|
||||
{
|
||||
'name': 'he.yml',
|
||||
'language': 'en'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
'title': 'Randomizer Seed'
|
||||
}
|
||||
@@ -442,34 +411,6 @@ def patch_kh2(self, output_directory):
|
||||
'en': f"Your Level Depth is {self.options.LevelDepth.current_option_name}"
|
||||
}
|
||||
]
|
||||
self.fight_and_form_text = [
|
||||
{
|
||||
'id': 15121, # poster name
|
||||
'en': f"Game Options"
|
||||
},
|
||||
{
|
||||
'id': 15122,
|
||||
'en': f"Fight Logic is {self.options.FightLogic.current_option_name}\n"
|
||||
f"Auto Form Logic is {self.options.AutoFormLogic.current_option_name}\n"
|
||||
f"Final Form Logic is {self.options.FinalFormLogic.current_option_name}"
|
||||
}
|
||||
|
||||
]
|
||||
self.cups_text = [
|
||||
{
|
||||
'id': 4043,
|
||||
'en': f"CupsToggle: {self.options.Cups.current_option_name}"
|
||||
},
|
||||
{
|
||||
'id': 4044,
|
||||
'en': f"CupsToggle: {self.options.Cups.current_option_name}"
|
||||
},
|
||||
{
|
||||
'id': 4045,
|
||||
'en': f"CupsToggle: {self.options.Cups.current_option_name}"
|
||||
},
|
||||
]
|
||||
|
||||
mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__)
|
||||
|
||||
self.mod_yml["title"] = f"Randomizer Seed {mod_name}"
|
||||
@@ -482,8 +423,7 @@ def patch_kh2(self, output_directory):
|
||||
"FmlvList.yml": yaml.dump(self.formattedFmlv, line_break="\n"),
|
||||
"mod.yml": yaml.dump(self.mod_yml, line_break="\n"),
|
||||
"po.yml": yaml.dump(self.pooh_text, line_break="\n"),
|
||||
"sys.yml": yaml.dump(self.level_depth_text + self.fight_and_form_text, line_break="\n"),
|
||||
"he.yml": yaml.dump(self.cups_text, line_break="\n")
|
||||
"sys.yml": yaml.dump(self.level_depth_text, line_break="\n"),
|
||||
}
|
||||
|
||||
mod = KH2Container(openkhmod, mod_dir, output_directory, self.player,
|
||||
|
||||
@@ -1,266 +1,92 @@
|
||||
import json
|
||||
import typing
|
||||
from websockets import WebSocketServerProtocol
|
||||
roomAddress = 0xFFF6
|
||||
mapIdAddress = 0xFFF7
|
||||
indoorFlagAddress = 0xDBA5
|
||||
entranceRoomOffset = 0xD800
|
||||
screenCoordAddress = 0xFFFA
|
||||
|
||||
from . import TrackerConsts as Consts
|
||||
from .TrackerConsts import EntranceCoord
|
||||
from .LADXR.entranceInfo import ENTRANCE_INFO
|
||||
|
||||
class Entrance:
|
||||
outdoor_room: int
|
||||
indoor_map: int
|
||||
indoor_address: int
|
||||
name: str
|
||||
other_side_name: str = None
|
||||
changed: bool = False
|
||||
known_to_server: bool = False
|
||||
|
||||
def __init__(self, outdoor: int, indoor: int, name: str, indoor_address: int=None):
|
||||
self.outdoor_room = outdoor
|
||||
self.indoor_map = indoor
|
||||
self.indoor_address = indoor_address
|
||||
self.name = name
|
||||
|
||||
def map(self, other_side: str, known_to_server: bool = False):
|
||||
if other_side != self.other_side_name:
|
||||
self.changed = True
|
||||
self.known_to_server = known_to_server
|
||||
|
||||
self.other_side_name = other_side
|
||||
mapMap = {
|
||||
0x00: 0x01,
|
||||
0x01: 0x01,
|
||||
0x02: 0x01,
|
||||
0x03: 0x01,
|
||||
0x04: 0x01,
|
||||
0x05: 0x01,
|
||||
0x06: 0x02,
|
||||
0x07: 0x02,
|
||||
0x08: 0x02,
|
||||
0x09: 0x02,
|
||||
0x0A: 0x02,
|
||||
0x0B: 0x02,
|
||||
0x0C: 0x02,
|
||||
0x0D: 0x02,
|
||||
0x0E: 0x02,
|
||||
0x0F: 0x02,
|
||||
0x10: 0x02,
|
||||
0x11: 0x02,
|
||||
0x12: 0x02,
|
||||
0x13: 0x02,
|
||||
0x14: 0x02,
|
||||
0x15: 0x02,
|
||||
0x16: 0x02,
|
||||
0x17: 0x02,
|
||||
0x18: 0x02,
|
||||
0x19: 0x02,
|
||||
0x1D: 0x01,
|
||||
0x1E: 0x01,
|
||||
0x1F: 0x01,
|
||||
0xFF: 0x03,
|
||||
}
|
||||
|
||||
class GpsTracker:
|
||||
room: int = None
|
||||
last_room: int = None
|
||||
last_different_room: int = None
|
||||
room_same_for: int = 0
|
||||
room_changed: bool = False
|
||||
screen_x: int = 0
|
||||
screen_y: int = 0
|
||||
spawn_x: int = 0
|
||||
spawn_y: int = 0
|
||||
indoors: int = None
|
||||
indoors_changed: bool = False
|
||||
spawn_map: int = None
|
||||
spawn_room: int = None
|
||||
spawn_changed: bool = False
|
||||
spawn_same_for: int = 0
|
||||
entrance_mapping: typing.Dict[str, str] = None
|
||||
entrances_by_name: typing.Dict[str, Entrance] = {}
|
||||
needs_found_entrances: bool = False
|
||||
needs_slot_data: bool = True
|
||||
room = None
|
||||
location_changed = False
|
||||
screenX = 0
|
||||
screenY = 0
|
||||
indoors = None
|
||||
|
||||
def __init__(self, gameboy) -> None:
|
||||
self.gameboy = gameboy
|
||||
|
||||
self.gameboy.set_location_range(
|
||||
Consts.link_motion_state,
|
||||
Consts.transition_sequence - Consts.link_motion_state + 1,
|
||||
[Consts.transition_state]
|
||||
)
|
||||
|
||||
async def read_byte(self, b: int):
|
||||
return (await self.gameboy.read_memory_cache([b]))[b]
|
||||
|
||||
def load_slot_data(self, slot_data: typing.Dict[str, typing.Any]):
|
||||
if 'entrance_mapping' not in slot_data:
|
||||
return
|
||||
|
||||
# We need to know how entrances were mapped at generation before we can autotrack them
|
||||
self.entrance_mapping = {}
|
||||
|
||||
# Convert to upstream's newer format
|
||||
for outside, inside in slot_data['entrance_mapping'].items():
|
||||
new_inside = f"{inside}:inside"
|
||||
self.entrance_mapping[outside] = new_inside
|
||||
self.entrance_mapping[new_inside] = outside
|
||||
|
||||
self.entrances_by_name = {}
|
||||
|
||||
for name, info in ENTRANCE_INFO.items():
|
||||
alternate_address = (
|
||||
Consts.entrance_address_overrides[info.target]
|
||||
if info.target in Consts.entrance_address_overrides
|
||||
else None
|
||||
)
|
||||
|
||||
entrance = Entrance(info.room, info.target, name, alternate_address)
|
||||
self.entrances_by_name[name] = entrance
|
||||
|
||||
inside_entrance = Entrance(info.target, info.room, f"{name}:inside", alternate_address)
|
||||
self.entrances_by_name[f"{name}:inside"] = inside_entrance
|
||||
|
||||
self.needs_slot_data = False
|
||||
self.needs_found_entrances = True
|
||||
async def read_byte(self, b):
|
||||
return (await self.gameboy.async_read_memory(b))[0]
|
||||
|
||||
async def read_location(self):
|
||||
# We need to wait for screen transitions to finish
|
||||
transition_state = await self.read_byte(Consts.transition_state)
|
||||
transition_target_x = await self.read_byte(Consts.transition_target_x)
|
||||
transition_target_y = await self.read_byte(Consts.transition_target_y)
|
||||
transition_scroll_x = await self.read_byte(Consts.transition_scroll_x)
|
||||
transition_scroll_y = await self.read_byte(Consts.transition_scroll_y)
|
||||
transition_sequence = await self.read_byte(Consts.transition_sequence)
|
||||
motion_state = await self.read_byte(Consts.link_motion_state)
|
||||
if (transition_state != 0
|
||||
or transition_target_x != transition_scroll_x
|
||||
or transition_target_y != transition_scroll_y
|
||||
or transition_sequence != 0x04):
|
||||
return
|
||||
|
||||
indoors = await self.read_byte(Consts.indoor_flag)
|
||||
indoors = await self.read_byte(indoorFlagAddress)
|
||||
|
||||
if indoors != self.indoors and self.indoors != None:
|
||||
self.indoors_changed = True
|
||||
|
||||
self.indoorsChanged = True
|
||||
|
||||
self.indoors = indoors
|
||||
|
||||
# We use the spawn point to know which entrance was most recently entered
|
||||
spawn_map = await self.read_byte(Consts.spawn_map)
|
||||
map_digit = Consts.map_map[spawn_map] << 8 if self.spawn_map else 0
|
||||
spawn_room = await self.read_byte(Consts.spawn_room) + map_digit
|
||||
spawn_x = await self.read_byte(Consts.spawn_x)
|
||||
spawn_y = await self.read_byte(Consts.spawn_y)
|
||||
|
||||
# The spawn point needs to be settled before we can trust location data
|
||||
if ((spawn_room != self.spawn_room and self.spawn_room != None)
|
||||
or (spawn_map != self.spawn_map and self.spawn_map != None)
|
||||
or (spawn_x != self.spawn_x and self.spawn_x != None)
|
||||
or (spawn_y != self.spawn_y and self.spawn_y != None)):
|
||||
self.spawn_changed = True
|
||||
self.spawn_same_for = 0
|
||||
else:
|
||||
self.spawn_same_for += 1
|
||||
|
||||
self.spawn_map = spawn_map
|
||||
self.spawn_room = spawn_room
|
||||
self.spawn_x = spawn_x
|
||||
self.spawn_y = spawn_y
|
||||
|
||||
# Spawn point is preferred, but doesn't work for the sidescroller entrances
|
||||
# Those can be addressed by keeping track of which room we're in
|
||||
# Also used to validate that we came from the right room for what the spawn point is mapped to
|
||||
map_id = await self.read_byte(Consts.map_id)
|
||||
if map_id not in Consts.map_map:
|
||||
print(f'Unknown map ID {hex(map_id)}')
|
||||
mapId = await self.read_byte(mapIdAddress)
|
||||
if mapId not in mapMap:
|
||||
print(f'Unknown map ID {hex(mapId)}')
|
||||
return
|
||||
|
||||
map_digit = Consts.map_map[map_id] << 8 if indoors else 0
|
||||
self.last_room = self.room
|
||||
self.room = await self.read_byte(Consts.room) + map_digit
|
||||
mapDigit = mapMap[mapId] << 8 if indoors else 0
|
||||
last_room = self.room
|
||||
self.room = await self.read_byte(roomAddress) + mapDigit
|
||||
|
||||
# Again, the room needs to settle before we can trust location data
|
||||
if self.last_room != self.room:
|
||||
self.room_same_for = 0
|
||||
self.room_changed = True
|
||||
self.last_different_room = self.last_room
|
||||
else:
|
||||
self.room_same_for += 1
|
||||
coords = await self.read_byte(screenCoordAddress)
|
||||
self.screenX = coords & 0x0F
|
||||
self.screenY = (coords & 0xF0) >> 4
|
||||
|
||||
# Only update Link's location when he's not in the air to avoid weirdness
|
||||
if motion_state in [0, 1]:
|
||||
coords = await self.read_byte(Consts.screen_coord)
|
||||
self.screen_x = coords & 0x0F
|
||||
self.screen_y = (coords & 0xF0) >> 4
|
||||
|
||||
async def read_entrances(self):
|
||||
if not self.last_different_room or not self.entrance_mapping:
|
||||
if (self.room != last_room):
|
||||
self.location_changed = True
|
||||
|
||||
last_message = {}
|
||||
async def send_location(self, socket, diff=False):
|
||||
if self.room is None:
|
||||
return
|
||||
|
||||
if self.spawn_changed and self.spawn_same_for > 0 and self.room_same_for > 0:
|
||||
# Use the spawn location, last room, and entrance mapping at generation to map the right entrance
|
||||
# A bit overkill for simple ER, but necessary for upstream's advanced ER
|
||||
spawn_coord = EntranceCoord(None, self.spawn_room, self.spawn_x, self.spawn_y)
|
||||
if str(spawn_coord) in Consts.entrance_lookup:
|
||||
valid_sources = {x.name for x in Consts.entrance_coords if x.room == self.last_different_room}
|
||||
dest_entrance = Consts.entrance_lookup[str(spawn_coord)].name
|
||||
source_entrance = [
|
||||
x for x in self.entrance_mapping
|
||||
if self.entrance_mapping[x] == dest_entrance and x in valid_sources
|
||||
]
|
||||
|
||||
if source_entrance:
|
||||
self.entrances_by_name[source_entrance[0]].map(dest_entrance)
|
||||
|
||||
self.spawn_changed = False
|
||||
elif self.room_changed and self.room_same_for > 0:
|
||||
# Check for the stupid sidescroller rooms that don't set your spawn point
|
||||
if self.last_different_room in Consts.sidescroller_rooms:
|
||||
source_entrance = Consts.sidescroller_rooms[self.last_different_room]
|
||||
if source_entrance in self.entrance_mapping:
|
||||
dest_entrance = self.entrance_mapping[source_entrance]
|
||||
|
||||
expected_room = self.entrances_by_name[dest_entrance].outdoor_room
|
||||
if dest_entrance.endswith(":indoor"):
|
||||
expected_room = self.entrances_by_name[dest_entrance].indoor_map
|
||||
|
||||
if expected_room == self.room:
|
||||
self.entrances_by_name[source_entrance].map(dest_entrance)
|
||||
|
||||
if self.room in Consts.sidescroller_rooms:
|
||||
valid_sources = {x.name for x in Consts.entrance_coords if x.room == self.last_different_room}
|
||||
dest_entrance = Consts.sidescroller_rooms[self.room]
|
||||
source_entrance = [
|
||||
x for x in self.entrance_mapping
|
||||
if self.entrance_mapping[x] == dest_entrance and x in valid_sources
|
||||
]
|
||||
|
||||
if source_entrance:
|
||||
self.entrances_by_name[source_entrance[0]].map(dest_entrance)
|
||||
|
||||
self.room_changed = False
|
||||
|
||||
last_location_message = {}
|
||||
async def send_location(self, socket: WebSocketServerProtocol) -> None:
|
||||
if self.room is None or self.room_same_for < 1:
|
||||
return
|
||||
|
||||
message = {
|
||||
"type":"location",
|
||||
"refresh": True,
|
||||
"version":"1.0",
|
||||
"room": f'0x{self.room:02X}',
|
||||
"x": self.screen_x,
|
||||
"y": self.screen_y,
|
||||
"drawFine": True,
|
||||
"x": self.screenX,
|
||||
"y": self.screenY,
|
||||
}
|
||||
|
||||
if message != self.last_location_message:
|
||||
self.last_location_message = message
|
||||
if message != self.last_message:
|
||||
self.last_message = message
|
||||
await socket.send(json.dumps(message))
|
||||
|
||||
async def send_entrances(self, socket: WebSocketServerProtocol, diff: bool=True) -> typing.Dict[str, str]:
|
||||
if not self.entrance_mapping:
|
||||
return
|
||||
|
||||
new_entrances = [x for x in self.entrances_by_name.values() if x.changed or (not diff and x.other_side_name)]
|
||||
|
||||
if not new_entrances:
|
||||
return
|
||||
|
||||
message = {
|
||||
"type":"entrance",
|
||||
"refresh": True,
|
||||
"diff": True,
|
||||
"entranceMap": {},
|
||||
}
|
||||
|
||||
for entrance in new_entrances:
|
||||
message['entranceMap'][entrance.name] = entrance.other_side_name
|
||||
entrance.changed = False
|
||||
|
||||
await socket.send(json.dumps(message))
|
||||
|
||||
new_to_server = {
|
||||
entrance.name: entrance.other_side_name
|
||||
for entrance in new_entrances
|
||||
if not entrance.known_to_server
|
||||
}
|
||||
|
||||
return new_to_server
|
||||
|
||||
def receive_found_entrances(self, found_entrances: typing.Dict[str, str]):
|
||||
if not found_entrances:
|
||||
return
|
||||
|
||||
for entrance, destination in found_entrances.items():
|
||||
if entrance in self.entrances_by_name:
|
||||
self.entrances_by_name[entrance].map(destination, known_to_server=True)
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import json
|
||||
gameStateAddress = 0xDB95
|
||||
validGameStates = {0x0B, 0x0C}
|
||||
gameStateResetThreshold = 0x06
|
||||
|
||||
inventorySlotCount = 16
|
||||
inventoryStartAddress = 0xDB00
|
||||
inventoryEndAddress = inventoryStartAddress + inventorySlotCount
|
||||
|
||||
rupeesHigh = 0xDB5D
|
||||
rupeesLow = 0xDB5E
|
||||
addRupeesHigh = 0xDB8F
|
||||
addRupeesLow = 0xDB90
|
||||
removeRupeesHigh = 0xDB91
|
||||
removeRupeesLow = 0xDB92
|
||||
|
||||
inventoryItemIds = {
|
||||
0x02: 'BOMB',
|
||||
0x05: 'BOW',
|
||||
@@ -102,11 +98,10 @@ dungeonItemOffsets = {
|
||||
'STONE_BEAK{}': 2,
|
||||
'NIGHTMARE_KEY{}': 3,
|
||||
'KEY{}': 4,
|
||||
'UNUSED_KEY{}': 4,
|
||||
}
|
||||
|
||||
class Item:
|
||||
def __init__(self, id, address, threshold=0, mask=None, increaseOnly=False, count=False, max=None, encodedCount=True):
|
||||
def __init__(self, id, address, threshold=0, mask=None, increaseOnly=False, count=False, max=None):
|
||||
self.id = id
|
||||
self.address = address
|
||||
self.threshold = threshold
|
||||
@@ -117,7 +112,6 @@ class Item:
|
||||
self.rawValue = 0
|
||||
self.diff = 0
|
||||
self.max = max
|
||||
self.encodedCount = encodedCount
|
||||
|
||||
def set(self, byte, extra):
|
||||
oldValue = self.value
|
||||
@@ -127,7 +121,7 @@ class Item:
|
||||
|
||||
if not self.count:
|
||||
byte = int(byte > self.threshold)
|
||||
elif self.encodedCount:
|
||||
else:
|
||||
# LADX seems to store one decimal digit per nibble
|
||||
byte = byte - (byte // 16 * 6)
|
||||
|
||||
@@ -171,7 +165,6 @@ class ItemTracker:
|
||||
Item('BOOMERANG', None),
|
||||
Item('TOADSTOOL', None),
|
||||
Item('ROOSTER', None),
|
||||
Item('RUPEE_COUNT', None, count=True, encodedCount=False),
|
||||
Item('SWORD', 0xDB4E, count=True),
|
||||
Item('POWER_BRACELET', 0xDB43, count=True),
|
||||
Item('SHIELD', 0xDB44, count=True),
|
||||
@@ -226,9 +219,9 @@ class ItemTracker:
|
||||
|
||||
self.itemDict = {item.id: item for item in self.items}
|
||||
|
||||
async def readItems(self):
|
||||
extraItems = self.extraItems
|
||||
missingItems = {x for x in self.items if x.address == None and x.id != 'RUPEE_COUNT'}
|
||||
async def readItems(state):
|
||||
extraItems = state.extraItems
|
||||
missingItems = {x for x in state.items if x.address == None}
|
||||
|
||||
# Add keys for opened key doors
|
||||
for i in range(len(dungeonKeyDoors)):
|
||||
@@ -237,16 +230,16 @@ class ItemTracker:
|
||||
|
||||
for address, masks in dungeonKeyDoors[i].items():
|
||||
for mask in masks:
|
||||
value = await self.readRamByte(address) & mask
|
||||
value = await state.readRamByte(address) & mask
|
||||
if value > 0:
|
||||
extraItems[item] += 1
|
||||
|
||||
# Main inventory items
|
||||
for i in range(inventoryStartAddress, inventoryEndAddress):
|
||||
value = await self.readRamByte(i)
|
||||
value = await state.readRamByte(i)
|
||||
|
||||
if value in inventoryItemIds:
|
||||
item = self.itemDict[inventoryItemIds[value]]
|
||||
item = state.itemDict[inventoryItemIds[value]]
|
||||
extra = extraItems[item.id] if item.id in extraItems else 0
|
||||
item.set(1, extra)
|
||||
missingItems.remove(item)
|
||||
@@ -256,21 +249,9 @@ class ItemTracker:
|
||||
item.set(0, extra)
|
||||
|
||||
# All other items
|
||||
for item in [x for x in self.items if x.address]:
|
||||
for item in [x for x in state.items if x.address]:
|
||||
extra = extraItems[item.id] if item.id in extraItems else 0
|
||||
item.set(await self.readRamByte(item.address), extra)
|
||||
|
||||
# The current rupee count is BCD, but the add/remove values are not
|
||||
currentRupees = self.calculateRupeeCount(await self.readRamByte(rupeesHigh), await self.readRamByte(rupeesLow))
|
||||
addingRupees = (await self.readRamByte(addRupeesHigh) << 8) + await self.readRamByte(addRupeesLow)
|
||||
removingRupees = (await self.readRamByte(removeRupeesHigh) << 8) + await self.readRamByte(removeRupeesLow)
|
||||
self.itemDict['RUPEE_COUNT'].set(currentRupees + addingRupees - removingRupees, 0)
|
||||
|
||||
def calculateRupeeCount(self, high: int, low: int) -> int:
|
||||
return (high - (high // 16 * 6)) * 100 + (low - (low // 16 * 6))
|
||||
|
||||
def setExtraItem(self, item: str, qty: int) -> None:
|
||||
self.extraItems[item] = qty
|
||||
item.set(await state.readRamByte(item.address), extra)
|
||||
|
||||
async def sendItems(self, socket, diff=False):
|
||||
if not self.items:
|
||||
@@ -278,6 +259,7 @@ class ItemTracker:
|
||||
message = {
|
||||
"type":"item",
|
||||
"refresh": True,
|
||||
"version":"1.0",
|
||||
"diff": diff,
|
||||
"items": [],
|
||||
}
|
||||
|
||||
@@ -7,12 +7,23 @@ from ..roomEditor import RoomEditor
|
||||
|
||||
|
||||
class StartItem(DroppedKey):
|
||||
# We need to give something here that we can use to progress.
|
||||
# FEATHER
|
||||
OPTIONS = [SWORD, SHIELD, POWER_BRACELET, OCARINA, BOOMERANG, MAGIC_ROD, TAIL_KEY, SHOVEL, HOOKSHOT, PEGASUS_BOOTS, MAGIC_POWDER, BOMB]
|
||||
MULTIWORLD = False
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(0x2A3)
|
||||
self.give_bowwow = False
|
||||
|
||||
def configure(self, options):
|
||||
if options.bowwow != 'normal':
|
||||
# When we have bowwow mode, we pretend to be a sword for logic reasons
|
||||
self.OPTIONS = [SWORD]
|
||||
self.give_bowwow = True
|
||||
if options.randomstartlocation and options.entranceshuffle != 'none':
|
||||
self.OPTIONS.append(FLIPPERS)
|
||||
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
assert multiworld is None
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class World:
|
||||
|
||||
mabe_village = Location("Mabe Village")
|
||||
Location().add(HeartPiece(0x2A4)).connect(mabe_village, r.bush) # well
|
||||
Location().add(FishingMinigame()).connect(mabe_village, AND(r.can_farm, COUNT("RUPEES", 20))) # fishing game, heart piece is directly done by the minigame.
|
||||
Location().add(FishingMinigame()).connect(mabe_village, AND(r.bush, COUNT("RUPEES", 20))) # fishing game, heart piece is directly done by the minigame.
|
||||
Location().add(Seashell(0x0A3)).connect(mabe_village, r.bush) # bushes below the shop
|
||||
Location().add(Seashell(0x0D2)).connect(mabe_village, PEGASUS_BOOTS) # smash into tree next to lv1
|
||||
Location().add(Song(0x092)).connect(mabe_village, OCARINA) # Marins song
|
||||
@@ -23,7 +23,7 @@ class World:
|
||||
papahl_house.connect(mamasha_trade, TRADING_ITEM_YOSHI_DOLL)
|
||||
|
||||
trendy_shop = Location("Trendy Shop")
|
||||
trendy_shop.connect(Location().add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)), AND(r.can_farm, FOUND("RUPEES", 50)))
|
||||
trendy_shop.connect(Location().add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)), FOUND("RUPEES", 50))
|
||||
outside_trendy = Location()
|
||||
outside_trendy.connect(mabe_village, r.bush)
|
||||
|
||||
@@ -43,8 +43,8 @@ class World:
|
||||
self._addEntrance("start_house", mabe_village, start_house, None)
|
||||
|
||||
shop = Location("Shop")
|
||||
Location().add(ShopItem(0)).connect(shop, OR(AND(r.can_farm, COUNT("RUPEES", 500)), SWORD))
|
||||
Location().add(ShopItem(1)).connect(shop, OR(AND(r.can_farm, COUNT("RUPEES", 1480)), SWORD))
|
||||
Location().add(ShopItem(0)).connect(shop, OR(COUNT("RUPEES", 500), SWORD))
|
||||
Location().add(ShopItem(1)).connect(shop, OR(COUNT("RUPEES", 1480), SWORD))
|
||||
self._addEntrance("shop", mabe_village, shop, None)
|
||||
|
||||
dream_hut = Location("Dream Hut")
|
||||
@@ -164,7 +164,7 @@ class World:
|
||||
self._addEntrance("prairie_left_cave2", ukuku_prairie, prairie_left_cave2, BOMB)
|
||||
self._addEntranceRequirementExit("prairie_left_cave2", None) # if exiting, you do not need bombs
|
||||
|
||||
mamu = Location().connect(Location().add(Song(0x2FB)), AND(OCARINA, r.can_farm, COUNT("RUPEES", 1480)))
|
||||
mamu = Location().connect(Location().add(Song(0x2FB)), AND(OCARINA, COUNT("RUPEES", 1480)))
|
||||
self._addEntrance("mamu", ukuku_prairie, mamu, AND(OR(AND(FEATHER, PEGASUS_BOOTS), ROOSTER), OR(HOOKSHOT, ROOSTER), POWER_BRACELET))
|
||||
|
||||
dungeon3_entrance = Location().connect(ukuku_prairie, OR(FEATHER, ROOSTER, FLIPPERS))
|
||||
@@ -377,7 +377,7 @@ class World:
|
||||
|
||||
# Raft game.
|
||||
raft_house = Location("Raft House")
|
||||
Location().add(KeyLocation("RAFT")).connect(raft_house, AND(r.can_farm, COUNT("RUPEES", 100)))
|
||||
Location().add(KeyLocation("RAFT")).connect(raft_house, AND(r.bush, COUNT("RUPEES", 100))) # add bush requirement for farming in case player has to try again
|
||||
raft_return_upper = Location()
|
||||
raft_return_lower = Location().connect(raft_return_upper, None, one_way=True)
|
||||
outside_raft_house = Location().connect(below_right_taltal, HOOKSHOT).connect(below_right_taltal, FLIPPERS, one_way=True)
|
||||
|
||||
@@ -253,7 +253,6 @@ def isConsumable(item) -> bool:
|
||||
|
||||
class RequirementsSettings:
|
||||
def __init__(self, options):
|
||||
self.can_farm = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, BOOMERANG, BOMB, HOOKSHOT, BOW)
|
||||
self.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG, BOMB)
|
||||
self.pit_bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, BOOMERANG, BOMB) # unique
|
||||
self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG)
|
||||
|
||||
@@ -4,7 +4,6 @@ from ..roomEditor import RoomEditor
|
||||
from .. import entityData
|
||||
import os
|
||||
import bsdiff4
|
||||
import pkgutil
|
||||
|
||||
def imageTo2bpp(filename):
|
||||
import PIL.Image
|
||||
@@ -180,9 +179,24 @@ def noText(rom):
|
||||
|
||||
def reduceMessageLengths(rom, rnd):
|
||||
# Into text from Marin. Got to go fast, so less text. (This intro text is very long)
|
||||
lines = pkgutil.get_data(__name__, "marin.txt").decode("unicode_escape").splitlines()
|
||||
lines = [l for l in lines if l.strip()]
|
||||
rom.texts[0x01] = formatText(rnd.choice(lines).strip())
|
||||
rom.texts[0x01] = formatText(rnd.choice([
|
||||
"Let's a go!",
|
||||
"Remember, sword goes on A!",
|
||||
"Avoid the heart piece of shame!",
|
||||
"Marin? No, this is Zelda. Welcome to Hyrule",
|
||||
"Why are you in my bed?",
|
||||
"This is not a Mario game!",
|
||||
"MuffinJets was here...",
|
||||
"Remember, there are no bugs in LADX",
|
||||
"#####, #####, you got to wake up!\nDinner is ready.",
|
||||
"Go find the stepladder",
|
||||
"Pizza power!",
|
||||
"Eastmost penninsula is the secret",
|
||||
"There is no cow level",
|
||||
"You cannot lift rocks with your bear hands",
|
||||
"Thank you, daid!",
|
||||
"There, there now. Just relax. You've been asleep for almost nine hours now."
|
||||
]))
|
||||
|
||||
# Reduce length of a bunch of common texts
|
||||
rom.texts[0xEA] = formatText("You've got a Guardian Acorn!")
|
||||
|
||||
@@ -1,465 +0,0 @@
|
||||
Let's a go!
|
||||
Remember, sword goes on A!
|
||||
Remember, sword goes on B!
|
||||
It's pronounced Hydrocity Zone.
|
||||
Avoid the heart piece of shame!
|
||||
Marin? No, this is Zelda. Welcome to Hyrule
|
||||
Why are you in my bed?
|
||||
This is not a Mario game!
|
||||
Wait, I thought Daid was French!
|
||||
Is it spicefather or spaceotter?
|
||||
kbranch finally took a break!
|
||||
Baby seed ahead.
|
||||
Abandon all hope ye who enter here...
|
||||
Link... Open your eyes...\nWait, you're #####?
|
||||
Remember, there are no bugs in LADX.
|
||||
#####, #####, you got to wake up!\nDinner is ready.
|
||||
Go find the stepladder.
|
||||
Pizza power!
|
||||
Eastmost peninsula is the secret.
|
||||
There is no cow level.
|
||||
You cannot lift rocks with your bear hands.
|
||||
Don't worry, the doghouse was patched.
|
||||
The carpet whale isn't real, it can't hurt you.
|
||||
Isn't this a demake of Phantom Hourglass?
|
||||
Go try the LAS rando!
|
||||
Go try the Oracles rando!
|
||||
Go try Archipelago!
|
||||
Go try touching grass!
|
||||
Please leave my house.
|
||||
Trust me, this will be a 2 hour seed, max.
|
||||
This is still better than doing Dampe dungeons.
|
||||
They say that Marin can be found here.
|
||||
Stalfos are such boneheads.
|
||||
90 percent bug-free!
|
||||
404 Marin.personality not found.
|
||||
Idk man, works on my machine.
|
||||
Hey guys, did you know that Vaporeon
|
||||
Trans rights!
|
||||
Support gay rights!\nAnd their lefts!
|
||||
Snake? Snake?! SNAAAAKE!!!
|
||||
Oh, you chose THESE settings?
|
||||
As seen on TV!
|
||||
May contain nuts.
|
||||
Limited edition!
|
||||
May contain RNG.
|
||||
Reticulating splines!
|
||||
Keyboard compatible!
|
||||
Teetsuuuuoooo!
|
||||
Kaaneeeedaaaa!
|
||||
Learn about allyship!
|
||||
This Marin text left intentionally blank.
|
||||
'Autological' is!
|
||||
Technoblade never dies!
|
||||
Thank you, CrystalSaver!
|
||||
Wait, LADX has a rando?
|
||||
Wait, how many Pokemon are there now?
|
||||
GOOD EMU
|
||||
Good luck finding the feather.
|
||||
Good luck finding the bracelets.
|
||||
Good luck finding the boots.
|
||||
Good luck finding your swords.
|
||||
Good luck finding the flippers.
|
||||
Good luck finding the rooster.
|
||||
Good luck finding the hookshot.
|
||||
Good luck finding the magic rod.
|
||||
It's not a fire rod.\nIt's a magic rod, it shoots magic.
|
||||
You should check the Seashell Mansion.
|
||||
Mt. Tamaranch
|
||||
WIND FISH IN NAME ONLY, FOR IT IS NEITHER.
|
||||
Stuck? Try Magpie!
|
||||
Ribbit! Ribbit! I'm Marin, on vocals!
|
||||
Try this rando at ladxr.daid.eu!
|
||||
He turned himself into a carpet whale!
|
||||
Which came first, the whale or the egg?
|
||||
Glan - Known Death and Taxes appreciator.
|
||||
Pokemon number 591.
|
||||
Would you?
|
||||
Sprinkle the desert skulls.
|
||||
Please don't curse in my Christian LADXR seed.
|
||||
... ... ... \n... ...smash.
|
||||
How was bedwetting practice?
|
||||
The Oracles decomp project is going well!
|
||||
#####, how do I download RAM?
|
||||
Is this a delayed April Fool's Joke?
|
||||
Play as if your footage will go in a\nSummoning Salt video.
|
||||
I hope you prepared for our date later.
|
||||
Isn't this the game where you date a seagull?
|
||||
You look pretty good for a guy who probably drowned.
|
||||
Remember, we race on Sundays.
|
||||
This randomizer was made possible by players like you. \n \n Thank you!
|
||||
Now with real fake doors!
|
||||
Now with real fake floors!
|
||||
You could be doing something productive right now.
|
||||
No eggs were harmed in the making of this game.
|
||||
I'm helping the goat, \ncatfishing Mr. Write is kinda the goal.
|
||||
There are actually two LADX randomizers.
|
||||
You're not gonna cheat... \n ...right?
|
||||
Mamu's singing is so bad it wakes the dead.
|
||||
Don't forget the Richard picture.
|
||||
Are you sure you wanna do this? I kinda like this island.
|
||||
SJ, BT, WW, OoB, HIJKLMNOP.
|
||||
5 dollars in the swear jar. Now.
|
||||
#####, I promise this seed will be better than the last one.
|
||||
Want your name here? Contribute to LADXR!
|
||||
Kappa
|
||||
HEY! \n \n LANGUAGE!
|
||||
I sell seashells on the seashore.
|
||||
Hey! Are you even listening to me?
|
||||
Your stay will total 10,000 rupees. I hope you have good insurance.
|
||||
I have like the biggest crush on you. Will you get the hints now?
|
||||
Daid watches Matty for ideas. \nBlame her if things go wrong.
|
||||
'All of you are to blame.' -Daid
|
||||
Batman Contingency Plan: Link. Step 1: Disguise yourself as a maiden to attract the young hero.
|
||||
I have flooded Koholint with a deadly neurotoxin.
|
||||
Ahh, General #####.
|
||||
Finally, Link's Awakening!
|
||||
Is the Wind Fish dreaming that he's sleeping in an egg? Or is he dreaming that he's you?
|
||||
Save Koholint. By destroying it. Huh? Don't ask me, I'm just a kid!
|
||||
There aren't enough women in this village to sustain a civilization.
|
||||
So does this game take place before or after Oracles?
|
||||
Have you tried the critically acclaimed MMORPG FINAL FANTASY XIV that has a free trial up to level 60 including the Heavensward expansion?
|
||||
The thumbs-up sign had been used by the Galactic Federation for ages. Me, I was known for giving the thumbs-down during briefing. I had my reasons, though... Commander Adam Malkovich was normally cool and not one to joke around, but he would end all of his mission briefings by saying, 'Any objections, Lady?'
|
||||
Hot hippos are near your location!
|
||||
#####, get up! It's my turn in the bed! Tarin's smells too much...
|
||||
Have you ever had a dream\nthat\nyo wa-\nyo had\nyo\nthat\nthat you could do anything?
|
||||
Next time, try a salad.
|
||||
seagull noises
|
||||
I'm telling you, YOU HAVE UNO, it came free with your Xbox!
|
||||
I'm telling you, YOU HAVE TRENDY, it came free with your Mabe!
|
||||
LADXR - Now with even more Marin quotes!
|
||||
You guys are spending more time adding Marin quotes than actually playing the game.
|
||||
NASA faked the moon.
|
||||
Doh, I missed!
|
||||
Beginning the seed in... 100\n99\n98\n97\n96\n...\nJust Kidding.
|
||||
Consider libre software!
|
||||
Consider a GNU/Linux installation!
|
||||
Now you're gonna tell me about how you need to get some instruments or maybe shells to hatch a whale out of an egg, right? All you boys are the same...
|
||||
Oh hey #####! I made pancakes!
|
||||
Oh hey #####! I made breakfast!
|
||||
Alright Tarin, test subject number 142857 was a failure, give him the item and the memory drug and we'll try next time.
|
||||
Betcha 100 rupees that Tarin gives you a sword.
|
||||
Betcha 100 rupees that Tarin gives you the feather.
|
||||
Betcha 100 rupees that Tarin gives you a bracelet.
|
||||
Betcha 100 rupees that Tarin gives you the boots.
|
||||
Betcha 100 rupees that Tarin gives you the hookshot.
|
||||
Betcha 100 rupees that Tarin gives you the rod.
|
||||
You'd think that Madam MeowMeow would be a cat person.
|
||||
Look at you, with them dry lips.
|
||||
You are now manually breathing. Hope that doesn't throw you off for this race.
|
||||
Lemme get a number nine, a number nine large, a number six, with extra dip...
|
||||
Tarin, the red-nosed deadbeat \nHad a mushroom addiction!
|
||||
I'm using tilt controls!
|
||||
SPLASH! \n \n \n ...Wait, you meant something else by 'splash text'?
|
||||
CRACKLE-FWOOSH!
|
||||
'Logic' is a strong word.
|
||||
They say that the go-to way for fixing things is just to add another one of me.
|
||||
gl hf
|
||||
Have you considered multi-classing as a THIEF?
|
||||
Don't call me Shirley
|
||||
WHY are you buying CLOTHES at the SOUP STORE?
|
||||
Believe it or not, this won't be the last time Link gets stranded on an island.
|
||||
Is this the real life? Or is this just fantasy?
|
||||
To the owner of the white sedan, your lights are on.
|
||||
Now remade, in beautiful SD 2D!
|
||||
Animal Village in my seed \nMarin and rabbits, loop de loop.
|
||||
You seem totally entranced in Marin's appearance.
|
||||
House hippoes are very timid creatures and are rarely seen, but they will defend their territory if provoked.
|
||||
New goal! Close this seed, open the LADXR source code, and find the typo.
|
||||
All your base are belong to us
|
||||
Really? Another seed?
|
||||
This seed brought to you by: the corners in the D2 boss room.
|
||||
Hey, THIEF! Oh wait, you haven't done anything wrong... yet.
|
||||
Hello World
|
||||
With these hands, I give you life!
|
||||
I heard we're a subcommunity of FFR now.
|
||||
Try the Final Fantasy Randomizer!
|
||||
How soon should we start calling you THIEF?
|
||||
... Why do you keep doing this to yourself?
|
||||
YOUR AD HERE
|
||||
Did Matty give you this seed? Yeesh, good luck.
|
||||
Yoooo I looked ahead into the spoiler log for this one...\n...\n...\n...good luck.
|
||||
Lemme check the spoiler log...\nOkay, cool, only the normal amount of stupid.
|
||||
Oh, you're alive. Dang. Guess I won't be needing THIS anymore.
|
||||
Now you're gonna go talk to my dad. Gosh, boys are so predictable.
|
||||
Shoot, I WAS going to steal your kidneys while you were asleep. Guess I'll have to find a moment when you don't expect me.
|
||||
You caught me, mid-suavamente!
|
||||
You'll be the bedwetting champion in no time.
|
||||
Link, stop doing that, this is the fifth time this week I've had to change the sheets!
|
||||
You mind napping in Not My Bed next time?
|
||||
Why do they call it oven when you of in the cold food of out hot eat the food?
|
||||
Marin sayings will never be generated by AI. Our community really is just that unfunny.
|
||||
skibidi toilet\n...\nYes, that joke WILL age well
|
||||
WHO DARES AWAKEN ME FROM MY THOUSAND-YEAR SLUMBER
|
||||
The wind... it is... blowing...
|
||||
Have I ever told you how much I hate sand?
|
||||
explosion.gif
|
||||
It is pronounced LADXR, not LADXR.
|
||||
Stop pronouncing it lah-decks.
|
||||
Someone once suggested to add all the nag messages all at once for me.
|
||||
Accidentally playing Song 2? In front of the egg? It's more likely than you think.
|
||||
Ladies and gentlemen? We got him.
|
||||
Ladies and gentlemen? We got her.
|
||||
Ladies and gentlemen? We got 'em.
|
||||
What a wake up! I thought you'd never Marin! You were feeling a bit woozy and Zelda... What? Koholint? No, my name's relief! You must still be tossing. You are on turning Island!
|
||||
...Zelda? Oh Marin is it? My apologies, thank you for saving me. So I'm on Koholint Island? Wait, where's my sword and shield?!
|
||||
Koholint? More like kOWOlint.
|
||||
What? The Wind Fish will grant my wish literally? I forsee nothing wrong happening with this.
|
||||
Hey Marin! You woke me up from a fine nap! ... Thanks a lot! But now, I'll get my revenge! Are you ready?!
|
||||
Why bother coming up with a funny quote? You're just gonna mash through it anyway.
|
||||
something something whale something something dream something something adventure.
|
||||
Some people won't be able to see this message!
|
||||
If you're playing Archipelago and see this message, say hi to zig for me!
|
||||
I think it may be time to stop playing LADXR seeds.
|
||||
Rings do nothing unless worn!
|
||||
Thank you Link, but our Instruments are in another Dungeon.
|
||||
Are you sure you loaded the right seed?
|
||||
Is this even randomized?
|
||||
This seed brought to you by... Corners!
|
||||
To this day I still don't know if we inconvenienced the Mad Batter or not.
|
||||
Oh, hi #####
|
||||
People forgot I was playable in Hyrule Warriors
|
||||
Join our Discord. Or else.
|
||||
Also try Minecraft!
|
||||
I see you're finally awake...
|
||||
OwO
|
||||
This is Todd Howard, and today I'm pleased to announce... The Elder Scrolls V: Skyrim for the Nintendo Game Boy Color!
|
||||
Hey dummy! Need a hint? The power bracelet is... !! Whoops! There I go, talking too much again.
|
||||
Thank you for visiting Toronbo Shores featuring Mabe Village. Don't forget your complimentary gift on the way out.
|
||||
They say that sand can be found in Yarna Desert.
|
||||
I got to see a previously unreleased cut yesterday. It only cost me 200 rupees. What a deal!
|
||||
Just let him sleep
|
||||
LADXR is going to be renamed X now.
|
||||
Did you hear this chart-topping song yet? It's called Manbo's Mambo, it's so catchy! OH!
|
||||
YOU DARE BRING LIGHT INTO MY LAIR?!?! You must DIE!
|
||||
But enough talk! Have at you!
|
||||
Please input your age for optimal meme-text delivery.
|
||||
So the bear is just calling the walrus fat beecause he's projecting, right?
|
||||
Please help, #####! The Nightmare has shuffled all the items around!
|
||||
One does not simply Wake the Wind Fish.
|
||||
Nothing unusual here, just a completely normal LADX game, Mister Nintendo.
|
||||
Remember:\n1) Play Vanilla\n2) Play Solo Rando\n3) Play Multi
|
||||
Is :) a good item?
|
||||
What version do we have anyway? 0.6.9?
|
||||
So, what &newgames are coming in the next AP version?
|
||||
Is !remaining fixed yet?
|
||||
Remember the APocalypse. Never forget the rooms we lost that day.
|
||||
Have you heard of Berserker's Multiworld?
|
||||
MILF. Man I love Fangames.
|
||||
How big can the Big Async be anyway? A hundred worlds?
|
||||
Have you heard of the After Dark server?
|
||||
Try Adventure!
|
||||
Try Aquaria!
|
||||
Try Blasphemous!
|
||||
Try Bomb Rush Cyberfunk!
|
||||
Try Bumper Stickers!
|
||||
Try Castlevania 64!
|
||||
Try Celeste 64!
|
||||
Try ChecksFinder!
|
||||
Try Clique!
|
||||
Try Dark Souls III!
|
||||
Try DLCQuest!
|
||||
Try Donkey Kong Country 3!
|
||||
Try DOOM 1993!
|
||||
Try DOOM II!
|
||||
Try Factorio!
|
||||
Try Final Fantasy!
|
||||
Try Final Fantasy Mystic Quest!
|
||||
Try A Hat in Time!
|
||||
Try Heretic!
|
||||
Try Hollow Knight!
|
||||
Try Hylics 2!
|
||||
Try Kingdom Hearts 2!
|
||||
Try Kirby's Dream Land 3!
|
||||
Try Landstalker - The Treasures of King Nole!
|
||||
Try The Legend of Zelda!
|
||||
Try Lingo!
|
||||
Try A Link to the Past!
|
||||
Try Links Awakening DX!
|
||||
Try Lufia II Ancient Cave!
|
||||
Try Mario & Luigi Superstar Saga!
|
||||
Try MegaMan Battle Network 3!
|
||||
Try Meritous!
|
||||
Try The Messenger!
|
||||
Try Minecraft!
|
||||
Try Muse Dash!
|
||||
Try Noita!
|
||||
Try Ocarina of Time!
|
||||
Try Overcooked! 2!
|
||||
Try Pokemon Emerald!
|
||||
Try Pokemon Red and Blue!
|
||||
Try Raft!
|
||||
Try Risk of Rain 2!
|
||||
Try Rogue Legacy!
|
||||
Try Secret of Evermore!
|
||||
Try Shivers!
|
||||
Try A Short Hike!
|
||||
Try Slay the Spire!
|
||||
Try SMZ3!
|
||||
Try Sonic Adventure 2 Battle!
|
||||
Try Starcraft 2!
|
||||
Try Stardew Valley!
|
||||
Try Subnautica!
|
||||
Try Sudoku!
|
||||
Try Super Mario 64!
|
||||
Try Super Mario World!
|
||||
Try Super Metroid!
|
||||
Try Terraria!
|
||||
Try Timespinner!
|
||||
Try TUNIC!
|
||||
Try Undertale!
|
||||
Try VVVVVV!
|
||||
Try Wargroove!
|
||||
Try The Witness!
|
||||
Try Yoshi's Island!
|
||||
Try Yu-Gi-Oh! 2006!
|
||||
Try Zillion!
|
||||
Try Zork Grand Inquisitor!
|
||||
Try Old School Runescape!
|
||||
Try Kingdom Hearts!
|
||||
Try Mega Man 2!
|
||||
Try Yacht Dice!
|
||||
VVVVVVVVVVVVVV this should be enough V right?
|
||||
If you see this message, please open a #bug-report about it\n\n\nDon't actually though.
|
||||
This YAML is going in the bucket, isn't it?
|
||||
Oh, this is a terrible seed for a Sync
|
||||
Oh, this is a terrible seed for an Async
|
||||
What does BK stand for anyway?
|
||||
Check out the #future-game-design forum
|
||||
This is actually a Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward and Stormblood expansions up to level 70 with no restrictions on playtime!
|
||||
Is it April yet? Can I play ArchipIDLE again?
|
||||
https://archipelago.gg/datapackage
|
||||
Hello, Link! (Disregard message if your player sprite is not Link.)
|
||||
Go back to sleep, Outer Wilds isn't supported yet.
|
||||
:)\nWelcome back!
|
||||
Don't forget about Aginah!
|
||||
Remind your Undertale player not to warp before Mad Dummy.
|
||||
You need\n9 instruments\nor maybe not. I wouldn't know.
|
||||
Try !\n\nIt makes the game easier.
|
||||
Have you tried The Witness? If you're a fan of games about waking up on an unfamiliar island, give it a shot!
|
||||
Have you tried turning it off and on again?
|
||||
Its about time. Now go and check
|
||||
This dream is a lie. Or is it a cake?
|
||||
Don't live your dream. Dream your live.
|
||||
Only 5 more minutes. zzzZ
|
||||
Tell me, for whom do you fight?\nHmmph. How very glib. And do you believe in Koholint?
|
||||
I wonder when Undertale will be merged?\nOh wait it already has.
|
||||
Hit me up if you get stuck -\nwe could go to Burger King together.
|
||||
Post this message to delay Silksong.
|
||||
Sorry #####, but your princess is in another castle!
|
||||
You've been met with a terrible fate, haven't you?
|
||||
Hey!\nListen!\nHey! Hey!\nListen!
|
||||
I bet there's a progression item at the 980 Rupee shop check.
|
||||
Lamp oil? Rope? Bombs? You want it? It's yours, my friend. As long as you have enough rubies.
|
||||
One day I happened to be occupied with the subject of generation of waves by wind.
|
||||
(nuzzles you) uwu
|
||||
why do they call it links awakening when links awake and IN links asleep OUT the wind fish
|
||||
For many years I have been looking, searching for, but never finding, the builder of this house...
|
||||
What the heck is a Quatro?
|
||||
Have you tried The Binding of Isaac yet?
|
||||
Have you played Pong? \n I hear it's still popular nowadays.
|
||||
Five Nights at Freddy's... \n That's where I wanna be
|
||||
Setting Coinsanity to -1...
|
||||
Your Feather can be found at Mask-Shard_Grey_Mourner
|
||||
Your Sword can be found in Ganon's Tower
|
||||
Your Rooster can be found in HylemXylem
|
||||
Your Bracelet can be found at Giant Floor Puzzle
|
||||
Your Flippers can be found in Valley of Bowser
|
||||
Your Magic Rod can be found in Victory Road
|
||||
Your Hookshot can be found in Bowser in the Sky
|
||||
Have they added Among Us to AP yet?
|
||||
Every copy of LADX is personalized, David.
|
||||
Looks like you're going on A Short Hike. Bring back feathers please?
|
||||
Functioning Brain is at...\nWait. This isn't Witness. Wrong game, sorry.
|
||||
Don't forget to check your Clique!\nIf, y'know, you have one. No pressure...
|
||||
:3
|
||||
Sorry ######, but your progression item is in another world.
|
||||
&newgames\n&oldgames
|
||||
Do arrows come with turners? I'm stuck in my Bumper Stickers world.
|
||||
This seed has dexsanity enabled. Don't get stuck in Dewford!
|
||||
Please purchase the Dialogue Pack for DLC Quest: Link's Adventure to read the rest of this text.
|
||||
No hints here. Maybe ask BK Sudoku for some?
|
||||
KILNS (Yellow Middle, 5) \n REVELATION (White Low, 9)
|
||||
Push the button! When someone lets you...
|
||||
You won't believe the WEIRD thing Tarin found at the beach! Go on, ask him about it!
|
||||
When's door randomizer getting added to AP?
|
||||
Can you get my Morph Ball?
|
||||
Shoutouts to Simpleflips
|
||||
Remember, Sword goes on C!\n...you have a C button, right?
|
||||
Ask Berserker for your Progressive Power Bracelets!
|
||||
I will be taking your Burger King order now to save you some time when you inevitably need it.
|
||||
Welcome to KOHOLINT ISLAND.\nNo, we do not have a BURGER KING.
|
||||
Welcome to Burger King, may I take your order?
|
||||
Rise and shine, #####. Rise and shine.
|
||||
Well, this is\nLITTLEROOT TOWN.\nHow do you like it?
|
||||
My boy, this peace is what all true warriors strive for!
|
||||
#####, you can do it!\nSave the Princess...\nZelda is your... ... ...
|
||||
Dear Mario:\nPlease come to the castle, I've baked a cake for you. Yours truly--\nPrincess Toadstool\nPeach
|
||||
Grass-sanity mode activated. Have fun!
|
||||
Don't forget to bring rupees to the signpost maze this time.
|
||||
UP UP DOWN DOWN LEFT RIGHT LEFT RIGHT B A START
|
||||
Try LADX!\nWait a minute...
|
||||
ERROR! Unable to verify player. Please drink a verification can.
|
||||
We have been trying to reach you about your raft's extended warranty
|
||||
Are you ready for the easiest BK of your life?
|
||||
Hello, welcome to the world of Pokemon!\nMy name is Marin, and I'm--
|
||||
Alright, this is very important, I need you to listen to what I'm about to tell you--\nHey, wait, where are you going?!
|
||||
Cheques?\nSorry we don't accept cheques here
|
||||
Hi! \nMarin. \nWho...? \nHow...? \nWait... \nWhy??? \nSorry... \n...\nThanks. \nBye!
|
||||
AHHH WHY IS THERE SO MUCH GRASS? \nHOLY SH*T GRASS SNAKE AHHHH
|
||||
Could you buy some strawberries on your way home? \nHuh it's out of logic??? What??
|
||||
I heard you sleeptalking about skeletons and genocide... Your past must have been full of misery (mire)
|
||||
It's time to let go... \nIt wasn't your fault... \nYou couldn't have known your first check was going to be hardmode...
|
||||
They say that your progression is in another castle...
|
||||
A minute of silence for the failed generations due to the Fitness Gram Pacer test.
|
||||
Save an Ice Trap for me, please?
|
||||
maren
|
||||
ERROR DETECTED IN YAML\nOHKO MODE FORCED ON
|
||||
she awaken my link (extremely loud incorrect buzzer)
|
||||
Is deathlink on? If so, be careful!
|
||||
Sorry, but you're about to be BK'd.
|
||||
Did you set up cheesetracker yet?
|
||||
I've got a hint I need you to get...
|
||||
You aren't planning to destroy this island and kill everyone on it are you?
|
||||
Have you ever had a dream, that, that you um you had you'd you would you could you'd do you wi you wants you you could do so you you'd do you could you you want you want him to do you so much you could do anything?
|
||||
R R R U L L U L U R U R D R D R U U
|
||||
I'm not sure how, but I am pretty sure this is Phar's fault.
|
||||
Oh, look at that. Link's Awakened.\nYou did it, you beat the game.
|
||||
Excellent armaments, #####. Please return - \nCOVERED IN BLOOD -\n...safe and sound.
|
||||
Pray return to the Link's Awakening Sands.
|
||||
This Marin dialogue was inspired by The Witness's audiologs.
|
||||
You're awake!\n....\nYou were warned.\nI'm now going to say every word beginning with Z!\nZA\nZABAGLIONE\nZABAGLIONES\nZABAIONE\nZABAIONES\nZABAJONE\nZABAJONES\nZABETA\nZABETAS\nZABRA\nZABRAS\nZABTIEH\nZABTIEHS\nZACATON\nZACATONS\nZACK\nZACKS\nZADDICK\nZADDIK\nZADDIKIM\nZADDIKS\nZAFFAR\nzAFFARS\nZAFFER\nZAFFERS\nZAFFIR\n....\n....\n....\nI'll let you off easy.\nThis time.
|
||||
Leave me alone, I'm Marinating.
|
||||
praise be to the tungsten cube
|
||||
If you play multiple seeds in a row, you can pretend that each run is the dream you awaken from in the next.
|
||||
If this is a competitive race,\n\nyour time has already started.
|
||||
If anything goes wrong, remember.\n Blame Phar.
|
||||
Better hope your Hookshot didn't land on the Sick Kid.
|
||||
One time, I accidentally said Konoliht instead of Koholint...
|
||||
Sometimes, you must become best girl yourself...
|
||||
You just woke up! My name's #####!\nYou must be Marin, right?
|
||||
I just had the strangest dream, I was a seagull!\nI sung many songs for everybody to hear!\nHave you ever had a strange dream before?
|
||||
If you think about it, Koholint sounds suspiciously similar to Coherent...
|
||||
All I kin remember is biting into a juicy toadstool. Then I had the strangest dream... I was a Marin! Yeah, it sounds strange, but it sure was fun!
|
||||
Prepare for a 100% run!
|
||||
Prediction: 1 hour
|
||||
Prediction: 4 hours
|
||||
Prediction: 6 hours
|
||||
Prediction: 12 hours
|
||||
Prediction: Impossible seed
|
||||
Oak's parcel has arrived.
|
||||
Don't forget to like and subscribe!
|
||||
Don't BK, eat healthy!
|
||||
No omega symbols broke this seed gen? Good!
|
||||
#####...\nYou're lucky.\nLooks like my summer vacation is...\nover.
|
||||
Are you ready to send nukes to someone's Factorio game?
|
||||
You're late... Is this a Cmario game?
|
||||
At least you don't have to fight Ganon... What?
|
||||
PRAISE THE SUN!
|
||||
I'd recommend more sleep before heading out there.
|
||||
You Must Construct Additional Pylons
|
||||
#####, you lazy bum. I knew that I'd find you snoozing down here.
|
||||
This is it, #####.\nJust breathe.\nWhy are you so nervous?
|
||||
Hey, you. You're finally awake.\nYou were trying to cross the border, huh?
|
||||
Hey, you. You're finally awake.\nYou were trying to leave the island, huh?\nSwam straight into that whirlpool, same as us, and that thief over there.
|
||||
Is my Triforce locked behind your Wind Fish?
|
||||
@@ -110,6 +110,15 @@ class LinksAwakeningLocation(Location):
|
||||
add_item_rule(self, filter_item)
|
||||
|
||||
|
||||
def has_free_weapon(state: CollectionState, player: int) -> bool:
|
||||
return state.has("Progressive Sword", player) or state.has("Magic Rod", player) or state.has("Boomerang", player) or state.has("Hookshot", player)
|
||||
|
||||
|
||||
# If the player has access to farm enough rupees to afford a game, we assume that they can keep beating the game
|
||||
def can_farm_rupees(state: CollectionState, player: int) -> bool:
|
||||
return has_free_weapon(state, player) and (state.has("Can Play Trendy Game", player=player) or state.has("RAFT", player=player))
|
||||
|
||||
|
||||
class LinksAwakeningRegion(Region):
|
||||
dungeon_index = None
|
||||
ladxr_region = None
|
||||
@@ -145,7 +154,9 @@ class GameStateAdapater:
|
||||
def get(self, item, default):
|
||||
# Don't allow any money usage if you can't get back wasted rupees
|
||||
if item == "RUPEES":
|
||||
return self.state.prog_items[self.player]["RUPEES"]
|
||||
if can_farm_rupees(self.state, self.player):
|
||||
return self.state.prog_items[self.player]["RUPEES"]
|
||||
return 0
|
||||
elif item.endswith("_USED"):
|
||||
return 0
|
||||
else:
|
||||
|
||||
@@ -527,20 +527,6 @@ class InGameHints(DefaultOnToggle):
|
||||
display_name = "In-game Hints"
|
||||
|
||||
|
||||
class TarinsGift(Choice):
|
||||
"""
|
||||
[Local Progression] Forces Tarin's gift to be an item that immediately opens up local checks.
|
||||
Has little effect in single player games, and isn't always necessary with randomized entrances.
|
||||
[Bush Breaker] Forces Tarin's gift to be an item that can destroy bushes.
|
||||
[Any Item] Tarin's gift can be any item for any world
|
||||
"""
|
||||
display_name = "Tarin's Gift"
|
||||
option_local_progression = 0
|
||||
option_bush_breaker = 1
|
||||
option_any_item = 2
|
||||
default = option_local_progression
|
||||
|
||||
|
||||
class StabilizeItemPool(DefaultOffToggle):
|
||||
"""
|
||||
By default, rupees in the item pool may be randomly swapped with bombs, arrows, powders, or capacity upgrades. This option disables that swapping, which is useful for plando.
|
||||
@@ -579,7 +565,6 @@ ladx_option_groups = [
|
||||
OptionGroup("Miscellaneous", [
|
||||
TradeQuest,
|
||||
Rooster,
|
||||
TarinsGift,
|
||||
Overworld,
|
||||
TrendyGame,
|
||||
InGameHints,
|
||||
@@ -653,7 +638,6 @@ class LinksAwakeningOptions(PerGameCommonOptions):
|
||||
text_mode: TextMode
|
||||
no_flash: NoFlash
|
||||
in_game_hints: InGameHints
|
||||
tarins_gift: TarinsGift
|
||||
overworld: Overworld
|
||||
stabilize_item_pool: StabilizeItemPool
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import typing
|
||||
|
||||
from worlds.ladx.GpsTracker import GpsTracker
|
||||
from .LADXR.checkMetadata import checkMetadataTable
|
||||
import json
|
||||
import logging
|
||||
@@ -13,14 +10,13 @@ logger = logging.getLogger("Tracker")
|
||||
# kbranch you're a hero
|
||||
# https://github.com/kbranch/Magpie/blob/master/autotracking/checks.py
|
||||
class Check:
|
||||
def __init__(self, id, address, mask, alternateAddress=None, linkedItem=None):
|
||||
def __init__(self, id, address, mask, alternateAddress=None):
|
||||
self.id = id
|
||||
self.address = address
|
||||
self.alternateAddress = alternateAddress
|
||||
self.mask = mask
|
||||
self.value = None
|
||||
self.diff = 0
|
||||
self.linkedItem = linkedItem
|
||||
|
||||
def set(self, bytes):
|
||||
oldValue = self.value
|
||||
@@ -90,27 +86,6 @@ class LocationTracker:
|
||||
|
||||
blacklist = {'None', '0x2A1-2'}
|
||||
|
||||
def seashellCondition(slot_data):
|
||||
return 'goal' not in slot_data or slot_data['goal'] != 'seashells'
|
||||
|
||||
linkedCheckItems = {
|
||||
'0x2E9': {'item': 'SEASHELL', 'qty': 20, 'condition': seashellCondition},
|
||||
'0x2A2': {'item': 'TOADSTOOL', 'qty': 1},
|
||||
'0x2A6-Trade': {'item': 'TRADING_ITEM_YOSHI_DOLL', 'qty': 1},
|
||||
'0x2B2-Trade': {'item': 'TRADING_ITEM_RIBBON', 'qty': 1},
|
||||
'0x2FE-Trade': {'item': 'TRADING_ITEM_DOG_FOOD', 'qty': 1},
|
||||
'0x07B-Trade': {'item': 'TRADING_ITEM_BANANAS', 'qty': 1},
|
||||
'0x087-Trade': {'item': 'TRADING_ITEM_STICK', 'qty': 1},
|
||||
'0x2D7-Trade': {'item': 'TRADING_ITEM_HONEYCOMB', 'qty': 1},
|
||||
'0x019-Trade': {'item': 'TRADING_ITEM_PINEAPPLE', 'qty': 1},
|
||||
'0x2D9-Trade': {'item': 'TRADING_ITEM_HIBISCUS', 'qty': 1},
|
||||
'0x2A8-Trade': {'item': 'TRADING_ITEM_LETTER', 'qty': 1},
|
||||
'0x0CD-Trade': {'item': 'TRADING_ITEM_BROOM', 'qty': 1},
|
||||
'0x2F5-Trade': {'item': 'TRADING_ITEM_FISHING_HOOK', 'qty': 1},
|
||||
'0x0C9-Trade': {'item': 'TRADING_ITEM_NECKLACE', 'qty': 1},
|
||||
'0x297-Trade': {'item': 'TRADING_ITEM_SCALE', 'qty': 1},
|
||||
}
|
||||
|
||||
# in no dungeons boss shuffle, the d3 boss in d7 set 0x20 in fascade's room (0x1BC)
|
||||
# after beating evil eagile in D6, 0x1BC is now 0xAC (other things may have happened in between)
|
||||
# entered d3, slime eye flag had already been set (0x15A 0x20). after killing angler fish, bits 0x0C were set
|
||||
@@ -123,8 +98,6 @@ class LocationTracker:
|
||||
address = addressOverrides[check_id] if check_id in addressOverrides else 0xD800 + int(
|
||||
room, 16)
|
||||
|
||||
linkedItem = linkedCheckItems[check_id] if check_id in linkedCheckItems else None
|
||||
|
||||
if 'Trade' in check_id or 'Owl' in check_id:
|
||||
mask = 0x20
|
||||
|
||||
@@ -138,19 +111,13 @@ class LocationTracker:
|
||||
highest_check = max(
|
||||
highest_check, alternateAddresses[check_id])
|
||||
|
||||
check = Check(
|
||||
check_id,
|
||||
address,
|
||||
mask,
|
||||
(alternateAddresses[check_id] if check_id in alternateAddresses else None),
|
||||
linkedItem,
|
||||
)
|
||||
|
||||
check = Check(check_id, address, mask,
|
||||
alternateAddresses[check_id] if check_id in alternateAddresses else None)
|
||||
if check_id == '0x2A3':
|
||||
self.start_check = check
|
||||
self.all_checks.append(check)
|
||||
self.remaining_checks = [check for check in self.all_checks]
|
||||
self.gameboy.set_checks_range(
|
||||
self.gameboy.set_cache_limits(
|
||||
lowest_check, highest_check - lowest_check + 1)
|
||||
|
||||
def has_start_item(self):
|
||||
@@ -180,17 +147,10 @@ class MagpieBridge:
|
||||
server = None
|
||||
checks = None
|
||||
item_tracker = None
|
||||
gps_tracker: GpsTracker = None
|
||||
ws = None
|
||||
features = []
|
||||
slot_data = {}
|
||||
|
||||
def use_entrance_tracker(self):
|
||||
return "entrances" in self.features \
|
||||
and self.slot_data \
|
||||
and "entrance_mapping" in self.slot_data \
|
||||
and any([k != v for k, v in self.slot_data["entrance_mapping"].items()])
|
||||
|
||||
async def handler(self, websocket):
|
||||
self.ws = websocket
|
||||
while True:
|
||||
@@ -199,18 +159,14 @@ class MagpieBridge:
|
||||
logger.info(
|
||||
f"Connected, supported features: {message['features']}")
|
||||
self.features = message["features"]
|
||||
|
||||
await self.send_handshAck()
|
||||
|
||||
if message["type"] == "sendFull":
|
||||
if message["type"] in ("handshake", "sendFull"):
|
||||
if "items" in self.features:
|
||||
await self.send_all_inventory()
|
||||
if "checks" in self.features:
|
||||
await self.send_all_checks()
|
||||
if "slot_data" in self.features and self.slot_data:
|
||||
if "slot_data" in self.features:
|
||||
await self.send_slot_data(self.slot_data)
|
||||
if self.use_entrance_tracker():
|
||||
await self.send_gps(diff=False)
|
||||
|
||||
# Translate renamed IDs back to LADXR IDs
|
||||
@staticmethod
|
||||
@@ -220,18 +176,6 @@ class MagpieBridge:
|
||||
if the_id == "0x2A7":
|
||||
return "0x2A1-1"
|
||||
return the_id
|
||||
|
||||
async def send_handshAck(self):
|
||||
if not self.ws:
|
||||
return
|
||||
|
||||
message = {
|
||||
"type": "handshAck",
|
||||
"version": "1.32",
|
||||
"name": "archipelago-ladx-client",
|
||||
}
|
||||
|
||||
await self.ws.send(json.dumps(message))
|
||||
|
||||
async def send_all_checks(self):
|
||||
while self.checks == None:
|
||||
@@ -241,6 +185,7 @@ class MagpieBridge:
|
||||
message = {
|
||||
"type": "check",
|
||||
"refresh": True,
|
||||
"version": "1.0",
|
||||
"diff": False,
|
||||
"checks": [{"id": self.fixup_id(check.id), "checked": check.value} for check in self.checks]
|
||||
}
|
||||
@@ -255,6 +200,7 @@ class MagpieBridge:
|
||||
message = {
|
||||
"type": "check",
|
||||
"refresh": True,
|
||||
"version": "1.0",
|
||||
"diff": True,
|
||||
"checks": [{"id": self.fixup_id(check), "checked": True} for check in checks]
|
||||
}
|
||||
@@ -276,17 +222,10 @@ class MagpieBridge:
|
||||
return
|
||||
await self.item_tracker.sendItems(self.ws, diff=True)
|
||||
|
||||
async def send_gps(self, diff: bool=True) -> typing.Dict[str, str]:
|
||||
async def send_gps(self, gps):
|
||||
if not self.ws:
|
||||
return
|
||||
|
||||
await self.gps_tracker.send_location(self.ws)
|
||||
|
||||
if self.use_entrance_tracker():
|
||||
if self.slot_data and self.gps_tracker.needs_slot_data:
|
||||
self.gps_tracker.load_slot_data(self.slot_data)
|
||||
|
||||
return await self.gps_tracker.send_entrances(self.ws, diff)
|
||||
await gps.send_location(self.ws)
|
||||
|
||||
async def send_slot_data(self, slot_data):
|
||||
if not self.ws:
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
class EntranceCoord:
|
||||
name: str
|
||||
room: int
|
||||
x: int
|
||||
y: int
|
||||
|
||||
def __init__(self, name: str, room: int, x: int, y: int):
|
||||
self.name = name
|
||||
self.room = room
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __repr__(self):
|
||||
return EntranceCoord.coordString(self.room, self.x, self.y)
|
||||
|
||||
def coordString(room: int, x: int, y: int):
|
||||
return f"{room:#05x}, {x}, {y}"
|
||||
|
||||
storage_key = "found_entrances"
|
||||
|
||||
room = 0xFFF6
|
||||
map_id = 0xFFF7
|
||||
indoor_flag = 0xDBA5
|
||||
spawn_map = 0xDB60
|
||||
spawn_room = 0xDB61
|
||||
spawn_x = 0xDB62
|
||||
spawn_y = 0xDB63
|
||||
entrance_room_offset = 0xD800
|
||||
transition_state = 0xC124
|
||||
transition_target_x = 0xC12C
|
||||
transition_target_y = 0xC12D
|
||||
transition_scroll_x = 0xFF96
|
||||
transition_scroll_y = 0xFF97
|
||||
link_motion_state = 0xC11C
|
||||
transition_sequence = 0xC16B
|
||||
screen_coord = 0xFFFA
|
||||
|
||||
entrance_address_overrides = {
|
||||
0x312: 0xDDF2,
|
||||
}
|
||||
|
||||
map_map = {
|
||||
0x00: 0x01,
|
||||
0x01: 0x01,
|
||||
0x02: 0x01,
|
||||
0x03: 0x01,
|
||||
0x04: 0x01,
|
||||
0x05: 0x01,
|
||||
0x06: 0x02,
|
||||
0x07: 0x02,
|
||||
0x08: 0x02,
|
||||
0x09: 0x02,
|
||||
0x0A: 0x02,
|
||||
0x0B: 0x02,
|
||||
0x0C: 0x02,
|
||||
0x0D: 0x02,
|
||||
0x0E: 0x02,
|
||||
0x0F: 0x02,
|
||||
0x10: 0x02,
|
||||
0x11: 0x02,
|
||||
0x12: 0x02,
|
||||
0x13: 0x02,
|
||||
0x14: 0x02,
|
||||
0x15: 0x02,
|
||||
0x16: 0x02,
|
||||
0x17: 0x02,
|
||||
0x18: 0x02,
|
||||
0x19: 0x02,
|
||||
0x1D: 0x01,
|
||||
0x1E: 0x01,
|
||||
0x1F: 0x01,
|
||||
0xFF: 0x03,
|
||||
}
|
||||
|
||||
sidescroller_rooms = {
|
||||
0x2e9: "seashell_mansion:inside",
|
||||
0x08a: "seashell_mansion",
|
||||
0x2fd: "mambo:inside",
|
||||
0x02a: "mambo",
|
||||
0x1eb: "castle_secret_exit:inside",
|
||||
0x049: "castle_secret_exit",
|
||||
0x1ec: "castle_secret_entrance:inside",
|
||||
0x04a: "castle_secret_entrance",
|
||||
0x117: "d1:inside", # not a sidescroller, but acts weird
|
||||
}
|
||||
|
||||
entrance_coords = [
|
||||
EntranceCoord("writes_house:inside", 0x2a8, 80, 124),
|
||||
EntranceCoord("rooster_grave", 0x92, 88, 82),
|
||||
EntranceCoord("start_house:inside", 0x2a3, 80, 124),
|
||||
EntranceCoord("dream_hut", 0x83, 40, 66),
|
||||
EntranceCoord("papahl_house_right:inside", 0x2a6, 80, 124),
|
||||
EntranceCoord("papahl_house_right", 0x82, 120, 82),
|
||||
EntranceCoord("papahl_house_left:inside", 0x2a5, 80, 124),
|
||||
EntranceCoord("papahl_house_left", 0x82, 88, 82),
|
||||
EntranceCoord("d2:inside", 0x136, 80, 124),
|
||||
EntranceCoord("shop", 0x93, 72, 98),
|
||||
EntranceCoord("armos_maze_cave:inside", 0x2fc, 104, 96),
|
||||
EntranceCoord("start_house", 0xa2, 88, 82),
|
||||
EntranceCoord("animal_house3:inside", 0x2d9, 80, 124),
|
||||
EntranceCoord("trendy_shop", 0xb3, 88, 82),
|
||||
EntranceCoord("mabe_phone:inside", 0x2cb, 80, 124),
|
||||
EntranceCoord("mabe_phone", 0xb2, 88, 82),
|
||||
EntranceCoord("ulrira:inside", 0x2a9, 80, 124),
|
||||
EntranceCoord("ulrira", 0xb1, 72, 98),
|
||||
EntranceCoord("moblin_cave:inside", 0x2f0, 80, 124),
|
||||
EntranceCoord("kennel", 0xa1, 88, 66),
|
||||
EntranceCoord("madambowwow:inside", 0x2a7, 80, 124),
|
||||
EntranceCoord("madambowwow", 0xa1, 56, 66),
|
||||
EntranceCoord("library:inside", 0x1fa, 80, 124),
|
||||
EntranceCoord("library", 0xb0, 56, 50),
|
||||
EntranceCoord("d5:inside", 0x1a1, 80, 124),
|
||||
EntranceCoord("d1", 0xd3, 104, 34),
|
||||
EntranceCoord("d1:inside", 0x117, 80, 124),
|
||||
EntranceCoord("d3:inside", 0x152, 80, 124),
|
||||
EntranceCoord("d3", 0xb5, 104, 32),
|
||||
EntranceCoord("banana_seller", 0xe3, 72, 48),
|
||||
EntranceCoord("armos_temple:inside", 0x28f, 80, 124),
|
||||
EntranceCoord("boomerang_cave", 0xf4, 24, 32),
|
||||
EntranceCoord("forest_madbatter:inside", 0x1e1, 136, 80),
|
||||
EntranceCoord("ghost_house", 0xf6, 88, 66),
|
||||
EntranceCoord("prairie_low_phone:inside", 0x29d, 80, 124),
|
||||
EntranceCoord("prairie_low_phone", 0xe8, 56, 98),
|
||||
EntranceCoord("prairie_madbatter_connector_entrance:inside", 0x1f6, 136, 112),
|
||||
EntranceCoord("prairie_madbatter_connector_entrance", 0xf9, 120, 80),
|
||||
EntranceCoord("prairie_madbatter_connector_exit", 0xe7, 104, 32),
|
||||
EntranceCoord("prairie_madbatter_connector_exit:inside", 0x1e5, 40, 48),
|
||||
EntranceCoord("ghost_house:inside", 0x1e3, 80, 124),
|
||||
EntranceCoord("prairie_madbatter", 0xe6, 72, 64),
|
||||
EntranceCoord("d4:inside", 0x17a, 80, 124),
|
||||
EntranceCoord("d5", 0xd9, 88, 64),
|
||||
EntranceCoord("prairie_right_cave_bottom:inside", 0x293, 48, 124),
|
||||
EntranceCoord("prairie_right_cave_bottom", 0xc8, 40, 80),
|
||||
EntranceCoord("prairie_right_cave_high", 0xb8, 88, 48),
|
||||
EntranceCoord("prairie_right_cave_high:inside", 0x295, 112, 124),
|
||||
EntranceCoord("prairie_right_cave_top", 0xb8, 120, 96),
|
||||
EntranceCoord("prairie_right_cave_top:inside", 0x292, 48, 124),
|
||||
EntranceCoord("prairie_to_animal_connector:inside", 0x2d0, 40, 64),
|
||||
EntranceCoord("prairie_to_animal_connector", 0xaa, 136, 64),
|
||||
EntranceCoord("animal_to_prairie_connector", 0xab, 120, 80),
|
||||
EntranceCoord("animal_to_prairie_connector:inside", 0x2d1, 120, 64),
|
||||
EntranceCoord("animal_phone:inside", 0x2e3, 80, 124),
|
||||
EntranceCoord("animal_phone", 0xdb, 120, 82),
|
||||
EntranceCoord("animal_house1:inside", 0x2db, 80, 124),
|
||||
EntranceCoord("animal_house1", 0xcc, 40, 80),
|
||||
EntranceCoord("animal_house2:inside", 0x2dd, 80, 124),
|
||||
EntranceCoord("animal_house2", 0xcc, 120, 80),
|
||||
EntranceCoord("hookshot_cave:inside", 0x2b3, 80, 124),
|
||||
EntranceCoord("animal_house3", 0xcd, 40, 80),
|
||||
EntranceCoord("animal_house4:inside", 0x2da, 80, 124),
|
||||
EntranceCoord("animal_house4", 0xcd, 88, 80),
|
||||
EntranceCoord("banana_seller:inside", 0x2fe, 80, 124),
|
||||
EntranceCoord("animal_house5", 0xdd, 88, 66),
|
||||
EntranceCoord("animal_cave:inside", 0x2f7, 96, 124),
|
||||
EntranceCoord("animal_cave", 0xcd, 136, 32),
|
||||
EntranceCoord("d6", 0x8c, 56, 64),
|
||||
EntranceCoord("madbatter_taltal:inside", 0x1e2, 136, 80),
|
||||
EntranceCoord("desert_cave", 0xcf, 88, 16),
|
||||
EntranceCoord("dream_hut:inside", 0x2aa, 80, 124),
|
||||
EntranceCoord("armos_maze_cave", 0xae, 72, 112),
|
||||
EntranceCoord("shop:inside", 0x2a1, 80, 124),
|
||||
EntranceCoord("armos_temple", 0xac, 88, 64),
|
||||
EntranceCoord("d6_connector_exit:inside", 0x1f0, 56, 16),
|
||||
EntranceCoord("d6_connector_exit", 0x9c, 88, 16),
|
||||
EntranceCoord("desert_cave:inside", 0x1f9, 120, 96),
|
||||
EntranceCoord("d6_connector_entrance:inside", 0x1f1, 136, 96),
|
||||
EntranceCoord("d6_connector_entrance", 0x9d, 56, 48),
|
||||
EntranceCoord("armos_fairy:inside", 0x1ac, 80, 124),
|
||||
EntranceCoord("armos_fairy", 0x8d, 56, 32),
|
||||
EntranceCoord("raft_return_enter:inside", 0x1f7, 136, 96),
|
||||
EntranceCoord("raft_return_enter", 0x8f, 8, 32),
|
||||
EntranceCoord("raft_return_exit", 0x2f, 24, 112),
|
||||
EntranceCoord("raft_return_exit:inside", 0x1e7, 72, 16),
|
||||
EntranceCoord("raft_house:inside", 0x2b0, 80, 124),
|
||||
EntranceCoord("raft_house", 0x3f, 40, 34),
|
||||
EntranceCoord("heartpiece_swim_cave:inside", 0x1f2, 72, 124),
|
||||
EntranceCoord("heartpiece_swim_cave", 0x2e, 88, 32),
|
||||
EntranceCoord("rooster_grave:inside", 0x1f4, 88, 112),
|
||||
EntranceCoord("d4", 0x2b, 72, 34),
|
||||
EntranceCoord("castle_phone:inside", 0x2cc, 80, 124),
|
||||
EntranceCoord("castle_phone", 0x4b, 72, 34),
|
||||
EntranceCoord("castle_main_entrance:inside", 0x2d3, 80, 124),
|
||||
EntranceCoord("castle_main_entrance", 0x69, 88, 64),
|
||||
EntranceCoord("castle_upper_left", 0x59, 24, 48),
|
||||
EntranceCoord("castle_upper_left:inside", 0x2d5, 80, 124),
|
||||
EntranceCoord("witch:inside", 0x2a2, 80, 124),
|
||||
EntranceCoord("castle_upper_right", 0x59, 88, 64),
|
||||
EntranceCoord("prairie_left_cave2:inside", 0x2f4, 64, 124),
|
||||
EntranceCoord("castle_jump_cave", 0x78, 40, 112),
|
||||
EntranceCoord("prairie_left_cave1:inside", 0x2cd, 80, 124),
|
||||
EntranceCoord("seashell_mansion", 0x8a, 88, 64),
|
||||
EntranceCoord("prairie_right_phone:inside", 0x29c, 80, 124),
|
||||
EntranceCoord("prairie_right_phone", 0x88, 88, 82),
|
||||
EntranceCoord("prairie_left_fairy:inside", 0x1f3, 80, 124),
|
||||
EntranceCoord("prairie_left_fairy", 0x87, 40, 16),
|
||||
EntranceCoord("bird_cave:inside", 0x27e, 96, 124),
|
||||
EntranceCoord("prairie_left_cave2", 0x86, 24, 64),
|
||||
EntranceCoord("prairie_left_cave1", 0x84, 152, 98),
|
||||
EntranceCoord("prairie_left_phone:inside", 0x2b4, 80, 124),
|
||||
EntranceCoord("prairie_left_phone", 0xa4, 56, 66),
|
||||
EntranceCoord("mamu:inside", 0x2fb, 136, 112),
|
||||
EntranceCoord("mamu", 0xd4, 136, 48),
|
||||
EntranceCoord("richard_house:inside", 0x2c7, 80, 124),
|
||||
EntranceCoord("richard_house", 0xd6, 72, 80),
|
||||
EntranceCoord("richard_maze:inside", 0x2c9, 128, 124),
|
||||
EntranceCoord("richard_maze", 0xc6, 56, 80),
|
||||
EntranceCoord("graveyard_cave_left:inside", 0x2de, 56, 64),
|
||||
EntranceCoord("graveyard_cave_left", 0x75, 56, 64),
|
||||
EntranceCoord("graveyard_cave_right:inside", 0x2df, 56, 48),
|
||||
EntranceCoord("graveyard_cave_right", 0x76, 104, 80),
|
||||
EntranceCoord("trendy_shop:inside", 0x2a0, 80, 124),
|
||||
EntranceCoord("d0", 0x77, 120, 46),
|
||||
EntranceCoord("boomerang_cave:inside", 0x1f5, 72, 124),
|
||||
EntranceCoord("witch", 0x65, 72, 50),
|
||||
EntranceCoord("toadstool_entrance:inside", 0x2bd, 80, 124),
|
||||
EntranceCoord("toadstool_entrance", 0x62, 120, 66),
|
||||
EntranceCoord("toadstool_exit", 0x50, 136, 50),
|
||||
EntranceCoord("toadstool_exit:inside", 0x2ab, 80, 124),
|
||||
EntranceCoord("prairie_madbatter:inside", 0x1e0, 136, 112),
|
||||
EntranceCoord("hookshot_cave", 0x42, 56, 66),
|
||||
EntranceCoord("castle_upper_right:inside", 0x2d6, 80, 124),
|
||||
EntranceCoord("forest_madbatter", 0x52, 104, 48),
|
||||
EntranceCoord("writes_phone:inside", 0x29b, 80, 124),
|
||||
EntranceCoord("writes_phone", 0x31, 104, 82),
|
||||
EntranceCoord("d0:inside", 0x312, 80, 92),
|
||||
EntranceCoord("writes_house", 0x30, 120, 50),
|
||||
EntranceCoord("writes_cave_left:inside", 0x2ae, 80, 124),
|
||||
EntranceCoord("writes_cave_left", 0x20, 136, 50),
|
||||
EntranceCoord("writes_cave_right:inside", 0x2af, 80, 124),
|
||||
EntranceCoord("writes_cave_right", 0x21, 24, 50),
|
||||
EntranceCoord("d6:inside", 0x1d4, 80, 124),
|
||||
EntranceCoord("d2", 0x24, 56, 34),
|
||||
EntranceCoord("animal_house5:inside", 0x2d7, 80, 124),
|
||||
EntranceCoord("moblin_cave", 0x35, 104, 80),
|
||||
EntranceCoord("crazy_tracy:inside", 0x2ad, 80, 124),
|
||||
EntranceCoord("crazy_tracy", 0x45, 136, 66),
|
||||
EntranceCoord("photo_house:inside", 0x2b5, 80, 124),
|
||||
EntranceCoord("photo_house", 0x37, 72, 66),
|
||||
EntranceCoord("obstacle_cave_entrance:inside", 0x2b6, 80, 124),
|
||||
EntranceCoord("obstacle_cave_entrance", 0x17, 56, 50),
|
||||
EntranceCoord("left_to_right_taltalentrance:inside", 0x2ee, 120, 48),
|
||||
EntranceCoord("left_to_right_taltalentrance", 0x7, 56, 80),
|
||||
EntranceCoord("obstacle_cave_outside_chest:inside", 0x2bb, 80, 124),
|
||||
EntranceCoord("obstacle_cave_outside_chest", 0x18, 104, 18),
|
||||
EntranceCoord("obstacle_cave_exit:inside", 0x2bc, 48, 124),
|
||||
EntranceCoord("obstacle_cave_exit", 0x18, 136, 18),
|
||||
EntranceCoord("papahl_entrance:inside", 0x289, 64, 124),
|
||||
EntranceCoord("papahl_entrance", 0x19, 136, 64),
|
||||
EntranceCoord("papahl_exit:inside", 0x28b, 80, 124),
|
||||
EntranceCoord("papahl_exit", 0xa, 24, 112),
|
||||
EntranceCoord("rooster_house:inside", 0x29f, 80, 124),
|
||||
EntranceCoord("rooster_house", 0xa, 72, 34),
|
||||
EntranceCoord("d7:inside", 0x20e, 80, 124),
|
||||
EntranceCoord("bird_cave", 0xa, 120, 112),
|
||||
EntranceCoord("multichest_top:inside", 0x2f2, 80, 124),
|
||||
EntranceCoord("multichest_top", 0xd, 24, 112),
|
||||
EntranceCoord("multichest_left:inside", 0x2f9, 32, 124),
|
||||
EntranceCoord("multichest_left", 0x1d, 24, 48),
|
||||
EntranceCoord("multichest_right:inside", 0x2fa, 112, 124),
|
||||
EntranceCoord("multichest_right", 0x1d, 120, 80),
|
||||
EntranceCoord("right_taltal_connector1:inside", 0x280, 32, 124),
|
||||
EntranceCoord("right_taltal_connector1", 0x1e, 56, 16),
|
||||
EntranceCoord("right_taltal_connector3:inside", 0x283, 128, 124),
|
||||
EntranceCoord("right_taltal_connector3", 0x1e, 120, 16),
|
||||
EntranceCoord("right_taltal_connector2:inside", 0x282, 112, 124),
|
||||
EntranceCoord("right_taltal_connector2", 0x1f, 40, 16),
|
||||
EntranceCoord("right_fairy:inside", 0x1fb, 80, 124),
|
||||
EntranceCoord("right_fairy", 0x1f, 56, 80),
|
||||
EntranceCoord("right_taltal_connector4:inside", 0x287, 96, 124),
|
||||
EntranceCoord("right_taltal_connector4", 0x1f, 88, 64),
|
||||
EntranceCoord("right_taltal_connector5:inside", 0x28c, 96, 124),
|
||||
EntranceCoord("right_taltal_connector5", 0x1f, 120, 16),
|
||||
EntranceCoord("right_taltal_connector6:inside", 0x28e, 112, 124),
|
||||
EntranceCoord("right_taltal_connector6", 0xf, 72, 80),
|
||||
EntranceCoord("d7", 0x0e, 88, 48),
|
||||
EntranceCoord("left_taltal_entrance:inside", 0x2ea, 80, 124),
|
||||
EntranceCoord("left_taltal_entrance", 0x15, 136, 64),
|
||||
EntranceCoord("castle_jump_cave:inside", 0x1fd, 88, 80),
|
||||
EntranceCoord("madbatter_taltal", 0x4, 120, 112),
|
||||
EntranceCoord("fire_cave_exit:inside", 0x1ee, 24, 64),
|
||||
EntranceCoord("fire_cave_exit", 0x3, 72, 80),
|
||||
EntranceCoord("fire_cave_entrance:inside", 0x1fe, 112, 124),
|
||||
EntranceCoord("fire_cave_entrance", 0x13, 88, 16),
|
||||
EntranceCoord("phone_d8:inside", 0x299, 80, 124),
|
||||
EntranceCoord("phone_d8", 0x11, 104, 50),
|
||||
EntranceCoord("kennel:inside", 0x2b2, 80, 124),
|
||||
EntranceCoord("d8", 0x10, 88, 16),
|
||||
EntranceCoord("d8:inside", 0x25d, 80, 124),
|
||||
]
|
||||
|
||||
entrance_lookup = {str(coord): coord for coord in entrance_coords}
|
||||
@@ -4,7 +4,6 @@ import os
|
||||
import pkgutil
|
||||
import tempfile
|
||||
import typing
|
||||
import logging
|
||||
import re
|
||||
|
||||
import bsdiff4
|
||||
@@ -179,10 +178,10 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
assert(start)
|
||||
|
||||
menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld)
|
||||
menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld)
|
||||
menu_region.exits = [Entrance(self.player, "Start Game", menu_region)]
|
||||
menu_region.exits[0].connect(start)
|
||||
|
||||
|
||||
self.multiworld.regions.append(menu_region)
|
||||
|
||||
# Place RAFT, other access events
|
||||
@@ -190,14 +189,14 @@ class LinksAwakeningWorld(World):
|
||||
for loc in region.locations:
|
||||
if loc.address is None:
|
||||
loc.place_locked_item(self.create_event(loc.ladxr_item.event))
|
||||
|
||||
|
||||
# Connect Windfish -> Victory
|
||||
windfish = self.multiworld.get_region("Windfish", self.player)
|
||||
l = Location(self.player, "Windfish", parent=windfish)
|
||||
windfish.locations = [l]
|
||||
|
||||
|
||||
l.place_locked_item(self.create_event("An Alarm Clock"))
|
||||
|
||||
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("An Alarm Clock", player=self.player)
|
||||
|
||||
def create_item(self, item_name: str):
|
||||
@@ -207,8 +206,6 @@ class LinksAwakeningWorld(World):
|
||||
return Item(event, ItemClassification.progression, None, self.player)
|
||||
|
||||
def create_items(self) -> None:
|
||||
itempool = []
|
||||
|
||||
exclude = [item.name for item in self.multiworld.precollected_items[self.player]]
|
||||
|
||||
self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ]
|
||||
@@ -268,9 +265,9 @@ class LinksAwakeningWorld(World):
|
||||
self.prefill_own_dungeons.append(item)
|
||||
self.pre_fill_items.append(item)
|
||||
else:
|
||||
itempool.append(item)
|
||||
self.multiworld.itempool.append(item)
|
||||
else:
|
||||
itempool.append(item)
|
||||
self.multiworld.itempool.append(item)
|
||||
|
||||
self.multi_key = self.generate_multi_key()
|
||||
|
||||
@@ -279,8 +276,8 @@ class LinksAwakeningWorld(World):
|
||||
event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region)
|
||||
trendy_region.locations.insert(0, event_location)
|
||||
event_location.place_locked_item(self.create_event("Can Play Trendy Game"))
|
||||
|
||||
self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []]
|
||||
|
||||
self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []]
|
||||
for r in self.multiworld.get_regions(self.player):
|
||||
# Set aside dungeon locations
|
||||
if r.dungeon_index:
|
||||
@@ -293,52 +290,21 @@ class LinksAwakeningWorld(World):
|
||||
# Properly fill locations within dungeon
|
||||
location.dungeon = r.dungeon_index
|
||||
|
||||
if self.options.tarins_gift != "any_item":
|
||||
self.force_start_item(itempool)
|
||||
# For now, special case first item
|
||||
FORCE_START_ITEM = True
|
||||
if FORCE_START_ITEM:
|
||||
self.force_start_item()
|
||||
|
||||
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
def force_start_item(self, itempool):
|
||||
def force_start_item(self):
|
||||
start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player)
|
||||
if not start_loc.item:
|
||||
"""
|
||||
Find an item that forces progression or a bush breaker for the player, depending on settings.
|
||||
"""
|
||||
def is_possible_start_item(item):
|
||||
return item.advancement and item.name not in self.options.non_local_items
|
||||
|
||||
def opens_new_regions(item):
|
||||
collection_state = base_collection_state.copy()
|
||||
collection_state.collect(item)
|
||||
return len(collection_state.reachable_regions[self.player]) > reachable_count
|
||||
|
||||
start_items = [item for item in itempool if is_possible_start_item(item)]
|
||||
self.random.shuffle(start_items)
|
||||
|
||||
if self.options.tarins_gift == "bush_breaker":
|
||||
start_item = next((item for item in start_items if item.name in links_awakening_item_name_groups["Bush Breakers"]), None)
|
||||
|
||||
else: # local_progression
|
||||
entrance_mapping = self.ladxr_logic.world_setup.entrance_mapping
|
||||
# Tail key opens a region but not a location if d1 entrance is not mapped to d1 or d4
|
||||
# exclude it in these cases to avoid fill errors
|
||||
if entrance_mapping['d1'] not in ['d1', 'd4']:
|
||||
start_items = [item for item in start_items if item.name != 'Tail Key']
|
||||
# Exclude shovel unless starting in Mabe Village
|
||||
if entrance_mapping['start_house'] not in ['start_house', 'shop']:
|
||||
start_items = [item for item in start_items if item.name != 'Shovel']
|
||||
base_collection_state = CollectionState(self.multiworld)
|
||||
base_collection_state.update_reachable_regions(self.player)
|
||||
reachable_count = len(base_collection_state.reachable_regions[self.player])
|
||||
start_item = next((item for item in start_items if opens_new_regions(item)), None)
|
||||
|
||||
if start_item:
|
||||
itempool.remove(start_item)
|
||||
possible_start_items = [index for index, item in enumerate(self.multiworld.itempool)
|
||||
if item.player == self.player
|
||||
and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS and not item.location]
|
||||
if possible_start_items:
|
||||
index = self.random.choice(possible_start_items)
|
||||
start_item = self.multiworld.itempool.pop(index)
|
||||
start_loc.place_locked_item(start_item)
|
||||
else:
|
||||
logging.getLogger("Link's Awakening Logger").warning(f"No {self.options.tarins_gift.current_option_name} available for Tarin's Gift.")
|
||||
|
||||
|
||||
def get_pre_fill_items(self):
|
||||
return self.pre_fill_items
|
||||
@@ -351,7 +317,7 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
# set containing the list of all possible dungeon locations for the player
|
||||
all_dungeon_locs = set()
|
||||
|
||||
|
||||
# Do dungeon specific things
|
||||
for dungeon_index in range(0, 9):
|
||||
# set up allow-list for dungeon specific items
|
||||
@@ -364,7 +330,7 @@ class LinksAwakeningWorld(World):
|
||||
# ...also set the rules for the dungeon
|
||||
for location in locs:
|
||||
orig_rule = location.item_rule
|
||||
# If an item is about to be placed on a dungeon location, it can go there iff
|
||||
# If an item is about to be placed on a dungeon location, it can go there iff
|
||||
# 1. it fits the general rules for that location (probably 'return True' for most places)
|
||||
# 2. Either
|
||||
# 2a. it's not a restricted dungeon item
|
||||
@@ -416,7 +382,7 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
# Sweep to pick up already placed items that are reachable with everything but the dungeon items.
|
||||
partial_all_state.sweep_for_advancements()
|
||||
|
||||
|
||||
fill_restrictive(self.multiworld, partial_all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False)
|
||||
|
||||
|
||||
@@ -455,7 +421,7 @@ class LinksAwakeningWorld(World):
|
||||
for name in possibles:
|
||||
if name in self.name_cache:
|
||||
return self.name_cache[name]
|
||||
|
||||
|
||||
return "TRADING_ITEM_LETTER"
|
||||
|
||||
@classmethod
|
||||
@@ -470,7 +436,7 @@ class LinksAwakeningWorld(World):
|
||||
for loc in r.locations:
|
||||
if isinstance(loc, LinksAwakeningLocation):
|
||||
assert(loc.item)
|
||||
|
||||
|
||||
# If we're a links awakening item, just use the item
|
||||
if isinstance(loc.item, LinksAwakeningItem):
|
||||
loc.ladxr_item.item = loc.item.item_data.ladxr_id
|
||||
@@ -504,7 +470,7 @@ class LinksAwakeningWorld(World):
|
||||
args = parser.parse_args([rom_name, "-o", out_name, "--dump"])
|
||||
|
||||
rom = generator.generateRom(args, self)
|
||||
|
||||
|
||||
with open(out_path, "wb") as handle:
|
||||
rom.save(handle, name="LADXR")
|
||||
|
||||
@@ -512,7 +478,7 @@ class LinksAwakeningWorld(World):
|
||||
if self.options.ap_title_screen:
|
||||
with tempfile.NamedTemporaryFile(delete=False) as title_patch:
|
||||
title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
|
||||
|
||||
|
||||
bsdiff4.file_patch_inplace(out_path, title_patch.name)
|
||||
os.unlink(title_patch.name)
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ class MessengerWorld(World):
|
||||
f"({self.options.total_seals}). Adjusting to {total_seals}"
|
||||
)
|
||||
self.total_seals = total_seals
|
||||
self.required_seals = max(1, int(self.options.percent_seals_required.value / 100 * self.total_seals))
|
||||
self.required_seals = int(self.options.percent_seals_required.value / 100 * self.total_seals)
|
||||
|
||||
seals = [self.create_item("Power Seal") for _ in range(self.total_seals)]
|
||||
itempool += seals
|
||||
|
||||
@@ -26,7 +26,7 @@ class MessengerRules:
|
||||
maximum_price = (world.multiworld.get_location("The Shop - Demon's Bane", self.player).cost +
|
||||
world.multiworld.get_location("The Shop - Focused Power Sense", self.player).cost)
|
||||
self.maximum_price = min(maximum_price, world.total_shards)
|
||||
self.required_seals = world.required_seals
|
||||
self.required_seals = max(1, world.required_seals)
|
||||
|
||||
# dict of connection names and requirements to traverse the exit
|
||||
self.connection_rules = {
|
||||
@@ -34,7 +34,7 @@ class MessengerRules:
|
||||
"Artificer's Portal":
|
||||
lambda state: state.has_all({"Demon King Crown", "Magic Firefly"}, self.player),
|
||||
"Shrink Down":
|
||||
lambda state: state.has_all(NOTES, self.player),
|
||||
lambda state: state.has_all(NOTES, self.player) or self.has_enough_seals(state),
|
||||
# the shop
|
||||
"Money Sink":
|
||||
lambda state: state.has("Money Wrench", self.player) and self.can_shop(state),
|
||||
@@ -314,9 +314,6 @@ class MessengerRules:
|
||||
self.has_dart,
|
||||
}
|
||||
|
||||
if self.required_seals:
|
||||
self.connection_rules["Shrink Down"] = self.has_enough_seals
|
||||
|
||||
def has_wingsuit(self, state: CollectionState) -> bool:
|
||||
return state.has("Wingsuit", self.player)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from BaseClasses import CollectionState, ItemClassification
|
||||
from BaseClasses import ItemClassification, CollectionState
|
||||
from . import MessengerTestBase
|
||||
|
||||
|
||||
@@ -10,9 +10,8 @@ class AllSealsRequired(MessengerTestBase):
|
||||
def test_chest_access(self) -> None:
|
||||
"""Defaults to a total of 45 power seals in the pool and required."""
|
||||
with self.subTest("Access Dependency"):
|
||||
self.assertEqual(
|
||||
len([seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]),
|
||||
self.world.options.total_seals)
|
||||
self.assertEqual(len([seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]),
|
||||
self.world.options.total_seals)
|
||||
locations = ["Rescue Phantom"]
|
||||
items = [["Power Seal"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
@@ -94,22 +93,3 @@ class MaxSealsWithShards(MessengerTestBase):
|
||||
if seal.classification == ItemClassification.progression_skip_balancing]
|
||||
self.assertEqual(len(total_seals), 85)
|
||||
self.assertEqual(len(required_seals), 85)
|
||||
|
||||
|
||||
class NoSealsRequired(MessengerTestBase):
|
||||
options = {
|
||||
"goal": "power_seal_hunt",
|
||||
"total_seals": 1,
|
||||
"percent_seals_required": 10, # percentage
|
||||
}
|
||||
|
||||
def test_seals_amount(self) -> None:
|
||||
"""Should be 1 seal and it should be progression."""
|
||||
self.assertEqual(self.world.options.total_seals, 1)
|
||||
self.assertEqual(self.world.total_seals, 1)
|
||||
self.assertEqual(self.world.required_seals, 1)
|
||||
total_seals = [item for item in self.multiworld.itempool if item.name == "Power Seal"]
|
||||
required_seals = [item for item in self.multiworld.itempool if
|
||||
item.advancement and item.name == "Power Seal"]
|
||||
self.assertEqual(len(total_seals), 1)
|
||||
self.assertEqual(len(required_seals), 1)
|
||||
|
||||
@@ -269,7 +269,7 @@ class MLSSClient(BizHawkClient):
|
||||
self.local_checked_locations = locs_to_send
|
||||
|
||||
if locs_to_send is not None:
|
||||
await ctx.check_locations(locs_to_send)
|
||||
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": list(locs_to_send)}])
|
||||
|
||||
except bizhawk.RequestFailedError:
|
||||
# Exit handler and return to main loop to reconnect.
|
||||
|
||||
@@ -153,6 +153,7 @@ enemies = [
|
||||
0x50458C,
|
||||
0x5045AC,
|
||||
0x50468C,
|
||||
# 0x5046CC, 6 enemy formation
|
||||
0x5046EC,
|
||||
0x50470C
|
||||
]
|
||||
@@ -165,7 +166,6 @@ bosses = [
|
||||
0x50360C,
|
||||
0x5037AC,
|
||||
0x5037CC,
|
||||
0x50396C,
|
||||
0x503A8C,
|
||||
0x503D6C,
|
||||
0x503F0C,
|
||||
|
||||
@@ -160,7 +160,6 @@ itemList: typing.List[ItemData] = [
|
||||
ItemData(77771142, "Game Boy Horror SP", ItemClassification.useful, 0xFE),
|
||||
ItemData(77771143, "Woo Bean", ItemClassification.skip_balancing, 0x1C),
|
||||
ItemData(77771144, "Hee Bean", ItemClassification.skip_balancing, 0x1F),
|
||||
ItemData(77771145, "Beanstar Emblem", ItemClassification.progression, 0x3E),
|
||||
]
|
||||
|
||||
item_frequencies: typing.Dict[str, int] = {
|
||||
@@ -187,12 +186,5 @@ item_frequencies: typing.Dict[str, int] = {
|
||||
"Hammers": 3,
|
||||
}
|
||||
|
||||
mlss_item_name_groups = {
|
||||
"Beanstar Piece": { "Beanstar Piece 1", "Beanstar Piece 2", "Beanstar Piece 3", "Beanstar Piece 4"},
|
||||
"Beanfruit": { "Bean Fruit 1", "Bean Fruit 2", "Bean Fruit 3", "Bean Fruit 4", "Bean Fruit 5", "Bean Fruit 6", "Bean Fruit 7"},
|
||||
"Neon Egg": { "Blue Neon Egg", "Red Neon Egg", "Green Neon Egg", "Yellow Neon Egg", "Purple Neon Egg", "Orange Neon Egg", "Azure Neon Egg"},
|
||||
"Chuckola Fruit": { "Red Chuckola Fruit", "Purple Chuckola Fruit", "White Chuckola Fruit"}
|
||||
}
|
||||
|
||||
item_table: typing.Dict[str, ItemData] = {item.itemName: item for item in itemList}
|
||||
items_by_id: typing.Dict[int, ItemData] = {item.code: item for item in itemList}
|
||||
|
||||
@@ -251,9 +251,9 @@ coins: typing.List[LocationData] = [
|
||||
LocationData("Hoohoo Village North Cave Room 1 Coin Block", 0x39DAA0, 0),
|
||||
LocationData("Hoohoo Village South Cave Coin Block 1", 0x39DAC5, 0),
|
||||
LocationData("Hoohoo Village South Cave Coin Block 2", 0x39DAD5, 0),
|
||||
LocationData("Hoohoo Mountain Base Boostatue Cave Coin Block 1", 0x39DAE2, 0),
|
||||
LocationData("Hoohoo Mountain Base Boostatue Cave Coin Block 2", 0x39DAF2, 0),
|
||||
LocationData("Hoohoo Mountain Base Boostatue Cave Coin Block 3", 0x39DAFA, 0),
|
||||
LocationData("Hoohoo Mountain Base Boo Statue Cave Coin Block 1", 0x39DAE2, 0),
|
||||
LocationData("Hoohoo Mountain Base Boo Statue Cave Coin Block 2", 0x39DAF2, 0),
|
||||
LocationData("Hoohoo Mountain Base Boo Statue Cave Coin Block 3", 0x39DAFA, 0),
|
||||
LocationData("Beanbean Outskirts NW Coin Block", 0x39DB8F, 0),
|
||||
LocationData("Beanbean Outskirts S Room 1 Coin Block", 0x39DC18, 0),
|
||||
LocationData("Beanbean Outskirts S Room 2 Coin Block", 0x39DC3D, 0),
|
||||
@@ -262,8 +262,6 @@ coins: typing.List[LocationData] = [
|
||||
LocationData("Chucklehuck Woods Cave Room 1 Coin Block", 0x39DD7A, 0),
|
||||
LocationData("Chucklehuck Woods Cave Room 2 Coin Block", 0x39DD97, 0),
|
||||
LocationData("Chucklehuck Woods Cave Room 3 Coin Block", 0x39DDB4, 0),
|
||||
LocationData("Chucklehuck Woods Solo Luigi Cave Room 1 Coin Block 1", 0x39DB48, 0),
|
||||
LocationData("Chucklehuck Woods Solo Luigi Cave Room 1 Coin Block 2", 0x39DB50, 0),
|
||||
LocationData("Chucklehuck Woods Pipe 5 Room Coin Block", 0x39DDE6, 0),
|
||||
LocationData("Chucklehuck Woods Room 7 Coin Block", 0x39DE31, 0),
|
||||
LocationData("Chucklehuck Woods Past Chuckleroot Coin Block", 0x39DF14, 0),
|
||||
@@ -291,7 +289,6 @@ baseUltraRocks: typing.List[LocationData] = [
|
||||
LocationData("Teehee Valley Upper Maze Room 1 Block", 0x39E5E0, 0),
|
||||
LocationData("Teehee Valley Upper Maze Room 2 Digspot 1", 0x39E5C8, 0),
|
||||
LocationData("Teehee Valley Upper Maze Room 2 Digspot 2", 0x39E5D0, 0),
|
||||
LocationData("Guffawha Ruins Block", 0x39E6A3, 0),
|
||||
LocationData("Hoohoo Mountain Base Guffawha Ruins Entrance Digspot", 0x39DA0B, 0),
|
||||
LocationData("Hoohoo Mountain Base Teehee Valley Entrance Digspot", 0x39DA20, 0),
|
||||
LocationData("Hoohoo Mountain Base Teehee Valley Entrance Block", 0x39DA18, 0),
|
||||
@@ -301,7 +298,7 @@ booStatue: typing.List[LocationData] = [
|
||||
LocationData("Beanbean Outskirts Before Harhall Digspot 1", 0x39E951, 0),
|
||||
LocationData("Beanbean Outskirts Before Harhall Digspot 2", 0x39E959, 0),
|
||||
LocationData("Beanstar Piece Harhall", 0x1E9441, 2),
|
||||
LocationData("Beanbean Outskirts Boostatue Mole", 0x1E9434, 2),
|
||||
LocationData("Beanbean Outskirts Boo Statue Mole", 0x1E9434, 2),
|
||||
LocationData("Harhall's Pants", 0x1E9444, 2),
|
||||
LocationData("Beanbean Outskirts S Room 2 Digspot 1", 0x39DC65, 0),
|
||||
LocationData("Beanbean Outskirts S Room 2 Digspot 2", 0x39DC5D, 0),
|
||||
@@ -320,9 +317,6 @@ chucklehuck: typing.List[LocationData] = [
|
||||
LocationData("Chucklehuck Woods Cave Room 1 Block 2", 0x39DD8A, 0),
|
||||
LocationData("Chucklehuck Woods Cave Room 2 Block", 0x39DD9F, 0),
|
||||
LocationData("Chucklehuck Woods Cave Room 3 Block", 0x39DDAC, 0),
|
||||
LocationData("Chucklehuck Woods Solo Luigi Cave Room 2 Block", 0x39DB72, 0),
|
||||
LocationData("Chucklehuck Woods Solo Luigi Cave Room 3 Block 1", 0x39DB5D, 0),
|
||||
LocationData("Chucklehuck Woods Solo Luigi Cave Room 3 Block 2", 0x39DB65, 0),
|
||||
LocationData("Chucklehuck Woods Room 2 Block", 0x39DDC1, 0),
|
||||
LocationData("Chucklehuck Woods Room 2 Digspot", 0x39DDC9, 0),
|
||||
LocationData("Chucklehuck Woods Pipe Room Block 1", 0x39DDD6, 0),
|
||||
@@ -792,7 +786,7 @@ nonBlock = [
|
||||
(0x4373, 0x10, 0x277A45), # Teehee Valley Mole
|
||||
(0x434D, 0x8, 0x1E9444), # Harhall's Pants
|
||||
(0x432E, 0x10, 0x1E9441), # Harhall Beanstar Piece
|
||||
(0x434B, 0x8, 0x1E9434), # Outskirts Boostatue Mole
|
||||
(0x434B, 0x8, 0x1E9434), # Outskirts Boo Statue Mole
|
||||
(0x42FE, 0x2, 0x1E943E), # Red Goblet
|
||||
(0x42FE, 0x4, 0x24E628), # Green Goblet
|
||||
(0x4301, 0x10, 0x250621), # Red Chuckola Fruit
|
||||
|
||||
@@ -59,7 +59,7 @@ class LocationName:
|
||||
HoohooMountainBaseBoostatueRoomDigspot1 = "Hoohoo Mountain Base Boostatue Room Digspot 1"
|
||||
HoohooMountainBaseBoostatueRoomDigspot2 = "Hoohoo Mountain Base Boostatue Room Digspot 2"
|
||||
HoohooMountainBaseBoostatueRoomDigspot3 = "Hoohoo Mountain Base Boostatue Room Digspot 3"
|
||||
BeanbeanOutskirtsBooStatueMole = "Beanbean Outskirts Boostatue Mole"
|
||||
BeanbeanOutskirtsBooStatueMole = "Beanbean Outskirts Boo Statue Mole"
|
||||
HoohooMountainBaseGrassyAreaBlock1 = "Hoohoo Mountain Base Grassy Area Block 1"
|
||||
HoohooMountainBaseGrassyAreaBlock2 = "Hoohoo Mountain Base Grassy Area Block 2"
|
||||
HoohooMountainBaseGuffawhaRuinsEntranceDigspot = "Hoohoo Mountain Base Guffawha Ruins Entrance Digspot"
|
||||
@@ -533,9 +533,9 @@ class LocationName:
|
||||
BadgeShopMomPiranhaFlag2 = "Badge Shop Mom Piranha Flag 2"
|
||||
BadgeShopMomPiranhaFlag3 = "Badge Shop Mom Piranha Flag 3"
|
||||
HarhallsPants = "Harhall's Pants"
|
||||
HoohooMountainBaseBooStatueCaveCoinBlock1 = "Hoohoo Mountain Base Boostatue Cave Coin Block 1"
|
||||
HoohooMountainBaseBooStatueCaveCoinBlock2 = "Hoohoo Mountain Base Boostatue Cave Coin Block 2"
|
||||
HoohooMountainBaseBooStatueCaveCoinBlock3 = "Hoohoo Mountain Base Boostatue Cave Coin Block 3"
|
||||
HoohooMountainBaseBooStatueCaveCoinBlock1 = "Hoohoo Mountain Base Boo Statue Cave Coin Block 1"
|
||||
HoohooMountainBaseBooStatueCaveCoinBlock2 = "Hoohoo Mountain Base Boo Statue Cave Coin Block 2"
|
||||
HoohooMountainBaseBooStatueCaveCoinBlock3 = "Hoohoo Mountain Base Boo Statue Cave Coin Block 3"
|
||||
BeanbeanOutskirtsNWCoinBlock = "Beanbean Outskirts NW Coin Block"
|
||||
BeanbeanOutskirtsSRoom1CoinBlock = "Beanbean Outskirts S Room 1 Coin Block"
|
||||
BeanbeanOutskirtsSRoom2CoinBlock = "Beanbean Outskirts S Room 2 Coin Block"
|
||||
|
||||
@@ -2,13 +2,13 @@ from Options import Choice, Toggle, StartInventoryPool, PerGameCommonOptions, Ra
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class SkipBowsersCastle(Toggle):
|
||||
class BowsersCastleSkip(Toggle):
|
||||
"""
|
||||
Skip straight from the Entrance Hall to Bowletta in Bowser's Castle.
|
||||
Skip straight from the entrance hall to Bowletta in Bowser's Castle.
|
||||
All Bowser's Castle locations will be removed from the location pool.
|
||||
"""
|
||||
|
||||
display_name = "Skip Bowser's Castle"
|
||||
display_name = "Bowser's Castle Skip"
|
||||
|
||||
|
||||
class ExtraPipes(Toggle):
|
||||
@@ -272,47 +272,13 @@ class ChuckleBeans(Choice):
|
||||
option_all = 2
|
||||
default = 2
|
||||
|
||||
class Goal(Choice):
|
||||
"""
|
||||
Vanilla: Complete jokes end with the required items and defeat Birdo to unlock Bowser's Castle.
|
||||
|
||||
Emblem Hunt: Find the required number of Beanstar Emblems to gain access to Bowser's Castle.
|
||||
"""
|
||||
display_name = "Goal"
|
||||
option_vanilla = 0
|
||||
option_emblem_hunt = 1
|
||||
default = 0
|
||||
|
||||
class EmblemsRequired(Range):
|
||||
"""
|
||||
Number of Beanstar Emblems to collect to unlock Bowser's Castle.
|
||||
|
||||
If Goal is not Emblem Hunt, this does nothing.
|
||||
"""
|
||||
display_name = "Emblems Required"
|
||||
range_start = 1
|
||||
range_end = 100
|
||||
default = 50
|
||||
|
||||
|
||||
class EmblemsAmount(Range):
|
||||
"""
|
||||
Number of Beanstar Emblems that are in the pool.
|
||||
|
||||
If Goal is not Emblem Hunt, this does nothing.
|
||||
"""
|
||||
display_name = "Emblems Available"
|
||||
range_start = 1
|
||||
range_end = 150
|
||||
default = 75
|
||||
|
||||
|
||||
@dataclass
|
||||
class MLSSOptions(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
coins: Coins
|
||||
difficult_logic: DifficultLogic
|
||||
castle_skip: SkipBowsersCastle
|
||||
castle_skip: BowsersCastleSkip
|
||||
extra_pipes: ExtraPipes
|
||||
skip_minecart: SkipMinecart
|
||||
disable_surf: DisableSurf
|
||||
@@ -320,9 +286,6 @@ class MLSSOptions(PerGameCommonOptions):
|
||||
harhalls_pants: Removed
|
||||
block_visibility: HiddenVisible
|
||||
chuckle_beans: ChuckleBeans
|
||||
goal: Goal
|
||||
emblems_required: EmblemsRequired
|
||||
emblems_amount: EmblemsAmount
|
||||
music_options: MusicOptions
|
||||
randomize_sounds: RandomSounds
|
||||
randomize_enemies: RandomizeEnemies
|
||||
|
||||
@@ -91,16 +91,6 @@ def connect_regions(world: "MLSSWorld"):
|
||||
connect(world, names, "Main Area", "BaseUltraRocks", lambda state: StateLogic.ultra(state, world.player))
|
||||
connect(world, names, "Main Area", "Chucklehuck Woods", lambda state: StateLogic.brooch(state, world.player))
|
||||
connect(world, names, "Main Area", "BooStatue", lambda state: StateLogic.canCrash(state, world.player))
|
||||
if world.options.goal == "emblem_hunt":
|
||||
if world.options.castle_skip:
|
||||
connect(world, names, "Main Area", "Cackletta's Soul",
|
||||
lambda state: state.has("Beanstar Emblem", world.player, world.options.emblems_required.value))
|
||||
else:
|
||||
connect(world, names, "Main Area", "Bowser's Castle", lambda state: state.has("Beanstar Emblem", world.player, world.options.emblems_required.value))
|
||||
connect(world, names, "Bowser's Castle", "Bowser's Castle Mini", lambda state:
|
||||
StateLogic.canMini(state, world.player)
|
||||
and StateLogic.thunder(state,world.player))
|
||||
connect(world, names, "Bowser's Castle Mini", "Cackletta's Soul", lambda state: StateLogic.soul(state, world.player))
|
||||
connect(
|
||||
world,
|
||||
names,
|
||||
@@ -223,8 +213,8 @@ def connect_regions(world: "MLSSWorld"):
|
||||
connect(world, names, "Surfable", "GwarharEntrance")
|
||||
connect(world, names, "Surfable", "Oasis")
|
||||
connect(world, names, "Surfable", "JokesEntrance", lambda state: StateLogic.fire(state, world.player))
|
||||
connect(world, names, "JokesMain", "PostJokes", lambda state: StateLogic.postJokes(state, world.player, world.options.goal.value))
|
||||
if not world.options.castle_skip and world.options.goal != "emblem_hunt":
|
||||
connect(world, names, "JokesMain", "PostJokes", lambda state: StateLogic.postJokes(state, world.player))
|
||||
if not world.options.castle_skip:
|
||||
connect(world, names, "PostJokes", "Bowser's Castle")
|
||||
connect(
|
||||
world,
|
||||
@@ -234,7 +224,7 @@ def connect_regions(world: "MLSSWorld"):
|
||||
lambda state: StateLogic.canMini(state, world.player) and StateLogic.thunder(state, world.player),
|
||||
)
|
||||
connect(world, names, "Bowser's Castle Mini", "Cackletta's Soul")
|
||||
elif world.options.goal != "emblem_hunt":
|
||||
else:
|
||||
connect(world, names, "PostJokes", "Cackletta's Soul")
|
||||
connect(world, names, "Chucklehuck Woods", "Winkle", lambda state: StateLogic.canDash(state, world.player))
|
||||
connect(
|
||||
@@ -257,14 +247,14 @@ def connect_regions(world: "MLSSWorld"):
|
||||
names,
|
||||
"Shop Starting Flag",
|
||||
"Shop Birdo Flag",
|
||||
lambda state: StateLogic.postJokes(state, world.player, world.options.goal.value),
|
||||
lambda state: StateLogic.postJokes(state, world.player),
|
||||
)
|
||||
connect(
|
||||
world,
|
||||
names,
|
||||
"Fungitown",
|
||||
"Fungitown Shop Birdo Flag",
|
||||
lambda state: StateLogic.postJokes(state, world.player, world.options.goal.value),
|
||||
lambda state: StateLogic.postJokes(state, world.player),
|
||||
)
|
||||
else:
|
||||
connect(
|
||||
@@ -286,14 +276,14 @@ def connect_regions(world: "MLSSWorld"):
|
||||
names,
|
||||
"Shop Starting Flag",
|
||||
"Shop Birdo Flag",
|
||||
lambda state: StateLogic.canCrash(state, world.player) and StateLogic.postJokes(state, world.player, world.options.goal.value),
|
||||
lambda state: StateLogic.canCrash(state, world.player) and StateLogic.postJokes(state, world.player),
|
||||
)
|
||||
connect(
|
||||
world,
|
||||
names,
|
||||
"Fungitown",
|
||||
"Fungitown Shop Birdo Flag",
|
||||
lambda state: StateLogic.canCrash(state, world.player) and StateLogic.postJokes(state, world.player, world.options.goal.value),
|
||||
lambda state: StateLogic.canCrash(state, world.player) and StateLogic.postJokes(state, world.player),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -177,10 +177,10 @@ class MLSSPatchExtension(APPatchExtension):
|
||||
for pos in enemies:
|
||||
stream.seek(pos + 8)
|
||||
for _ in range(6):
|
||||
enemy = int.from_bytes(stream.read(1), "little")
|
||||
enemy = int.from_bytes(stream.read(1))
|
||||
if enemy > 0:
|
||||
stream.seek(1, 1)
|
||||
flag = int.from_bytes(stream.read(1), "little")
|
||||
flag = int.from_bytes(stream.read(1))
|
||||
if flag == 0x7:
|
||||
break
|
||||
if flag in [0x0, 0x2, 0x4]:
|
||||
@@ -196,12 +196,12 @@ class MLSSPatchExtension(APPatchExtension):
|
||||
stream.seek(pos + 8)
|
||||
|
||||
for _ in range(6):
|
||||
enemy = int.from_bytes(stream.read(1), "little")
|
||||
enemy = int.from_bytes(stream.read(1))
|
||||
if enemy > 0 and enemy not in Data.flying and enemy not in Data.pestnut:
|
||||
if enemy == 0x52:
|
||||
chomp = True
|
||||
stream.seek(1, 1)
|
||||
flag = int.from_bytes(stream.read(1), "little")
|
||||
flag = int.from_bytes(stream.read(1))
|
||||
if flag not in [0x0, 0x2, 0x4]:
|
||||
stream.seek(1, 1)
|
||||
continue
|
||||
@@ -234,7 +234,7 @@ class MLSSPatchExtension(APPatchExtension):
|
||||
stream.seek(pos)
|
||||
temp = stream.read(1)
|
||||
stream.seek(pos)
|
||||
stream.write(bytes([temp[0] | 0x80]))
|
||||
stream.write(bytes([temp[0] | 0x8]))
|
||||
stream.seek(pos + 1)
|
||||
stream.write(groups.pop())
|
||||
|
||||
@@ -316,10 +316,6 @@ def write_tokens(world: "MLSSWorld", patch: MLSSProcedurePatch) -> None:
|
||||
|
||||
patch.write_token(APTokenTypes.WRITE, 0xD00003, bytes([world.options.xp_multiplier.value]))
|
||||
|
||||
if world.options.goal == 1:
|
||||
patch.write_token(APTokenTypes.WRITE, 0xD00008, bytes([world.options.goal.value]))
|
||||
patch.write_token(APTokenTypes.WRITE, 0xD00009, bytes([world.options.emblems_required.value]))
|
||||
|
||||
if world.options.tattle_hp:
|
||||
patch.write_token(APTokenTypes.WRITE, 0xD00000, bytes([0x1]))
|
||||
|
||||
@@ -431,4 +427,4 @@ def desc_inject(world: "MLSSWorld", patch: MLSSProcedurePatch, location: Locatio
|
||||
index = value.index(location.address) + 66
|
||||
|
||||
dstring = f"{world.multiworld.player_name[item.player]}: {item.name}"
|
||||
patch.write_token(APTokenTypes.WRITE, 0xD12000 + (index * 0x40), dstring.encode("UTF8"))
|
||||
patch.write_token(APTokenTypes.WRITE, 0xD11000 + (index * 0x40), dstring.encode("UTF8"))
|
||||
|
||||
@@ -28,14 +28,11 @@ def set_rules(world: "MLSSWorld", excluded):
|
||||
lambda state: StateLogic.canDig(state, world.player),
|
||||
)
|
||||
if "Shop" in location.name and "Coffee" not in location.name and location.name not in excluded:
|
||||
forbid_item(world.get_location(location.name), "Hammers", world.player)
|
||||
if "Badge" in location.name or "Pants" in location.name:
|
||||
add_rule(
|
||||
world.get_location(location.name),
|
||||
lambda state: (StateLogic.brooch(state, world.player) and StateLogic.fruits(state, world.player)
|
||||
and (StateLogic.hammers(state, world.player)
|
||||
or StateLogic.fire(state, world.player)
|
||||
or StateLogic.thunder(state, world.player)))
|
||||
or StateLogic.rose(state, world.player),
|
||||
lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
|
||||
)
|
||||
if location.itemType != 0 and location.name not in excluded:
|
||||
if "Bowser" in location.name and world.options.castle_skip:
|
||||
@@ -102,86 +99,9 @@ def set_rules(world: "MLSSWorld", excluded):
|
||||
lambda state: StateLogic.ultra(state, world.player) and StateLogic.thunder(state, world.player),
|
||||
)
|
||||
|
||||
if world.options.goal == 1 and not world.options.castle_skip:
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleRoyCorridorBlock1),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleRoyCorridorBlock2),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleMiniMarioSidescrollerBlock1),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleMiniMarioSidescrollerBlock2),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleMiniMarioMazeBlock1),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleMiniMarioMazeBlock2),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleBeforeWendyFightBlock1),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
and StateLogic.ultra(state, world.player)
|
||||
and StateLogic.fire(state, world.player)
|
||||
and StateLogic.canCrash(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleBeforeWendyFightBlock2),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
and StateLogic.ultra(state, world.player)
|
||||
and StateLogic.fire(state, world.player)
|
||||
and StateLogic.canCrash(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleLarryRoomBlock),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
and StateLogic.ultra(state, world.player)
|
||||
and StateLogic.canDash(state, world.player)
|
||||
and StateLogic.canCrash(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleWendyLarryHallwayDigspot),
|
||||
lambda state: StateLogic.ultra(state, world.player)
|
||||
and StateLogic.fire(state, world.player)
|
||||
and StateLogic.canCrash(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleBeforeFawfulFightBlock1),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
and StateLogic.ultra(state, world.player)
|
||||
and StateLogic.canDash(state, world.player)
|
||||
and StateLogic.canCrash(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleBeforeFawfulFightBlock2),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
and StateLogic.ultra(state, world.player)
|
||||
and StateLogic.canDash(state, world.player)
|
||||
and StateLogic.canCrash(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleGreatDoorBlock1),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
and StateLogic.ultra(state, world.player)
|
||||
and StateLogic.canDash(state, world.player)
|
||||
and StateLogic.canCrash(state, world.player)
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.BowsersCastleGreatDoorBlock2),
|
||||
lambda state: StateLogic.canDig(state, world.player)
|
||||
and StateLogic.ultra(state, world.player)
|
||||
and StateLogic.canDash(state, world.player)
|
||||
and StateLogic.canCrash(state, world.player)
|
||||
)
|
||||
forbid_item(
|
||||
world.get_location(LocationName.SSChuckolaMembershipCard), "Nuts", world.player
|
||||
) # Bandaid Fix
|
||||
|
||||
add_rule(
|
||||
world.get_location(LocationName.HoohooVillageHammerHouseBlock),
|
||||
@@ -478,10 +398,6 @@ def set_rules(world: "MLSSWorld", excluded):
|
||||
world.get_location(LocationName.BeanstarPieceWinkleArea),
|
||||
lambda state: StateLogic.winkle(state, world.player),
|
||||
)
|
||||
add_rule(
|
||||
world.get_location("Guffawha Ruins Block"),
|
||||
lambda state: StateLogic.thunder(state, world.player),
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.GwarharLagoonSpangleReward),
|
||||
lambda state: StateLogic.spangle(state, world.player),
|
||||
@@ -490,18 +406,6 @@ def set_rules(world: "MLSSWorld", excluded):
|
||||
world.get_location(LocationName.PantsShopMomPiranhaFlag1),
|
||||
lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
|
||||
)
|
||||
add_rule(
|
||||
world.get_location("Chucklehuck Woods Solo Luigi Cave Room 2 Block"),
|
||||
lambda state: StateLogic.brooch(state, world.player) and StateLogic.canDig(state, world.player),
|
||||
)
|
||||
add_rule(
|
||||
world.get_location("Chucklehuck Woods Solo Luigi Cave Room 3 Block 1"),
|
||||
lambda state: StateLogic.brooch(state, world.player) and StateLogic.canDig(state, world.player),
|
||||
)
|
||||
add_rule(
|
||||
world.get_location("Chucklehuck Woods Solo Luigi Cave Room 3 Block 2"),
|
||||
lambda state: StateLogic.brooch(state, world.player) and StateLogic.canDig(state, world.player),
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.PantsShopMomPiranhaFlag2),
|
||||
lambda state: StateLogic.brooch(state, world.player) or StateLogic.rose(state, world.player),
|
||||
@@ -696,14 +600,6 @@ def set_rules(world: "MLSSWorld", excluded):
|
||||
world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock1),
|
||||
lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
|
||||
)
|
||||
add_rule(
|
||||
world.get_location("Chucklehuck Woods Solo Luigi Cave Room 1 Coin Block 1"),
|
||||
lambda state: StateLogic.canDig(state, world.player) and StateLogic.brooch(state, world.player),
|
||||
)
|
||||
add_rule(
|
||||
world.get_location("Chucklehuck Woods Solo Luigi Cave Room 1 Coin Block 2"),
|
||||
lambda state: StateLogic.canDig(state, world.player) and StateLogic.brooch(state, world.player),
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.HoohooMountainBaseBooStatueCaveCoinBlock2),
|
||||
lambda state: StateLogic.canCrash(state, world.player) or StateLogic.super(state, world.player),
|
||||
@@ -783,7 +679,7 @@ def set_rules(world: "MLSSWorld", excluded):
|
||||
add_rule(
|
||||
world.get_location(LocationName.GwarharLagoonFirstUnderwaterAreaRoom2CoinBlock),
|
||||
lambda state: StateLogic.canDash(state, world.player)
|
||||
and (StateLogic.membership(state, world.player) or StateLogic.surfable(state, world.player)),
|
||||
and (StateLogic.membership(state, world.player) or StateLogic.surfable(state, world.player)),
|
||||
)
|
||||
add_rule(
|
||||
world.get_location(LocationName.JokesEndSecondFloorWestRoomCoinBlock),
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from .Options import Goal
|
||||
|
||||
|
||||
def canDig(state, player):
|
||||
return state.has("Green Goblet", player) and state.has("Hammers", player)
|
||||
|
||||
@@ -108,9 +105,8 @@ def surfable(state, player):
|
||||
)
|
||||
|
||||
|
||||
def postJokes(state, player, goal):
|
||||
if goal == Goal.option_vanilla: # Logic for beating jokes end without beanstar emblems
|
||||
return (
|
||||
def postJokes(state, player):
|
||||
return (
|
||||
surfable(state, player)
|
||||
and canDig(state, player)
|
||||
and dressBeanstar(state, player)
|
||||
@@ -119,13 +115,7 @@ def postJokes(state, player, goal):
|
||||
and brooch(state, player)
|
||||
and rose(state, player)
|
||||
and canDash(state, player)
|
||||
)
|
||||
else: # Logic for beating jokes end with beanstar emblems
|
||||
return (
|
||||
surfable(state, player)
|
||||
and canDig(state, player)
|
||||
and canDash(state, player)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def teehee(state, player):
|
||||
@@ -163,10 +153,3 @@ def birdo_shop(state, player):
|
||||
|
||||
def fungitown_birdo_shop(state, player):
|
||||
return state.can_reach("Fungitown Shop Birdo Flag", "Region", player)
|
||||
|
||||
def soul(state, player):
|
||||
return (ultra(state, player)
|
||||
and canMini(state, player)
|
||||
and canDig(state, player)
|
||||
and canDash(state, player)
|
||||
and canCrash(state, player))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import typing
|
||||
@@ -8,7 +7,7 @@ from worlds.AutoWorld import WebWorld, World
|
||||
from typing import Set, Dict, Any
|
||||
from .Locations import all_locations, location_table, bowsers, bowsersMini, hidden, coins
|
||||
from .Options import MLSSOptions
|
||||
from .Items import MLSSItem, itemList, item_frequencies, item_table, mlss_item_name_groups
|
||||
from .Items import MLSSItem, itemList, item_frequencies, item_table
|
||||
from .Names.LocationName import LocationName
|
||||
from .Client import MLSSClient
|
||||
from .Regions import create_regions, connect_regions
|
||||
@@ -54,7 +53,6 @@ class MLSSWorld(World):
|
||||
options_dataclass = MLSSOptions
|
||||
options: MLSSOptions
|
||||
settings: typing.ClassVar[MLSSSettings]
|
||||
item_name_groups = mlss_item_name_groups
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = {loc_data.name: loc_data.id for loc_data in all_locations}
|
||||
required_client_version = (0, 5, 0)
|
||||
@@ -63,12 +61,6 @@ class MLSSWorld(World):
|
||||
|
||||
def generate_early(self) -> None:
|
||||
self.disabled_locations = set()
|
||||
if self.options.goal == "emblem_hunt":
|
||||
if self.options.emblems_amount < self.options.emblems_required:
|
||||
self.options.emblems_amount.value = self.options.emblems_required.value
|
||||
logging.warning(
|
||||
f"{self.player_name}'s number of emblems required is greater than the number of emblems available. "
|
||||
f"Changing to {self.options.emblems_required.value}.")
|
||||
if self.options.skip_minecart:
|
||||
self.disabled_locations.update([LocationName.HoohooMountainBaseMinecartCaveDigspot])
|
||||
if self.options.disable_surf:
|
||||
@@ -119,8 +111,6 @@ class MLSSWorld(World):
|
||||
for item in itemList:
|
||||
if item.classification != ItemClassification.filler and item.classification != ItemClassification.skip_balancing:
|
||||
freq = item_frequencies.get(item.itemName, 1)
|
||||
if item.itemName == "Beanstar Emblem":
|
||||
freq = (0 if self.options.goal != "emblem_hunt" else self.options.emblems_amount.value)
|
||||
if item in precollected:
|
||||
freq = max(freq - precollected.count(item), 0)
|
||||
if self.options.disable_harhalls_pants and "Harhall's" in item.itemName:
|
||||
@@ -148,6 +138,7 @@ class MLSSWorld(World):
|
||||
|
||||
# And finally take as many fillers as we need to have the same amount of items and locations.
|
||||
remaining = len(all_locations) - len(required_items) - len(self.disabled_locations) - 5
|
||||
|
||||
self.multiworld.itempool += [
|
||||
self.create_item(filler_item_name) for filler_item_name in self.random.sample(filler_items, remaining)
|
||||
]
|
||||
|
||||
Binary file not shown.
@@ -92,7 +92,7 @@ def set_rules(world: "MM2World") -> None:
|
||||
world.wily_5_weapons = slot_data["wily_5_weapons"]
|
||||
else:
|
||||
if world.options.random_weakness == RandomWeaknesses.option_shuffled:
|
||||
weapon_tables = [table.copy() for weapon, table in weapon_damage.items() if weapon not in (0, 8)]
|
||||
weapon_tables = [table for weapon, table in weapon_damage.items() if weapon not in (0, 8)]
|
||||
world.random.shuffle(weapon_tables)
|
||||
for i in range(1, 8):
|
||||
world.weapon_damage[i] = weapon_tables.pop()
|
||||
|
||||
@@ -24,10 +24,9 @@ class MuseDashCollections:
|
||||
MUSE_PLUS_DLC,
|
||||
"CHUNITHM COURSE MUSE", # Part of Muse Plus. Goes away 22nd May 2027.
|
||||
"maimai DX Limited-time Suite", # Part of Muse Plus. Goes away 31st Jan 2026.
|
||||
"MSR Anthology", # Goes away January 26, 2026.
|
||||
"MSR Anthology", # Now no longer available.
|
||||
"Miku in Museland", # Paid DLC not included in Muse Plus
|
||||
"Rin Len's Mirrorland", # Paid DLC not included in Muse Plus
|
||||
"MSR Anthology_Vol.02", # Goes away January 26, 2026.
|
||||
]
|
||||
|
||||
REMOVED_SONGS = [
|
||||
|
||||
@@ -612,19 +612,4 @@ SONG_DATA: Dict[str, SongData] = {
|
||||
"Usagi Flap": SongData(2900736, "81-1", "MD-level Tactical Training Blu-ray", False, 3, 6, 8),
|
||||
"RE Aoharu": SongData(2900737, "81-2", "MD-level Tactical Training Blu-ray", False, 3, 5, 8),
|
||||
"Operation*DOTABATA!": SongData(2900738, "81-3", "MD-level Tactical Training Blu-ray", False, 5, 7, 10),
|
||||
"Break Through the Dome": SongData(2900739, "82-0", "MSR Anthology_Vol.02", False, 5, 7, 9),
|
||||
"Here in Vernal Terrene": SongData(2900740, "82-1", "MSR Anthology_Vol.02", False, 3, 5, 7),
|
||||
"Everything's Alright": SongData(2900741, "82-2", "MSR Anthology_Vol.02", False, 3, 5, 8),
|
||||
"Operation Ashring": SongData(2900742, "82-3", "MSR Anthology_Vol.02", False, 3, 5, 7),
|
||||
"Misty Memory Day Version": SongData(2900743, "82-4", "MSR Anthology_Vol.02", False, 3, 5, 7),
|
||||
"Misty Memory Night Version": SongData(2900744, "82-5", "MSR Anthology_Vol.02", False, 2, 6, 9),
|
||||
"Arsonist": SongData(2900745, "82-6", "MSR Anthology_Vol.02", False, 3, 6, 8),
|
||||
"Operation Deepness": SongData(2900746, "82-7", "MSR Anthology_Vol.02", False, 2, 4, 6),
|
||||
"ALL!!!": SongData(2900747, "82-8", "MSR Anthology_Vol.02", False, 6, 8, 10),
|
||||
"LUNATiC CiRCUiT": SongData(2900748, "83-0", "Cosmic Radio 2024", False, 7, 9, 11),
|
||||
"Synthesis.": SongData(2900749, "83-1", "Cosmic Radio 2024", True, 6, 8, 10),
|
||||
"COSMiC FANFARE!!!!": SongData(2900750, "83-2", "Cosmic Radio 2024", False, 7, 9, 11),
|
||||
"Sharp Bubbles": SongData(2900751, "83-3", "Cosmic Radio 2024", True, 7, 9, 11),
|
||||
"Replay": SongData(2900752, "83-4", "Cosmic Radio 2024", True, 5, 7, 9),
|
||||
"Cosmic Dusty Girl": SongData(2900753, "83-5", "Cosmic Radio 2024", True, 5, 7, 9),
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ def create_kantele(victory_condition: VictoryCondition) -> List[str]:
|
||||
def create_random_items(world: NoitaWorld, weights: Dict[str, int], count: int) -> List[str]:
|
||||
filler_pool = weights.copy()
|
||||
if not world.options.bad_effects:
|
||||
filler_pool["Trap"] = 0
|
||||
filler_pool["Greed Die"] = 0
|
||||
del filler_pool["Trap"]
|
||||
del filler_pool["Greed Die"]
|
||||
|
||||
return world.random.choices(population=list(filler_pool.keys()),
|
||||
weights=list(filler_pool.values()),
|
||||
|
||||
@@ -27,7 +27,6 @@ from .pokemon import (get_random_move, get_species_id_by_label, randomize_abilit
|
||||
randomize_legendary_encounters, randomize_misc_pokemon, randomize_starters,
|
||||
randomize_tm_hm_compatibility,randomize_types, randomize_wild_encounters)
|
||||
from .rom import PokemonEmeraldProcedurePatch, write_tokens
|
||||
from .util import get_encounter_type_label
|
||||
|
||||
|
||||
class PokemonEmeraldWebWorld(WebWorld):
|
||||
@@ -637,11 +636,32 @@ class PokemonEmeraldWorld(World):
|
||||
|
||||
spoiler_handle.write(f"\n\nWild Pokemon ({self.player_name}):\n\n")
|
||||
|
||||
slot_to_rod_suffix = {
|
||||
0: " (Old Rod)",
|
||||
1: " (Old Rod)",
|
||||
2: " (Good Rod)",
|
||||
3: " (Good Rod)",
|
||||
4: " (Good Rod)",
|
||||
5: " (Super Rod)",
|
||||
6: " (Super Rod)",
|
||||
7: " (Super Rod)",
|
||||
8: " (Super Rod)",
|
||||
9: " (Super Rod)",
|
||||
}
|
||||
|
||||
species_maps = defaultdict(set)
|
||||
for map_data in self.modified_maps.values():
|
||||
for encounter_type, encounter_data in map_data.encounters.items():
|
||||
for i, encounter in enumerate(encounter_data.slots):
|
||||
species_maps[encounter].add(f"{map_data.label} ({get_encounter_type_label(encounter_type, i)})")
|
||||
for map in self.modified_maps.values():
|
||||
if map.land_encounters is not None:
|
||||
for encounter in map.land_encounters.slots:
|
||||
species_maps[encounter].add(map.label + " (Land)")
|
||||
|
||||
if map.water_encounters is not None:
|
||||
for encounter in map.water_encounters.slots:
|
||||
species_maps[encounter].add(map.label + " (Water)")
|
||||
|
||||
if map.fishing_encounters is not None:
|
||||
for slot, encounter in enumerate(map.fishing_encounters.slots):
|
||||
species_maps[encounter].add(map.label + slot_to_rod_suffix[slot])
|
||||
|
||||
lines = [f"{emerald_data.species[species].label}: {', '.join(sorted(maps))}\n"
|
||||
for species, maps in species_maps.items()]
|
||||
@@ -655,11 +675,32 @@ class PokemonEmeraldWorld(World):
|
||||
if self.options.dexsanity:
|
||||
from collections import defaultdict
|
||||
|
||||
slot_to_rod_suffix = {
|
||||
0: " (Old Rod)",
|
||||
1: " (Old Rod)",
|
||||
2: " (Good Rod)",
|
||||
3: " (Good Rod)",
|
||||
4: " (Good Rod)",
|
||||
5: " (Super Rod)",
|
||||
6: " (Super Rod)",
|
||||
7: " (Super Rod)",
|
||||
8: " (Super Rod)",
|
||||
9: " (Super Rod)",
|
||||
}
|
||||
|
||||
species_maps = defaultdict(set)
|
||||
for map_data in self.modified_maps.values():
|
||||
for encounter_type, encounter_data in map_data.encounters.items():
|
||||
for i, encounter in enumerate(encounter_data.slots):
|
||||
species_maps[encounter].add(f"{map_data.label} ({get_encounter_type_label(encounter_type, i)})")
|
||||
for map in self.modified_maps.values():
|
||||
if map.land_encounters is not None:
|
||||
for encounter in map.land_encounters.slots:
|
||||
species_maps[encounter].add(map.label + " (Land)")
|
||||
|
||||
if map.water_encounters is not None:
|
||||
for encounter in map.water_encounters.slots:
|
||||
species_maps[encounter].add(map.label + " (Water)")
|
||||
|
||||
if map.fishing_encounters is not None:
|
||||
for slot, encounter in enumerate(map.fishing_encounters.slots):
|
||||
species_maps[encounter].add(map.label + slot_to_rod_suffix[slot])
|
||||
|
||||
hint_data[self.player] = {
|
||||
self.location_name_to_id[f"Pokedex - {emerald_data.species[species].label}"]: ", ".join(sorted(maps))
|
||||
|
||||
@@ -5,7 +5,7 @@ defined data (like location labels or usable pokemon species), some cleanup
|
||||
and sorting, and Warp methods.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum, Enum
|
||||
from enum import IntEnum
|
||||
import orjson
|
||||
from typing import Dict, List, NamedTuple, Optional, Set, FrozenSet, Tuple, Any, Union
|
||||
import pkgutil
|
||||
@@ -148,20 +148,14 @@ class EncounterTableData(NamedTuple):
|
||||
address: int
|
||||
|
||||
|
||||
# class EncounterType(StrEnum): # StrEnum introduced in python 3.11
|
||||
class EncounterType(Enum):
|
||||
LAND = "LAND"
|
||||
WATER = "WATER"
|
||||
FISHING = "FISHING"
|
||||
ROCK_SMASH = "ROCK_SMASH"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MapData:
|
||||
name: str
|
||||
label: str
|
||||
header_address: int
|
||||
encounters: Dict[EncounterType, EncounterTableData]
|
||||
land_encounters: Optional[EncounterTableData]
|
||||
water_encounters: Optional[EncounterTableData]
|
||||
fishing_encounters: Optional[EncounterTableData]
|
||||
|
||||
|
||||
class EventData(NamedTuple):
|
||||
@@ -221,9 +215,34 @@ class EvolutionMethodEnum(IntEnum):
|
||||
FRIENDSHIP_NIGHT = 11
|
||||
|
||||
|
||||
def _str_to_evolution_method(string: str) -> EvolutionMethodEnum:
|
||||
if string == "LEVEL":
|
||||
return EvolutionMethodEnum.LEVEL
|
||||
if string == "LEVEL_ATK_LT_DEF":
|
||||
return EvolutionMethodEnum.LEVEL_ATK_LT_DEF
|
||||
if string == "LEVEL_ATK_EQ_DEF":
|
||||
return EvolutionMethodEnum.LEVEL_ATK_EQ_DEF
|
||||
if string == "LEVEL_ATK_GT_DEF":
|
||||
return EvolutionMethodEnum.LEVEL_ATK_GT_DEF
|
||||
if string == "LEVEL_SILCOON":
|
||||
return EvolutionMethodEnum.LEVEL_SILCOON
|
||||
if string == "LEVEL_CASCOON":
|
||||
return EvolutionMethodEnum.LEVEL_CASCOON
|
||||
if string == "LEVEL_NINJASK":
|
||||
return EvolutionMethodEnum.LEVEL_NINJASK
|
||||
if string == "LEVEL_SHEDINJA":
|
||||
return EvolutionMethodEnum.LEVEL_SHEDINJA
|
||||
if string == "FRIENDSHIP":
|
||||
return EvolutionMethodEnum.FRIENDSHIP
|
||||
if string == "FRIENDSHIP_DAY":
|
||||
return EvolutionMethodEnum.FRIENDSHIP_DAY
|
||||
if string == "FRIENDSHIP_NIGHT":
|
||||
return EvolutionMethodEnum.FRIENDSHIP_NIGHT
|
||||
|
||||
|
||||
class EvolutionData(NamedTuple):
|
||||
method: EvolutionMethodEnum
|
||||
param: int # Level/item id/friendship/etc.; depends on method
|
||||
param: int
|
||||
species_id: int
|
||||
|
||||
|
||||
@@ -354,27 +373,25 @@ def _init() -> None:
|
||||
if map_name in IGNORABLE_MAPS:
|
||||
continue
|
||||
|
||||
encounter_tables: Dict[EncounterType, EncounterTableData] = {}
|
||||
land_encounters = None
|
||||
water_encounters = None
|
||||
fishing_encounters = None
|
||||
|
||||
if "land_encounters" in map_json:
|
||||
encounter_tables[EncounterType.LAND] = EncounterTableData(
|
||||
land_encounters = EncounterTableData(
|
||||
map_json["land_encounters"]["slots"],
|
||||
map_json["land_encounters"]["address"]
|
||||
)
|
||||
if "water_encounters" in map_json:
|
||||
encounter_tables[EncounterType.WATER] = EncounterTableData(
|
||||
water_encounters = EncounterTableData(
|
||||
map_json["water_encounters"]["slots"],
|
||||
map_json["water_encounters"]["address"]
|
||||
)
|
||||
if "fishing_encounters" in map_json:
|
||||
encounter_tables[EncounterType.FISHING] = EncounterTableData(
|
||||
fishing_encounters = EncounterTableData(
|
||||
map_json["fishing_encounters"]["slots"],
|
||||
map_json["fishing_encounters"]["address"]
|
||||
)
|
||||
if "rock_smash_encounters" in map_json:
|
||||
encounter_tables[EncounterType.ROCK_SMASH] = EncounterTableData(
|
||||
map_json["rock_smash_encounters"]["slots"],
|
||||
map_json["rock_smash_encounters"]["address"]
|
||||
)
|
||||
|
||||
# Derive a user-facing label
|
||||
label = []
|
||||
@@ -406,7 +423,9 @@ def _init() -> None:
|
||||
map_name,
|
||||
" ".join(label),
|
||||
map_json["header_address"],
|
||||
encounter_tables
|
||||
land_encounters,
|
||||
water_encounters,
|
||||
fishing_encounters
|
||||
)
|
||||
|
||||
# Load/merge region json files
|
||||
@@ -940,7 +959,7 @@ def _init() -> None:
|
||||
(species_data["types"][0], species_data["types"][1]),
|
||||
(species_data["abilities"][0], species_data["abilities"][1]),
|
||||
[EvolutionData(
|
||||
EvolutionMethodEnum[evolution_json["method"]],
|
||||
_str_to_evolution_method(evolution_json["method"]),
|
||||
evolution_json["param"],
|
||||
evolution_json["species"],
|
||||
) for evolution_json in species_data["evolutions"]],
|
||||
@@ -958,34 +977,24 @@ def _init() -> None:
|
||||
data.species[evolution.species_id].pre_evolution = species.species_id
|
||||
|
||||
# Replace default item for dex entry locations based on evo stage of species
|
||||
evo_stage_to_ball_map: Dict[int, int] = {
|
||||
evo_stage_to_ball_map = {
|
||||
0: data.constants["ITEM_POKE_BALL"],
|
||||
1: data.constants["ITEM_GREAT_BALL"],
|
||||
2: data.constants["ITEM_ULTRA_BALL"],
|
||||
}
|
||||
|
||||
for species in data.species.values():
|
||||
default_item: Optional[int] = None
|
||||
pre_evolution = species.pre_evolution
|
||||
|
||||
if pre_evolution is not None:
|
||||
evo_data = next(evo for evo in data.species[pre_evolution].evolutions if evo.species_id == species.species_id)
|
||||
if evo_data.method == EvolutionMethodEnum.ITEM:
|
||||
default_item = evo_data.param
|
||||
|
||||
evo_stage = 0
|
||||
if default_item is None:
|
||||
while pre_evolution is not None:
|
||||
evo_stage += 1
|
||||
pre_evolution = data.species[pre_evolution].pre_evolution
|
||||
default_item = evo_stage_to_ball_map[evo_stage]
|
||||
pre_evolution = species.pre_evolution
|
||||
while pre_evolution is not None:
|
||||
evo_stage += 1
|
||||
pre_evolution = data.species[pre_evolution].pre_evolution
|
||||
|
||||
dex_location_name = f"POKEDEX_REWARD_{str(species.national_dex_number).zfill(3)}"
|
||||
data.locations[dex_location_name] = LocationData(
|
||||
data.locations[dex_location_name].name,
|
||||
data.locations[dex_location_name].label,
|
||||
data.locations[dex_location_name].parent_region,
|
||||
default_item,
|
||||
evo_stage_to_ball_map[evo_stage],
|
||||
data.locations[dex_location_name].address,
|
||||
data.locations[dex_location_name].flag,
|
||||
data.locations[dex_location_name].category,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,8 +4,7 @@ Functions related to pokemon species and moves
|
||||
import functools
|
||||
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple
|
||||
|
||||
from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterType, EncounterTableData, LearnsetMove, SpeciesData,
|
||||
MapData, data)
|
||||
from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, SpeciesData, data)
|
||||
from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters,
|
||||
RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon,
|
||||
TmTutorCompatibility)
|
||||
@@ -227,42 +226,6 @@ def randomize_types(world: "PokemonEmeraldWorld") -> None:
|
||||
evolutions += [world.modified_species[evo.species_id] for evo in evolution.evolutions]
|
||||
|
||||
|
||||
_encounter_subcategory_ranges: Dict[EncounterType, Dict[range, Optional[str]]] = {
|
||||
EncounterType.LAND: {range(0, 12): None},
|
||||
EncounterType.WATER: {range(0, 5): None},
|
||||
EncounterType.FISHING: {range(0, 2): "OLD_ROD", range(2, 5): "GOOD_ROD", range(5, 10): "SUPER_ROD"},
|
||||
}
|
||||
|
||||
|
||||
def _rename_wild_events(world: "PokemonEmeraldWorld", map_data: MapData, new_slots: List[int], encounter_type: EncounterType):
|
||||
"""
|
||||
Renames the events that correspond to wild encounters to reflect the new species there after randomization
|
||||
"""
|
||||
for i, new_species_id in enumerate(new_slots):
|
||||
# Get the subcategory for rods
|
||||
subcategory_range, subcategory_name = next(
|
||||
(r, sc)
|
||||
for r, sc in _encounter_subcategory_ranges[encounter_type].items()
|
||||
if i in r
|
||||
)
|
||||
subcategory_species = []
|
||||
for k in subcategory_range:
|
||||
if new_slots[k] not in subcategory_species:
|
||||
subcategory_species.append(new_slots[k])
|
||||
|
||||
# Create the name of the location that corresponds to this encounter slot
|
||||
# Fishing locations include the rod name
|
||||
subcategory_str = "" if subcategory_name is None else "_" + subcategory_name
|
||||
encounter_location_index = subcategory_species.index(new_species_id) + 1
|
||||
encounter_location_name = f"{map_data.name}_{encounter_type.value}_ENCOUNTERS{subcategory_str}_{encounter_location_index}"
|
||||
try:
|
||||
# Get the corresponding location and change the event name to reflect the new species
|
||||
slot_location = world.multiworld.get_location(encounter_location_name, world.player)
|
||||
slot_location.item.name = f"CATCH_{data.species[new_species_id].name}"
|
||||
except KeyError:
|
||||
pass # Map probably isn't included; should be careful here about bad encounter location names
|
||||
|
||||
|
||||
def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
|
||||
if world.options.wild_pokemon == RandomizeWildPokemon.option_vanilla:
|
||||
return
|
||||
@@ -290,96 +253,120 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
|
||||
placed_priority_species = False
|
||||
map_data = world.modified_maps[map_name]
|
||||
|
||||
new_encounters: Dict[EncounterType, EncounterTableData] = {}
|
||||
new_encounters: List[Optional[EncounterTableData]] = [None, None, None]
|
||||
old_encounters = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters]
|
||||
|
||||
for encounter_type, table in map_data.encounters.items():
|
||||
# Create a map from the original species to new species
|
||||
# instead of just randomizing every slot.
|
||||
# Force area 1-to-1 mapping, in other words.
|
||||
species_old_to_new_map: Dict[int, int] = {}
|
||||
for species_id in table.slots:
|
||||
if species_id not in species_old_to_new_map:
|
||||
if not placed_priority_species and len(priority_species) > 0 \
|
||||
and encounter_type != EncounterType.ROCK_SMASH and map_name not in OUT_OF_LOGIC_MAPS:
|
||||
new_species_id = priority_species.pop()
|
||||
placed_priority_species = True
|
||||
else:
|
||||
original_species = data.species[species_id]
|
||||
|
||||
# Construct progressive tiers of blacklists that can be peeled back if they
|
||||
# collectively cover too much of the pokedex. A lower index in `blacklists`
|
||||
# indicates a more important set of species to avoid. Entries at `0` will
|
||||
# always be blacklisted.
|
||||
blacklists: Dict[int, List[Set[int]]] = defaultdict(list)
|
||||
|
||||
# Blacklist pokemon already on this table
|
||||
blacklists[0].append(set(species_old_to_new_map.values()))
|
||||
|
||||
# If doing legendary hunt, blacklist Latios from wild encounters so
|
||||
# it can be tracked as the roamer. Otherwise it may be impossible
|
||||
# to tell whether a highlighted route is the roamer or a wild
|
||||
# encounter.
|
||||
if world.options.goal == Goal.option_legendary_hunt:
|
||||
blacklists[0].append({data.constants["SPECIES_LATIOS"]})
|
||||
|
||||
# If dexsanity/catch 'em all mode, blacklist already placed species
|
||||
# until every species has been placed once
|
||||
if world.options.dexsanity and len(already_placed) < num_placeable_species:
|
||||
blacklists[1].append(already_placed)
|
||||
|
||||
# Blacklist from player options
|
||||
blacklists[2].append(world.blacklisted_wilds)
|
||||
|
||||
# Type matching blacklist
|
||||
if should_match_type:
|
||||
blacklists[3].append({
|
||||
species.species_id
|
||||
for species in world.modified_species.values()
|
||||
if not bool(set(species.types) & set(original_species.types))
|
||||
})
|
||||
|
||||
merged_blacklist: Set[int] = set()
|
||||
for max_priority in reversed(sorted(blacklists.keys())):
|
||||
merged_blacklist = set()
|
||||
for priority in blacklists.keys():
|
||||
if priority <= max_priority:
|
||||
for blacklist in blacklists[priority]:
|
||||
merged_blacklist |= blacklist
|
||||
|
||||
if len(merged_blacklist) < NUM_REAL_SPECIES:
|
||||
break
|
||||
for i, table in enumerate(old_encounters):
|
||||
if table is not None:
|
||||
# Create a map from the original species to new species
|
||||
# instead of just randomizing every slot.
|
||||
# Force area 1-to-1 mapping, in other words.
|
||||
species_old_to_new_map: Dict[int, int] = {}
|
||||
for species_id in table.slots:
|
||||
if species_id not in species_old_to_new_map:
|
||||
if not placed_priority_species and len(priority_species) > 0 \
|
||||
and map_name not in OUT_OF_LOGIC_MAPS:
|
||||
new_species_id = priority_species.pop()
|
||||
placed_priority_species = True
|
||||
else:
|
||||
raise RuntimeError("This should never happen")
|
||||
original_species = data.species[species_id]
|
||||
|
||||
candidates = [
|
||||
species
|
||||
for species in world.modified_species.values()
|
||||
if species.species_id not in merged_blacklist
|
||||
]
|
||||
# Construct progressive tiers of blacklists that can be peeled back if they
|
||||
# collectively cover too much of the pokedex. A lower index in `blacklists`
|
||||
# indicates a more important set of species to avoid. Entries at `0` will
|
||||
# always be blacklisted.
|
||||
blacklists: Dict[int, List[Set[int]]] = defaultdict(list)
|
||||
|
||||
if should_match_bst:
|
||||
candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
|
||||
# Blacklist pokemon already on this table
|
||||
blacklists[0].append(set(species_old_to_new_map.values()))
|
||||
|
||||
new_species_id = world.random.choice(candidates).species_id
|
||||
# If doing legendary hunt, blacklist Latios from wild encounters so
|
||||
# it can be tracked as the roamer. Otherwise it may be impossible
|
||||
# to tell whether a highlighted route is the roamer or a wild
|
||||
# encounter.
|
||||
if world.options.goal == Goal.option_legendary_hunt:
|
||||
blacklists[0].append({data.constants["SPECIES_LATIOS"]})
|
||||
|
||||
species_old_to_new_map[species_id] = new_species_id
|
||||
# If dexsanity/catch 'em all mode, blacklist already placed species
|
||||
# until every species has been placed once
|
||||
if world.options.dexsanity and len(already_placed) < num_placeable_species:
|
||||
blacklists[1].append(already_placed)
|
||||
|
||||
if world.options.dexsanity and encounter_type != EncounterType.ROCK_SMASH \
|
||||
and map_name not in OUT_OF_LOGIC_MAPS:
|
||||
already_placed.add(new_species_id)
|
||||
# Blacklist from player options
|
||||
blacklists[2].append(world.blacklisted_wilds)
|
||||
|
||||
# Actually create the new list of slots and encounter table
|
||||
new_slots: List[int] = []
|
||||
for species_id in table.slots:
|
||||
new_slots.append(species_old_to_new_map[species_id])
|
||||
# Type matching blacklist
|
||||
if should_match_type:
|
||||
blacklists[3].append({
|
||||
species.species_id
|
||||
for species in world.modified_species.values()
|
||||
if not bool(set(species.types) & set(original_species.types))
|
||||
})
|
||||
|
||||
new_encounters[encounter_type] = EncounterTableData(new_slots, table.address)
|
||||
merged_blacklist: Set[int] = set()
|
||||
for max_priority in reversed(sorted(blacklists.keys())):
|
||||
merged_blacklist = set()
|
||||
for priority in blacklists.keys():
|
||||
if priority <= max_priority:
|
||||
for blacklist in blacklists[priority]:
|
||||
merged_blacklist |= blacklist
|
||||
|
||||
# Rock smash encounters not used in logic, so they have no events
|
||||
if encounter_type != EncounterType.ROCK_SMASH:
|
||||
_rename_wild_events(world, map_data, new_slots, encounter_type)
|
||||
if len(merged_blacklist) < NUM_REAL_SPECIES:
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("This should never happen")
|
||||
|
||||
map_data.encounters = new_encounters
|
||||
candidates = [
|
||||
species
|
||||
for species in world.modified_species.values()
|
||||
if species.species_id not in merged_blacklist
|
||||
]
|
||||
|
||||
if should_match_bst:
|
||||
candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
|
||||
|
||||
new_species_id = world.random.choice(candidates).species_id
|
||||
species_old_to_new_map[species_id] = new_species_id
|
||||
|
||||
if world.options.dexsanity and map_name not in OUT_OF_LOGIC_MAPS:
|
||||
already_placed.add(new_species_id)
|
||||
|
||||
# Actually create the new list of slots and encounter table
|
||||
new_slots: List[int] = []
|
||||
for species_id in table.slots:
|
||||
new_slots.append(species_old_to_new_map[species_id])
|
||||
|
||||
new_encounters[i] = EncounterTableData(new_slots, table.address)
|
||||
|
||||
# Rename event items for the new wild pokemon species
|
||||
slot_category: Tuple[str, List[Tuple[Optional[str], range]]] = [
|
||||
("LAND", [(None, range(0, 12))]),
|
||||
("WATER", [(None, range(0, 5))]),
|
||||
("FISHING", [("OLD_ROD", range(0, 2)), ("GOOD_ROD", range(2, 5)), ("SUPER_ROD", range(5, 10))]),
|
||||
][i]
|
||||
for j, new_species_id in enumerate(new_slots):
|
||||
# Get the subcategory for rods
|
||||
subcategory = next(sc for sc in slot_category[1] if j in sc[1])
|
||||
subcategory_species = []
|
||||
for k in subcategory[1]:
|
||||
if new_slots[k] not in subcategory_species:
|
||||
subcategory_species.append(new_slots[k])
|
||||
|
||||
# Create the name of the location that corresponds to this encounter slot
|
||||
# Fishing locations include the rod name
|
||||
subcategory_str = "" if subcategory[0] is None else "_" + subcategory[0]
|
||||
encounter_location_index = subcategory_species.index(new_species_id) + 1
|
||||
encounter_location_name = f"{map_data.name}_{slot_category[0]}_ENCOUNTERS{subcategory_str}_{encounter_location_index}"
|
||||
try:
|
||||
# Get the corresponding location and change the event name to reflect the new species
|
||||
slot_location = world.multiworld.get_location(encounter_location_name, world.player)
|
||||
slot_location.item.name = f"CATCH_{data.species[new_species_id].name}"
|
||||
except KeyError:
|
||||
pass # Map probably isn't included; should be careful here about bad encounter location names
|
||||
|
||||
map_data.land_encounters = new_encounters[0]
|
||||
map_data.water_encounters = new_encounters[1]
|
||||
map_data.fishing_encounters = new_encounters[2]
|
||||
|
||||
|
||||
def randomize_abilities(world: "PokemonEmeraldWorld") -> None:
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from BaseClasses import CollectionState, ItemClassification, Region
|
||||
|
||||
from .data import EncounterType, data
|
||||
from .data import data
|
||||
from .items import PokemonEmeraldItem
|
||||
from .locations import PokemonEmeraldLocation
|
||||
|
||||
@@ -19,11 +19,11 @@ def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]:
|
||||
Also creates and places events and connects regions via warps and the exits defined in the JSON.
|
||||
"""
|
||||
# Used in connect_to_map_encounters. Splits encounter categories into "subcategories" and gives them names
|
||||
# and rules so the rods can only access their specific slots. Rock smash encounters are not considered in logic.
|
||||
encounter_categories: Dict[EncounterType, List[Tuple[Optional[str], range, Optional[Callable[[CollectionState], bool]]]]] = {
|
||||
EncounterType.LAND: [(None, range(0, 12), None)],
|
||||
EncounterType.WATER: [(None, range(0, 5), None)],
|
||||
EncounterType.FISHING: [
|
||||
# and rules so the rods can only access their specific slots.
|
||||
encounter_categories: Dict[str, List[Tuple[Optional[str], range, Optional[Callable[[CollectionState], bool]]]]] = {
|
||||
"LAND": [(None, range(0, 12), None)],
|
||||
"WATER": [(None, range(0, 5), None)],
|
||||
"FISHING": [
|
||||
("OLD_ROD", range(0, 2), lambda state: state.has("Old Rod", world.player)),
|
||||
("GOOD_ROD", range(2, 5), lambda state: state.has("Good Rod", world.player)),
|
||||
("SUPER_ROD", range(5, 10), lambda state: state.has("Super Rod", world.player)),
|
||||
@@ -41,19 +41,19 @@ def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]:
|
||||
These regions are created lazily and dynamically so as not to bother with unused maps.
|
||||
"""
|
||||
# For each of land, water, and fishing, connect the region if indicated by include_slots
|
||||
for i, (encounter_type, subcategories) in enumerate(encounter_categories.items()):
|
||||
for i, encounter_category in enumerate(encounter_categories.items()):
|
||||
if include_slots[i]:
|
||||
region_name = f"{map_name}_{encounter_type.value}_ENCOUNTERS"
|
||||
region_name = f"{map_name}_{encounter_category[0]}_ENCOUNTERS"
|
||||
|
||||
# If the region hasn't been created yet, create it now
|
||||
try:
|
||||
encounter_region = world.multiworld.get_region(region_name, world.player)
|
||||
except KeyError:
|
||||
encounter_region = Region(region_name, world.player, world.multiworld)
|
||||
encounter_slots = data.maps[map_name].encounters[encounter_type].slots
|
||||
encounter_slots = getattr(data.maps[map_name], f"{encounter_category[0].lower()}_encounters").slots
|
||||
|
||||
# Subcategory is for splitting fishing rods; land and water only have one subcategory
|
||||
for subcategory in subcategories:
|
||||
for subcategory in encounter_category[1]:
|
||||
# Want to create locations per species, not per slot
|
||||
# encounter_categories includes info on which slots belong to which subcategory
|
||||
unique_species = []
|
||||
|
||||
@@ -696,10 +696,12 @@ def _set_encounter_tables(world: "PokemonEmeraldWorld", patch: PokemonEmeraldPro
|
||||
}
|
||||
"""
|
||||
for map_data in world.modified_maps.values():
|
||||
for table in map_data.encounters.values():
|
||||
for i, species_id in enumerate(table.slots):
|
||||
address = table.address + 2 + (4 * i)
|
||||
patch.write_token(APTokenTypes.WRITE, address, struct.pack("<H", species_id))
|
||||
tables = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters]
|
||||
for table in tables:
|
||||
if table is not None:
|
||||
for i, species_id in enumerate(table.slots):
|
||||
address = table.address + 2 + (4 * i)
|
||||
patch.write_token(APTokenTypes.WRITE, address, struct.pack("<H", species_id))
|
||||
|
||||
|
||||
def _set_species_info(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch, easter_egg: Tuple[int, int]) -> None:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import orjson
|
||||
from typing import Any, Dict, List, Optional, Tuple, Iterable
|
||||
|
||||
from .data import NATIONAL_ID_TO_SPECIES_ID, EncounterType, data
|
||||
from .data import NATIONAL_ID_TO_SPECIES_ID, data
|
||||
|
||||
|
||||
CHARACTER_DECODING_MAP = {
|
||||
@@ -86,28 +86,6 @@ def decode_string(string_data: Iterable[int]) -> str:
|
||||
return string
|
||||
|
||||
|
||||
def get_encounter_type_label(encounter_type: EncounterType, slot: int) -> str:
|
||||
if encounter_type == EncounterType.FISHING:
|
||||
return {
|
||||
0: "Old Rod",
|
||||
1: "Old Rod",
|
||||
2: "Good Rod",
|
||||
3: "Good Rod",
|
||||
4: "Good Rod",
|
||||
5: "Super Rod",
|
||||
6: "Super Rod",
|
||||
7: "Super Rod",
|
||||
8: "Super Rod",
|
||||
9: "Super Rod",
|
||||
}[slot]
|
||||
|
||||
return {
|
||||
EncounterType.LAND: 'Land',
|
||||
EncounterType.WATER: 'Water',
|
||||
EncounterType.ROCK_SMASH: 'Rock Smash',
|
||||
}[encounter_type]
|
||||
|
||||
|
||||
def get_easter_egg(easter_egg: str) -> Tuple[int, int]:
|
||||
easter_egg = easter_egg.upper()
|
||||
result1 = 0
|
||||
|
||||
@@ -1,58 +1,47 @@
|
||||
from typing import NamedTuple
|
||||
from BaseClasses import Item
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
|
||||
sm64ex_base_id: int = 3626000
|
||||
|
||||
class SM64Item(Item):
|
||||
game: str = "Super Mario 64"
|
||||
|
||||
class SM64ItemData(NamedTuple):
|
||||
code: int | None = None
|
||||
classification: ItemClassification = ItemClassification.progression
|
||||
|
||||
generic_item_data_table: dict[str, SM64ItemData] = {
|
||||
"Power Star": SM64ItemData(sm64ex_base_id + 0, ItemClassification.progression_skip_balancing),
|
||||
"Basement Key": SM64ItemData(sm64ex_base_id + 178),
|
||||
"Second Floor Key": SM64ItemData(sm64ex_base_id + 179),
|
||||
"Progressive Key": SM64ItemData(sm64ex_base_id + 180),
|
||||
"Wing Cap": SM64ItemData(sm64ex_base_id + 181),
|
||||
"Metal Cap": SM64ItemData(sm64ex_base_id + 182),
|
||||
"Vanish Cap": SM64ItemData(sm64ex_base_id + 183),
|
||||
"1Up Mushroom": SM64ItemData(sm64ex_base_id + 184, ItemClassification.filler),
|
||||
generic_item_table = {
|
||||
"Power Star": 3626000,
|
||||
"Basement Key": 3626178,
|
||||
"Second Floor Key": 3626179,
|
||||
"Progressive Key": 3626180,
|
||||
"Wing Cap": 3626181,
|
||||
"Metal Cap": 3626182,
|
||||
"Vanish Cap": 3626183,
|
||||
"1Up Mushroom": 3626184
|
||||
}
|
||||
|
||||
action_item_data_table: dict[str, SM64ItemData] = {
|
||||
"Double Jump": SM64ItemData(sm64ex_base_id + 185),
|
||||
"Triple Jump": SM64ItemData(sm64ex_base_id + 186),
|
||||
"Long Jump": SM64ItemData(sm64ex_base_id + 187),
|
||||
"Backflip": SM64ItemData(sm64ex_base_id + 188),
|
||||
"Side Flip": SM64ItemData(sm64ex_base_id + 189),
|
||||
"Wall Kick": SM64ItemData(sm64ex_base_id + 190),
|
||||
"Dive": SM64ItemData(sm64ex_base_id + 191),
|
||||
"Ground Pound": SM64ItemData(sm64ex_base_id + 192),
|
||||
"Kick": SM64ItemData(sm64ex_base_id + 193),
|
||||
"Climb": SM64ItemData(sm64ex_base_id + 194),
|
||||
"Ledge Grab": SM64ItemData(sm64ex_base_id + 195),
|
||||
action_item_table = {
|
||||
"Double Jump": 3626185,
|
||||
"Triple Jump": 3626186,
|
||||
"Long Jump": 3626187,
|
||||
"Backflip": 3626188,
|
||||
"Side Flip": 3626189,
|
||||
"Wall Kick": 3626190,
|
||||
"Dive": 3626191,
|
||||
"Ground Pound": 3626192,
|
||||
"Kick": 3626193,
|
||||
"Climb": 3626194,
|
||||
"Ledge Grab": 3626195
|
||||
}
|
||||
|
||||
cannon_item_data_table: dict[str, SM64ItemData] = {
|
||||
"Cannon Unlock BoB": SM64ItemData(sm64ex_base_id + 200),
|
||||
"Cannon Unlock WF": SM64ItemData(sm64ex_base_id + 201),
|
||||
"Cannon Unlock JRB": SM64ItemData(sm64ex_base_id + 202),
|
||||
"Cannon Unlock CCM": SM64ItemData(sm64ex_base_id + 203),
|
||||
"Cannon Unlock SSL": SM64ItemData(sm64ex_base_id + 207),
|
||||
"Cannon Unlock SL": SM64ItemData(sm64ex_base_id + 209),
|
||||
"Cannon Unlock WDW": SM64ItemData(sm64ex_base_id + 210),
|
||||
"Cannon Unlock TTM": SM64ItemData(sm64ex_base_id + 211),
|
||||
"Cannon Unlock THI": SM64ItemData(sm64ex_base_id + 212),
|
||||
"Cannon Unlock RR": SM64ItemData(sm64ex_base_id + 214),
|
||||
|
||||
cannon_item_table = {
|
||||
"Cannon Unlock BoB": 3626200,
|
||||
"Cannon Unlock WF": 3626201,
|
||||
"Cannon Unlock JRB": 3626202,
|
||||
"Cannon Unlock CCM": 3626203,
|
||||
"Cannon Unlock SSL": 3626207,
|
||||
"Cannon Unlock SL": 3626209,
|
||||
"Cannon Unlock WDW": 3626210,
|
||||
"Cannon Unlock TTM": 3626211,
|
||||
"Cannon Unlock THI": 3626212,
|
||||
"Cannon Unlock RR": 3626214
|
||||
}
|
||||
|
||||
item_data_table = {
|
||||
**generic_item_data_table,
|
||||
**action_item_data_table,
|
||||
**cannon_item_data_table
|
||||
}
|
||||
|
||||
item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None}
|
||||
item_table = {**generic_item_table, **action_item_table, **cannon_item_table}
|
||||
@@ -1,7 +1,7 @@
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet, OptionGroup
|
||||
from .Items import action_item_data_table
|
||||
from .Items import action_item_table
|
||||
|
||||
class EnableCoinStars(Choice):
|
||||
"""
|
||||
@@ -135,7 +135,7 @@ class MoveRandomizerActions(OptionSet):
|
||||
"""Which actions to randomize when Move Randomizer is enabled"""
|
||||
display_name = "Randomized Moves"
|
||||
# HACK: Disable randomization for double jump
|
||||
valid_keys = [action for action in action_item_data_table if action != 'Double Jump']
|
||||
valid_keys = [action for action in action_item_table if action != 'Double Jump']
|
||||
default = valid_keys
|
||||
|
||||
sm64_options_groups = [
|
||||
|
||||
@@ -6,7 +6,7 @@ from .Locations import location_table
|
||||
from .Options import SM64Options
|
||||
from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level,\
|
||||
sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances
|
||||
from .Items import action_item_data_table
|
||||
from .Items import action_item_table
|
||||
|
||||
def shuffle_dict_keys(world, dictionary: dict) -> dict:
|
||||
keys = list(dictionary.keys())
|
||||
@@ -372,9 +372,8 @@ class RuleFactory:
|
||||
item = self.token_table.get(token, None)
|
||||
if not item:
|
||||
raise Exception(f"Invalid token: '{item}'")
|
||||
if item in action_item_data_table:
|
||||
double_jump_bitvec_offset = action_item_data_table['Double Jump'].code
|
||||
if self.move_rando_bitvec & (1 << (action_item_data_table[item].code - double_jump_bitvec_offset)) == 0:
|
||||
if item in action_item_table:
|
||||
if self.move_rando_bitvec & (1 << (action_item_table[item] - action_item_table['Double Jump'])) == 0:
|
||||
# This action item is not randomized.
|
||||
return True
|
||||
return item
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import typing
|
||||
import os
|
||||
import json
|
||||
from .Items import item_data_table, action_item_data_table, cannon_item_data_table, item_table, SM64Item
|
||||
from .Items import item_table, action_item_table, cannon_item_table, SM64Item
|
||||
from .Locations import location_table, SM64Location
|
||||
from .Options import sm64_options_groups, SM64Options
|
||||
from .Rules import set_rules
|
||||
@@ -65,10 +65,9 @@ class SM64World(World):
|
||||
max_stars -= 15
|
||||
self.move_rando_bitvec = 0
|
||||
if self.options.enable_move_rando:
|
||||
double_jump_bitvec_offset = action_item_data_table['Double Jump'].code
|
||||
for action in self.options.move_rando_actions.value:
|
||||
max_stars -= 1
|
||||
self.move_rando_bitvec |= (1 << (action_item_data_table[action].code - double_jump_bitvec_offset))
|
||||
self.move_rando_bitvec |= (1 << (action_item_table[action] - action_item_table['Double Jump']))
|
||||
if self.options.exclamation_boxes:
|
||||
max_stars += 29
|
||||
self.number_of_stars = min(self.options.amount_of_stars, max_stars)
|
||||
@@ -101,8 +100,14 @@ class SM64World(World):
|
||||
'entrance', self.player)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
data = item_data_table[name]
|
||||
item = SM64Item(name, data.classification, data.code, self.player)
|
||||
item_id = item_table[name]
|
||||
if name == "1Up Mushroom":
|
||||
classification = ItemClassification.filler
|
||||
elif name == "Power Star":
|
||||
classification = ItemClassification.progression_skip_balancing
|
||||
else:
|
||||
classification = ItemClassification.progression
|
||||
item = SM64Item(name, classification, item_id, self.player)
|
||||
|
||||
return item
|
||||
|
||||
@@ -126,12 +131,11 @@ class SM64World(World):
|
||||
self.multiworld.itempool += [self.create_item(cap_name) for cap_name in ["Wing Cap", "Metal Cap", "Vanish Cap"]]
|
||||
# Cannons
|
||||
if (self.options.buddy_checks):
|
||||
self.multiworld.itempool += [self.create_item(cannon_name) for cannon_name in cannon_item_data_table.keys()]
|
||||
self.multiworld.itempool += [self.create_item(name) for name, id in cannon_item_table.items()]
|
||||
# Moves
|
||||
double_jump_bitvec_offset = action_item_data_table['Double Jump'].code
|
||||
self.multiworld.itempool += [self.create_item(action)
|
||||
for action, itemdata in action_item_data_table.items()
|
||||
if self.move_rando_bitvec & (1 << itemdata.code - double_jump_bitvec_offset)]
|
||||
for action, itemid in action_item_table.items()
|
||||
if self.move_rando_bitvec & (1 << itemid - action_item_table['Double Jump'])]
|
||||
|
||||
def generate_basic(self):
|
||||
if not (self.options.buddy_checks):
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import typing
|
||||
from random import Random
|
||||
from typing import Dict, Any, Iterable, Optional, List, TextIO, cast
|
||||
|
||||
@@ -10,15 +9,14 @@ from .bundles.bundle_room import BundleRoom
|
||||
from .bundles.bundles import get_all_bundles
|
||||
from .content import StardewContent, create_content
|
||||
from .early_items import setup_early_items
|
||||
from .items import item_table, create_items, ItemData, Group, items_by_group, generate_filler_choice_pool
|
||||
from .items import item_table, create_items, ItemData, Group, items_by_group, get_all_filler_items, remove_limited_amount_packs
|
||||
from .locations import location_table, create_locations, LocationData, locations_by_tag
|
||||
from .logic.logic import StardewLogic
|
||||
from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, EnabledFillerBuffs, NumberOfMovementBuffs, \
|
||||
BuildingProgression, EntranceRandomization, FarmType
|
||||
BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization, FarmType
|
||||
from .options.forced_options import force_change_options_if_incompatible
|
||||
from .options.option_groups import sv_option_groups
|
||||
from .options.presets import sv_options_presets
|
||||
from .options.worlds_group import apply_most_restrictive_options
|
||||
from .regions import create_regions
|
||||
from .rules import set_rules
|
||||
from .stardew_rule import True_, StardewRule, HasProgressionPercent
|
||||
@@ -91,16 +89,6 @@ class StardewValleyWorld(World):
|
||||
|
||||
total_progression_items: int
|
||||
|
||||
@classmethod
|
||||
def create_group(cls, multiworld: MultiWorld, new_player_id: int, players: set[int]) -> World:
|
||||
world_group = super().create_group(multiworld, new_player_id, players)
|
||||
|
||||
group_options = typing.cast(StardewValleyOptions, world_group.options)
|
||||
worlds_options = [typing.cast(StardewValleyOptions, multiworld.worlds[player].options) for player in players]
|
||||
apply_most_restrictive_options(group_options, worlds_options)
|
||||
|
||||
return world_group
|
||||
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
super().__init__(multiworld, player)
|
||||
self.filler_item_pool_names = []
|
||||
@@ -311,9 +299,32 @@ class StardewValleyWorld(World):
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
if not self.filler_item_pool_names:
|
||||
self.filler_item_pool_names = generate_filler_choice_pool(self.options)
|
||||
self.generate_filler_item_pool_names()
|
||||
return self.random.choice(self.filler_item_pool_names)
|
||||
|
||||
def generate_filler_item_pool_names(self):
|
||||
include_traps, exclude_island = self.get_filler_item_rules()
|
||||
available_filler = get_all_filler_items(include_traps, exclude_island)
|
||||
available_filler = remove_limited_amount_packs(available_filler)
|
||||
self.filler_item_pool_names = [item.name for item in available_filler]
|
||||
|
||||
def get_filler_item_rules(self):
|
||||
if self.player in self.multiworld.groups:
|
||||
link_group = self.multiworld.groups[self.player]
|
||||
include_traps = True
|
||||
exclude_island = False
|
||||
for player in link_group["players"]:
|
||||
if self.multiworld.game[player] != self.game:
|
||||
continue
|
||||
player_options = cast(StardewValleyOptions, self.multiworld.worlds[player].options)
|
||||
if player_options.trap_items == TrapItems.option_no_traps:
|
||||
include_traps = False
|
||||
if player_options.exclude_ginger_island == ExcludeGingerIsland.option_true:
|
||||
exclude_island = True
|
||||
return include_traps, exclude_island
|
||||
else:
|
||||
return self.options.trap_items != TrapItems.option_no_traps, self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
|
||||
|
||||
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
|
||||
"""Write to the spoiler header. If individual it's right at the end of that player's options,
|
||||
if as stage it's right under the common header before per-player options."""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from . import content_packs
|
||||
from .feature import cropsanity, friendsanity, fishsanity, booksanity, skill_progression, tool_progression
|
||||
from .feature import cropsanity, friendsanity, fishsanity, booksanity, skill_progression
|
||||
from .game_content import ContentPack, StardewContent, StardewFeatures
|
||||
from .unpacking import unpack_content
|
||||
from .. import options
|
||||
@@ -33,7 +33,6 @@ def choose_features(player_options: options.StardewValleyOptions) -> StardewFeat
|
||||
choose_fishsanity(player_options.fishsanity),
|
||||
choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size),
|
||||
choose_skill_progression(player_options.skill_progression),
|
||||
choose_tool_progression(player_options.tool_progression, player_options.skill_progression),
|
||||
)
|
||||
|
||||
|
||||
@@ -123,18 +122,3 @@ def choose_skill_progression(skill_progression_option: options.SkillProgression)
|
||||
raise ValueError(f"No skill progression feature mapped to {str(skill_progression_option.value)}")
|
||||
|
||||
return skill_progression_feature
|
||||
|
||||
|
||||
def choose_tool_progression(tool_option: options.ToolProgression, skill_option: options.SkillProgression) -> tool_progression.ToolProgressionFeature:
|
||||
if tool_option.is_vanilla:
|
||||
return tool_progression.ToolProgressionVanilla()
|
||||
|
||||
tools_distribution = tool_progression.get_tools_distribution(
|
||||
progressive_tools_enabled=True,
|
||||
skill_masteries_enabled=skill_option == options.SkillProgression.option_progressive_with_masteries,
|
||||
)
|
||||
|
||||
if tool_option.is_progressive:
|
||||
return tool_progression.ToolProgressionProgressive(tools_distribution)
|
||||
|
||||
raise ValueError(f"No tool progression feature mapped to {str(tool_option.value)}")
|
||||
|
||||
@@ -3,4 +3,3 @@ from . import cropsanity
|
||||
from . import fishsanity
|
||||
from . import friendsanity
|
||||
from . import skill_progression
|
||||
from . import tool_progression
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
from abc import ABC
|
||||
from collections import Counter
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from functools import cache
|
||||
from types import MappingProxyType
|
||||
from typing import ClassVar
|
||||
|
||||
from ...strings.tool_names import Tool
|
||||
|
||||
|
||||
def to_progressive_item(tool: str) -> str:
|
||||
"""Return the name of the progressive item."""
|
||||
return f"Progressive {tool}"
|
||||
|
||||
|
||||
# The golden scythe is always randomized
|
||||
VANILLA_TOOL_DISTRIBUTION = MappingProxyType({
|
||||
Tool.scythe: 1,
|
||||
})
|
||||
|
||||
PROGRESSIVE_TOOL_DISTRIBUTION = MappingProxyType({
|
||||
Tool.axe: 4,
|
||||
Tool.hoe: 4,
|
||||
Tool.pickaxe: 4,
|
||||
Tool.pan: 4,
|
||||
Tool.trash_can: 4,
|
||||
Tool.watering_can: 4,
|
||||
Tool.fishing_rod: 4,
|
||||
})
|
||||
|
||||
# Masteries add another tier to the scythe and the fishing rod
|
||||
SKILL_MASTERIES_TOOL_DISTRIBUTION = MappingProxyType({
|
||||
Tool.scythe: 1,
|
||||
Tool.fishing_rod: 1,
|
||||
})
|
||||
|
||||
|
||||
@cache
|
||||
def get_tools_distribution(progressive_tools_enabled: bool, skill_masteries_enabled: bool) -> Mapping[str, int]:
|
||||
distribution = Counter(VANILLA_TOOL_DISTRIBUTION)
|
||||
|
||||
if progressive_tools_enabled:
|
||||
distribution += PROGRESSIVE_TOOL_DISTRIBUTION
|
||||
|
||||
if skill_masteries_enabled:
|
||||
distribution += SKILL_MASTERIES_TOOL_DISTRIBUTION
|
||||
|
||||
return MappingProxyType(distribution)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolProgressionFeature(ABC):
|
||||
is_progressive: ClassVar[bool]
|
||||
tool_distribution: Mapping[str, int]
|
||||
|
||||
to_progressive_item = staticmethod(to_progressive_item)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolProgressionVanilla(ToolProgressionFeature):
|
||||
is_progressive = False
|
||||
# FIXME change the default_factory to a simple default when python 3.11 is no longer supported
|
||||
tool_distribution: Mapping[str, int] = field(default_factory=lambda: VANILLA_TOOL_DISTRIBUTION)
|
||||
|
||||
|
||||
class ToolProgressionProgressive(ToolProgressionFeature):
|
||||
is_progressive = True
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union
|
||||
|
||||
from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_progression, tool_progression
|
||||
from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_progression
|
||||
from ..data.fish_data import FishItem
|
||||
from ..data.game_item import GameItem, ItemSource, ItemTag
|
||||
from ..data.skill import Skill
|
||||
@@ -54,7 +54,6 @@ class StardewFeatures:
|
||||
fishsanity: fishsanity.FishsanityFeature
|
||||
friendsanity: friendsanity.FriendsanityFeature
|
||||
skill_progression: skill_progression.SkillProgressionFeature
|
||||
tool_progression: tool_progression.ToolProgressionFeature
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -150,8 +150,7 @@ base_game = BaseGameContentPack(
|
||||
Seed.coffee_starter: (CustomRuleSource(lambda logic: logic.traveling_merchant.has_days(3) & logic.monster.can_kill_many(Monster.dust_sprite)),),
|
||||
Seed.coffee: (HarvestCropSource(seed=Seed.coffee_starter, seasons=(Season.spring, Season.summer,)),),
|
||||
|
||||
Vegetable.tea_leaves: (
|
||||
CustomRuleSource(lambda logic: logic.has(WildSeeds.tea_sapling) & logic.time.has_lived_months(2) & logic.season.has_any_not_winter()),),
|
||||
Vegetable.tea_leaves: (CustomRuleSource(lambda logic: logic.has(Sapling.tea) & logic.time.has_lived_months(2) & logic.season.has_any_not_winter()),),
|
||||
},
|
||||
artisan_good_sources={
|
||||
Beverage.beer: (MachineSource(item=Vegetable.wheat, machine=Machine.keg),),
|
||||
|
||||
@@ -32,7 +32,7 @@ def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions,
|
||||
if options.backpack_progression == stardew_options.BackpackProgression.option_early_progressive:
|
||||
early_forced.append("Progressive Backpack")
|
||||
|
||||
if content.features.tool_progression.is_progressive:
|
||||
if options.tool_progression & stardew_options.ToolProgression.option_progressive:
|
||||
if content.features.fishsanity.is_enabled:
|
||||
early_candidates.append("Progressive Fishing Rod")
|
||||
early_forced.append("Progressive Pickaxe")
|
||||
|
||||
@@ -15,7 +15,7 @@ from .data.game_item import ItemTag
|
||||
from .logic.logic_event import all_events
|
||||
from .mods.mod_data import ModNames
|
||||
from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \
|
||||
BuildingProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \
|
||||
BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \
|
||||
Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs
|
||||
from .strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName
|
||||
from .strings.ap_names.ap_weapon_names import APWeapon
|
||||
@@ -23,7 +23,6 @@ from .strings.ap_names.buff_names import Buff
|
||||
from .strings.ap_names.community_upgrade_names import CommunityUpgrade
|
||||
from .strings.ap_names.mods.mod_items import SVEQuestItem
|
||||
from .strings.currency_names import Currency
|
||||
from .strings.tool_names import Tool
|
||||
from .strings.wallet_item_names import Wallet
|
||||
|
||||
ITEM_CODE_OFFSET = 717000
|
||||
@@ -120,6 +119,11 @@ class StardewItemFactory(Protocol):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class StardewItemDeleter(Protocol):
|
||||
def __call__(self, item: Item):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def load_item_csv():
|
||||
from importlib.resources import files
|
||||
|
||||
@@ -222,7 +226,7 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley
|
||||
create_weapons(item_factory, options, items)
|
||||
items.append(item_factory("Skull Key"))
|
||||
create_elevators(item_factory, options, items)
|
||||
create_tools(item_factory, content, items)
|
||||
create_tools(item_factory, options, content, items)
|
||||
create_skills(item_factory, content, items)
|
||||
create_wizard_buildings(item_factory, options, items)
|
||||
create_carpenter_buildings(item_factory, options, items)
|
||||
@@ -312,17 +316,23 @@ def create_elevators(item_factory: StardewItemFactory, options: StardewValleyOpt
|
||||
items.extend([item_factory(item) for item in ["Progressive Skull Cavern Elevator"] * 8])
|
||||
|
||||
|
||||
def create_tools(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]):
|
||||
tool_progression = content.features.tool_progression
|
||||
for tool, count in tool_progression.tool_distribution.items():
|
||||
item = item_table[tool_progression.to_progressive_item(tool)]
|
||||
def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item]):
|
||||
if options.tool_progression & ToolProgression.option_progressive:
|
||||
for item_data in items_by_group[Group.PROGRESSIVE_TOOLS]:
|
||||
name = item_data.name
|
||||
if "Trash Can" in name:
|
||||
items.extend([item_factory(item) for item in [item_data] * 3])
|
||||
items.append(item_factory(item_data, ItemClassification.useful))
|
||||
else:
|
||||
items.extend([item_factory(item) for item in [item_data] * 4])
|
||||
|
||||
# Trash can is only used in tool upgrade logic, so the last trash can is not progression because it basically does not unlock anything.
|
||||
if tool == Tool.trash_can:
|
||||
count -= 1
|
||||
items.append(item_factory(item, ItemClassification.useful))
|
||||
if content.features.skill_progression.are_masteries_shuffled:
|
||||
# Masteries add another tier to the scythe and the fishing rod
|
||||
items.append(item_factory("Progressive Scythe"))
|
||||
items.append(item_factory("Progressive Fishing Rod"))
|
||||
|
||||
items.extend([item_factory(item) for _ in range(count)])
|
||||
# The golden scythe is always randomized
|
||||
items.append(item_factory("Progressive Scythe"))
|
||||
|
||||
|
||||
def create_skills(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]):
|
||||
@@ -808,16 +818,6 @@ def remove_excluded_items_island_mods(items, exclude_ginger_island: bool, mods:
|
||||
return mod_filter
|
||||
|
||||
|
||||
def generate_filler_choice_pool(options: StardewValleyOptions) -> list[str]:
|
||||
include_traps = options.trap_items != TrapItems.option_no_traps
|
||||
exclude_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true
|
||||
|
||||
available_filler = get_all_filler_items(include_traps, exclude_island)
|
||||
available_filler = remove_limited_amount_packs(available_filler)
|
||||
|
||||
return [item.name for item in available_filler]
|
||||
|
||||
|
||||
def remove_limited_amount_packs(packs):
|
||||
return [pack for pack in packs if Group.MAXIMUM_ONE not in pack.groups and Group.EXACTLY_TWO not in pack.groups]
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from .data.game_item import ItemTag
|
||||
from .data.museum_data import all_museum_items
|
||||
from .mods.mod_data import ModNames
|
||||
from .options import ExcludeGingerIsland, ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \
|
||||
FestivalLocations, BuildingProgression, ElevatorProgression, BackpackProgression, FarmType
|
||||
FestivalLocations, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, FarmType
|
||||
from .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity
|
||||
from .strings.goal_names import Goal
|
||||
from .strings.quest_names import ModQuest, Quest
|
||||
@@ -473,7 +473,7 @@ def create_locations(location_collector: StardewLocationCollector,
|
||||
extend_bundle_locations(randomized_locations, bundle_rooms)
|
||||
extend_backpack_locations(randomized_locations, options)
|
||||
|
||||
if content.features.tool_progression.is_progressive:
|
||||
if options.tool_progression & ToolProgression.option_progressive:
|
||||
randomized_locations.extend(locations_by_tag[LocationTags.TOOL_UPGRADE])
|
||||
|
||||
extend_elevator_locations(randomized_locations, options)
|
||||
|
||||
@@ -67,6 +67,7 @@ from ..strings.fish_names import Fish, Trash, WaterItem, WaterChest
|
||||
from ..strings.flower_names import Flower
|
||||
from ..strings.food_names import Meal, Beverage
|
||||
from ..strings.forageable_names import Forageable
|
||||
from ..strings.fruit_tree_names import Sapling
|
||||
from ..strings.generic_names import Generic
|
||||
from ..strings.geode_names import Geode
|
||||
from ..strings.gift_names import Gift
|
||||
@@ -299,6 +300,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
Ore.radioactive: self.ability.can_mine_perfectly() & self.region.can_reach(Region.qi_walnut_room),
|
||||
RetainingSoil.basic: self.money.can_spend_at(Region.pierre_store, 100),
|
||||
RetainingSoil.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
|
||||
Sapling.tea: self.relationship.has_hearts(NPC.caroline, 2) & self.has(Material.fiber) & self.has(Material.wood),
|
||||
SpeedGro.basic: self.money.can_spend_at(Region.pierre_store, 100),
|
||||
SpeedGro.deluxe: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
|
||||
Trash.broken_cd: self.skill.can_crab_pot,
|
||||
|
||||
@@ -10,11 +10,12 @@ from .region_logic import RegionLogicMixin
|
||||
from .skill_logic import SkillLogicMixin
|
||||
from .tool_logic import ToolLogicMixin
|
||||
from .. import options
|
||||
from ..options import ToolProgression
|
||||
from ..stardew_rule import StardewRule, True_
|
||||
from ..strings.performance_names import Performance
|
||||
from ..strings.region_names import Region
|
||||
from ..strings.skill_names import Skill
|
||||
from ..strings.tool_names import ToolMaterial
|
||||
from ..strings.tool_names import Tool, ToolMaterial
|
||||
|
||||
|
||||
class MineLogicMixin(BaseLogicMixin):
|
||||
@@ -55,12 +56,11 @@ SkillLogicMixin, CookingLogicMixin]]):
|
||||
def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule:
|
||||
tier = floor // 40
|
||||
rules = []
|
||||
|
||||
weapon_rule = self.logic.mine.get_weapon_rule_for_floor_tier(tier)
|
||||
rules.append(weapon_rule)
|
||||
|
||||
tool_rule = self.logic.tool.can_mine_using(ToolMaterial.tiers[tier])
|
||||
rules.append(tool_rule)
|
||||
if self.options.tool_progression & ToolProgression.option_progressive:
|
||||
rules.append(self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier]))
|
||||
|
||||
# No alternative for vanilla because we assume that you will grind the levels in the mines.
|
||||
if self.content.features.skill_progression.is_progressive:
|
||||
@@ -85,12 +85,11 @@ SkillLogicMixin, CookingLogicMixin]]):
|
||||
def can_progress_in_the_skull_cavern_from_floor(self, floor: int) -> StardewRule:
|
||||
tier = floor // 50
|
||||
rules = []
|
||||
|
||||
weapon_rule = self.logic.combat.has_great_weapon
|
||||
rules.append(weapon_rule)
|
||||
|
||||
tool_rule = self.logic.tool.can_mine_using(ToolMaterial.tiers[min(4, max(0, tier + 2))])
|
||||
rules.append(tool_rule)
|
||||
if self.options.tool_progression & ToolProgression.option_progressive:
|
||||
rules.append(self.logic.received("Progressive Pickaxe", min(4, max(0, tier + 2))))
|
||||
|
||||
# No alternative for vanilla because we assume that you will grind the levels in the mines.
|
||||
if self.content.features.skill_progression.is_progressive:
|
||||
|
||||
@@ -8,11 +8,12 @@ from .received_logic import ReceivedLogicMixin
|
||||
from .region_logic import RegionLogicMixin
|
||||
from .season_logic import SeasonLogicMixin
|
||||
from ..mods.logic.magic_logic import MagicLogicMixin
|
||||
from ..options import ToolProgression
|
||||
from ..stardew_rule import StardewRule, True_, False_
|
||||
from ..strings.ap_names.skill_level_names import ModSkillLevel
|
||||
from ..strings.region_names import Region, LogicRegion
|
||||
from ..strings.region_names import Region
|
||||
from ..strings.spells import MagicSpell
|
||||
from ..strings.tool_names import ToolMaterial, Tool, APTool
|
||||
from ..strings.tool_names import ToolMaterial, Tool
|
||||
|
||||
fishing_rod_prices = {
|
||||
3: 1800,
|
||||
@@ -56,10 +57,10 @@ class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixi
|
||||
if material == ToolMaterial.basic or tool == Tool.scythe:
|
||||
return True_()
|
||||
|
||||
if self.content.features.tool_progression.is_progressive:
|
||||
if self.options.tool_progression & ToolProgression.option_progressive:
|
||||
return self.logic.received(f"Progressive {tool}", tool_materials[material])
|
||||
|
||||
can_upgrade_rule = self.logic.tool._can_purchase_upgrade(material)
|
||||
can_upgrade_rule = self.logic.has(f"{material} Bar") & self.logic.money.can_spend_at(Region.blacksmith, tool_upgrade_prices[material])
|
||||
if tool == Tool.pan:
|
||||
has_base_pan = self.logic.received("Glittering Boulder Removed") & self.logic.region.can_reach(Region.mountain)
|
||||
if material == ToolMaterial.copper:
|
||||
@@ -68,20 +69,6 @@ class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixi
|
||||
|
||||
return can_upgrade_rule
|
||||
|
||||
@cache_self1
|
||||
def can_mine_using(self, material: str) -> StardewRule:
|
||||
if material == ToolMaterial.basic:
|
||||
return self.logic.true_
|
||||
|
||||
if self.content.features.tool_progression.is_progressive:
|
||||
return self.logic.received(APTool.pickaxe, tool_materials[material])
|
||||
else:
|
||||
return self.logic.tool._can_purchase_upgrade(material)
|
||||
|
||||
@cache_self1
|
||||
def _can_purchase_upgrade(self, material: str) -> StardewRule:
|
||||
return self.logic.region.can_reach(LogicRegion.blacksmith_upgrade(material))
|
||||
|
||||
def can_use_tool_at(self, tool: str, material: str, region: str) -> StardewRule:
|
||||
return self.has_tool(tool, material) & self.logic.region.can_reach(region)
|
||||
|
||||
@@ -89,8 +76,8 @@ class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixi
|
||||
def has_fishing_rod(self, level: int) -> StardewRule:
|
||||
assert 1 <= level <= 4, "Fishing rod 0 isn't real, it can't hurt you. Training is 1, Bamboo is 2, Fiberglass is 3 and Iridium is 4."
|
||||
|
||||
if self.content.features.tool_progression.is_progressive:
|
||||
return self.logic.received(APTool.fishing_rod, level)
|
||||
if self.options.tool_progression & ToolProgression.option_progressive:
|
||||
return self.logic.received(f"Progressive {Tool.fishing_rod}", level)
|
||||
|
||||
if level <= 2:
|
||||
# We assume you always have access to the Bamboo pole, because mod side there is a builtin way to get it back.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Dict, Union
|
||||
|
||||
from ..mod_data import ModNames
|
||||
from ... import options
|
||||
from ...logic.base_logic import BaseLogicMixin, BaseLogic
|
||||
from ...logic.combat_logic import CombatLogicMixin
|
||||
from ...logic.cooking_logic import CookingLogicMixin
|
||||
@@ -79,7 +80,7 @@ FarmingLogicMixin]]):
|
||||
# Gingerbread House
|
||||
}
|
||||
|
||||
if self.content.features.tool_progression.is_progressive:
|
||||
if self.options.tool_progression & options.ToolProgression.option_progressive:
|
||||
options_to_update.update({
|
||||
Ore.iridium: items[Ore.iridium] | self.logic.tool.can_use_tool_at(Tool.axe, ToolMaterial.iridium, DeepWoodsRegion.floor_50), # Iridium Tree
|
||||
})
|
||||
|
||||
@@ -247,14 +247,6 @@ class ToolProgression(Choice):
|
||||
option_progressive_cheap = 0b011 # 3
|
||||
option_progressive_very_cheap = 0b101 # 5
|
||||
|
||||
@property
|
||||
def is_vanilla(self):
|
||||
return not self.is_progressive
|
||||
|
||||
@property
|
||||
def is_progressive(self):
|
||||
return bool(self.value & self.option_progressive)
|
||||
|
||||
|
||||
class ElevatorProgression(Choice):
|
||||
"""Shuffle the elevator?
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
from typing import Iterable
|
||||
|
||||
from .options import StardewValleyOptions
|
||||
|
||||
|
||||
def apply_most_restrictive_options(group_option: StardewValleyOptions, world_options: Iterable[StardewValleyOptions]) -> None:
|
||||
"""Merge the options of the worlds member of the group that can impact fillers generation into the option class of the group.
|
||||
"""
|
||||
|
||||
# If at least one world disabled ginger island, disabling it for the whole group
|
||||
group_option.exclude_ginger_island.value = max(o.exclude_ginger_island.value for o in world_options)
|
||||
|
||||
# If at least one world disabled traps, disabling them for the whole group
|
||||
group_option.trap_items.value = min(o.trap_items.value for o in world_options)
|
||||
@@ -19,8 +19,9 @@ from .logic.logic import StardewLogic
|
||||
from .logic.time_logic import MAX_MONTHS
|
||||
from .logic.tool_logic import tool_upgrade_prices
|
||||
from .mods.mod_data import ModNames
|
||||
from .options import BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \
|
||||
Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, StardewValleyOptions, Walnutsanity
|
||||
from .options import StardewValleyOptions, Walnutsanity
|
||||
from .options import ToolProgression, BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \
|
||||
Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity
|
||||
from .stardew_rule import And, StardewRule, true_
|
||||
from .stardew_rule.indirect_connection import look_for_indirect_connection
|
||||
from .stardew_rule.rule_explain import explain
|
||||
@@ -68,7 +69,7 @@ def set_rules(world):
|
||||
set_entrance_rules(logic, multiworld, player, world_options)
|
||||
set_ginger_island_rules(logic, multiworld, player, world_options)
|
||||
|
||||
set_tool_rules(logic, multiworld, player, world_content)
|
||||
set_tool_rules(logic, multiworld, player, world_options)
|
||||
set_skills_rules(logic, multiworld, player, world_content)
|
||||
set_bundle_rules(bundle_rooms, logic, multiworld, player, world_options)
|
||||
set_building_rules(logic, multiworld, player, world_options)
|
||||
@@ -110,8 +111,8 @@ def set_isolated_locations_rules(logic: StardewLogic, multiworld, player):
|
||||
logic.season.has(Season.spring))
|
||||
|
||||
|
||||
def set_tool_rules(logic: StardewLogic, multiworld, player, content: StardewContent):
|
||||
if not content.features.tool_progression.is_progressive:
|
||||
def set_tool_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
|
||||
if not world_options.tool_progression & ToolProgression.option_progressive:
|
||||
return
|
||||
|
||||
MultiWorldRules.add_rule(multiworld.get_location("Purchase Fiberglass Rod", player),
|
||||
@@ -280,6 +281,13 @@ def set_skull_cavern_floor_entrance_rules(logic, multiworld, player):
|
||||
set_entrance_rule(multiworld, player, dig_to_skull_floor(floor), rule)
|
||||
|
||||
|
||||
def set_blacksmith_entrance_rules(logic, multiworld, player):
|
||||
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_copper, MetalBar.copper, ToolMaterial.copper)
|
||||
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iron, MetalBar.iron, ToolMaterial.iron)
|
||||
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_gold, MetalBar.gold, ToolMaterial.gold)
|
||||
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iridium, MetalBar.iridium, ToolMaterial.iridium)
|
||||
|
||||
|
||||
def set_skill_entrance_rules(logic, multiworld, player, world_options: StardewValleyOptions):
|
||||
set_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops, logic.farming.has_farming_tools & logic.season.has_spring)
|
||||
set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops, logic.farming.has_farming_tools & logic.season.has_summer)
|
||||
@@ -298,13 +306,6 @@ def set_skill_entrance_rules(logic, multiworld, player, world_options: StardewVa
|
||||
set_entrance_rule(multiworld, player, LogicEntrance.fishing, logic.skill.can_get_fishing_xp)
|
||||
|
||||
|
||||
def set_blacksmith_entrance_rules(logic, multiworld, player):
|
||||
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_copper, MetalBar.copper, ToolMaterial.copper)
|
||||
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iron, MetalBar.iron, ToolMaterial.iron)
|
||||
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_gold, MetalBar.gold, ToolMaterial.gold)
|
||||
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iridium, MetalBar.iridium, ToolMaterial.iridium)
|
||||
|
||||
|
||||
def set_blacksmith_upgrade_rule(logic, multiworld, player, entrance_name: str, item_name: str, tool_material: str):
|
||||
upgrade_rule = logic.has(item_name) & logic.money.can_spend(tool_upgrade_prices[tool_material])
|
||||
set_entrance_rule(multiworld, player, entrance_name, upgrade_rule)
|
||||
|
||||
@@ -194,15 +194,10 @@ class LogicEntrance:
|
||||
island_cooking = "Island Cooking"
|
||||
shipping = "Use Shipping Bin"
|
||||
watch_queen_of_sauce = "Watch Queen of Sauce"
|
||||
|
||||
@staticmethod
|
||||
def blacksmith_upgrade(material: str) -> str:
|
||||
return f"Upgrade {material} Tools"
|
||||
|
||||
blacksmith_copper = blacksmith_upgrade("Copper")
|
||||
blacksmith_iron = blacksmith_upgrade("Iron")
|
||||
blacksmith_gold = blacksmith_upgrade("Gold")
|
||||
blacksmith_iridium = blacksmith_upgrade("Iridium")
|
||||
blacksmith_copper = "Upgrade Copper Tools"
|
||||
blacksmith_iron = "Upgrade Iron Tools"
|
||||
blacksmith_gold = "Upgrade Gold Tools"
|
||||
blacksmith_iridium = "Upgrade Iridium Tools"
|
||||
|
||||
grow_spring_crops = "Grow Spring Crops"
|
||||
grow_summer_crops = "Grow Summer Crops"
|
||||
|
||||
@@ -7,3 +7,4 @@ class Sapling:
|
||||
pomegranate = "Pomegranate Sapling"
|
||||
banana = "Banana Sapling"
|
||||
mango = "Mango Sapling"
|
||||
tea = "Tea Sapling"
|
||||
|
||||
@@ -159,15 +159,10 @@ class LogicRegion:
|
||||
kitchen = "Kitchen"
|
||||
shipping = "Shipping"
|
||||
queen_of_sauce = "The Queen of Sauce"
|
||||
|
||||
@staticmethod
|
||||
def blacksmith_upgrade(material: str) -> str:
|
||||
return f"Blacksmith {material} Upgrades"
|
||||
|
||||
blacksmith_copper = blacksmith_upgrade("Copper")
|
||||
blacksmith_iron = blacksmith_upgrade("Iron")
|
||||
blacksmith_gold = blacksmith_upgrade("Gold")
|
||||
blacksmith_iridium = blacksmith_upgrade("Iridium")
|
||||
blacksmith_copper = "Blacksmith Copper Upgrades"
|
||||
blacksmith_iron = "Blacksmith Iron Upgrades"
|
||||
blacksmith_gold = "Blacksmith Gold Upgrades"
|
||||
blacksmith_iridium = "Blacksmith Iridium Upgrades"
|
||||
|
||||
spring_farming = "Spring Farming"
|
||||
summer_farming = "Summer Farming"
|
||||
|
||||
@@ -5,8 +5,8 @@ from . import SVTestBase
|
||||
from .. import items, location_table, options
|
||||
from ..items import Group
|
||||
from ..locations import LocationTags
|
||||
from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, SkillProgression, \
|
||||
Booksanity, Walnutsanity
|
||||
from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, ToolProgression, \
|
||||
SkillProgression, Booksanity, Walnutsanity
|
||||
from ..strings.region_names import Region
|
||||
|
||||
|
||||
@@ -320,7 +320,7 @@ class TestProgressiveElevator(SVTestBase):
|
||||
class TestSkullCavernLogic(SVTestBase):
|
||||
options = {
|
||||
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
|
||||
options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
|
||||
ToolProgression.internal_name: ToolProgression.option_progressive,
|
||||
options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class TestBitFlagsVanilla(SVTestBase):
|
||||
|
||||
def test_options_are_not_detected_as_progressive(self):
|
||||
world_options = self.world.options
|
||||
tool_progressive = self.world.content.features.tool_progression.is_progressive
|
||||
tool_progressive = world_options.tool_progression & ToolProgression.option_progressive
|
||||
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
|
||||
self.assertFalse(tool_progressive)
|
||||
self.assertFalse(building_progressive)
|
||||
@@ -26,7 +26,7 @@ class TestBitFlagsVanillaCheap(SVTestBase):
|
||||
|
||||
def test_options_are_not_detected_as_progressive(self):
|
||||
world_options = self.world.options
|
||||
tool_progressive = self.world.content.features.tool_progression.is_progressive
|
||||
tool_progressive = world_options.tool_progression & ToolProgression.option_progressive
|
||||
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
|
||||
self.assertFalse(tool_progressive)
|
||||
self.assertFalse(building_progressive)
|
||||
@@ -43,7 +43,7 @@ class TestBitFlagsVanillaVeryCheap(SVTestBase):
|
||||
|
||||
def test_options_are_not_detected_as_progressive(self):
|
||||
world_options = self.world.options
|
||||
tool_progressive = self.world.content.features.tool_progression.is_progressive
|
||||
tool_progressive = world_options.tool_progression & ToolProgression.option_progressive
|
||||
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
|
||||
self.assertFalse(tool_progressive)
|
||||
self.assertFalse(building_progressive)
|
||||
@@ -60,7 +60,7 @@ class TestBitFlagsProgressive(SVTestBase):
|
||||
|
||||
def test_options_are_detected_as_progressive(self):
|
||||
world_options = self.world.options
|
||||
tool_progressive = self.world.content.features.tool_progression.is_progressive
|
||||
tool_progressive = world_options.tool_progression & ToolProgression.option_progressive
|
||||
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
|
||||
self.assertTrue(tool_progressive)
|
||||
self.assertTrue(building_progressive)
|
||||
@@ -77,7 +77,7 @@ class TestBitFlagsProgressiveCheap(SVTestBase):
|
||||
|
||||
def test_options_are_detected_as_progressive(self):
|
||||
world_options = self.world.options
|
||||
tool_progressive = self.world.content.features.tool_progression.is_progressive
|
||||
tool_progressive = world_options.tool_progression & ToolProgression.option_progressive
|
||||
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
|
||||
self.assertTrue(tool_progressive)
|
||||
self.assertTrue(building_progressive)
|
||||
@@ -94,7 +94,7 @@ class TestBitFlagsProgressiveVeryCheap(SVTestBase):
|
||||
|
||||
def test_options_are_detected_as_progressive(self):
|
||||
world_options = self.world.options
|
||||
tool_progressive = self.world.content.features.tool_progression.is_progressive
|
||||
tool_progressive = world_options.tool_progression & ToolProgression.option_progressive
|
||||
building_progressive = world_options.building_progression & BuildingProgression.option_progressive
|
||||
self.assertTrue(tool_progressive)
|
||||
self.assertTrue(building_progressive)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user