SA2B: v2.0 Content Update (#1294)
Changelog:
Features:
- Completely reworked mission progression system
- Control of which mission types can be active per-gameplay-style
- Control of how many missions are active per-gameplay-style
- Mission order shuffle
- Two new Chaos Emerald Hunt goals
- `Chaos Emerald Hunt` involves finding the seven Chaos Emeralds and beating Green Hill
- `FinalHazard Chaos Emerald Hunt` is the same, but with the FinalHazard fight at the end of Green Hill
- New optional Location Checks
- Keysanity (Chao Containers)
- Whistlesanity (Animal Pipes and hidden whistle spots)
- Beetlesanity (Destroying Gold Beetles)
- Option to require clearing all active Cannon's Core Missions for access to the Biolizard fight in `Biolizard` goal
- Hard Logic option
- More Music Options
- Option to use SADX music
- New `Singularity` music shuffle option
- Option to choose the Narrator theme
- New Traps
- Tiny Trap is now permanent within a level
- Gravity Trap
- Exposition Trap
Quality of Life:
- Significant revamp to Stage Select screen information conveyance
- Icons are displayed for:
- Relevant character's upgrades
- Which location checks are active/checked
- Chaos Emeralds found (if relevant)
- Gate and Cannon's Core emblem costs
- The above stage-specific info can also be viewed when paused in-level
- The current mission is also displayed when paused
- Emblem Symbol on Mission Select subscreen now only displays if a high enough rank has been gotten on that mission to send the location check
- Hints including SA2B locations will now specify which Gate that level is located in
- Save file now stores slot name to help prevent false location checks in the case of one player having multiple SA2B slots in the same seed
- Chao Intermediate and Expert race sets are now swapped, per player feedback
- Intermediate now includes Beginner + Challenge + Hero + Dark
- Expert now includes Beginner + Challenge + Hero + Dark + Jewel
- New mod config option for the color of the Message Queue text
Bug Fixes:
- Fixed bug where game stops properly tracking items after 127 have been received.
- Several logic fixes
- Game now refers to `Knuckles - Shovel Claws` correctly
- Minor AP World code cleanup
This commit is contained in:
@@ -2,7 +2,7 @@ import typing
|
||||
import math
|
||||
|
||||
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
||||
from .Items import SA2BItem, ItemData, item_table, upgrades_table, junk_table, trap_table
|
||||
from .Items import SA2BItem, ItemData, item_table, upgrades_table, emeralds_table, junk_table, trap_table, item_groups
|
||||
from .Locations import SA2BLocation, all_locations, setup_locations
|
||||
from .Options import sa2b_options
|
||||
from .Regions import create_regions, shuffleable_regions, connect_regions, LevelGate, gate_0_whitelist_regions, \
|
||||
@@ -11,6 +11,7 @@ from .Rules import set_rules
|
||||
from .Names import ItemName, LocationName
|
||||
from ..AutoWorld import WebWorld, World
|
||||
from .GateBosses import get_gate_bosses, get_boss_name
|
||||
from .Missions import get_mission_table, get_mission_count_table, get_first_and_last_cannons_core_missions
|
||||
import Patch
|
||||
|
||||
|
||||
@@ -29,7 +30,7 @@ class SA2BWeb(WebWorld):
|
||||
tutorials = [setup_en]
|
||||
|
||||
|
||||
def check_for_impossible_shuffle(shuffled_levels: typing.List[int], gate_0_range: int, world: MultiWorld):
|
||||
def check_for_impossible_shuffle(shuffled_levels: typing.List[int], gate_0_range: int, multiworld: MultiWorld):
|
||||
blacklist_level_count = 0
|
||||
|
||||
for i in range(gate_0_range):
|
||||
@@ -37,7 +38,7 @@ def check_for_impossible_shuffle(shuffled_levels: typing.List[int], gate_0_range
|
||||
blacklist_level_count += 1
|
||||
|
||||
if blacklist_level_count == gate_0_range:
|
||||
index_to_swap = world.random.randint(0, gate_0_range)
|
||||
index_to_swap = multiworld.random.randint(0, gate_0_range)
|
||||
for i in range(len(shuffled_levels)):
|
||||
if shuffled_levels[i] in gate_0_whitelist_regions:
|
||||
shuffled_levels[i], shuffled_levels[index_to_swap] = shuffled_levels[index_to_swap], shuffled_levels[i]
|
||||
@@ -51,14 +52,17 @@ class SA2BWorld(World):
|
||||
game: str = "Sonic Adventure 2 Battle"
|
||||
option_definitions = sa2b_options
|
||||
topology_present = False
|
||||
data_version = 2
|
||||
data_version = 3
|
||||
|
||||
item_name_groups = item_groups
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = all_locations
|
||||
|
||||
location_table: typing.Dict[str, int]
|
||||
|
||||
music_map: typing.Dict[int, int]
|
||||
mission_map: typing.Dict[int, int]
|
||||
mission_count_map: typing.Dict[int, int]
|
||||
emblems_for_cannons_core: int
|
||||
region_emblem_map: typing.Dict[int, int]
|
||||
gate_costs: typing.Dict[int, int]
|
||||
@@ -67,15 +71,22 @@ class SA2BWorld(World):
|
||||
|
||||
def _get_slot_data(self):
|
||||
return {
|
||||
"ModVersion": 101,
|
||||
"ModVersion": 200,
|
||||
"Goal": self.multiworld.goal[self.player].value,
|
||||
"MusicMap": self.music_map,
|
||||
"MissionMap": self.mission_map,
|
||||
"MissionCountMap": self.mission_count_map,
|
||||
"MusicShuffle": self.multiworld.music_shuffle[self.player].value,
|
||||
"Narrator": self.multiworld.narrator[self.player].value,
|
||||
"RequiredRank": self.multiworld.required_rank[self.player].value,
|
||||
"ChaoKeys": self.multiworld.keysanity[self.player].value,
|
||||
"Whistlesanity": self.multiworld.whistlesanity[self.player].value,
|
||||
"GoldBeetles": self.multiworld.beetlesanity[self.player].value,
|
||||
"ChaoRaceChecks": self.multiworld.chao_race_checks[self.player].value,
|
||||
"ChaoGardenDifficulty": self.multiworld.chao_garden_difficulty[self.player].value,
|
||||
"DeathLink": self.multiworld.death_link[self.player].value,
|
||||
"IncludeMissions": self.multiworld.include_missions[self.player].value,
|
||||
"EmblemPercentageForCannonsCore": self.multiworld.emblem_percentage_for_cannons_core[self.player].value,
|
||||
"RequiredCannonsCoreMissions": self.multiworld.required_cannons_core_missions[self.player].value,
|
||||
"NumberOfLevelGates": self.multiworld.number_of_level_gates[self.player].value,
|
||||
"LevelGateDistribution": self.multiworld.level_gate_distribution[self.player].value,
|
||||
"EmblemsForCannonsCore": self.emblems_for_cannons_core,
|
||||
@@ -137,7 +148,10 @@ class SA2BWorld(World):
|
||||
self.gate_bosses = get_gate_bosses(self.multiworld, self.player)
|
||||
|
||||
def generate_basic(self):
|
||||
self.multiworld.get_location(LocationName.biolizard, self.player).place_locked_item(self.create_item(ItemName.maria))
|
||||
if self.multiworld.goal[self.player].value == 0 or self.multiworld.goal[self.player].value == 2:
|
||||
self.multiworld.get_location(LocationName.finalhazard, self.player).place_locked_item(self.create_item(ItemName.maria))
|
||||
elif self.multiworld.goal[self.player].value == 1:
|
||||
self.multiworld.get_location(LocationName.green_hill, self.player).place_locked_item(self.create_item(ItemName.maria))
|
||||
|
||||
itempool: typing.List[SA2BItem] = []
|
||||
|
||||
@@ -149,6 +163,11 @@ class SA2BWorld(World):
|
||||
for item in {**upgrades_table}:
|
||||
itempool += self._create_items(item)
|
||||
|
||||
if self.multiworld.goal[self.player].value == 1 or self.multiworld.goal[self.player].value == 2:
|
||||
# Some flavor of Chaos Emerald Hunt
|
||||
for item in {**emeralds_table}:
|
||||
itempool += self._create_items(item)
|
||||
|
||||
# Cap at 180 Emblems
|
||||
raw_emblem_count = total_required_locations - len(itempool)
|
||||
total_emblem_count = min(raw_emblem_count, 180)
|
||||
@@ -195,7 +214,9 @@ class SA2BWorld(World):
|
||||
|
||||
self.region_emblem_map = dict(zip(shuffled_region_list, emblem_requirement_list))
|
||||
|
||||
connect_regions(self.multiworld, self.player, gates, self.emblems_for_cannons_core, self.gate_bosses)
|
||||
first_cannons_core_mission, final_cannons_core_mission = get_first_and_last_cannons_core_missions(self.mission_map, self.mission_count_map)
|
||||
|
||||
connect_regions(self.multiworld, self.player, gates, self.emblems_for_cannons_core, self.gate_bosses, first_cannons_core_mission, final_cannons_core_mission)
|
||||
|
||||
max_required_emblems = max(max(emblem_requirement_list), self.emblems_for_cannons_core)
|
||||
itempool += [self.create_item(ItemName.emblem) for _ in range(max_required_emblems)]
|
||||
@@ -210,6 +231,9 @@ class SA2BWorld(World):
|
||||
trap_weights += ([ItemName.timestop_trap] * self.multiworld.timestop_trap_weight[self.player].value)
|
||||
trap_weights += ([ItemName.confuse_trap] * self.multiworld.confusion_trap_weight[self.player].value)
|
||||
trap_weights += ([ItemName.tiny_trap] * self.multiworld.tiny_trap_weight[self.player].value)
|
||||
trap_weights += ([ItemName.gravity_trap] * self.multiworld.gravity_trap_weight[self.player].value)
|
||||
trap_weights += ([ItemName.exposition_trap] * self.multiworld.exposition_trap_weight[self.player].value)
|
||||
#trap_weights += ([ItemName.darkness_trap] * self.multiworld.darkness_trap_weight[self.player].value)
|
||||
|
||||
junk_count += extra_junk_count
|
||||
trap_count = 0 if (len(trap_weights) == 0) else math.ceil(junk_count * (self.multiworld.trap_fill_percentage[self.player].value / 100.0))
|
||||
@@ -237,17 +261,59 @@ class SA2BWorld(World):
|
||||
musiclist_o = list(range(0, 47))
|
||||
musiclist_s = musiclist_o.copy()
|
||||
self.multiworld.random.shuffle(musiclist_s)
|
||||
musiclist_o.extend(range(47, 78))
|
||||
musiclist_s.extend(range(47, 78))
|
||||
|
||||
if self.multiworld.sadx_music[self.player].value == 1:
|
||||
musiclist_s = [x+100 for x in musiclist_s]
|
||||
elif self.multiworld.sadx_music[self.player].value == 2:
|
||||
for i in range(len(musiclist_s)):
|
||||
if self.multiworld.random.randint(0,1):
|
||||
musiclist_s[i] += 100
|
||||
|
||||
self.music_map = dict(zip(musiclist_o, musiclist_s))
|
||||
elif self.multiworld.music_shuffle[self.player] == "full":
|
||||
musiclist_o = list(range(0, 78))
|
||||
musiclist_s = musiclist_o.copy()
|
||||
self.multiworld.random.shuffle(musiclist_s)
|
||||
|
||||
if self.multiworld.sadx_music[self.player].value == 1:
|
||||
musiclist_s = [x+100 for x in musiclist_s]
|
||||
elif self.multiworld.sadx_music[self.player].value == 2:
|
||||
for i in range(len(musiclist_s)):
|
||||
if self.multiworld.random.randint(0,1):
|
||||
musiclist_s[i] += 100
|
||||
|
||||
self.music_map = dict(zip(musiclist_o, musiclist_s))
|
||||
elif self.multiworld.music_shuffle[self.player] == "singularity":
|
||||
musiclist_o = list(range(0, 78))
|
||||
musiclist_s = [self.multiworld.random.choice(musiclist_o)] * len(musiclist_o)
|
||||
|
||||
if self.multiworld.sadx_music[self.player].value == 1:
|
||||
musiclist_s = [x+100 for x in musiclist_s]
|
||||
elif self.multiworld.sadx_music[self.player].value == 2:
|
||||
if self.multiworld.random.randint(0,1):
|
||||
musiclist_s = [x+100 for x in musiclist_s]
|
||||
|
||||
self.music_map = dict(zip(musiclist_o, musiclist_s))
|
||||
else:
|
||||
self.music_map = dict()
|
||||
musiclist_o = list(range(0, 78))
|
||||
musiclist_s = musiclist_o.copy()
|
||||
|
||||
if self.multiworld.sadx_music[self.player].value == 1:
|
||||
musiclist_s = [x+100 for x in musiclist_s]
|
||||
elif self.multiworld.sadx_music[self.player].value == 2:
|
||||
for i in range(len(musiclist_s)):
|
||||
if self.multiworld.random.randint(0,1):
|
||||
musiclist_s[i] += 100
|
||||
|
||||
self.music_map = dict(zip(musiclist_o, musiclist_s))
|
||||
|
||||
def create_regions(self):
|
||||
self.location_table = setup_locations(self.multiworld, self.player)
|
||||
self.mission_map = get_mission_table(self.multiworld, self.player)
|
||||
self.mission_count_map = get_mission_count_table(self.multiworld, self.player)
|
||||
|
||||
self.location_table = setup_locations(self.multiworld, self.player, self.mission_map, self.mission_count_map)
|
||||
create_regions(self.multiworld, self.player, self.location_table)
|
||||
|
||||
def create_item(self, name: str, force_non_progression=False) -> Item:
|
||||
@@ -268,18 +334,52 @@ class SA2BWorld(World):
|
||||
|
||||
return created_item
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
self.multiworld.random.choice(junk_table.keys())
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self.multiworld, self.player, self.gate_bosses)
|
||||
set_rules(self.multiworld, self.player, self.gate_bosses, self.mission_map, self.mission_count_map)
|
||||
|
||||
def write_spoiler(self, spoiler_handle: typing.TextIO):
|
||||
spoiler_handle.write("\n")
|
||||
header_text = "Sonic Adventure 2 Bosses for {}:\n"
|
||||
header_text = header_text.format(self.multiworld.player_name[self.player])
|
||||
spoiler_handle.write(header_text)
|
||||
for x in range(len(self.gate_bosses.values())):
|
||||
text = "Gate {0} Boss: {1}\n"
|
||||
text = text.format((x + 1), get_boss_name(self.gate_bosses[x + 1]))
|
||||
spoiler_handle.writelines(text)
|
||||
if self.multiworld.number_of_level_gates[self.player].value > 0:
|
||||
spoiler_handle.write("\n")
|
||||
header_text = "Sonic Adventure 2 Bosses for {}:\n"
|
||||
header_text = header_text.format(self.multiworld.player_name[self.player])
|
||||
spoiler_handle.write(header_text)
|
||||
for x in range(len(self.gate_bosses.values())):
|
||||
text = "Gate {0} Boss: {1}\n"
|
||||
text = text.format((x + 1), get_boss_name(self.gate_bosses[x + 1]))
|
||||
spoiler_handle.writelines(text)
|
||||
|
||||
def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]):
|
||||
gate_names = [
|
||||
LocationName.gate_0_region,
|
||||
LocationName.gate_1_region,
|
||||
LocationName.gate_2_region,
|
||||
LocationName.gate_3_region,
|
||||
LocationName.gate_4_region,
|
||||
LocationName.gate_5_region,
|
||||
]
|
||||
no_hint_region_names = [
|
||||
LocationName.cannon_core_region,
|
||||
LocationName.chao_garden_beginner_region,
|
||||
LocationName.chao_garden_intermediate_region,
|
||||
LocationName.chao_garden_expert_region,
|
||||
]
|
||||
er_hint_data = {}
|
||||
for i in range(self.multiworld.number_of_level_gates[self.player].value + 1):
|
||||
gate_name = gate_names[i]
|
||||
gate_region = self.multiworld.get_region(gate_name, self.player)
|
||||
if not gate_region:
|
||||
continue
|
||||
for exit in gate_region.exits:
|
||||
if exit.connected_region.name in gate_names or exit.connected_region.name in no_hint_region_names:
|
||||
continue
|
||||
level_region = exit.connected_region
|
||||
for location in level_region.locations:
|
||||
er_hint_data[location.address] = gate_name
|
||||
|
||||
hint_data[self.player] = er_hint_data
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
|
||||
|
||||
Reference in New Issue
Block a user