Archipelago/worlds/soe/options.py

320 lines
8.6 KiB
Python

from dataclasses import dataclass, fields
from datetime import datetime
from typing import Any, ClassVar, cast, Dict, Iterator, List, Tuple, Protocol
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \
ProgressionBalancing, Range, Toggle
# typing boilerplate
class FlagsProtocol(Protocol):
value: int
default: ClassVar[int]
flags: List[str]
class FlagProtocol(Protocol):
value: int
default: ClassVar[int]
flag: str
# meta options
class EvermizerFlags:
flags: List[str]
def to_flag(self: FlagsProtocol) -> str:
return self.flags[self.value]
class EvermizerFlag:
flag: str
def to_flag(self: FlagProtocol) -> str:
return self.flag if self.value != self.default else ''
class OffOnFullChoice(Choice):
option_off = 0
option_on = 1
option_full = 2
alias_chaos = 2
class OffOnLogicChoice(Choice):
option_off = 0
option_on = 1
option_logic = 2
# actual options
class Difficulty(EvermizerFlags, Choice):
"""Changes relative spell cost and stuff"""
display_name = "Difficulty"
option_easy = 0
option_normal = 1
option_hard = 2
option_mystery = 3 # 'random' is reserved
alias_chaos = 3
default = 1
flags = ['e', 'n', 'h', 'x']
class EnergyCore(EvermizerFlags, Choice):
"""How to obtain the Energy Core"""
display_name = "Energy Core"
option_vanilla = 0
option_shuffle = 1
option_fragments = 2
default = 1
flags = ['z', '', 'Z']
class RequiredFragments(Range):
"""Required fragment count for Energy Core = Fragments"""
display_name = "Required Fragments"
range_start = 1
range_end = 99
default = 10
class AvailableFragments(Range):
"""Placed fragment count for Energy Core = Fragments"""
display_name = "Available Fragments"
range_start = 1
range_end = 99
default = 11
class MoneyModifier(Range):
"""Money multiplier in %"""
display_name = "Money Modifier"
range_start = 1
range_end = 2500
default = 200
class ExpModifier(Range):
"""EXP multiplier for Weapons, Characters and Spells in %"""
display_name = "Exp Modifier"
range_start = 1
range_end = 2500
default = 200
class SequenceBreaks(EvermizerFlags, OffOnLogicChoice):
"""Disable, enable some sequence breaks or put them in logic"""
display_name = "Sequence Breaks"
default = 0
flags = ['', 'j', 'J']
class OutOfBounds(EvermizerFlags, OffOnLogicChoice):
"""Disable, enable the out-of-bounds glitch or put it in logic"""
display_name = "Out Of Bounds"
default = 0
flags = ['', 'u', 'U']
class FixCheats(EvermizerFlag, DefaultOnToggle):
"""Fix cheats left in by the devs (not desert skip)"""
display_name = "Fix Cheats"
flag = '2'
class FixInfiniteAmmo(EvermizerFlag, Toggle):
"""Fix infinite ammo glitch"""
display_name = "Fix Infinite Ammo"
flag = '5'
class FixAtlasGlitch(EvermizerFlag, Toggle):
"""Fix atlas underflowing stats"""
display_name = "Fix Atlas Glitch"
flag = '6'
class FixWingsGlitch(EvermizerFlag, Toggle):
"""Fix wings making you invincible in some areas"""
display_name = "Fix Wings Glitch"
flag = '7'
class ShorterDialogs(EvermizerFlag, DefaultOnToggle):
"""Cuts some dialogs"""
display_name = "Shorter Dialogs"
flag = '9'
class ShortBossRush(EvermizerFlag, DefaultOnToggle):
"""Start boss rush at Metal Magmar, cut enemy HP in half"""
display_name = "Short Boss Rush"
flag = 'f'
class Ingredienizer(EvermizerFlags, OffOnFullChoice):
"""On Shuffles, Full randomizes spell ingredients"""
display_name = "Ingredienizer"
default = 1
flags = ['i', '', 'I']
class Sniffamizer(EvermizerFlags, Choice):
"""
Off: all vanilla items in sniff spots
Shuffle: sniff items shuffled into random sniff spots
"""
display_name = "Sniffamizer"
option_off = 0
option_shuffle = 1
if datetime.today().year > 2024 or datetime.today().month > 3:
option_everywhere = 2
__doc__ = __doc__ + " Everywhere: add sniff spots to multiworld pool"
alias_true = 1
default = 1
flags = ['s', '', 'S']
class SniffIngredients(EvermizerFlag, Choice):
"""Select which items should be used as sniff items"""
display_name = "Sniff Ingredients"
option_vanilla_ingredients = 0
option_random_ingredients = 1
flag = 'v'
class Callbeadamizer(EvermizerFlags, OffOnFullChoice):
"""On Shuffles call bead characters, Full shuffles individual spells"""
display_name = "Callbeadamizer"
default = 1
flags = ['c', '', 'C']
class Musicmizer(EvermizerFlag, Toggle):
"""Randomize music for some rooms"""
display_name = "Musicmizer"
flag = 'm'
class Doggomizer(EvermizerFlags, OffOnFullChoice):
"""On shuffles dog per act, Full randomizes dog per screen, Pupdunk gives you Everpupper everywhere"""
display_name = "Doggomizer"
option_pupdunk = 3
default = 0
flags = ['', 'd', 'D', 'p']
class TurdoMode(EvermizerFlag, Toggle):
"""Replace offensive spells by Turd Balls with varying strength and make weapons weak"""
display_name = "Turdo Mode"
flag = 't'
class TrapCount(Range):
"""Replace some filler items with traps"""
display_name = "Trap Count"
range_start = 0
range_end = 100
default = 0
# more meta options
class ItemChanceMeta(AssembleOptions):
def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[Any, Any]) -> "ItemChanceMeta":
if 'item_name' in attrs:
attrs["display_name"] = f"{attrs['item_name']} Chance"
attrs["range_start"] = 0
attrs["range_end"] = 100
cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs) # type: ignore[no-untyped-call]
return cast(ItemChanceMeta, cls)
class TrapChance(Range, metaclass=ItemChanceMeta):
item_name: str
default = 20
# more actual options
class TrapChanceQuake(TrapChance):
"""Sets the chance/ratio of quake traps"""
item_name = "Quake Trap"
class TrapChancePoison(TrapChance):
"""Sets the chance/ratio of poison effect traps"""
item_name = "Poison Trap"
class TrapChanceConfound(TrapChance):
"""Sets the chance/ratio of confound effect traps"""
item_name = "Confound Trap"
class TrapChanceHUD(TrapChance):
"""Sets the chance/ratio of HUD visibility traps"""
item_name = "HUD Trap"
class TrapChanceOHKO(TrapChance):
"""Sets the chance/ratio of OHKO (1HP left) traps"""
item_name = "OHKO Trap"
class SoEProgressionBalancing(ProgressionBalancing):
default = 30
__doc__ = ProgressionBalancing.__doc__.replace(f"default {ProgressionBalancing.default}", f"default {default}") \
if ProgressionBalancing.__doc__ else None
special_range_names = {**ProgressionBalancing.special_range_names, "normal": default}
# noinspection SpellCheckingInspection
@dataclass
class SoEOptions(PerGameCommonOptions):
difficulty: Difficulty
energy_core: EnergyCore
required_fragments: RequiredFragments
available_fragments: AvailableFragments
money_modifier: MoneyModifier
exp_modifier: ExpModifier
sequence_breaks: SequenceBreaks
out_of_bounds: OutOfBounds
fix_cheats: FixCheats
fix_infinite_ammo: FixInfiniteAmmo
fix_atlas_glitch: FixAtlasGlitch
fix_wings_glitch: FixWingsGlitch
shorter_dialogs: ShorterDialogs
short_boss_rush: ShortBossRush
ingredienizer: Ingredienizer
sniffamizer: Sniffamizer
sniff_ingredients: SniffIngredients
callbeadamizer: Callbeadamizer
musicmizer: Musicmizer
doggomizer: Doggomizer
turdo_mode: TurdoMode
death_link: DeathLink
trap_count: TrapCount
trap_chance_quake: TrapChanceQuake
trap_chance_poison: TrapChancePoison
trap_chance_confound: TrapChanceConfound
trap_chance_hud: TrapChanceHUD
trap_chance_ohko: TrapChanceOHKO
progression_balancing: SoEProgressionBalancing
@property
def trap_chances(self) -> Iterator[TrapChance]:
for field in fields(self):
option = getattr(self, field.name)
if isinstance(option, TrapChance):
yield option
@property
def flags(self) -> str:
flags = ''
for field in fields(self):
option = getattr(self, field.name)
if isinstance(option, (EvermizerFlag, EvermizerFlags)):
assert isinstance(option, Option)
# noinspection PyUnresolvedReferences
flags += option.to_flag()
return flags