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 .er_data import portal_mapping, RegionInfo, tunic_er_regions
|
||||
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 worlds.AutoWorld import WebWorld, World
|
||||
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
|
||||
|
||||
def generate_early(self) -> None:
|
||||
check_options(self)
|
||||
|
||||
if self.options.logic_rules >= LogicRules.option_no_major_glitches:
|
||||
self.options.laurels_zips.value = LaurelsZips.option_true
|
||||
self.options.ice_grappling.value = IceGrappling.option_medium
|
||||
@@ -144,6 +147,7 @@ class TunicWorld(World):
|
||||
self.options.lanternless.value = self.passthrough["lanternless"]
|
||||
self.options.maskless.value = self.passthrough["maskless"]
|
||||
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.shuffle_ladders.value = self.passthrough["shuffle_ladders"]
|
||||
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()}
|
||||
|
||||
# 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]:
|
||||
items_to_create["Fool Trap"] += items_to_create[money_fool]
|
||||
items_to_create[money_fool] = 0
|
||||
@@ -291,11 +299,21 @@ class TunicWorld(World):
|
||||
items_to_create["Grass"] -= len(excluded_grass_locations)
|
||||
|
||||
if self.options.keys_behind_bosses:
|
||||
for rgb_hexagon, location in hexagon_locations.items():
|
||||
hex_item = self.create_item(gold_hexagon if self.options.hexagon_quest else rgb_hexagon)
|
||||
self.get_location(location).place_locked_item(hex_item)
|
||||
items_to_create[rgb_hexagon] = 0
|
||||
items_to_create[gold_hexagon] -= 3
|
||||
rgb_hexagons = list(hexagon_locations.keys())
|
||||
# shuffle these in case not all are placed in hex quest
|
||||
self.random.shuffle(rgb_hexagons)
|
||||
for rgb_hexagon in rgb_hexagons:
|
||||
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
|
||||
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)
|
||||
|
||||
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
|
||||
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()
|
||||
items_to_create[filler_name] += items_to_create[replaced_item]
|
||||
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:
|
||||
self.tunic_portal_pairs = {}
|
||||
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
|
||||
if self.using_ut:
|
||||
@@ -504,7 +520,8 @@ class TunicWorld(World):
|
||||
return change
|
||||
|
||||
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")
|
||||
for ability in self.ability_unlocks:
|
||||
# Remove parentheses for better readability
|
||||
@@ -567,6 +584,7 @@ class TunicWorld(World):
|
||||
"sword_progression": self.options.sword_progression.value,
|
||||
"ability_shuffling": self.options.ability_shuffling.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,
|
||||
"laurels_zips": self.options.laurels_zips.value,
|
||||
"ice_grappling": self.options.ice_grappling.value,
|
||||
|
@@ -1,8 +1,14 @@
|
||||
import logging
|
||||
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,
|
||||
PerGameCommonOptions, OptionGroup, Visibility, NamedRange)
|
||||
from .er_data import portal_mapping
|
||||
if TYPE_CHECKING:
|
||||
from . import TunicWorld
|
||||
|
||||
|
||||
class SwordProgression(DefaultOnToggle):
|
||||
@@ -24,6 +30,7 @@ class StartWithSword(Toggle):
|
||||
class KeysBehindBosses(Toggle):
|
||||
"""
|
||||
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"
|
||||
display_name = "Keys Behind Bosses"
|
||||
@@ -32,7 +39,8 @@ class KeysBehindBosses(Toggle):
|
||||
class AbilityShuffling(DefaultOnToggle):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
internal_name = "ability_shuffling"
|
||||
@@ -84,14 +92,16 @@ class HexagonGoal(Range):
|
||||
"""
|
||||
internal_name = "hexagon_goal"
|
||||
display_name = "Gold Hexagons Required"
|
||||
range_start = 15
|
||||
range_end = 50
|
||||
range_start = 1
|
||||
range_end = 100
|
||||
default = 20
|
||||
|
||||
|
||||
class ExtraHexagonPercentage(Range):
|
||||
"""
|
||||
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"
|
||||
display_name = "Percentage of Extra Gold Hexagons"
|
||||
@@ -100,11 +110,27 @@ class ExtraHexagonPercentage(Range):
|
||||
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):
|
||||
"""
|
||||
Randomize the connections between scenes.
|
||||
A small, very lost fox on a big adventure.
|
||||
|
||||
|
||||
If you set this option's value to a string, it will be used as a custom seed.
|
||||
Every player who uses the same custom seed will have the same entrances, choosing the most restrictive settings among these players for the purpose of pairing entrances.
|
||||
"""
|
||||
@@ -301,6 +327,7 @@ class TunicOptions(PerGameCommonOptions):
|
||||
hexagon_quest: HexagonQuest
|
||||
hexagon_goal: HexagonGoal
|
||||
extra_hexagon_percentage: ExtraHexagonPercentage
|
||||
hexagon_quest_ability_type: HexagonQuestAbilityUnlockType
|
||||
|
||||
shuffle_ladders: ShuffleLadders
|
||||
grass_randomizer: GrassRandomizer
|
||||
@@ -323,6 +350,12 @@ class TunicOptions(PerGameCommonOptions):
|
||||
|
||||
|
||||
tunic_option_groups = [
|
||||
OptionGroup("Hexagon Quest Options", [
|
||||
HexagonQuest,
|
||||
HexagonGoal,
|
||||
ExtraHexagonPercentage,
|
||||
HexagonQuestAbilityUnlockType
|
||||
]),
|
||||
OptionGroup("Logic Options", [
|
||||
CombatLogic,
|
||||
Lanternless,
|
||||
@@ -357,3 +390,23 @@ tunic_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
"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 decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
from worlds.generic.Rules import set_rule, forbid_item, add_rule
|
||||
from BaseClasses import CollectionState
|
||||
from .options import TunicOptions, LadderStorage, IceGrappling
|
||||
from .options import LadderStorage, IceGrappling, HexagonQuestAbilityUnlockType
|
||||
if TYPE_CHECKING:
|
||||
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"]
|
||||
|
||||
|
||||
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]
|
||||
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
|
||||
# Set ability unlocks to 25, 50, and 75% of goal amount
|
||||
ability_requirement = [hexagon_goal // 4, hexagon_goal // 2, hexagon_goal * 3 // 4]
|
||||
abilities = [prayer, holy_cross, icebolt]
|
||||
random.shuffle(abilities)
|
||||
if any(req == 0 for req in ability_requirement):
|
||||
ability_requirement = [1, 2, 3]
|
||||
|
||||
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
|
||||
if not options.ability_shuffling:
|
||||
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(ability, world.player)
|
||||
|
||||
|
Reference in New Issue
Block a user