mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 12:11:33 -06:00

* 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>
783 lines
24 KiB
Python
783 lines
24 KiB
Python
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
|
|
|
|
|
|
class Condition:
|
|
def __init__(
|
|
self,
|
|
# True = positive, False = negative
|
|
sign: bool,
|
|
# See the `COND_*` constants
|
|
type: int,
|
|
# Condition name or list
|
|
condition: Union[str, Tuple[Union[bool, None], List["Condition"]]],
|
|
argument: Union[str, int, None],
|
|
):
|
|
self.sign = sign
|
|
self.type = type
|
|
self.condition = condition
|
|
self.argument = argument
|
|
|
|
|
|
class Rule:
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
# Name to arg
|
|
flags: Dict[str, Union[str, int, None]],
|
|
# True = or, False = and, None = N/A
|
|
operator: Union[bool, None],
|
|
conditions: List[Condition],
|
|
):
|
|
self.name = name
|
|
self.flags = flags
|
|
self.operator = operator
|
|
self.conditions = conditions
|
|
|
|
|
|
def validate_conditions(
|
|
rule: str,
|
|
rule_indices: dict,
|
|
conditions: List[Condition],
|
|
):
|
|
for condition in conditions:
|
|
if condition.type == COND_ITEM:
|
|
if condition.condition not in rule_indices:
|
|
raise Exception(
|
|
f"item `{condition.condition}` in `{rule}` is not defined"
|
|
)
|
|
elif condition.type == COND_LOC:
|
|
if condition.condition not in rule_indices:
|
|
raise Exception(
|
|
f"location `{condition.condition}` in `{rule}` is not defined"
|
|
)
|
|
elif condition.type == COND_FN:
|
|
if condition.condition not in {
|
|
"npc",
|
|
"calamity",
|
|
"grindy",
|
|
"pickaxe",
|
|
"hammer",
|
|
"mech_boss",
|
|
"minions",
|
|
"getfixedboi",
|
|
}:
|
|
raise Exception(
|
|
f"function `{condition.condition}` in `{rule}` is not defined"
|
|
)
|
|
elif condition.type == COND_GROUP:
|
|
_, conditions = condition.condition
|
|
validate_conditions(rule, rule_indices, conditions)
|
|
|
|
|
|
def mark_progression(
|
|
conditions: List[Condition],
|
|
progression: Set[str],
|
|
rules: list,
|
|
rule_indices: dict,
|
|
loc_to_item: dict,
|
|
):
|
|
for condition in conditions:
|
|
if condition.type == COND_ITEM:
|
|
prog = condition.condition in progression
|
|
progression.add(loc_to_item[condition.condition])
|
|
rule = rules[rule_indices[condition.condition]]
|
|
if (
|
|
not prog
|
|
and "Achievement" not in rule.flags
|
|
and "Location" not in rule.flags
|
|
and "Item" not in rule.flags
|
|
):
|
|
mark_progression(
|
|
rule.conditions, progression, rules, rule_indices, loc_to_item
|
|
)
|
|
elif condition.type == COND_LOC:
|
|
|
|
mark_progression(
|
|
rules[rule_indices[condition.condition]].conditions,
|
|
progression,
|
|
rules,
|
|
rule_indices,
|
|
loc_to_item,
|
|
)
|
|
elif condition.type == COND_GROUP:
|
|
_, conditions = condition.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[Rule],
|
|
# 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(Condition(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(Condition(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(Condition(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(Condition(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(Condition(sign, COND_FN, function, None))
|
|
pos = END
|
|
elif id == AND:
|
|
conditions.append(Condition(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(Condition(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(Condition(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(Condition(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(Condition(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(Rule(name, flags, operator, conditions))
|
|
|
|
for flag in flags:
|
|
if flag not in {
|
|
"Location",
|
|
"Item",
|
|
"Goal",
|
|
"Early",
|
|
"Achievement",
|
|
"Grindy",
|
|
"Fishing",
|
|
"Npc",
|
|
"Pickaxe",
|
|
"Hammer",
|
|
"Minions",
|
|
"Armor Minions",
|
|
"Mech Boss",
|
|
"Final Boss",
|
|
"Getfixedboi",
|
|
"Not Getfixedboi",
|
|
"Calamity",
|
|
"Not Calamity",
|
|
"Not Calamity Getfixedboi",
|
|
}:
|
|
raise Exception(
|
|
f"rule `{name}` on line `{line + 1}` has unrecognized flag `{flag}`"
|
|
)
|
|
|
|
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("Armor Minions")) 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 rule in rules:
|
|
if "Goal" in rule.flags:
|
|
if (name := rule.flags.get("Goal")) is not None:
|
|
goal_name = name
|
|
else:
|
|
goal_name = (
|
|
rule.name.translate(str.maketrans("", "", string.punctuation))
|
|
.replace(" ", "_")
|
|
.lower()
|
|
)
|
|
|
|
_, items = goals[goal_indices[goal_name]]
|
|
items.add(rule.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 rule in rules:
|
|
validate_conditions(rule.name, rule_indices, rule.conditions)
|
|
|
|
for rule in rules:
|
|
prog = False
|
|
if (
|
|
"Npc" in rule.flags
|
|
or "Goal" in rule.flags
|
|
or "Pickaxe" in rule.flags
|
|
or "Hammer" in rule.flags
|
|
or "Mech Boss" in rule.flags
|
|
or "Final Boss" in rule.flags
|
|
or "Minions" in rule.flags
|
|
or "Armor Minions" in rule.flags
|
|
):
|
|
progression.add(loc_to_item[rule.name])
|
|
prog = True
|
|
if prog or "Location" in rule.flags or "Achievement" in rule.flags:
|
|
mark_progression(
|
|
rule.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
|
|
next_id += 1
|
|
|
|
location_name_to_id = {}
|
|
|
|
for rule in rules:
|
|
if "Location" in rule.flags or "Achievement" in rule.flags:
|
|
if rule.name in location_name_to_id:
|
|
raise Exception(f"location `{rule.name}` shadows a previous location")
|
|
location_name_to_id[rule.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()
|