mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-23 11:23:22 -07:00
Compare commits
11 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2404e18c03 | ||
|
|
6dc461609b | ||
|
|
58d460678e | ||
|
|
0f7fd48cdd | ||
|
|
18de035b4d | ||
|
|
11fa43f0a4 | ||
|
|
91a8fc91d6 | ||
|
|
15bde56551 | ||
|
|
d744e086ef | ||
|
|
378fa5d5c4 | ||
|
|
8349774c5c |
4
.github/workflows/ctest.yml
vendored
4
.github/workflows/ctest.yml
vendored
@@ -11,7 +11,7 @@ on:
|
|||||||
- '**.hh?'
|
- '**.hh?'
|
||||||
- '**.hpp'
|
- '**.hpp'
|
||||||
- '**.hxx'
|
- '**.hxx'
|
||||||
- '**.CMakeLists'
|
- '**/CMakeLists.txt'
|
||||||
- '.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'
|
- '**/CMakeLists.txt'
|
||||||
- '.github/workflows/ctest.yml'
|
- '.github/workflows/ctest.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ 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]
|
||||||
@@ -132,11 +133,13 @@ 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:
|
if not game_data_packages and not missing_checksum:
|
||||||
# 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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
cmake_minimum_required(VERSION 3.5)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
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 "/MTd")
|
# set(CMAKE_CXX_FLAGS_DEBUG "/MDd") # this is the default
|
||||||
set(CMAKE_CXX_FLAGS_RELEASE "/MT")
|
# set(CMAKE_CXX_FLAGS_RELEASE "/MD") # this is the default
|
||||||
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,11 +3,13 @@
|
|||||||
## 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](https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest)
|
- [Dark Souls III AP Client]
|
||||||
|
|
||||||
|
[Dark Souls III AP Client]: https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest
|
||||||
|
|
||||||
## Optional Software
|
## Optional Software
|
||||||
|
|
||||||
- Map tracker not yet updated for 3.0.0
|
- [Map tracker](https://github.com/TVV1GK/DS3_AP_Maptracker)
|
||||||
|
|
||||||
## Setting Up
|
## Setting Up
|
||||||
|
|
||||||
@@ -73,3 +75,65 @@ 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,6 +235,12 @@ 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):
|
||||||
@@ -257,7 +263,8 @@ 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,6 +49,73 @@ 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,6 +134,9 @@ 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
|
||||||
@@ -143,9 +146,11 @@ 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
|
||||||
@@ -722,12 +727,10 @@ 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
|
||||||
current_character = player.character
|
if player.character then
|
||||||
if current_character ~= nil then
|
attempt_teleport_player(player, 1)
|
||||||
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.bush, COUNT("RUPEES", 20))) # fishing game, heart piece is directly done by the minigame.
|
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(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)), FOUND("RUPEES", 50))
|
trendy_shop.connect(Location().add(TradeSequenceItem(0x2A0, TRADING_ITEM_YOSHI_DOLL)), AND(r.can_farm, 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(COUNT("RUPEES", 500), SWORD))
|
Location().add(ShopItem(0)).connect(shop, OR(AND(r.can_farm, COUNT("RUPEES", 500)), SWORD))
|
||||||
Location().add(ShopItem(1)).connect(shop, OR(COUNT("RUPEES", 1480), SWORD))
|
Location().add(ShopItem(1)).connect(shop, OR(AND(r.can_farm, 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, COUNT("RUPEES", 1480)))
|
mamu = Location().connect(Location().add(Song(0x2FB)), AND(OCARINA, r.can_farm, 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.bush, COUNT("RUPEES", 100))) # add bush requirement for farming in case player has to try again
|
Location().add(KeyLocation("RAFT")).connect(raft_house, AND(r.can_farm, COUNT("RUPEES", 100)))
|
||||||
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,6 +253,7 @@ 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,15 +110,6 @@ 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
|
||||||
@@ -154,9 +145,7 @@ 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":
|
||||||
if can_farm_rupees(self.state, self.player):
|
return self.state.prog_items[self.player]["RUPEES"]
|
||||||
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:
|
||||||
del filler_pool["Trap"]
|
filler_pool["Trap"] = 0
|
||||||
del filler_pool["Greed Die"]
|
filler_pool["Greed Die"] = 0
|
||||||
|
|
||||||
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,24 +140,14 @@ 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
|
||||||
equipment.remove("Sword")
|
if stick_bool:
|
||||||
if has_magic:
|
equipment.remove("Sword")
|
||||||
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
|
||||||
|
|
||||||
@@ -204,7 +194,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 + 4,
|
modified_stats.sp_level, modified_stats.mp_level + 2,
|
||||||
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
|
||||||
@@ -222,7 +212,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:
|
if data.mp_level > 1 and "Magic" in data.equipment:
|
||||||
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,7 +990,9 @@ 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"],
|
||||||
@@ -1832,7 +1834,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", "Garden Knight", dagger=True)
|
combat_logic_to_loc("Overworld - [Northeast] Chest Above Patrol Cave", "West Garden", 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"])
|
combat_items.extend(["Stick", "Sword", "Sword Upgrade", "Magic Wand", "Hero's Laurels", "Gun"])
|
||||||
|
|
||||||
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", "Monastery Back"),
|
"Monastery - Monastery Chest": TunicLocationData("Monastery Back", "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("Quarry", "Quarry Monastery Entry"),
|
"Quarry - [Central] Above Ladder": TunicLocationData("Monastery", "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("Quarry", "Quarry Monastery Entry"),
|
"Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Monastery", "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", "Hero Relic - Quarry"),
|
"Hero's Grave - Ash Relic": TunicLocationData("Monastery Back", "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,9 +13,10 @@ 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",),
|
"Quarry Back": ("Quarry", "Monastery"),
|
||||||
"Quarry": ("Monastery", "Lower Quarry"),
|
"Quarry": ("Monastery", "Lower Quarry"),
|
||||||
"Monastery": tuple(),
|
"Monastery": ("Monastery Back",),
|
||||||
|
"Monastery Back": tuple(),
|
||||||
"Lower Quarry": ("Rooted Ziggurat",),
|
"Lower Quarry": ("Rooted Ziggurat",),
|
||||||
"Rooted Ziggurat": tuple(),
|
"Rooted Ziggurat": tuple(),
|
||||||
"Swamp": ("Cathedral",),
|
"Swamp": ("Cathedral",),
|
||||||
|
|||||||
@@ -124,6 +124,11 @@ 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)
|
get_hp_level, get_def_level, get_sp_level, has_combat_reqs)
|
||||||
from ..items import item_table
|
from ..items import item_table
|
||||||
from .. import TunicWorld
|
from .. import TunicWorld
|
||||||
|
|
||||||
@@ -81,3 +81,39 @@ 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)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ GENERAL_LOCATIONS = {
|
|||||||
"Outside Tutorial Outpost Entry Panel",
|
"Outside Tutorial Outpost Entry Panel",
|
||||||
"Outside Tutorial Outpost Exit Panel",
|
"Outside Tutorial Outpost Exit Panel",
|
||||||
|
|
||||||
|
"Glass Factory Entry Panel",
|
||||||
"Glass Factory Discard",
|
"Glass Factory Discard",
|
||||||
"Glass Factory Back Wall 5",
|
"Glass Factory Back Wall 5",
|
||||||
"Glass Factory Front 3",
|
"Glass Factory Front 3",
|
||||||
|
|||||||
Reference in New Issue
Block a user