From 1c42bdb3537f59ab25e5af171d1baadbde74e926 Mon Sep 17 00:00:00 2001
From: massimilianodelliubaldini
<8584296+massimilianodelliubaldini@users.noreply.github.com>
Date: Thu, 27 Jun 2024 10:12:48 -0400
Subject: [PATCH] Move Randomizer (#26)
* Finally remove debug-segment text, update Python imports to relative paths.
* HUGE refactor to Regions/Rules to support move rando, first hub area coded.
* More refactoring.
* Another refactor - may squash.
* Fix some Rules, reuse some code by returning key regions from build_regions.
* More regions added. A couple of TODOs.
* Fixed trade logic, added LPC regions.
* Added Spider, Snowy, Boggy. Fixed Misty's orbs.
* Fix circular import, assert orb counts per level, fix a few naming errors.
* Citadel added, missing locs and connections fixed. First move rando seed generated.
* Add Move Rando to Options class.
* Fixed rules for prerequisite moves.
* Implement client functionality for move rando, add blurbs to game info page.
* Fix wrong address for cache checks.
* Fix byte alignment of offsets, refactor read_memory for better code reuse.
* Refactor memory offsets and add some unit tests.
* Make green eco the filler item, also define a maximum ID. Fix Boggy tether locations.
---
worlds/jakanddaxter/Client.py | 6 +-
worlds/jakanddaxter/GameID.py | 3 +
worlds/jakanddaxter/Items.py | 40 ++-
worlds/jakanddaxter/JakAndDaxterOptions.py | 21 +-
worlds/jakanddaxter/Locations.py | 6 +-
worlds/jakanddaxter/Regions.py | 335 ++++--------------
worlds/jakanddaxter/Rules.py | 288 ++-------------
worlds/jakanddaxter/__init__.py | 65 ++--
worlds/jakanddaxter/client/MemoryReader.py | 132 ++++---
worlds/jakanddaxter/client/ReplClient.py | 67 +++-
.../en_Jak and Daxter The Precursor Legacy.md | 60 +++-
worlds/jakanddaxter/docs/setup_en.md | 4 +-
worlds/jakanddaxter/locs/OrbCacheLocations.py | 50 +++
worlds/jakanddaxter/locs/OrbLocations.py | 4 +-
worlds/jakanddaxter/regs/BoggySwampRegions.py | 154 ++++++++
worlds/jakanddaxter/regs/FireCanyonRegions.py | 17 +
.../regs/ForbiddenJungleRegions.py | 83 +++++
worlds/jakanddaxter/regs/GeyserRockRegions.py | 26 ++
.../regs/GolAndMaiasCitadelRegions.py | 122 +++++++
worlds/jakanddaxter/regs/LavaTubeRegions.py | 17 +
.../regs/LostPrecursorCityRegions.py | 130 +++++++
.../jakanddaxter/regs/MistyIslandRegions.py | 116 ++++++
.../jakanddaxter/regs/MountainPassRegions.py | 34 ++
.../regs/PrecursorBasinRegions.py | 17 +
worlds/jakanddaxter/regs/RegionBase.py | 69 ++++
.../jakanddaxter/regs/RockVillageRegions.py | 63 ++++
.../regs/SandoverVillageRegions.py | 71 ++++
.../jakanddaxter/regs/SentinelBeachRegions.py | 85 +++++
.../jakanddaxter/regs/SnowyMountainRegions.py | 181 ++++++++++
worlds/jakanddaxter/regs/SpiderCaveRegions.py | 110 ++++++
.../regs/VolcanicCraterRegions.py | 38 ++
worlds/jakanddaxter/test/__init__.py | 91 +++++
worlds/jakanddaxter/test/test_locations.py | 42 +++
33 files changed, 1902 insertions(+), 645 deletions(-)
create mode 100644 worlds/jakanddaxter/locs/OrbCacheLocations.py
create mode 100644 worlds/jakanddaxter/regs/BoggySwampRegions.py
create mode 100644 worlds/jakanddaxter/regs/FireCanyonRegions.py
create mode 100644 worlds/jakanddaxter/regs/ForbiddenJungleRegions.py
create mode 100644 worlds/jakanddaxter/regs/GeyserRockRegions.py
create mode 100644 worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py
create mode 100644 worlds/jakanddaxter/regs/LavaTubeRegions.py
create mode 100644 worlds/jakanddaxter/regs/LostPrecursorCityRegions.py
create mode 100644 worlds/jakanddaxter/regs/MistyIslandRegions.py
create mode 100644 worlds/jakanddaxter/regs/MountainPassRegions.py
create mode 100644 worlds/jakanddaxter/regs/PrecursorBasinRegions.py
create mode 100644 worlds/jakanddaxter/regs/RegionBase.py
create mode 100644 worlds/jakanddaxter/regs/RockVillageRegions.py
create mode 100644 worlds/jakanddaxter/regs/SandoverVillageRegions.py
create mode 100644 worlds/jakanddaxter/regs/SentinelBeachRegions.py
create mode 100644 worlds/jakanddaxter/regs/SnowyMountainRegions.py
create mode 100644 worlds/jakanddaxter/regs/SpiderCaveRegions.py
create mode 100644 worlds/jakanddaxter/regs/VolcanicCraterRegions.py
create mode 100644 worlds/jakanddaxter/test/__init__.py
create mode 100644 worlds/jakanddaxter/test/test_locations.py
diff --git a/worlds/jakanddaxter/Client.py b/worlds/jakanddaxter/Client.py
index 3612946548..397f63e7ca 100644
--- a/worlds/jakanddaxter/Client.py
+++ b/worlds/jakanddaxter/Client.py
@@ -11,9 +11,9 @@ import Utils
from NetUtils import ClientStatus
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled
-from worlds.jakanddaxter.GameID import jak1_name
-from worlds.jakanddaxter.client.ReplClient import JakAndDaxterReplClient
-from worlds.jakanddaxter.client.MemoryReader import JakAndDaxterMemoryReader
+from .GameID import jak1_name
+from .client.ReplClient import JakAndDaxterReplClient
+from .client.MemoryReader import JakAndDaxterMemoryReader
import ModuleUpdate
ModuleUpdate.update()
diff --git a/worlds/jakanddaxter/GameID.py b/worlds/jakanddaxter/GameID.py
index 555be696af..85dd32a213 100644
--- a/worlds/jakanddaxter/GameID.py
+++ b/worlds/jakanddaxter/GameID.py
@@ -1,5 +1,8 @@
# All Jak And Daxter Archipelago IDs must be offset by this number.
jak1_id = 741000000
+# This is maximum ID we will allow.
+jak1_max = jak1_id + 999999
+
# The name of the game.
jak1_name = "Jak and Daxter The Precursor Legacy"
diff --git a/worlds/jakanddaxter/Items.py b/worlds/jakanddaxter/Items.py
index 56743b7cef..4f94e7b85c 100644
--- a/worlds/jakanddaxter/Items.py
+++ b/worlds/jakanddaxter/Items.py
@@ -1,6 +1,10 @@
from BaseClasses import Item
-from .GameID import jak1_name
-from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials
+from .GameID import jak1_name, jak1_max
+from .locs import (OrbLocations as Orbs,
+ CellLocations as Cells,
+ ScoutLocations as Scouts,
+ SpecialLocations as Specials,
+ OrbCacheLocations as Caches)
class JakAndDaxterItem(Item):
@@ -34,13 +38,14 @@ scout_item_table = {
91: "Scout Fly - GMC",
}
-# TODO - Orbs are also generic and interchangeable.
-# orb_item_table = {
-# ???: "Precursor Orb",
-# }
+# Orbs are also generic and interchangeable.
+orb_item_table = {
+ 1: "Precursor Orb",
+}
# These are special items representing unique unlocks in the world. Notice that their Item ID equals their
# respective Location ID. Like scout flies, this is necessary for game<->archipelago communication.
+# TODO - These numbers of checks may be inaccurate post-region refactor.
special_item_table = {
5: "Fisherman's Boat", # Unlocks 14 checks in Misty Island
4: "Jungle Elevator", # Unlocks 2 checks in Forbidden Jungle
@@ -56,11 +61,32 @@ special_item_table = {
70: "Freed The Green Sage", # Unlocks the final elevator
}
+# These are the move items for move randomizer. Notice that their Item ID equals some of the Orb Cache Location ID's.
+# This was 100% arbitrary. There's no reason to tie moves to orb caches except that I need a place to put them. ;_;
+move_item_table = {
+ 10344: "Crouch",
+ 10369: "Crouch Jump",
+ 11072: "Crouch Uppercut",
+ 12634: "Roll",
+ 12635: "Roll Jump",
+ 10945: "Double Jump",
+ 14507: "Jump Dive",
+ 14838: "Jump Kick",
+ 23348: "Punch",
+ 23349: "Punch Uppercut",
+ 23350: "Kick",
+ # 24038: "Orb Cache at End of Blast Furnace", # TODO - IDK, we didn't need all of the orb caches for move rando.
+ # 24039: "Orb Cache at End of Launch Pad Room",
+ # 24040: "Orb Cache at Start of Launch Pad Room",
+}
+
# All Items
# While we're here, do all the ID conversions needed.
item_table = {
**{Cells.to_ap_id(k): cell_item_table[k] for k in cell_item_table},
**{Scouts.to_ap_id(k): scout_item_table[k] for k in scout_item_table},
- # **{Orbs.to_ap_id(k): orb_item_table[k] for k in orb_item_table},
+ **{Orbs.to_ap_id(k): orb_item_table[k] for k in orb_item_table},
**{Specials.to_ap_id(k): special_item_table[k] for k in special_item_table},
+ **{Caches.to_ap_id(k): move_item_table[k] for k in move_item_table},
+ jak1_max: "Green Eco Pill" # Filler item.
}
diff --git a/worlds/jakanddaxter/JakAndDaxterOptions.py b/worlds/jakanddaxter/JakAndDaxterOptions.py
index cfed39815c..de5581f4c9 100644
--- a/worlds/jakanddaxter/JakAndDaxterOptions.py
+++ b/worlds/jakanddaxter/JakAndDaxterOptions.py
@@ -3,18 +3,21 @@ from dataclasses import dataclass
from Options import Toggle, PerGameCommonOptions
-# class EnableScoutFlies(Toggle):
-# """Enable to include each Scout Fly as a check. Adds 112 checks to the pool."""
-# display_name = "Enable Scout Flies"
+class EnableMoveRandomizer(Toggle):
+ """Enable to include movement options as items in the randomizer.
+ Jak is only able to run, swim, and single jump, until you find his other moves.
+ Adds 11 items to the pool."""
+ display_name = "Enable Move Randomizer"
-# class EnablePrecursorOrbs(Toggle):
-# """Enable to include each Precursor Orb as a check. Adds 2000 checks to the pool."""
-# display_name = "Enable Precursor Orbs"
+# class EnableOrbsanity(Toggle):
+# """Enable to include Precursor Orbs as an ordered list of progressive checks.
+# Each orb you collect triggers the next release in the list.
+# Adds 2000 items to the pool."""
+# display_name = "Enable Orbsanity"
@dataclass
class JakAndDaxterOptions(PerGameCommonOptions):
- # enable_scout_flies: EnableScoutFlies
- # enable_precursor_orbs: EnablePrecursorOrbs
- pass
+ enable_move_randomizer: EnableMoveRandomizer
+ # enable_orbsanity: EnableOrbsanity
diff --git a/worlds/jakanddaxter/Locations.py b/worlds/jakanddaxter/Locations.py
index ac18ce84c8..3cea4f2ace 100644
--- a/worlds/jakanddaxter/Locations.py
+++ b/worlds/jakanddaxter/Locations.py
@@ -1,6 +1,9 @@
from BaseClasses import Location
from .GameID import jak1_name
-from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials
+from .locs import (CellLocations as Cells,
+ ScoutLocations as Scouts,
+ SpecialLocations as Specials,
+ OrbCacheLocations as Caches)
class JakAndDaxterLocation(Location):
@@ -44,4 +47,5 @@ location_table = {
**{Scouts.to_ap_id(k): Scouts.locLT_scoutTable[k] for k in Scouts.locLT_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locGMC_scoutTable[k] for k in Scouts.locGMC_scoutTable},
**{Specials.to_ap_id(k): Specials.loc_specialTable[k] for k in Specials.loc_specialTable},
+ **{Caches.to_ap_id(k): Caches.loc_orbCacheTable[k] for k in Caches.loc_orbCacheTable},
}
diff --git a/worlds/jakanddaxter/Regions.py b/worlds/jakanddaxter/Regions.py
index 44490c9bc4..4e4ca73b32 100644
--- a/worlds/jakanddaxter/Regions.py
+++ b/worlds/jakanddaxter/Regions.py
@@ -1,278 +1,81 @@
import typing
-from enum import Enum, auto
-from BaseClasses import MultiWorld, Region
-from .GameID import jak1_name
+from BaseClasses import MultiWorld
+from .Items import item_table
from .JakAndDaxterOptions import JakAndDaxterOptions
-from .Locations import JakAndDaxterLocation, location_table
-from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials
+from .locs import (CellLocations as Cells,
+ ScoutLocations as Scouts)
+from .regs.RegionBase import JakAndDaxterRegion
+from .regs import (GeyserRockRegions as GeyserRock,
+ SandoverVillageRegions as SandoverVillage,
+ ForbiddenJungleRegions as ForbiddenJungle,
+ SentinelBeachRegions as SentinelBeach,
+ MistyIslandRegions as MistyIsland,
+ FireCanyonRegions as FireCanyon,
+ RockVillageRegions as RockVillage,
+ PrecursorBasinRegions as PrecursorBasin,
+ LostPrecursorCityRegions as LostPrecursorCity,
+ BoggySwampRegions as BoggySwamp,
+ MountainPassRegions as MountainPass,
+ VolcanicCraterRegions as VolcanicCrater,
+ SpiderCaveRegions as SpiderCave,
+ SnowyMountainRegions as SnowyMountain,
+ LavaTubeRegions as LavaTube,
+ GolAndMaiasCitadelRegions as GolAndMaiasCitadel)
-class JakAndDaxterRegion(Region):
- game: str = jak1_name
-
-
-# Holds information like the level name, the number of orbs available there, etc. Applies to both Levels and SubLevels.
-# We especially need orb_counts to be tracked here because we need to know how many orbs you have access to
-# in order to know when you can afford the 90-orb and 120-orb payments for more checks.
-class Jak1LevelInfo:
- name: str
- orb_count: int
-
- def __init__(self, name: str, orb_count: int):
- self.name = name
- self.orb_count = orb_count
-
-
-class Jak1Level(int, Enum):
- SCOUT_FLY_POWER_CELLS = auto() # This is a virtual location to reward you receiving 7 scout flies.
- GEYSER_ROCK = auto()
- SANDOVER_VILLAGE = auto()
- FORBIDDEN_JUNGLE = auto()
- SENTINEL_BEACH = auto()
- MISTY_ISLAND = auto()
- FIRE_CANYON = auto()
- ROCK_VILLAGE = auto()
- PRECURSOR_BASIN = auto()
- LOST_PRECURSOR_CITY = auto()
- BOGGY_SWAMP = auto()
- MOUNTAIN_PASS = auto()
- VOLCANIC_CRATER = auto()
- SPIDER_CAVE = auto()
- SNOWY_MOUNTAIN = auto()
- LAVA_TUBE = auto()
- GOL_AND_MAIAS_CITADEL = auto()
-
-
-class Jak1SubLevel(int, Enum):
- FORBIDDEN_JUNGLE_SWITCH_ROOM = auto()
- FORBIDDEN_JUNGLE_PLANT_ROOM = auto()
- SENTINEL_BEACH_CANNON_TOWER = auto()
- ROCK_VILLAGE_PONTOON_BRIDGE = auto()
- BOGGY_SWAMP_FLUT_FLUT = auto()
- MOUNTAIN_PASS_SHORTCUT = auto()
- SNOWY_MOUNTAIN_FLUT_FLUT = auto()
- SNOWY_MOUNTAIN_LURKER_FORT = auto()
- SNOWY_MOUNTAIN_FROZEN_BOX = auto()
- GOL_AND_MAIAS_CITADEL_ROTATING_TOWER = auto()
- GOL_AND_MAIAS_CITADEL_FINAL_BOSS = auto()
-
-
-level_table: typing.Dict[Jak1Level, Jak1LevelInfo] = {
- Jak1Level.SCOUT_FLY_POWER_CELLS:
- Jak1LevelInfo("Scout Fly Power Cells", 0), # Virtual location.
- Jak1Level.GEYSER_ROCK:
- Jak1LevelInfo("Geyser Rock", 50),
- Jak1Level.SANDOVER_VILLAGE:
- Jak1LevelInfo("Sandover Village", 50),
- Jak1Level.FORBIDDEN_JUNGLE:
- Jak1LevelInfo("Forbidden Jungle", 99),
- Jak1Level.SENTINEL_BEACH:
- Jak1LevelInfo("Sentinel Beach", 128),
- Jak1Level.MISTY_ISLAND:
- Jak1LevelInfo("Misty Island", 150),
- Jak1Level.FIRE_CANYON:
- Jak1LevelInfo("Fire Canyon", 50),
- Jak1Level.ROCK_VILLAGE:
- Jak1LevelInfo("Rock Village", 43),
- Jak1Level.PRECURSOR_BASIN:
- Jak1LevelInfo("Precursor Basin", 200),
- Jak1Level.LOST_PRECURSOR_CITY:
- Jak1LevelInfo("Lost Precursor City", 200),
- Jak1Level.BOGGY_SWAMP:
- Jak1LevelInfo("Boggy Swamp", 177),
- Jak1Level.MOUNTAIN_PASS:
- Jak1LevelInfo("Mountain Pass", 50),
- Jak1Level.VOLCANIC_CRATER:
- Jak1LevelInfo("Volcanic Crater", 50),
- Jak1Level.SPIDER_CAVE:
- Jak1LevelInfo("Spider Cave", 200),
- Jak1Level.SNOWY_MOUNTAIN:
- Jak1LevelInfo("Snowy Mountain", 113),
- Jak1Level.LAVA_TUBE:
- Jak1LevelInfo("Lava Tube", 50),
- Jak1Level.GOL_AND_MAIAS_CITADEL:
- Jak1LevelInfo("Gol and Maia's Citadel", 180),
-}
-
-sub_level_table: typing.Dict[Jak1SubLevel, Jak1LevelInfo] = {
- Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM:
- Jak1LevelInfo("Forbidden Jungle Switch Room", 24),
- Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM:
- Jak1LevelInfo("Forbidden Jungle Plant Room", 27),
- Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER:
- Jak1LevelInfo("Sentinel Beach Cannon Tower", 22),
- Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE:
- Jak1LevelInfo("Rock Village Pontoon Bridge", 7),
- Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT:
- Jak1LevelInfo("Boggy Swamp Flut Flut", 23),
- Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT:
- Jak1LevelInfo("Mountain Pass Shortcut", 0),
- Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT:
- Jak1LevelInfo("Snowy Mountain Flut Flut", 15),
- Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT:
- Jak1LevelInfo("Snowy Mountain Lurker Fort", 72),
- Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX:
- Jak1LevelInfo("Snowy Mountain Frozen Box", 0),
- Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER:
- Jak1LevelInfo("Gol and Maia's Citadel Rotating Tower", 20),
- Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS:
- Jak1LevelInfo("Gol and Maia's Citadel Final Boss", 0),
-}
-
-
-# Use the original game ID's for each item to tell the Region which Locations are available in it.
-# You do NOT need to add the item offsets or game ID, that will be handled by create_*_locations.
def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int):
# Always start with Menu.
- multiworld.regions.append(JakAndDaxterRegion("Menu", player, multiworld))
+ menu = JakAndDaxterRegion("Menu", player, multiworld)
+ multiworld.regions.append(menu)
- region_7sf = create_region(player, multiworld, Jak1Level.SCOUT_FLY_POWER_CELLS)
- create_cell_locations(region_7sf, Cells.loc7SF_cellTable)
+ # Build the special "Free 7 Scout Flies" Region. This is a virtual region always accessible to Menu.
+ # The Power Cells within it are automatically checked when you receive the 7th scout fly for the corresponding cell.
+ free7 = JakAndDaxterRegion("'Free 7 Scout Flies' Power Cells", player, multiworld)
+ free7.add_cell_locations(Cells.loc7SF_cellTable.keys())
+ for scout_fly_cell in free7.locations:
- region_gr = create_region(player, multiworld, Jak1Level.GEYSER_ROCK)
- create_cell_locations(region_gr, Cells.locGR_cellTable)
- create_fly_locations(region_gr, Scouts.locGR_scoutTable)
+ # Translate from Cell AP ID to Scout AP ID using game ID as an intermediary.
+ scout_fly_id = Scouts.to_ap_id(Cells.to_game_id(scout_fly_cell.address))
+ scout_fly_cell.access_rule = lambda state, flies=scout_fly_id: state.has(item_table[flies], player, 7)
+ multiworld.regions.append(free7)
- region_sv = create_region(player, multiworld, Jak1Level.SANDOVER_VILLAGE)
- create_cell_locations(region_sv, Cells.locSV_cellTable)
- create_fly_locations(region_sv, Scouts.locSV_scoutTable)
+ # Build all regions. Include their intra-connecting Rules, their Locations, and their Location access rules.
+ [gr] = GeyserRock.build_regions("Geyser Rock", player, multiworld)
+ [sv] = SandoverVillage.build_regions("Sandover Village", player, multiworld)
+ [fj] = ForbiddenJungle.build_regions("Forbidden Jungle", player, multiworld)
+ [sb] = SentinelBeach.build_regions("Sentinel Beach", player, multiworld)
+ [mi] = MistyIsland.build_regions("Misty Island", player, multiworld)
+ [fc] = FireCanyon.build_regions("Fire Canyon", player, multiworld)
+ [rv, rvp, rvc] = RockVillage.build_regions("Rock Village", player, multiworld)
+ [pb] = PrecursorBasin.build_regions("Precursor Basin", player, multiworld)
+ [lpc] = LostPrecursorCity.build_regions("Lost Precursor City", player, multiworld)
+ [bs] = BoggySwamp.build_regions("Boggy Swamp", player, multiworld)
+ [mp, mpr] = MountainPass.build_regions("Mountain Pass", player, multiworld)
+ [vc] = VolcanicCrater.build_regions("Volcanic Crater", player, multiworld)
+ [sc] = SpiderCave.build_regions("Spider Cave", player, multiworld)
+ [sm] = SnowyMountain.build_regions("Snowy Mountain", player, multiworld)
+ [lt] = LavaTube.build_regions("Lava Tube", player, multiworld)
+ [gmc, fb] = GolAndMaiasCitadel.build_regions("Gol and Maia's Citadel", player, multiworld)
- region_fj = create_region(player, multiworld, Jak1Level.FORBIDDEN_JUNGLE)
- create_cell_locations(region_fj, {k: Cells.locFJ_cellTable[k] for k in {3, 4, 5, 8, 9}})
- create_fly_locations(region_fj, Scouts.locFJ_scoutTable)
- create_special_locations(region_fj, {k: Specials.loc_specialTable[k] for k in {4, 5}})
+ # Define the interconnecting rules.
+ menu.connect(free7)
+ menu.connect(gr)
+ gr.connect(sv) # Geyser Rock modified to let you leave at any time.
+ sv.connect(fj)
+ sv.connect(sb)
+ sv.connect(mi, rule=lambda state: state.has("Fisherman's Boat", player))
+ sv.connect(fc, rule=lambda state: state.has("Power Cell", player, 20))
+ fc.connect(rv)
+ rv.connect(pb)
+ rv.connect(lpc)
+ rvp.connect(bs) # rv->rvp/rvc connections defined internally by RockVillageRegions.
+ rvc.connect(mp, rule=lambda state: state.has("Power Cell", player, 45))
+ mpr.connect(vc) # mp->mpr connection defined internally by MountainPassRegions.
+ vc.connect(sc)
+ vc.connect(sm, rule=lambda state: state.has("Snowy Mountain Gondola", player))
+ vc.connect(lt, rule=lambda state: state.has("Power Cell", player, 72))
+ lt.connect(gmc) # gmc->fb connection defined internally by GolAndMaiasCitadelRegions.
- sub_region_fjsr = create_subregion(region_fj, Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM)
- create_cell_locations(sub_region_fjsr, {k: Cells.locFJ_cellTable[k] for k in {2}})
- create_special_locations(sub_region_fjsr, {k: Specials.loc_specialTable[k] for k in {2}})
-
- sub_region_fjpr = create_subregion(sub_region_fjsr, Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM)
- create_cell_locations(sub_region_fjpr, {k: Cells.locFJ_cellTable[k] for k in {6}})
-
- region_sb = create_region(player, multiworld, Jak1Level.SENTINEL_BEACH)
- create_cell_locations(region_sb, {k: Cells.locSB_cellTable[k] for k in {15, 17, 16, 18, 21, 22}})
- create_fly_locations(region_sb, Scouts.locSB_scoutTable)
- create_special_locations(region_sb, {k: Specials.loc_specialTable[k] for k in {17}})
-
- sub_region_sbct = create_subregion(region_sb, Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER)
- create_cell_locations(sub_region_sbct, {k: Cells.locSB_cellTable[k] for k in {19}})
-
- region_mi = create_region(player, multiworld, Jak1Level.MISTY_ISLAND)
- create_cell_locations(region_mi, Cells.locMI_cellTable)
- create_fly_locations(region_mi, Scouts.locMI_scoutTable)
-
- region_fc = create_region(player, multiworld, Jak1Level.FIRE_CANYON)
- create_cell_locations(region_fc, Cells.locFC_cellTable)
- create_fly_locations(region_fc, Scouts.locFC_scoutTable)
-
- region_rv = create_region(player, multiworld, Jak1Level.ROCK_VILLAGE)
- create_cell_locations(region_rv, Cells.locRV_cellTable)
- create_fly_locations(region_rv, {k: Scouts.locRV_scoutTable[k]
- for k in {76, 131148, 196684, 262220, 65612, 327756}})
- create_special_locations(region_rv, {k: Specials.loc_specialTable[k] for k in {33}})
-
- sub_region_rvpb = create_subregion(region_rv, Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE)
- create_fly_locations(sub_region_rvpb, {k: Scouts.locRV_scoutTable[k] for k in {393292}})
-
- region_pb = create_region(player, multiworld, Jak1Level.PRECURSOR_BASIN)
- create_cell_locations(region_pb, Cells.locPB_cellTable)
- create_fly_locations(region_pb, Scouts.locPB_scoutTable)
-
- region_lpc = create_region(player, multiworld, Jak1Level.LOST_PRECURSOR_CITY)
- create_cell_locations(region_lpc, Cells.locLPC_cellTable)
- create_fly_locations(region_lpc, Scouts.locLPC_scoutTable)
-
- region_bs = create_region(player, multiworld, Jak1Level.BOGGY_SWAMP)
- create_cell_locations(region_bs, {k: Cells.locBS_cellTable[k] for k in {36, 38, 39, 40, 41, 42}})
- create_fly_locations(region_bs, {k: Scouts.locBS_scoutTable[k] for k in {43, 393259, 65579, 262187, 196651}})
-
- sub_region_bsff = create_subregion(region_bs, Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT)
- create_cell_locations(sub_region_bsff, {k: Cells.locBS_cellTable[k] for k in {37}})
- create_fly_locations(sub_region_bsff, {k: Scouts.locBS_scoutTable[k] for k in {327723, 131115}})
-
- region_mp = create_region(player, multiworld, Jak1Level.MOUNTAIN_PASS)
- create_cell_locations(region_mp, {k: Cells.locMP_cellTable[k] for k in {86, 87}})
- create_fly_locations(region_mp, Scouts.locMP_scoutTable)
-
- sub_region_mps = create_subregion(region_mp, Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT)
- create_cell_locations(sub_region_mps, {k: Cells.locMP_cellTable[k] for k in {110}})
-
- region_vc = create_region(player, multiworld, Jak1Level.VOLCANIC_CRATER)
- create_cell_locations(region_vc, Cells.locVC_cellTable)
- create_fly_locations(region_vc, Scouts.locVC_scoutTable)
- create_special_locations(region_vc, {k: Specials.loc_specialTable[k] for k in {105}})
-
- region_sc = create_region(player, multiworld, Jak1Level.SPIDER_CAVE)
- create_cell_locations(region_sc, Cells.locSC_cellTable)
- create_fly_locations(region_sc, Scouts.locSC_scoutTable)
-
- region_sm = create_region(player, multiworld, Jak1Level.SNOWY_MOUNTAIN)
- create_cell_locations(region_sm, {k: Cells.locSM_cellTable[k] for k in {60, 61, 66, 64}})
- create_fly_locations(region_sm, {k: Scouts.locSM_scoutTable[k] for k in {65, 327745, 65601, 131137, 393281}})
- create_special_locations(region_sm, {k: Specials.loc_specialTable[k] for k in {60}})
-
- sub_region_smfb = create_subregion(region_sm, Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX)
- create_cell_locations(sub_region_smfb, {k: Cells.locSM_cellTable[k] for k in {67}})
-
- sub_region_smff = create_subregion(region_sm, Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT)
- create_cell_locations(sub_region_smff, {k: Cells.locSM_cellTable[k] for k in {63}})
- create_special_locations(sub_region_smff, {k: Specials.loc_specialTable[k] for k in {63}})
-
- sub_region_smlf = create_subregion(region_sm, Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT)
- create_cell_locations(sub_region_smlf, {k: Cells.locSM_cellTable[k] for k in {62}})
- create_fly_locations(sub_region_smlf, {k: Scouts.locSM_scoutTable[k] for k in {196673, 262209}})
-
- region_lt = create_region(player, multiworld, Jak1Level.LAVA_TUBE)
- create_cell_locations(region_lt, Cells.locLT_cellTable)
- create_fly_locations(region_lt, Scouts.locLT_scoutTable)
-
- region_gmc = create_region(player, multiworld, Jak1Level.GOL_AND_MAIAS_CITADEL)
- create_cell_locations(region_gmc, {k: Cells.locGMC_cellTable[k] for k in {71, 72, 73}})
- create_fly_locations(region_gmc, {k: Scouts.locGMC_scoutTable[k]
- for k in {91, 65627, 196699, 262235, 393307, 131163}})
- create_special_locations(region_gmc, {k: Specials.loc_specialTable[k] for k in {71, 72, 73}})
-
- sub_region_gmcrt = create_subregion(region_gmc, Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER)
- create_cell_locations(sub_region_gmcrt, {k: Cells.locGMC_cellTable[k] for k in {70}})
- create_fly_locations(sub_region_gmcrt, {k: Scouts.locGMC_scoutTable[k] for k in {327771}})
- create_special_locations(sub_region_gmcrt, {k: Specials.loc_specialTable[k] for k in {70}})
-
- create_subregion(sub_region_gmcrt, Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS)
-
-
-def create_region(player: int, multiworld: MultiWorld, level: Jak1Level) -> JakAndDaxterRegion:
- name = level_table[level].name
- region = JakAndDaxterRegion(name, player, multiworld)
- multiworld.regions.append(region)
- return region
-
-
-def create_subregion(parent: Region, sub_level: Jak1SubLevel) -> JakAndDaxterRegion:
- name = sub_level_table[sub_level].name
- region = JakAndDaxterRegion(name, parent.player, parent.multiworld)
- parent.multiworld.regions.append(region)
- return region
-
-
-def create_cell_locations(region: Region, locations: typing.Dict[int, str]):
- region.locations += [JakAndDaxterLocation(region.player,
- location_table[Cells.to_ap_id(loc)],
- Cells.to_ap_id(loc),
- region) for loc in locations]
-
-
-def create_fly_locations(region: Region, locations: typing.Dict[int, str]):
- region.locations += [JakAndDaxterLocation(region.player,
- location_table[Scouts.to_ap_id(loc)],
- Scouts.to_ap_id(loc),
- region) for loc in locations]
-
-
-# Special Locations should be matched alongside their respective Power Cell Locations,
-# so you get 2 unlocks for these rather than 1.
-def create_special_locations(region: Region, locations: typing.Dict[int, str]):
- region.locations += [JakAndDaxterLocation(region.player,
- location_table[Specials.to_ap_id(loc)],
- Specials.to_ap_id(loc),
- region) for loc in locations]
+ # Finally, set the completion condition.
+ multiworld.completion_condition[player] = lambda state: state.can_reach(fb, "Region", player)
diff --git a/worlds/jakanddaxter/Rules.py b/worlds/jakanddaxter/Rules.py
index 8ce4d5c255..9ff3d29d3e 100644
--- a/worlds/jakanddaxter/Rules.py
+++ b/worlds/jakanddaxter/Rules.py
@@ -1,270 +1,40 @@
-from typing import List
-
+import typing
from BaseClasses import MultiWorld, CollectionState
from .JakAndDaxterOptions import JakAndDaxterOptions
-from .Regions import Jak1Level, Jak1SubLevel, level_table, sub_level_table
-from .Items import item_table
-from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials
-from worlds.jakanddaxter.Locations import location_table
-
-
-def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int):
-
- # Setting up some useful variables here because the offset numbers can get confusing
- # for access rules. Feel free to add more variables here to keep the code more readable.
- # You DO need to convert the game ID's to AP ID's here.
- power_cell = item_table[Cells.to_ap_id(0)]
-
- # The int/list structure here is intentional, see `set_trade_requirements` for how we handle these.
- sv_traders = [11, 12, [13, 14]] # Mayor, Uncle, Oracle 1 and 2
- rv_traders = [31, 32, 33, [34, 35]] # Geologist, Gambler, Warrior, Oracle 3 and 4
- vc_traders = [[96, 97, 98, 99], [100, 101]] # Miners 1-4, Oracle 5 and 6
-
- fj_jungle_elevator = item_table[Specials.to_ap_id(4)]
- fj_blue_switch = item_table[Specials.to_ap_id(2)]
- fj_fisherman = item_table[Specials.to_ap_id(5)]
-
- sb_flut_flut = item_table[Specials.to_ap_id(17)]
- rv_pontoon_bridge = item_table[Specials.to_ap_id(33)]
-
- sm_yellow_switch = item_table[Specials.to_ap_id(60)]
- sm_fort_gate = item_table[Specials.to_ap_id(63)]
- sm_gondola = item_table[Specials.to_ap_id(105)]
-
- gmc_blue_sage = item_table[Specials.to_ap_id(71)]
- gmc_red_sage = item_table[Specials.to_ap_id(72)]
- gmc_yellow_sage = item_table[Specials.to_ap_id(73)]
- gmc_green_sage = item_table[Specials.to_ap_id(70)]
-
- # Start connecting regions and set their access rules.
-
- # Scout Fly Power Cells is a virtual region, not a physical one, so connect it to Menu.
- connect_start(multiworld, player, Jak1Level.SCOUT_FLY_POWER_CELLS)
- set_fly_requirements(multiworld, player)
-
- # You start the game in front of Green Sage's Hut, so you don't get stuck on Geyser Rock in the first 5 minutes.
- connect_start(multiworld, player, Jak1Level.SANDOVER_VILLAGE)
- set_trade_requirements(multiworld, player, Jak1Level.SANDOVER_VILLAGE, sv_traders, 1530)
-
- # Geyser Rock is accessible at any time, just check the 3 naked cell Locations to return.
- connect_regions(multiworld, player,
- Jak1Level.SANDOVER_VILLAGE,
- Jak1Level.GEYSER_ROCK)
-
- connect_regions(multiworld, player,
- Jak1Level.SANDOVER_VILLAGE,
- Jak1Level.FORBIDDEN_JUNGLE)
-
- connect_region_to_sub(multiworld, player,
- Jak1Level.FORBIDDEN_JUNGLE,
- Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM,
- lambda state: state.has(fj_jungle_elevator, player))
-
- connect_subregions(multiworld, player,
- Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM,
- Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM,
- lambda state: state.has(fj_blue_switch, player))
-
- # You just need to defeat the plant boss to escape this subregion, no specific Item required.
- connect_sub_to_region(multiworld, player,
- Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM,
- Jak1Level.FORBIDDEN_JUNGLE)
-
- connect_regions(multiworld, player,
- Jak1Level.SANDOVER_VILLAGE,
- Jak1Level.SENTINEL_BEACH)
-
- # Just jump off the tower to escape this subregion.
- connect_region_to_sub(multiworld, player,
- Jak1Level.SENTINEL_BEACH,
- Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER,
- lambda state: state.has(fj_blue_switch, player))
-
- connect_regions(multiworld, player,
- Jak1Level.SANDOVER_VILLAGE,
- Jak1Level.MISTY_ISLAND,
- lambda state: state.has(fj_fisherman, player))
-
- connect_regions(multiworld, player,
- Jak1Level.SANDOVER_VILLAGE,
- Jak1Level.FIRE_CANYON,
- lambda state: state.has(power_cell, player, 20))
-
- connect_regions(multiworld, player,
- Jak1Level.FIRE_CANYON,
- Jak1Level.ROCK_VILLAGE)
- set_trade_requirements(multiworld, player, Jak1Level.ROCK_VILLAGE, rv_traders, 1530)
-
- connect_regions(multiworld, player,
- Jak1Level.ROCK_VILLAGE,
- Jak1Level.PRECURSOR_BASIN)
-
- connect_regions(multiworld, player,
- Jak1Level.ROCK_VILLAGE,
- Jak1Level.LOST_PRECURSOR_CITY)
-
- # This pontoon bridge locks out Boggy Swamp and Mountain Pass,
- # effectively making it required to complete the game.
- connect_region_to_sub(multiworld, player,
- Jak1Level.ROCK_VILLAGE,
- Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE,
- lambda state: state.has(rv_pontoon_bridge, player))
-
- connect_sub_to_region(multiworld, player,
- Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE,
- Jak1Level.BOGGY_SWAMP)
-
- # Flut Flut only has one landing pad here, so leaving this subregion is as easy
- # as dismounting Flut Flut right where you found her.
- connect_region_to_sub(multiworld, player,
- Jak1Level.BOGGY_SWAMP,
- Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT,
- lambda state: state.has(sb_flut_flut, player))
-
- connect_sub_to_region(multiworld, player,
- Jak1SubLevel.ROCK_VILLAGE_PONTOON_BRIDGE,
- Jak1Level.MOUNTAIN_PASS,
- lambda state: state.has(power_cell, player, 45))
-
- connect_region_to_sub(multiworld, player,
- Jak1Level.MOUNTAIN_PASS,
- Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT,
- lambda state: state.has(sm_yellow_switch, player))
-
- connect_regions(multiworld, player,
- Jak1Level.MOUNTAIN_PASS,
- Jak1Level.VOLCANIC_CRATER)
- set_trade_requirements(multiworld, player, Jak1Level.VOLCANIC_CRATER, vc_traders, 1530)
-
- connect_regions(multiworld, player,
- Jak1Level.VOLCANIC_CRATER,
- Jak1Level.SPIDER_CAVE)
-
- # Custom-added unlock for snowy mountain's gondola.
- connect_regions(multiworld, player,
- Jak1Level.VOLCANIC_CRATER,
- Jak1Level.SNOWY_MOUNTAIN,
- lambda state: state.has(sm_gondola, player))
-
- connect_region_to_sub(multiworld, player,
- Jak1Level.SNOWY_MOUNTAIN,
- Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX,
- lambda state: state.has(sm_yellow_switch, player))
-
- # Flut Flut has both a start and end landing pad here, but there's an elevator that takes you up
- # from the end pad to the entrance of the fort, so you're back to the "main area."
- connect_region_to_sub(multiworld, player,
- Jak1Level.SNOWY_MOUNTAIN,
- Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT,
- lambda state: state.has(sb_flut_flut, player))
-
- connect_region_to_sub(multiworld, player,
- Jak1Level.SNOWY_MOUNTAIN,
- Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT,
- lambda state: state.has(sm_fort_gate, player))
-
- connect_regions(multiworld, player,
- Jak1Level.VOLCANIC_CRATER,
- Jak1Level.LAVA_TUBE,
- lambda state: state.has(power_cell, player, 72))
-
- connect_regions(multiworld, player,
- Jak1Level.LAVA_TUBE,
- Jak1Level.GOL_AND_MAIAS_CITADEL)
-
- # The stairs up to Samos's cage is only activated when you get the Items for freeing the other 3 Sages.
- # But you can climb back down that staircase (or fall down from the top) to escape this subregion.
- connect_region_to_sub(multiworld, player,
- Jak1Level.GOL_AND_MAIAS_CITADEL,
- Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER,
- lambda state: state.has(gmc_blue_sage, player) and
- state.has(gmc_red_sage, player) and
- state.has(gmc_yellow_sage, player))
-
- # This is the final elevator, only active when you get the Item for freeing the Green Sage.
- connect_subregions(multiworld, player,
- Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER,
- Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS,
- lambda state: state.has(gmc_green_sage, player))
-
- multiworld.completion_condition[player] = lambda state: state.can_reach(
- multiworld.get_region(sub_level_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS].name, player),
- "Region",
- player)
-
-
-def connect_start(multiworld: MultiWorld, player: int, target: Jak1Level):
- menu_region = multiworld.get_region("Menu", player)
- start_region = multiworld.get_region(level_table[target].name, player)
- menu_region.connect(start_region)
-
-
-def connect_regions(multiworld: MultiWorld, player: int, source: Jak1Level, target: Jak1Level, rule=None):
- source_region = multiworld.get_region(level_table[source].name, player)
- target_region = multiworld.get_region(level_table[target].name, player)
- source_region.connect(target_region, rule=rule)
-
-
-def connect_region_to_sub(multiworld: MultiWorld, player: int, source: Jak1Level, target: Jak1SubLevel, rule=None):
- source_region = multiworld.get_region(level_table[source].name, player)
- target_region = multiworld.get_region(sub_level_table[target].name, player)
- source_region.connect(target_region, rule=rule)
-
-
-def connect_sub_to_region(multiworld: MultiWorld, player: int, source: Jak1SubLevel, target: Jak1Level, rule=None):
- source_region = multiworld.get_region(sub_level_table[source].name, player)
- target_region = multiworld.get_region(level_table[target].name, player)
- source_region.connect(target_region, rule=rule)
-
-
-def connect_subregions(multiworld: MultiWorld, player: int, source: Jak1SubLevel, target: Jak1SubLevel, rule=None):
- source_region = multiworld.get_region(sub_level_table[source].name, player)
- target_region = multiworld.get_region(sub_level_table[target].name, player)
- source_region.connect(target_region, rule=rule)
-
-
-# The "Free 7 Scout Fly" Locations are automatically checked when you receive the 7th scout fly Item.
-def set_fly_requirements(multiworld: MultiWorld, player: int):
- region = multiworld.get_region(level_table[Jak1Level.SCOUT_FLY_POWER_CELLS].name, player)
- for loc in region.locations:
- scout_fly_id = Scouts.to_ap_id(Cells.to_game_id(loc.address)) # Translate using game ID as an intermediary.
- loc.access_rule = lambda state, flies=scout_fly_id: state.has(item_table[flies], player, 7)
+from .locs import CellLocations as Cells
+from .Locations import location_table
+from .Regions import JakAndDaxterRegion
# TODO - Until we come up with a better progressive system for the traders (that avoids hard-locking if you pay the
# wrong ones and can't afford the right ones) just make all the traders locked behind the total amount to pay them all.
-def set_trade_requirements(multiworld: MultiWorld, player: int, level: Jak1Level, traders: List, orb_count: int):
+def can_trade(state: CollectionState,
+ player: int,
+ multiworld: MultiWorld,
+ required_orbs: int,
+ required_previous_trade: int = None) -> bool:
- def count_accessible_orbs(state) -> int:
- accessible_orbs = 0
- for level_info in [*level_table.values(), *sub_level_table.values()]:
- reg = multiworld.get_region(level_info.name, player)
- if reg.can_reach(state):
- accessible_orbs += level_info.orb_count
- return accessible_orbs
+ accessible_orbs = 0
+ for region in multiworld.get_regions(player):
+ if state.can_reach(region, "Region", player):
+ accessible_orbs += typing.cast(JakAndDaxterRegion, region).orb_count
- region = multiworld.get_region(level_table[level].name, player)
- names_to_index = {region.locations[i].name: i for i in range(0, len(region.locations))}
- for trader in traders:
+ if required_previous_trade:
+ name_of_previous_trade = location_table[Cells.to_ap_id(required_previous_trade)]
+ return (accessible_orbs >= required_orbs
+ and state.can_reach(name_of_previous_trade, "Location", player=player))
+ else:
+ return accessible_orbs >= required_orbs
- # Singleton integers indicate a trader who has only one Location to check.
- # (Mayor, Uncle, etc)
- if type(trader) is int:
- loc = region.locations[names_to_index[location_table[Cells.to_ap_id(trader)]]]
- loc.access_rule = lambda state, orbs=orb_count: (
- count_accessible_orbs(state) >= orbs)
- # Lists of integers indicate a trader who has sequential Locations to check, each dependent on the last.
- # (Oracles and Miners)
- elif type(trader) is list:
- previous_loc = None
- for trade in trader:
- loc = region.locations[names_to_index[location_table[Cells.to_ap_id(trade)]]]
- loc.access_rule = lambda state, orbs=orb_count, prev=previous_loc: (
- count_accessible_orbs(state) >= orbs and
- (state.can_reach(prev, player) if prev else True)) # TODO - Can Reach or Has Reached?
- previous_loc = loc
+def can_free_scout_flies(state: CollectionState, player: int) -> bool:
+ return (state.has("Jump Dive", player)
+ or (state.has("Crouch", player)
+ and state.has("Crouch Uppercut", player)))
- # Any other type of element in the traders list is wrong.
- else:
- raise TypeError(f"Tried to set trade requirements on an unknown type {trader}.")
+
+def can_fight(state: CollectionState, player: int) -> bool:
+ return (state.has("Jump Dive", player)
+ or state.has("Jump Kick", player)
+ or state.has("Punch", player)
+ or state.has("Kick", player))
diff --git a/worlds/jakanddaxter/__init__.py b/worlds/jakanddaxter/__init__.py
index 5cbf3ca4e4..2ba611d76b 100644
--- a/worlds/jakanddaxter/__init__.py
+++ b/worlds/jakanddaxter/__init__.py
@@ -1,16 +1,18 @@
import typing
import settings
-from Utils import local_path
+from Utils import local_path, visualize_regions
from BaseClasses import Item, ItemClassification, Tutorial
-from .GameID import jak1_id, jak1_name
+from .GameID import jak1_id, jak1_name, jak1_max
from .JakAndDaxterOptions import JakAndDaxterOptions
-from .Items import JakAndDaxterItem
from .Locations import JakAndDaxterLocation, location_table
from .Items import JakAndDaxterItem, item_table
-from .locs import CellLocations as Cells, ScoutLocations as Scouts, OrbLocations as Orbs, SpecialLocations as Specials
+from .locs import (CellLocations as Cells,
+ ScoutLocations as Scouts,
+ SpecialLocations as Specials,
+ OrbCacheLocations as Caches,
+ OrbLocations as Orbs)
from .Regions import create_regions
-from .Rules import set_rules
from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import components, Component, launch_subprocess, Type, icon_paths
@@ -34,7 +36,7 @@ class JakAndDaxterSettings(settings.Group):
"""Path to folder containing the ArchipelaGOAL mod executables (gk.exe and goalc.exe)."""
description = "ArchipelaGOAL Root Directory"
- root_directory: RootDirectory = RootDirectory("D:/Files/Repositories/ArchipelaGOAL/out/build/Release/bin")
+ root_directory: RootDirectory = RootDirectory("D:/Files/Repositories/ArchipelaGOAL/out/build/Release/bin")
class JakAndDaxterWebWorld(WebWorld):
@@ -83,17 +85,18 @@ class JakAndDaxterWorld(World):
"Scout Flies": {item_table[k]: k for k in item_table
if k in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset)},
"Specials": {item_table[k]: k for k in item_table
- if k in range(jak1_id + Specials.special_offset, jak1_id + Orbs.orb_offset)},
- # TODO - Make group for Precursor Orbs.
- # "Precursor Orbs": {item_table[k]: k for k in item_table
- # if k in range(jak1_id + Orbs.orb_offset, ???)},
+ if k in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset)},
+ "Moves": {item_table[k]: k for k in item_table
+ if k in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset)},
+ "Precursor Orbs": {item_table[k]: k for k in item_table
+ if k in range(jak1_id + Orbs.orb_offset, jak1_max)},
}
+ # Regions and Rules
+ # This will also set Locations, Location access rules, Region access rules, etc.
def create_regions(self):
create_regions(self.multiworld, self.options, self.player)
-
- def set_rules(self):
- set_rules(self.multiworld, self.options, self.player)
+ # visualize_regions(self.multiworld.get_region("Menu", self.player), "jak.puml")
# Helper function to reuse some nasty if/else trees.
@staticmethod
@@ -109,14 +112,25 @@ class JakAndDaxterWorld(World):
count = 7
# Make only 1 of each Special Item.
- elif item in range(jak1_id + Specials.special_offset, jak1_id + Orbs.orb_offset):
+ elif item in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset):
classification = ItemClassification.progression
count = 1
- # TODO - Make ??? Precursor Orbs.
- # elif item in range(jak1_id + Orbs.orb_offset, ???):
- # classification = ItemClassification.filler
- # count = ???
+ # Make only 1 of each Move Item.
+ elif item in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset):
+ classification = ItemClassification.progression
+ count = 1
+
+ # TODO - Make 2000 Precursor Orbs, ONLY IF Orbsanity is enabled.
+ elif item in range(jak1_id + Orbs.orb_offset, jak1_max):
+ classification = ItemClassification.progression_skip_balancing
+ count = 0
+
+ # Under normal circumstances, we will create 0 filler items.
+ # We will manually create filler items as needed.
+ elif item == jak1_max:
+ classification = ItemClassification.filler
+ count = 0
# If we try to make items with ID's higher than we've defined, something has gone wrong.
else:
@@ -126,8 +140,17 @@ class JakAndDaxterWorld(World):
def create_items(self):
for item_id in item_table:
- count, _ = self.item_type_helper(item_id)
- self.multiworld.itempool += [self.create_item(item_table[item_id]) for k in range(0, count)]
+
+ # Handle Move Randomizer option.
+ # If it is OFF, put all moves in your starting inventory instead of the item pool,
+ # then fill the item pool with a corresponding amount of filler items.
+ if not self.options.enable_move_randomizer and item_table[item_id] in self.item_name_groups["Moves"]:
+ self.multiworld.push_precollected(self.create_item(item_table[item_id]))
+ self.multiworld.itempool += [self.create_item(self.get_filler_item_name())]
+ else:
+ count, classification = self.item_type_helper(item_id)
+ self.multiworld.itempool += [JakAndDaxterItem(item_table[item_id], classification, item_id, self.player)
+ for _ in range(count)]
def create_item(self, name: str) -> Item:
item_id = self.item_name_to_id[name]
@@ -135,7 +158,7 @@ class JakAndDaxterWorld(World):
return JakAndDaxterItem(name, classification, item_id, self.player)
def get_filler_item_name(self) -> str:
- return "Power Cell" # TODO - Make Precursor Orb the filler item. Until then, enjoy the free progression.
+ return "Green Eco Pill"
def launch_client():
diff --git a/worlds/jakanddaxter/client/MemoryReader.py b/worlds/jakanddaxter/client/MemoryReader.py
index c91eba8286..a446836457 100644
--- a/worlds/jakanddaxter/client/MemoryReader.py
+++ b/worlds/jakanddaxter/client/MemoryReader.py
@@ -1,34 +1,72 @@
import random
import typing
+import json
import pymem
from pymem import pattern
from pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinAPIError
-import json
+from dataclasses import dataclass
from CommonClient import logger
-from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies, SpecialLocations as Specials
+from ..locs import (CellLocations as Cells,
+ ScoutLocations as Flies,
+ SpecialLocations as Specials,
+ OrbCacheLocations as Caches)
# Some helpful constants.
sizeof_uint64 = 8
sizeof_uint32 = 4
sizeof_uint8 = 1
-next_cell_index_offset = 0 # Each of these is an uint64, so 8 bytes.
-next_buzzer_index_offset = 8 # Each of these is an uint64, so 8 bytes.
-next_special_index_offset = 16 # Each of these is an uint64, so 8 bytes.
-cells_checked_offset = 24
-buzzers_checked_offset = 428 # cells_checked_offset + (sizeof uint32 * 101 cells)
-specials_checked_offset = 876 # buzzers_checked_offset + (sizeof uint32 * 112 buzzers)
+# IMPORTANT: OpenGOAL memory structures are particular about the alignment, in memory, of member elements according to
+# their size in bits. The address for an N-bit field must be divisible by N. Use this class to define the memory offsets
+# of important values in the struct. It will also do the byte alignment properly for you.
+# See https://opengoal.dev/docs/reference/type_system/#arrays
+@dataclass
+class OffsetFactory:
+ current_offset: int = 0
-buzzers_received_offset = 1004 # specials_checked_offset + (sizeof uint32 * 32 specials)
-specials_received_offset = 1020 # buzzers_received_offset + (sizeof uint8 * 16 levels (for scout fly groups))
+ def define(self, size: int, length: int = 1) -> int:
-died_offset = 1052 # specials_received_offset + (sizeof uint8 * 32 specials)
+ # If necessary, align current_offset to the current size first.
+ bytes_to_alignment = self.current_offset % size
+ if bytes_to_alignment != 0:
+ self.current_offset += (size - bytes_to_alignment)
-deathlink_enabled_offset = 1053 # died_offset + sizeof uint8
+ # Increment current_offset so the next definition can be made.
+ offset_to_use = self.current_offset
+ self.current_offset += (size * length)
+ return offset_to_use
-end_marker_offset = 1054 # deathlink_enabled_offset + sizeof uint8
+
+# Start defining important memory address offsets here. They must be in the same order, have the same sizes, and have
+# the same lengths, as defined in `ap-info-jak1`.
+offsets = OffsetFactory()
+
+# Cell, Buzzer, and Special information.
+next_cell_index_offset = offsets.define(sizeof_uint64)
+next_buzzer_index_offset = offsets.define(sizeof_uint64)
+next_special_index_offset = offsets.define(sizeof_uint64)
+
+cells_checked_offset = offsets.define(sizeof_uint32, 101)
+buzzers_checked_offset = offsets.define(sizeof_uint32, 112)
+specials_checked_offset = offsets.define(sizeof_uint32, 32)
+
+buzzers_received_offset = offsets.define(sizeof_uint8, 16)
+specials_received_offset = offsets.define(sizeof_uint8, 32)
+
+# Deathlink information.
+died_offset = offsets.define(sizeof_uint8)
+deathlink_enabled_offset = offsets.define(sizeof_uint8)
+
+# Move Rando information.
+next_orb_cache_index_offset = offsets.define(sizeof_uint64)
+orb_caches_checked_offset = offsets.define(sizeof_uint32, 16)
+moves_received_offset = offsets.define(sizeof_uint8, 16)
+moverando_enabled_offset = offsets.define(sizeof_uint8)
+
+# The End.
+end_marker_offset = offsets.define(sizeof_uint8, 4)
# "Jak" to be replaced by player name in the Client.
@@ -67,7 +105,6 @@ def autopsy(died: int) -> str:
return "Jak got Flut Flut hurt."
if died == 18:
return "Jak poisoned the whole darn catch."
-
return "Jak died."
@@ -170,50 +207,26 @@ class JakAndDaxterMemoryReader:
def read_memory(self) -> typing.List[int]:
try:
- next_cell_index = int.from_bytes(
- self.gk_process.read_bytes(self.goal_address, sizeof_uint64),
- byteorder="little",
- signed=False)
- next_buzzer_index = int.from_bytes(
- self.gk_process.read_bytes(self.goal_address + next_buzzer_index_offset, sizeof_uint64),
- byteorder="little",
- signed=False)
- next_special_index = int.from_bytes(
- self.gk_process.read_bytes(self.goal_address + next_special_index_offset, sizeof_uint64),
- byteorder="little",
- signed=False)
+ next_cell_index = self.read_goal_address(0, sizeof_uint64)
+ next_buzzer_index = self.read_goal_address(next_buzzer_index_offset, sizeof_uint64)
+ next_special_index = self.read_goal_address(next_special_index_offset, sizeof_uint64)
for k in range(0, next_cell_index):
- next_cell = int.from_bytes(
- self.gk_process.read_bytes(
- self.goal_address + cells_checked_offset + (k * sizeof_uint32),
- sizeof_uint32),
- byteorder="little",
- signed=False)
+ next_cell = self.read_goal_address(cells_checked_offset + (k * sizeof_uint32), sizeof_uint32)
cell_ap_id = Cells.to_ap_id(next_cell)
if cell_ap_id not in self.location_outbox:
self.location_outbox.append(cell_ap_id)
logger.debug("Checked power cell: " + str(next_cell))
for k in range(0, next_buzzer_index):
- next_buzzer = int.from_bytes(
- self.gk_process.read_bytes(
- self.goal_address + buzzers_checked_offset + (k * sizeof_uint32),
- sizeof_uint32),
- byteorder="little",
- signed=False)
+ next_buzzer = self.read_goal_address(buzzers_checked_offset + (k * sizeof_uint32), sizeof_uint32)
buzzer_ap_id = Flies.to_ap_id(next_buzzer)
if buzzer_ap_id not in self.location_outbox:
self.location_outbox.append(buzzer_ap_id)
logger.debug("Checked scout fly: " + str(next_buzzer))
for k in range(0, next_special_index):
- next_special = int.from_bytes(
- self.gk_process.read_bytes(
- self.goal_address + specials_checked_offset + (k * sizeof_uint32),
- sizeof_uint32),
- byteorder="little",
- signed=False)
+ next_special = self.read_goal_address(specials_checked_offset + (k * sizeof_uint32), sizeof_uint32)
# 112 is the game-task ID of `finalboss-movies`, which is written to this array when you grab
# the white eco. This is our victory condition, so we need to catch it and act on it.
@@ -228,22 +241,27 @@ class JakAndDaxterMemoryReader:
self.location_outbox.append(special_ap_id)
logger.debug("Checked special: " + str(next_special))
- died = int.from_bytes(
- self.gk_process.read_bytes(self.goal_address + died_offset, sizeof_uint8),
- byteorder="little",
- signed=False)
-
+ died = self.read_goal_address(died_offset, sizeof_uint8)
if died > 0:
self.send_deathlink = True
self.cause_of_death = autopsy(died)
- deathlink_flag = int.from_bytes(
- self.gk_process.read_bytes(self.goal_address + deathlink_enabled_offset, sizeof_uint8),
- byteorder="little",
- signed=False)
+ # Listen for any changes to this setting.
+ deathlink_flag = self.read_goal_address(deathlink_enabled_offset, sizeof_uint8)
+ self.deathlink_enabled = bool(deathlink_flag)
+
+ next_cache_index = self.read_goal_address(next_orb_cache_index_offset, sizeof_uint64)
+
+ for k in range(0, next_cache_index):
+ next_cache = self.read_goal_address(orb_caches_checked_offset + (k * sizeof_uint32), sizeof_uint32)
+ cache_ap_id = Caches.to_ap_id(next_cache)
+ if cache_ap_id not in self.location_outbox:
+ self.location_outbox.append(cache_ap_id)
+ logger.debug("Checked orb cache: " + str(next_cache))
# Listen for any changes to this setting.
- self.deathlink_enabled = bool(deathlink_flag)
+ moverando_flag = self.read_goal_address(moverando_enabled_offset, sizeof_uint8)
+ self.moverando_enabled = bool(moverando_flag)
except (ProcessError, MemoryReadError, WinAPIError):
logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.")
@@ -251,6 +269,12 @@ class JakAndDaxterMemoryReader:
return self.location_outbox
+ def read_goal_address(self, offset: int, length: int) -> int:
+ return int.from_bytes(
+ self.gk_process.read_bytes(self.goal_address + offset, length),
+ byteorder="little",
+ signed=False)
+
def save_data(self):
with open("jakanddaxter_location_outbox.json", "w+") as f:
dump = {
diff --git a/worlds/jakanddaxter/client/ReplClient.py b/worlds/jakanddaxter/client/ReplClient.py
index 7c176c1224..c1fe78d3b2 100644
--- a/worlds/jakanddaxter/client/ReplClient.py
+++ b/worlds/jakanddaxter/client/ReplClient.py
@@ -10,13 +10,14 @@ from pymem.exception import ProcessNotFound, ProcessError
from CommonClient import logger
from NetUtils import NetworkItem
-from worlds.jakanddaxter.GameID import jak1_id
-from worlds.jakanddaxter.Items import item_table
-from worlds.jakanddaxter.locs import (
+from ..GameID import jak1_id, jak1_max
+from ..Items import item_table
+from ..locs import (
+ OrbLocations as Orbs,
CellLocations as Cells,
ScoutLocations as Flies,
- OrbLocations as Orbs,
- SpecialLocations as Specials)
+ SpecialLocations as Specials,
+ OrbCacheLocations as Caches)
class JakAndDaxterReplClient:
@@ -150,8 +151,10 @@ class JakAndDaxterReplClient:
"(new-sound-id) 1024 0 0 (sound-group sfx) #t))", print_ok=False):
ok_count += 1
- # Disable cheat-mode and debug (close the visual cue).
- # self.send_form("(set! *debug-segment* #f)")
+ # Disable cheat-mode and debug (close the visual cues).
+ if self.send_form("(set! *debug-segment* #f)", print_ok=False):
+ ok_count += 1
+
if self.send_form("(set! *cheat-mode* #f)", print_ok=False):
ok_count += 1
@@ -159,8 +162,8 @@ class JakAndDaxterReplClient:
if self.send_form("(start \'play (get-continue-by-name *game-info* \"title-start\"))"):
ok_count += 1
- # Now wait until we see the success message... 6 times.
- if ok_count == 7:
+ # Now wait until we see the success message... 8 times.
+ if ok_count == 8:
self.connected = True
else:
self.connected = False
@@ -194,10 +197,14 @@ class JakAndDaxterReplClient:
self.receive_power_cell(ap_id)
elif ap_id in range(jak1_id + Flies.fly_offset, jak1_id + Specials.special_offset):
self.receive_scout_fly(ap_id)
- elif ap_id in range(jak1_id + Specials.special_offset, jak1_id + Orbs.orb_offset):
+ elif ap_id in range(jak1_id + Specials.special_offset, jak1_id + Caches.orb_cache_offset):
self.receive_special(ap_id)
- # elif ap_id in range(jak1_id + Orbs.orb_offset, ???):
- # self.receive_precursor_orb(ap_id) # TODO -- Ponder the Orbs.
+ elif ap_id in range(jak1_id + Caches.orb_cache_offset, jak1_id + Orbs.orb_offset):
+ self.receive_move(ap_id)
+ elif ap_id in range(jak1_id + Orbs.orb_offset, jak1_max):
+ self.receive_precursor_orb(ap_id) # Ponder the Orbs.
+ elif ap_id == jak1_max:
+ self.receive_green_eco() # Ponder why I chose to do ID's this way.
else:
raise KeyError(f"Tried to receive item with unknown AP ID {ap_id}.")
@@ -237,6 +244,42 @@ class JakAndDaxterReplClient:
logger.error(f"Unable to receive special unlock {item_table[ap_id]}!")
return ok
+ def receive_move(self, ap_id: int) -> bool:
+ move_id = Caches.to_game_id(ap_id)
+ ok = self.send_form("(send-event "
+ "*target* \'get-archipelago "
+ "(pickup-type ap-move) "
+ "(the float " + str(move_id) + "))")
+ if ok:
+ logger.debug(f"Received the ability to {item_table[ap_id]}!")
+ else:
+ logger.error(f"Unable to receive the ability to {item_table[ap_id]}!")
+ return ok
+
+ def receive_precursor_orb(self, ap_id: int) -> bool:
+ orb_id = Orbs.to_game_id(ap_id)
+ ok = self.send_form("(send-event "
+ "*target* \'get-archipelago "
+ "(pickup-type money) "
+ "(the float " + str(orb_id) + "))")
+ if ok:
+ logger.debug(f"Received a Precursor Orb!")
+ else:
+ logger.error(f"Unable to receive a Precursor Orb!")
+ return ok
+
+ # Green eco pills are our filler item. Use the get-pickup event instead to handle being full health.
+ def receive_green_eco(self) -> bool:
+ ok = self.send_form("(send-event "
+ "*target* \'get-pickup "
+ "(pickup-type eco-pill) "
+ "(the float 1))")
+ if ok:
+ logger.debug(f"Received a green eco pill!")
+ else:
+ logger.error(f"Unable to receive a green eco pill!")
+ return ok
+
def receive_deathlink(self) -> bool:
# Because it should at least be funny sometimes.
diff --git a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md
index 9160701be7..740afb3262 100644
--- a/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md
+++ b/worlds/jakanddaxter/docs/en_Jak and Daxter The Precursor Legacy.md
@@ -12,9 +12,13 @@ At this time, there are several caveats and restrictions:
- This is to prevent hard locks, where an item required for progression is locked behind a trade you can't afford.
## What does randomization do to this game?
-All 101 Power Cells and 112 Scout Flies are now Location Checks and may contain Items for different games,
-as well as different Items from within Jak and Daxter. Additionally, several special checks and corresponding items
-have been added that are required to complete the game.
+The game now contains the following Location checks:
+- All 101 Power Cells
+- All 112 Scout Flies
+- All the Orb Caches that are not in Gol and Maia's Citadel (a total of 11)
+
+These may contain Items for different games, as well as different Items from within Jak and Daxter.
+Additionally, several special checks and corresponding items have been added that are required to complete the game.
## What are the special checks and how do I check them?
| Check Name | How To Check |
@@ -46,6 +50,11 @@ have been added that are required to complete the game.
| Freed The Blue Sage
Freed The Red Sage
Freed The Yellow Sage | The final staircase in Gol and Maia's Citadel |
| Freed The Green Sage | The final elevator in Gol and Maia's Citadel |
+## How do I know which special items I have?
+Open the game's menu, navigate to `Options`, then `Archipelago Options`, then `Item Tracker`.
+This will show you a list of all the special items in the game, ones not normally tracked as power cells or scout flies.
+Gray items indicate you do not possess that item, light blue items indicate you possess that item.
+
## What is the goal of the game once randomized?
To complete the game, you must defeat the Gol and Maia and stop them from opening the Dark Eco silo.
@@ -79,16 +88,51 @@ scout fly. So in short:
- First, you will receive that scout fly, as normal.
- Second, you will immediately complete the "Free 7 Scout Flies" check, which will send out another item.
+## What does Deathlink do?
+If you enable Deathlink, all the other players in your Multiworld who also have it enabled will be linked on death.
+That means when Jak dies in your game, the players in your Deathlink group also die. Likewise, if any of the other
+players die, Jak will also die in a random fashion.
+
+You can turn off Deathlink at any time in the game by opening the game's menu, navigate to `Options`,
+then `Archipelago Options`, then `Deathlink`.
+
+## What does Move Randomizer do?
+If you enable Move Randomizer, most of Jak's movement set will be added to the randomized item pool, and you will need
+to receive the move in order to use it (i.e. you must find it, or another player must send it to you). Some moves have
+prerequisite moves that you must also have in order to use them (e.g. Crouch Jump is dependent on Crouch). Jak will only
+be able to run, swim (including underwater), and perform single jumps. Note that Flut Flut will have access to her full
+movement set at all times.
+
+You can turn off Move Rando at any time in the game by opening the game's menu, navigate to `Options`,
+then `Archipelago Options`, then `Move Randomizer`. This will give you access to the full movement set again.
+
+## What are the movement options in Move Randomizer?
+| Move Name | Prerequisite Moves |
+|-----------------|--------------------|
+| Crouch | |
+| Crouch Jump | Crouch |
+| Crouch Uppercut | Crouch |
+| Roll | |
+| Roll Jump | Roll |
+| Double Jump | |
+| Jump Dive | |
+| Jump Kick | |
+| Punch | |
+| Punch Uppercut | Punch |
+| Kick | |
+
+## How do I know which moves I have?
+Open the game's menu, navigate to `Options`, then `Archipelago Options`, then `Move Tracker`.
+This will show you a list of all the moves in the game.
+- Gray items indicate you do not possess that move.
+- Yellow items indicate you possess that move, but you are missing its prerequisites.
+- Light blue items indicate you possess that move, as well as its prerequisites.
+
## I got soft-locked and can't leave, how do I get out of here?
Open the game's menu, navigate to `Options`, then to `Archipelago Options`, then to `Warp To Home`.
Selecting this option will ask if you want to be teleported to Geyser Rock. From there, you can teleport back
to the nearest sage's hut to continue your journey.
-## How do I know which special items I have?
-Open the game's menu, navigate to `Options`, then to `Archipelago Options`, then to `Item Tracker`.
-This will show you a list of all the special items in the game, ones not normally tracked as power cells or scout flies.
-Grayed-out items indicate you do not possess that item, light blue items indicate you possess that item.
-
## I think I found a bug, where should I report it?
Depending on the nature of the bug, there are a couple of different options.
diff --git a/worlds/jakanddaxter/docs/setup_en.md b/worlds/jakanddaxter/docs/setup_en.md
index db1385a4f8..cdd2bcad7b 100644
--- a/worlds/jakanddaxter/docs/setup_en.md
+++ b/worlds/jakanddaxter/docs/setup_en.md
@@ -102,6 +102,6 @@ Input file iso_data/jak1/MUS/TWEAKVAL.MUS does not exist.
### Known Issues
-- The game needs to run in debug mode in order to allow the repl to connect to it. At some point I want to make sure it can run in retail mode, or at least hide the debug text on screen and play the game's introductory cutscenes properly.
+- The game needs to run in debug mode in order to allow the repl to connect to it. We hide the debug text on screen and play the game's introductory cutscenes properly.
+- The powershell windows cannot be run as background processes due to how the repl works, so the best we can do is minimize them.
- The client is currently not very robust and doesn't handle failures gracefully. This may result in items not being delivered to the game, or location checks not being delivered to the server.
-- The game relates tasks and power cells closely but separately. Some issues may result from custom code to add distinct items to the game (like the Fisherman's Boat, the Pontoons, or the Gondola).
diff --git a/worlds/jakanddaxter/locs/OrbCacheLocations.py b/worlds/jakanddaxter/locs/OrbCacheLocations.py
new file mode 100644
index 0000000000..5d237c797b
--- /dev/null
+++ b/worlds/jakanddaxter/locs/OrbCacheLocations.py
@@ -0,0 +1,50 @@
+from ..GameID import jak1_id
+
+# These are the locations of Orb Caches throughout the game, unlockable only with blue eco.
+# They are not game collectables and thus don't have the same kinds of game ID's. They do, however, have actor ID's.
+# There are a total of 14 in the game.
+
+# When these are opened, we can execute a hook in the mod that might be able to tell us which orb cache we opened,
+# by ID, and that will allow us to map a Location object to it. We'll be using these for Move Randomizer,
+# where each move is "mapped" to an Orb Cache being unlocked. Obviously, they will then be randomized, but with moves
+# not being considered Items by the game, we need to conjure SOME kind of Location for them, and Orb Caches is the best
+# we can do.
+
+# We can use 2^12 to offset these from special checks, just like we offset those from scout flies
+# by 2^11. Special checks don't exceed an ID of (jak1_id + 2153).
+orb_cache_offset = 4096
+
+
+# These helper functions do all the math required to get information about each
+# special check and translate its ID between AP and OpenGOAL. Similar to Scout Flies, these large numbers are not
+# necessary, and we can flatten out the range in which these numbers lie.
+def to_ap_id(game_id: int) -> int:
+ assert game_id < jak1_id, f"Attempted to convert {game_id} to an AP ID, but it already is one."
+ uncompressed_id = jak1_id + orb_cache_offset + game_id # Add the offsets and the orb cache Actor ID.
+ return uncompressed_id - 10344 # Subtract the smallest Actor ID.
+
+
+def to_game_id(ap_id: int) -> int:
+ assert ap_id >= jak1_id, f"Attempted to convert {ap_id} to a Jak 1 ID, but it already is one."
+ uncompressed_id = ap_id + 10344 # Reverse process, add back the smallest Actor ID.
+ return uncompressed_id - jak1_id - orb_cache_offset # Subtract the offsets.
+
+
+# The ID's you see below correlate to the Actor ID of each Orb Cache.
+
+loc_orbCacheTable = {
+ 10344: "Orb Cache in Sandover Village",
+ 10369: "Orb Cache in Forbidden Jungle",
+ 11072: "Orb Cache on Misty Island",
+ 12634: "Orb Cache near Flut Flut Egg",
+ 12635: "Orb Cache near Pelican's Nest",
+ 10945: "Orb Cache in Rock Village",
+ 14507: "Orb Cache in First Sunken Chamber",
+ 14838: "Orb Cache in Second Sunken Chamber",
+ 23348: "Orb Cache in Snowy Fort (1)",
+ 23349: "Orb Cache in Snowy Fort (2)",
+ 23350: "Orb Cache in Snowy Fort (3)",
+ # 24038: "Orb Cache at End of Blast Furnace", # TODO - IDK, we didn't need all of the orb caches for move rando.
+ # 24039: "Orb Cache at End of Launch Pad Room",
+ # 24040: "Orb Cache at Start of Launch Pad Room",
+}
diff --git a/worlds/jakanddaxter/locs/OrbLocations.py b/worlds/jakanddaxter/locs/OrbLocations.py
index 4e586a65fb..6e45a300d0 100644
--- a/worlds/jakanddaxter/locs/OrbLocations.py
+++ b/worlds/jakanddaxter/locs/OrbLocations.py
@@ -16,9 +16,7 @@ from ..GameID import jak1_id
# The maximum number of orbs that any actor can spawn is 30 (the orb caches in citadel). Covering
# these ID-less orbs may need to be a future enhancement. TODO ^^
-# Standalone orbs need 15 bits to identify themselves by Actor ID,
-# so we can use 2^15 to offset them from scout flies, just like we offset
-# scout flies from power cells by 2^10.
+# We can use 2^15 to offset them from Orb Caches, because Orb Cache ID's max out at (jak1_id + 17792).
orb_offset = 32768
diff --git a/worlds/jakanddaxter/regs/BoggySwampRegions.py b/worlds/jakanddaxter/regs/BoggySwampRegions.py
new file mode 100644
index 0000000000..9f5600f5ab
--- /dev/null
+++ b/worlds/jakanddaxter/regs/BoggySwampRegions.py
@@ -0,0 +1,154 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..Rules import can_fight
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ # This level is full of short-medium gaps that cannot be crossed by single jump alone.
+ # These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...)
+ def can_jump_farther(state: CollectionState, p: int) -> bool:
+ return (state.has("Double Jump", p)
+ or state.has("Jump Kick", p)
+ or (state.has("Punch", p) and state.has("Punch Uppercut", p)))
+
+ def can_jump_higher(state: CollectionState, p: int) -> bool:
+ return (state.has("Double Jump", p)
+ or (state.has("Crouch", p) and state.has("Crouch Jump", p))
+ or (state.has("Crouch", p) and state.has("Crouch Uppercut", p))
+ or (state.has("Punch", p) and state.has("Punch Uppercut", p)))
+
+ # Orb crates and fly box in this area can be gotten with yellow eco and goggles.
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23)
+ main_area.add_fly_locations([43])
+
+ # Includes 4 orbs collectable with the blue eco vent.
+ first_bats = JakAndDaxterRegion("First Bats Area", player, multiworld, level_name, 4)
+
+ first_jump_pad = JakAndDaxterRegion("First Jump Pad", player, multiworld, level_name, 0)
+ first_jump_pad.add_fly_locations([393259])
+
+ # The tethers in this level are all out of order... a casual playthrough has the following order for the cell ID's:
+ # 42, 39, 40, 41. So that is the order we're calling "first, second, third, fourth".
+
+ # First tether cell is collectable with yellow eco and goggles.
+ first_tether = JakAndDaxterRegion("First Tether", player, multiworld, level_name, 7)
+ first_tether.add_cell_locations([42])
+
+ # This rat colony has 3 orbs on top of it, requires special movement.
+ first_tether_rat_colony = JakAndDaxterRegion("First Tether Rat Colony", player, multiworld, level_name, 3)
+
+ # If quick enough, combat not required.
+ second_jump_pad = JakAndDaxterRegion("Second Jump Pad", player, multiworld, level_name, 0)
+ second_jump_pad.add_fly_locations([65579])
+
+ first_pole_course = JakAndDaxterRegion("First Pole Course", player, multiworld, level_name, 28)
+
+ # You can break this tether with a yellow eco vent and goggles,
+ # but you can't reach the platform unless you can jump high.
+ second_tether = JakAndDaxterRegion("Second Tether", player, multiworld, level_name, 0)
+ second_tether.add_cell_locations([39], access_rule=lambda state: can_jump_higher(state, player))
+
+ # Fly and orbs are collectable with nearby blue eco cluster.
+ second_bats = JakAndDaxterRegion("Second Bats Area", player, multiworld, level_name, 27)
+ second_bats.add_fly_locations([262187], access_rule=lambda state: can_jump_farther(state, player))
+
+ third_jump_pad = JakAndDaxterRegion("Third Jump Pad (Arena)", player, multiworld, level_name, 0)
+ third_jump_pad.add_cell_locations([38], access_rule=lambda state: can_fight(state, player))
+
+ # The platform for the third tether might look high, but you can get a boost from the yellow eco vent.
+ fourth_jump_pad = JakAndDaxterRegion("Fourth Jump Pad (Third Tether)", player, multiworld, level_name, 9)
+ fourth_jump_pad.add_cell_locations([40])
+
+ # Orbs collectable here with yellow eco and goggles.
+ flut_flut_pad = JakAndDaxterRegion("Flut Flut Pad", player, multiworld, level_name, 36)
+
+ flut_flut_course = JakAndDaxterRegion("Flut Flut Course", player, multiworld, level_name, 23)
+ flut_flut_course.add_cell_locations([37])
+ flut_flut_course.add_fly_locations([327723, 131115])
+
+ # Includes some orbs on the way to the cabin, blue+yellow eco to collect.
+ farthy_snacks = JakAndDaxterRegion("Farthy's Snacks", player, multiworld, level_name, 7)
+ farthy_snacks.add_cell_locations([36])
+
+ # Scout fly in this field can be broken with yellow eco.
+ box_field = JakAndDaxterRegion("Field of Boxes", player, multiworld, level_name, 10)
+ box_field.add_fly_locations([196651])
+
+ last_tar_pit = JakAndDaxterRegion("Last Tar Pit", player, multiworld, level_name, 12)
+
+ fourth_tether = JakAndDaxterRegion("Fourth Tether", player, multiworld, level_name, 11)
+ fourth_tether.add_cell_locations([41], access_rule=lambda state: can_jump_higher(state, player))
+
+ main_area.connect(first_bats, rule=lambda state: can_jump_farther(state, player))
+
+ first_bats.connect(main_area)
+ first_bats.connect(first_jump_pad)
+ first_bats.connect(first_tether)
+
+ first_jump_pad.connect(first_bats)
+
+ first_tether.connect(first_bats)
+ first_tether.connect(first_tether_rat_colony, rule=lambda state:
+ (state.has("Roll", player) and state.has("Roll Jump", player))
+ or (state.has("Double Jump", player)
+ and state.has("Jump Kick", player)))
+ first_tether.connect(second_jump_pad)
+ first_tether.connect(first_pole_course)
+
+ first_tether_rat_colony.connect(first_tether)
+
+ second_jump_pad.connect(first_tether)
+
+ first_pole_course.connect(first_tether)
+ first_pole_course.connect(second_tether)
+
+ second_tether.connect(first_pole_course, rule=lambda state: can_jump_higher(state, player))
+ second_tether.connect(second_bats)
+
+ second_bats.connect(second_tether)
+ second_bats.connect(third_jump_pad)
+ second_bats.connect(fourth_jump_pad)
+ second_bats.connect(flut_flut_pad)
+
+ third_jump_pad.connect(second_bats)
+ fourth_jump_pad.connect(second_bats)
+
+ flut_flut_pad.connect(second_bats)
+ flut_flut_pad.connect(flut_flut_course, rule=lambda state: state.has("Flut Flut", player)) # Naturally.
+ flut_flut_pad.connect(farthy_snacks)
+
+ flut_flut_course.connect(flut_flut_pad)
+
+ farthy_snacks.connect(flut_flut_pad)
+ farthy_snacks.connect(box_field, rule=lambda state: can_jump_higher(state, player))
+
+ box_field.connect(farthy_snacks, rule=lambda state: can_jump_higher(state, player))
+ box_field.connect(last_tar_pit, rule=lambda state: can_jump_farther(state, player))
+
+ last_tar_pit.connect(box_field, rule=lambda state: can_jump_farther(state, player))
+ last_tar_pit.connect(fourth_tether, rule=lambda state: can_jump_farther(state, player))
+
+ fourth_tether.connect(last_tar_pit, rule=lambda state: can_jump_farther(state, player))
+ fourth_tether.connect(main_area) # Fall down.
+
+ multiworld.regions.append(main_area)
+ multiworld.regions.append(first_bats)
+ multiworld.regions.append(first_jump_pad)
+ multiworld.regions.append(first_tether)
+ multiworld.regions.append(first_tether_rat_colony)
+ multiworld.regions.append(second_jump_pad)
+ multiworld.regions.append(first_pole_course)
+ multiworld.regions.append(second_tether)
+ multiworld.regions.append(second_bats)
+ multiworld.regions.append(third_jump_pad)
+ multiworld.regions.append(fourth_jump_pad)
+ multiworld.regions.append(flut_flut_pad)
+ multiworld.regions.append(flut_flut_course)
+ multiworld.regions.append(farthy_snacks)
+ multiworld.regions.append(box_field)
+ multiworld.regions.append(last_tar_pit)
+ multiworld.regions.append(fourth_tether)
+
+ return [main_area]
diff --git a/worlds/jakanddaxter/regs/FireCanyonRegions.py b/worlds/jakanddaxter/regs/FireCanyonRegions.py
new file mode 100644
index 0000000000..b77d28b7d6
--- /dev/null
+++ b/worlds/jakanddaxter/regs/FireCanyonRegions.py
@@ -0,0 +1,17 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..locs import CellLocations as Cells, ScoutLocations as Scouts
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50)
+
+ # Everything is accessible by making contact with the zoomer.
+ main_area.add_cell_locations(Cells.locFC_cellTable.keys())
+ main_area.add_fly_locations(Scouts.locFC_scoutTable.keys())
+
+ multiworld.regions.append(main_area)
+
+ return [main_area]
diff --git a/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py
new file mode 100644
index 0000000000..33e49a9d04
--- /dev/null
+++ b/worlds/jakanddaxter/regs/ForbiddenJungleRegions.py
@@ -0,0 +1,83 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..Rules import can_free_scout_flies, can_fight
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 25)
+
+ # You can get this scout fly by running from the blue eco vent across the temple bridge,
+ # falling onto the river, collecting the 3 blue clusters, using the jump pad, and running straight to the box.
+ main_area.add_fly_locations([393223])
+
+ lurker_machine = JakAndDaxterRegion("Lurker Machine", player, multiworld, level_name, 5)
+ lurker_machine.add_cell_locations([3], access_rule=lambda state: can_fight(state, player))
+
+ # This cell and this scout fly can both be gotten with the blue eco clusters near the jump pad.
+ lurker_machine.add_cell_locations([9])
+ lurker_machine.add_fly_locations([131079])
+
+ river = JakAndDaxterRegion("River", player, multiworld, level_name, 42)
+
+ # All of these can be gotten with blue eco, hitting the dark eco boxes, or by running.
+ river.add_cell_locations([5, 8])
+ river.add_fly_locations([7, 196615])
+ river.add_special_locations([5])
+ river.add_cache_locations([10369])
+
+ temple_exit = JakAndDaxterRegion("Temple Exit", player, multiworld, level_name, 12)
+
+ # This fly is too far from accessible blue eco sources.
+ temple_exit.add_fly_locations([262151], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ temple_exterior = JakAndDaxterRegion("Temple Exterior", player, multiworld, level_name, 10)
+
+ # All of these can be gotten with blue eco and running.
+ temple_exterior.add_cell_locations([4])
+ temple_exterior.add_fly_locations([327687, 65543])
+ temple_exterior.add_special_locations([4])
+
+ temple_int_pre_blue = JakAndDaxterRegion("Temple Interior (Pre Blue Eco)", player, multiworld, level_name, 17)
+ temple_int_pre_blue.add_cell_locations([2])
+ temple_int_pre_blue.add_special_locations([2])
+
+ temple_int_post_blue = JakAndDaxterRegion("Temple Interior (Post Blue Eco)", player, multiworld, level_name, 39)
+ temple_int_post_blue.add_cell_locations([6], access_rule=lambda state: can_fight(state, player))
+
+ main_area.connect(lurker_machine) # Run and jump (tree stump platforms).
+ main_area.connect(river) # Jump down.
+ main_area.connect(temple_exit) # Run and jump (bridges).
+
+ lurker_machine.connect(main_area) # Jump down.
+ lurker_machine.connect(river) # Jump down.
+ lurker_machine.connect(temple_exterior) # Jump down (ledge).
+
+ river.connect(main_area) # Jump up (ledges near fisherman).
+ river.connect(lurker_machine) # Jump pad (aim toward machine).
+ river.connect(temple_exit) # Run and jump (trampolines).
+ river.connect(temple_exterior) # Jump pad (aim toward temple door).
+
+ temple_exit.connect(main_area) # Run and jump (bridges).
+ temple_exit.connect(river) # Jump down.
+ temple_exit.connect(temple_exterior) # Run and jump (bridges, dodge spikes).
+
+ # Requires Jungle Elevator.
+ temple_exterior.connect(temple_int_pre_blue, rule=lambda state: state.has("Jungle Elevator", player))
+
+ # Requires Blue Eco Switch.
+ temple_int_pre_blue.connect(temple_int_post_blue, rule=lambda state: state.has("Blue Eco Switch", player))
+
+ # Requires defeating the plant boss (combat).
+ temple_int_post_blue.connect(temple_exit, rule=lambda state: can_fight(state, player))
+
+ multiworld.regions.append(main_area)
+ multiworld.regions.append(lurker_machine)
+ multiworld.regions.append(river)
+ multiworld.regions.append(temple_exit)
+ multiworld.regions.append(temple_exterior)
+ multiworld.regions.append(temple_int_pre_blue)
+ multiworld.regions.append(temple_int_post_blue)
+
+ return [main_area]
diff --git a/worlds/jakanddaxter/regs/GeyserRockRegions.py b/worlds/jakanddaxter/regs/GeyserRockRegions.py
new file mode 100644
index 0000000000..ed4c4daaf3
--- /dev/null
+++ b/worlds/jakanddaxter/regs/GeyserRockRegions.py
@@ -0,0 +1,26 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..locs import ScoutLocations as Scouts
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50)
+ main_area.add_cell_locations([92, 93])
+ main_area.add_fly_locations(Scouts.locGR_scoutTable.keys()) # All Flies here are accessible with blue eco.
+
+ cliff = JakAndDaxterRegion("Cliff", player, multiworld, level_name, 0)
+ cliff.add_cell_locations([94])
+
+ main_area.connect(cliff, rule=lambda state:
+ ((state.has("Crouch", player) and state.has("Crouch Jump", player))
+ or (state.has("Crouch", player) and state.has("Crouch Uppercut", player))
+ or state.has("Double Jump", player)))
+
+ cliff.connect(main_area) # Jump down or ride blue eco elevator.
+
+ multiworld.regions.append(main_area)
+ multiworld.regions.append(cliff)
+
+ return [main_area]
diff --git a/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py
new file mode 100644
index 0000000000..d9439e0be3
--- /dev/null
+++ b/worlds/jakanddaxter/regs/GolAndMaiasCitadelRegions.py
@@ -0,0 +1,122 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..Rules import can_free_scout_flies, can_fight
+
+
+# God help me... here we go.
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ # This level is full of short-medium gaps that cannot be crossed by single jump alone.
+ # These helper functions list out the moves that can cross all these gaps (painting with a broad brush but...)
+ def can_jump_farther(state: CollectionState, p: int) -> bool:
+ return (state.has("Double Jump", p)
+ or state.has("Jump Kick", p)
+ or (state.has("Punch", p) and state.has("Punch Uppercut", p)))
+
+ def can_uppercut_spin(state: CollectionState, p: int) -> bool:
+ return (state.has("Punch", p)
+ and state.has("Punch Uppercut", p)
+ and state.has("Jump Kick", p))
+
+ def can_triple_jump(state: CollectionState, p: int) -> bool:
+ return state.has("Double Jump", p) and state.has("Jump Kick", p)
+
+ # Don't @ me on the name.
+ def can_move_fancy(state: CollectionState, p: int) -> bool:
+ return can_uppercut_spin(state, p) or can_triple_jump(state, p)
+
+ def can_jump_stairs(state: CollectionState, p: int) -> bool:
+ return (state.has("Double Jump", p)
+ or (state.has("Crouch", p) and state.has("Crouch Jump", p))
+ or (state.has("Crouch", p) and state.has("Crouch Uppercut", p))
+ or state.has("Jump Dive", p))
+
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0)
+ main_area.add_fly_locations([91], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ robot_scaffolding = JakAndDaxterRegion("Scaffolding Around Robot", player, multiworld, level_name, 8)
+ robot_scaffolding.add_fly_locations([196699], access_rule=lambda state:
+ can_free_scout_flies(state, player))
+
+ jump_pad_room = JakAndDaxterRegion("Jump Pad Chamber", player, multiworld, level_name, 88)
+ jump_pad_room.add_cell_locations([73], access_rule=lambda state: can_fight(state, player))
+ jump_pad_room.add_special_locations([73], access_rule=lambda state: can_fight(state, player))
+ jump_pad_room.add_fly_locations([131163]) # Blue eco vent is right next to it.
+ jump_pad_room.add_fly_locations([65627], access_rule=lambda state:
+ can_free_scout_flies(state, player)
+ and can_jump_farther(state, player))
+
+ blast_furnace = JakAndDaxterRegion("Blast Furnace", player, multiworld, level_name, 39)
+ blast_furnace.add_cell_locations([71], access_rule=lambda state: can_fight(state, player))
+ blast_furnace.add_special_locations([71], access_rule=lambda state: can_fight(state, player))
+ blast_furnace.add_fly_locations([393307]) # Blue eco vent nearby.
+
+ bunny_room = JakAndDaxterRegion("Bunny Chamber", player, multiworld, level_name, 45)
+ bunny_room.add_cell_locations([72], access_rule=lambda state: can_fight(state, player))
+ bunny_room.add_special_locations([72], access_rule=lambda state: can_fight(state, player))
+ bunny_room.add_fly_locations([262235], access_rule=lambda state:
+ can_free_scout_flies(state, player))
+
+ rotating_tower = JakAndDaxterRegion("Rotating Tower", player, multiworld, level_name, 20)
+ rotating_tower.add_cell_locations([70], access_rule=lambda state: can_fight(state, player))
+ rotating_tower.add_special_locations([70], access_rule=lambda state: can_fight(state, player))
+ rotating_tower.add_fly_locations([327771], access_rule=lambda state:
+ can_free_scout_flies(state, player))
+
+ final_boss = JakAndDaxterRegion("Final Boss", player, multiworld, level_name, 0)
+
+ # Jump Dive required for a lot of buttons, prepare yourself.
+ main_area.connect(robot_scaffolding, rule=lambda state:
+ state.has("Jump Dive", player)
+ or (state.has("Roll", player) and state.has("Roll Jump", player)))
+ main_area.connect(jump_pad_room)
+
+ robot_scaffolding.connect(main_area, rule=lambda state: state.has("Jump Dive", player))
+ robot_scaffolding.connect(blast_furnace, rule=lambda state:
+ state.has("Jump Dive", player)
+ and ((state.has("Roll", player) and state.has("Roll Jump", player))
+ or can_uppercut_spin(state, player)))
+ robot_scaffolding.connect(bunny_room, rule=lambda state:
+ can_fight(state, player)
+ and (can_move_fancy(state, player)
+ or (state.has("Roll", player) and state.has("Roll Jump", player))))
+
+ jump_pad_room.connect(main_area)
+ jump_pad_room.connect(robot_scaffolding, rule=lambda state:
+ state.has("Jump Dive", player)
+ and ((state.has("Roll", player) and state.has("Roll Jump", player))
+ or can_triple_jump(state, player)))
+
+ blast_furnace.connect(robot_scaffolding) # Blue eco elevator takes you right back.
+
+ bunny_room.connect(robot_scaffolding, rule=lambda state:
+ state.has("Jump Dive", player)
+ and ((state.has("Roll", player) and state.has("Roll Jump", player))
+ or can_triple_jump(state, player)))
+
+ # Final climb.
+ robot_scaffolding.connect(rotating_tower, rule=lambda state:
+ state.has("Freed The Blue Sage", player)
+ and state.has("Freed The Red Sage", player)
+ and state.has("Freed The Yellow Sage", player)
+ and can_jump_stairs(state, player))
+
+ rotating_tower.connect(main_area) # Take stairs back down.
+
+ # You're going to need free-shooting yellow eco to defeat the robot.
+ rotating_tower.connect(final_boss, rule=lambda state:
+ state.has("Freed The Green Sage", player)
+ and state.has("Punch", player))
+
+ final_boss.connect(rotating_tower) # Take elevator back down.
+
+ multiworld.regions.append(main_area)
+ multiworld.regions.append(robot_scaffolding)
+ multiworld.regions.append(jump_pad_room)
+ multiworld.regions.append(blast_furnace)
+ multiworld.regions.append(bunny_room)
+ multiworld.regions.append(rotating_tower)
+ multiworld.regions.append(final_boss)
+
+ return [main_area, final_boss]
diff --git a/worlds/jakanddaxter/regs/LavaTubeRegions.py b/worlds/jakanddaxter/regs/LavaTubeRegions.py
new file mode 100644
index 0000000000..d8c8a7ec41
--- /dev/null
+++ b/worlds/jakanddaxter/regs/LavaTubeRegions.py
@@ -0,0 +1,17 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..locs import CellLocations as Cells, ScoutLocations as Scouts
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50)
+
+ # Everything is accessible by making contact with the zoomer.
+ main_area.add_cell_locations(Cells.locLT_cellTable.keys())
+ main_area.add_fly_locations(Scouts.locLT_scoutTable.keys())
+
+ multiworld.regions.append(main_area)
+
+ return [main_area]
diff --git a/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py
new file mode 100644
index 0000000000..de5251a787
--- /dev/null
+++ b/worlds/jakanddaxter/regs/LostPrecursorCityRegions.py
@@ -0,0 +1,130 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..Rules import can_free_scout_flies, can_fight
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ # Just the starting area.
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 4)
+
+ first_room_upper = JakAndDaxterRegion("First Chamber (Upper)", player, multiworld, level_name, 21)
+
+ first_room_lower = JakAndDaxterRegion("First Chamber (Lower)", player, multiworld, level_name, 0)
+ first_room_lower.add_fly_locations([262193], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ first_room_orb_cache = JakAndDaxterRegion("First Chamber Orb Cache", player, multiworld, level_name, 22)
+
+ # Need jump dive to activate button, double jump to reach blue eco to unlock cache.
+ first_room_orb_cache.add_cache_locations([14507], access_rule=lambda state:
+ state.has("Jump Dive", player)
+ and state.has("Double Jump", player))
+
+ first_hallway = JakAndDaxterRegion("First Hallway", player, multiworld, level_name, 10)
+ first_hallway.add_fly_locations([131121], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ # This entire room is accessible with floating platforms and single jump.
+ second_room = JakAndDaxterRegion("Second Chamber", player, multiworld, level_name, 28)
+
+ # These items can only be gotten with jump dive to activate a button.
+ second_room.add_cell_locations([45], access_rule=lambda state: state.has("Jump Dive", player))
+ second_room.add_fly_locations([49, 65585], access_rule=lambda state: state.has("Jump Dive", player))
+
+ # This is the scout fly on the way to the pipe cell, requires normal breaking moves.
+ second_room.add_fly_locations([196657], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ # This orb vent and scout fly are right next to each other, can be gotten with blue eco and the floating platforms.
+ second_room.add_fly_locations([393265])
+ second_room.add_cache_locations([14838])
+
+ # Named after the cell, includes the armored lurker room.
+ center_complex = JakAndDaxterRegion("Center of the Complex", player, multiworld, level_name, 17)
+ center_complex.add_cell_locations([51])
+
+ color_platforms = JakAndDaxterRegion("Color Platforms", player, multiworld, level_name, 6)
+ color_platforms.add_cell_locations([44], access_rule=lambda state: can_fight(state, player))
+
+ quick_platforms = JakAndDaxterRegion("Quick Platforms", player, multiworld, level_name, 3)
+
+ # Jump dive to activate button.
+ quick_platforms.add_cell_locations([48], access_rule=lambda state: state.has("Jump Dive", player))
+
+ first_slide = JakAndDaxterRegion("First Slide", player, multiworld, level_name, 22)
+
+ # Raised chamber room, includes vent room with scout fly prior to second slide.
+ capsule_room = JakAndDaxterRegion("Capsule Chamber", player, multiworld, level_name, 6)
+
+ # Use jump dive to activate button inside the capsule. Blue eco vent can ready the chamber and get the scout fly.
+ capsule_room.add_cell_locations([47], access_rule=lambda state: state.has("Jump Dive", player))
+ capsule_room.add_fly_locations([327729])
+
+ second_slide = JakAndDaxterRegion("Second Slide", player, multiworld, level_name, 31)
+
+ helix_room = JakAndDaxterRegion("Helix Chamber", player, multiworld, level_name, 30)
+ helix_room.add_cell_locations([46], access_rule=lambda state:
+ state.has("Double Jump", player)
+ or state.has("Jump Kick", player)
+ or (state.has("Punch", player) and state.has("Punch Uppercut", player)))
+ helix_room.add_cell_locations([50], access_rule=lambda state:
+ state.has("Double Jump", player)
+ or can_fight(state, player))
+
+ main_area.connect(first_room_upper) # Run.
+
+ first_room_upper.connect(main_area) # Run.
+ first_room_upper.connect(first_hallway) # Run and jump (floating platforms).
+ first_room_upper.connect(first_room_lower) # Run and jump down.
+
+ first_room_lower.connect(first_room_upper) # Run and jump (floating platforms).
+
+ # Needs some movement to reach these orbs and orb cache.
+ first_room_lower.connect(first_room_orb_cache, rule=lambda state:
+ state.has("Jump Dive", player)
+ and state.has("Double Jump", player))
+ first_room_orb_cache.connect(first_room_lower, rule=lambda state:
+ state.has("Jump Dive", player)
+ and state.has("Double Jump", player))
+
+ first_hallway.connect(first_room_upper) # Run and jump down.
+ first_hallway.connect(second_room) # Run and jump (floating platforms).
+
+ second_room.connect(first_hallway) # Run and jump.
+ second_room.connect(center_complex) # Run and jump down.
+
+ center_complex.connect(second_room) # Run and jump (swim).
+ center_complex.connect(color_platforms) # Run and jump (swim).
+ center_complex.connect(quick_platforms) # Run and jump (swim).
+
+ color_platforms.connect(center_complex) # Run and jump (swim).
+
+ quick_platforms.connect(center_complex) # Run and jump (swim).
+ quick_platforms.connect(first_slide) # Slide.
+
+ first_slide.connect(capsule_room) # Slide.
+
+ capsule_room.connect(second_slide) # Slide.
+ capsule_room.connect(main_area, rule=lambda state: # Chamber goes back to surface.
+ state.has("Jump Dive", player)) # (Assume one-way for sanity.)
+
+ second_slide.connect(helix_room) # Slide.
+
+ helix_room.connect(quick_platforms, rule=lambda state: # Escape to get back to here.
+ state.has("Double Jump", player) # Capsule is a convenient exit to the level.
+ or can_fight(state, player))
+
+ multiworld.regions.append(main_area)
+ multiworld.regions.append(first_room_upper)
+ multiworld.regions.append(first_room_lower)
+ multiworld.regions.append(first_room_orb_cache)
+ multiworld.regions.append(first_hallway)
+ multiworld.regions.append(second_room)
+ multiworld.regions.append(center_complex)
+ multiworld.regions.append(color_platforms)
+ multiworld.regions.append(quick_platforms)
+ multiworld.regions.append(first_slide)
+ multiworld.regions.append(capsule_room)
+ multiworld.regions.append(second_slide)
+ multiworld.regions.append(helix_room)
+
+ return [main_area]
diff --git a/worlds/jakanddaxter/regs/MistyIslandRegions.py b/worlds/jakanddaxter/regs/MistyIslandRegions.py
new file mode 100644
index 0000000000..259e9c5c23
--- /dev/null
+++ b/worlds/jakanddaxter/regs/MistyIslandRegions.py
@@ -0,0 +1,116 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..Rules import can_free_scout_flies, can_fight
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 9)
+
+ muse_course = JakAndDaxterRegion("Muse Course", player, multiworld, level_name, 21)
+ muse_course.add_cell_locations([23])
+ muse_course.add_fly_locations([327708], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ zoomer = JakAndDaxterRegion("Zoomer", player, multiworld, level_name, 32)
+ zoomer.add_cell_locations([27, 29])
+ zoomer.add_fly_locations([393244])
+
+ ship = JakAndDaxterRegion("Ship", player, multiworld, level_name, 10)
+ ship.add_cell_locations([24])
+ ship.add_fly_locations([131100], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ far_side = JakAndDaxterRegion("Far Side", player, multiworld, level_name, 16)
+
+ # In order to even reach this fly, you must use the seesaw or crouch jump.
+ far_side_cliff = JakAndDaxterRegion("Far Side Cliff", player, multiworld, level_name, 5)
+ far_side_cliff.add_fly_locations([28], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ # To carry the blue eco fast enough to open this cache, you need to break the bone bridges along the way.
+ far_side_cache = JakAndDaxterRegion("Far Side Orb Cache", player, multiworld, level_name, 15)
+ far_side_cache.add_cache_locations([11072], access_rule=lambda state: can_fight(state, player))
+
+ barrel_course = JakAndDaxterRegion("Barrel Course", player, multiworld, level_name, 10)
+ barrel_course.add_fly_locations([196636], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ # 14 orbs for the boxes you can only break with the cannon.
+ cannon = JakAndDaxterRegion("Cannon", player, multiworld, level_name, 14)
+ cannon.add_cell_locations([26], access_rule=lambda state: can_fight(state, player))
+
+ upper_approach = JakAndDaxterRegion("Upper Arena Approach", player, multiworld, level_name, 6)
+ upper_approach.add_fly_locations([65564, 262172], access_rule=lambda state:
+ can_free_scout_flies(state, player))
+
+ lower_approach = JakAndDaxterRegion("Lower Arena Approach", player, multiworld, level_name, 7)
+ lower_approach.add_cell_locations([30])
+
+ arena = JakAndDaxterRegion("Arena", player, multiworld, level_name, 5)
+ arena.add_cell_locations([25], access_rule=lambda state: can_fight(state, player))
+
+ main_area.connect(muse_course) # TODO - What do you need to chase the muse the whole way around?
+ main_area.connect(zoomer) # Run and jump down.
+ main_area.connect(ship) # Run and jump.
+ main_area.connect(lower_approach) # Run and jump.
+
+ # Need to break the bone bridge to access.
+ main_area.connect(upper_approach, rule=lambda state: can_fight(state, player))
+
+ muse_course.connect(main_area) # Run and jump down.
+
+ # The zoomer pad is low enough that it requires Crouch Jump specifically.
+ zoomer.connect(main_area, rule=lambda state:
+ (state.has("Crouch", player)
+ and state.has("Crouch Jump", player)))
+
+ ship.connect(main_area) # Run and jump down.
+ ship.connect(far_side) # Run and jump down.
+ ship.connect(barrel_course) # Run and jump (dodge barrels).
+
+ far_side.connect(ship) # Run and jump.
+ far_side.connect(arena) # Run and jump.
+
+ # Only if you can use the seesaw or Crouch Jump from the seesaw's edge.
+ far_side.connect(far_side_cliff, rule=lambda state:
+ (state.has("Crouch", player)
+ and state.has("Crouch Jump", player))
+ or state.has("Jump Dive", player))
+
+ # Only if you can break the bone bridges to carry blue eco over the mud pit.
+ far_side.connect(far_side_cache, rule=lambda state: can_fight(state, player))
+
+ far_side_cliff.connect(far_side) # Run and jump down.
+
+ barrel_course.connect(cannon) # Run and jump (dodge barrels).
+
+ cannon.connect(barrel_course) # Run and jump (dodge barrels).
+ cannon.connect(arena) # Run and jump down.
+ cannon.connect(upper_approach) # Run and jump down.
+
+ upper_approach.connect(lower_approach) # Jump down.
+ upper_approach.connect(arena) # Jump down.
+
+ # One cliff is accessible, but only via Crouch Jump.
+ lower_approach.connect(upper_approach, rule=lambda state:
+ (state.has("Crouch", player)
+ and state.has("Crouch Jump", player)))
+
+ # Requires breaking bone bridges.
+ lower_approach.connect(arena, rule=lambda state: can_fight(state, player))
+
+ arena.connect(lower_approach) # Run.
+ arena.connect(far_side) # Run.
+
+ multiworld.regions.append(main_area)
+ multiworld.regions.append(muse_course)
+ multiworld.regions.append(zoomer)
+ multiworld.regions.append(ship)
+ multiworld.regions.append(far_side)
+ multiworld.regions.append(far_side_cliff)
+ multiworld.regions.append(far_side_cache)
+ multiworld.regions.append(barrel_course)
+ multiworld.regions.append(cannon)
+ multiworld.regions.append(upper_approach)
+ multiworld.regions.append(lower_approach)
+ multiworld.regions.append(arena)
+
+ return [main_area]
diff --git a/worlds/jakanddaxter/regs/MountainPassRegions.py b/worlds/jakanddaxter/regs/MountainPassRegions.py
new file mode 100644
index 0000000000..b1eaea1019
--- /dev/null
+++ b/worlds/jakanddaxter/regs/MountainPassRegions.py
@@ -0,0 +1,34 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..locs import ScoutLocations as Scouts
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ # This is basically just Klaww.
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0)
+ main_area.add_cell_locations([86])
+
+ race = JakAndDaxterRegion("Race", player, multiworld, level_name, 50)
+ race.add_cell_locations([87])
+
+ # All scout flies can be broken with the zoomer.
+ race.add_fly_locations(Scouts.locMP_scoutTable.keys())
+
+ shortcut = JakAndDaxterRegion("Shortcut", player, multiworld, level_name, 0)
+ shortcut.add_cell_locations([110])
+
+ main_area.connect(race)
+
+ # You cannot go backwards from Klaww.
+ race.connect(shortcut, rule=lambda state: state.has("Yellow Eco Switch", player))
+
+ shortcut.connect(race)
+
+ multiworld.regions.append(main_area)
+ multiworld.regions.append(race)
+ multiworld.regions.append(shortcut)
+
+ # Return race required for inter-level connections.
+ return [main_area, race]
diff --git a/worlds/jakanddaxter/regs/PrecursorBasinRegions.py b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py
new file mode 100644
index 0000000000..7b1ea8a883
--- /dev/null
+++ b/worlds/jakanddaxter/regs/PrecursorBasinRegions.py
@@ -0,0 +1,17 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..locs import CellLocations as Cells, ScoutLocations as Scouts
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 200)
+
+ # Everything is accessible by making contact with the zoomer.
+ main_area.add_cell_locations(Cells.locPB_cellTable.keys())
+ main_area.add_fly_locations(Scouts.locPB_scoutTable.keys())
+
+ multiworld.regions.append(main_area)
+
+ return [main_area]
diff --git a/worlds/jakanddaxter/regs/RegionBase.py b/worlds/jakanddaxter/regs/RegionBase.py
new file mode 100644
index 0000000000..40e4cb9273
--- /dev/null
+++ b/worlds/jakanddaxter/regs/RegionBase.py
@@ -0,0 +1,69 @@
+from typing import List, Callable
+from BaseClasses import MultiWorld, Region
+from ..GameID import jak1_name
+from ..JakAndDaxterOptions import JakAndDaxterOptions
+from ..Locations import JakAndDaxterLocation, location_table
+from ..locs import (CellLocations as Cells,
+ ScoutLocations as Scouts,
+ SpecialLocations as Specials,
+ OrbCacheLocations as Caches)
+
+
+class JakAndDaxterRegion(Region):
+ """
+ Holds region information such as name, level name, number of orbs available, etc.
+ We especially need orb counts to be tracked because we need to know when you can
+ afford the 90-orb and 120-orb payments for more checks.
+ """
+ game: str = jak1_name
+ level_name: str
+ orb_count: int
+
+ def __init__(self, name: str, player: int, multiworld: MultiWorld, level_name: str = "", orb_count: int = 0):
+ formatted_name = f"{level_name} {name}".strip()
+ super().__init__(formatted_name, player, multiworld)
+ self.level_name = level_name
+ self.orb_count = orb_count
+
+ def add_cell_locations(self, locations: List[int], access_rule: Callable = None):
+ """
+ Adds a Power Cell Location to this region with the given access rule.
+ Converts Game ID's to AP ID's for you.
+ """
+ for loc in locations:
+ self.add_jak_locations(Cells.to_ap_id(loc), access_rule)
+
+ def add_fly_locations(self, locations: List[int], access_rule: Callable = None):
+ """
+ Adds a Scout Fly Location to this region with the given access rule.
+ Converts Game ID's to AP ID's for you.
+ """
+ for loc in locations:
+ self.add_jak_locations(Scouts.to_ap_id(loc), access_rule)
+
+ def add_special_locations(self, locations: List[int], access_rule: Callable = None):
+ """
+ Adds a Special Location to this region with the given access rule.
+ Converts Game ID's to AP ID's for you.
+ Special Locations should be matched alongside their respective
+ Power Cell Locations, so you get 2 unlocks for these rather than 1.
+ """
+ for loc in locations:
+ self.add_jak_locations(Specials.to_ap_id(loc), access_rule)
+
+ def add_cache_locations(self, locations: List[int], access_rule: Callable = None):
+ """
+ Adds an Orb Cache Location to this region with the given access rule.
+ Converts Game ID's to AP ID's for you.
+ """
+ for loc in locations:
+ self.add_jak_locations(Caches.to_ap_id(loc), access_rule)
+
+ def add_jak_locations(self, ap_id: int, access_rule: Callable = None):
+ """
+ Helper function to add Locations. Not to be used directly.
+ """
+ location = JakAndDaxterLocation(self.player, location_table[ap_id], ap_id, self)
+ if access_rule:
+ location.access_rule = access_rule
+ self.locations.append(location)
diff --git a/worlds/jakanddaxter/regs/RockVillageRegions.py b/worlds/jakanddaxter/regs/RockVillageRegions.py
new file mode 100644
index 0000000000..09b0858ff3
--- /dev/null
+++ b/worlds/jakanddaxter/regs/RockVillageRegions.py
@@ -0,0 +1,63 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..Rules import can_free_scout_flies, can_trade
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ # This includes most of the area surrounding LPC as well, for orb_count purposes. You can swim and single jump.
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 23)
+ main_area.add_cell_locations([31], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530))
+ main_area.add_cell_locations([32], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530))
+ main_area.add_cell_locations([33], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530))
+ main_area.add_cell_locations([34], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530))
+ main_area.add_cell_locations([35], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530, 34))
+
+ # These 2 scout fly boxes can be broken by running with nearby blue eco.
+ main_area.add_fly_locations([196684, 262220])
+ main_area.add_fly_locations([76, 131148, 65612, 327756], access_rule=lambda state:
+ can_free_scout_flies(state, player))
+
+ # Warrior Pontoon check. You just talk to him and get his introduction.
+ main_area.add_special_locations([33])
+
+ orb_cache = JakAndDaxterRegion("Orb Cache", player, multiworld, level_name, 20)
+
+ # You need roll jump to be able to reach this before the blue eco runs out.
+ orb_cache.add_cache_locations([10945], access_rule=lambda state:
+ (state.has("Roll", player) and state.has("Roll Jump", player)))
+
+ pontoon_bridge = JakAndDaxterRegion("Pontoon Bridge", player, multiworld, level_name, 7)
+ pontoon_bridge.add_fly_locations([393292], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ klaww_cliff = JakAndDaxterRegion("Klaww's Cliff", player, multiworld, level_name, 0)
+
+ main_area.connect(orb_cache, rule=lambda state: (state.has("Roll", player) and state.has("Roll Jump", player)))
+ main_area.connect(pontoon_bridge, rule=lambda state: state.has("Warrior's Pontoons", player))
+
+ orb_cache.connect(main_area)
+
+ pontoon_bridge.connect(main_area, rule=lambda state: state.has("Warrior's Pontoons", player))
+ pontoon_bridge.connect(klaww_cliff, rule=lambda state:
+ state.has("Double Jump", player)
+ or (state.has("Crouch", player)
+ and state.has("Crouch Jump", player))
+ or (state.has("Crouch", player)
+ and state.has("Crouch Uppercut", player)
+ and state.has("Jump Kick", player)))
+
+ klaww_cliff.connect(pontoon_bridge) # Just jump back down.
+
+ multiworld.regions.append(main_area)
+ multiworld.regions.append(orb_cache)
+ multiworld.regions.append(pontoon_bridge)
+ multiworld.regions.append(klaww_cliff)
+
+ # Return klaww_cliff required for inter-level connections.
+ return [main_area, pontoon_bridge, klaww_cliff]
diff --git a/worlds/jakanddaxter/regs/SandoverVillageRegions.py b/worlds/jakanddaxter/regs/SandoverVillageRegions.py
new file mode 100644
index 0000000000..ac4d904a45
--- /dev/null
+++ b/worlds/jakanddaxter/regs/SandoverVillageRegions.py
@@ -0,0 +1,71 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..Rules import can_free_scout_flies, can_trade
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 26)
+
+ # Yakows requires no combat.
+ main_area.add_cell_locations([10])
+ main_area.add_cell_locations([11], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530))
+ main_area.add_cell_locations([12], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530))
+
+ # These 4 scout fly boxes can be broken by running with all the blue eco from Sentinel Beach.
+ main_area.add_fly_locations([262219, 327755, 131147, 65611])
+
+ # The farmer's scout fly. You can either get the Orb Cache Cliff blue eco, or break it normally.
+ main_area.add_fly_locations([196683], access_rule=lambda state:
+ (state.has("Crouch", player) and state.has("Crouch Jump", player))
+ or state.has("Double Jump", player)
+ or can_free_scout_flies(state, player))
+
+ orb_cache_cliff = JakAndDaxterRegion("Orb Cache Cliff", player, multiworld, level_name, 15)
+ orb_cache_cliff.add_cache_locations([10344])
+
+ yakow_cliff = JakAndDaxterRegion("Yakow Cliff", player, multiworld, level_name, 3)
+ yakow_cliff.add_fly_locations([75], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ oracle_platforms = JakAndDaxterRegion("Oracle Platforms", player, multiworld, level_name, 6)
+ oracle_platforms.add_cell_locations([13], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530))
+ oracle_platforms.add_cell_locations([14], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530, 13))
+ oracle_platforms.add_fly_locations([393291], access_rule=lambda state:
+ can_free_scout_flies(state, player))
+
+ main_area.connect(orb_cache_cliff, rule=lambda state:
+ state.has("Double Jump", player)
+ or (state.has("Crouch", player)
+ and state.has("Crouch Jump", player))
+ or (state.has("Crouch", player)
+ and state.has("Crouch Uppercut", player)
+ and state.has("Jump Kick", player)))
+
+ main_area.connect(yakow_cliff, rule=lambda state:
+ state.has("Double Jump", player)
+ or (state.has("Crouch", player)
+ and state.has("Crouch Jump", player))
+ or (state.has("Crouch", player)
+ and state.has("Crouch Uppercut", player)
+ and state.has("Jump Kick", player)))
+
+ main_area.connect(oracle_platforms, rule=lambda state:
+ (state.has("Roll", player) and state.has("Roll Jump", player))
+ or (state.has("Double Jump", player) and state.has("Jump Kick", player)))
+
+ # All these can go back to main_area immediately.
+ orb_cache_cliff.connect(main_area)
+ yakow_cliff.connect(main_area)
+ oracle_platforms.connect(main_area)
+
+ multiworld.regions.append(main_area)
+ multiworld.regions.append(orb_cache_cliff)
+ multiworld.regions.append(yakow_cliff)
+ multiworld.regions.append(oracle_platforms)
+
+ return [main_area]
diff --git a/worlds/jakanddaxter/regs/SentinelBeachRegions.py b/worlds/jakanddaxter/regs/SentinelBeachRegions.py
new file mode 100644
index 0000000000..0e85dc0573
--- /dev/null
+++ b/worlds/jakanddaxter/regs/SentinelBeachRegions.py
@@ -0,0 +1,85 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..Rules import can_free_scout_flies, can_fight
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 128)
+ main_area.add_cell_locations([18, 21, 22])
+
+ # These 3 scout fly boxes can be broken by running with freely accessible blue eco.
+ main_area.add_fly_locations([327700, 20, 65556])
+
+ # These 2 scout fly boxes can be broken with the locked blue eco vent, or by normal combat tricks.
+ main_area.add_fly_locations([262164, 393236], access_rule=lambda state:
+ state.has("Blue Eco Switch", player)
+ or can_free_scout_flies(state, player))
+
+ # No need for the blue eco vent for the orb caches.
+ main_area.add_cache_locations([12634, 12635])
+
+ pelican = JakAndDaxterRegion("Pelican", player, multiworld, level_name, 0)
+ pelican.add_cell_locations([16], access_rule=lambda state: can_fight(state, player))
+
+ # Only these specific attacks can push the flut flut egg off the cliff.
+ flut_flut_egg = JakAndDaxterRegion("Flut Flut Egg", player, multiworld, level_name, 0)
+ flut_flut_egg.add_cell_locations([17], access_rule=lambda state:
+ state.has("Punch", player)
+ or state.has("Kick", player)
+ or state.has("Jump Kick", player))
+ flut_flut_egg.add_special_locations([17], access_rule=lambda state:
+ state.has("Punch", player)
+ or state.has("Kick", player)
+ or state.has("Jump Kick", player))
+
+ eco_harvesters = JakAndDaxterRegion("Eco Harvesters", player, multiworld, level_name, 0)
+ eco_harvesters.add_cell_locations([15], access_rule=lambda state: can_fight(state, player))
+
+ green_ridge = JakAndDaxterRegion("Ridge Near Green Vents", player, multiworld, level_name, 5)
+ green_ridge.add_fly_locations([131092], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ blue_ridge = JakAndDaxterRegion("Ridge Near Blue Vent", player, multiworld, level_name, 5)
+ blue_ridge.add_fly_locations([196628], access_rule=lambda state:
+ state.has("Blue Eco Switch", player)
+ or can_free_scout_flies(state, player))
+
+ cannon_tower = JakAndDaxterRegion("Cannon Tower", player, multiworld, level_name, 12)
+ cannon_tower.add_cell_locations([19], access_rule=lambda state: can_fight(state, player))
+
+ main_area.connect(pelican) # Swim and jump.
+ main_area.connect(flut_flut_egg) # Run and jump.
+ main_area.connect(eco_harvesters) # Run.
+
+ # You don't need any kind of uppercut to reach this place, just a high jump from a convenient nearby ledge.
+ main_area.connect(green_ridge, rule=lambda state:
+ (state.has("Crouch", player) and state.has("Crouch Jump", player))
+ or state.has("Double Jump", player))
+
+ # Can either uppercut the log and jump from it, or use the blue eco jump pad.
+ main_area.connect(blue_ridge, rule=lambda state:
+ state.has("Blue Eco Switch", player)
+ or (state.has("Double Jump", player)
+ and ((state.has("Crouch", player) and state.has("Crouch Uppercut", player))
+ or (state.has("Punch", player) and state.has("Punch Uppercut", player)))))
+
+ main_area.connect(cannon_tower, rule=lambda state: state.has("Blue Eco Switch", player))
+
+ # All these can go back to main_area immediately.
+ pelican.connect(main_area)
+ flut_flut_egg.connect(main_area)
+ eco_harvesters.connect(main_area)
+ green_ridge.connect(main_area)
+ blue_ridge.connect(main_area)
+ cannon_tower.connect(main_area)
+
+ multiworld.regions.append(main_area)
+ multiworld.regions.append(pelican)
+ multiworld.regions.append(flut_flut_egg)
+ multiworld.regions.append(eco_harvesters)
+ multiworld.regions.append(green_ridge)
+ multiworld.regions.append(blue_ridge)
+ multiworld.regions.append(cannon_tower)
+
+ return [main_area]
diff --git a/worlds/jakanddaxter/regs/SnowyMountainRegions.py b/worlds/jakanddaxter/regs/SnowyMountainRegions.py
new file mode 100644
index 0000000000..0c1ffe9428
--- /dev/null
+++ b/worlds/jakanddaxter/regs/SnowyMountainRegions.py
@@ -0,0 +1,181 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..Rules import can_free_scout_flies, can_fight
+
+
+# God help me... here we go.
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ # We need a few helper functions.
+ def can_uppercut_spin(state: CollectionState, p: int) -> bool:
+ return (state.has("Punch", p)
+ and state.has("Punch Uppercut", p)
+ and state.has("Jump Kick", p))
+
+ def can_triple_jump(state: CollectionState, p: int) -> bool:
+ return state.has("Double Jump", p) and state.has("Jump Kick", p)
+
+ # Don't @ me on the name.
+ def can_move_fancy(state: CollectionState, p: int) -> bool:
+ return can_uppercut_spin(state, p) or can_triple_jump(state, p)
+
+ def can_jump_blockers(state: CollectionState, p: int) -> bool:
+ return (state.has("Double Jump", p)
+ or (state.has("Crouch", p) and state.has("Crouch Jump", p))
+ or (state.has("Crouch", p) and state.has("Crouch Uppercut", p))
+ or (state.has("Punch", p) and state.has("Punch Uppercut", p))
+ or state.has("Jump Dive", p))
+
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 0)
+ main_area.add_fly_locations([65], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ # We need a few virtual regions like we had for Dark Crystals in Spider Cave.
+ # First, a virtual region for the glacier lurkers, who all require combat.
+ glacier_lurkers = JakAndDaxterRegion("Glacier Lurkers", player, multiworld, level_name, 0)
+ glacier_lurkers.add_cell_locations([61], access_rule=lambda state: can_fight(state, player))
+
+ # Second, a virtual region for the precursor blockers. Unlike the others, this contains orbs:
+ # the total number of orbs that sit on top of the blockers. Yes, there are only 8.
+ blockers = JakAndDaxterRegion("Precursor Blockers", player, multiworld, level_name, 8)
+ blockers.add_cell_locations([66], access_rule=lambda state: can_fight(state, player))
+
+ snowball_canyon = JakAndDaxterRegion("Snowball Canyon", player, multiworld, level_name, 28)
+
+ frozen_box_cave = JakAndDaxterRegion("Frozen Box Cave", player, multiworld, level_name, 12)
+ frozen_box_cave.add_cell_locations([67], access_rule=lambda state: state.has("Yellow Eco Switch", player))
+ frozen_box_cave.add_fly_locations([327745], access_rule=lambda state:
+ state.has("Yellow Eco Switch", player)
+ or can_free_scout_flies(state, player))
+
+ frozen_box_cave_crates = JakAndDaxterRegion("Frozen Box Cave Orb Crates", player, multiworld, level_name, 8)
+
+ # Include 6 orbs on the twin elevator ice ramp.
+ ice_skating_rink = JakAndDaxterRegion("Ice Skating Rink", player, multiworld, level_name, 20)
+ ice_skating_rink.add_fly_locations([131137], access_rule=lambda state: can_free_scout_flies(state, player))
+
+ flut_flut_course = JakAndDaxterRegion("Flut Flut Course", player, multiworld, level_name, 15)
+ flut_flut_course.add_cell_locations([63], access_rule=lambda state: state.has("Flut Flut", player))
+ flut_flut_course.add_special_locations([63], access_rule=lambda state: state.has("Flut Flut", player))
+
+ # Includes the bridge from snowball_canyon, the area beneath that bridge, and the areas around the fort.
+ fort_exterior = JakAndDaxterRegion("Fort Exterior", player, multiworld, level_name, 20)
+ fort_exterior.add_fly_locations([65601, 393281], access_rule=lambda state:
+ can_free_scout_flies(state, player))
+
+ # Includes the icy island and bridge outside the cave entrance.
+ bunny_cave_start = JakAndDaxterRegion("Bunny Cave (Start)", player, multiworld, level_name, 10)
+
+ # Includes the cell and 3 orbs at the exit.
+ bunny_cave_end = JakAndDaxterRegion("Bunny Cave (End)", player, multiworld, level_name, 3)
+ bunny_cave_end.add_cell_locations([64])
+
+ switch_cave = JakAndDaxterRegion("Yellow Eco Switch Cave", player, multiworld, level_name, 4)
+ switch_cave.add_cell_locations([60])
+ switch_cave.add_special_locations([60])
+
+ # Only what can be covered by single jump.
+ fort_interior = JakAndDaxterRegion("Fort Interior (Main)", player, multiworld, level_name, 19)
+
+ # Reaching the top of the watch tower, getting the fly with the blue eco, and falling down to get the caches.
+ fort_interior_caches = JakAndDaxterRegion("Fort Interior (Caches)", player, multiworld, level_name, 51)
+ fort_interior_caches.add_fly_locations([196673])
+ fort_interior_caches.add_cache_locations([23348, 23349, 23350])
+
+ # Need higher jump.
+ fort_interior_base = JakAndDaxterRegion("Fort Interior (Base)", player, multiworld, level_name, 0)
+ fort_interior_base.add_fly_locations([262209], access_rule=lambda state:
+ can_free_scout_flies(state, player))
+
+ # Need farther jump.
+ fort_interior_course_end = JakAndDaxterRegion("Fort Interior (Course End)", player, multiworld, level_name, 2)
+ fort_interior_course_end.add_cell_locations([62])
+
+ # Wire up the virtual regions first.
+ main_area.connect(blockers, rule=lambda state: can_jump_blockers(state, player))
+ main_area.connect(glacier_lurkers, rule=lambda state: can_fight(state, player))
+
+ # Yes, the only way into the rest of the level requires advanced movement.
+ main_area.connect(snowball_canyon, rule=lambda state:
+ (state.has("Roll", player) and state.has("Roll Jump", player))
+ or can_move_fancy(state, player))
+
+ snowball_canyon.connect(main_area) # But you can just jump down and run up the ramp.
+ snowball_canyon.connect(bunny_cave_start) # Jump down from the glacier troop cliff.
+ snowball_canyon.connect(fort_exterior) # Jump down, to the left of frozen box cave.
+ snowball_canyon.connect(frozen_box_cave, rule=lambda state: # More advanced movement.
+ can_move_fancy(state, player))
+
+ frozen_box_cave.connect(snowball_canyon, rule=lambda state: # Same movement to go back.
+ can_move_fancy(state, player))
+ frozen_box_cave.connect(frozen_box_cave_crates, rule=lambda state: # Same movement to get these crates.
+ state.has("Yellow Eco Switch", player)
+ and can_move_fancy(state, player))
+ frozen_box_cave.connect(ice_skating_rink, rule=lambda state: # Same movement to go forward.
+ can_move_fancy(state, player))
+
+ frozen_box_cave_crates.connect(frozen_box_cave) # Semi-virtual region, no moves req'd.
+
+ ice_skating_rink.connect(frozen_box_cave, rule=lambda state: # Same movement to go back.
+ can_move_fancy(state, player))
+ ice_skating_rink.connect(flut_flut_course, rule=lambda state: # Duh.
+ state.has("Flut Flut", player))
+ ice_skating_rink.connect(fort_exterior) # Just slide down the elevator ramp.
+
+ fort_exterior.connect(ice_skating_rink, rule=lambda state: # Twin elevators are tough to reach.
+ state.has("Double Jump", player)
+ or state.has("Jump Kick", player))
+ fort_exterior.connect(snowball_canyon) # Run across bridge.
+ fort_exterior.connect(fort_interior, rule=lambda state: # Duh.
+ state.has("Snowy Fort Gate", player))
+ fort_exterior.connect(bunny_cave_start) # Run across bridge.
+ fort_exterior.connect(switch_cave, rule=lambda state: # Yes, blocker jumps work here.
+ can_jump_blockers(state, player))
+
+ fort_interior.connect(fort_interior_caches, rule=lambda state: # Just need a little height.
+ (state.has("Crouch", player)
+ and state.has("Crouch Jump", player))
+ or state.has("Double Jump", player))
+ fort_interior.connect(fort_interior_base, rule=lambda state: # Just need a little height.
+ (state.has("Crouch", player)
+ and state.has("Crouch Jump", player))
+ or state.has("Double Jump", player))
+ fort_interior.connect(fort_interior_course_end, rule=lambda state: # Just need a little distance.
+ (state.has("Punch", player)
+ and state.has("Punch Uppercut", player))
+ or state.has("Double Jump", player))
+
+ flut_flut_course.connect(fort_exterior) # Ride the elevator.
+
+ # Must fight way through cave, but there is also a grab-less ledge we must jump over.
+ bunny_cave_start.connect(bunny_cave_end, rule=lambda state:
+ can_fight(state, player)
+ and ((state.has("Crouch", player) and state.has("Crouch Jump", player))
+ or state.has("Double Jump", player)))
+
+ # All jump down.
+ fort_interior_caches.connect(fort_interior)
+ fort_interior_base.connect(fort_interior)
+ fort_interior_course_end.connect(fort_interior)
+ switch_cave.connect(fort_exterior)
+ bunny_cave_end.connect(fort_exterior)
+
+ # I really hope that is everything.
+ multiworld.regions.append(main_area)
+ multiworld.regions.append(glacier_lurkers)
+ multiworld.regions.append(blockers)
+ multiworld.regions.append(snowball_canyon)
+ multiworld.regions.append(frozen_box_cave)
+ multiworld.regions.append(frozen_box_cave_crates)
+ multiworld.regions.append(ice_skating_rink)
+ multiworld.regions.append(flut_flut_course)
+ multiworld.regions.append(fort_exterior)
+ multiworld.regions.append(bunny_cave_start)
+ multiworld.regions.append(bunny_cave_end)
+ multiworld.regions.append(switch_cave)
+ multiworld.regions.append(fort_interior)
+ multiworld.regions.append(fort_interior_caches)
+ multiworld.regions.append(fort_interior_base)
+ multiworld.regions.append(fort_interior_course_end)
+
+ return [main_area]
diff --git a/worlds/jakanddaxter/regs/SpiderCaveRegions.py b/worlds/jakanddaxter/regs/SpiderCaveRegions.py
new file mode 100644
index 0000000000..3d9e1093e1
--- /dev/null
+++ b/worlds/jakanddaxter/regs/SpiderCaveRegions.py
@@ -0,0 +1,110 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..Rules import can_free_scout_flies, can_fight
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ # A large amount of this area can be covered by single jump, floating platforms, web trampolines, and goggles.
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 63)
+ main_area.add_cell_locations([78, 84])
+ main_area.add_fly_locations([327765, 393301, 196693, 131157])
+
+ # This is a virtual region describing what you need to DO to get the Dark Crystal power cell,
+ # rather than describing where each of the crystals ARE, because you can destroy them in any order,
+ # and you need to destroy ALL of them to get the cell.
+ dark_crystals = JakAndDaxterRegion("Dark Crystals", player, multiworld, level_name, 0)
+
+ # can_fight = The underwater crystal in dark cave.
+ # Roll Jump = The underwater crystal across a long dark eco pool.
+ # The rest of the crystals can be destroyed with yellow eco in main_area.
+ dark_crystals.add_cell_locations([79], access_rule=lambda state:
+ can_fight(state, player)
+ and (state.has("Roll", player) and state.has("Roll Jump", player)))
+
+ dark_cave = JakAndDaxterRegion("Dark Cave", player, multiworld, level_name, 5)
+ dark_cave.add_cell_locations([80], access_rule=lambda state:
+ can_fight(state, player)
+ and ((state.has("Crouch", player) and state.has("Crouch Jump", player))
+ or state.has("Double Jump", player)))
+ dark_cave.add_fly_locations([262229], access_rule=lambda state:
+ can_fight(state, player)
+ and can_free_scout_flies(state, player)
+ and ((state.has("Crouch", player) and state.has("Crouch Jump", player))
+ or state.has("Double Jump", player)))
+
+ robot_cave = JakAndDaxterRegion("Robot Cave", player, multiworld, level_name, 0)
+
+ # Need double jump for orbs.
+ scaffolding_level_zero = JakAndDaxterRegion("Robot Scaffolding Level 0", player, multiworld, level_name, 12)
+
+ scaffolding_level_one = JakAndDaxterRegion("Robot Scaffolding Level 1", player, multiworld, level_name, 53)
+ scaffolding_level_one.add_fly_locations([85]) # Shootable.
+
+ scaffolding_level_two = JakAndDaxterRegion("Robot Scaffolding Level 2", player, multiworld, level_name, 4)
+
+ scaffolding_level_three = JakAndDaxterRegion("Robot Scaffolding Level 3", player, multiworld, level_name, 29)
+ scaffolding_level_three.add_cell_locations([81])
+ scaffolding_level_three.add_fly_locations([65621], access_rule=lambda state:
+ can_free_scout_flies(state, player)) # Not shootable.
+
+ pole_course = JakAndDaxterRegion("Pole Course", player, multiworld, level_name, 18)
+ pole_course.add_cell_locations([82])
+
+ # You only need combat to fight through the spiders, but to collect the orb crates,
+ # you will need the yellow eco vent unlocked.
+ spider_tunnel = JakAndDaxterRegion("Spider Tunnel", player, multiworld, level_name, 4)
+ spider_tunnel.add_cell_locations([83])
+
+ spider_tunnel_crates = JakAndDaxterRegion("Spider Tunnel Orb Crates", player, multiworld, level_name, 12)
+
+ main_area.connect(dark_crystals)
+ main_area.connect(robot_cave)
+ main_area.connect(dark_cave, rule=lambda state: can_fight(state, player))
+
+ robot_cave.connect(main_area)
+ robot_cave.connect(pole_course) # Nothing special required.
+ robot_cave.connect(scaffolding_level_one) # Ramps lead to level 1.
+ robot_cave.connect(spider_tunnel) # Web trampolines (bounce twice on each to gain momentum).
+
+ pole_course.connect(robot_cave) # Blue eco platform down.
+
+ scaffolding_level_one.connect(robot_cave) # All scaffolding (level 1+) connects back by jumping down.
+
+ # Elevator, but the orbs need double jump.
+ scaffolding_level_one.connect(scaffolding_level_zero, rule=lambda state: state.has("Double Jump", player))
+
+ # Narrow enough that enemies are unavoidable.
+ scaffolding_level_one.connect(scaffolding_level_two, rule=lambda state: can_fight(state, player))
+
+ scaffolding_level_zero.connect(scaffolding_level_one) # Elevator.
+
+ scaffolding_level_two.connect(robot_cave) # Jump down.
+ scaffolding_level_two.connect(scaffolding_level_one) # Elevator.
+
+ # Elevator, but narrow enough that enemies are unavoidable.
+ scaffolding_level_two.connect(scaffolding_level_three, rule=lambda state: can_fight(state, player))
+
+ scaffolding_level_three.connect(robot_cave) # Jump down.
+ scaffolding_level_three.connect(scaffolding_level_two) # Elevator.
+
+ spider_tunnel.connect(robot_cave) # Back to web trampolines.
+ spider_tunnel.connect(main_area) # Escape with jump pad.
+
+ # Requires yellow eco switch.
+ spider_tunnel.connect(spider_tunnel_crates, rule=lambda state: state.has("Yellow Eco Switch", player))
+
+ multiworld.regions.append(main_area)
+ multiworld.regions.append(dark_crystals)
+ multiworld.regions.append(dark_cave)
+ multiworld.regions.append(robot_cave)
+ multiworld.regions.append(scaffolding_level_zero)
+ multiworld.regions.append(scaffolding_level_one)
+ multiworld.regions.append(scaffolding_level_two)
+ multiworld.regions.append(scaffolding_level_three)
+ multiworld.regions.append(pole_course)
+ multiworld.regions.append(spider_tunnel)
+ multiworld.regions.append(spider_tunnel_crates)
+
+ return [main_area]
diff --git a/worlds/jakanddaxter/regs/VolcanicCraterRegions.py b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py
new file mode 100644
index 0000000000..48241d647c
--- /dev/null
+++ b/worlds/jakanddaxter/regs/VolcanicCraterRegions.py
@@ -0,0 +1,38 @@
+from typing import List
+from BaseClasses import CollectionState, MultiWorld
+from .RegionBase import JakAndDaxterRegion
+from ..Rules import can_free_scout_flies, can_trade
+from ..locs import CellLocations as Cells, ScoutLocations as Scouts
+
+
+def build_regions(level_name: str, player: int, multiworld: MultiWorld) -> List[JakAndDaxterRegion]:
+
+ # No area is inaccessible in VC even with only running and jumping.
+ main_area = JakAndDaxterRegion("Main Area", player, multiworld, level_name, 50)
+ main_area.add_cell_locations([96], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530))
+ main_area.add_cell_locations([97], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530, 96))
+ main_area.add_cell_locations([98], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530, 97))
+ main_area.add_cell_locations([99], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530, 98))
+ main_area.add_cell_locations([100], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530))
+ main_area.add_cell_locations([101], access_rule=lambda state:
+ can_trade(state, player, multiworld, 1530, 100))
+
+ # Hidden Power Cell: you can carry yellow eco from Spider Cave just by running and jumping
+ # and using your Goggles to shoot the box (you do not need Punch to shoot from FP mode).
+ main_area.add_cell_locations([74])
+
+ # No blue eco sources in this area, all boxes must be broken by hand (yellow eco can't be carried far enough).
+ main_area.add_fly_locations(Scouts.locVC_scoutTable.keys(), access_rule=lambda state:
+ can_free_scout_flies(state, player))
+
+ # Approach the gondola to get this check.
+ main_area.add_special_locations([105])
+
+ multiworld.regions.append(main_area)
+
+ return [main_area]
diff --git a/worlds/jakanddaxter/test/__init__.py b/worlds/jakanddaxter/test/__init__.py
new file mode 100644
index 0000000000..a1d7bfb390
--- /dev/null
+++ b/worlds/jakanddaxter/test/__init__.py
@@ -0,0 +1,91 @@
+from .. import JakAndDaxterWorld
+from ..GameID import jak1_name
+from test.bases import WorldTestBase
+
+
+class JakAndDaxterTestBase(WorldTestBase):
+ game = jak1_name
+ world: JakAndDaxterWorld
+
+ level_info = {
+ "Geyser Rock": {
+ "cells": 4,
+ "flies": 7,
+ "orbs": 50,
+ },
+ "Sandover Village": {
+ "cells": 6,
+ "flies": 7,
+ "orbs": 50,
+ },
+ "Forbidden Jungle": {
+ "cells": 8,
+ "flies": 7,
+ "orbs": 150,
+ },
+ "Sentinel Beach": {
+ "cells": 8,
+ "flies": 7,
+ "orbs": 150,
+ },
+ "Misty Island": {
+ "cells": 8,
+ "flies": 7,
+ "orbs": 150,
+ },
+ "Fire Canyon": {
+ "cells": 2,
+ "flies": 7,
+ "orbs": 50,
+ },
+ "Rock Village": {
+ "cells": 6,
+ "flies": 7,
+ "orbs": 50,
+ },
+ "Precursor Basin": {
+ "cells": 8,
+ "flies": 7,
+ "orbs": 200,
+ },
+ "Lost Precursor City": {
+ "cells": 8,
+ "flies": 7,
+ "orbs": 200,
+ },
+ "Boggy Swamp": {
+ "cells": 8,
+ "flies": 7,
+ "orbs": 200,
+ },
+ "Mountain Pass": {
+ "cells": 4,
+ "flies": 7,
+ "orbs": 50,
+ },
+ "Volcanic Crater": {
+ "cells": 8,
+ "flies": 7,
+ "orbs": 50,
+ },
+ "Spider Cave": {
+ "cells": 8,
+ "flies": 7,
+ "orbs": 200,
+ },
+ "Snowy Mountain": {
+ "cells": 8,
+ "flies": 7,
+ "orbs": 200,
+ },
+ "Lava Tube": {
+ "cells": 2,
+ "flies": 7,
+ "orbs": 50,
+ },
+ "Gol and Maia's Citadel": {
+ "cells": 5,
+ "flies": 7,
+ "orbs": 200,
+ },
+ }
diff --git a/worlds/jakanddaxter/test/test_locations.py b/worlds/jakanddaxter/test/test_locations.py
new file mode 100644
index 0000000000..4bd144d623
--- /dev/null
+++ b/worlds/jakanddaxter/test/test_locations.py
@@ -0,0 +1,42 @@
+import typing
+
+from . import JakAndDaxterTestBase
+from .. import jak1_id
+from ..regs.RegionBase import JakAndDaxterRegion
+from ..locs import (OrbLocations as Orbs,
+ CellLocations as Cells,
+ ScoutLocations as Scouts,
+ SpecialLocations as Specials,
+ OrbCacheLocations as Caches)
+
+
+class LocationsTest(JakAndDaxterTestBase):
+
+ def test_count_cells(self):
+ regions = [typing.cast(JakAndDaxterRegion, reg) for reg in self.multiworld.get_regions(self.player)]
+ for level in self.level_info:
+ cell_count = 0
+ sublevels = [reg for reg in regions if reg.level_name == level]
+ for sl in sublevels:
+ for loc in sl.locations:
+ if loc.address in range(jak1_id, jak1_id + Scouts.fly_offset):
+ cell_count += 1
+ self.assertEqual(self.level_info[level]["cells"] - 1, cell_count, level) # Don't count the Free 7 Cells.
+
+ def test_count_flies(self):
+ regions = [typing.cast(JakAndDaxterRegion, reg) for reg in self.multiworld.get_regions(self.player)]
+ for level in self.level_info:
+ fly_count = 0
+ sublevels = [reg for reg in regions if reg.level_name == level]
+ for sl in sublevels:
+ for loc in sl.locations:
+ if loc.address in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset):
+ fly_count += 1
+ self.assertEqual(self.level_info[level]["flies"], fly_count, level)
+
+ def test_count_orbs(self):
+ regions = [typing.cast(JakAndDaxterRegion, reg) for reg in self.multiworld.get_regions(self.player)]
+ for level in self.level_info:
+ sublevels = [reg for reg in regions if reg.level_name == level]
+ orb_count = sum([reg.orb_count for reg in sublevels])
+ self.assertEqual(self.level_info[level]["orbs"], orb_count, level)