diff --git a/README.md b/README.md index ac14c4b8..54b65939 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Currently, the following games are supported: * Mega Man Battle Network 3: Blue Version * Muse Dash * DOOM 1993 +* Terraria For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/worlds/terraria/Checks.py b/worlds/terraria/Checks.py new file mode 100644 index 00000000..3e09d178 --- /dev/null +++ b/worlds/terraria/Checks.py @@ -0,0 +1,736 @@ +from BaseClasses import Item, Location +from typing import Tuple, Union, Set, List, Dict +import string +import pkgutil + + +class TerrariaItem(Item): + game = "Terraria" + + +class TerrariaLocation(Location): + game = "Terraria" + + +def add_token( + tokens: List[Tuple[int, int, Union[str, int, None]]], + token: Union[str, int], + token_index: int, +): + if token == "": + return + if type(token) == str: + tokens.append((token_index, 0, token.rstrip())) + elif type(token) == int: + tokens.append((token_index, 1, token)) + + +IDENT = 0 +NUM = 1 +SEMI = 2 +HASH = 3 +AT = 4 +NOT = 5 +AND = 6 +OR = 7 +LPAREN = 8 +RPAREN = 9 +END_OF_LINE = 10 + +CHAR_TO_TOKEN_ID = { + ";": SEMI, + "#": HASH, + "@": AT, + "~": NOT, + "&": AND, + "|": OR, + "(": LPAREN, + ")": RPAREN, +} + +TOKEN_ID_TO_CHAR = {id: char for char, id in CHAR_TO_TOKEN_ID.items()} + + +def tokens(rule: str) -> List[Tuple[int, int, Union[str, int, None]]]: + token_list = [] + token = "" + token_index = 0 + escaped = False + for index, char in enumerate(rule): + if escaped: + if token == "": + token_index = index + token += char + escaped = False + elif char == "\\": + if type(token) == int: + add_token(token_list, token, token_index) + token = "" + escaped = True + elif char == "/" and token.endswith("/"): + add_token(token_list, token[:-1], token_index) + return token_list + elif token == "" and char.isspace(): + pass + elif token == "" and char.isdigit(): + token_index = index + token = int(char) + elif type(token) == int and char.isdigit(): + token = token * 10 + int(char) + elif (id := CHAR_TO_TOKEN_ID.get(char)) != None: + add_token(token_list, token, token_index) + token_list.append((index, id, None)) + token = "" + else: + if token == "": + token_index = index + token += char + + add_token(token_list, token, token_index) + return token_list + + +NAME = 0 +NAME_SEMI = 1 +FLAG_OR_SEMI = 2 +POST_FLAG = 3 +FLAG = 4 +FLAG_ARG = 5 +FLAG_ARG_END = 6 +COND_OR_SEMI = 7 +POST_COND = 8 +COND = 9 +LOC = 10 +FN = 11 +POST_FN = 12 +FN_ARG = 13 +FN_ARG_END = 14 +END = 15 +GOAL = 16 + +POS_FMT = [ + "name or `#`", + "`;`", + "flag or `;`", + "`;`, `|`, or `(`", + "flag", + "text or number", + "`)`", + "name, `#`, `@`, `~`, `(`, or `;`", + "`;`, `&`, `|`, or `)`", + "name, `#`, `@`, `~`, or `(`", + "name", + "name", + "`(`, `;`, `&`, `|`, or `)`", + "text or number", + "`)`", + "end of line", + "goal", +] + +RWD_NAME = 0 +RWD_NAME_SEMI = 1 +RWD_FLAG = 2 +RWD_FLAG_SEMI = 3 +RWD_END = 4 +RWD_LABEL = 5 + +RWD_POS_FMT = ["name or `#`", "`;`", "flag", "`;`", "end of line", "name"] + + +def unexpected(line: int, char: int, id: int, token, pos, pos_fmt, file): + if id == IDENT or id == NUM: + token_fmt = f"`{token}`" + elif id == END_OF_LINE: + token_fmt = "end of line" + else: + token_fmt = f"`{TOKEN_ID_TO_CHAR[id]}`" + + raise Exception( + f"in `{file}`, found {token_fmt} at {line + 1}:{char + 1}; expected {pos_fmt[pos]}" + ) + + +COND_ITEM = 0 +COND_LOC = 1 +COND_FN = 2 +COND_GROUP = 3 + + +def validate_conditions( + rule: str, + rule_indices: dict, + conditions: List[ + Tuple[ + bool, int, Union[str, Tuple[Union[bool, None], list]], Union[str, int, None] + ] + ], +): + for _, type, condition, _ in conditions: + if type == COND_ITEM: + if condition not in rule_indices: + raise Exception(f"item `{condition}` in `{rule}` is not defined") + elif type == COND_LOC: + if condition not in rule_indices: + raise Exception(f"location `{condition}` in `{rule}` is not defined") + elif type == COND_FN: + if condition not in { + "npc", + "calamity", + "pickaxe", + "hammer", + "mech_boss", + "minions", + }: + raise Exception(f"function `{condition}` in `{rule}` is not defined") + elif type == COND_GROUP: + _, conditions = condition + validate_conditions(rule, rule_indices, conditions) + + +def mark_progression( + conditions: List[ + Tuple[ + bool, int, Union[str, tuple[Union[bool, None], list]], Union[str, int, None] + ] + ], + progression: Set[str], + rules: list, + rule_indices: dict, + loc_to_item: dict, +): + for _, type, condition, _ in conditions: + if type == COND_ITEM: + prog = condition in progression + progression.add(loc_to_item[condition]) + _, flags, _, conditions = rules[rule_indices[condition]] + if ( + not prog + and "Achievement" not in flags + and "Location" not in flags + and "Item" not in flags + ): + mark_progression( + conditions, progression, rules, rule_indices, loc_to_item + ) + elif type == COND_LOC: + _, _, _, conditions = rules[rule_indices[condition]] + mark_progression(conditions, progression, rules, rule_indices, loc_to_item) + elif type == COND_GROUP: + _, conditions = condition + mark_progression(conditions, progression, rules, rule_indices, loc_to_item) + + +def read_data() -> ( + Tuple[ + # Goal to rule index that ends that goal's range and the locations required + List[Tuple[int, Set[str]]], + # Rules + List[ + Tuple[ + # Rule + str, + # Flag to flag arg + Dict[str, Union[str, int, None]], + # True = or, False = and, None = N/A + Union[bool, None], + # Conditions + List[ + Tuple[ + # True = positive, False = negative + bool, + # Condition type + int, + # Condition name or list (True = or, False = and, None = N/A) (list shares type with outer) + Union[str, Tuple[Union[bool, None], List]], + # Condition arg + Union[str, int, None], + ] + ], + ] + ], + # Rule to rule index + Dict[str, int], + # Label to rewards + Dict[str, List[str]], + # Reward to flags + Dict[str, Set[str]], + # Item name to ID + Dict[str, int], + # Location name to ID + Dict[str, int], + # NPCs + List[str], + # Pickaxe to pick power + Dict[str, int], + # Hammer to hammer power + Dict[str, int], + # Mechanical bosses + List[str], + # Calamity final bosses + List[str], + # Progression rules + Set[str], + # Armor to minion count, + Dict[str, int], + # Accessory to minion count, + Dict[str, int], + ] +): + next_id = 0x7E0000 + item_name_to_id = {} + + goals = [] + goal_indices = {} + rules = [] + rule_indices = {} + loc_to_item = {} + + npcs = [] + pickaxes = {} + hammers = {} + mech_boss_loc = [] + mech_bosses = [] + final_boss_loc = [] + final_bosses = [] + armor_minions = {} + accessory_minions = {} + + progression = set() + + for line, rule in enumerate( + pkgutil.get_data(__name__, "Rules.dsv").decode().splitlines() + ): + goal = None + name = None + flags = {} + + sign = True + operator = None + outer = [] + conditions = [] + + pos = NAME + for char, id, token in tokens(rule): + if pos == NAME: + if id == IDENT: + name = token + pos = NAME_SEMI + elif id == HASH: + pos = GOAL + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == NAME_SEMI: + if id == SEMI: + pos = FLAG_OR_SEMI + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == FLAG_OR_SEMI: + if id == IDENT: + flag = token + pos = POST_FLAG + elif id == SEMI: + pos = COND_OR_SEMI + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == POST_FLAG: + if id == SEMI: + if flag is not None: + if flag in flags: + raise Exception( + f"set flag `{flag}` at {line + 1}:{char + 1} that was already set" + ) + flags[flag] = None + flag = None + pos = COND_OR_SEMI + elif id == OR: + pos = FLAG + elif id == LPAREN: + pos = FLAG_ARG + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == FLAG: + if id == IDENT: + if flag is not None: + if flag in flags: + raise Exception( + f"set flag `{flag}` at {line + 1}:{char + 1} that was already set" + ) + flags[flag] = None + flag = None + flag = token + pos = POST_FLAG + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == FLAG_ARG: + if id == IDENT or id == NUM: + if flag in flags: + raise Exception( + f"set flag `{flag}` at {line + 1}:{char + 1} that was already set" + ) + flags[flag] = token + flag = None + pos = FLAG_ARG_END + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == FLAG_ARG_END: + if id == RPAREN: + pos = POST_FLAG + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == COND_OR_SEMI: + if id == IDENT: + conditions.append((sign, COND_ITEM, token, None)) + sign = True + pos = POST_COND + elif id == HASH: + pos = LOC + elif id == AT: + pos = FN + elif id == NOT: + sign = not sign + pos = COND + elif id == LPAREN: + outer.append((sign, None, conditions)) + conditions = [] + sign = True + pos = COND + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == POST_COND: + if id == SEMI: + if outer: + raise Exception( + f"found `;` at {line + 1}:{char + 1} after unclosed `(`" + ) + pos = END + elif id == AND: + if operator is True: + raise Exception( + f"found `&` at {line + 1}:{char + 1} in group containing `|`" + ) + operator = False + pos = COND + elif id == OR: + if operator is False: + raise Exception( + f"found `|` at {line + 1}:{char + 1} in group containing `&`" + ) + operator = True + pos = COND + elif id == RPAREN: + if not outer: + raise Exception( + f"found `)` at {line + 1}:{char + 1} without matching `(`" + ) + condition = operator, conditions + sign, operator, conditions = outer.pop() + conditions.append((sign, COND_GROUP, condition, None)) + sign = True + pos = POST_COND + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == COND: + if id == IDENT: + conditions.append((sign, COND_ITEM, token, None)) + sign = True + pos = POST_COND + elif id == HASH: + pos = LOC + elif id == AT: + pos = FN + elif id == NOT: + sign = not sign + elif id == LPAREN: + outer.append((sign, operator, conditions)) + conditions = [] + sign = True + operator = None + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == LOC: + if id == IDENT: + conditions.append((sign, COND_LOC, token, None)) + sign = True + pos = POST_COND + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == FN: + if id == IDENT: + function = token + pos = POST_FN + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == POST_FN: + if id == LPAREN: + pos = FN_ARG + elif id == SEMI: + conditions.append((sign, COND_FN, function, None)) + pos = END + elif id == AND: + conditions.append((sign, COND_FN, function, None)) + sign = True + if operator is True: + raise Exception( + f"found `&` at {line + 1}:{char + 1} in group containing `|`" + ) + operator = False + pos = COND + elif id == OR: + conditions.append((sign, COND_FN, function, None)) + sign = True + if operator is False: + raise Exception( + f"found `|` at {line + 1}:{char + 1} in group containing `&`" + ) + operator = True + pos = COND + elif id == RPAREN: + conditions.append((sign, COND_FN, function, None)) + if not outer: + raise Exception( + f"found `)` at {line + 1}:{char + 1} without matching `(`" + ) + condition = operator, conditions + sign, operator, conditions = outer.pop() + conditions.append((sign, COND_GROUP, condition, None)) + sign = True + pos = POST_COND + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == FN_ARG: + if id == IDENT or id == NUM: + conditions.append((sign, COND_FN, function, token)) + sign = True + pos = FN_ARG_END + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == FN_ARG_END: + if id == RPAREN: + pos = POST_COND + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + elif pos == END: + unexpected(line, char, id, token, pos) + elif pos == GOAL: + if id == IDENT: + goal = token + pos = END + else: + unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv") + + if pos != NAME and pos != FLAG_OR_SEMI and pos != COND_OR_SEMI and pos != END: + unexpected(line, char + 1, END_OF_LINE, None, pos, POS_FMT, "Rules.dsv") + + if name: + if name in rule_indices: + raise Exception( + f"rule `{name}` on line `{line + 1}` shadows a previous rule" + ) + rule_indices[name] = len(rules) + rules.append((name, flags, operator, conditions)) + + if "Item" in flags: + item_name = flags["Item"] or f"Post-{name}" + if item_name in item_name_to_id: + raise Exception( + f"item `{item_name}` on line `{line + 1}` shadows a previous item" + ) + item_name_to_id[item_name] = next_id + next_id += 1 + loc_to_item[name] = item_name + else: + loc_to_item[name] = name + + if "Npc" in flags: + npcs.append(name) + + if (power := flags.get("Pickaxe")) is not None: + pickaxes[name] = power + + if (power := flags.get("Hammer")) is not None: + hammers[name] = power + + if "Mech Boss" in flags: + mech_bosses.append(flags["Item"] or f"Post-{name}") + mech_boss_loc.append(name) + + if "Final Boss" in flags: + final_bosses.append(flags["Item"] or f"Post-{name}") + final_boss_loc.append(name) + + if (minions := flags.get("ArmorMinions")) is not None: + armor_minions[name] = minions + + if (minions := flags.get("Minions")) is not None: + accessory_minions[name] = minions + + if goal: + if goal in goal_indices: + raise Exception( + f"goal `{goal}` on line `{line + 1}` shadows a previous goal" + ) + goal_indices[goal] = len(goals) + goals.append((len(rules), set())) + + for name, flags, _, _ in rules: + if "Goal" in flags: + _, items = goals[ + goal_indices[ + name.translate(str.maketrans("", "", string.punctuation)) + .replace(" ", "_") + .lower() + ] + ] + items.add(name) + + _, mech_boss_items = goals[goal_indices["mechanical_bosses"]] + mech_boss_items.update(mech_boss_loc) + + _, final_boss_items = goals[goal_indices["calamity_final_bosses"]] + final_boss_items.update(final_boss_loc) + + for name, _, _, conditions in rules: + validate_conditions(name, rule_indices, conditions) + + for name, flags, _, conditions in rules: + prog = False + if ( + "Npc" in flags + or "Goal" in flags + or "Pickaxe" in flags + or "Hammer" in flags + or "Mech Boss" in flags + or "Minions" in flags + or "ArmorMinions" in flags + ): + progression.add(loc_to_item[name]) + prog = True + if prog or "Location" in flags or "Achievement" in flags: + mark_progression(conditions, progression, rules, rule_indices, loc_to_item) + + # Will be randomized via `slot_randoms` / `self.multiworld.random` + label = None + labels = {} + rewards = {} + + for line in pkgutil.get_data(__name__, "Rewards.dsv").decode().splitlines(): + reward = None + flags = set() + + pos = RWD_NAME + for char, id, token in tokens(line): + if pos == RWD_NAME: + if id == IDENT: + reward = f"Reward: {token}" + pos = RWD_NAME_SEMI + elif id == HASH: + pos = RWD_LABEL + else: + unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv") + elif pos == RWD_NAME_SEMI: + if id == SEMI: + pos = RWD_FLAG + else: + unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv") + elif pos == RWD_FLAG: + if id == IDENT: + if token in flags: + raise Exception( + f"set flag `{token}` at {line + 1}:{char + 1} that was already set" + ) + flags.add(token) + pos = RWD_FLAG_SEMI + else: + unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv") + elif pos == RWD_FLAG_SEMI: + if id == SEMI: + pos = RWD_END + else: + unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv") + elif pos == RWD_END: + unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv") + elif pos == RWD_LABEL: + if id == IDENT: + label = token + if label in labels: + raise Exception( + f"started label `{label}` at {line + 1}:{char + 1} that was already used" + ) + labels[label] = [] + pos = RWD_END + else: + unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv") + + if pos != RWD_NAME and pos != RWD_FLAG and pos != RWD_END: + unexpected(line, char + 1, END_OF_LINE, None, pos) + + if reward: + if reward in rewards: + raise Exception( + f"reward `{reward}` on line `{line + 1}` shadows a previous reward" + ) + rewards[reward] = flags + + if not label: + raise Exception( + f"reward `{reward}` on line `{line + 1}` is not labeled" + ) + labels[label].append(reward) + + if reward in item_name_to_id: + raise Exception( + f"item `{reward}` on line `{line + 1}` shadows a previous item" + ) + item_name_to_id[reward] = next_id + next_id += 1 + + item_name_to_id["Reward: Coins"] = next_id + item_name_to_id["Victory"] = next_id + 1 + next_id += 2 + + location_name_to_id = {} + + for name, flags, _, _ in rules: + if "Location" in flags or "Achievement" in flags: + if name in location_name_to_id: + raise Exception(f"location `{name}` shadows a previous location") + location_name_to_id[name] = next_id + next_id += 1 + + return ( + goals, + rules, + rule_indices, + labels, + rewards, + item_name_to_id, + location_name_to_id, + npcs, + pickaxes, + hammers, + mech_bosses, + final_bosses, + progression, + armor_minions, + accessory_minions, + ) + + +( + goals, + rules, + rule_indices, + labels, + rewards, + item_name_to_id, + location_name_to_id, + npcs, + pickaxes, + hammers, + mech_bosses, + final_bosses, + progression, + armor_minions, + accessory_minions, +) = read_data() diff --git a/worlds/terraria/Options.py b/worlds/terraria/Options.py new file mode 100644 index 00000000..1f9ba69a --- /dev/null +++ b/worlds/terraria/Options.py @@ -0,0 +1,57 @@ +from Options import Choice, Option, Toggle, DeathLink +import typing + + +class Goal(Choice): + """The victory condition for your run. Stuff after the goal will not be shuffled.""" + + display_name = "Goal" + option_mechanical_bosses = 0 + # option_calamitas_clone = 1 + option_plantera = 2 + option_golem = 3 + option_empress_of_light = 4 + option_lunatic_cultist = 5 + # option_astrum_deus = 6 + option_moon_lord = 7 + # option_providence_the_profaned_goddess = 8 + # option_devourer_of_gods = 9 + # option_yharon_dragon_of_rebirth = 10 + option_zenith = 11 + # option_calamity_final_bosses = 12 + # option_adult_eidolon_wyrm = 13 + default = 0 + + +class Achievements(Choice): + """ + Adds checks upon collecting achievements. Achievements for clearing bosses and events are excluded. + "Exclude Grindy" also excludes fishing achievements. + """ + + display_name = "Achievements" + option_none = 0 + option_exclude_grindy = 1 + option_exclude_fishing = 2 + option_all = 3 + default = 1 + + +class FillExtraChecksWith(Choice): + """ + Applies if you have achievements enabled. "Useful Items" helps to make the early game less grindy. + Items are rewarded to all players in your Terraria world. + """ + + display_name = "Fill Extra Checks With" + option_coins = 0 + option_useful_items = 1 + default = 1 + + +options: typing.Dict[str, type(Option)] = { # type: ignore + "goal": Goal, + "achievements": Achievements, + "fill_extra_checks_with": FillExtraChecksWith, + "death_link": DeathLink, +} diff --git a/worlds/terraria/Rewards.dsv b/worlds/terraria/Rewards.dsv new file mode 100644 index 00000000..dbae37b4 --- /dev/null +++ b/worlds/terraria/Rewards.dsv @@ -0,0 +1,154 @@ +# ordered + +Hermes Boots; +Magic Mirror; +Demon Conch; +Magic Conch; +Cosmolight; Calamity; +Grappling Hook; +Cloud in a Bottle; +Climbing Claws; +Ancient Chisel; +Fledgling Wings; +Rod of Discord; + +# random + +// Movement +Aglet; +Anklet of the Wind; +Ice Skates; +Lava Charm; +Water Walking Boots; + +Flipper; +Frog Leg; +Shoe Spikes; +Tabi; +Black Belt; + +Flying Carpet; + +Moon Charm; +Neptune's Shell; + +// Informational (Cell Phone) +Compass; +Depth Meter; +Radar; +Tally Counter; +Lifeform Analyzer; +DPS Meter; +Stopwatch; +Metal Detector; +Fisherman's Pocket Guide; +Weather Radio; +Sextant; + + +// Health and Mana +Band of Regeneration; + +Celestial Magnet; +Nature's Gift; +Philosopher's Stone; + +// Combat +Cobalt Shield; + +Armor Polish; +Vitamins; +Bezoar; +Adhesive Bandage; +Megaphone; +Nazar; +Fast Clock; +Trifold Map; +Blindfold; +Pocket Mirror; + +Paladin's Shield; +Frozen Turtle Shell; + +Flesh Knuckles; +Putrid Scent; + +Feral Claws; + +Cross Necklace; +Star Cloak; +Titan Glove; + +Obsidian Rose; +Magma Stone; + +Shark Tooth Necklace; + +Magic Quiver; +Rifle Scope; + +// Construction (Hand of Creation) +Brick Layer; +Extendo Grip; +Paint Sprayer; +Portable Cement Mixer; +Treasure Magnet; +Step Stool; + +// Money (Greedy Ring) +Discount Card; +Gold Ring; +Lucky Coin; + +// Fishing (Lavaproof Tackle Bag) +High Test Fishing Line; +Angler Earring; +Tackle Box; +Lavaproof Fishing Hook; + + +// Yoyo Stuff +Red Counterweight; +Yoyo Glove; + +// Vanilla Forced to Calamity +Diving Helmet; Calamity; +Jellyfish Necklace; Calamity; + + +// Calamity Accessories +Corrupt Flask; Calamity; +Crimson Flask; Calamity; +Craw Carapace; Calamity; +Giant Shell; Calamity; +Fungal Carapace; Calamity; +Life Jelly; Calamity; +Vital Jelly; Calamity; +Mana Jelly; Calamity; +Giant Tortoise Shell; Calamity; +Coin of Deceit; Calamity; +Ink Bomb; Calamity; +Voltaic Jelly; Calamity; +Wulfrum Battery; Calamity; +Luxor's Gift; Calamity; +Raider's Talisman; Calamity; +Rotten Dogtooth; Calamity; +Scuttler's Jewel; Calamity; +Unstable Granite Core; Calamity; +Amidias' Spark; Calamity; +Ursa Sergeant; Calamity; +Trinket of Chi; Calamity; +The Transformer; Calamity; +Rover Drive; Calamity; +Marnite Repulsion Shield; Calamity; +Frost Barrier; Calamity; +Ancient Fossil; Calamity; +Spelunker's Amulet; Calamity; +Fungal Symbiote; Calamity; +Gladiator's Locket; Calamity; +Wulfrum Acrobatics Pack; Calamity; +Depths Charm; Calamity; +Anechoic Plating; Calamity; +Iron Boots; Calamity; +Sprit Glyph; Calamity; +Abyssal Amulet; Calamity; \ No newline at end of file diff --git a/worlds/terraria/Rules.dsv b/worlds/terraria/Rules.dsv new file mode 100644 index 00000000..6017060b --- /dev/null +++ b/worlds/terraria/Rules.dsv @@ -0,0 +1,573 @@ +// TODO Calamity minion armor + +// Starting gear +Copper Shortsword; +Guide; Npc; + +// Immediately accessible +Timber!!; Achievement; +Benched; Achievement; +Stop! Hammer Time!; Achievement; +Matching Attire; Achievement; +Fashion Statement; Achievement; +Ooo! Shiny!; Achievement; +No Hobo; Achievement; +Merchant; Npc; +Bug Net; ; @calamity | Merchant; +Heavy Metal; Achievement; +Nurse; Npc; Merchant; +The Frequent Flyer; Achievement; Nurse; +Demolitionist; Npc; Merchant; +Dye Trader; Npc; @npc(4); +Dye Hard; Achievement; Dye Trader; +Lucky Break; Achievement; +Star Power; Achievement; +You Can Do It!; Achievement; + +// Surface exploration +Aglet; +Heliophobia; Achievement; +Blighted Gel; Calamity; +Archaeologist; Achievement | Grindy; +Zoologist; Npc; +Cat; Npc; Zoologist; +Feeling Petty; Achievement; Cat | Dog; +Dog; Npc; Zoologist; +A Rather Blustery Day; Achievement | Grindy; +Enchanted Sword; +Pretty in Pink; Achievement | Grindy; +Marathon Medalist; Achievement | Grindy; +Angler; Npc; +Servant-in-Training; Achievement | Fishing; Angler; +\10 Fishing Quests; Achievement | Fishing; Angler; +Trout Monkey; Achievement | Fishing; Angler; +Glorious Golden Pole; Achievement | Fishing; Angler; +Fast and Fishious; Achievement | Fishing; Angler; +Supreme Helper Minion!; Achievement | Fishing; Angler; +Water Walking Boots; +Painter; Npc; @npc(8); + +// Sky exploration +Into Orbit; Achievement; +Mysterious Circuitry; Calamity; +Dubious Plating; Calamity; +Charging Station; Calamity; +Codebreaker Base; Calamity; +Starfury; + +// Underground +Watch Your Step!; Achievement; +Throwing Lines; Achievement; +Torch God; Location | Item(Reward: Torch God's Favor); +Vehicular Manslaughter; Achievement; +Hey! Listen!; Achievement; +I Am Loot!; Achievement; +Heart Breaker; Achievement; +Hold on Tight!; Achievement; +Feller of Evergreens; Calamity; +Gold Hammer; Hammer(55); +Gold Pickaxe; Pickaxe(55); +Like a Boss; Achievement; +Hermes Boots; +Jeepers Creepers; Achievement; +Stylist; Npc; +Funkytown; Achievement; +Deceiver of Fools; Achievement | Grindy; +Dead Men Tell No Tales; Achievement; +Bulldozer; Achievement | Grindy; + +// Cavern +Obsidian; +Obsidian Skull; ; Obsidian; +There are Some Who Call Him...; Achievement | Grindy; +Lava Charm; +Demonite Ore; +Demonite Bar; ; Demonite Ore | (@calamity & #Calamity Evil Boss); +Evil Sword; ; Demonite Bar; + +// Underground Ice +Ice Skates; +Flinx Fur Coat; ArmorMinions(1); + +// Underground Desert +Golfer; Npc; + +// Sunken Sea +Sea Prism; Calamity; +Navyplate; Calamity; Sea Prism & Obsidian; + +// Underground Jungle +Anklet of the Wind; +Stinger; +Jungle Spores; +Vine; +Blade of Grass; ; Stinger & Jungle Spores & Vine; +Summoning Potion; Minions(1); + +// Underworld +It's Getting Hot in Here; Achievement; +Rock Bottom; Achievement; +Obsidian Rose; +Havocplate; Calamity; + +// Evil +Smashing, Poppet!; Achievement; +Arms Dealer; Npc; +Leading Landlord; Achievement; Nurse & Arms Dealer; // The logic is way more complex, but that doesn't affect anything +Completely Awesome; Achievement; Arms Dealer; + +// King Slime +King Slime; Location | Item; +Sticky Situation; Achievement | Grindy; +The Cavalry; Achievement; +Solidifier; ; #King Slime; + +// Desert Scourge +Desert Scourge; Calamity | Location | Item; +Pearl Shard; Calamity; #Desert Scourge; +Sea Remains; Calamity; Pearl Shard; +Reefclaw Hamaxe; Calamity | Hammer(60); Sea Remains; +Sandstorm; ; ~@calamity | Desert Scourge; +Voltaic Jelly; Calamity | Minions(1); Desert Scourge; // Jelly-Charged Battery doesn't stack. This is the case for all Calamity minion accessory upgrades. + +// Giant Clam +Giant Clam; Calamity | Location | Item; Desert Scourge; +Amidias; Calamity; Desert Scourge & #Giant Clam; + +// Blood Moon +Bloodbath; Achievement | Grindy; +Til Death...; Achievement | Grindy; +Quiet Neighborhood; Achievement; + +// Eye of Cthulhu +Eye of Cthulhu; Location | Item; +Dryad; Npc; Eye of Cthulhu | Evil Boss | Skeletron; +Pumpkin Seeds; ; Dryad; +Pumpkin; ; Pumpkin Seeds; +Purification Powder; ; Dryad; // Shimmered from Evil Powder in 1.4.4. Not bought from Dryad in get fixed boi. +Party Girl; Npc; @npc(14); +Jolly Jamboree; Achievement | Grindy; Party Girl; +Acid Rain Tier 1; Calamity | Location | Item; Eye of Cthulhu; + +// Crabulon +Crabulon; Calamity | Location | Item; + +// Evil Boss +Evil Boss; Location | Item; +Evil Boss Part; ; #Evil Boss; +Evil Pickaxe; Pickaxe(65); Evil Boss Part; +Obsidian Armor; ArmorMinions(1); Obsidian & Evil Boss Part; +Tavernkeep; Npc; Evil Boss; +Old One's Army Tier 1; Location | Item; Tavernkeep; +Meteorite; ; Evil Boss; +Meteorite Bar; ; Meteorite; +Meteor Hamaxe; Hammer(60); Meteorite Bar; +Hellforge; ; @pickaxe(60); +Hellstone; ; @pickaxe(65) | Wall of Flesh; +Hellstone Bar; ; Hellstone; +Fiery Greatsword; ; Hellstone Bar; +Molten Hamaxe; Hammer(70); Hellstone Bar; +Molten Pickaxe; Pickaxe(100); Hellstone Bar; +Miner for Fire; Achievement; Molten Pickaxe; +Hot Reels!; Achievement; Hellstone Bar & Bug Net; // TODO Calamity +Brimstone Slag; Calamity; @pickaxe(100); + +// Goblin Army +Goblin Army; Location | Item; +Goblin Tinkerer; Npc; Goblin Army; +Tinkerer's Workshop; ; Goblin Tinkerer; +Rocket Boots; ; Goblin Tinkerer; +Spectre Boots; ; Tinkerer's Workshop & Hermes Boots & Rocket Boots; +Lightning Boots; ; Tinkerer's Workshop & Spectre Boots & Anklet of the Wind & Aglet; +Frostspark Boots; ; Tinkerer's Workshop & Lightning Boots & Ice Skates; +Lava Waders; ; Tinkerer's Workshop & Obsidian Skull & Lava Charm & Obsidian Rose & Water Walking Boots; +Terraspark Boots; ; Tinkerer's Workshop & Frostspark Boots & Lava Waders; +Boots of the Hero; Achievement | Grindy; Terraspark Boots; + +// Queen Bee +Where's My Honey?; Achievement; +Queen Bee; Location | Item; +Bee Keeper; ; #Queen Bee; +Bee Wax; ; #Queen Bee; +Bee Armor; ArmorMinions(2); Bee Wax; +Not the Bees!; Achievement; #Queen Bee & Bee Armor; +Witch Doctor; Npc; Queen Bee; +Pygmy Necklace; Minions(1); Witch Doctor; + +// Calamity Evil Boss +Calamity Evil Boss; Calamity | Location | Item; +Aerialite Ore; Calamity; Calamity Evil Boss & @pickaxe(65); +Aerialite Bar; Calamity; Aerialite Ore; +Aerial Hamaxe; Calamity | Hammer(70); Aerialite Bar; +Skyfringe Pickaxe; Calamity | Pickaxe(75); Aerialite Bar; + +// Skeletron +Skeletron; Location | Item; +Clothier; Npc; Skeletron; +Dungeon; ; Skeletron; +Dungeon Heist; Achievement; Dungeon; +Bone; ; Dungeon | (@calamity & #Skeletron); +Bewitching Table; Minions(1); Dungeon; +Mechanic; ; Dungeon; +Wire; ; Mechanic; +Decryption Computer; Calamity; Mysterious Circuitry & Dubious Plating & Wire; +Actuator; ; Mechanic; +Muramasa; ; Dungeon; + +// Deerclops +Deerclops; Location | Item; + +// The Slime God +The Slime God; Calamity | Location | Item; Blighted Gel; +Purified Gel; Calamity; #The Slime God; +Static Refiner; Calamity; Purified Gel & Solidifier; +Gelpick; Calamity | Pickaxe(100); Static Refiner & Purified Gel & Blighted Gel; +Night's Edge; ; Evil Sword & Muramasa & Blade of Grass & Fiery Greatsword & (~@calamity | Purified Gel); + +// Wall of Flesh +Wall of Flesh; Location | Item(Hardmode); Guide; +Pwnhammer; Hammer(80); #Wall of Flesh; +Wizard; Npc; Wall of Flesh; +Tax Collector; Npc; Purification Powder & Wall of Flesh; +Spider Fangs; ; Wall of Flesh; +Spider Armor; ArmorMinions(3); Spider Fangs; +Cross Necklace; ; Wall of Flesh; +Altar; ; Wall of Flesh & @hammer(80); +Begone, Evil!; Achievement; Altar; +Cobalt Ore; ; ((~@calamity & Altar) | (@calamity & Wall of Flesh)) & @pickaxe(100); +Extra Shiny!; Achievement; Cobalt Ore | Mythril Ore | Adamantite Ore | Chlorophyte Ore; +Cobalt Bar; ; Cobalt Ore; +Cobalt Pickaxe; Pickaxe(110); Cobalt Bar; +Soul of Night; ; Wall of Flesh | (@calamity & Altar); +Hallow; ; Wall of Flesh; +Pixie Dust; ; Hallow; +Unicorn Horn; ; Hallow; +Crystal Shard; ; Hallow; +Axe of Purity; Calamity; Feller of Evergreens & Purification Powder & Pixie Dust & Crystal Shard; +Soul of Light; ; Hallow | (@calamity & #Queen Slime); +Blessed Apple; ; Hallow; +Rod of Discord; ; Hallow; +Gelatin World Tour; Achievement | Grindy; Dungeon & Wall of Flesh & Hallow & #King Slime; +Soul of Flight; ; Wall of Flesh; +Head in the Clouds; Achievement; (Soul of Flight & ((Hardmode Anvil & (Soul of Light | Soul of Night | Pixie Dust | Wall of Flesh | Solar Eclipse | @mech_boss(1) | Plantera | Spectre Bar | #Golem)) | (Shroomite Bar & Autohammer) | #Mourning Wood | #Pumpking)) | Steampunker | (Wall of Flesh & Witch Doctor) | (Solar Eclipse & Plantera) | #Everscream | #Old One's Army Tier 3 | #Empress of Light | #Duke Fishron | (Fragment & Luminite Bar & Ancient Manipulator); // Leaf Wings are Post-Plantera in 1.4.4 +Bunny; Npc; Zoologist & Wall of Flesh; // Extremely simplified +Forbidden Fragment; ; Sandstorm & Wall of Flesh; +Astral Infection; Calamity; Wall of Flesh; +Stardust; Calamity; Astral Infection | #Astrum Aureus | #Astrum Deus; +Trapper Bulb; Calamity; Wall of Flesh; +Titan Heart; Calamity; Astral Infection; +Essence of Sunlight; Calamity; Wall of Flesh | Golem; +Essence of Eleum; Calamity; Wall of Flesh | Cryogen | #Cryogen; // TODO Check +Essence of Havoc; Calamity; Wall of Flesh | #Calamitas Clone | #Brimstone Elemental; +Don't Dread on Me; Achievement; Wall of Flesh; +Earth Elemental; Calamity | Location | Item; Wall of Flesh; +Cloud Elemental; Calamity | Location | Item; Wall of Flesh; +Truffle; Npc; Wall of Flesh; +It Can Talk?!; Achievement; Truffle; +The First Shadowflame; Calamity | Minions(1); Goblin Army | Wall of Flesh; + +// Pirate Invasion +Pirate Invasion; Location | Item; Wall of Flesh; +Pirate; Npc; Pirate Invasion; + +// Queen Slime +Queen Slime; Location | Item; Hallow; + +// Aquatic Scourge +Mythril Ore; ; ((~@calamity & Altar) | (@calamity & @mech_boss(1))) & @pickaxe(110); +Mythril Bar; ; Mythril Ore; +Hardmode Anvil; ; Mythril Bar; +Mythril Pickaxe; Pickaxe(150); Hardmode Anvil & Mythril Bar; +Adamantite Ore; ; ((~@calamity & Altar) | (@calamity & @mech_boss(2))) & @pickaxe(150); +Hardmode Forge; ; Hardmode Anvil & Adamantite Ore & Hellforge; +Adamantite Bar; ; Hardmode Forge & Adamantite Ore; +Adamantite Pickaxe; Pickaxe(180); Hardmode Anvil & Adamantite Bar; +Forbidden Armor; ArmorMinions(2); Hardmode Anvil & Adamantite Bar & Forbidden Fragment; +Aquatic Scourge; Calamity | Location | Item; +The Twins; Location | Item | Mech Boss; (@calamity | Hardmode Anvil) & Soul of Light; +Brimstone Elemental; Calamity | Location | Item; Soul of Night & Essence of Havoc & Unholy Core; +The Destroyer; Location | Item | Mech Boss; (@calamity | Hardmode Anvil) & Soul of Night; +Cryogen; Calamity | Location | Item; Soul of Night & Soul of Light & Essence of Eleum; +Skeletron Prime; Location | Item | Mech Boss; (@calamity | Hardmode Anvil) & Soul of Night & Soul of Light & Bone; +# mechanical_bosses +Cragmaw Mire; Calamity | Location | Item; #Acid Rain Tier 2; +Nuclear Rod; Calamity | Minions(1); #Cragmaw Mire; +Acid Rain Tier 2; Calamity | Location | Item; #Acid Rain Tier 1 & Aquatic Scourge; + +// The Twins +Soul of Sight; ; The Twins; +Steampunker; Npc; @mech_boss(1); +Hammush; ; Truffle & @mech_boss(1); +Rainbow Rod; ; Hardmode Anvil & Crystal Shard & Unicorn Horn & Pixie Dust & Soul of Light & Soul of Sight; +Prismancer; Achievement; Rainbow Rod; +Long Ranged Sensor Array; Calamity; Hardmode Anvil & Mysterious Circuitry & Dubious Plating & Mythril Bar & Wire & Decryption Computer & Codebreaker Base; +Hydraulic Volt Crusher; Calamity; Hardmode Anvil & Mysterious Circuitry & Dubious Plating & Mythril Bar & Soul of Sight; +Life Fruit; ; (@mech_boss(1) & Wall of Flesh) | (@calamity & (Living Shard | Wall of Flesh)); +Get a Life; Achievement; Life Fruit; +Topped Off; Achievement; Life Fruit; +Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & (@mech_boss(1) | #Old One's Army Tier 3); + +// Brimstone Elemental +Infernal Suevite; Calamity; @pickaxe(150) | Brimstone Elemental; +Unholy Core; Calamity; Infernal Suevite & Hellstone; + +// The Destroyer +Soul of Might; ; #The Destroyer; + +// Cryogen +Cryonic Ore; Calamity; Cryogen & (@pickaxe(180) | @mech_boss(2)); +Cryonic Bar; Calamity; (Hardmode Forge & Cryonic Ore) | Fleshy Geode | Necromantic Geode; +Abyssal Warhammer; Calamity | Hammer(88); Hardmode Anvil & Cryonic Bar; +Shardlight Pickaxe; Calamity | Pickaxe(180); Hardmode Anvil & Cryonic Bar; + +// Skeletron Prime +Soul of Fright; ; #Skeletron Prime; +Inferna Cutter; Calamity; Hardmode Anvil & Axe of Purity & Soul of Fright & Essence of Havoc; +Buckets of Bolts; Achievement; #The Twins & #The Destroyer & #Skeletron Prime; +Mecha Mayhem; Achievement; #The Twins & #The Destroyer & #Skeletron Prime; +Hallowed Bar; ; (#The Twins | #The Destroyer | #Skeletron Prime) & (~@calamity | @mech_boss(3)); // Can't count on Hallowed Ore, since the player may be in prehardmode (TODO Check this) +Hallowed Armor; ArmorMinions(3); Hardmode Anvil & Hallowed Bar; +Excalibur; ; Hardmode Anvil & Hallowed Bar; +Pickaxe Axe; Pickaxe(200); Hardmode Anvil & Hallowed Bar & Soul of Fright & Soul of Might & Soul of Sight; +Drax Attax; Achievement; Pickaxe Axe; +True Night's Edge; ; Hardmode Anvil & Night's Edge & Soul of Fright & Soul of Might & Soul of Sight; +Chlorophyte Ore; ; Wall of Flesh & @pickaxe(200); +Photosynthesis; Achievement; Chlorophyte Ore; +Chlorophyte Bar; ; Hardmode Forge & Chlorophyte Ore; +True Excalibur; ; Hardmode Anvil & Excalibur & Chlorophyte Bar; +Chlorophyte Pickaxe; Pickaxe(200); Hardmode Anvil & Chlorophyte Bar; +Chlorophyte Warhammer; Hammer(90); Hardmode Anvil & Chlorophyte Bar; + +// Calamitas Clone +Calamitas Clone; Calamity | Location | Item | Goal; Hardmode Anvil & Hellstone Bar & Essence of Havoc; +Plantera; Location | Item | Goal; Wall of Flesh & (@mech_boss(3) | (@calamity & Hardmode Anvil & Trapper Bulb)); +# calamitas_clone +# plantera +Ashes of Calamity; Calamity; #Calamitas Clone; + +// Plantera +The Axe; Hammer(100); #Plantera; +Seedler; ; #Plantera; +Living Shard; Calamity; #Plantera; +Tiki Armor; ArmorMinions(4); Witch Doctor & Wall of Flesh & Plantera; +Hercules Beetle; ; Witch Doctor & Wall of Flesh & Plantera; +You and What Army?; Achievement; @minions(8); +Cyborg; Npc; Plantera; +Autohammer; ; Truffle & Plantera; +Shroomite Bar; ; Autohammer & Chlorophyte Bar; +Shroomite Digging Claw; Pickaxe(200); Hardmode Anvil & Shroomite Bar; +Princess; Npc; Guide & Merchant & Nurse & Demolitionist & Dye Trader & Zoologist & Angler & Painter & Stylist & Golfer & Arms Dealer & Dryad & Party Girl & Tavernkeep & Goblin Tinkerer & Witch Doctor & Clothier & Wizard & Truffle & Tax Collector & Pirate & Steampunker & Cyborg; +Real Estate Agent; Achievement; Princess; +Ectoplasm; ; ((Dungeon & Wall of Flesh) | @calamity) & Plantera; +Paladin's Shield; ; Dungeon & Wall of Flesh & Plantera; +Core of Sunlight; Calamity; (Hardmode Anvil & Essence of Sunlight & Ectoplasm) | Fleshy Geode | Necromantic Geode; +Core of Eleum; Calamity; (Hardmode Anvil & Essence of Eleum & Ectoplasm) | Fleshy Geode | Necromantic Geode; +Core of Havoc; Calamity; (Hardmode Anvil & Essence of Havoc & Ectoplasm) | Fleshy Geode | Necromantic Geode; +Core of Calamity; Calamity; (Hardmode Anvil & Core of Sunlight & Core of Eleum & Core of Havoc & Ashes of Calamity) | Necromantic Geode; +Spectre Bar; ; Hardmode Forge & Chlorophyte Bar & Ectoplasm; +Spectre Pickaxe; Pickaxe(200); Hardmode Anvil & Spectre Bar; +Spectre Hamaxe; Hammer(90); Hardmode Anvil & Spectre Bar; +Robbing the Grave; Achievement; Dungeon & Plantera; +Evil Key; ; Plantera | (@calamity & #Wall of Flesh); +Frozen Key; ; Plantera | (@calamity & #Cryogen); +Jungle Key; ; Plantera | (@calamity & #Plantera); +Hallowed Key; ; (Plantera & Hallow) | (@calamity & #Queen Slime); +Desert Key; ; Plantera | (@calamity & #Great Sand Shark); +Big Booty; Achievement; Dungeon & Wall of Flesh & Plantera & (Evil Key | Frozen Key | Jungle Key | Hallowed Key | Desert Key); +Rainbow Gun; ; Dungeon & Wall of Flesh & Plantera & Hallowed Key; +Rainbows and Unicorns; Achievement; Blessed Apple & Rainbow Gun; +Perennial Ore; Calamity; Plantera; +Perennial Bar; Calamity; Hardmode Forge & Perennial Ore; +Beastial Pickaxe; Calamity | Pickaxe(200); Hardmode Anvil & Perennial Bar; +Armored Digger; Calamity | Location | Item; Plantera; // TODO Check + +// Solar Eclipse +Temple Raider; Achievement; #Plantera; +Lihzahrd Temple; ; #Plantera | (Plantera & Actuator) | @pickaxe(210) | (@calamity & Hardmode Anvil & Soul of Light & Soul of Night); +Solar Eclipse; ; Lihzahrd Temple & Wall of Flesh; +Broken Hero Sword; ; (Solar Eclipse & Plantera) | (@calamity & #Calamitas Clone); +Terra Blade; ; Hardmode Anvil & True Night's Edge & True Excalibur & Broken Hero Sword & (~@calamity | Living Shard); +Sword of the Hero; Achievement; Terra Blade; +Kill the Sun; Achievement; Solar Eclipse; + +// Great Sand Shark +Great Sand Shark; Calamity | Location | Item; Hardmode Anvil & Forbidden Fragment & Core of Sunlight; + +// Leviathan and Anahita +Leviathan and Anahita; Calamity | Location | Item; + +// Astrum Aureus +Astrum Aureus; Calamity | Location | Item; Hardmode Anvil & Stardust & Astral Infection; +Starbuster Core; Calamity | Minions(1); Astrum Aureus & Astral Infection; + +// Golem +Golem; Location | Item | Goal; (Wall of Flesh & Plantera & Lihzahrd Temple) | (@calamity & Hardmode Anvil & Lihzahrd Temple & Essence of Sunlight); +# golem +Picksaw; Pickaxe(210); #Golem; +Lihzahrd Brick; ; @pickaxe(210); +Scoria Ore; Calamity; Golem | @pickaxe(210); +Scoria Bar; Calamity; Hardmode Forge & Scoria Ore; +Seismic Hampick; Calamity | Pickaxe(210) | Hammer(95); Hardmode Anvil & Scoria Bar; +Life Alloy; Calamity; (Hardmode Anvil & Cryonic Bar & Perennial Bar & Scoria Bar) | Necromantic Geode; +Advanced Display; Calamity; Hardmode Anvil & Mysterious Circuitry & Dubious Plating & Life Alloy & Long Ranged Sensor Array; +Old One's Army Tier 3; Location | Item; #Old One's Army Tier 1 & Golem; + +// Martian Madness +Martian Madness; Location | Item; Wall of Flesh & Golem; +Influx Waver; ; #Martian Madness; + +// The Plaguebringer Goliath +Plague Cell Canister; Calamity; Golem; +Plaguebringer; Calamity | Location | Item; Golem; +The Plaguebringer Goliath; Calamity | Location | Item; Hardmode Anvil & Plague Cell Canister; + +// Duke Fishron +Duke Fishron; Location | Item; Bug Net & Wall of Flesh; + +// Pumpkin Moon +Pumpkin Moon; ; Hardmode Anvil & Pumpkin & Ectoplasm & (@calamity | Hallowed Bar); +Spooky Armor; ArmorMinions(4); Pumpkin Moon; +Mourning Wood; Location | Item; Pumpkin Moon; +Necromantic Scroll; Minions(1); Mourning Wood; +Papyrus Scarab; Minions(1); Tinkerer's Workshop & Hercules Beetle & Necromantic Scroll; +Pumpking; Location | Item; Pumpkin Moon; +The Horseman's Blade; ; #Pumpking; +Baleful Harvest; Achievement; Pumpkin Moon; + +// Frost Moon +Frost Moon; ; Hardmode Anvil & Ectoplasm & (@calamity | Soul of Fright); +Everscream; Location | Item; Frost Moon; +Santa-NK1; Location | Item; Frost Moon; +Ice Queen; Location | Item; Frost Moon; +Ice Scream; Achievement; Frost Moon; +Christmas; ; Frost Moon; + +// Frost Legion +Frost Legion; Location | Item; (Wall of Flesh & Christmas) | (@calamity & Soul of Light & Soul of Night); +Santa Claus; Npc; Frost Legion & Christmas; + +// Ravager +Ravager; Calamity | Location | Item; Hardmode Anvil & Lihzahrd Temple & Lihzahrd Brick; +Fleshy Geode; Calamity; #Ravager; + +// Empress of Light +Empress of Light; Location | Item | Goal; Wall of Flesh & Hallow & (@calamity | Plantera); +# empress_of_light + +// Lunatic Cultist +Lunatic Cultist; Location | Item; (@calamity | (Dungeon & Golem)) & Wall of Flesh; +Astrum Deus; Calamity | Location | Item; Titan Heart; +# lunatic_cultist +# astrum_deus +Ancient Manipulator; ; #Lunatic Cultist; + +// Lunar Events +Lunar Events; Location | Item; #Lunatic Cultist; +Fragment; ; #Lunar Events | #Astrum Deus; +Galactica Singularity; Calamity; Ancient Manipulator & Fragment; +Meld Blob; Calamity; #Lunar Events | #Astrum Deus; +Meld Construct; Calamity; Ancient Manipulator & Meld Blob & Stardust; + +// Astrum Deus +Astral Ore; Calamity; Wall of Flesh & Astrum Deus; +Astral Bar; Calamity; Ancient Manipulator & Stardust & Astral Ore; +Astral Hamaxe; Calamity | Hammer(100); Ancient Manipulator & Astral Bar; +Astral Pickaxe; Calamity | Pickaxe(220); Ancient Manipulator & Astral Bar; + +// Moon Lord +Moon Lord; Location | Item | Goal; #Lunar Events; +# moon_lord +Slayer of Worlds; Achievement; #Evil Boss & #The Destroyer & #Duke Fishron & #Eye of Cthulhu & #Golem & #King Slime & #Lunatic Cultist & #Moon Lord & #Plantera & #Queen Bee & #Skeletron & #Skeletron Prime & #The Twins & #Wall of Flesh; +Luminite; ; #Moon Lord; +Luminite Bar; ; Ancient Manipulator & Luminite; +Luminite Hamaxe; Hammer(100); Ancient Manipulator & Fragment & Luminite Bar; +Luminite Pickaxe; Pickaxe(225); Ancient Manipulator & Fragment & Luminite Bar; +Genesis Pickaxe; Calamity | Pickaxe(225); Ancient Manipulator & Meld Construct & Luminite Bar; +Stardust Armor; ArmorMinions(5); Ancient Manipulator & Fragment & Luminite Bar; +Terrarian; ; #Moon Lord; +Sick Throw; Achievement; Terrarian; +Meowmere; ; #Moon Lord; +Star Wrath; ; #Moon Lord; +Exodium Cluster; Calamity; Moon Lord & @pickaxe(225); +Normality Relocator; Calamity; Ancient Manipulator & Rod of Discord & Exodium Cluster & Fragment; +Unholy Essence; Calamity; Moon Lord | #Providence, the Profaned Goddess; +Phantoplasm; Calamity; Moon Lord & (Wall of Flesh | Dungeon); // TODO Check +Eldritch Soul Artifact; Calamity; Exodium Cluster & Navyplate & Phantoplasm; + +// Profaned Guardians +Profaned Guardians; Calamity | Location | Item; Ancient Manipulator & Unholy Essence & Luminite Bar; + +// Dragonfolly +The Dragonfolly; Calamity | Location | Item; Ancient Manipulator & Unholy Essence & Luminite Bar; +Effulgent Feather; Calamity; Moon Lord | #The Dragonfolly; + +// Providence, the Profaned Goddess +Providence, the Profaned Goddess; Calamity | Location | Item | Goal; #Profaned Guardians; +# providence_the_profaned_goddess +Divine Geode; Calamity; #Providence, the Profaned Goddess; +Profaned Soul Artifact; Calamity | Minions(1); Exodium Cluster & Havocplate & Divine Geode; +Rune of Kos; Calamity; #Providence, the Profaned Goddess; +Uelibloom Ore; Calamity; Providence, the Profaned Goddess; +Uelibloom Bar; Calamity; Hardmode Forge & Uelibloom Ore; +Grax; Calamity | Hammer(110); Ancient Manipulator & Inferna Cutter & Luminite Hamaxe & Uelibloom Bar; +Blossom Pickaxe; Calamity | Pickaxe(250); Ancient Manipulator & Uelibloom Bar; +Voltage Regulation System; Calamity; Ancient Manipulator & Mysterious Circuitry & Dubious Plating & Uelibloom Bar & Luminite Bar & Advanced Display; +Necromantic Geode; Calamity; #Ravager & Providence, the Profaned Goddess; + +// Sentinels of the Devourer +Storm Weaver; Calamity | Location | Item; Rune of Kos; +Armored Shell; Calamity; #Storm Weaver; +Ceaseless Void; Calamity | Location | Item; Rune of Kos; +Dark Plasma; Calamity; #Ceaseless Void; +Signus, Envoy of the Devourer; Calamity | Location | Item; Rune of Kos; +Twisting Nether; Calamity; #Signus, Envoy of the Devourer; + +// Polterghast +Polterghast; Calamity | Location | Item; Dungeon & ((Ancient Manipulator & Phantoplasm) | Moon Lord); +Colossal Squid; Calamity | Location | Item; +Reaper Shark; Calamity | Location | Item; +Eidolon Wyrm; Calamity | Location | Item; + +// The Old Duke +Mauler; Calamity | Location | Item; #Acid Rain Tier 3; +Nuclear Terror; Calamity | Location | Item; #Acid Rain Tier 3; +Acid Rain Tier 3; Calamity | Location | Item; #Acid Rain Tier 1 & Polterghast; // TODO Check +The Old Duke; Calamity | Location | Item; #Acid Rain Tier 3 | (Bug Net & Moon Lord) | (Amidias & The Old Duke); + +// The Devourer of Gods +The Devourer of Gods; Calamity | Location | Item | Goal; Ancient Manipulator & ((Armored Shell & Twisting Nether & Dark Plasma) | (Luminite Bar & Galactica Singularity & Phantoplasm)); +# the_devourer_of_gods +Cosmilite Bar; Calamity; #The Devourer of Gods; +Cosmic Anvil; Calamity; Ancient Manipulator & Hardmode Anvil & Cosmilite Bar & Luminite Bar & Galactica Singularity & Exodium Cluster; +Nightmare Fuel; Calamity; Pumpkin Moon & The Devourer of Gods; +Endothermic Energy; Calamity; Frost Moon & The Devourer of Gods; +Darksun Fragment; Calamity; Solar Eclipse & The Devourer of Gods; +Dark Sun Ring; Calamity; Cosmic Anvil & Uelibloom Bar & Darksun Fragment; +Ascendant Spirit Essence; Calamity; Ancient Manipulator & Phantoplasm & Nightmare Fuel & Endothermic Energy & Darksun Fragment; + +// Yharon, Dragon of Rebirth +Yharon, Dragon of Rebirth; Calamity | Location | Item | Goal; Ancient Manipulator & Effulgent Feather & Life Alloy; +# yharon_dragon_of_rebirth +Yharon Soul Fragment; Calamity; #Yharon, Dragon of Rebirth; +Auric Ore; Calamity; Yharon, Dragon of Rebirth & @pickaxe(250); +Auric Bar; Calamity; Cosmic Anvil & Auric Ore & Yharon Soul Fragment; +Zenith; Location | Item(Has Zenith) | Goal; Hardmode Anvil & Terra Blade & Meowmere & Star Wrath & Influx Waver & The Horseman's Blade & Seedler & Starfury & Bee Keeper & Enchanted Sword & Copper Shortsword & (~@calamity | Auric Bar); +# zenith + +// Exo Mechs +Auric Quantum Cooling Cell; Calamity; Cosmic Anvil & Auric Bar & Mysterious Circuitry & Dubious Plating & Endothermic Energy & Core of Eleum & Voltage Regulation System; +Exo Mechs; Calamity | Location | Item | Final Boss; Codebreaker Base & Decryption Computer & Auric Quantum Cooling Cell; +Supreme Witch, Calamitas; Calamity | Location | Item | Final Boss; Cosmic Anvil & Brimstone Slag & Auric Bar & Core of Calamity & Ashes of Calamity; +# calamity_final_bosses +Exo Prism; Calamity; #Exo Mechs; +Draedon's Forge; Calamity; Cosmic Anvil & Hardmode Forge & Tinkerer's Workshop & Ancient Manipulator & Auric Bar & Exo Prism & Ascendant Spirit Essence; + +// Supreme Witch, Calamitas +Ashes of Annihilation; Calamity; #Supreme Witch, Calamitas; +Shadowspec Bar; Calamity; Draedon's Forge & Auric Bar & Exo Prism & Ashes of Annihilation; +Crystyl Crusher; Calamity | Pickaxe(1000); Draedon's Forge & Luminite Pickaxe & Blossom Pickaxe & Shadowspec Bar; +Angelic Alliance; Calamity | Minions(2); Draedon's Forge & Hallowed Armor & Paladin's Shield & True Excalibur & Cross Necklace & Shadowspec Bar; + +// Adult Eidolon Wyrm; +Adult Eidolon Wyrm; Calamity | Location | Item | Goal; Rod of Discord | Normality Relocator; +# adult_eidolon_wyrm diff --git a/worlds/terraria/__init__.py b/worlds/terraria/__init__.py new file mode 100644 index 00000000..a56f4760 --- /dev/null +++ b/worlds/terraria/__init__.py @@ -0,0 +1,342 @@ +# Look at `Rules.dsv` first to get an idea for how this works + +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, + 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 options + + +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() + option_definitions = options + + # data_version is used to signal that items, locations or their names + # changed. Set this to 0 during development so other games' clients do not + # cache any texts, then increase by 1 for each release that makes changes. + data_version = 2 + + item_name_to_id = item_name_to_id + location_name_to_id = location_name_to_id + + # Turn into an option when calamity is supported in the mod + calamity = 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.multiworld.goal[self.player].value] + ter_goals = {} + goal_items = set() + for location in goal_locations: + _, flags, _, _ = rules[rule_indices[location]] + item = flags.get("Item") or f"Post-{location}" + ter_goals[item] = location + goal_items.add(item) + + achievements = self.multiworld.achievements[self.player].value + location_count = 0 + locations = [] + for rule, flags, _, _ in rules[:goal]: + if ( + (not self.calamity and "Calamity" in flags) + or (achievements < 1 and "Achievement" in flags) + or (achievements < 2 and "Grindy" in flags) + or (achievements < 3 and "Fishing" in flags) + or ( + rule == "Zenith" and self.multiworld.goal[self.player].value != 11 + ) # Bad hardcoding + ): + continue + if "Location" in flags or ("Achievement" in flags and achievements >= 1): + # Location + location_count += 1 + locations.append(rule) + elif ( + "Achievement" not in flags + and "Location" not in flags + and "Item" not in flags + ): + # Event + locations.append(rule) + + item_count = 0 + items = [] + for rule, flags, _, _ in rules[:goal]: + if not self.calamity and "Calamity" in flags: + continue + if "Item" in flags: + # Item + item_count += 1 + if rule not in goal_locations: + items.append(rule) + elif ( + "Achievement" not in flags + and "Location" not in flags + and "Item" not in flags + ): + # Event + items.append(rule) + + extra_checks = self.multiworld.fill_extra_checks_with[self.player].value + ordered_rewards = [ + reward + for reward in labels["ordered"] + if self.calamity or "Calamity" not in rewards[reward] + ] + while extra_checks == 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.calamity or "Calamity" not in rewards[reward] + ] + self.multiworld.random.shuffle(random_rewards) + while extra_checks == 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: + _, flags, _, _ = rules[rule_index] + if "Item" in flags: + name = 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: + _, flags, _, _ = rules[rule_indices[location]] + if "Location" not in flags and "Achievement" not in 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, + sign: bool, + ty: int, + condition: Union[str, Tuple[Union[bool, None], list]], + arg: Union[str, int, None], + ) -> bool: + if ty == COND_ITEM: + _, flags, _, _ = rules[rule_indices[condition]] + if "Item" in flags: + name = flags.get("Item") or f"Post-{condition}" + else: + name = condition + + return sign == state.has(name, self.player) + elif ty == COND_LOC: + _, _, operator, conditions = rules[rule_indices[condition]] + return sign == self.check_conditions(state, operator, conditions) + elif ty == COND_FN: + if condition == "npc": + if type(arg) 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 >= arg: + return sign + + return not sign + elif condition == "calamity": + return sign == self.calamity + elif condition == "pickaxe": + if type(arg) is not int: + raise Exception("@pickaxe requires an integer argument") + + for pickaxe, power in pickaxes.items(): + if power >= arg and state.has(pickaxe, self.player): + return sign + + return not sign + elif condition == "hammer": + if type(arg) is not int: + raise Exception("@hammer requires an integer argument") + + for hammer, power in hammers.items(): + if power >= arg and state.has(hammer, self.player): + return sign + + return not sign + elif condition == "mech_boss": + if type(arg) 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 >= arg: + return sign + + return not sign + elif condition == "minions": + if type(arg) 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 >= arg: + return sign + + for accessory, minions in accessory_minions.items(): + if state.has(accessory, self.player): + minion_count += minions + if minion_count >= arg: + return sign + + return not sign + else: + raise Exception(f"Unknown function {condition}") + elif ty == COND_GROUP: + operator, conditions = condition + return 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): + _, _, operator, conditions = rules[rule_indices[location]] + return self.check_conditions(state, operator, 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.multiworld.death_link[self.player]), + } diff --git a/worlds/terraria/docs/en_Terraria.md b/worlds/terraria/docs/en_Terraria.md new file mode 100644 index 00000000..b0a8529b --- /dev/null +++ b/worlds/terraria/docs/en_Terraria.md @@ -0,0 +1,19 @@ +# Terraria + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Boss/event flags are randomized. So, defeating Empress of Light could give you Post-Skeletron, which allows you to enter +the Dungeon, for example. In your player settings, you may also add item rewards and achievements to the pool. + +## What Terraria items can appear in other players' worlds? + +Boss/event flags, and item rewards if enabled. + +## What does another world's item look like in Terraria? + +You won't see the items in Terraria, but you will receive a chat message, telling you what item you sent to whom. diff --git a/worlds/terraria/docs/setup_en.md b/worlds/terraria/docs/setup_en.md new file mode 100644 index 00000000..008d069d --- /dev/null +++ b/worlds/terraria/docs/setup_en.md @@ -0,0 +1,52 @@ +# Terraria for Archipelago Setup Guide + +## Required Software + +Download and install [Terraria](https://store.steampowered.com/app/105600/Terraria/) +and [TModLoader](https://store.steampowered.com/app/1281930/tModLoader/) on Steam + +## Installing the Archipelago Mod + +Subscribe to [the mod](https://steamcommunity.com/sharedfiles/filedetails/?id=2922217554) on Steam. + +This mod might not work with mods that significantly alter progression or vanilla features. It is +highly recommended to use utility mods and features to speed up gameplay, such as: + +- Journey Mode +- Ore Excavator +- Magic Storage +- Alchemist NPC Lite +- Reduced Grinding + +## Configuring your YAML File + +### What is a YAML and why do I need one? + +You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here +on the Archipelago website to learn about why Archipelago uses YAML files and what they're for. + +### Where do I get a YAML? + +You can use the [game settings page for Terraria](/games/Terraria/player-settings) here +on the Archipelago website to generate a YAML using a graphical interface. + +## Joining an Archipelago Game in Terraria + +1. Launch TModLoader +2. In Workshop > Manage Mods, edit Archipelago Randomizer's settings + - "Name" should be the player name you set when creating your YAML file + - "Port" should be the port number associated with the Archipelago server. It will be a 4 or 5 + digit number. + - If you're not hosting your game on the Archipelago website, change "Address" to the server's + URL or IP address +3. Create a new character and world as normal (or use an existing one if you prefer). Terraria is +usually significantly more difficult with this mod, so it is recommended to choose a lower +difficulty than you normally would. +4. Open the world in single player or multiplayer +5. When you're ready, open chat, and enter `/apstart` to start the game. + +## Commands + +While playing Archipelago, you can interact with the server using the commands listed in the +[commands guide](/tutorial/Archipelago/commands/en). To send a command, open chat, and enter `/ap`, +followed by the command you want to send. For example, `/ap !help`.