mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
TUNIC: Expanded hexagon quest options (#4076)
* More hex quest updates - Implement page ability shuffle for hex quest - Fix keys behind bosses if hex goal is less than 3 - Added check to fix conflicting hex quest options - Add option to slot data * Change option comparison * Change option checking and fix some stuff - also keep prayer first on low hex counts * Update option defaulting * Update option checking * Fix option assignment again * Show player name in option warning * Add new option to universal tracker stuff * Update __init__.py * Make helper method for getting total hexagons in itempool * Update options.py * Update option value passthrough * Change ability shuffle to default on * Check for hexagons option when writing spoiler
This commit is contained in:
@@ -11,7 +11,8 @@ from .er_scripts import create_er_regions
|
|||||||
from .grass import grass_location_table, grass_location_name_to_id, grass_location_name_groups, excluded_grass_locations
|
from .grass import grass_location_table, grass_location_name_to_id, grass_location_name_groups, excluded_grass_locations
|
||||||
from .er_data import portal_mapping, RegionInfo, tunic_er_regions
|
from .er_data import portal_mapping, RegionInfo, tunic_er_regions
|
||||||
from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections,
|
from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections,
|
||||||
LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage)
|
LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage, check_options,
|
||||||
|
get_hexagons_in_pool, HexagonQuestAbilityUnlockType)
|
||||||
from .combat_logic import area_data, CombatState
|
from .combat_logic import area_data, CombatState
|
||||||
from worlds.AutoWorld import WebWorld, World
|
from worlds.AutoWorld import WebWorld, World
|
||||||
from Options import PlandoConnection, OptionError
|
from Options import PlandoConnection, OptionError
|
||||||
@@ -109,6 +110,8 @@ class TunicWorld(World):
|
|||||||
ut_can_gen_without_yaml = True # class var that tells it to ignore the player yaml
|
ut_can_gen_without_yaml = True # class var that tells it to ignore the player yaml
|
||||||
|
|
||||||
def generate_early(self) -> None:
|
def generate_early(self) -> None:
|
||||||
|
check_options(self)
|
||||||
|
|
||||||
if self.options.logic_rules >= LogicRules.option_no_major_glitches:
|
if self.options.logic_rules >= LogicRules.option_no_major_glitches:
|
||||||
self.options.laurels_zips.value = LaurelsZips.option_true
|
self.options.laurels_zips.value = LaurelsZips.option_true
|
||||||
self.options.ice_grappling.value = IceGrappling.option_medium
|
self.options.ice_grappling.value = IceGrappling.option_medium
|
||||||
@@ -144,6 +147,7 @@ class TunicWorld(World):
|
|||||||
self.options.lanternless.value = self.passthrough["lanternless"]
|
self.options.lanternless.value = self.passthrough["lanternless"]
|
||||||
self.options.maskless.value = self.passthrough["maskless"]
|
self.options.maskless.value = self.passthrough["maskless"]
|
||||||
self.options.hexagon_quest.value = self.passthrough["hexagon_quest"]
|
self.options.hexagon_quest.value = self.passthrough["hexagon_quest"]
|
||||||
|
self.options.hexagon_quest_ability_type.value = self.passthrough.get("hexagon_quest_ability_type", 0)
|
||||||
self.options.entrance_rando.value = self.passthrough["entrance_rando"]
|
self.options.entrance_rando.value = self.passthrough["entrance_rando"]
|
||||||
self.options.shuffle_ladders.value = self.passthrough["shuffle_ladders"]
|
self.options.shuffle_ladders.value = self.passthrough["shuffle_ladders"]
|
||||||
self.options.grass_randomizer.value = self.passthrough.get("grass_randomizer", 0)
|
self.options.grass_randomizer.value = self.passthrough.get("grass_randomizer", 0)
|
||||||
@@ -261,6 +265,10 @@ class TunicWorld(World):
|
|||||||
|
|
||||||
items_to_create: Dict[str, int] = {item: data.quantity_in_item_pool for item, data in item_table.items()}
|
items_to_create: Dict[str, int] = {item: data.quantity_in_item_pool for item, data in item_table.items()}
|
||||||
|
|
||||||
|
# Calculate number of hexagons in item pool
|
||||||
|
if self.options.hexagon_quest:
|
||||||
|
items_to_create[gold_hexagon] = get_hexagons_in_pool(self)
|
||||||
|
|
||||||
for money_fool in fool_tiers[self.options.fool_traps]:
|
for money_fool in fool_tiers[self.options.fool_traps]:
|
||||||
items_to_create["Fool Trap"] += items_to_create[money_fool]
|
items_to_create["Fool Trap"] += items_to_create[money_fool]
|
||||||
items_to_create[money_fool] = 0
|
items_to_create[money_fool] = 0
|
||||||
@@ -291,11 +299,21 @@ class TunicWorld(World):
|
|||||||
items_to_create["Grass"] -= len(excluded_grass_locations)
|
items_to_create["Grass"] -= len(excluded_grass_locations)
|
||||||
|
|
||||||
if self.options.keys_behind_bosses:
|
if self.options.keys_behind_bosses:
|
||||||
for rgb_hexagon, location in hexagon_locations.items():
|
rgb_hexagons = list(hexagon_locations.keys())
|
||||||
hex_item = self.create_item(gold_hexagon if self.options.hexagon_quest else rgb_hexagon)
|
# shuffle these in case not all are placed in hex quest
|
||||||
self.get_location(location).place_locked_item(hex_item)
|
self.random.shuffle(rgb_hexagons)
|
||||||
items_to_create[rgb_hexagon] = 0
|
for rgb_hexagon in rgb_hexagons:
|
||||||
items_to_create[gold_hexagon] -= 3
|
location = hexagon_locations[rgb_hexagon]
|
||||||
|
if self.options.hexagon_quest:
|
||||||
|
if items_to_create[gold_hexagon] > 0:
|
||||||
|
hex_item = self.create_item(gold_hexagon)
|
||||||
|
items_to_create[gold_hexagon] -= 1
|
||||||
|
items_to_create[rgb_hexagon] = 0
|
||||||
|
self.get_location(location).place_locked_item(hex_item)
|
||||||
|
else:
|
||||||
|
hex_item = self.create_item(rgb_hexagon)
|
||||||
|
self.get_location(location).place_locked_item(hex_item)
|
||||||
|
items_to_create[rgb_hexagon] = 0
|
||||||
|
|
||||||
# Filler items in the item pool
|
# Filler items in the item pool
|
||||||
available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and
|
available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and
|
||||||
@@ -323,13 +341,11 @@ class TunicWorld(World):
|
|||||||
remove_filler(ladder_count)
|
remove_filler(ladder_count)
|
||||||
|
|
||||||
if self.options.hexagon_quest:
|
if self.options.hexagon_quest:
|
||||||
# Calculate number of hexagons in item pool
|
|
||||||
hexagon_goal = self.options.hexagon_goal
|
|
||||||
extra_hexagons = self.options.extra_hexagon_percentage
|
|
||||||
items_to_create[gold_hexagon] += int((Decimal(100 + extra_hexagons) / 100 * hexagon_goal).to_integral_value(rounding=ROUND_HALF_UP))
|
|
||||||
|
|
||||||
# Replace pages and normal hexagons with filler
|
# Replace pages and normal hexagons with filler
|
||||||
for replaced_item in list(filter(lambda item: "Pages" in item or item in hexagon_locations, items_to_create)):
|
for replaced_item in list(filter(lambda item: "Pages" in item or item in hexagon_locations, items_to_create)):
|
||||||
|
if replaced_item in item_name_groups["Abilities"] and self.options.ability_shuffling \
|
||||||
|
and self.options.hexagon_quest_ability_type == "pages":
|
||||||
|
continue
|
||||||
filler_name = self.get_filler_item_name()
|
filler_name = self.get_filler_item_name()
|
||||||
items_to_create[filler_name] += items_to_create[replaced_item]
|
items_to_create[filler_name] += items_to_create[replaced_item]
|
||||||
if items_to_create[filler_name] >= 1 and filler_name not in available_filler:
|
if items_to_create[filler_name] >= 1 and filler_name not in available_filler:
|
||||||
@@ -441,7 +457,7 @@ class TunicWorld(World):
|
|||||||
def create_regions(self) -> None:
|
def create_regions(self) -> None:
|
||||||
self.tunic_portal_pairs = {}
|
self.tunic_portal_pairs = {}
|
||||||
self.er_portal_hints = {}
|
self.er_portal_hints = {}
|
||||||
self.ability_unlocks = randomize_ability_unlocks(self.random, self.options)
|
self.ability_unlocks = randomize_ability_unlocks(self)
|
||||||
|
|
||||||
# stuff for universal tracker support, can be ignored for standard gen
|
# stuff for universal tracker support, can be ignored for standard gen
|
||||||
if self.using_ut:
|
if self.using_ut:
|
||||||
@@ -504,7 +520,8 @@ class TunicWorld(World):
|
|||||||
return change
|
return change
|
||||||
|
|
||||||
def write_spoiler_header(self, spoiler_handle: TextIO):
|
def write_spoiler_header(self, spoiler_handle: TextIO):
|
||||||
if self.options.hexagon_quest and self.options.ability_shuffling:
|
if self.options.hexagon_quest and self.options.ability_shuffling\
|
||||||
|
and self.options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons:
|
||||||
spoiler_handle.write("\nAbility Unlocks (Hexagon Quest):\n")
|
spoiler_handle.write("\nAbility Unlocks (Hexagon Quest):\n")
|
||||||
for ability in self.ability_unlocks:
|
for ability in self.ability_unlocks:
|
||||||
# Remove parentheses for better readability
|
# Remove parentheses for better readability
|
||||||
@@ -567,6 +584,7 @@ class TunicWorld(World):
|
|||||||
"sword_progression": self.options.sword_progression.value,
|
"sword_progression": self.options.sword_progression.value,
|
||||||
"ability_shuffling": self.options.ability_shuffling.value,
|
"ability_shuffling": self.options.ability_shuffling.value,
|
||||||
"hexagon_quest": self.options.hexagon_quest.value,
|
"hexagon_quest": self.options.hexagon_quest.value,
|
||||||
|
"hexagon_quest_ability_type": self.options.hexagon_quest_ability_type.value,
|
||||||
"fool_traps": self.options.fool_traps.value,
|
"fool_traps": self.options.fool_traps.value,
|
||||||
"laurels_zips": self.options.laurels_zips.value,
|
"laurels_zips": self.options.laurels_zips.value,
|
||||||
"ice_grappling": self.options.ice_grappling.value,
|
"ice_grappling": self.options.ice_grappling.value,
|
||||||
|
@@ -1,8 +1,14 @@
|
|||||||
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any, TYPE_CHECKING
|
||||||
|
|
||||||
|
from decimal import Decimal, ROUND_HALF_UP
|
||||||
|
|
||||||
from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections,
|
from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections,
|
||||||
PerGameCommonOptions, OptionGroup, Visibility, NamedRange)
|
PerGameCommonOptions, OptionGroup, Visibility, NamedRange)
|
||||||
from .er_data import portal_mapping
|
from .er_data import portal_mapping
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import TunicWorld
|
||||||
|
|
||||||
|
|
||||||
class SwordProgression(DefaultOnToggle):
|
class SwordProgression(DefaultOnToggle):
|
||||||
@@ -24,6 +30,7 @@ class StartWithSword(Toggle):
|
|||||||
class KeysBehindBosses(Toggle):
|
class KeysBehindBosses(Toggle):
|
||||||
"""
|
"""
|
||||||
Places the three hexagon keys behind their respective boss fight in your world.
|
Places the three hexagon keys behind their respective boss fight in your world.
|
||||||
|
If playing Hexagon Quest, it will place three gold hexagons at the boss locations.
|
||||||
"""
|
"""
|
||||||
internal_name = "keys_behind_bosses"
|
internal_name = "keys_behind_bosses"
|
||||||
display_name = "Keys Behind Bosses"
|
display_name = "Keys Behind Bosses"
|
||||||
@@ -32,7 +39,8 @@ class KeysBehindBosses(Toggle):
|
|||||||
class AbilityShuffling(DefaultOnToggle):
|
class AbilityShuffling(DefaultOnToggle):
|
||||||
"""
|
"""
|
||||||
Locks the usage of Prayer, Holy Cross*, and the Icebolt combo until the relevant pages of the manual have been found.
|
Locks the usage of Prayer, Holy Cross*, and the Icebolt combo until the relevant pages of the manual have been found.
|
||||||
If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required Hexagon goal amount.
|
If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required
|
||||||
|
Hexagon goal amount, unless the option is set to have them unlock via pages instead.
|
||||||
* Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other player-facing codes.
|
* Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other player-facing codes.
|
||||||
"""
|
"""
|
||||||
internal_name = "ability_shuffling"
|
internal_name = "ability_shuffling"
|
||||||
@@ -84,14 +92,16 @@ class HexagonGoal(Range):
|
|||||||
"""
|
"""
|
||||||
internal_name = "hexagon_goal"
|
internal_name = "hexagon_goal"
|
||||||
display_name = "Gold Hexagons Required"
|
display_name = "Gold Hexagons Required"
|
||||||
range_start = 15
|
range_start = 1
|
||||||
range_end = 50
|
range_end = 100
|
||||||
default = 20
|
default = 20
|
||||||
|
|
||||||
|
|
||||||
class ExtraHexagonPercentage(Range):
|
class ExtraHexagonPercentage(Range):
|
||||||
"""
|
"""
|
||||||
How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount.
|
How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount.
|
||||||
|
The max number of Gold Questagons that can be in the item pool is 100, so this option may be overridden and/or
|
||||||
|
reduced if the Hexagon Goal amount is greater than 50.
|
||||||
"""
|
"""
|
||||||
internal_name = "extra_hexagon_percentage"
|
internal_name = "extra_hexagon_percentage"
|
||||||
display_name = "Percentage of Extra Gold Hexagons"
|
display_name = "Percentage of Extra Gold Hexagons"
|
||||||
@@ -100,6 +110,22 @@ class ExtraHexagonPercentage(Range):
|
|||||||
default = 50
|
default = 50
|
||||||
|
|
||||||
|
|
||||||
|
class HexagonQuestAbilityUnlockType(Choice):
|
||||||
|
"""
|
||||||
|
Determines how abilities are unlocked when playing Hexagon Quest with Shuffled Abilities enabled.
|
||||||
|
|
||||||
|
Hexagons: A new ability is randomly unlocked after obtaining 25%, 50%, and 75% of the required Hexagon goal amount. Requires at least 3 Gold Hexagons in the item pool, or 15 if Keys Behind Bosses is enabled.
|
||||||
|
Pages: Abilities are unlocked by finding specific pages in the manual.
|
||||||
|
|
||||||
|
This option does nothing if Shuffled Abilities is not enabled.
|
||||||
|
"""
|
||||||
|
internal_name = "hexagon_quest_ability_type"
|
||||||
|
display_name = "Hexagon Quest Ability Unlocks"
|
||||||
|
option_hexagons = 0
|
||||||
|
option_pages = 1
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
class EntranceRando(TextChoice):
|
class EntranceRando(TextChoice):
|
||||||
"""
|
"""
|
||||||
Randomize the connections between scenes.
|
Randomize the connections between scenes.
|
||||||
@@ -301,6 +327,7 @@ class TunicOptions(PerGameCommonOptions):
|
|||||||
hexagon_quest: HexagonQuest
|
hexagon_quest: HexagonQuest
|
||||||
hexagon_goal: HexagonGoal
|
hexagon_goal: HexagonGoal
|
||||||
extra_hexagon_percentage: ExtraHexagonPercentage
|
extra_hexagon_percentage: ExtraHexagonPercentage
|
||||||
|
hexagon_quest_ability_type: HexagonQuestAbilityUnlockType
|
||||||
|
|
||||||
shuffle_ladders: ShuffleLadders
|
shuffle_ladders: ShuffleLadders
|
||||||
grass_randomizer: GrassRandomizer
|
grass_randomizer: GrassRandomizer
|
||||||
@@ -323,6 +350,12 @@ class TunicOptions(PerGameCommonOptions):
|
|||||||
|
|
||||||
|
|
||||||
tunic_option_groups = [
|
tunic_option_groups = [
|
||||||
|
OptionGroup("Hexagon Quest Options", [
|
||||||
|
HexagonQuest,
|
||||||
|
HexagonGoal,
|
||||||
|
ExtraHexagonPercentage,
|
||||||
|
HexagonQuestAbilityUnlockType
|
||||||
|
]),
|
||||||
OptionGroup("Logic Options", [
|
OptionGroup("Logic Options", [
|
||||||
CombatLogic,
|
CombatLogic,
|
||||||
Lanternless,
|
Lanternless,
|
||||||
@@ -357,3 +390,23 @@ tunic_option_presets: Dict[str, Dict[str, Any]] = {
|
|||||||
"lanternless": True,
|
"lanternless": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_options(world: "TunicWorld"):
|
||||||
|
options = world.options
|
||||||
|
if options.hexagon_quest and options.ability_shuffling and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons:
|
||||||
|
total_hexes = get_hexagons_in_pool(world)
|
||||||
|
min_hexes = 3
|
||||||
|
|
||||||
|
if options.keys_behind_bosses:
|
||||||
|
min_hexes = 15
|
||||||
|
if total_hexes < min_hexes:
|
||||||
|
logging.warning(f"TUNIC: Not enough Gold Hexagons in {world.player_name}'s item pool for Hexagon Ability Shuffle with the selected options. Ability Shuffle mode will be switched to Pages.")
|
||||||
|
options.hexagon_quest_ability_type.value = HexagonQuestAbilityUnlockType.option_pages
|
||||||
|
|
||||||
|
|
||||||
|
def get_hexagons_in_pool(world: "TunicWorld"):
|
||||||
|
# Calculate number of hexagons in item pool
|
||||||
|
options = world.options
|
||||||
|
return min(int((Decimal(100 + options.extra_hexagon_percentage) / 100 * options.hexagon_goal)
|
||||||
|
.to_integral_value(rounding=ROUND_HALF_UP)), 100)
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
from random import Random
|
|
||||||
from typing import Dict, TYPE_CHECKING
|
from typing import Dict, TYPE_CHECKING
|
||||||
|
from decimal import Decimal, ROUND_HALF_UP
|
||||||
|
|
||||||
from worlds.generic.Rules import set_rule, forbid_item, add_rule
|
from worlds.generic.Rules import set_rule, forbid_item, add_rule
|
||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState
|
||||||
from .options import TunicOptions, LadderStorage, IceGrappling
|
from .options import LadderStorage, IceGrappling, HexagonQuestAbilityUnlockType
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import TunicWorld
|
from . import TunicWorld
|
||||||
|
|
||||||
@@ -34,14 +34,21 @@ bomb_walls = ["East Forest - Bombable Wall", "Eastern Vault Fortress - [East Win
|
|||||||
"Quarry - [West] Upper Area Bombable Wall", "Ruined Atoll - [Northwest] Bombable Wall"]
|
"Quarry - [West] Upper Area Bombable Wall", "Ruined Atoll - [Northwest] Bombable Wall"]
|
||||||
|
|
||||||
|
|
||||||
def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str, int]:
|
def randomize_ability_unlocks(world: "TunicWorld") -> Dict[str, int]:
|
||||||
|
random = world.random
|
||||||
|
options = world.options
|
||||||
|
|
||||||
|
abilities = [prayer, holy_cross, icebolt]
|
||||||
ability_requirement = [1, 1, 1]
|
ability_requirement = [1, 1, 1]
|
||||||
if options.hexagon_quest.value:
|
random.shuffle(abilities)
|
||||||
|
|
||||||
|
if options.hexagon_quest.value and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons:
|
||||||
hexagon_goal = options.hexagon_goal.value
|
hexagon_goal = options.hexagon_goal.value
|
||||||
# Set ability unlocks to 25, 50, and 75% of goal amount
|
# Set ability unlocks to 25, 50, and 75% of goal amount
|
||||||
ability_requirement = [hexagon_goal // 4, hexagon_goal // 2, hexagon_goal * 3 // 4]
|
ability_requirement = [hexagon_goal // 4, hexagon_goal // 2, hexagon_goal * 3 // 4]
|
||||||
abilities = [prayer, holy_cross, icebolt]
|
if any(req == 0 for req in ability_requirement):
|
||||||
random.shuffle(abilities)
|
ability_requirement = [1, 2, 3]
|
||||||
|
|
||||||
return dict(zip(abilities, ability_requirement))
|
return dict(zip(abilities, ability_requirement))
|
||||||
|
|
||||||
|
|
||||||
@@ -50,7 +57,7 @@ def has_ability(ability: str, state: CollectionState, world: "TunicWorld") -> bo
|
|||||||
ability_unlocks = world.ability_unlocks
|
ability_unlocks = world.ability_unlocks
|
||||||
if not options.ability_shuffling:
|
if not options.ability_shuffling:
|
||||||
return True
|
return True
|
||||||
if options.hexagon_quest:
|
if options.hexagon_quest and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons:
|
||||||
return state.has(gold_hexagon, world.player, ability_unlocks[ability])
|
return state.has(gold_hexagon, world.player, ability_unlocks[ability])
|
||||||
return state.has(ability, world.player)
|
return state.has(ability, world.player)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user