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>
This commit is contained in:
@@ -157,24 +157,57 @@ 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[
|
||||
Tuple[
|
||||
bool, int, Union[str, Tuple[Union[bool, None], list]], Union[str, int, None]
|
||||
]
|
||||
],
|
||||
conditions: List[Condition],
|
||||
):
|
||||
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 {
|
||||
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",
|
||||
@@ -182,43 +215,48 @@ def validate_conditions(
|
||||
"hammer",
|
||||
"mech_boss",
|
||||
"minions",
|
||||
"getfixedboi",
|
||||
}:
|
||||
raise Exception(f"function `{condition}` in `{rule}` is not defined")
|
||||
elif type == COND_GROUP:
|
||||
_, conditions = condition
|
||||
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[
|
||||
Tuple[
|
||||
bool, int, Union[str, Tuple[Union[bool, None], list]], Union[str, int, None]
|
||||
]
|
||||
],
|
||||
conditions: List[Condition],
|
||||
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]]
|
||||
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 flags
|
||||
and "Location" not in flags
|
||||
and "Item" not in flags
|
||||
and "Achievement" not in rule.flags
|
||||
and "Location" not in rule.flags
|
||||
and "Item" not in rule.flags
|
||||
):
|
||||
mark_progression(
|
||||
conditions, progression, rules, rule_indices, loc_to_item
|
||||
rule.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
|
||||
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)
|
||||
|
||||
|
||||
@@ -226,29 +264,7 @@ 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],
|
||||
]
|
||||
],
|
||||
]
|
||||
],
|
||||
List[Rule],
|
||||
# Rule to rule index
|
||||
Dict[str, int],
|
||||
# Label to rewards
|
||||
@@ -379,7 +395,7 @@ def read_data() -> Tuple[
|
||||
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))
|
||||
conditions.append(Condition(sign, COND_ITEM, token, None))
|
||||
sign = True
|
||||
pos = POST_COND
|
||||
elif id == HASH:
|
||||
@@ -424,14 +440,14 @@ def read_data() -> Tuple[
|
||||
)
|
||||
condition = operator, conditions
|
||||
sign, operator, conditions = outer.pop()
|
||||
conditions.append((sign, COND_GROUP, condition, None))
|
||||
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((sign, COND_ITEM, token, None))
|
||||
conditions.append(Condition(sign, COND_ITEM, token, None))
|
||||
sign = True
|
||||
pos = POST_COND
|
||||
elif id == HASH:
|
||||
@@ -449,7 +465,7 @@ def read_data() -> Tuple[
|
||||
unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
|
||||
elif pos == LOC:
|
||||
if id == IDENT:
|
||||
conditions.append((sign, COND_LOC, token, None))
|
||||
conditions.append(Condition(sign, COND_LOC, token, None))
|
||||
sign = True
|
||||
pos = POST_COND
|
||||
else:
|
||||
@@ -464,10 +480,10 @@ def read_data() -> Tuple[
|
||||
if id == LPAREN:
|
||||
pos = FN_ARG
|
||||
elif id == SEMI:
|
||||
conditions.append((sign, COND_FN, function, None))
|
||||
conditions.append(Condition(sign, COND_FN, function, None))
|
||||
pos = END
|
||||
elif id == AND:
|
||||
conditions.append((sign, COND_FN, function, None))
|
||||
conditions.append(Condition(sign, COND_FN, function, None))
|
||||
sign = True
|
||||
if operator is True:
|
||||
raise Exception(
|
||||
@@ -476,7 +492,7 @@ def read_data() -> Tuple[
|
||||
operator = False
|
||||
pos = COND
|
||||
elif id == OR:
|
||||
conditions.append((sign, COND_FN, function, None))
|
||||
conditions.append(Condition(sign, COND_FN, function, None))
|
||||
sign = True
|
||||
if operator is False:
|
||||
raise Exception(
|
||||
@@ -485,21 +501,21 @@ def read_data() -> Tuple[
|
||||
operator = True
|
||||
pos = COND
|
||||
elif id == RPAREN:
|
||||
conditions.append((sign, COND_FN, function, None))
|
||||
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((sign, COND_GROUP, condition, None))
|
||||
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((sign, COND_FN, function, token))
|
||||
conditions.append(Condition(sign, COND_FN, function, token))
|
||||
sign = True
|
||||
pos = FN_ARG_END
|
||||
else:
|
||||
@@ -527,7 +543,33 @@ def read_data() -> Tuple[
|
||||
f"rule `{name}` on line `{line + 1}` shadows a previous rule"
|
||||
)
|
||||
rule_indices[name] = len(rules)
|
||||
rules.append((name, flags, operator, conditions))
|
||||
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}"
|
||||
@@ -558,7 +600,7 @@ def read_data() -> Tuple[
|
||||
final_bosses.append(flags["Item"] or f"Post-{name}")
|
||||
final_boss_loc.append(name)
|
||||
|
||||
if (minions := flags.get("ArmorMinions")) is not None:
|
||||
if (minions := flags.get("Armor Minions")) is not None:
|
||||
armor_minions[name] = minions
|
||||
|
||||
if (minions := flags.get("Minions")) is not None:
|
||||
@@ -572,16 +614,19 @@ def read_data() -> Tuple[
|
||||
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))
|
||||
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.add(name)
|
||||
)
|
||||
|
||||
_, 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)
|
||||
@@ -589,24 +634,27 @@ def read_data() -> Tuple[
|
||||
_, 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 rule in rules:
|
||||
validate_conditions(rule.name, rule_indices, rule.conditions)
|
||||
|
||||
for name, flags, _, conditions in rules:
|
||||
for rule 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
|
||||
"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[name])
|
||||
progression.add(loc_to_item[rule.name])
|
||||
prog = True
|
||||
if prog or "Location" in flags or "Achievement" in flags:
|
||||
mark_progression(conditions, progression, rules, rule_indices, loc_to_item)
|
||||
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
|
||||
@@ -685,16 +733,15 @@ def read_data() -> Tuple[
|
||||
next_id += 1
|
||||
|
||||
item_name_to_id["Reward: Coins"] = next_id
|
||||
item_name_to_id["Victory"] = next_id + 1
|
||||
next_id += 2
|
||||
next_id += 1
|
||||
|
||||
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
|
||||
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 (
|
||||
|
||||
Reference in New Issue
Block a user