Jak and Daxter: Genericize Items, Update Scout Fly logic, Add Victory Condition. (#3)

* Jak 1: Update to 0.4.6. Decouple locations from items, support filler items.

* Jak 1: Total revamp of Items. This is where everything broke.

* Jak 1: Decouple 7 scout fly checks from normal checks, update regions/rules for orb counts/traders.

* Jak 1: correct regions/rules, account for sequential oracle/miner locations.

* Jak 1: make nicer strings.

* Jak 1: Add logic for finished game. First full run complete!

* Jak 1: update group names.
This commit is contained in:
massimilianodelliubaldini
2024-05-18 14:37:01 -04:00
committed by GitHub
parent 0f4c2211d8
commit 7cf50b0935
10 changed files with 564 additions and 236 deletions

View File

@@ -6,6 +6,7 @@ import asyncio
import colorama
import Utils
from NetUtils import ClientStatus
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled
from worlds.jakanddaxter.GameID import jak1_name
@@ -108,12 +109,21 @@ class JakAndDaxterContext(CommonContext):
logger.info(args)
self.repl.item_inbox[index] = item
async def ap_inform_location_checks(self, location_ids: typing.List[int]):
async def ap_inform_location_check(self, location_ids: typing.List[int]):
message = [{"cmd": "LocationChecks", "locations": location_ids}]
await self.send_msgs(message)
def on_locations(self, location_ids: typing.List[int]):
create_task_log_exception(self.ap_inform_location_checks(location_ids))
def on_location_check(self, location_ids: typing.List[int]):
create_task_log_exception(self.ap_inform_location_check(location_ids))
async def ap_inform_finished_game(self):
if not self.finished_game and self.memr.finished_game:
message = [{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]
await self.send_msgs(message)
self.finished_game = True
def on_finish(self):
create_task_log_exception(self.ap_inform_finished_game())
async def run_repl_loop(self):
while True:
@@ -122,7 +132,7 @@ class JakAndDaxterContext(CommonContext):
async def run_memr_loop(self):
while True:
await self.memr.main_tick(self.on_locations)
await self.memr.main_tick(self.on_location_check, self.on_finish)
await asyncio.sleep(0.1)
@@ -189,7 +199,7 @@ async def main():
ctx.run_cli()
# Find and run the game (gk) and compiler/repl (goalc).
await run_game(ctx)
# await run_game(ctx)
await ctx.exit_event.wait()
await ctx.shutdown()

View File

@@ -1,7 +1,64 @@
from BaseClasses import Item
from .GameID import jak1_name
from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials
class JakAndDaxterItem(Item):
game: str = jak1_name
# Power Cells are generic, fungible, interchangeable items. Every cell is indistinguishable from every other.
cell_item_table = {
0: "Power Cell",
}
# Scout flies are interchangeable within their respective sets of 7. Notice the abbreviated level name after each item.
# Also, notice that their Item ID equals their respective Power Cell's Location ID. This is necessary for
# game<->archipelago communication.
scout_item_table = {
95: "Scout Fly - GR",
75: "Scout Fly - SV",
7: "Scout Fly - FJ",
20: "Scout Fly - SB",
28: "Scout Fly - MI",
68: "Scout Fly - FC",
76: "Scout Fly - RV",
57: "Scout Fly - PB",
49: "Scout Fly - LPC",
43: "Scout Fly - BS",
88: "Scout Fly - MP",
77: "Scout Fly - VC",
85: "Scout Fly - SC",
65: "Scout Fly - SM",
90: "Scout Fly - LT",
91: "Scout Fly - GMC",
}
# TODO - Orbs are also generic and interchangeable.
# orb_item_table = {
# ???: "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.
special_item_table = {
5: "Fisherman's Boat",
4: "Jungle Elevator",
2: "Blue Eco Switch",
17: "Flut Flut",
60: "Yellow Eco Switch",
63: "Snowy Fort Gate",
71: "Freed The Blue Sage",
72: "Freed The Red Sage",
73: "Freed The Yellow Sage",
70: "Freed The Green Sage",
}
# 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},
**{Specials.to_ap_id(k): special_item_table[k] for k in special_item_table},
}

View File

@@ -1,6 +1,6 @@
from BaseClasses import Location
from .GameID import jak1_name
from .locs import CellLocations as Cells, ScoutLocations as Scouts
from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials
class JakAndDaxterLocation(Location):
@@ -8,9 +8,9 @@ class JakAndDaxterLocation(Location):
# All Locations
# Because all items in Jak And Daxter are unique and do not regenerate, we can use this same table as our item table.
# Each Item ID == its corresponding Location ID. While we're here, do all the ID conversions needed.
location_table = {
**{Cells.to_ap_id(k): Cells.loc7SF_cellTable[k] for k in Cells.loc7SF_cellTable},
**{Cells.to_ap_id(k): Cells.locGR_cellTable[k] for k in Cells.locGR_cellTable},
**{Cells.to_ap_id(k): Cells.locSV_cellTable[k] for k in Cells.locSV_cellTable},
**{Cells.to_ap_id(k): Cells.locFJ_cellTable[k] for k in Cells.locFJ_cellTable},
@@ -42,5 +42,6 @@ location_table = {
**{Scouts.to_ap_id(k): Scouts.locSC_scoutTable[k] for k in Scouts.locSC_scoutTable},
**{Scouts.to_ap_id(k): Scouts.locSM_scoutTable[k] for k in Scouts.locSM_scoutTable},
**{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}
**{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},
}

View File

@@ -4,10 +4,27 @@ 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
from .locs import CellLocations as Cells, ScoutLocations as Scouts, SpecialLocations as Specials
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()
@@ -27,7 +44,6 @@ class Jak1Level(int, Enum):
class Jak1SubLevel(int, Enum):
MAIN_AREA = auto()
FORBIDDEN_JUNGLE_SWITCH_ROOM = auto()
FORBIDDEN_JUNGLE_PLANT_ROOM = auto()
SENTINEL_BEACH_CANNON_TOWER = auto()
@@ -44,172 +60,213 @@ class Jak1SubLevel(int, Enum):
GOL_AND_MAIAS_CITADEL_FINAL_BOSS = auto()
level_table: typing.Dict[Jak1Level, str] = {
Jak1Level.GEYSER_ROCK: "Geyser Rock",
Jak1Level.SANDOVER_VILLAGE: "Sandover Village",
Jak1Level.FORBIDDEN_JUNGLE: "Forbidden Jungle",
Jak1Level.SENTINEL_BEACH: "Sentinel Beach",
Jak1Level.MISTY_ISLAND: "Misty Island",
Jak1Level.FIRE_CANYON: "Fire Canyon",
Jak1Level.ROCK_VILLAGE: "Rock Village",
Jak1Level.PRECURSOR_BASIN: "Precursor Basin",
Jak1Level.LOST_PRECURSOR_CITY: "Lost Precursor City",
Jak1Level.BOGGY_SWAMP: "Boggy Swamp",
Jak1Level.MOUNTAIN_PASS: "Mountain Pass",
Jak1Level.VOLCANIC_CRATER: "Volcanic Crater",
Jak1Level.SPIDER_CAVE: "Spider Cave",
Jak1Level.SNOWY_MOUNTAIN: "Snowy Mountain",
Jak1Level.LAVA_TUBE: "Lava Tube",
Jak1Level.GOL_AND_MAIAS_CITADEL: "Gol and Maia's Citadel"
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", 50),
Jak1Level.PRECURSOR_BASIN:
Jak1LevelInfo("Precursor Basin", 200),
Jak1Level.LOST_PRECURSOR_CITY:
Jak1LevelInfo("Lost Precursor City", 133),
Jak1Level.BOGGY_SWAMP:
Jak1LevelInfo("Boggy Swamp", 177),
Jak1Level.MOUNTAIN_PASS:
Jak1LevelInfo("Mountain Pass", 0),
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),
}
subLevel_table: typing.Dict[Jak1SubLevel, str] = {
Jak1SubLevel.MAIN_AREA: "Main Area",
Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM: "Forbidden Jungle Switch Room",
Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM: "Forbidden Jungle Plant Room",
Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER: "Sentinel Beach Cannon Tower",
Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS: "Precursor Basin Blue Rings",
Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM: "Lost Precursor City Sunken Room",
Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM: "Lost Precursor City Helix Room",
Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT: "Boggy Swamp Flut Flut",
Jak1SubLevel.MOUNTAIN_PASS_RACE: "Mountain Pass Race",
Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT: "Mountain Pass Shortcut",
Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT: "Snowy Mountain Flut Flut",
Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT: "Snowy Mountain Lurker Fort",
Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX: "Snowy Mountain Frozen Box",
Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER: "Gol and Maia's Citadel Rotating Tower",
Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS: "Gol and Maia's Citadel Final Boss"
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.PRECURSOR_BASIN_BLUE_RINGS:
Jak1LevelInfo("Precursor Basin Blue Rings", 0), # Another virtual location, no orbs.
Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM:
Jak1LevelInfo("Lost Precursor City Sunken Room", 37),
Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM:
Jak1LevelInfo("Lost Precursor City Helix Room", 30),
Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT:
Jak1LevelInfo("Boggy Swamp Flut Flut", 23),
Jak1SubLevel.MOUNTAIN_PASS_RACE:
Jak1LevelInfo("Mountain Pass Race", 50),
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),
}
class JakAndDaxterRegion(Region):
game: str = jak1_name
# 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):
create_region(player, multiworld, "Menu")
region_gr = create_region(player, multiworld, level_table[Jak1Level.GEYSER_ROCK])
# Always start with Menu.
multiworld.regions.append(JakAndDaxterRegion("Menu", player, multiworld))
region_7sf = create_region(player, multiworld, Jak1Level.SCOUT_FLY_POWER_CELLS)
create_cell_locations(region_7sf, Cells.loc7SF_cellTable)
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)
region_sv = create_region(player, multiworld, level_table[Jak1Level.SANDOVER_VILLAGE])
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)
region_fj = create_region(player, multiworld, level_table[Jak1Level.FORBIDDEN_JUNGLE])
create_cell_locations(region_fj, {k: Cells.locFJ_cellTable[k] for k in {3, 4, 5, 8, 9, 7}})
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}})
sub_region_fjsr = create_subregion(region_fj, subLevel_table[Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM])
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, subLevel_table[Jak1SubLevel.FORBIDDEN_JUNGLE_PLANT_ROOM])
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, level_table[Jak1Level.SENTINEL_BEACH])
create_cell_locations(region_sb, {k: Cells.locSB_cellTable[k] for k in {15, 17, 16, 18, 21, 22, 20}})
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, subLevel_table[Jak1SubLevel.SENTINEL_BEACH_CANNON_TOWER])
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, level_table[Jak1Level.MISTY_ISLAND])
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, level_table[Jak1Level.FIRE_CANYON])
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, level_table[Jak1Level.ROCK_VILLAGE])
region_rv = create_region(player, multiworld, Jak1Level.ROCK_VILLAGE)
create_cell_locations(region_rv, Cells.locRV_cellTable)
create_fly_locations(region_rv, Scouts.locRV_scoutTable)
region_pb = create_region(player, multiworld, level_table[Jak1Level.PRECURSOR_BASIN])
create_cell_locations(region_pb, {k: Cells.locPB_cellTable[k] for k in {54, 53, 52, 56, 55, 58, 57}})
region_pb = create_region(player, multiworld, Jak1Level.PRECURSOR_BASIN)
create_cell_locations(region_pb, {k: Cells.locPB_cellTable[k] for k in {54, 53, 52, 56, 55, 58}})
create_fly_locations(region_pb, Scouts.locPB_scoutTable)
sub_region_pbbr = create_subregion(region_pb, subLevel_table[Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS])
sub_region_pbbr = create_subregion(region_pb, Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS)
create_cell_locations(sub_region_pbbr, {k: Cells.locPB_cellTable[k] for k in {59}})
region_lpc = create_region(player, multiworld, level_table[Jak1Level.LOST_PRECURSOR_CITY])
region_lpc = create_region(player, multiworld, Jak1Level.LOST_PRECURSOR_CITY)
create_cell_locations(region_lpc, {k: Cells.locLPC_cellTable[k] for k in {45, 48, 44, 51}})
create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k]
for k in {262193, 131121, 393265, 196657, 49, 65585}})
sub_region_lpcsr = create_subregion(region_lpc, subLevel_table[Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM])
create_cell_locations(sub_region_lpcsr, {k: Cells.locLPC_cellTable[k] for k in {47, 49}})
sub_region_lpcsr = create_subregion(region_lpc, Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM)
create_cell_locations(sub_region_lpcsr, {k: Cells.locLPC_cellTable[k] for k in {47}})
create_fly_locations(region_lpc, {k: Scouts.locLPC_scoutTable[k] for k in {327729}})
sub_region_lpchr = create_subregion(region_lpc, subLevel_table[Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM])
sub_region_lpchr = create_subregion(region_lpc, Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM)
create_cell_locations(sub_region_lpchr, {k: Cells.locLPC_cellTable[k] for k in {46, 50}})
region_bs = create_region(player, multiworld, level_table[Jak1Level.BOGGY_SWAMP])
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, subLevel_table[Jak1SubLevel.BOGGY_SWAMP_FLUT_FLUT])
create_cell_locations(sub_region_bsff, {k: Cells.locBS_cellTable[k] for k in {43, 37}})
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, level_table[Jak1Level.MOUNTAIN_PASS])
region_mp = create_region(player, multiworld, Jak1Level.MOUNTAIN_PASS)
create_cell_locations(region_mp, {k: Cells.locMP_cellTable[k] for k in {86}})
sub_region_mpr = create_subregion(region_mp, subLevel_table[Jak1SubLevel.MOUNTAIN_PASS_RACE])
create_cell_locations(sub_region_mpr, {k: Cells.locMP_cellTable[k] for k in {87, 88}})
sub_region_mpr = create_subregion(region_mp, Jak1SubLevel.MOUNTAIN_PASS_RACE)
create_cell_locations(sub_region_mpr, {k: Cells.locMP_cellTable[k] for k in {87}})
create_fly_locations(sub_region_mpr, Scouts.locMP_scoutTable)
sub_region_mps = create_subregion(sub_region_mpr, subLevel_table[Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT])
sub_region_mps = create_subregion(sub_region_mpr, 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, level_table[Jak1Level.VOLCANIC_CRATER])
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)
region_sc = create_region(player, multiworld, level_table[Jak1Level.SPIDER_CAVE])
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, level_table[Jak1Level.SNOWY_MOUNTAIN])
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, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX])
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, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_FLUT_FLUT])
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, subLevel_table[Jak1SubLevel.SNOWY_MOUNTAIN_LURKER_FORT])
create_cell_locations(sub_region_smlf, {k: Cells.locSM_cellTable[k] for k in {62, 65}})
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, level_table[Jak1Level.LAVA_TUBE])
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, level_table[Jak1Level.GOL_AND_MAIAS_CITADEL])
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, subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_ROTATING_TOWER])
create_cell_locations(sub_region_gmcrt, {k: Cells.locGMC_cellTable[k] for k in {70, 91}})
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, subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS])
create_subregion(sub_region_gmcrt, Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS)
def create_region(player: int, multiworld: MultiWorld, name: str) -> JakAndDaxterRegion:
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, name: str) -> JakAndDaxterRegion:
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
@@ -227,3 +284,12 @@ def create_fly_locations(region: Region, locations: typing.Dict[int, str]):
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]

View File

@@ -1,52 +1,52 @@
from typing import List
from BaseClasses import MultiWorld, CollectionState
from .JakAndDaxterOptions import JakAndDaxterOptions
from .Regions import Jak1Level, Jak1SubLevel, level_table, subLevel_table
from .Locations import location_table as item_table
from .locs import CellLocations as Cells, ScoutLocations as Scouts
# Helper function for a handful of special cases
# where we need "at least any N" number of a specific set of cells.
def has_count_of(cell_list: set, required_count: int, player: int, state: CollectionState) -> bool:
c: int = 0
for k in cell_list:
if state.has(item_table[k], player):
c += 1
if c >= required_count:
return True
return False
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.
gr_cells = {Cells.to_ap_id(k) for k in Cells.locGR_cellTable}
fj_temple_top = Cells.to_ap_id(4)
fj_blue_switch = Cells.to_ap_id(2)
fj_plant_boss = Cells.to_ap_id(6)
fj_fisherman = Cells.to_ap_id(5)
sb_flut_flut = Cells.to_ap_id(17)
fc_end = Cells.to_ap_id(69)
pb_purple_rings = Cells.to_ap_id(58)
lpc_sunken = Cells.to_ap_id(47)
lpc_helix = Cells.to_ap_id(50)
mp_klaww = Cells.to_ap_id(86)
mp_end = Cells.to_ap_id(87)
pre_sm_cells = {Cells.to_ap_id(k) for k in {**Cells.locVC_cellTable, **Cells.locSC_cellTable}}
sm_yellow_switch = Cells.to_ap_id(60)
sm_fort_gate = Cells.to_ap_id(63)
lt_end = Cells.to_ap_id(89)
gmc_rby_sages = {Cells.to_ap_id(k) for k in {71, 72, 73}}
gmc_green_sage = Cells.to_ap_id(70)
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)]
sm_yellow_switch = item_table[Specials.to_ap_id(60)]
sm_fort_gate = item_table[Specials.to_ap_id(63)]
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.
connect_start(multiworld, player, Jak1Level.GEYSER_ROCK)
# 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.GEYSER_ROCK,
Jak1Level.SANDOVER_VILLAGE,
lambda state: has_count_of(gr_cells, 4, player, state))
Jak1Level.GEYSER_ROCK)
connect_regions(multiworld, player,
Jak1Level.SANDOVER_VILLAGE,
@@ -55,50 +55,52 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int)
connect_region_to_sub(multiworld, player,
Jak1Level.FORBIDDEN_JUNGLE,
Jak1SubLevel.FORBIDDEN_JUNGLE_SWITCH_ROOM,
lambda state: state.has(item_table[fj_temple_top], player))
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(item_table[fj_blue_switch], player))
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,
lambda state: state.has(item_table[fj_plant_boss], player))
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(item_table[fj_blue_switch], player))
lambda state: state.has(fj_blue_switch, player))
connect_regions(multiworld, player,
Jak1Level.SANDOVER_VILLAGE,
Jak1Level.MISTY_ISLAND,
lambda state: state.has(item_table[fj_fisherman], player))
lambda state: state.has(fj_fisherman, player))
connect_regions(multiworld, player,
Jak1Level.SANDOVER_VILLAGE,
Jak1Level.FIRE_CANYON,
lambda state: state.count_group("Power Cell", player) >= 20)
lambda state: state.has(power_cell, player, 20))
connect_regions(multiworld, player,
Jak1Level.FIRE_CANYON,
Jak1Level.ROCK_VILLAGE,
lambda state: state.has(item_table[fc_end], player))
Jak1Level.ROCK_VILLAGE)
set_trade_requirements(multiworld, player, Jak1Level.ROCK_VILLAGE, rv_traders, 1530)
connect_regions(multiworld, player,
Jak1Level.ROCK_VILLAGE,
Jak1Level.PRECURSOR_BASIN)
# This is another virtual location that shares it's "borders" with its parent location.
# You can do blue rings as soon as you finish purple rings.
connect_region_to_sub(multiworld, player,
Jak1Level.PRECURSOR_BASIN,
Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS,
lambda state: state.has(item_table[pb_purple_rings], player))
Jak1SubLevel.PRECURSOR_BASIN_BLUE_RINGS)
connect_regions(multiworld, player,
Jak1Level.ROCK_VILLAGE,
@@ -112,121 +114,179 @@ def set_rules(multiworld: MultiWorld, options: JakAndDaxterOptions, player: int)
Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM,
Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM)
# LPC is such a mess logistically... once you complete the climb up the helix room,
# you are back to the room before the first slide, which is still the "main area" of LPC.
connect_sub_to_region(multiworld, player,
Jak1SubLevel.LOST_PRECURSOR_CITY_HELIX_ROOM,
Jak1Level.LOST_PRECURSOR_CITY,
lambda state: state.has(item_table[lpc_helix], player))
Jak1Level.LOST_PRECURSOR_CITY)
# Once you raise the sunken room to the surface, you can access Rock Village directly.
# You just need to complete the Location check to do this, you don't need to receive the power cell Item.
connect_sub_to_region(multiworld, player,
Jak1SubLevel.LOST_PRECURSOR_CITY_SUNKEN_ROOM,
Jak1Level.ROCK_VILLAGE,
lambda state: state.has(item_table[lpc_sunken], player))
Jak1Level.ROCK_VILLAGE)
connect_regions(multiworld, player,
Jak1Level.ROCK_VILLAGE,
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(item_table[sb_flut_flut], player))
lambda state: state.has(sb_flut_flut, player))
# Klaww is considered the "main area" of MP, and the "race" is a subregion.
# It's not really intended to get back up the ledge overlooking Klaww's lava pit.
connect_regions(multiworld, player,
Jak1Level.ROCK_VILLAGE,
Jak1Level.MOUNTAIN_PASS,
lambda state: state.count_group("Power Cell", player) >= 45)
lambda state: state.has(power_cell, player, 45))
connect_region_to_sub(multiworld, player,
Jak1Level.MOUNTAIN_PASS,
Jak1SubLevel.MOUNTAIN_PASS_RACE,
lambda state: state.has(item_table[mp_klaww], player))
Jak1SubLevel.MOUNTAIN_PASS_RACE)
connect_subregions(multiworld, player,
Jak1SubLevel.MOUNTAIN_PASS_RACE,
Jak1SubLevel.MOUNTAIN_PASS_SHORTCUT,
lambda state: state.has(item_table[sm_yellow_switch], player))
lambda state: state.has(sm_yellow_switch, player))
connect_sub_to_region(multiworld, player,
Jak1SubLevel.MOUNTAIN_PASS_RACE,
Jak1Level.VOLCANIC_CRATER,
lambda state: state.has(item_table[mp_end], player))
Jak1Level.VOLCANIC_CRATER)
set_trade_requirements(multiworld, player, Jak1Level.VOLCANIC_CRATER, vc_traders, 1530)
connect_regions(multiworld, player,
Jak1Level.VOLCANIC_CRATER,
Jak1Level.SPIDER_CAVE)
# TODO - Yeah, this is a weird one. You technically need either 71 power cells OR
# any 2 power cells after arriving at Volcanic Crater. Not sure how to model this...
connect_regions(multiworld, player,
Jak1Level.VOLCANIC_CRATER,
Jak1Level.SNOWY_MOUNTAIN,
lambda state: has_count_of(pre_sm_cells, 2, player, state)
or state.count_group("Power Cell", player) >= 71) # Yeah, this is a weird one.
Jak1Level.SNOWY_MOUNTAIN)
connect_region_to_sub(multiworld, player,
Jak1Level.SNOWY_MOUNTAIN,
Jak1SubLevel.SNOWY_MOUNTAIN_FROZEN_BOX,
lambda state: state.has(item_table[sm_yellow_switch], player))
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(item_table[sb_flut_flut], player))
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(item_table[sm_fort_gate], player))
lambda state: state.has(sm_fort_gate, player))
connect_regions(multiworld, player,
Jak1Level.VOLCANIC_CRATER,
Jak1Level.LAVA_TUBE,
lambda state: state.count_group("Power Cell", player) >= 72)
lambda state: state.has(power_cell, player, 72))
connect_regions(multiworld, player,
Jak1Level.LAVA_TUBE,
Jak1Level.GOL_AND_MAIAS_CITADEL,
lambda state: state.has(item_table[lt_end], player))
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: has_count_of(gmc_rby_sages, 3, player, state))
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(item_table[gmc_green_sage], player))
lambda state: state.has(gmc_green_sage, player))
multiworld.completion_condition[player] = lambda state: state.can_reach(
multiworld.get_region(subLevel_table[Jak1SubLevel.GOL_AND_MAIAS_CITADEL_FINAL_BOSS], player),
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], 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], player)
target_region = multiworld.get_region(level_table[target], player)
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], player)
target_region = multiworld.get_region(subLevel_table[target], player)
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(subLevel_table[source], player)
target_region = multiworld.get_region(level_table[target], player)
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(subLevel_table[source], player)
target_region = multiworld.get_region(subLevel_table[target], player)
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)
# 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 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
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:
# 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
# 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}.")

View File

@@ -5,12 +5,13 @@ from BaseClasses import Item, ItemClassification, Tutorial
from .GameID import jak1_id, jak1_name
from .JakAndDaxterOptions import JakAndDaxterOptions
from .Items import JakAndDaxterItem
from .Locations import JakAndDaxterLocation, location_table as item_table
from .locs import CellLocations as Cells, ScoutLocations as Scouts, OrbLocations as Orbs
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 .Regions import create_regions
from .Rules import set_rules
from ..AutoWorld import World, WebWorld
from ..LauncherComponents import components, Component, launch_subprocess, Type
from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import components, Component, launch_subprocess, Type
class JakAndDaxterSettings(settings.Group):
@@ -46,8 +47,7 @@ class JakAndDaxterWorld(World):
"""
# ID, name, version
game = jak1_name
data_version = 1
required_client_version = (0, 4, 5)
required_client_version = (0, 4, 6)
# Options
settings: typing.ClassVar[JakAndDaxterSettings]
@@ -61,13 +61,17 @@ class JakAndDaxterWorld(World):
# Stored as {ID: Name} pairs, these must now be swapped to {Name: ID} pairs.
# Remember, the game ID and various offsets for each item type have already been calculated.
item_name_to_id = {item_table[k]: k for k in item_table}
location_name_to_id = {item_table[k]: k for k in item_table}
location_name_to_id = {location_table[k]: k for k in location_table}
item_name_groups = {
"Power Cell": {item_table[k]: k for k in item_table
if k in range(jak1_id, jak1_id + Scouts.fly_offset)},
"Scout Fly": {item_table[k]: k for k in item_table
if k in range(jak1_id + Scouts.fly_offset, jak1_id + Orbs.orb_offset)}
# "Precursor Orb": {} # TODO
"Power Cells": {item_table[k]: k for k in item_table
if k in range(jak1_id, jak1_id + Scouts.fly_offset)},
"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, ???)},
}
def create_regions(self):
@@ -76,25 +80,47 @@ class JakAndDaxterWorld(World):
def set_rules(self):
set_rules(self.multiworld, self.options, self.player)
# Helper function to reuse some nasty if/else trees.
@staticmethod
def item_type_helper(item) -> (int, ItemClassification):
# Make 101 Power Cells.
if item in range(jak1_id, jak1_id + Scouts.fly_offset):
classification = ItemClassification.progression_skip_balancing
count = 101
# Make 7 Scout Flies per level.
elif item in range(jak1_id + Scouts.fly_offset, jak1_id + Specials.special_offset):
classification = ItemClassification.progression_skip_balancing
count = 7
# Make only 1 of each Special Item.
elif item in range(jak1_id + Specials.special_offset, jak1_id + Orbs.orb_offset):
classification = ItemClassification.progression
count = 1
# TODO - Make ??? Precursor Orbs.
# elif item in range(jak1_id + Orbs.orb_offset, ???):
# classification = ItemClassification.filler
# count = ???
# If we try to make items with ID's higher than we've defined, something has gone wrong.
else:
raise KeyError(f"Tried to fill item pool with unknown ID {item}.")
return count, classification
def create_items(self):
self.multiworld.itempool += [self.create_item(item_table[k]) for k in item_table]
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)]
def create_item(self, name: str) -> Item:
item_id = self.item_name_to_id[name]
if item_id in range(jak1_id, jak1_id + Scouts.fly_offset):
# Power Cell
classification = ItemClassification.progression_skip_balancing
elif item_id in range(jak1_id + Scouts.fly_offset, jak1_id + Orbs.orb_offset):
# Scout Fly
classification = ItemClassification.progression_skip_balancing
elif item_id > jak1_id + Orbs.orb_offset:
# Precursor Orb
classification = ItemClassification.filler # TODO
else:
classification = ItemClassification.filler
_, classification = self.item_type_helper(item_id)
return JakAndDaxterItem(name, classification, item_id, self.player)
item = JakAndDaxterItem(name, classification, item_id, self.player)
return item
def get_filler_item_name(self) -> str:
return "Power Cell" # TODO - Make Precursor Orb the filler item. Until then, enjoy the free progression.
def launch_client():

View File

@@ -4,20 +4,25 @@ from pymem import pattern
from pymem.exception import ProcessNotFound, ProcessError, MemoryReadError, WinAPIError
from CommonClient import logger
from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies
from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies, SpecialLocations as Specials
# Some helpful constants.
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.
cells_offset = 16
buzzers_offset = 420 # cells_offset + (sizeof uint32 * 101 cells) = 16 + (4 * 101)
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.
# buzzers_offset
# + (sizeof uint32 * 112 flies) <-- The buzzers themselves.
# + (sizeof uint8 * 116 tasks) <-- A "cells-received" array for the game to handle new ownership logic.
# = 420 + (4 * 112) + (1 * 116)
end_marker_offset = 984
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)
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))
end_marker_offset = 1052 # specials_received_offset + (sizeof uint8 * 32 specials)
class JakAndDaxterMemoryReader:
@@ -31,12 +36,13 @@ class JakAndDaxterMemoryReader:
location_outbox = []
outbox_index = 0
finished_game = False
def __init__(self, marker: typing.ByteString = b'UnLiStEdStRaTs_JaK1\x00'):
self.marker = marker
self.connect()
async def main_tick(self, location_callback: typing.Callable):
async def main_tick(self, location_callback: typing.Callable, finish_callback: typing.Callable):
if self.initiated_connect:
await self.connect()
self.initiated_connect = False
@@ -59,6 +65,9 @@ class JakAndDaxterMemoryReader:
location_callback(self.location_outbox)
self.outbox_index += 1
if self.finished_game:
finish_callback()
async def connect(self):
try:
self.gk_process = pymem.Pymem("gk.exe") # The GOAL Kernel
@@ -74,9 +83,9 @@ class JakAndDaxterMemoryReader:
if marker_address:
# At this address is another address that contains the struct we're looking for: the game's state.
# From here we need to add the length in bytes for the marker and 4 bytes of padding,
# and the struct address is 8 bytes long (it's u64).
# and the struct address is 8 bytes long (it's a uint64).
goal_pointer = marker_address + len(self.marker) + 4
self.goal_address = int.from_bytes(self.gk_process.read_bytes(goal_pointer, 8),
self.goal_address = int.from_bytes(self.gk_process.read_bytes(goal_pointer, sizeof_uint64),
byteorder="little",
signed=False)
logger.info("Found the archipelago memory address: " + str(self.goal_address))
@@ -98,17 +107,23 @@ class JakAndDaxterMemoryReader:
def read_memory(self) -> typing.List[int]:
try:
next_cell_index = int.from_bytes(
self.gk_process.read_bytes(self.goal_address, 8),
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, 8),
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)
for k in range(0, next_cell_index):
next_cell = int.from_bytes(
self.gk_process.read_bytes(self.goal_address + cells_offset + (k * 4), 4),
self.gk_process.read_bytes(
self.goal_address + cells_checked_offset + (k * sizeof_uint32),
sizeof_uint32),
byteorder="little",
signed=False)
cell_ap_id = Cells.to_ap_id(next_cell)
@@ -118,7 +133,9 @@ class JakAndDaxterMemoryReader:
for k in range(0, next_buzzer_index):
next_buzzer = int.from_bytes(
self.gk_process.read_bytes(self.goal_address + buzzers_offset + (k * 4), 4),
self.gk_process.read_bytes(
self.goal_address + buzzers_checked_offset + (k * sizeof_uint32),
sizeof_uint32),
byteorder="little",
signed=False)
buzzer_ap_id = Flies.to_ap_id(next_buzzer)
@@ -126,6 +143,27 @@ class JakAndDaxterMemoryReader:
self.location_outbox.append(buzzer_ap_id)
logger.info("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)
# 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.
if next_special == 112 and not self.finished_game:
self.finished_game = True
logger.info("Congratulations! You finished the game!")
else:
# All other special checks handled as normal.
special_ap_id = Specials.to_ap_id(next_special)
if special_ap_id not in self.location_outbox:
self.location_outbox.append(special_ap_id)
logger.info("Checked special: " + str(next_special))
except (ProcessError, MemoryReadError, WinAPIError):
logger.error("The gk process has died. Restart the game and run \"/memr connect\" again.")
self.connected = False

View File

@@ -6,8 +6,13 @@ import pymem
from pymem.exception import ProcessNotFound, ProcessError
from CommonClient import logger
from worlds.jakanddaxter.locs import CellLocations as Cells, ScoutLocations as Flies, OrbLocations as Orbs
from worlds.jakanddaxter.GameID import jak1_id
from worlds.jakanddaxter.Items import item_table
from worlds.jakanddaxter.locs import (
CellLocations as Cells,
ScoutLocations as Flies,
OrbLocations as Orbs,
SpecialLocations as Specials)
class JakAndDaxterReplClient:
@@ -170,12 +175,14 @@ class JakAndDaxterReplClient:
# Determine the type of item to receive.
if ap_id in range(jak1_id, jak1_id + Flies.fly_offset):
self.receive_power_cell(ap_id)
elif ap_id in range(jak1_id + Flies.fly_offset, jak1_id + Orbs.orb_offset):
elif ap_id in range(jak1_id + Flies.fly_offset, jak1_id + Specials.special_offset):
self.receive_scout_fly(ap_id)
elif ap_id > jak1_id + Orbs.orb_offset:
pass # TODO
elif ap_id in range(jak1_id + Specials.special_offset, jak1_id + Orbs.orb_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.
else:
raise KeyError(f"Tried to receive item with unknown AP ID {ap_id}.")
def receive_power_cell(self, ap_id: int) -> bool:
cell_id = Cells.to_game_id(ap_id)
@@ -184,9 +191,9 @@ class JakAndDaxterReplClient:
"(pickup-type fuel-cell) "
"(the float " + str(cell_id) + "))")
if ok:
logger.info(f"Received power cell {cell_id}!")
logger.info(f"Received a Power Cell!")
else:
logger.error(f"Unable to receive power cell {cell_id}!")
logger.error(f"Unable to receive a Power Cell!")
return ok
def receive_scout_fly(self, ap_id: int) -> bool:
@@ -196,7 +203,19 @@ class JakAndDaxterReplClient:
"(pickup-type buzzer) "
"(the float " + str(fly_id) + "))")
if ok:
logger.info(f"Received scout fly {fly_id}!")
logger.info(f"Received a {item_table[ap_id]}!")
else:
logger.error(f"Unable to receive scout fly {fly_id}!")
logger.error(f"Unable to receive a {item_table[ap_id]}!")
return ok
def receive_special(self, ap_id: int) -> bool:
special_id = Specials.to_game_id(ap_id)
ok = self.send_form("(send-event "
"*target* \'get-archipelago "
"(pickup-type ap-special) "
"(the float " + str(special_id) + "))")
if ok:
logger.info(f"Received special unlock {item_table[ap_id]}!")
else:
logger.error(f"Unable to receive special unlock {item_table[ap_id]}!")
return ok

View File

@@ -22,12 +22,31 @@ def to_game_id(ap_id: int) -> int:
# The ID's you see below correspond directly to that cell's game-task ID.
# The "Free 7 Scout Flies" Power Cells will be unlocked separately from their respective levels.
loc7SF_cellTable = {
95: "GR: Free 7 Scout Flies",
75: "SV: Free 7 Scout Flies",
7: "FJ: Free 7 Scout Flies",
20: "SB: Free 7 Scout Flies",
28: "MI: Free 7 Scout Flies",
68: "FC: Free 7 Scout Flies",
76: "RV: Free 7 Scout Flies",
57: "PB: Free 7 Scout Flies",
49: "LPC: Free 7 Scout Flies",
43: "BS: Free 7 Scout Flies",
88: "MP: Free 7 Scout Flies",
77: "VC: Free 7 Scout Flies",
85: "SC: Free 7 Scout Flies",
65: "SM: Free 7 Scout Flies",
90: "LT: Free 7 Scout Flies",
91: "GMC: Free 7 Scout Flies",
}
# Geyser Rock
locGR_cellTable = {
92: "GR: Find The Cell On The Path",
93: "GR: Open The Precursor Door",
94: "GR: Climb Up The Cliff",
95: "GR: Free 7 Scout Flies"
}
# Sandover Village
@@ -37,7 +56,6 @@ locSV_cellTable = {
10: "SV: Herd The Yakows Into The Pen",
13: "SV: Bring 120 Orbs To The Oracle (1)",
14: "SV: Bring 120 Orbs To The Oracle (2)",
75: "SV: Free 7 Scout Flies"
}
# Forbidden Jungle
@@ -49,7 +67,6 @@ locFJ_cellTable = {
5: "FJ: Catch 200 Pounds Of Fish",
8: "FJ: Follow The Canyon To The Sea",
9: "FJ: Open The Locked Temple Door",
7: "FJ: Free 7 Scout Flies"
}
# Sentinel Beach
@@ -61,7 +78,6 @@ locSB_cellTable = {
19: "SB: Launch Up To The Cannon Tower",
21: "SB: Explore The Beach",
22: "SB: Climb The Sentinel",
20: "SB: Free 7 Scout Flies"
}
# Misty Island
@@ -73,13 +89,11 @@ locMI_cellTable = {
27: "MI: Destroy the Balloon Lurkers",
29: "MI: Use Zoomer To Reach Power Cell",
30: "MI: Use Blue Eco To Reach Power Cell",
28: "MI: Free 7 Scout Flies"
}
# Fire Canyon
locFC_cellTable = {
69: "FC: Reach The End Of Fire Canyon",
68: "FC: Free 7 Scout Flies"
}
# Rock Village
@@ -89,7 +103,6 @@ locRV_cellTable = {
33: "RV: Bring 90 Orbs To The Warrior",
34: "RV: Bring 120 Orbs To The Oracle (1)",
35: "RV: Bring 120 Orbs To The Oracle (2)",
76: "RV: Free 7 Scout Flies"
}
# Precursor Basin
@@ -101,7 +114,6 @@ locPB_cellTable = {
55: "PB: Cure Dark Eco Infected Plants",
58: "PB: Navigate The Purple Precursor Rings",
59: "PB: Navigate The Blue Precursor Rings",
57: "PB: Free 7 Scout Flies"
}
# Lost Precursor City
@@ -113,7 +125,6 @@ locLPC_cellTable = {
44: "LPC: Match The Platform Colors",
50: "LPC: Climb The Slide Tube",
51: "LPC: Reach The Center Of The Complex",
49: "LPC: Free 7 Scout Flies"
}
# Boggy Swamp
@@ -125,7 +136,6 @@ locBS_cellTable = {
40: "BS: Break The Tethers To The Zeppelin (2)",
41: "BS: Break The Tethers To The Zeppelin (3)",
42: "BS: Break The Tethers To The Zeppelin (4)",
43: "BS: Free 7 Scout Flies"
}
# Mountain Pass
@@ -133,7 +143,6 @@ locMP_cellTable = {
86: "MP: Defeat Klaww",
87: "MP: Reach The End Of The Mountain Pass",
110: "MP: Find The Hidden Power Cell",
88: "MP: Free 7 Scout Flies"
}
# Volcanic Crater
@@ -145,7 +154,6 @@ locVC_cellTable = {
100: "VC: Bring 120 Orbs To The Oracle (1)",
101: "VC: Bring 120 Orbs To The Oracle (2)",
74: "VC: Find The Hidden Power Cell",
77: "VC: Free 7 Scout Flies"
}
# Spider Cave
@@ -157,7 +165,6 @@ locSC_cellTable = {
82: "SC: Launch To The Poles",
83: "SC: Navigate The Spider Tunnel",
84: "SC: Climb the Precursor Platforms",
85: "SC: Free 7 Scout Flies"
}
# Snowy Mountain
@@ -169,13 +176,11 @@ locSM_cellTable = {
63: "SM: Open The Lurker Fort Gate",
62: "SM: Get Through The Lurker Fort",
64: "SM: Survive The Lurker Infested Cave",
65: "SM: Free 7 Scout Flies"
}
# Lava Tube
locLT_cellTable = {
89: "LT: Cross The Lava Tube",
90: "LT: Free 7 Scout Flies"
}
# Gol and Maias Citadel
@@ -184,5 +189,4 @@ locGMC_cellTable = {
72: "GMC: Free The Red Sage",
73: "GMC: Free The Yellow Sage",
70: "GMC: Free The Green Sage",
91: "GMC: Free 7 Scout Flies"
}

View File

@@ -0,0 +1,47 @@
from ..GameID import jak1_id
# These are special checks that the game normally does not track. They are not game entities and thus
# don't have game ID's.
# Normally, for example, completing the fishing minigame is what gives you access to the
# fisherman's boat to get to Misty Island. The game treats completion of the fishing minigame as well as the
# power cell you receive as one and the same. The fisherman only gives you one item, a power cell.
# We're significantly altering the game logic here to decouple these concepts. First, completing the fishing minigame
# now counts as 2 Location checks. Second, the fisherman should give you a power cell (a generic item) as well as
# the "keys" to his boat (a special item). It is the "keys" that we are defining in this file, and the respective
# Item representing those keys will be defined in Items.py. These aren't real in the sense that
# they have a model and texture, they are just the logical representation of the boat unlock.
# We can use 2^11 to offset these from scout flies, just like we offset scout flies from power cells
# by 2^10. Even with the high-16 reminder bits, scout flies don't exceed an ID of (jak1_id + 1887).
special_offset = 2048
# These helper functions do all the math required to get information about each
# special check and translate its ID between AP and OpenGOAL.
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."
return jak1_id + special_offset + game_id # Add the offsets and the orb 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."
return ap_id - jak1_id - special_offset # Reverse process, subtract the offsets.
# The ID's you see below correlate to each of their respective game-tasks, even though they are separate.
# This makes it easier for the new game logic to know what relates to what. I hope. God I hope.
loc_specialTable = {
5: "Fisherman's Boat",
4: "Jungle Elevator",
2: "Blue Eco Switch",
17: "Flut Flut",
60: "Yellow Eco Switch",
63: "Snowy Fort Gate",
71: "Freed The Blue Sage",
72: "Freed The Red Sage",
73: "Freed The Yellow Sage",
70: "Freed The Green Sage",
}