Skip to content

Commit

Permalink
Orbsanity (ArchipelagoMW#32)
Browse files Browse the repository at this point in the history
* My big dumb shortcut: a 2000 item array.

* A better idea: bundle orbs as a numerical option and make array variable size.

* Have Item/Region generation respect the chosen Orbsanity bundle size. Fix trade logic.

* Separate Global/Local Orbsanity options. TODO - re-introduce orb factory for per-level option.

* Per-level Orbsanity implemented w/ orb bundle factory.

* Implement Orbsanity for client, fix some things up for regions.

* Fix location name/id mappings.

* Fix client orb collection on connection.

* Fix minor Deathlink bug, add Update instructions.
  • Loading branch information
massimilianodelliubaldini authored Jul 11, 2024
1 parent f875169 commit f7b688d
Show file tree
Hide file tree
Showing 28 changed files with 822 additions and 196 deletions.
24 changes: 22 additions & 2 deletions worlds/jakanddaxter/Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ async def server_auth(self, password_requested: bool = False):
await self.send_connect()

def on_package(self, cmd: str, args: dict):

if cmd == "Connected":
slot_data = args["slot_data"]
if slot_data["enable_orbsanity"] == 1:
self.repl.setup_orbsanity(slot_data["enable_orbsanity"], slot_data["level_orbsanity_bundle_size"])
elif slot_data["enable_orbsanity"] == 2:
self.repl.setup_orbsanity(slot_data["enable_orbsanity"], slot_data["global_orbsanity_bundle_size"])
else:
self.repl.setup_orbsanity(slot_data["enable_orbsanity"], 1)

if cmd == "ReceivedItems":
for index, item in enumerate(args["items"], start=args["index"]):
logger.debug(f"index: {str(index)}, item: {str(item)}")
Expand Down Expand Up @@ -137,7 +147,8 @@ def on_finish_check(self):

async def ap_inform_deathlink(self):
if self.memr.deathlink_enabled:
death_text = self.memr.cause_of_death.replace("Jak", self.player_names[self.slot])
player = self.player_names[self.slot] if self.slot is not None else "Jak"
death_text = self.memr.cause_of_death.replace("Jak", player)
await self.send_death(death_text)
logger.info(death_text)

Expand All @@ -155,6 +166,14 @@ async def ap_inform_deathlink_toggle(self):
def on_deathlink_toggle(self):
create_task_log_exception(self.ap_inform_deathlink_toggle())

async def repl_reset_orbsanity(self):
if self.memr.orbsanity_enabled:
self.memr.reset_orbsanity = False
self.repl.reset_orbsanity()

def on_orbsanity_check(self):
create_task_log_exception(self.repl_reset_orbsanity())

async def run_repl_loop(self):
while True:
await self.repl.main_tick()
Expand All @@ -165,7 +184,8 @@ async def run_memr_loop(self):
await self.memr.main_tick(self.on_location_check,
self.on_finish_check,
self.on_deathlink_check,
self.on_deathlink_toggle)
self.on_deathlink_toggle,
self.on_orbsanity_check)
await asyncio.sleep(0.1)


Expand Down
23 changes: 22 additions & 1 deletion worlds/jakanddaxter/Items.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,29 @@ class JakAndDaxterItem(Item):
}

# Orbs are also generic and interchangeable.
# These items are only used by Orbsanity, and only one of these
# items will be used corresponding to the chosen bundle size.
orb_item_table = {
1: "Precursor Orb",
2: "Bundle of 2 Precursor Orbs",
4: "Bundle of 4 Precursor Orbs",
5: "Bundle of 5 Precursor Orbs",
8: "Bundle of 8 Precursor Orbs",
10: "Bundle of 10 Precursor Orbs",
16: "Bundle of 16 Precursor Orbs",
20: "Bundle of 20 Precursor Orbs",
25: "Bundle of 25 Precursor Orbs",
40: "Bundle of 40 Precursor Orbs",
50: "Bundle of 50 Precursor Orbs",
80: "Bundle of 80 Precursor Orbs",
100: "Bundle of 100 Precursor Orbs",
125: "Bundle of 125 Precursor Orbs",
200: "Bundle of 200 Precursor Orbs",
250: "Bundle of 250 Precursor Orbs",
400: "Bundle of 400 Precursor Orbs",
500: "Bundle of 500 Precursor Orbs",
1000: "Bundle of 1000 Precursor Orbs",
2000: "Bundle of 2000 Precursor Orbs",
}

# These are special items representing unique unlocks in the world. Notice that their Item ID equals their
Expand Down Expand Up @@ -85,8 +106,8 @@ class JakAndDaxterItem(Item):
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},
**{Caches.to_ap_id(k): move_item_table[k] for k in move_item_table},
**{Orbs.to_ap_id(k): orb_item_table[k] for k in orb_item_table},
jak1_max: "Green Eco Pill" # Filler item.
}
73 changes: 62 additions & 11 deletions worlds/jakanddaxter/JakAndDaxterOptions.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,74 @@
import os
from dataclasses import dataclass
from Options import Toggle, PerGameCommonOptions
from Options import Toggle, PerGameCommonOptions, Choice


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."""
"""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. This adds 11 items to the pool."""
display_name = "Enable Move Randomizer"


# 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"
class EnableOrbsanity(Choice):
"""Enable to include bundles of Precursor Orbs as an ordered list of progressive checks. Every time you collect
the chosen number of orbs, you will trigger the next release in the list. "Per Level" means these lists are
generated and populated for each level in the game (Geyser Rock, Sandover Village, etc.). "Global" means there is
only one list for the entire game.
This adds a number of Items and Locations to the pool inversely proportional to the size of the bundle.
For example, if your bundle size is 20 orbs, you will add 100 items to the pool. If your bundle size is 250 orbs,
you will add 8 items to the pool."""
display_name = "Enable Orbsanity"
option_off = 0
option_per_level = 1
option_global = 2
default = 0


class GlobalOrbsanityBundleSize(Choice):
"""Set the size of the bundle for Global Orbsanity.
This only applies if "Enable Orbsanity" is set to "Global."
There are 2000 orbs in the game, so your bundle size must be a factor of 2000."""
display_name = "Global Orbsanity Bundle Size"
option_1_orb = 1
option_2_orbs = 2
option_4_orbs = 4
option_5_orbs = 5
option_8_orbs = 8
option_10_orbs = 10
option_16_orbs = 16
option_20_orbs = 20
option_25_orbs = 25
option_40_orbs = 40
option_50_orbs = 50
option_80_orbs = 80
option_100_orbs = 100
option_125_orbs = 125
option_200_orbs = 200
option_250_orbs = 250
option_400_orbs = 400
option_500_orbs = 500
option_1000_orbs = 1000
option_2000_orbs = 2000
default = 1


class PerLevelOrbsanityBundleSize(Choice):
"""Set the size of the bundle for Per Level Orbsanity.
This only applies if "Enable Orbsanity" is set to "Per Level."
There are 50, 150, or 200 orbs per level, so your bundle size must be a factor of 50."""
display_name = "Per Level Orbsanity Bundle Size"
option_1_orb = 1
option_2_orbs = 2
option_5_orbs = 5
option_10_orbs = 10
option_25_orbs = 25
option_50_orbs = 50
default = 1


@dataclass
class JakAndDaxterOptions(PerGameCommonOptions):
enable_move_randomizer: EnableMoveRandomizer
# enable_orbsanity: EnableOrbsanity
enable_orbsanity: EnableOrbsanity
global_orbsanity_bundle_size: GlobalOrbsanityBundleSize
level_orbsanity_bundle_size: PerLevelOrbsanityBundleSize
4 changes: 3 additions & 1 deletion worlds/jakanddaxter/Locations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from BaseClasses import Location
from .GameID import jak1_name
from .locs import (CellLocations as Cells,
from .locs import (OrbLocations as Orbs,
CellLocations as Cells,
ScoutLocations as Scouts,
SpecialLocations as Specials,
OrbCacheLocations as Caches)
Expand Down Expand Up @@ -48,4 +49,5 @@ class JakAndDaxterLocation(Location):
**{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},
**{Orbs.to_ap_id(k): Orbs.loc_orbBundleTable[k] for k in Orbs.loc_orbBundleTable}
}
62 changes: 41 additions & 21 deletions worlds/jakanddaxter/Regions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import typing
from BaseClasses import MultiWorld
from .Items import item_table
from .JakAndDaxterOptions import JakAndDaxterOptions
from .locs import (CellLocations as Cells,
from .Items import item_table
from .Rules import can_reach_orbs
from .locs import (OrbLocations as Orbs,
CellLocations as Cells,
ScoutLocations as Scouts)
from .regs.RegionBase import JakAndDaxterRegion
from .regs import (GeyserRockRegions as GeyserRock,
Expand Down Expand Up @@ -30,7 +31,7 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player:
multiworld.regions.append(menu)

# 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.
# The Locations within 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:
Expand All @@ -39,27 +40,46 @@ def create_regions(multiworld: MultiWorld, options: JakAndDaxterOptions, player:
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)
menu.connect(free7)

# If Global Orbsanity is enabled, build the special Orbsanity Region. This is a virtual region always
# accessible to Menu. The Locations within are automatically checked when you collect enough orbs.
if options.enable_orbsanity.value == 2:
orbs = JakAndDaxterRegion("Orbsanity", player, multiworld)

bundle_size = options.global_orbsanity_bundle_size.value
bundle_count = int(2000 / bundle_size)
for bundle_index in range(bundle_count):

# Unlike Per-Level Orbsanity, Global Orbsanity Locations always have a level_index of 16.
orbs.add_orb_locations(16,
bundle_index,
bundle_size,
access_rule=lambda state, bundle=bundle_index:
can_reach_orbs(state, player, multiworld, options)
>= (bundle_size * (bundle + 1)))
multiworld.regions.append(orbs)
menu.connect(orbs)

# 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)
[gr] = GeyserRock.build_regions("Geyser Rock", multiworld, options, player)
[sv] = SandoverVillage.build_regions("Sandover Village", multiworld, options, player)
[fj] = ForbiddenJungle.build_regions("Forbidden Jungle", multiworld, options, player)
[sb] = SentinelBeach.build_regions("Sentinel Beach", multiworld, options, player)
[mi] = MistyIsland.build_regions("Misty Island", multiworld, options, player)
[fc] = FireCanyon.build_regions("Fire Canyon", multiworld, options, player)
[rv, rvp, rvc] = RockVillage.build_regions("Rock Village", multiworld, options, player)
[pb] = PrecursorBasin.build_regions("Precursor Basin", multiworld, options, player)
[lpc] = LostPrecursorCity.build_regions("Lost Precursor City", multiworld, options, player)
[bs] = BoggySwamp.build_regions("Boggy Swamp", multiworld, options, player)
[mp, mpr] = MountainPass.build_regions("Mountain Pass", multiworld, options, player)
[vc] = VolcanicCrater.build_regions("Volcanic Crater", multiworld, options, player)
[sc] = SpiderCave.build_regions("Spider Cave", multiworld, options, player)
[sm] = SnowyMountain.build_regions("Snowy Mountain", multiworld, options, player)
[lt] = LavaTube.build_regions("Lava Tube", multiworld, options, player)
[gmc, fb] = GolAndMaiasCitadel.build_regions("Gol and Maia's Citadel", multiworld, options, player)

# 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)
Expand Down
82 changes: 77 additions & 5 deletions worlds/jakanddaxter/Rules.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,80 @@
import math
import typing
from BaseClasses import MultiWorld, CollectionState
from .JakAndDaxterOptions import JakAndDaxterOptions
from .Items import orb_item_table
from .locs import CellLocations as Cells
from .Locations import location_table
from .Regions import JakAndDaxterRegion
from .regs.RegionBase import JakAndDaxterRegion


def can_reach_orbs(state: CollectionState,
player: int,
multiworld: MultiWorld,
options: JakAndDaxterOptions,
level_name: str = None) -> int:

# Global Orbsanity and No Orbsanity both treat orbs as completely interchangeable.
# Per Level Orbsanity needs to know if you can reach orbs *in a particular level.*
if options.enable_orbsanity.value in [0, 2]:
return can_reach_orbs_global(state, player, multiworld)
else:
return can_reach_orbs_level(state, player, multiworld, level_name)


def can_reach_orbs_global(state: CollectionState,
player: int,
multiworld: MultiWorld) -> int:

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

return accessible_orbs


def can_reach_orbs_level(state: CollectionState,
player: int,
multiworld: MultiWorld,
level_name: str) -> int:

accessible_orbs = 0
regions = [typing.cast(JakAndDaxterRegion, reg) for reg in multiworld.get_regions(player)]
for region in regions:
if region.level_name == level_name and state.can_reach(region, "Region", player):
accessible_orbs += region.orb_count

return accessible_orbs


# 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 can_trade(state: CollectionState,
player: int,
multiworld: MultiWorld,
options: JakAndDaxterOptions,
required_orbs: int,
required_previous_trade: int = None) -> bool:

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
if options.enable_orbsanity.value == 1:
bundle_size = options.level_orbsanity_bundle_size.value
return can_trade_orbsanity(state, player, bundle_size, required_orbs, required_previous_trade)
elif options.enable_orbsanity.value == 2:
bundle_size = options.global_orbsanity_bundle_size.value
return can_trade_orbsanity(state, player, bundle_size, required_orbs, required_previous_trade)
else:
return can_trade_regular(state, player, multiworld, required_orbs, required_previous_trade)


def can_trade_regular(state: CollectionState,
player: int,
multiworld: MultiWorld,
required_orbs: int,
required_previous_trade: int = None) -> bool:

# We know that Orbsanity is off, so count orbs globally.
accessible_orbs = can_reach_orbs_global(state, player, multiworld)
if required_previous_trade:
name_of_previous_trade = location_table[Cells.to_ap_id(required_previous_trade)]
return (accessible_orbs >= required_orbs
Expand All @@ -27,6 +83,22 @@ def can_trade(state: CollectionState,
return accessible_orbs >= required_orbs


def can_trade_orbsanity(state: CollectionState,
player: int,
orb_bundle_size: int,
required_orbs: int,
required_previous_trade: int = None) -> bool:

required_count = math.ceil(required_orbs / orb_bundle_size)
orb_bundle_name = orb_item_table[orb_bundle_size]
if required_previous_trade:
name_of_previous_trade = location_table[Cells.to_ap_id(required_previous_trade)]
return (state.has(orb_bundle_name, player, required_count)
and state.can_reach(name_of_previous_trade, "Location", player=player))
else:
return state.has(orb_bundle_name, player, required_count)


def can_free_scout_flies(state: CollectionState, player: int) -> bool:
return (state.has("Jump Dive", player)
or (state.has("Crouch", player)
Expand Down
Loading

0 comments on commit f7b688d

Please sign in to comment.