Files
Grinch-AP/worlds/terraria/__init__.py
Seldom 1873c52aa6 Terraria: 1.4.4 and Calamity support (#3847)
* Terraria integration

* Precollected items for debugging

* Fix item classification

* Golem requires Plantera's Bulb

* Pumpkin Moon requires Dungeon

* Progressive Dungeon

* Reorg, Options.py work

* Items are boss flags

* Removed unused option

* Removed nothing

* Wall, Plantera, and Zenith goals

* Achievements and items

* Fixed The Cavalry and Completely Awesome achievements

* Made "Dead Men Tell No Tales" a grindy achievement

* Some docs, Python 3.8 compat

* docs

* Fix extra item and "Head in the Clouds" being included when achievements are disabled

* Requested changes

* Fix potential thread unsafety, replace Nothing with 50 Silver

* Remove a log

* Corrected heading

* Added incompatible mods list

* In-progress calamity integration

* Terraria events progress

* Rules use events

* Removed an intentional crash I accidentally left in

* Fixed infinite loop

* Moved rules to data file

* Moved item rewards to data file

* Generating from data file

* Fixed broken Mech Boss goal

* Changes Calamity makes to vanilla rules, Calamity final bosses goal

* Added Deerclops, fixed Zenith goal

* Final detailed vanilla pass

* Disable calamity goals

* Typo

* Fixed some reward items not adding to item pool

* In-progress unit test fixes

* Unit test fixes

* `.apworld` compat

* Organized rewards file, made Frog Leg and Fllpper available in vanilla

* Water Walking Boots and Titan Glove rewards

* Add goals to slot data

* Fixed Hammush logic in Post-Mech goal

* Fixed coin rewards

* Updated Terraria docs

* Formatted

* Deathlink in-progress

* Boots of the Hero is grindy

* Fixed zenith goal not placing an item

* Address review

* Gelatin World Tour is grindy

* Difficulty notice

* Switched some achievements' grindiness

* Added "Hey! Listen!" achievement

* Terarria Python 3.8 compat

* Fixed Terraria You and What Army logic

* Calamity minion accessories

* Typo

* Calamity integration

* `deathlink` -> `death_link`

Co-authored-by: Zach Parks <zach@alliware.com>

* Missing `.`

Co-authored-by: Zach Parks <zach@alliware.com>

* Incorrect type annotation

Co-authored-by: Zach Parks <zach@alliware.com>

* `deathlink` -> `death_link` 2

Co-authored-by: Zach Parks <zach@alliware.com>

* Style

Co-authored-by: Zach Parks <zach@alliware.com>

* Markdown style

Co-authored-by: Zach Parks <zach@alliware.com>

* Markdown style 2

Co-authored-by: Zach Parks <zach@alliware.com>

* Address review

* Fix bad merge

* Terraria utility mod recommendations

* Calamity minion armor logic

* ArmorMinions -> Armor Minions, boss rush goal, fixed unplaced item

* Fixed unplaced item

* Started on Terraria 1.4.4

* Crate logic

* getfixedboi, 1.4.4 achievements, shimmer, town slimes, `Rule`, `Condition`, etc

* More clam getfixedboi logic, bar decraft logic, `NotGetfixedboi` -> `Not Getfixedboi`

* Calamity fixes

* Calamity crate ore logic

* Fixed item accessibility not generating in getfixedboi, fixed not generating with incompatible options, fixed grindy function

* Early achievements, separate achievement category options

* Infinity +1 Sword achievement can be location in later goals

* The Frequent Flyer is impossible in Calamity getfixedboi

* Add Enchanted Sword and Starfury for starting inventories

* Don't Dread on Me is redundant in Calamity

* In Calamity getfixedboi, Queen Bee summons enemies who drop Plague Cell Canisters

* Can't use Gelatin Crystal outside Hallow

* You can't get the Terminus without flags

* Typo

* Options difficult warnings

* Robbing the Grave is Hardmode

* Don't reserve an ID for unused Victory item

* Plantera is accessible early in Calamity via Giant Plantera's Bulbs

* Unshuffled Life Crystal and Defender Medal items

* Comment about Midas' Blessing

* Update worlds/terraria/Options.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Remove stray expression

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Review suggestions

* Option naming caps consistency, add Laser Drill, Lunatic Cultist alt reqs, fix Eldritch Soul Artifact, Ceaseless Void reqs Dungeon

* Cal Clone doesn't drop Broken Hero Sword anymore, Laser Drill is weaker in Calamity

Co-authored-by: Seatori <92278897+Seatori@users.noreply.github.com>

* Fix Acid Rain logic

* Fix XB-∞ Hekate failing accessibility checks (by commenting it out bc it doesn't affect logic)

* Hardmode ores being fishable early in Calamity is not a bug anymore

* Mecha Mayhem is inaccessible in getfixedboi

* Update worlds/terraria/Rules.dsv

Co-authored-by: Seafo <92278897+Seatori@users.noreply.github.com>

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Zach Parks <zach@alliware.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Seatori <92278897+Seatori@users.noreply.github.com>
2025-04-15 15:51:05 +02:00

375 lines
13 KiB
Python

# Look at `Rules.dsv` first to get an idea for how this works
import logging
from typing import Union, Tuple, List, Dict, Set
from worlds.AutoWorld import WebWorld, World
from BaseClasses import Region, ItemClassification, Tutorial, CollectionState
from .Checks import (
TerrariaItem,
TerrariaLocation,
Condition,
goals,
rules,
rule_indices,
labels,
rewards,
item_name_to_id,
location_name_to_id,
COND_ITEM,
COND_LOC,
COND_FN,
COND_GROUP,
npcs,
pickaxes,
hammers,
mech_bosses,
progression,
armor_minions,
accessory_minions,
)
from .Options import TerrariaOptions, Goal
class TerrariaWeb(WebWorld):
tutorials = [
Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Terraria randomizer connected to an Archipelago Multiworld.",
"English",
"setup_en.md",
"setup/en",
["Seldom"],
)
]
class TerrariaWorld(World):
"""
Terraria is a 2D multiplayer sandbox game featuring mining, building, exploration, and combat.
Features 18 bosses and 4 classes.
"""
game = "Terraria"
web = TerrariaWeb()
options_dataclass = TerrariaOptions
options: TerrariaOptions
item_name_to_id = item_name_to_id
location_name_to_id = location_name_to_id
calamity = False
getfixedboi = False
ter_items: List[str]
ter_locations: List[str]
ter_goals: Dict[str, str]
goal_items: Set[str]
goal_locations: Set[str]
def generate_early(self) -> None:
goal, goal_locations = goals[self.options.goal.value]
ter_goals = {}
goal_items = set()
for location in goal_locations:
flags = rules[rule_indices[location]].flags
if not self.options.calamity.value and "Calamity" in flags:
logging.warning(
f"Terraria goal `{Goal.name_lookup[self.options.goal.value]}`, which requires Calamity, was selected with Calamity disabled; enabling Calamity"
)
self.options.calamity.value = True
item = flags.get("Item") or f"Post-{location}"
ter_goals[item] = location
goal_items.add(item)
location_count = 0
locations = []
item_count = 0
items = []
for rule in rules[:goal]:
early = "Early" in rule.flags
grindy = "Grindy" in rule.flags
fishing = "Fishing" in rule.flags
if (
(not self.options.getfixedboi.value and "Getfixedboi" in rule.flags)
or (self.options.getfixedboi.value and "Not Getfixedboi" in rule.flags)
or (not self.options.calamity.value and "Calamity" in rule.flags)
or (self.options.calamity.value and "Not Calamity" in rule.flags)
or (
self.options.getfixedboi.value
and self.options.calamity.value
and "Not Calamity Getfixedboi" in rule.flags
)
or (not self.options.early_achievements.value and early)
or (
not self.options.normal_achievements.value
and "Achievement" in rule.flags
and not early
and not grindy
and not fishing
)
or (not self.options.grindy_achievements.value and grindy)
or (not self.options.fishing_achievements.value and fishing)
) and rule.name not in goal_locations:
continue
if "Location" in rule.flags or "Achievement" in rule.flags:
# Location
location_count += 1
locations.append(rule.name)
elif (
"Achievement" not in rule.flags
and "Location" not in rule.flags
and "Item" not in rule.flags
):
# Event
locations.append(rule.name)
if "Item" in rule.flags and not (
"Achievement" in rule.flags and rule.name not in goal_locations
):
# Item
item_count += 1
if rule.name not in goal_locations:
items.append(rule.name)
elif (
"Achievement" not in rule.flags
and "Location" not in rule.flags
and "Item" not in rule.flags
):
# Event
items.append(rule.name)
ordered_rewards = [
reward
for reward in labels["ordered"]
if self.options.calamity.value or "Calamity" not in rewards[reward]
]
while (
self.options.fill_extra_checks_with.value == 1
and item_count < location_count
and ordered_rewards
):
items.append(ordered_rewards.pop(0))
item_count += 1
random_rewards = [
reward
for reward in labels["random"]
if self.options.calamity.value or "Calamity" not in rewards[reward]
]
self.multiworld.random.shuffle(random_rewards)
while (
self.options.fill_extra_checks_with.value == 1
and item_count < location_count
and random_rewards
):
items.append(random_rewards.pop(0))
item_count += 1
while item_count < location_count:
items.append("Reward: Coins")
item_count += 1
self.ter_items = items
self.ter_locations = locations
self.ter_goals = ter_goals
self.goal_items = goal_items
self.goal_locations = goal_locations
def create_regions(self) -> None:
menu = Region("Menu", self.player, self.multiworld)
for location in self.ter_locations:
menu.locations.append(
TerrariaLocation(
self.player, location, location_name_to_id.get(location), menu
)
)
self.multiworld.regions.append(menu)
def create_item(self, item: str) -> TerrariaItem:
if item in progression:
classification = ItemClassification.progression
else:
classification = ItemClassification.filler
return TerrariaItem(item, classification, item_name_to_id[item], self.player)
def create_items(self) -> None:
for item in self.ter_items:
if (rule_index := rule_indices.get(item)) is not None:
rule = rules[rule_index]
if "Item" in rule.flags:
name = rule.flags.get("Item") or f"Post-{item}"
else:
continue
else:
name = item
self.multiworld.itempool.append(self.create_item(name))
locked_items = {}
for location in self.ter_locations:
rule = rules[rule_indices[location]]
if "Location" not in rule.flags and "Achievement" not in rule.flags:
if location in progression:
classification = ItemClassification.progression
else:
classification = ItemClassification.useful
locked_items[location] = TerrariaItem(
location, classification, None, self.player
)
for item, location in self.ter_goals.items():
locked_items[location] = self.create_item(item)
for location, item in locked_items.items():
self.multiworld.get_location(location, self.player).place_locked_item(item)
def check_condition(self, state, condition: Condition) -> bool:
if condition.type == COND_ITEM:
rule = rules[rule_indices[condition.condition]]
if "Item" in rule.flags:
name = rule.flags.get("Item") or f"Post-{condition.condition}"
else:
name = condition.condition
return condition.sign == state.has(name, self.player)
elif condition.type == COND_LOC:
rule = rules[rule_indices[condition.condition]]
return condition.sign == self.check_conditions(
state, rule.operator, rule.conditions
)
elif condition.type == COND_FN:
if condition.condition == "npc":
if type(condition.argument) is not int:
raise Exception("@npc requires an integer argument")
npc_count = 0
for npc in npcs:
if state.has(npc, self.player):
npc_count += 1
if npc_count >= condition.argument:
return condition.sign
return not condition.sign
elif condition.condition == "calamity":
return condition.sign == self.options.calamity.value
elif condition.condition == "grindy":
return condition.sign == self.options.grindy_achievements.value
elif condition.condition == "pickaxe":
if type(condition.argument) is not int:
raise Exception("@pickaxe requires an integer argument")
for pickaxe, power in pickaxes.items():
if power >= condition.argument and state.has(pickaxe, self.player):
return condition.sign
return not condition.sign
elif condition.condition == "hammer":
if type(condition.argument) is not int:
raise Exception("@hammer requires an integer argument")
for hammer, power in hammers.items():
if power >= condition.argument and state.has(hammer, self.player):
return condition.sign
return not condition.sign
elif condition.condition == "mech_boss":
if type(condition.argument) is not int:
raise Exception("@mech_boss requires an integer argument")
boss_count = 0
for boss in mech_bosses:
if state.has(boss, self.player):
boss_count += 1
if boss_count >= condition.argument:
return condition.sign
return not condition.sign
elif condition.condition == "minions":
if type(condition.argument) is not int:
raise Exception("@minions requires an integer argument")
minion_count = 1
for armor, minions in armor_minions.items():
if state.has(armor, self.player) and minions + 1 > minion_count:
minion_count = minions + 1
if minion_count >= condition.argument:
return condition.sign
for accessory, minions in accessory_minions.items():
if state.has(accessory, self.player):
minion_count += minions
if minion_count >= condition.argument:
return condition.sign
return not condition.sign
elif condition.condition == "getfixedboi":
return condition.sign == self.options.getfixedboi.value
else:
raise Exception(f"Unknown function {condition.condition}")
elif condition.type == COND_GROUP:
operator, conditions = condition.condition
return condition.sign == self.check_conditions(state, operator, conditions)
def check_conditions(
self,
state,
operator: Union[bool, None],
conditions: List[
Tuple[
bool,
int,
Union[str, Tuple[Union[bool, None], list]],
Union[str, int, None],
]
],
) -> bool:
if operator is None:
if len(conditions) == 0:
return True
if len(conditions) > 1:
raise Exception("Found multiple conditions without an operator")
return self.check_condition(state, conditions[0])
elif operator:
return any(
self.check_condition(state, condition) for condition in conditions
)
else:
return all(
self.check_condition(state, condition) for condition in conditions
)
def set_rules(self) -> None:
for location in self.ter_locations:
def check(state: CollectionState, location=location):
rule = rules[rule_indices[location]]
return self.check_conditions(state, rule.operator, rule.conditions)
self.multiworld.get_location(location, self.player).access_rule = check
self.multiworld.completion_condition[self.player] = lambda state: state.has_all(
self.goal_items, self.player
)
def fill_slot_data(self) -> Dict[str, object]:
return {
"goal": list(self.goal_locations),
"deathlink": bool(self.options.death_link),
# The rest of these are included for trackers
"calamity": self.options.calamity.value,
"getfixedboi": self.options.getfixedboi.value,
"early_achievements": self.options.early_achievements.value,
"normal_achievements": self.options.normal_achievements.value,
"grindy_achievements": self.options.grindy_achievements.value,
"fishing_achievements": self.options.fishing_achievements.value,
}