mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-10 10:48:17 -07:00
Compare commits
2 Commits
fix-links-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ead8efbc24 | ||
|
|
aa1180e0aa |
4
.github/workflows/ctest.yml
vendored
4
.github/workflows/ctest.yml
vendored
@@ -11,7 +11,7 @@ on:
|
|||||||
- '**.hh?'
|
- '**.hh?'
|
||||||
- '**.hpp'
|
- '**.hpp'
|
||||||
- '**.hxx'
|
- '**.hxx'
|
||||||
- '**/CMakeLists.txt'
|
- '**.CMakeLists'
|
||||||
- '.github/workflows/ctest.yml'
|
- '.github/workflows/ctest.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
@@ -21,7 +21,7 @@ on:
|
|||||||
- '**.hh?'
|
- '**.hh?'
|
||||||
- '**.hpp'
|
- '**.hpp'
|
||||||
- '**.hxx'
|
- '**.hxx'
|
||||||
- '**/CMakeLists.txt'
|
- '**.CMakeLists'
|
||||||
- '.github/workflows/ctest.yml'
|
- '.github/workflows/ctest.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ class WebHostContext(Context):
|
|||||||
self.gamespackage = {"Archipelago": static_gamespackage.get("Archipelago", {})} # this may be modified by _load
|
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.item_name_groups = {"Archipelago": static_item_name_groups.get("Archipelago", {})}
|
||||||
self.location_name_groups = {"Archipelago": static_location_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", {})):
|
for game in list(multidata.get("datapackage", {})):
|
||||||
game_data = multidata["datapackage"][game]
|
game_data = multidata["datapackage"][game]
|
||||||
@@ -133,13 +132,11 @@ class WebHostContext(Context):
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
self.logger.warning(f"Did not find game_data_package for {game}: {game_data['checksum']}")
|
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.gamespackage[game] = static_gamespackage.get(game, {})
|
||||||
self.item_name_groups[game] = static_item_name_groups.get(game, {})
|
self.item_name_groups[game] = static_item_name_groups.get(game, {})
|
||||||
self.location_name_groups[game] = static_location_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
|
# all static -> use the static dicts directly
|
||||||
self.gamespackage = static_gamespackage
|
self.gamespackage = static_gamespackage
|
||||||
self.item_name_groups = static_item_name_groups
|
self.item_name_groups = static_item_name_groups
|
||||||
|
|||||||
@@ -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
|
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
|
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
|
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
|
#### 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
|
[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
|
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.
|
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.
|
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").
|
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
|
### 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 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.
|
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.
|
avoiding the need for indirect conditions at the expense of performance.
|
||||||
|
|
||||||
### Item Rules
|
### Item Rules
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.5)
|
||||||
project(ap-cpp-tests)
|
project(ap-cpp-tests)
|
||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
@@ -7,8 +7,8 @@ find_package(GTest REQUIRED)
|
|||||||
|
|
||||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
|
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
|
||||||
add_definitions("/source-charset:utf-8")
|
add_definitions("/source-charset:utf-8")
|
||||||
# set(CMAKE_CXX_FLAGS_DEBUG "/MDd") # this is the default
|
set(CMAKE_CXX_FLAGS_DEBUG "/MTd")
|
||||||
# set(CMAKE_CXX_FLAGS_RELEASE "/MD") # this is the default
|
set(CMAKE_CXX_FLAGS_RELEASE "/MT")
|
||||||
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||||
# enable static analysis for gcc
|
# enable static analysis for gcc
|
||||||
add_compile_options(-fanalyzer -Werror)
|
add_compile_options(-fanalyzer -Werror)
|
||||||
|
|||||||
@@ -3,13 +3,11 @@
|
|||||||
## Required Software
|
## Required Software
|
||||||
|
|
||||||
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
|
- [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
|
## Optional Software
|
||||||
|
|
||||||
- [Map tracker](https://github.com/TVV1GK/DS3_AP_Maptracker)
|
- Map tracker not yet updated for 3.0.0
|
||||||
|
|
||||||
## Setting Up
|
## Setting Up
|
||||||
|
|
||||||
@@ -75,65 +73,3 @@ things to keep in mind:
|
|||||||
|
|
||||||
[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0
|
[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0
|
||||||
[WINE]: https://www.winehq.org/
|
[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.
|
|
||||||
|
|||||||
@@ -235,12 +235,6 @@ class FactorioStartItems(OptionDict):
|
|||||||
"""Mapping of Factorio internal item-name to amount granted on start."""
|
"""Mapping of Factorio internal item-name to amount granted on start."""
|
||||||
display_name = "Starting Items"
|
display_name = "Starting Items"
|
||||||
default = {"burner-mining-drill": 4, "stone-furnace": 4, "raw-fish": 50}
|
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):
|
class FactorioFreeSampleBlacklist(OptionSet):
|
||||||
@@ -263,8 +257,7 @@ class AttackTrapCount(TrapCount):
|
|||||||
|
|
||||||
|
|
||||||
class TeleportTrapCount(TrapCount):
|
class TeleportTrapCount(TrapCount):
|
||||||
"""Trap items that when received trigger a random teleport.
|
"""Trap items that when received trigger a random teleport."""
|
||||||
It is ensured the player can walk back to where they got teleported from."""
|
|
||||||
display_name = "Teleport Traps"
|
display_name = "Teleport Traps"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,73 +49,6 @@ function fire_entity_at_entities(entity_name, entities, speed)
|
|||||||
end
|
end
|
||||||
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)
|
function spill_character_inventory(character)
|
||||||
if not (character and character.valid) then
|
if not (character and character.valid) then
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -134,9 +134,6 @@ end
|
|||||||
|
|
||||||
script.on_event(defines.events.on_player_changed_position, on_player_changed_position)
|
script.on_event(defines.events.on_player_changed_position, on_player_changed_position)
|
||||||
{% endif %}
|
{% 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()
|
function count_energy_bridges()
|
||||||
local count = 0
|
local count = 0
|
||||||
for i, bridge in pairs(storage.energy_link_bridges) do
|
for i, bridge in pairs(storage.energy_link_bridges) do
|
||||||
@@ -146,11 +143,9 @@ function count_energy_bridges()
|
|||||||
end
|
end
|
||||||
return count
|
return count
|
||||||
end
|
end
|
||||||
|
|
||||||
function get_energy_increment(bridge)
|
function get_energy_increment(bridge)
|
||||||
return ENERGY_INCREMENT + (ENERGY_INCREMENT * 0.3 * bridge.quality.level)
|
return ENERGY_INCREMENT + (ENERGY_INCREMENT * 0.3 * bridge.quality.level)
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_check_energy_link(event)
|
function on_check_energy_link(event)
|
||||||
--- assuming 1 MJ increment and 5MJ battery:
|
--- assuming 1 MJ increment and 5MJ battery:
|
||||||
--- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing
|
--- 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.forces["enemy"].set_evolution_factor(new_factor, "nauvis")
|
||||||
game.print({"", "New evolution factor:", new_factor})
|
game.print({"", "New evolution factor:", new_factor})
|
||||||
end,
|
end,
|
||||||
["Teleport Trap"] = function()
|
["Teleport Trap"] = function ()
|
||||||
for _, player in ipairs(game.forces["player"].players) do
|
for _, player in ipairs(game.forces["player"].players) do
|
||||||
if player.character then
|
current_character = player.character
|
||||||
attempt_teleport_player(player, 1)
|
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
|
end
|
||||||
end,
|
end,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class World:
|
|||||||
|
|
||||||
mabe_village = Location("Mabe Village")
|
mabe_village = Location("Mabe Village")
|
||||||
Location().add(HeartPiece(0x2A4)).connect(mabe_village, r.bush) # well
|
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(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(Seashell(0x0D2)).connect(mabe_village, PEGASUS_BOOTS) # smash into tree next to lv1
|
||||||
Location().add(Song(0x092)).connect(mabe_village, OCARINA) # Marins song
|
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)
|
papahl_house.connect(mamasha_trade, TRADING_ITEM_YOSHI_DOLL)
|
||||||
|
|
||||||
trendy_shop = Location("Trendy Shop")
|
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 = Location()
|
||||||
outside_trendy.connect(mabe_village, r.bush)
|
outside_trendy.connect(mabe_village, r.bush)
|
||||||
|
|
||||||
@@ -43,8 +43,8 @@ class World:
|
|||||||
self._addEntrance("start_house", mabe_village, start_house, None)
|
self._addEntrance("start_house", mabe_village, start_house, None)
|
||||||
|
|
||||||
shop = Location("Shop")
|
shop = Location("Shop")
|
||||||
Location().add(ShopItem(0)).connect(shop, OR(AND(r.can_farm, COUNT("RUPEES", 500)), SWORD))
|
Location().add(ShopItem(0)).connect(shop, OR(COUNT("RUPEES", 500), SWORD))
|
||||||
Location().add(ShopItem(1)).connect(shop, OR(AND(r.can_farm, COUNT("RUPEES", 1480)), SWORD))
|
Location().add(ShopItem(1)).connect(shop, OR(COUNT("RUPEES", 1480), SWORD))
|
||||||
self._addEntrance("shop", mabe_village, shop, None)
|
self._addEntrance("shop", mabe_village, shop, None)
|
||||||
|
|
||||||
dream_hut = Location("Dream Hut")
|
dream_hut = Location("Dream Hut")
|
||||||
@@ -164,7 +164,7 @@ class World:
|
|||||||
self._addEntrance("prairie_left_cave2", ukuku_prairie, prairie_left_cave2, BOMB)
|
self._addEntrance("prairie_left_cave2", ukuku_prairie, prairie_left_cave2, BOMB)
|
||||||
self._addEntranceRequirementExit("prairie_left_cave2", None) # if exiting, you do not need bombs
|
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))
|
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))
|
dungeon3_entrance = Location().connect(ukuku_prairie, OR(FEATHER, ROOSTER, FLIPPERS))
|
||||||
@@ -377,7 +377,7 @@ class World:
|
|||||||
|
|
||||||
# Raft game.
|
# Raft game.
|
||||||
raft_house = Location("Raft House")
|
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_upper = Location()
|
||||||
raft_return_lower = Location().connect(raft_return_upper, None, one_way=True)
|
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)
|
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:
|
class RequirementsSettings:
|
||||||
def __init__(self, options):
|
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.bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, POWER_BRACELET, BOOMERANG, BOMB)
|
||||||
self.pit_bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, BOOMERANG, BOMB) # unique
|
self.pit_bush = OR(SWORD, MAGIC_POWDER, MAGIC_ROD, BOOMERANG, BOMB) # unique
|
||||||
self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG)
|
self.attack = OR(SWORD, BOMB, BOW, MAGIC_ROD, BOOMERANG)
|
||||||
|
|||||||
@@ -110,6 +110,15 @@ class LinksAwakeningLocation(Location):
|
|||||||
add_item_rule(self, filter_item)
|
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):
|
class LinksAwakeningRegion(Region):
|
||||||
dungeon_index = None
|
dungeon_index = None
|
||||||
ladxr_region = None
|
ladxr_region = None
|
||||||
@@ -145,7 +154,9 @@ class GameStateAdapater:
|
|||||||
def get(self, item, default):
|
def get(self, item, default):
|
||||||
# Don't allow any money usage if you can't get back wasted rupees
|
# Don't allow any money usage if you can't get back wasted rupees
|
||||||
if item == "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"):
|
elif item.endswith("_USED"):
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -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]:
|
def create_random_items(world: NoitaWorld, weights: Dict[str, int], count: int) -> List[str]:
|
||||||
filler_pool = weights.copy()
|
filler_pool = weights.copy()
|
||||||
if not world.options.bad_effects:
|
if not world.options.bad_effects:
|
||||||
filler_pool["Trap"] = 0
|
del filler_pool["Trap"]
|
||||||
filler_pool["Greed Die"] = 0
|
del filler_pool["Greed Die"]
|
||||||
|
|
||||||
return world.random.choices(population=list(filler_pool.keys()),
|
return world.random.choices(population=list(filler_pool.keys()),
|
||||||
weights=list(filler_pool.values()),
|
weights=list(filler_pool.values()),
|
||||||
|
|||||||
@@ -140,14 +140,24 @@ def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_d
|
|||||||
# need sword for bosses
|
# need sword for bosses
|
||||||
if data.is_boss:
|
if data.is_boss:
|
||||||
return False
|
return False
|
||||||
if stick_bool:
|
equipment.remove("Sword")
|
||||||
equipment.remove("Sword")
|
if has_magic:
|
||||||
|
if "Magic" not in equipment:
|
||||||
|
equipment.append("Magic")
|
||||||
|
# +4 mp pretty much makes up for the lack of sword, at least in Quarry
|
||||||
|
extra_mp_needed += 4
|
||||||
|
if stick_bool:
|
||||||
|
# stick is a backup plan, and doesn't scale well, so let's require a little less
|
||||||
|
equipment.append("Stick")
|
||||||
|
extra_att_needed -= 2
|
||||||
|
else:
|
||||||
|
extra_mp_needed += 2
|
||||||
|
extra_att_needed -= 32
|
||||||
|
elif stick_bool:
|
||||||
equipment.append("Stick")
|
equipment.append("Stick")
|
||||||
# may revise this later based on feedback
|
# may revise this later based on feedback
|
||||||
extra_att_needed += 3
|
extra_att_needed += 3
|
||||||
extra_def_needed += 2
|
extra_def_needed += 2
|
||||||
# this is for when it changes over to the magic-only state if it needs to later
|
|
||||||
extra_mp_needed += 4
|
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -194,7 +204,7 @@ def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_d
|
|||||||
equip_list.append("Magic")
|
equip_list.append("Magic")
|
||||||
more_modified_stats = AreaStats(modified_stats.att_level - 32, modified_stats.def_level,
|
more_modified_stats = AreaStats(modified_stats.att_level - 32, modified_stats.def_level,
|
||||||
modified_stats.potion_level, modified_stats.hp_level,
|
modified_stats.potion_level, modified_stats.hp_level,
|
||||||
modified_stats.sp_level, modified_stats.mp_level + 2,
|
modified_stats.sp_level, modified_stats.mp_level + 4,
|
||||||
modified_stats.potion_count, equip_list, data.is_boss)
|
modified_stats.potion_count, equip_list, data.is_boss)
|
||||||
if check_combat_reqs("none", state, player, more_modified_stats):
|
if check_combat_reqs("none", state, player, more_modified_stats):
|
||||||
return True
|
return True
|
||||||
@@ -212,7 +222,7 @@ def has_required_stats(data: AreaStats, state: CollectionState, player: int) ->
|
|||||||
player_att, att_offerings = get_att_level(state, player)
|
player_att, att_offerings = get_att_level(state, player)
|
||||||
|
|
||||||
# if you have 2 more attack than needed, we can forego needing mp
|
# if you have 2 more attack than needed, we can forego needing mp
|
||||||
if data.mp_level > 1 and "Magic" in data.equipment:
|
if data.mp_level > 1:
|
||||||
if player_att < data.att_level + 2:
|
if player_att < data.att_level + 2:
|
||||||
player_mp, mp_offerings = get_mp_level(state, player)
|
player_mp, mp_offerings = get_mp_level(state, player)
|
||||||
if player_mp < data.mp_level:
|
if player_mp < data.mp_level:
|
||||||
|
|||||||
@@ -990,9 +990,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||||||
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world))
|
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world))
|
||||||
|
|
||||||
monastery_front_to_back = regions["Monastery Front"].connect(
|
monastery_front_to_back = regions["Monastery Front"].connect(
|
||||||
connecting_region=regions["Monastery Back"],
|
connecting_region=regions["Monastery Back"])
|
||||||
rule=lambda state: has_sword(state, player) or state.has(fire_wand, player)
|
|
||||||
or laurels_zip(state, world))
|
|
||||||
# laurels through the gate, no setup needed
|
# laurels through the gate, no setup needed
|
||||||
regions["Monastery Back"].connect(
|
regions["Monastery Back"].connect(
|
||||||
connecting_region=regions["Monastery Front"],
|
connecting_region=regions["Monastery Front"],
|
||||||
@@ -1834,7 +1832,7 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
|||||||
if world.options.combat_logic == CombatLogic.option_on:
|
if world.options.combat_logic == CombatLogic.option_on:
|
||||||
combat_logic_to_loc("Overworld - [Northeast] Flowers Holy Cross", "Garden Knight")
|
combat_logic_to_loc("Overworld - [Northeast] Flowers Holy Cross", "Garden Knight")
|
||||||
combat_logic_to_loc("Overworld - [Northwest] Chest Near Quarry Gate", "Before Well", dagger=True)
|
combat_logic_to_loc("Overworld - [Northwest] Chest Near Quarry Gate", "Before Well", dagger=True)
|
||||||
combat_logic_to_loc("Overworld - [Northeast] Chest Above Patrol Cave", "West Garden", dagger=True)
|
combat_logic_to_loc("Overworld - [Northeast] Chest Above Patrol Cave", "Garden Knight", dagger=True)
|
||||||
combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret", "Overworld", dagger=True)
|
combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret", "Overworld", dagger=True)
|
||||||
combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret 2", "Overworld")
|
combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret 2", "Overworld")
|
||||||
combat_logic_to_loc("Overworld - [Southwest] Bombable Wall Near Fountain", "Before Well", dagger=True)
|
combat_logic_to_loc("Overworld - [Southwest] Bombable Wall Near Fountain", "Before Well", dagger=True)
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ slot_data_item_names = [
|
|||||||
|
|
||||||
combat_items: List[str] = [name for name, data in item_table.items()
|
combat_items: List[str] = [name for name, data in item_table.items()
|
||||||
if data.combat_ic and IC.progression in data.combat_ic]
|
if data.combat_ic and IC.progression in data.combat_ic]
|
||||||
combat_items.extend(["Stick", "Sword", "Sword Upgrade", "Magic Wand", "Hero's Laurels", "Gun"])
|
combat_items.extend(["Stick", "Sword", "Sword Upgrade", "Magic Wand", "Hero's Laurels"])
|
||||||
|
|
||||||
item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()}
|
item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()}
|
||||||
|
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ location_table: Dict[str, TunicLocationData] = {
|
|||||||
"Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", location_group="Holy Cross"),
|
"Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", location_group="Holy Cross"),
|
||||||
"Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", location_group="Holy Cross"),
|
"Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", location_group="Holy Cross"),
|
||||||
"Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", location_group="Holy Cross"),
|
"Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", location_group="Holy Cross"),
|
||||||
"Monastery - Monastery Chest": TunicLocationData("Monastery Back", "Monastery Back"),
|
"Monastery - Monastery Chest": TunicLocationData("Monastery", "Monastery Back"),
|
||||||
"Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="Holy Cross"),
|
"Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="Holy Cross"),
|
||||||
"Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"),
|
"Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"),
|
||||||
"Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry Back", "Quarry Back"),
|
"Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry Back", "Quarry Back"),
|
||||||
@@ -220,12 +220,12 @@ location_table: Dict[str, TunicLocationData] = {
|
|||||||
"Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"),
|
"Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"),
|
||||||
"Quarry - [Central] Top Floor Overhang": TunicLocationData("Quarry", "Quarry"),
|
"Quarry - [Central] Top Floor Overhang": TunicLocationData("Quarry", "Quarry"),
|
||||||
"Quarry - [East] Near Bridge": TunicLocationData("Quarry", "Quarry"),
|
"Quarry - [East] Near Bridge": TunicLocationData("Quarry", "Quarry"),
|
||||||
"Quarry - [Central] Above Ladder": TunicLocationData("Monastery", "Quarry Monastery Entry"),
|
"Quarry - [Central] Above Ladder": TunicLocationData("Quarry", "Quarry Monastery Entry"),
|
||||||
"Quarry - [Central] Obscured Behind Staircase": TunicLocationData("Quarry", "Quarry"),
|
"Quarry - [Central] Obscured Behind Staircase": TunicLocationData("Quarry", "Quarry"),
|
||||||
"Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Monastery", "Quarry Monastery Entry"),
|
"Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Quarry", "Quarry Monastery Entry"),
|
||||||
"Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"),
|
"Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"),
|
||||||
"Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"),
|
"Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"),
|
||||||
"Hero's Grave - Ash Relic": TunicLocationData("Monastery Back", "Hero Relic - Quarry"),
|
"Hero's Grave - Ash Relic": TunicLocationData("Monastery", "Hero Relic - Quarry"),
|
||||||
"Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"),
|
"Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"),
|
||||||
"Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"),
|
"Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"),
|
||||||
"Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"),
|
"Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"),
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ tunic_regions: dict[str, tuple[str]] = {
|
|||||||
"Library": tuple(),
|
"Library": tuple(),
|
||||||
"Eastern Vault Fortress": ("Beneath the Vault",),
|
"Eastern Vault Fortress": ("Beneath the Vault",),
|
||||||
"Beneath the Vault": ("Eastern Vault Fortress",),
|
"Beneath the Vault": ("Eastern Vault Fortress",),
|
||||||
"Quarry Back": ("Quarry", "Monastery"),
|
"Quarry Back": ("Quarry",),
|
||||||
"Quarry": ("Monastery", "Lower Quarry"),
|
"Quarry": ("Monastery", "Lower Quarry"),
|
||||||
"Monastery": ("Monastery Back",),
|
"Monastery": tuple(),
|
||||||
"Monastery Back": tuple(),
|
|
||||||
"Lower Quarry": ("Rooted Ziggurat",),
|
"Lower Quarry": ("Rooted Ziggurat",),
|
||||||
"Rooted Ziggurat": tuple(),
|
"Rooted Ziggurat": tuple(),
|
||||||
"Swamp": ("Cathedral",),
|
"Swamp": ("Cathedral",),
|
||||||
|
|||||||
@@ -124,11 +124,6 @@ def set_region_rules(world: "TunicWorld") -> None:
|
|||||||
and (state.has_any({grapple, laurels, gun}, player) or can_ladder_storage(state, world))
|
and (state.has_any({grapple, laurels, gun}, player) or can_ladder_storage(state, world))
|
||||||
world.get_entrance("Quarry Back -> Quarry").access_rule = \
|
world.get_entrance("Quarry Back -> Quarry").access_rule = \
|
||||||
lambda state: has_sword(state, player) or state.has(fire_wand, player)
|
lambda state: has_sword(state, player) or state.has(fire_wand, player)
|
||||||
world.get_entrance("Quarry Back -> Monastery").access_rule = \
|
|
||||||
lambda state: state.has(laurels, player)
|
|
||||||
world.get_entrance("Monastery -> Monastery Back").access_rule = \
|
|
||||||
lambda state: (has_sword(state, player) or state.has(fire_wand, player)
|
|
||||||
or laurels_zip(state, world))
|
|
||||||
world.get_entrance("Quarry -> Lower Quarry").access_rule = \
|
world.get_entrance("Quarry -> Lower Quarry").access_rule = \
|
||||||
lambda state: has_mask(state, world)
|
lambda state: has_mask(state, world)
|
||||||
world.get_entrance("Lower Quarry -> Rooted Ziggurat").access_rule = \
|
world.get_entrance("Lower Quarry -> Rooted Ziggurat").access_rule = \
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from collections import Counter
|
|||||||
from . import TunicTestBase
|
from . import TunicTestBase
|
||||||
from .. import options
|
from .. import options
|
||||||
from ..combat_logic import (check_combat_reqs, area_data, get_money_count, calc_effective_hp, get_potion_level,
|
from ..combat_logic import (check_combat_reqs, area_data, get_money_count, calc_effective_hp, get_potion_level,
|
||||||
get_hp_level, get_def_level, get_sp_level, has_combat_reqs)
|
get_hp_level, get_def_level, get_sp_level)
|
||||||
from ..items import item_table
|
from ..items import item_table
|
||||||
from .. import TunicWorld
|
from .. import TunicWorld
|
||||||
|
|
||||||
@@ -81,39 +81,3 @@ class TestCombat(TunicTestBase):
|
|||||||
f"Free Def and Offerings: {player_def - def_offerings}, {def_offerings}\n"
|
f"Free Def and Offerings: {player_def - def_offerings}, {def_offerings}\n"
|
||||||
f"Free SP and Offerings: {player_sp - sp_offerings}, {sp_offerings}")
|
f"Free SP and Offerings: {player_sp - sp_offerings}, {sp_offerings}")
|
||||||
prev_statuses[area] = curr_statuses[area]
|
prev_statuses[area] = curr_statuses[area]
|
||||||
|
|
||||||
# the issue was that a direct check of the logic and the cache had different results
|
|
||||||
# it was actually due to the combat_items in items.py not having the Gun in it
|
|
||||||
# but this test is still helpful for verifying the cache
|
|
||||||
def test_combat_magic_weapons(self):
|
|
||||||
combat_items = self.combat_items.copy()
|
|
||||||
combat_items.remove("Magic Wand")
|
|
||||||
combat_items.remove("Gun")
|
|
||||||
area_names = list(area_data.keys())
|
|
||||||
self.multiworld.worlds[1].random.shuffle(combat_items)
|
|
||||||
self.multiworld.worlds[1].random.shuffle(area_names)
|
|
||||||
current_items = Counter()
|
|
||||||
state = self.multiworld.state.copy()
|
|
||||||
player = self.player
|
|
||||||
gun = TunicWorld.create_item(self.world, "Gun")
|
|
||||||
|
|
||||||
for current_item_name in combat_items:
|
|
||||||
current_item = TunicWorld.create_item(self.world, current_item_name)
|
|
||||||
state.collect(current_item)
|
|
||||||
current_items[current_item_name] += 1
|
|
||||||
for area in area_names:
|
|
||||||
if check_combat_reqs(area, state, player) != has_combat_reqs(area, state, player):
|
|
||||||
raise Exception(f"Cache for {area} does not match a direct check "
|
|
||||||
f"after collecting {current_item_name}.\n"
|
|
||||||
f"Current items: {current_items}.\n"
|
|
||||||
f"Cache {'succeeded' if has_combat_reqs(area, state, player) else 'failed'}\n"
|
|
||||||
f"Direct {'succeeded' if check_combat_reqs(area, state, player) else 'failed'}")
|
|
||||||
state.collect(gun)
|
|
||||||
for area in area_names:
|
|
||||||
if check_combat_reqs(area, state, player) != has_combat_reqs(area, state, player):
|
|
||||||
raise Exception(f"Cache for {area} does not match a direct check "
|
|
||||||
f"after collecting the Gun.\n"
|
|
||||||
f"Current items: {current_items}.\n"
|
|
||||||
f"Cache {'succeeded' if has_combat_reqs(area, state, player) else 'failed'}\n"
|
|
||||||
f"Direct {'succeeded' if check_combat_reqs(area, state, player) else 'failed'}")
|
|
||||||
state.remove(gun)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user