Skip to content

Commit

Permalink
Merge branch 'main' into tunc-upper-gh2
Browse files Browse the repository at this point in the history
  • Loading branch information
ScipioWright authored Dec 15, 2024
2 parents 53e6b0c + 728d249 commit 933c595
Show file tree
Hide file tree
Showing 15 changed files with 1,356 additions and 260 deletions.
3 changes: 2 additions & 1 deletion Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,8 @@ def handle_exception(exc_type, exc_value, exc_traceback):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logging.getLogger(exception_logger).exception("Uncaught exception",
exc_info=(exc_type, exc_value, exc_traceback))
exc_info=(exc_type, exc_value, exc_traceback),
extra={"NoStream": exception_logger is None})
return orig_hook(exc_type, exc_value, exc_traceback)

handle_exception._wrapped = True
Expand Down
14 changes: 13 additions & 1 deletion worlds/AutoWorld.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import time
from random import Random
from dataclasses import make_dataclass
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple,
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, Iterable, List, Mapping, Optional, Set, TextIO, Tuple,
TYPE_CHECKING, Type, Union)

from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
Expand Down Expand Up @@ -534,12 +534,24 @@ def create_filler(self) -> "Item":
def get_location(self, location_name: str) -> "Location":
return self.multiworld.get_location(location_name, self.player)

def get_locations(self) -> "Iterable[Location]":
return self.multiworld.get_locations(self.player)

def get_entrance(self, entrance_name: str) -> "Entrance":
return self.multiworld.get_entrance(entrance_name, self.player)

def get_entrances(self) -> "Iterable[Entrance]":
return self.multiworld.get_entrances(self.player)

def get_region(self, region_name: str) -> "Region":
return self.multiworld.get_region(region_name, self.player)

def get_regions(self) -> "Iterable[Region]":
return self.multiworld.get_regions(self.player)

def push_precollected(self, item: Item) -> None:
self.multiworld.push_precollected(item)

@property
def player_name(self) -> str:
return self.multiworld.get_player_name(self.player)
Expand Down
25 changes: 25 additions & 0 deletions worlds/hk/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ def get_costs(self, random_source: Random) -> typing.List[int]:
return charms


class CharmCost(Range):
range_end = 6


class PlandoCharmCosts(OptionDict):
"""Allows setting a Charm's Notch costs directly, mapping {name: cost}.
This is set after any random Charm Notch costs, if applicable."""
Expand All @@ -303,6 +307,27 @@ class PlandoCharmCosts(OptionDict):
Optional(name): And(int, lambda n: 6 >= n >= 0, error="Charm costs must be integers in the range 0-6.") for name in charm_names
})

def __init__(self, value):
# To handle keys of random like other options, create an option instance from their values
# Additionally a vanilla keyword is added to plando individual charms to vanilla costs
# and default is disabled so as to not cause confusion
self.value = {}
for key, data in value.items():
if isinstance(data, str):
if data.lower() == "vanilla" and key in self.valid_keys:
self.value[key] = vanilla_costs[charm_names.index(key)]
continue
elif data.lower() == "default":
# default is too easily confused with vanilla but actually 0
# skip CharmCost resolution to fail schema afterwords
self.value[key] = data
continue
try:
self.value[key] = CharmCost.from_any(data).value
except ValueError as ex:
# will fail schema afterwords
self.value[key] = data

def get_costs(self, charm_costs: typing.List[int]) -> typing.List[int]:
for name, cost in self.value.items():
charm_costs[charm_names.index(name)] = cost
Expand Down
10 changes: 8 additions & 2 deletions worlds/pokemon_emerald/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,13 +416,16 @@ def get_location(location: str):
)

# Dewford Town
entrance = get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH")
set_rule(
get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH"),
entrance,
lambda state:
state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player)
and state.has("EVENT_TALK_TO_MR_STONE", world.player)
and state.has("EVENT_DELIVER_LETTER", world.player)
)
world.multiworld.register_indirect_condition(
get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN").parent_region, entrance)
set_rule(
get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN"),
lambda state:
Expand Down Expand Up @@ -451,14 +454,17 @@ def get_location(location: str):
)

# Route 109
entrance = get_entrance("REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN")
set_rule(
get_entrance("REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN"),
entrance,
lambda state:
state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player)
and state.can_reach("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH", "Entrance", world.player)
and state.has("EVENT_TALK_TO_MR_STONE", world.player)
and state.has("EVENT_DELIVER_LETTER", world.player)
)
world.multiworld.register_indirect_condition(
get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN").parent_region, entrance)
set_rule(
get_entrance("REGION_ROUTE109/BEACH -> REGION_ROUTE109/SEA"),
hm_rules["HM03 Surf"]
Expand Down
45 changes: 37 additions & 8 deletions worlds/tunic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union
from logging import warning
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld
from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names,
combat_items)
from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations
from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon
from .er_rules import set_er_location_rules
Expand All @@ -10,6 +11,7 @@
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)
from .combat_logic import area_data, CombatState
from worlds.AutoWorld import WebWorld, World
from Options import PlandoConnection
from decimal import Decimal, ROUND_HALF_UP
Expand Down Expand Up @@ -127,11 +129,21 @@ def generate_early(self) -> None:
self.options.shuffle_ladders.value = passthrough["shuffle_ladders"]
self.options.fixed_shop.value = self.options.fixed_shop.option_false
self.options.laurels_location.value = self.options.laurels_location.option_anywhere
self.options.combat_logic.value = passthrough["combat_logic"]

@classmethod
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC")
for tunic in tunic_worlds:
# setting up state combat logic stuff, see has_combat_reqs for its use
# and this is magic so pycharm doesn't like it, unfortunately
if tunic.options.combat_logic:
multiworld.state.tunic_need_to_reset_combat_from_collect[tunic.player] = False
multiworld.state.tunic_need_to_reset_combat_from_remove[tunic.player] = False
multiworld.state.tunic_area_combat_state[tunic.player] = {}
for area_name in area_data.keys():
multiworld.state.tunic_area_combat_state[tunic.player][area_name] = CombatState.unchecked

# if it's one of the options, then it isn't a custom seed group
if tunic.options.entrance_rando.value in EntranceRando.options.values():
continue
Expand Down Expand Up @@ -190,10 +202,12 @@ def stage_generate_early(cls, multiworld: MultiWorld) -> None:

def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem:
item_data = item_table[name]
return TunicItem(name, classification or item_data.classification, self.item_name_to_id[name], self.player)
# if item_data.combat_ic is None, it'll take item_data.classification instead
itemclass: ItemClassification = ((item_data.combat_ic if self.options.combat_logic else None)
or item_data.classification)
return TunicItem(name, classification or itemclass, self.item_name_to_id[name], self.player)

def create_items(self) -> None:

tunic_items: List[TunicItem] = []
self.slot_data_items = []

Expand Down Expand Up @@ -322,15 +336,15 @@ def create_regions(self) -> None:
self.ability_unlocks["Pages 42-43 (Holy Cross)"] = passthrough["Hexagon Quest Holy Cross"]
self.ability_unlocks["Pages 52-53 (Icebolt)"] = passthrough["Hexagon Quest Icebolt"]

# ladder rando uses ER with vanilla connections, so that we're not managing more rules files
if self.options.entrance_rando or self.options.shuffle_ladders:
# Ladders and Combat Logic uses ER rules with vanilla connections for easier maintenance
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
portal_pairs = create_er_regions(self)
if self.options.entrance_rando:
# these get interpreted by the game to tell it which entrances to connect
for portal1, portal2 in portal_pairs.items():
self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination()
else:
# for non-ER, non-ladders
# uses the original rules, easier to navigate and reference
for region_name in tunic_regions:
region = Region(region_name, self.player, self.multiworld)
self.multiworld.regions.append(region)
Expand All @@ -351,7 +365,8 @@ def create_regions(self) -> None:
victory_region.locations.append(victory_location)

def set_rules(self) -> None:
if self.options.entrance_rando or self.options.shuffle_ladders:
# same reason as in create_regions, could probably be put into create_regions
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
set_er_location_rules(self)
else:
set_region_rules(self)
Expand All @@ -360,6 +375,19 @@ def set_rules(self) -> None:
def get_filler_item_name(self) -> str:
return self.random.choice(filler_items)

# cache whether you can get through combat logic areas
def collect(self, state: CollectionState, item: Item) -> bool:
change = super().collect(state, item)
if change and self.options.combat_logic and item.name in combat_items:
state.tunic_need_to_reset_combat_from_collect[self.player] = True
return change

def remove(self, state: CollectionState, item: Item) -> bool:
change = super().remove(state, item)
if change and self.options.combat_logic and item.name in combat_items:
state.tunic_need_to_reset_combat_from_remove[self.player] = True
return change

def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
if self.options.entrance_rando:
hint_data.update({self.player: {}})
Expand Down Expand Up @@ -426,6 +454,7 @@ def fill_slot_data(self) -> Dict[str, Any]:
"maskless": self.options.maskless.value,
"entrance_rando": int(bool(self.options.entrance_rando.value)),
"shuffle_ladders": self.options.shuffle_ladders.value,
"combat_logic": self.options.combat_logic.value,
"Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"],
"Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"],
"Hexagon Quest Icebolt": self.ability_unlocks["Pages 52-53 (Icebolt)"],
Expand Down
Loading

0 comments on commit 933c595

Please sign in to comment.