Files
dockipelago/worlds/k64/regions.py
Jonathan Tinney 7971961166
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
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

236 lines
11 KiB
Python

import typing
from BaseClasses import Region, CollectionState, ItemClassification
from .consumable_info import consumable_by_level
from .locations import K64Location, location_table, star_locations, food_locations, one_up_locations
from .items import K64Item
from .names import LocationName, ItemName
from .rules import (burn_levels, needle_levels, stone_levels,
spark_levels, bomb_levels, ice_levels, cutter_levels, dedede_copy_levels, waddle_copy_levels)
if typing.TYPE_CHECKING:
from . import K64World
class K64Region(Region):
game = "Kirby 64 - The Crystal Shards"
def copy_ability_sweep(self, state: "CollectionState"):
for ability, regions in zip(["Burning Ability", "Stone Ability", "Ice Ability",
"Needle Ability", "Bomb Ability", "Spark Ability", "Cutter Ability"],
[burn_levels, stone_levels, ice_levels,
needle_levels, bomb_levels, spark_levels, cutter_levels]):
regions: list[str] = regions.copy()
if ability in dedede_copy_levels and state.has(ItemName.king_dedede, self.player):
regions.extend(dedede_copy_levels[ability])
if ability in waddle_copy_levels and state.has(ItemName.waddle_dee, self.player):
regions.extend(waddle_copy_levels[ability])
if any(state.can_reach(region, "Region", self.player) for region in regions):
state.prog_items[self.player][ability] = 1
else:
del state.prog_items[self.player][ability]
def can_reach(self, state: CollectionState) -> bool:
stale = state.k64_stale[self.player]
if stale:
state.k64_stale[self.player] = False
self.copy_ability_sweep(state)
return super().can_reach(state)
default_levels = {
1: [0x0001, 0x0002, 0x0003, 0x0200],
2: [0x0004, 0x0005, 0x0006, 0x0007, 0x0201],
3: [0x0008, 0x0009, 0x000A, 0x000B, 0x0202],
4: [0x000C, 0x000D, 0x000E, 0x000F, 0x0203],
5: [0x0010, 0x0011, 0x0012, 0x0013, 0x0204],
6: [0x0014, 0x0015, 0x0016, 0x0205],
}
first_level_restrict = {
0x0009,
0x000A,
0x000D,
0x000F,
0x0010,
0x0013,
0x0015,
}
def generate_valid_level(level, stage, possible_stages, restrict, slot_random):
new_stage = slot_random.choice(possible_stages)
possible_stages.remove(new_stage)
if restrict and new_stage in first_level_restrict:
return generate_valid_level(level, stage, possible_stages, restrict, slot_random)
else:
return new_stage
def generate_valid_levels(world: "K64World", enforce_world: bool) -> dict:
levels: typing.Dict[int, typing.List[typing.Optional[int]]] = {
1: [None for _ in range(4)],
2: [None for _ in range(5)],
3: [None for _ in range(5)],
4: [None for _ in range(5)],
5: [None for _ in range(5)],
6: [None for _ in range(4)]
}
possible_stages = list()
for level in default_levels:
for stage in range(len(default_levels[level]) - 1):
possible_stages.append(default_levels[level][stage])
if world.options.plando_connections:
for connection in world.options.plando_connections:
try:
entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1)
stage_world, stage_stage = connection.exit.rsplit(" ", 1)
new_stage = default_levels[LocationName.level_names_inverse[stage_world.strip()]][int(stage_stage) - 1]
levels[LocationName.level_names_inverse[entrance_world.strip()]][int(entrance_stage) - 1] = new_stage
possible_stages.remove(new_stage)
except Exception:
raise Exception(
f"Invalid connection: {connection.entrance} =>"
f" {connection.exit} for player {world.player} ({world.player_name})")
for level in range(1, 7):
for stage in range(len(default_levels[level]) - 1):
# Randomize bosses separately
try:
if levels[level][stage] is None:
stage_candidates = [candidate for candidate in possible_stages
if (not enforce_world)
or (enforce_world and candidate in default_levels[level])
]
if level == 1:
if any(levels[level][x] in first_level_restrict for x in range(len(levels[level]))):
restrict = True
else:
restrict = False
else:
restrict = False
new_stage = generate_valid_level(level, stage, stage_candidates, restrict, world.random)
possible_stages.remove(new_stage)
levels[level][stage] = new_stage
except Exception:
raise Exception(f"Failed to find valid stage for {level}-{stage}. Remaining Stages:{possible_stages}")
"""
# now handle bosses
boss_shuffle: typing.Union[int, str] = world.options.boss_shuffle.value
plando_bosses = []
if isinstance(boss_shuffle, str):
# boss plando
options = boss_shuffle.split(";")
boss_shuffle = BossShuffle.options[options.pop()]
for option in options:
if "-" in option:
loc, boss = option.split("-")
loc = loc.title()
boss = boss.title()
levels[LocationName.level_names[loc]][6] = LocationName.boss_names[boss]
plando_bosses.append(LocationName.boss_names[boss])
else:
option = option.title()
for level in levels:
if levels[level][6] is None:
levels[level][6] = LocationName.boss_names[option]
plando_bosses.append(LocationName.boss_names[option])
if boss_shuffle > 0:
if boss_shuffle == 2:
possible_bosses = [default_levels[world.random.randint(1, 5)][6]
for _ in range(5 - len(plando_bosses))]
elif boss_shuffle == 3:
boss = world.random.randint(1, 5)
possible_bosses = [default_levels[boss][6] for _ in range(5 - len(plando_bosses))]
else:
possible_bosses = [default_levels[level][6] for level in default_levels
if default_levels[level][6] not in plando_bosses]
for level in levels:
if levels[level][6] is None:
boss = world.random.choice(possible_bosses)
levels[level][6] = boss
possible_bosses.remove(boss)
else:"""
for level in levels:
if levels[level][len(default_levels[level]) - 1] is None:
levels[level][len(default_levels[level]) - 1] = default_levels[level][len(default_levels[level]) - 1]
return levels
def create_levels(world: "K64World") -> None:
menu = K64Region("Menu", world.player, world.multiworld)
level1 = K64Region("Pop Star", world.player, world.multiworld)
level2 = K64Region("Rock Star", world.player, world.multiworld)
level3 = K64Region("Aqua Star", world.player, world.multiworld)
level4 = K64Region("Neo Star", world.player, world.multiworld)
level5 = K64Region("Shiver Star", world.player, world.multiworld)
level6 = K64Region("Ripple Star", world.player, world.multiworld)
level7 = K64Region("Dark Star", world.player, world.multiworld)
levels = {
1: level1,
2: level2,
3: level3,
4: level4,
5: level5,
6: level6,
}
level_shuffle = world.options.stage_shuffle.value
if (hasattr(world.multiworld, "re_gen_passthrough")
and "Kirby 64 - The Crystal Shards" in getattr(world.multiworld, "re_gen_passthrough")):
slot_data = getattr(world.multiworld, "re_gen_passthrough")["Kirby 64 - The Crystal Shards"]
world.player_levels = slot_data["player_levels"]
elif level_shuffle != 0:
world.player_levels = generate_valid_levels(world, level_shuffle == 1)
for level in world.player_levels:
for stage in range(len(world.player_levels[level])):
real_stage = world.player_levels[level][stage]
assert real_stage is not None, "Level tried to be sent with a None stage, incorrect plando?"
# placeholder for when I want to add a data file eventually
region = K64Region(location_table[real_stage].replace(" - Complete", "").replace(" Defeated", ""),
world.player, world.multiworld)
levels[level].connect(region)
crystals = [(((real_stage & 0xFF) - 1) * 3) + i + 0x0101 for i in range(3) if not real_stage & 0x200]
real_consumables = []
if real_stage in consumable_by_level:
consumables = range(*consumable_by_level[real_stage])
for consumable in consumables:
if consumable in star_locations and "Stars" in world.options.consumables:
real_consumables.append(consumable)
elif consumable in one_up_locations and "1-Ups" in world.options.consumables:
real_consumables.append(consumable)
elif consumable in food_locations and "Food" in world.options.consumables:
real_consumables.append(consumable)
locations = {
location_table[code]: code for code in location_table
if code in [real_stage, *crystals, *real_consumables]
}
region.add_locations(locations, K64Location)
world.multiworld.regions.append(region)
dark_star_locations: dict[str, int|None] = {LocationName.dark_star: None}
if "Food" in world.options.consumables:
dark_star_locations[LocationName.dark_star_adeleine] = 0x0761
level7.add_locations(dark_star_locations, K64Location)
menu.connect(level1, "Start Game")
level1.connect(level2, "To Level 2")
level2.connect(level3, "To Level 3")
level3.connect(level4, "To Level 4")
level4.connect(level5, "To Level 5")
level5.connect(level6, "To Level 6")
menu.connect(level7, "To Level 7") # put the connection on menu, since you can reach it before level 6 on fast goal
world.multiworld.regions.extend([menu, level1, level2, level3, level4, level5, level6, level7])
goal_location = world.get_location(LocationName.dark_star)
goal_location.place_locked_item(
K64Item(ItemName.ribbons_crystal, ItemClassification.progression, None, world.player))