forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
1322 lines
63 KiB
Python
1322 lines
63 KiB
Python
"""Contains the class which holds logic variables, and the master copy of regions."""
|
|
|
|
from math import ceil
|
|
from functools import lru_cache
|
|
from collections import Counter
|
|
|
|
import randomizer.CollectibleLogicFiles.AngryAztec
|
|
import randomizer.CollectibleLogicFiles.CreepyCastle
|
|
import randomizer.CollectibleLogicFiles.CrystalCaves
|
|
import randomizer.CollectibleLogicFiles.DKIsles
|
|
import randomizer.CollectibleLogicFiles.FranticFactory
|
|
import randomizer.CollectibleLogicFiles.FungiForest
|
|
import randomizer.CollectibleLogicFiles.GloomyGalleon
|
|
import randomizer.CollectibleLogicFiles.JungleJapes
|
|
import randomizer.LogicFiles.AngryAztec
|
|
import randomizer.LogicFiles.CreepyCastle
|
|
import randomizer.LogicFiles.CrystalCaves
|
|
import randomizer.LogicFiles.DKIsles
|
|
import randomizer.LogicFiles.FranticFactory
|
|
import randomizer.LogicFiles.FungiForest
|
|
import randomizer.LogicFiles.GloomyGalleon
|
|
import randomizer.LogicFiles.HideoutHelm
|
|
import randomizer.LogicFiles.JungleJapes
|
|
import randomizer.LogicFiles.Shops
|
|
from randomizer.Enums.Collectibles import Collectibles
|
|
from randomizer.Enums.Events import Events
|
|
from randomizer.Enums.Items import Items
|
|
from randomizer.Enums.Kongs import Kongs
|
|
from randomizer.Enums.Levels import Levels
|
|
from randomizer.Enums.Locations import Locations
|
|
from randomizer.Enums.Regions import Regions as RegionEnum
|
|
from randomizer.Enums.Switches import Switches
|
|
from randomizer.Enums.SwitchTypes import SwitchType
|
|
from randomizer.Enums.Settings import (
|
|
ActivateAllBananaports,
|
|
BananaportRando,
|
|
ClimbingStatus,
|
|
DamageAmount,
|
|
FasterChecksSelected,
|
|
GlitchesSelected,
|
|
HardModeSelected,
|
|
HardBossesSelected,
|
|
HardBossesSelected,
|
|
LogicType,
|
|
MiscChangesSelected,
|
|
ProgressiveHintItem,
|
|
RemovedBarriersSelected,
|
|
ShockwaveStatus,
|
|
ShuffleLoadingZones,
|
|
TrainingBarrels,
|
|
HelmSetting,
|
|
KongModels,
|
|
SlamRequirement,
|
|
WinConditionComplex,
|
|
)
|
|
from randomizer.Enums.Time import Time
|
|
from randomizer.Enums.Types import Types, BarrierItems
|
|
from randomizer.Lists.Item import ItemList
|
|
from randomizer.Enums.Maps import Maps
|
|
from randomizer.Lists.ShufflableExit import GetShuffledLevelIndex
|
|
from randomizer.Lists.Warps import BananaportVanilla
|
|
from randomizer.Patching.Library.Generic import IsItemSelected, getProgHintBarrierItem
|
|
from randomizer.Prices import AnyKongCanBuy, CanBuy, GetPriceAtLocation
|
|
|
|
STARTING_SLAM = 0 # Currently we're assuming you always start with 1 slam
|
|
|
|
|
|
def IsGlitchEnabled(settings, glitch_enum):
|
|
"""Check if glitch is enabled in the settings."""
|
|
return len(settings.glitches_selected) == 0 or glitch_enum in settings.glitches_selected
|
|
|
|
|
|
class LogicVarHolder:
|
|
"""Used to store variables when checking logic conditions."""
|
|
|
|
def __init__(self, spoiler):
|
|
"""Initialize with given parameters."""
|
|
settings = spoiler.settings
|
|
self.settings = settings
|
|
self.spoiler = spoiler
|
|
# Some restrictions are added to the item placement fill for the sake of reducing indirect errors. We can overlook these restrictions once we know the fill is valid.
|
|
self.assumeFillSuccess = False
|
|
# See CalculateWothPaths method for details on these assumptions
|
|
self.assumePaidBLockers = False
|
|
self.assumeInfiniteCoins = False
|
|
self.assumeAztecEntry = False
|
|
self.assumeLevel4Entry = False
|
|
self.assumeLevel8Entry = False # Extra important to never assume this in LZR!
|
|
self.assumeUpperIslesAccess = False
|
|
self.assumeKRoolAccess = False
|
|
|
|
self.startkong = self.settings.starting_kong
|
|
# Glitch Logic
|
|
enable_glitch_logic = self.settings.logic_type == LogicType.glitch
|
|
self.phasewalk = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.phase_walking)
|
|
self.phaseswim = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.phase_swimming)
|
|
self.moonkicks = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.moonkicks)
|
|
self.ledgeclip = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.ledge_clips)
|
|
self.generalclips = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.general_clips) # General clips which have no real category
|
|
self.lanky_blocker_skip = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.b_locker_skips) # Also includes ppunch skip
|
|
self.dk_blocker_skip = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.b_locker_skips)
|
|
self.troff_skip = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.troff_n_scoff_skips)
|
|
self.spawn_snags = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.spawn_snags)
|
|
self.advanced_platforming = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.advanced_platforming)
|
|
self.tbs = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.tag_barrel_storage) and not self.settings.disable_tag_barrels
|
|
self.swim_through_shores = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.swim_through_shores)
|
|
self.boulder_clip = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.boulder_clips) and False # Temporarily disabled
|
|
self.skew = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.skew)
|
|
self.moontail = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.moontail)
|
|
self.phasefall = enable_glitch_logic and IsGlitchEnabled(settings, GlitchesSelected.phasefall)
|
|
# Reset
|
|
self.Reset()
|
|
|
|
def Reset(self):
|
|
"""Reset all logic variables.
|
|
|
|
Done between reachability searches and upon initialization.
|
|
"""
|
|
self.latest_owned_items = []
|
|
self.found_test_item = False
|
|
|
|
self.donkey = Kongs.donkey in self.settings.starting_kong_list
|
|
self.diddy = Kongs.diddy in self.settings.starting_kong_list
|
|
self.lanky = Kongs.lanky in self.settings.starting_kong_list
|
|
self.tiny = Kongs.tiny in self.settings.starting_kong_list
|
|
self.chunky = Kongs.chunky in self.settings.starting_kong_list
|
|
|
|
# Right now assuming start with training barrels
|
|
self.vines = self.settings.training_barrels == TrainingBarrels.normal
|
|
self.swim = self.settings.training_barrels == TrainingBarrels.normal
|
|
self.oranges = self.settings.training_barrels == TrainingBarrels.normal
|
|
self.barrels = self.settings.training_barrels == TrainingBarrels.normal
|
|
self.climbing = self.settings.climbing_status == ClimbingStatus.normal
|
|
self.can_use_vines = self.vines # and self.climbing to restore old behavior
|
|
|
|
progDonkey = 0
|
|
self.blast = False
|
|
self.strongKong = False
|
|
self.grab = False
|
|
|
|
progDiddy = 0
|
|
self.charge = False
|
|
self.jetpack = False
|
|
self.spring = False
|
|
|
|
progLanky = 0
|
|
self.handstand = False
|
|
self.balloon = False
|
|
self.sprint = False
|
|
|
|
progTiny = 0
|
|
self.mini = False
|
|
self.twirl = False
|
|
self.monkeyport = False
|
|
|
|
progChunky = 0
|
|
self.hunkyChunky = False
|
|
self.punch = False
|
|
self.gorillaGone = False
|
|
|
|
self.coconut = False
|
|
self.peanut = False
|
|
self.grape = False
|
|
self.feather = False
|
|
self.pineapple = False
|
|
|
|
self.bongos = False
|
|
self.guitar = False
|
|
self.trombone = False
|
|
self.saxophone = False
|
|
self.triangle = False
|
|
|
|
self.nintendoCoin = False
|
|
self.rarewareCoin = False
|
|
|
|
self.camera = self.settings.shockwave_status == ShockwaveStatus.start_with
|
|
self.shockwave = self.settings.shockwave_status == ShockwaveStatus.start_with
|
|
|
|
self.scope = False
|
|
self.homing = False
|
|
|
|
self.JapesKey = False
|
|
self.AztecKey = False
|
|
self.FactoryKey = False
|
|
self.GalleonKey = False
|
|
self.ForestKey = False
|
|
self.CavesKey = False
|
|
self.CastleKey = False
|
|
self.HelmKey = False
|
|
|
|
self.crankyAccess = False
|
|
self.funkyAccess = False
|
|
self.candyAccess = False
|
|
self.snideAccess = False
|
|
|
|
self.HelmDonkey1 = False
|
|
self.HelmDonkey2 = False
|
|
self.HelmDiddy1 = False
|
|
self.HelmDiddy2 = False
|
|
self.HelmLanky1 = False
|
|
self.HelmLanky2 = False
|
|
self.HelmTiny1 = False
|
|
self.HelmTiny2 = False
|
|
self.HelmChunky1 = False
|
|
self.HelmChunky2 = False
|
|
|
|
self.allTrainingChecks = self.settings.fast_start_beginning_of_game
|
|
|
|
self.Slam = STARTING_SLAM
|
|
self.AmmoBelts = 0
|
|
self.InstUpgrades = 0
|
|
self.Melons = 0
|
|
|
|
self.GoldenBananas = 0
|
|
self.BananaFairies = 0
|
|
self.BananaMedals = 0
|
|
self.BattleCrowns = 0
|
|
|
|
self.superSlam = False
|
|
self.superDuperSlam = False
|
|
|
|
self.Blueprints = []
|
|
|
|
self.Events = []
|
|
|
|
self.Hints = []
|
|
|
|
self.SpecialLocationsReached = []
|
|
|
|
# Set key events for keys which are given to the player at start of game
|
|
keyEvents = [
|
|
Events.JapesKeyTurnedIn,
|
|
Events.AztecKeyTurnedIn,
|
|
Events.FactoryKeyTurnedIn,
|
|
Events.GalleonKeyTurnedIn,
|
|
Events.ForestKeyTurnedIn,
|
|
Events.CavesKeyTurnedIn,
|
|
Events.CastleKeyTurnedIn,
|
|
Events.HelmKeyTurnedIn,
|
|
]
|
|
for keyEvent in keyEvents:
|
|
if keyEvent not in self.settings.krool_keys_required:
|
|
# This is horrifyingly bad to go keys -> events -> keys but the patcher is expecting events in krool_keys_required and I'm not touching the math there to fix it
|
|
if keyEvent == Events.JapesKeyTurnedIn:
|
|
self.JapesKey = True
|
|
elif keyEvent == Events.AztecKeyTurnedIn:
|
|
self.AztecKey = True
|
|
elif keyEvent == Events.FactoryKeyTurnedIn:
|
|
self.FactoryKey = True
|
|
elif keyEvent == Events.GalleonKeyTurnedIn:
|
|
self.GalleonKey = True
|
|
elif keyEvent == Events.ForestKeyTurnedIn:
|
|
self.ForestKey = True
|
|
elif keyEvent == Events.CavesKeyTurnedIn:
|
|
self.CavesKey = True
|
|
elif keyEvent == Events.CastleKeyTurnedIn:
|
|
self.CastleKey = True
|
|
elif keyEvent == Events.HelmKeyTurnedIn:
|
|
self.HelmKey = True
|
|
|
|
activated_warp_maps = []
|
|
if self.settings.activate_all_bananaports == ActivateAllBananaports.all:
|
|
activated_warp_maps = [
|
|
Maps.JungleJapes,
|
|
Maps.AngryAztec,
|
|
Maps.AztecLlamaTemple,
|
|
Maps.FranticFactory,
|
|
Maps.GloomyGalleon,
|
|
Maps.FungiForest,
|
|
Maps.CrystalCaves,
|
|
Maps.CreepyCastle,
|
|
Maps.CastleCrypt,
|
|
Maps.Isles,
|
|
# Maps.HideoutHelmLobby, # Not in BananaportVanilla
|
|
]
|
|
elif self.settings.activate_all_bananaports == ActivateAllBananaports.isles:
|
|
activated_warp_maps = [Maps.Isles]
|
|
elif self.settings.activate_all_bananaports == ActivateAllBananaports.isles_inc_helm_lobby:
|
|
activated_warp_maps = [
|
|
Maps.Isles,
|
|
# Maps.HideoutHelmLobby, # Not in BananaportVanilla
|
|
]
|
|
if any(activated_warp_maps):
|
|
for warp_data in BananaportVanilla.values():
|
|
if warp_data.map_id in activated_warp_maps:
|
|
self.Events.append(warp_data.event)
|
|
if self.settings.activate_all_bananaports in (ActivateAllBananaports.all, ActivateAllBananaports.isles_inc_helm_lobby):
|
|
self.Events.extend([Events.HelmLobbyW1aTagged, Events.HelmLobbyW1bTagged])
|
|
|
|
# Colored banana and coin arrays
|
|
# Colored bananas as 9 arrays of 5 (8 levels for 5 kongs, Helm is level index 7, so skip this)
|
|
self.ColoredBananas = []
|
|
for i in range(9):
|
|
self.ColoredBananas.append([0] * 5)
|
|
|
|
self.Coins = [0] * 5
|
|
self.RegularCoins = [0] * 5
|
|
self.RainbowCoins = 0
|
|
self.SpentCoins = [0] * 5
|
|
|
|
self.kong = self.startkong
|
|
|
|
self.bananaHoard = False
|
|
|
|
self.UpdateKongs()
|
|
|
|
def isPriorHelmComplete(self, kong: Kongs):
|
|
"""Determine if there is access to the kong's helm room."""
|
|
if self.settings.helm_setting == HelmSetting.skip_all or Events.HelmFinished in self.Events:
|
|
return True
|
|
room_seq = (Kongs.donkey, Kongs.chunky, Kongs.tiny, Kongs.lanky, Kongs.diddy)
|
|
kong_evt = (
|
|
Events.HelmDonkeyDone,
|
|
Events.HelmDiddyDone,
|
|
Events.HelmLankyDone,
|
|
Events.HelmTinyDone,
|
|
Events.HelmChunkyDone,
|
|
)
|
|
desired_index = room_seq.index(kong)
|
|
helm_order = self.settings.helm_order
|
|
if desired_index in helm_order:
|
|
sequence_slot = helm_order.index(desired_index)
|
|
if sequence_slot > 0:
|
|
prior_kong = room_seq[helm_order[sequence_slot - 1]]
|
|
return kong_evt[prior_kong] in self.Events
|
|
return True
|
|
|
|
def UpdateCoins(self):
|
|
"""Update coin total."""
|
|
for x in range(5):
|
|
self.Coins[x] = (self.RegularCoins[x] + (5 * self.RainbowCoins)) - self.SpentCoins[x]
|
|
|
|
def Update(self, ownedItems):
|
|
"""Update logic variables based on owned items."""
|
|
ownedItems = [item for item in ownedItems]
|
|
item_counts = Counter(ownedItems)
|
|
self.latest_owned_items = ownedItems
|
|
self.found_test_item = self.found_test_item or Items.TestItem in ownedItems
|
|
|
|
self.donkey = self.donkey or Items.Donkey in ownedItems or self.startkong == Kongs.donkey
|
|
self.diddy = self.diddy or Items.Diddy in ownedItems or self.startkong == Kongs.diddy
|
|
self.lanky = self.lanky or Items.Lanky in ownedItems or self.startkong == Kongs.lanky
|
|
self.tiny = self.tiny or Items.Tiny in ownedItems or self.startkong == Kongs.tiny
|
|
self.chunky = self.chunky or Items.Chunky in ownedItems or self.startkong == Kongs.chunky
|
|
|
|
self.climbing = self.climbing or Items.Climbing in ownedItems
|
|
self.vines = self.vines or Items.Vines in ownedItems
|
|
self.swim = self.swim or Items.Swim in ownedItems
|
|
self.oranges = self.oranges or Items.Oranges in ownedItems
|
|
self.barrels = self.barrels or Items.Barrels in ownedItems
|
|
self.can_use_vines = self.vines # and self.climbing to restore old behavior
|
|
|
|
progDonkey = item_counts[Items.ProgressiveDonkeyPotion]
|
|
self.blast = self.blast or (Items.BaboonBlast in ownedItems or progDonkey >= 1) and self.donkey
|
|
self.strongKong = self.strongKong or (Items.StrongKong in ownedItems or progDonkey >= 2) and self.donkey
|
|
self.grab = self.grab or (Items.GorillaGrab in ownedItems or progDonkey >= 3) and self.donkey
|
|
|
|
progDiddy = item_counts[Items.ProgressiveDiddyPotion]
|
|
self.charge = self.charge or (Items.ChimpyCharge in ownedItems or progDiddy >= 1) and self.diddy
|
|
self.jetpack = self.jetpack or (Items.RocketbarrelBoost in ownedItems or progDiddy >= 2) and self.diddy
|
|
self.spring = self.spring or (Items.SimianSpring in ownedItems or progDiddy >= 3) and self.diddy
|
|
|
|
progLanky = item_counts[Items.ProgressiveLankyPotion]
|
|
self.handstand = self.handstand or (Items.Orangstand in ownedItems or progLanky >= 1) and self.lanky
|
|
self.balloon = self.balloon or (Items.BaboonBalloon in ownedItems or progLanky >= 2) and self.lanky
|
|
self.sprint = self.sprint or (Items.OrangstandSprint in ownedItems or progLanky >= 3) and self.lanky
|
|
|
|
progTiny = item_counts[Items.ProgressiveTinyPotion]
|
|
self.mini = self.mini or (Items.MiniMonkey in ownedItems or progTiny >= 1) and self.tiny
|
|
self.twirl = self.twirl or (Items.PonyTailTwirl in ownedItems or progTiny >= 2) and self.tiny
|
|
self.monkeyport = self.monkeyport or (Items.Monkeyport in ownedItems or progTiny >= 3) and self.tiny
|
|
|
|
progChunky = item_counts[Items.ProgressiveChunkyPotion]
|
|
self.hunkyChunky = self.hunkyChunky or (Items.HunkyChunky in ownedItems or progChunky >= 1) and self.chunky
|
|
self.punch = self.punch or (Items.PrimatePunch in ownedItems or progChunky >= 2) and self.chunky
|
|
self.gorillaGone = self.gorillaGone or (Items.GorillaGone in ownedItems or progChunky >= 3) and self.chunky
|
|
|
|
self.coconut = self.coconut or Items.Coconut in ownedItems and self.donkey
|
|
self.peanut = self.peanut or Items.Peanut in ownedItems and self.diddy
|
|
self.grape = self.grape or Items.Grape in ownedItems and self.lanky
|
|
self.feather = self.feather or Items.Feather in ownedItems and self.tiny
|
|
self.pineapple = self.pineapple or Items.Pineapple in ownedItems and self.chunky
|
|
|
|
self.bongos = self.bongos or Items.Bongos in ownedItems and self.donkey
|
|
self.guitar = self.guitar or Items.Guitar in ownedItems and self.diddy
|
|
self.trombone = self.trombone or Items.Trombone in ownedItems and self.lanky
|
|
self.saxophone = self.saxophone or Items.Saxophone in ownedItems and self.tiny
|
|
self.triangle = self.triangle or Items.Triangle in ownedItems and self.chunky
|
|
|
|
self.crankyAccess = self.crankyAccess or Items.Cranky in ownedItems
|
|
self.funkyAccess = self.funkyAccess or Items.Funky in ownedItems
|
|
self.candyAccess = self.candyAccess or Items.Candy in ownedItems
|
|
self.snideAccess = self.snideAccess or Items.Snide in ownedItems
|
|
|
|
self.nintendoCoin = self.nintendoCoin or Items.NintendoCoin in ownedItems
|
|
self.rarewareCoin = self.rarewareCoin or Items.RarewareCoin in ownedItems
|
|
|
|
self.JapesKey = self.JapesKey or Items.JungleJapesKey in ownedItems
|
|
self.AztecKey = self.AztecKey or Items.AngryAztecKey in ownedItems
|
|
self.FactoryKey = self.FactoryKey or Items.FranticFactoryKey in ownedItems
|
|
self.GalleonKey = self.GalleonKey or Items.GloomyGalleonKey in ownedItems
|
|
self.ForestKey = self.ForestKey or Items.FungiForestKey in ownedItems
|
|
self.CavesKey = self.CavesKey or Items.CrystalCavesKey in ownedItems
|
|
self.CastleKey = self.CastleKey or Items.CreepyCastleKey in ownedItems
|
|
self.HelmKey = self.HelmKey or Items.HideoutHelmKey in ownedItems
|
|
|
|
self.HelmDonkey1 = self.HelmDonkey1 or Items.HelmDonkey1 in ownedItems
|
|
self.HelmDonkey2 = self.HelmDonkey2 or Items.HelmDonkey2 in ownedItems
|
|
self.HelmDiddy1 = self.HelmDiddy1 or Items.HelmDiddy1 in ownedItems
|
|
self.HelmDiddy2 = self.HelmDiddy2 or Items.HelmDiddy2 in ownedItems
|
|
self.HelmLanky1 = self.HelmLanky1 or Items.HelmLanky1 in ownedItems
|
|
self.HelmLanky2 = self.HelmLanky2 or Items.HelmLanky2 in ownedItems
|
|
self.HelmTiny1 = self.HelmTiny1 or Items.HelmTiny1 in ownedItems
|
|
self.HelmTiny2 = self.HelmTiny2 or Items.HelmTiny2 in ownedItems
|
|
self.HelmChunky1 = self.HelmChunky1 or Items.HelmChunky1 in ownedItems
|
|
self.HelmChunky2 = self.HelmChunky2 or Items.HelmChunky2 in ownedItems
|
|
|
|
has_all = True
|
|
if not self.settings.fast_start_beginning_of_game:
|
|
has_all = all(
|
|
self.spoiler.LocationList[loc].inaccessible or self.spoiler.LocationList[loc].item in ownedItems
|
|
for loc in (
|
|
Locations.IslesSwimTrainingBarrel,
|
|
Locations.IslesVinesTrainingBarrel,
|
|
Locations.IslesBarrelsTrainingBarrel,
|
|
Locations.IslesOrangesTrainingBarrel,
|
|
)
|
|
)
|
|
self.allTrainingChecks = self.allTrainingChecks or has_all
|
|
|
|
self.Slam = item_counts[Items.ProgressiveSlam] + STARTING_SLAM
|
|
self.AmmoBelts = item_counts[Items.ProgressiveAmmoBelt]
|
|
self.InstUpgrades = item_counts[Items.ProgressiveInstrumentUpgrade]
|
|
self.Melons = 1
|
|
if self.bongos or self.guitar or self.trombone or self.saxophone or self.triangle or self.InstUpgrades > 0:
|
|
self.Melons = 2
|
|
if self.InstUpgrades >= 2:
|
|
self.Melons = 3
|
|
|
|
self.GoldenBananas = item_counts[Items.GoldenBanana]
|
|
self.BananaFairies = item_counts[Items.BananaFairy]
|
|
self.BananaMedals = item_counts[Items.BananaMedal]
|
|
self.BattleCrowns = item_counts[Items.BattleCrown]
|
|
self.RainbowCoins = item_counts[Items.RainbowCoin]
|
|
|
|
self.camera = self.camera or Items.CameraAndShockwave in ownedItems or Items.Camera in ownedItems
|
|
self.shockwave = self.shockwave or Items.CameraAndShockwave in ownedItems or Items.Shockwave in ownedItems
|
|
|
|
self.scope = self.scope or Items.SniperSight in ownedItems
|
|
# Having the homing ammo ability also requires having reliable access to homing ammo. This is not a perfect fix, but should cover 99.9% of cases and won't show up in hint paths.
|
|
self.homing = self.homing or (Items.HomingAmmo in ownedItems and (Events.ForestEntered in self.Events or Events.CastleEntered in self.Events or self.assumeFillSuccess))
|
|
|
|
self.superSlam = self.Slam >= 2
|
|
self.superDuperSlam = self.Slam >= 3
|
|
|
|
self.Blueprints = [x for x in ownedItems if x >= Items.JungleJapesDonkeyBlueprint and x <= Items.DKIslesChunkyBlueprint]
|
|
self.Hints = [x for x in ownedItems if x >= Items.JapesDonkeyHint and x <= Items.CastleChunkyHint]
|
|
self.Beans = sum(1 for x in ownedItems if x == Items.Bean)
|
|
self.Pearls = sum(1 for x in ownedItems if x == Items.Pearl)
|
|
|
|
self.UpdateCoins()
|
|
|
|
self.bananaHoard = self.bananaHoard or Items.BananaHoard in ownedItems
|
|
|
|
def GetCoins(self, kong):
|
|
"""Get Coin Total for a kong."""
|
|
self.UpdateCoins()
|
|
return self.Coins[kong]
|
|
|
|
def CanSlamSwitch(self, level: Levels, default_requirement_level: int):
|
|
"""Determine whether the player can operate the necessary slam operation.
|
|
|
|
Keyword arguments:
|
|
level -- level which the switch takes place
|
|
default_requirement_level -- Default requirement for the switch without randomization. 1 - Base slam, 2 - Super, 3 - Super Duper.
|
|
"""
|
|
slam_req = default_requirement_level
|
|
if self.settings.alter_switch_allocation:
|
|
slam_req = self.settings.switch_allocation[level]
|
|
if slam_req == 2:
|
|
return self.superSlam
|
|
elif slam_req == 3:
|
|
return self.superDuperSlam
|
|
return self.Slam
|
|
|
|
@lru_cache(maxsize=None)
|
|
def IsLavaWater(self) -> bool:
|
|
"""Determine whether the water is lava water or not."""
|
|
return IsItemSelected(self.settings.hard_mode, self.settings.hard_mode_selected, HardModeSelected.water_is_lava, False)
|
|
|
|
@lru_cache(maxsize=None)
|
|
def HardBossesSettingEnabled(self, check: HardBossesSelected) -> bool:
|
|
"""Determine whether the hard bosses feature is enabled or not."""
|
|
return IsItemSelected(self.settings.hard_bosses, self.settings.hard_bosses_selected, check, False)
|
|
|
|
@lru_cache(maxsize=None)
|
|
def IsHardFallDamage(self) -> bool:
|
|
"""Determine whether the lowered fall damage height threshold is enabled or not."""
|
|
return IsItemSelected(self.settings.hard_mode, self.settings.hard_mode_selected, HardModeSelected.reduced_fall_damage_threshold, False)
|
|
|
|
def canAccessHelm(self) -> bool:
|
|
"""Determine whether the player can access helm whilst the timer is active."""
|
|
if IsItemSelected(self.settings.hard_mode, self.settings.hard_mode_selected, HardModeSelected.strict_helm_timer, False):
|
|
return self.snideAccess and len(self.Blueprints) > (4 + (2 * self.settings.helm_phase_count))
|
|
return self.snideAccess or self.assumeFillSuccess
|
|
|
|
@lru_cache(maxsize=None)
|
|
def checkFastCheck(self, check: FasterChecksSelected):
|
|
"""Determine whether a fast check is selected."""
|
|
return IsItemSelected(self.settings.faster_checks_enabled, self.settings.faster_checks_selected, check)
|
|
|
|
@lru_cache(maxsize=None)
|
|
def checkBarrier(self, check: RemovedBarriersSelected):
|
|
"""Determine whether a barrier has been removed by the removed barriers setting."""
|
|
return IsItemSelected(self.settings.remove_barriers_enabled, self.settings.remove_barriers_selected, check)
|
|
|
|
@lru_cache(maxsize=None)
|
|
def galleonGatesStayOpen(self) -> bool:
|
|
"""Determine whether the galleon gates stay open once the instrument is played."""
|
|
return IsItemSelected(
|
|
self.settings.quality_of_life,
|
|
self.settings.misc_changes_selected,
|
|
MiscChangesSelected.remove_galleon_ship_timers,
|
|
)
|
|
|
|
@lru_cache(maxsize=None)
|
|
def cabinBarrelMoved(self) -> bool:
|
|
"""Determine whether the upper cabin rocketbarrel has been moved."""
|
|
return IsItemSelected(
|
|
self.settings.quality_of_life,
|
|
self.settings.misc_changes_selected,
|
|
MiscChangesSelected.move_spring_cabin_rocketbarrel,
|
|
)
|
|
|
|
def canOpenLlamaTemple(self):
|
|
"""Determine whether the switches on the Llama Temple can be shot."""
|
|
if not (self.checkBarrier(RemovedBarriersSelected.aztec_llama_switches) or Events.LlamaFreed in self.Events):
|
|
return False
|
|
return self.hasMoveSwitchsanity(Switches.AztecLlamaCoconut) or self.hasMoveSwitchsanity(Switches.AztecLlamaGrape) or self.hasMoveSwitchsanity(Switches.AztecLlamaFeather)
|
|
|
|
def canTravelToMechFish(self):
|
|
"""Determine whether or not there is a fast enough path to the Mech Fish is open."""
|
|
if self.settings.shuffle_loading_zones != ShuffleLoadingZones.all or self.settings.bananaport_rando == BananaportRando.off:
|
|
return self.swim
|
|
lighthouse_gate = self.checkBarrier(RemovedBarriersSelected.galleon_lighthouse_gate) or self.hasMoveSwitchsanity(Switches.GalleonLighthouse, False)
|
|
shipyard_gate = self.checkBarrier(RemovedBarriersSelected.galleon_shipyard_area_gate) or self.hasMoveSwitchsanity(Switches.GalleonShipwreck, False)
|
|
return self.swim and lighthouse_gate and shipyard_gate
|
|
|
|
def hasMoveSwitchsanity(
|
|
self,
|
|
switchsanity_setting: Switches,
|
|
kong_needs_current: bool = True,
|
|
level: Levels = Levels.JungleJapes,
|
|
default_slam_level: int = 0,
|
|
) -> bool:
|
|
"""Determine whether the kong has the necessary moves based on the switchsanity data."""
|
|
data = self.settings.switchsanity_data[switchsanity_setting]
|
|
kong_data = self.IsKong(data.kong)
|
|
if not kong_needs_current:
|
|
kong_data = self.HasKong(data.kong)
|
|
if data.switch_type == SwitchType.PadMove:
|
|
pad_abilities = [self.blast, self.spring, self.balloon, self.monkeyport, self.gorillaGone]
|
|
return kong_data and pad_abilities[data.kong]
|
|
elif data.switch_type == SwitchType.MiscActivator:
|
|
misc_abilities = [self.grab, self.charge, False, False, False]
|
|
return kong_data and misc_abilities[data.kong]
|
|
elif data.switch_type == SwitchType.GunSwitch:
|
|
gun_abilities = [self.coconut, self.peanut, self.grape, self.feather, self.pineapple]
|
|
return kong_data and gun_abilities[data.kong]
|
|
elif data.switch_type == SwitchType.InstrumentPad:
|
|
instrument_abilities = [self.bongos, self.guitar, self.trombone, self.saxophone, self.triangle]
|
|
return kong_data and instrument_abilities[data.kong]
|
|
elif data.switch_type == SwitchType.SlamSwitch:
|
|
return kong_data and self.CanSlamSwitch(level, default_slam_level)
|
|
return False
|
|
|
|
def CanPhaseswim(self):
|
|
"""Determine whether the player can perform phase swim."""
|
|
return self.phaseswim and self.swim
|
|
|
|
def CanSTS(self):
|
|
"""Determine whether the player can perform swim through shores."""
|
|
return self.swim_through_shores and self.swim
|
|
|
|
def CanMoonkick(self):
|
|
"""Determine whether the player can perform a moonkick."""
|
|
return self.moonkicks and self.isdonkey and self.settings.kong_model_dk == KongModels.default
|
|
|
|
def CanOStandTBSNoclip(self):
|
|
"""Determine whether the player can perform Orangstand TBS Noclip."""
|
|
return self.tbs and self.handstand and self.islanky
|
|
|
|
def CanAccessRNDRoom(self):
|
|
"""Determine whether the player can enter an R&D Room with glitches."""
|
|
return self.CanPhase() or self.generalclips or self.CanOStandTBSNoclip()
|
|
|
|
def CanGetOnCannonGamePlatform(self):
|
|
"""Determine whether the player can get on the platform in Cannon Game Room in Gloomy Galleon."""
|
|
return Events.WaterRaised in self.Events or (self.advanced_platforming and (self.ischunky or (self.islanky and self.settings.kong_model_lanky == KongModels.default)))
|
|
|
|
def CanSkew(self, swim, is_japes=True, kong_req=Kongs.any):
|
|
"""Determine whether the player can skew."""
|
|
if swim:
|
|
return self.skew and self.swim and self.HasGun(kong_req) and self.CanPhaseswim()
|
|
satisfies_cannon_req = True
|
|
if is_japes:
|
|
satisfies_cannon_req = Events.JapesAccessToCannon in self.Events
|
|
return self.skew and self.oranges and self.settings.damage_amount != DamageAmount.ohko and satisfies_cannon_req
|
|
|
|
def canFulfillProgHint(self, value: int) -> bool:
|
|
"""Determine whether the player can view a progressive hint."""
|
|
req_item = self.settings.progressive_hint_item
|
|
if req_item == ProgressiveHintItem.off:
|
|
return True
|
|
barrier_item = getProgHintBarrierItem(req_item)
|
|
if barrier_item is None:
|
|
raise Exception("Invalid Item for progressive hints")
|
|
return self.ItemCheck(barrier_item, value)
|
|
|
|
def CanMoontail(self):
|
|
"""Determine whether the player can perform a Moontail."""
|
|
return self.moontail and self.isdiddy and self.settings.kong_model_diddy == KongModels.default # Krusha doesnt have the jump height that Diddy has
|
|
|
|
def CanPhase(self):
|
|
"""Determine whether the player can phase."""
|
|
return self.phasewalk or (self.phasefall and (self.ischunky and self.camera))
|
|
|
|
def AddEvent(self, event):
|
|
"""Add an event to events list so it can be checked for logically."""
|
|
self.Events.append(event)
|
|
|
|
def SetKong(self, kong):
|
|
"""Set current kong for logic."""
|
|
self.kong = kong
|
|
self.UpdateKongs()
|
|
|
|
def GetKongs(self):
|
|
"""Return all owned kongs."""
|
|
ownedKongs = []
|
|
if self.donkey:
|
|
ownedKongs.append(Kongs.donkey)
|
|
if self.diddy:
|
|
ownedKongs.append(Kongs.diddy)
|
|
if self.lanky:
|
|
ownedKongs.append(Kongs.lanky)
|
|
if self.tiny:
|
|
ownedKongs.append(Kongs.tiny)
|
|
if self.chunky:
|
|
ownedKongs.append(Kongs.chunky)
|
|
return ownedKongs
|
|
|
|
def UpdateKongs(self):
|
|
"""Set variables for current kong based on self.kong."""
|
|
self.isdonkey = self.kong == Kongs.donkey
|
|
self.isdiddy = self.kong == Kongs.diddy
|
|
self.islanky = self.kong == Kongs.lanky
|
|
self.istiny = self.kong == Kongs.tiny
|
|
self.ischunky = self.kong == Kongs.chunky
|
|
|
|
def IsKong(self, kong):
|
|
"""Check if logic is currently a specific kong."""
|
|
if kong == Kongs.donkey:
|
|
return self.isdonkey
|
|
if kong == Kongs.diddy:
|
|
return self.isdiddy
|
|
if kong == Kongs.lanky:
|
|
return self.islanky
|
|
if kong == Kongs.tiny:
|
|
return self.istiny
|
|
if kong == Kongs.chunky:
|
|
return self.ischunky
|
|
if kong == Kongs.any:
|
|
return True
|
|
|
|
def HasKong(self, kong):
|
|
"""Check if logic currently owns a specific kong."""
|
|
if kong == Kongs.donkey:
|
|
return self.donkey
|
|
if kong == Kongs.diddy:
|
|
return self.diddy
|
|
if kong == Kongs.lanky:
|
|
return self.lanky
|
|
if kong == Kongs.tiny:
|
|
return self.tiny
|
|
if kong == Kongs.chunky:
|
|
return self.chunky
|
|
if kong == Kongs.any:
|
|
return True
|
|
|
|
def HasGun(self, kong):
|
|
"""Check if logic currently is currently the specified kong and owns a gun for them."""
|
|
if kong == Kongs.donkey:
|
|
return self.coconut and self.isdonkey
|
|
elif kong == Kongs.diddy:
|
|
return self.peanut and self.isdiddy
|
|
elif kong == Kongs.lanky:
|
|
return self.grape and self.islanky
|
|
elif kong == Kongs.tiny:
|
|
return self.feather and self.istiny
|
|
elif kong == Kongs.chunky:
|
|
return self.pineapple and self.ischunky
|
|
elif kong == Kongs.any:
|
|
return (self.coconut and self.isdonkey) or (self.peanut and self.isdiddy) or (self.grape and self.islanky) or (self.feather and self.istiny) or (self.pineapple and self.ischunky)
|
|
return False
|
|
|
|
def HasInstrument(self, kong):
|
|
"""Check if logic currently is currently the specified kong and owns an instrument for them."""
|
|
if kong == Kongs.donkey:
|
|
return self.bongos and self.isdonkey
|
|
if kong == Kongs.diddy:
|
|
return self.guitar and self.isdiddy
|
|
if kong == Kongs.lanky:
|
|
return self.trombone and self.islanky
|
|
if kong == Kongs.tiny:
|
|
return self.saxophone and self.istiny
|
|
if kong == Kongs.chunky:
|
|
return self.triangle and self.ischunky
|
|
if kong == Kongs.any:
|
|
return (self.bongos and self.isdonkey) or (self.guitar and self.isdiddy) or (self.trombone and self.islanky) or (self.saxophone and self.istiny) or (self.triangle and self.ischunky)
|
|
|
|
def ItemCounts(self):
|
|
"""Get the amount of items collected in terms of B. Locker-relevant items."""
|
|
# Calculate Colored Bananas count
|
|
CBCount = sum(sum(lvl) for lvl in self.ColoredBananas)
|
|
|
|
# List of moves
|
|
moves = [
|
|
self.vines,
|
|
self.swim,
|
|
self.oranges,
|
|
self.barrels,
|
|
self.climbing,
|
|
self.blast,
|
|
self.strongKong,
|
|
self.grab,
|
|
self.charge,
|
|
self.jetpack,
|
|
self.spring,
|
|
self.handstand,
|
|
self.balloon,
|
|
self.sprint,
|
|
self.mini,
|
|
self.twirl,
|
|
self.monkeyport,
|
|
self.hunkyChunky,
|
|
self.punch,
|
|
self.gorillaGone,
|
|
self.coconut,
|
|
self.peanut,
|
|
self.grape,
|
|
self.feather,
|
|
self.pineapple,
|
|
self.bongos,
|
|
self.guitar,
|
|
self.trombone,
|
|
self.saxophone,
|
|
self.triangle,
|
|
self.camera,
|
|
self.shockwave,
|
|
self.scope,
|
|
self.homing,
|
|
]
|
|
|
|
# Calculate keys count
|
|
keys = sum([self.JapesKey, self.AztecKey, self.FactoryKey, self.GalleonKey, self.ForestKey, self.CavesKey, self.CastleKey, self.HelmKey])
|
|
|
|
# Calculate company coins count
|
|
company_coins = self.nintendoCoin + self.rarewareCoin
|
|
|
|
# Calculate game percentage
|
|
game_percentage = 0.4 * self.GoldenBananas + 0.5 * self.BattleCrowns + 0.2 * self.BananaFairies + 0.2 * self.BananaMedals + 0.25 * keys + 0.5 * company_coins
|
|
if game_percentage == 100.4:
|
|
game_percentage = 101
|
|
|
|
# Create check counts dictionary
|
|
check_counts = {
|
|
BarrierItems.GoldenBanana: self.GoldenBananas,
|
|
BarrierItems.Blueprint: len(self.Blueprints),
|
|
BarrierItems.CompanyCoin: company_coins,
|
|
BarrierItems.Key: keys,
|
|
BarrierItems.Medal: self.BananaMedals,
|
|
BarrierItems.Crown: self.BattleCrowns,
|
|
BarrierItems.Fairy: self.BananaFairies,
|
|
BarrierItems.RainbowCoin: self.RainbowCoins,
|
|
BarrierItems.Bean: self.Beans,
|
|
BarrierItems.Pearl: self.Pearls,
|
|
BarrierItems.ColoredBanana: CBCount,
|
|
BarrierItems.IceTrap: True, # TODO
|
|
BarrierItems.Kong: sum([self.donkey, self.diddy, self.lanky, self.tiny, self.chunky]),
|
|
BarrierItems.Move: sum(moves) + self.Slam + self.AmmoBelts + self.InstUpgrades,
|
|
BarrierItems.Percentage: int(game_percentage),
|
|
}
|
|
|
|
return check_counts
|
|
|
|
def ItemCheck(self, item: BarrierItems, count: int) -> bool:
|
|
"""Check if item requirement has been fulfilled."""
|
|
check_counts = self.ItemCounts()
|
|
if item in check_counts.keys():
|
|
return check_counts[item] >= count
|
|
return True
|
|
|
|
def CrownDoorOpened(self):
|
|
"""Check if Crown Door is opened."""
|
|
if self.settings.crown_door_item == BarrierItems.Nothing:
|
|
return True
|
|
return self.ItemCheck(self.settings.crown_door_item, self.settings.crown_door_item_count)
|
|
|
|
def CoinDoorOpened(self):
|
|
"""Check if Coin Door is opened."""
|
|
if self.settings.coin_door_item == BarrierItems.Nothing:
|
|
return True
|
|
return self.ItemCheck(self.settings.coin_door_item, self.settings.coin_door_item_count)
|
|
|
|
def CanFreeDiddy(self):
|
|
"""Check if the cage locking Diddy's vanilla location can be opened."""
|
|
return self.spoiler.LocationList[Locations.DiddyKong].item == Items.NoItem or self.HasGun(self.settings.diddy_freeing_kong)
|
|
|
|
def CanOpenJapesGates(self):
|
|
"""Check if we can pick up the item inside Diddy's cage, thus opening the gates in Japes."""
|
|
caged_item_id = self.spoiler.LocationList[Locations.JapesDonkeyFreeDiddy].item
|
|
# If it's NoItem, then the gates are already open
|
|
if caged_item_id == Items.NoItem:
|
|
return True
|
|
# If we can't free Diddy, then we can't access the item so we can't reach the item
|
|
if not self.CanFreeDiddy():
|
|
return False
|
|
# If we are the right kong, then we can always get the item
|
|
if self.IsKong(self.settings.diddy_freeing_kong):
|
|
return True
|
|
# If we aren't the right kong, we need free trade to be on
|
|
elif self.settings.free_trade_items:
|
|
# During the fill we can't assume this item is accessible quite yet - this could cause errors with placing items in the back of Japes
|
|
if caged_item_id is None:
|
|
return False
|
|
# If it's not a blueprint, free trade gets us the item
|
|
if ItemList[caged_item_id].type != Types.Blueprint:
|
|
return True
|
|
# But if it is a blueprint, we need to check blueprint access (which checks blueprint free trade)
|
|
else:
|
|
return self.BlueprintAccess(ItemList[caged_item_id])
|
|
# If we failed to hit a successful condition, we failed to reach the caged item
|
|
return False
|
|
|
|
def CanFreeTiny(self):
|
|
"""Check if kong at Tiny location can be freed, requires either chimpy charge or primate punch."""
|
|
if self.spoiler.LocationList[Locations.TinyKong].item == Items.NoItem:
|
|
return self.IsKong(self.settings.tiny_freeing_kong) or self.settings.free_trade_items
|
|
elif self.settings.tiny_freeing_kong == Kongs.diddy:
|
|
return self.charge and self.isdiddy
|
|
elif self.settings.tiny_freeing_kong == Kongs.chunky:
|
|
return self.punch and self.ischunky
|
|
# Used only as placeholder during fill when kong puzzles are not yet assigned
|
|
elif self.settings.tiny_freeing_kong == Kongs.any:
|
|
return True
|
|
|
|
def CanLlamaSpit(self):
|
|
"""Check if the Llama spit can be triggered."""
|
|
return self.HasInstrument(self.settings.lanky_freeing_kong)
|
|
|
|
def CanFreeLanky(self):
|
|
"""Check if kong at Lanky location can be freed, requires freeing kong to have its gun and instrument."""
|
|
return (self.HasGun(self.settings.lanky_freeing_kong) or self.spoiler.LocationList[Locations.LankyKong].item == Items.NoItem) and (
|
|
(self.swim and self.HasInstrument(self.settings.lanky_freeing_kong)) or self.CanPhase() or self.CanPhaseswim()
|
|
)
|
|
|
|
def CanFreeChunky(self):
|
|
"""Check if kong at Chunky location can be freed."""
|
|
# If the cage is empty, the item is just lying on the ground
|
|
if self.spoiler.LocationList[Locations.ChunkyKong].item == Items.NoItem:
|
|
return self.IsKong(self.settings.chunky_freeing_kong) or self.settings.free_trade_items
|
|
# Otherwise you need the right slam level (usually 1)
|
|
else:
|
|
return self.CanSlamSwitch(Levels.FranticFactory, 1) and self.IsKong(self.settings.chunky_freeing_kong)
|
|
|
|
def LevelEntered(self, level):
|
|
"""Check whether a level, or any level above it, has been entered."""
|
|
if Events.CastleEntered in self.Events:
|
|
return True
|
|
elif Events.CavesEntered in self.Events and level <= Levels.CrystalCaves:
|
|
return True
|
|
elif Events.ForestEntered in self.Events and level <= Levels.FungiForest:
|
|
return True
|
|
elif Events.GalleonEntered in self.Events and level <= Levels.GloomyGalleon:
|
|
return True
|
|
elif Events.FactoryEntered in self.Events and level <= Levels.FranticFactory:
|
|
return True
|
|
elif Events.AztecEntered in self.Events and level <= Levels.AngryAztec:
|
|
return True
|
|
elif Events.JapesEntered in self.Events and level <= Levels.JungleJapes:
|
|
return True
|
|
return False
|
|
|
|
def AddCollectible(self, collectible, level):
|
|
"""Add a collectible."""
|
|
if collectible.enabled:
|
|
missingGun = False
|
|
if collectible.type == Collectibles.coin:
|
|
# Normal coins, add amount for the kong
|
|
self.Coins[collectible.kong] += collectible.amount
|
|
self.RegularCoins[collectible.kong] += collectible.amount
|
|
# Add bananas for correct level for this kong
|
|
elif collectible.type == Collectibles.banana:
|
|
self.ColoredBananas[level][collectible.kong] += collectible.amount
|
|
# Add 5 times amount of banana bunches
|
|
elif collectible.type == Collectibles.bunch:
|
|
self.ColoredBananas[level][collectible.kong] += collectible.amount * 5
|
|
# Add 10 bananas for a balloon
|
|
elif collectible.type == Collectibles.balloon:
|
|
if self.HasGun(collectible.kong):
|
|
self.ColoredBananas[level][collectible.kong] += collectible.amount * 10
|
|
collectible.added = True
|
|
missingGun = True
|
|
if not missingGun:
|
|
collectible.added = True
|
|
|
|
def PurchaseShopItem(self, location_id):
|
|
"""Purchase from this location and subtract price from logical coin counts."""
|
|
location = self.spoiler.LocationList[location_id]
|
|
price = GetPriceAtLocation(self.settings, location_id, location, self.Slam, self.AmmoBelts, self.InstUpgrades)
|
|
if price is None: # This shouldn't happen but it's probably harmless
|
|
return # TODO: solve this
|
|
# If shared move, take the price from all kongs EVEN IF THEY AREN'T FREED YET
|
|
if location.kong == Kongs.any:
|
|
for kong in range(0, 5):
|
|
self.Coins[kong] -= price
|
|
self.SpentCoins[kong] += price
|
|
return
|
|
# If kong specific move, just that kong paid for it
|
|
else:
|
|
self.Coins[location.kong] -= price
|
|
self.SpentCoins[location.kong] += price
|
|
return
|
|
|
|
def HasAccess(self, region, kong):
|
|
"""Check if a certain kong has access to a certain region.
|
|
|
|
Usually the region's own HasAccess function is used, but this is necessary for checking access for other regions in logic files.
|
|
"""
|
|
return self.spoiler.RegionList[region].HasAccess(kong)
|
|
|
|
def TimeAccess(self, region, time):
|
|
"""Check if a certain region has the given time of day access for current kong."""
|
|
if time == Time.Day:
|
|
return self.spoiler.RegionList[region].dayAccess[self.kong]
|
|
elif time == Time.Night:
|
|
return self.spoiler.RegionList[region].nightAccess[self.kong]
|
|
# Not sure when this'd be used
|
|
else: # if time == Time.Both
|
|
return self.spoiler.RegionList[region].dayAccess[self.kong] or self.spoiler.RegionList[region].nightAccess[self.kong]
|
|
|
|
def BlueprintAccess(self, item):
|
|
"""Check if we are the correct kong for this blueprint item."""
|
|
if item is None or item.type != Types.Blueprint:
|
|
return False
|
|
return self.settings.free_trade_blueprints or self.IsKong(item.kong)
|
|
|
|
def HintAccess(self, location, region_id):
|
|
"""Check if we are the right kong for this hint door."""
|
|
# The only weird exception: vanilla Fungi Lobby hint doors only check for Chunky, not the current Kong, and all besides Chunky's needs grab
|
|
if not self.settings.wrinkly_location_rando and not self.settings.remove_wrinkly_puzzles and region_id == RegionEnum.FungiForestLobby:
|
|
return self.chunky and (location.kong == Kongs.chunky or (self.donkey and self.grab))
|
|
# Last step: either have the kong or have Kongless Hint Doors enabled
|
|
return self.HasKong(location.kong) or self.settings.wrinkly_available
|
|
|
|
def CanBuy(self, location, buy_empty=False):
|
|
"""Check if there are enough coins to purchase this location."""
|
|
return CanBuy(self.spoiler, location, self, buy_empty)
|
|
|
|
def AnyKongCanBuy(self, location, buy_empty=False):
|
|
"""Check if there are enough coins for any owned kong to purchase this location."""
|
|
return AnyKongCanBuy(self.spoiler, location, self, buy_empty)
|
|
|
|
def CanAccessKRool(self):
|
|
"""Make sure that each required key has been turned in."""
|
|
required_base_keys = [
|
|
Events.JapesKeyTurnedIn,
|
|
Events.AztecKeyTurnedIn,
|
|
Events.FactoryKeyTurnedIn,
|
|
Events.GalleonKeyTurnedIn,
|
|
Events.ForestKeyTurnedIn,
|
|
Events.CavesKeyTurnedIn,
|
|
Events.CastleKeyTurnedIn,
|
|
Events.HelmKeyTurnedIn,
|
|
]
|
|
if self.settings.k_rool_vanilla_requirement:
|
|
required_base_keys = [
|
|
Events.FactoryKeyTurnedIn,
|
|
Events.HelmKeyTurnedIn,
|
|
]
|
|
return all(not keyRequired not in self.Events for keyRequired in self.settings.krool_keys_required if keyRequired in required_base_keys)
|
|
|
|
def IsKLumsyFree(self):
|
|
"""Check all keys."""
|
|
return all(not keyRequired not in self.Events for keyRequired in self.settings.krool_keys_required)
|
|
|
|
def IsBossReachable(self, level):
|
|
"""Check if the boss banana requirement is met."""
|
|
return self.HasEnoughKongs(level) and ((sum(self.ColoredBananas[level]) >= self.settings.BossBananas[level]) or self.troff_skip)
|
|
|
|
def HasEnoughKongs(self, level, forPreviousLevel=False):
|
|
"""Check if kongs are required for progression, do we have enough to reach the given level."""
|
|
# If your kongs are not progression (LZR, no logic, etc.) or it's *complex* level order, these requirements don't apply
|
|
if self.settings.kongs_for_progression and not self.settings.hard_level_progression:
|
|
levelIndex = 8
|
|
if level != Levels.HideoutHelm:
|
|
# Figure out where this level fits in the progression
|
|
levelIndex = GetShuffledLevelIndex(level)
|
|
if forPreviousLevel:
|
|
levelIndex = levelIndex - 1
|
|
# Must have sufficient kongs freed to make forward progress for first 5 levels
|
|
if levelIndex < 5:
|
|
return len(self.GetKongs()) > levelIndex
|
|
else:
|
|
# Expect to have all the kongs by level 6
|
|
return len(self.GetKongs()) == 5
|
|
else:
|
|
return True
|
|
|
|
def isKrushaAdjacent(self, kong: Kongs):
|
|
"""Check if player is a krusha-adjacent model."""
|
|
settings_values = [
|
|
self.settings.kong_model_dk,
|
|
self.settings.kong_model_diddy,
|
|
self.settings.kong_model_lanky,
|
|
self.settings.kong_model_tiny,
|
|
self.settings.kong_model_chunky,
|
|
]
|
|
return settings_values[kong] in (KongModels.krusha, KongModels.krool_cutscene, KongModels.krool_fight)
|
|
|
|
def CanSlamChunkyPhaseSwitch(self):
|
|
"""Check if the player can slam the switch in Chunky Phase."""
|
|
stg = self.settings.chunky_phase_slam_req_internal
|
|
if stg == SlamRequirement.blue:
|
|
return self.superSlam
|
|
elif stg == SlamRequirement.red:
|
|
return self.superDuperSlam
|
|
return self.Slam
|
|
|
|
def IsBossBeatable(self, level):
|
|
"""Return true if the boss for a given level is beatable according to boss location rando and boss kong rando."""
|
|
requiredKong = self.settings.boss_kongs[level]
|
|
bossFight = self.settings.boss_maps[level]
|
|
# Ensure we have the required moves for the boss fight itself
|
|
hasRequiredMoves = True
|
|
if (
|
|
bossFight == Maps.FactoryBoss
|
|
and requiredKong == Kongs.tiny
|
|
and not (self.HardBossesSettingEnabled(HardBossesSelected.alternative_mad_jack_kongs) and self.settings.kong_model_tiny == KongModels.default)
|
|
):
|
|
hasRequiredMoves = self.twirl and self.Slam
|
|
elif bossFight == Maps.FactoryBoss:
|
|
hasRequiredMoves = self.Slam
|
|
elif bossFight == Maps.FungiBoss:
|
|
hasRequiredMoves = self.hunkyChunky and self.barrels
|
|
elif bossFight == Maps.JapesBoss or bossFight == Maps.AztecBoss or bossFight == Maps.CavesBoss:
|
|
hasRequiredMoves = self.barrels
|
|
elif bossFight == Maps.CastleBoss and self.IsLavaWater():
|
|
hasRequiredMoves = self.Melons >= 3
|
|
elif bossFight == Maps.KroolDonkeyPhase:
|
|
hasRequiredMoves = (self.blast or (not self.settings.cannons_require_blast)) and self.climbing
|
|
elif bossFight == Maps.KroolDiddyPhase:
|
|
hasRequiredMoves = self.jetpack and self.peanut
|
|
elif bossFight == Maps.KroolLankyPhase:
|
|
hasRequiredMoves = self.barrels and self.trombone
|
|
elif bossFight == Maps.KroolTinyPhase:
|
|
hasRequiredMoves = self.mini and self.feather
|
|
elif bossFight == Maps.KroolChunkyPhase:
|
|
hasRequiredMoves = self.punch and self.CanSlamChunkyPhaseSwitch() and self.hunkyChunky and self.gorillaGone
|
|
# In simple level order, there are a couple very specific cases we have to account for in order to prevent boss fill failures
|
|
level_order_matters = not self.settings.hard_level_progression and self.settings.shuffle_loading_zones in (
|
|
ShuffleLoadingZones.none,
|
|
ShuffleLoadingZones.levels,
|
|
)
|
|
if level_order_matters and not self.assumeFillSuccess: # These conditions only matter on fill, not on playthrough
|
|
order_of_level = 8 # Guaranteed to be 1-8 here
|
|
for level_order in self.settings.level_order:
|
|
if self.settings.level_order[level_order] == level:
|
|
order_of_level = level_order
|
|
if order_of_level == 4 and not self.barrels: # Prevent Barrels on boss 3
|
|
return False
|
|
if order_of_level == 7 and (
|
|
not self.hunkyChunky or (not self.twirl and not self.HardBossesSettingEnabled(HardBossesSelected.alternative_mad_jack_kongs))
|
|
): # Prevent Hunky on boss 7, and also Twirl on non-hard bosses
|
|
return False
|
|
return self.IsKong(requiredKong) and hasRequiredMoves
|
|
|
|
def HasFillRequirementsForLevel(self, level):
|
|
"""Check if we meet the fill's move requirements for the given level."""
|
|
# These requirements are only relevant for fill purposes - once we know the fill is valid, we can ignore these requirements
|
|
if self.assumeFillSuccess:
|
|
return True
|
|
# Additionally, these restrictions only apply to simple level order, as these are the only seeds progressing levels in 1-7 order
|
|
level_order_matters = not self.settings.hard_level_progression and self.settings.shuffle_loading_zones in (
|
|
ShuffleLoadingZones.none,
|
|
ShuffleLoadingZones.levels,
|
|
)
|
|
if level_order_matters:
|
|
# Levels have some special requirements depending on where they fall in the level order
|
|
order_of_level = 8
|
|
order_of_aztec = 0
|
|
for level_order in self.settings.level_order:
|
|
if self.settings.level_order[level_order] == level:
|
|
order_of_level = level_order
|
|
if self.settings.level_order[level_order] == Levels.AngryAztec:
|
|
order_of_aztec = level_order
|
|
# You need to have vines or twirl before you can enter Aztec or any level beyond it
|
|
if order_of_level >= order_of_aztec and not (self.can_use_vines or (self.istiny and self.twirl)):
|
|
return False
|
|
if order_of_level >= 4:
|
|
# Require the following moves by level 4:
|
|
# - Swim so you can get into Lobby 4. This prevents logic from skipping this level for T&S requirements, preventing 0'd T&S.
|
|
# - Barrels so there will always be an eligible boss fill given the available moves at any level.
|
|
# - Vines for gameplay reasons. Needing vines for Helm is a frequent bottleneck and this eases the hunt for it.
|
|
if not self.swim or not self.barrels or not self.can_use_vines:
|
|
return False
|
|
# Require one of twirl or hunky chunky by level 7 to prevent non-hard-boss fill failures
|
|
if not self.HardBossesSettingEnabled(HardBossesSelected.alternative_mad_jack_kongs) and order_of_level >= 7 and not (self.twirl or self.hunkyChunky):
|
|
return False
|
|
# Require both hunky chunky and twirl (or hard bosses) before Helm to prevent boss fill failures
|
|
if order_of_level > 7 and not (self.hunkyChunky and (self.twirl or self.HardBossesSettingEnabled(HardBossesSelected.alternative_mad_jack_kongs))):
|
|
return False
|
|
# Make sure we have access to all prior required keys before entering the next level - this prevents keys from being placed in levels beyond what they unlock
|
|
if order_of_level > 1 and not self.JapesKey:
|
|
return False
|
|
elif order_of_level > 2 and not self.AztecKey:
|
|
return False
|
|
elif order_of_level > 4 and (not self.FactoryKey or not self.GalleonKey):
|
|
return False
|
|
elif order_of_level > 5 and not self.ForestKey:
|
|
return False
|
|
elif order_of_level > 7 and (not self.CavesKey or not self.CastleKey):
|
|
return False
|
|
# If we have the moves, ensure we have enough kongs as well
|
|
return self.HasEnoughKongs(level, forPreviousLevel=True)
|
|
|
|
def CanBeatLankyPhase(self):
|
|
"""Check whether the player can beat Lanky phase of K Rool."""
|
|
if self.HardBossesSettingEnabled(HardBossesSelected.beta_lanky_phase):
|
|
return self.lanky and self.grape and self.barrels
|
|
return self.lanky and self.trombone and self.barrels
|
|
|
|
def IsLevelEnterable(self, level):
|
|
"""Check if level entry requirement is met."""
|
|
# We must meet the fill's kong and move requirements to enter this level
|
|
if not self.HasFillRequirementsForLevel(level):
|
|
return False
|
|
# Calculate what levels we can glitch into
|
|
dk_skip_levels = [
|
|
Levels.AngryAztec,
|
|
Levels.GloomyGalleon,
|
|
Levels.FungiForest,
|
|
Levels.CrystalCaves,
|
|
Levels.CreepyCastle,
|
|
]
|
|
if self.CanMoonkick():
|
|
dk_skip_levels.append(Levels.HideoutHelm)
|
|
can_dk_skip = self.isdonkey and self.dk_blocker_skip and level in dk_skip_levels
|
|
can_diddy_skip = self.isdiddy and self.lanky_blocker_skip and level == Levels.HideoutHelm and self.generalclips
|
|
can_lanky_skip = self.islanky and self.lanky_blocker_skip and level != Levels.HideoutHelm
|
|
can_tiny_skip = self.istiny and self.lanky_blocker_skip and level == Levels.HideoutHelm and self.generalclips
|
|
can_chunky_skip = self.ischunky and self.lanky_blocker_skip and self.punch and level not in (Levels.FranticFactory, Levels.HideoutHelm)
|
|
available_items = self.ItemCounts()
|
|
can_pay_blocker = self.assumePaidBLockers or available_items[self.settings.BLockerEntryItems[level]] >= self.settings.BLockerEntryCount[level]
|
|
# To enter a level, we either need (or assume) enough stuff to get rid of B. Locker or a glitch way to bypass it
|
|
return can_pay_blocker or can_dk_skip or can_diddy_skip or can_lanky_skip or can_tiny_skip or can_chunky_skip
|
|
|
|
def WinConditionMet(self):
|
|
"""Check if the current game state has met the win condition."""
|
|
# Special Win Cons
|
|
if self.settings.win_condition_item == WinConditionComplex.beat_krool:
|
|
return Events.KRoolDefeated in self.Events
|
|
elif self.settings.win_condition_item == WinConditionComplex.krem_kapture: # Photo taking doesn't have a perfect wincon so this'll do until something better is concocted
|
|
return Events.KRoolDefeated in self.Events and self.camera
|
|
elif self.settings.win_condition_item == WinConditionComplex.get_key8:
|
|
return self.HelmKey
|
|
elif self.settings.win_condition_item == WinConditionComplex.dk_rap_items:
|
|
dk_rap_items = [
|
|
self.donkey,
|
|
self.diddy,
|
|
self.lanky,
|
|
self.tiny,
|
|
self.chunky,
|
|
self.coconut,
|
|
self.peanut,
|
|
self.grape,
|
|
self.pineapple,
|
|
self.guitar,
|
|
self.trombone,
|
|
self.strongKong,
|
|
# self.spring,
|
|
self.jetpack,
|
|
self.handstand,
|
|
self.balloon,
|
|
self.mini,
|
|
self.twirl,
|
|
# self.hunkyChunky,
|
|
self.barrels,
|
|
self.oranges,
|
|
# self.shockwave,
|
|
self.climbing,
|
|
# self.superDuperSlam,
|
|
self.crankyAccess,
|
|
]
|
|
for k in dk_rap_items:
|
|
if not k:
|
|
return False
|
|
return True
|
|
# Get X amount of Y item win cons
|
|
win_con_table = {
|
|
WinConditionComplex.req_bean: BarrierItems.Bean,
|
|
WinConditionComplex.req_bp: BarrierItems.Blueprint,
|
|
WinConditionComplex.req_companycoins: BarrierItems.CompanyCoin,
|
|
WinConditionComplex.req_crown: BarrierItems.Crown,
|
|
WinConditionComplex.req_fairy: BarrierItems.Fairy,
|
|
WinConditionComplex.req_key: BarrierItems.Key,
|
|
WinConditionComplex.req_gb: BarrierItems.GoldenBanana,
|
|
WinConditionComplex.req_medal: BarrierItems.Medal,
|
|
WinConditionComplex.req_pearl: BarrierItems.Pearl,
|
|
WinConditionComplex.req_rainbowcoin: BarrierItems.RainbowCoin,
|
|
}
|
|
if self.settings.win_condition_item not in win_con_table:
|
|
raise Exception(f"Invalid Win Condition {self.settings.win_condition_item.name}")
|
|
return self.ItemCheck(win_con_table[self.settings.win_condition_item], self.settings.win_condition_count)
|
|
|
|
def CanGetRarewareCoin(self):
|
|
"""Check if you meet the logical requirements to obtain the Rareware Coin."""
|
|
have_enough_medals = self.BananaMedals >= self.settings.medal_requirement
|
|
# Make sure you have access to enough levels to fit the locations in. This isn't super precise and doesn't need to be.
|
|
required_level_order = max(2, min(ceil(self.settings.medal_requirement / 4), 7)) # At least level 2 to give space for medal placements, at most level 6 to allow shenanigans
|
|
return have_enough_medals and self.HasFillRequirementsForLevel(self.settings.level_order[required_level_order])
|
|
|
|
def CanGetRarewareGB(self):
|
|
"""Check if you meet the logical requirements to obtain the Rareware GB."""
|
|
have_enough_fairies = self.BananaFairies >= self.settings.rareware_gb_fairies
|
|
is_correct_kong = self.istiny or self.settings.free_trade_items
|
|
required_level_order = max(2, min(ceil(self.settings.rareware_gb_fairies / 2), 5)) # At least level 2 to give space for fairy placements, at most level 5 to allow shenanigans
|
|
return have_enough_fairies and is_correct_kong and self.HasFillRequirementsForLevel(self.settings.level_order[required_level_order])
|
|
|
|
def HasAllItems(self):
|
|
"""Return if you have all progression items."""
|
|
self.Update(self.latest_owned_items)
|
|
# If you didn't beat the game, you obviously don't have all the progression items - this covers the possible need for camera and each key
|
|
if not self.WinConditionMet():
|
|
return False
|
|
# Otherwise return true if you have all major moves
|
|
return (
|
|
self.donkey
|
|
and self.diddy
|
|
and self.lanky
|
|
and self.tiny
|
|
and self.chunky
|
|
and self.vines
|
|
and self.climbing
|
|
and self.swim
|
|
and self.barrels
|
|
and self.oranges
|
|
and self.blast
|
|
and self.strongKong
|
|
and self.grab
|
|
and self.charge
|
|
and self.jetpack
|
|
and self.spring
|
|
and self.handstand
|
|
and self.balloon
|
|
and self.sprint
|
|
and self.mini
|
|
and self.twirl
|
|
and self.monkeyport
|
|
and self.hunkyChunky
|
|
and self.punch
|
|
and self.gorillaGone
|
|
and self.superDuperSlam
|
|
and self.coconut
|
|
and self.peanut
|
|
and self.grape
|
|
and self.feather
|
|
and self.pineapple
|
|
and self.homing
|
|
and self.scope
|
|
and self.shockwave
|
|
and self.bongos
|
|
and self.guitar
|
|
and self.trombone
|
|
and self.saxophone
|
|
and self.triangle
|
|
)
|
|
|
|
def CanSurviveFallDamage(self):
|
|
"""Check if you can survive a single instance of fall damage."""
|
|
if self.settings.damage_amount != DamageAmount.ohko:
|
|
if self.settings.damage_amount != DamageAmount.quad or self.Melons > 1:
|
|
return True
|
|
return False
|
|
|
|
|
|
# Import regions from logic files
|
|
RegionsOriginal = {
|
|
**randomizer.LogicFiles.DKIsles.LogicRegions,
|
|
**randomizer.LogicFiles.JungleJapes.LogicRegions,
|
|
**randomizer.LogicFiles.AngryAztec.LogicRegions,
|
|
**randomizer.LogicFiles.FranticFactory.LogicRegions,
|
|
**randomizer.LogicFiles.GloomyGalleon.LogicRegions,
|
|
**randomizer.LogicFiles.FungiForest.LogicRegions,
|
|
**randomizer.LogicFiles.CrystalCaves.LogicRegions,
|
|
**randomizer.LogicFiles.CreepyCastle.LogicRegions,
|
|
**randomizer.LogicFiles.HideoutHelm.LogicRegions,
|
|
**randomizer.LogicFiles.Shops.LogicRegions,
|
|
}
|
|
|
|
# Auxillary regions for colored bananas and banana coins
|
|
CollectibleRegionsOriginal = {
|
|
**randomizer.CollectibleLogicFiles.DKIsles.LogicRegions,
|
|
**randomizer.CollectibleLogicFiles.JungleJapes.LogicRegions,
|
|
**randomizer.CollectibleLogicFiles.AngryAztec.LogicRegions,
|
|
**randomizer.CollectibleLogicFiles.FranticFactory.LogicRegions,
|
|
**randomizer.CollectibleLogicFiles.GloomyGalleon.LogicRegions,
|
|
**randomizer.CollectibleLogicFiles.FungiForest.LogicRegions,
|
|
**randomizer.CollectibleLogicFiles.CrystalCaves.LogicRegions,
|
|
**randomizer.CollectibleLogicFiles.CreepyCastle.LogicRegions,
|
|
}
|