mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-09 09:03:46 -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?'
|
||||
- '**.hpp'
|
||||
- '**.hxx'
|
||||
- '**.CMakeLists'
|
||||
- '**/CMakeLists.txt'
|
||||
- '.github/workflows/ctest.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
@@ -21,7 +21,7 @@ on:
|
||||
- '**.hh?'
|
||||
- '**.hpp'
|
||||
- '**.hxx'
|
||||
- '**.CMakeLists'
|
||||
- '**/CMakeLists.txt'
|
||||
- '.github/workflows/ctest.yml'
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -117,6 +117,7 @@ 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]
|
||||
@@ -132,11 +133,13 @@ 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:
|
||||
if not game_data_packages and not missing_checksum:
|
||||
# all static -> use the static dicts directly
|
||||
self.gamespackage = static_gamespackage
|
||||
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)
|
||||
|
||||
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 "/MTd")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "/MT")
|
||||
# set(CMAKE_CXX_FLAGS_DEBUG "/MDd") # this is the default
|
||||
# set(CMAKE_CXX_FLAGS_RELEASE "/MD") # this is the default
|
||||
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
# enable static analysis for gcc
|
||||
add_compile_options(-fanalyzer -Werror)
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
## Required Software
|
||||
|
||||
- [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
|
||||
|
||||
- Map tracker not yet updated for 3.0.0
|
||||
- [Map tracker](https://github.com/TVV1GK/DS3_AP_Maptracker)
|
||||
|
||||
## Setting Up
|
||||
|
||||
@@ -73,3 +75,65 @@ 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.
|
||||
|
||||
@@ -235,6 +235,12 @@ 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):
|
||||
@@ -257,7 +263,8 @@ class AttackTrapCount(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"
|
||||
|
||||
|
||||
|
||||
@@ -49,6 +49,73 @@ 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,6 +134,9 @@ 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
|
||||
@@ -143,9 +146,11 @@ 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
|
||||
@@ -722,12 +727,10 @@ 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
|
||||
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))
|
||||
if player.character then
|
||||
attempt_teleport_player(player, 1)
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
@@ -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.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(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)), 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.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(COUNT("RUPEES", 500), SWORD))
|
||||
Location().add(ShopItem(1)).connect(shop, OR(COUNT("RUPEES", 1480), SWORD))
|
||||
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))
|
||||
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, 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))
|
||||
|
||||
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.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_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,6 +253,7 @@ 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)
|
||||
|
||||
@@ -110,15 +110,6 @@ 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
|
||||
@@ -154,9 +145,7 @@ class GameStateAdapater:
|
||||
def get(self, item, default):
|
||||
# Don't allow any money usage if you can't get back wasted rupees
|
||||
if item == "RUPEES":
|
||||
if can_farm_rupees(self.state, self.player):
|
||||
return self.state.prog_items[self.player]["RUPEES"]
|
||||
return 0
|
||||
return self.state.prog_items[self.player]["RUPEES"]
|
||||
elif item.endswith("_USED"):
|
||||
return 0
|
||||
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]:
|
||||
filler_pool = weights.copy()
|
||||
if not world.options.bad_effects:
|
||||
del filler_pool["Trap"]
|
||||
del filler_pool["Greed Die"]
|
||||
filler_pool["Trap"] = 0
|
||||
filler_pool["Greed Die"] = 0
|
||||
|
||||
return world.random.choices(population=list(filler_pool.keys()),
|
||||
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
|
||||
if data.is_boss:
|
||||
return False
|
||||
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:
|
||||
if stick_bool:
|
||||
equipment.remove("Sword")
|
||||
equipment.append("Stick")
|
||||
# may revise this later based on feedback
|
||||
extra_att_needed += 3
|
||||
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:
|
||||
return False
|
||||
|
||||
@@ -204,7 +194,7 @@ def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_d
|
||||
equip_list.append("Magic")
|
||||
more_modified_stats = AreaStats(modified_stats.att_level - 32, modified_stats.def_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)
|
||||
if check_combat_reqs("none", state, player, more_modified_stats):
|
||||
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)
|
||||
|
||||
# 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:
|
||||
player_mp, mp_offerings = get_mp_level(state, player)
|
||||
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))
|
||||
|
||||
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
|
||||
regions["Monastery Back"].connect(
|
||||
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:
|
||||
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 - [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 2", "Overworld")
|
||||
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()
|
||||
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()}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
"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"),
|
||||
"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] Chest": 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] Top Floor Overhang": 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] 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 - [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] Near 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(),
|
||||
"Eastern Vault Fortress": ("Beneath the Vault",),
|
||||
"Beneath the Vault": ("Eastern Vault Fortress",),
|
||||
"Quarry Back": ("Quarry",),
|
||||
"Quarry Back": ("Quarry", "Monastery"),
|
||||
"Quarry": ("Monastery", "Lower Quarry"),
|
||||
"Monastery": tuple(),
|
||||
"Monastery": ("Monastery Back",),
|
||||
"Monastery Back": tuple(),
|
||||
"Lower Quarry": ("Rooted Ziggurat",),
|
||||
"Rooted Ziggurat": tuple(),
|
||||
"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))
|
||||
world.get_entrance("Quarry Back -> Quarry").access_rule = \
|
||||
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 = \
|
||||
lambda state: has_mask(state, world)
|
||||
world.get_entrance("Lower Quarry -> Rooted Ziggurat").access_rule = \
|
||||
|
||||
@@ -4,7 +4,7 @@ from collections import Counter
|
||||
from . import TunicTestBase
|
||||
from .. import options
|
||||
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 .. import TunicWorld
|
||||
|
||||
@@ -81,3 +81,39 @@ class TestCombat(TunicTestBase):
|
||||
f"Free Def and Offerings: {player_def - def_offerings}, {def_offerings}\n"
|
||||
f"Free SP and Offerings: {player_sp - sp_offerings}, {sp_offerings}")
|
||||
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 Exit Panel",
|
||||
|
||||
"Glass Factory Entry Panel",
|
||||
"Glass Factory Discard",
|
||||
"Glass Factory Back Wall 5",
|
||||
"Glass Factory Front 3",
|
||||
|
||||
Reference in New Issue
Block a user