Lingo: Add option to prevent shuffling postgame (#3350)
* Lingo: Add option to prevent shuffling postgame * Allow roof access on door shuffle * Fix broken unit test * Simplified THE END edge case * Revert unnecessary change * Review comments * Fix mastery unit test * Update generated.dat * Added player's name to error message
This commit is contained in:
parent
878d5141ce
commit
e714d2e129
|
@ -3,7 +3,7 @@ Archipelago init file for Lingo
|
||||||
"""
|
"""
|
||||||
from logging import warning
|
from logging import warning
|
||||||
|
|
||||||
from BaseClasses import Item, ItemClassification, Tutorial
|
from BaseClasses import CollectionState, Item, ItemClassification, Tutorial
|
||||||
from Options import OptionError
|
from Options import OptionError
|
||||||
from worlds.AutoWorld import WebWorld, World
|
from worlds.AutoWorld import WebWorld, World
|
||||||
from .datatypes import Room, RoomEntrance
|
from .datatypes import Room, RoomEntrance
|
||||||
|
@ -68,6 +68,37 @@ class LingoWorld(World):
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
create_regions(self)
|
create_regions(self)
|
||||||
|
|
||||||
|
if not self.options.shuffle_postgame:
|
||||||
|
state = CollectionState(self.multiworld)
|
||||||
|
state.collect(LingoItem("Prevent Victory", ItemClassification.progression, None, self.player), True)
|
||||||
|
|
||||||
|
# Note: relies on the assumption that real_items is a definitive list of real progression items in this
|
||||||
|
# world, and is not modified after being created.
|
||||||
|
for item in self.player_logic.real_items:
|
||||||
|
state.collect(self.create_item(item), True)
|
||||||
|
|
||||||
|
# Exception to the above: a forced good item is not considered a "real item", but needs to be here anyway.
|
||||||
|
if self.player_logic.forced_good_item != "":
|
||||||
|
state.collect(self.create_item(self.player_logic.forced_good_item), True)
|
||||||
|
|
||||||
|
all_locations = self.multiworld.get_locations(self.player)
|
||||||
|
state.sweep_for_events(locations=all_locations)
|
||||||
|
|
||||||
|
unreachable_locations = [location for location in all_locations
|
||||||
|
if not state.can_reach_location(location.name, self.player)]
|
||||||
|
|
||||||
|
for location in unreachable_locations:
|
||||||
|
if location.name in self.player_logic.event_loc_to_item.keys():
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.player_logic.real_locations.remove(location.name)
|
||||||
|
location.parent_region.locations.remove(location)
|
||||||
|
|
||||||
|
if len(self.player_logic.real_items) > len(self.player_logic.real_locations):
|
||||||
|
raise OptionError(f"{self.player_name}'s Lingo world does not have enough locations to fit the number"
|
||||||
|
f" of required items without shuffling the postgame. Either enable postgame"
|
||||||
|
f" shuffling, or choose different options.")
|
||||||
|
|
||||||
def create_items(self):
|
def create_items(self):
|
||||||
pool = [self.create_item(name) for name in self.player_logic.real_items]
|
pool = [self.create_item(name) for name in self.player_logic.real_items]
|
||||||
|
|
||||||
|
|
|
@ -879,6 +879,8 @@
|
||||||
panel: DRAWL + RUNS
|
panel: DRAWL + RUNS
|
||||||
- room: Owl Hallway
|
- room: Owl Hallway
|
||||||
panel: READS + RUST
|
panel: READS + RUST
|
||||||
|
- room: Ending Area
|
||||||
|
panel: THE END
|
||||||
paintings:
|
paintings:
|
||||||
- id: eye_painting
|
- id: eye_painting
|
||||||
disable: True
|
disable: True
|
||||||
|
@ -2322,7 +2324,7 @@
|
||||||
orientation: east
|
orientation: east
|
||||||
- id: hi_solved_painting
|
- id: hi_solved_painting
|
||||||
orientation: west
|
orientation: west
|
||||||
Orange Tower Seventh Floor:
|
Ending Area:
|
||||||
entrances:
|
entrances:
|
||||||
Orange Tower Sixth Floor:
|
Orange Tower Sixth Floor:
|
||||||
room: Orange Tower
|
room: Orange Tower
|
||||||
|
@ -2334,6 +2336,18 @@
|
||||||
check: True
|
check: True
|
||||||
tag: forbid
|
tag: forbid
|
||||||
non_counting: True
|
non_counting: True
|
||||||
|
location_name: Orange Tower Seventh Floor - THE END
|
||||||
|
doors:
|
||||||
|
End:
|
||||||
|
event: True
|
||||||
|
panels:
|
||||||
|
- THE END
|
||||||
|
Orange Tower Seventh Floor:
|
||||||
|
entrances:
|
||||||
|
Ending Area:
|
||||||
|
room: Ending Area
|
||||||
|
door: End
|
||||||
|
panels:
|
||||||
THE MASTER:
|
THE MASTER:
|
||||||
# We will set up special rules for this in code.
|
# We will set up special rules for this in code.
|
||||||
id: Countdown Panels/Panel_master_master
|
id: Countdown Panels/Panel_master_master
|
||||||
|
|
Binary file not shown.
|
@ -272,8 +272,9 @@ panels:
|
||||||
PAINTING (4): 445081
|
PAINTING (4): 445081
|
||||||
PAINTING (5): 445082
|
PAINTING (5): 445082
|
||||||
ROOM: 445083
|
ROOM: 445083
|
||||||
Orange Tower Seventh Floor:
|
Ending Area:
|
||||||
THE END: 444620
|
THE END: 444620
|
||||||
|
Orange Tower Seventh Floor:
|
||||||
THE MASTER: 444621
|
THE MASTER: 444621
|
||||||
MASTERY: 444622
|
MASTERY: 444622
|
||||||
Behind A Smile:
|
Behind A Smile:
|
||||||
|
|
|
@ -194,6 +194,11 @@ class EarlyColorHallways(Toggle):
|
||||||
display_name = "Early Color Hallways"
|
display_name = "Early Color Hallways"
|
||||||
|
|
||||||
|
|
||||||
|
class ShufflePostgame(Toggle):
|
||||||
|
"""When off, locations that could not be reached without also reaching your victory condition are removed."""
|
||||||
|
display_name = "Shuffle Postgame"
|
||||||
|
|
||||||
|
|
||||||
class TrapPercentage(Range):
|
class TrapPercentage(Range):
|
||||||
"""Replaces junk items with traps, at the specified rate."""
|
"""Replaces junk items with traps, at the specified rate."""
|
||||||
display_name = "Trap Percentage"
|
display_name = "Trap Percentage"
|
||||||
|
@ -263,6 +268,7 @@ class LingoOptions(PerGameCommonOptions):
|
||||||
mastery_achievements: MasteryAchievements
|
mastery_achievements: MasteryAchievements
|
||||||
level_2_requirement: Level2Requirement
|
level_2_requirement: Level2Requirement
|
||||||
early_color_hallways: EarlyColorHallways
|
early_color_hallways: EarlyColorHallways
|
||||||
|
shuffle_postgame: ShufflePostgame
|
||||||
trap_percentage: TrapPercentage
|
trap_percentage: TrapPercentage
|
||||||
trap_weights: TrapWeights
|
trap_weights: TrapWeights
|
||||||
puzzle_skip_percentage: PuzzleSkipPercentage
|
puzzle_skip_percentage: PuzzleSkipPercentage
|
||||||
|
|
|
@ -19,22 +19,25 @@ class AccessRequirements:
|
||||||
doors: Set[RoomAndDoor]
|
doors: Set[RoomAndDoor]
|
||||||
colors: Set[str]
|
colors: Set[str]
|
||||||
the_master: bool
|
the_master: bool
|
||||||
|
postgame: bool
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.rooms = set()
|
self.rooms = set()
|
||||||
self.doors = set()
|
self.doors = set()
|
||||||
self.colors = set()
|
self.colors = set()
|
||||||
self.the_master = False
|
self.the_master = False
|
||||||
|
self.postgame = False
|
||||||
|
|
||||||
def merge(self, other: "AccessRequirements"):
|
def merge(self, other: "AccessRequirements"):
|
||||||
self.rooms |= other.rooms
|
self.rooms |= other.rooms
|
||||||
self.doors |= other.doors
|
self.doors |= other.doors
|
||||||
self.colors |= other.colors
|
self.colors |= other.colors
|
||||||
self.the_master |= other.the_master
|
self.the_master |= other.the_master
|
||||||
|
self.postgame |= other.postgame
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})," \
|
return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors}," \
|
||||||
f" the_master={self.the_master}"
|
f" the_master={self.the_master}, postgame={self.postgame})"
|
||||||
|
|
||||||
|
|
||||||
class PlayerLocation(NamedTuple):
|
class PlayerLocation(NamedTuple):
|
||||||
|
@ -190,16 +193,6 @@ class LingoPlayerLogic:
|
||||||
if color_shuffle:
|
if color_shuffle:
|
||||||
self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR]
|
self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR]
|
||||||
|
|
||||||
# Create events for each achievement panel, so that we can determine when THE MASTER is accessible.
|
|
||||||
for room_name, room_data in PANELS_BY_ROOM.items():
|
|
||||||
for panel_name, panel_data in room_data.items():
|
|
||||||
if panel_data.achievement:
|
|
||||||
access_req = AccessRequirements()
|
|
||||||
access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world))
|
|
||||||
access_req.rooms.add(room_name)
|
|
||||||
|
|
||||||
self.mastery_reqs.append(access_req)
|
|
||||||
|
|
||||||
# Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need
|
# Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need
|
||||||
# to prevent the actual victory condition from becoming a check.
|
# to prevent the actual victory condition from becoming a check.
|
||||||
self.mastery_location = "Orange Tower Seventh Floor - THE MASTER"
|
self.mastery_location = "Orange Tower Seventh Floor - THE MASTER"
|
||||||
|
@ -207,7 +200,7 @@ class LingoPlayerLogic:
|
||||||
|
|
||||||
if victory_condition == VictoryCondition.option_the_end:
|
if victory_condition == VictoryCondition.option_the_end:
|
||||||
self.victory_condition = "Orange Tower Seventh Floor - THE END"
|
self.victory_condition = "Orange Tower Seventh Floor - THE END"
|
||||||
self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, [], world)
|
self.add_location("Ending Area", "The End (Solved)", None, [], world)
|
||||||
self.event_loc_to_item["The End (Solved)"] = "Victory"
|
self.event_loc_to_item["The End (Solved)"] = "Victory"
|
||||||
elif victory_condition == VictoryCondition.option_the_master:
|
elif victory_condition == VictoryCondition.option_the_master:
|
||||||
self.victory_condition = "Orange Tower Seventh Floor - THE MASTER"
|
self.victory_condition = "Orange Tower Seventh Floor - THE MASTER"
|
||||||
|
@ -231,6 +224,16 @@ class LingoPlayerLogic:
|
||||||
[RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world)
|
[RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world)
|
||||||
self.event_loc_to_item["PILGRIM (Solved)"] = "Victory"
|
self.event_loc_to_item["PILGRIM (Solved)"] = "Victory"
|
||||||
|
|
||||||
|
# Create events for each achievement panel, so that we can determine when THE MASTER is accessible.
|
||||||
|
for room_name, room_data in PANELS_BY_ROOM.items():
|
||||||
|
for panel_name, panel_data in room_data.items():
|
||||||
|
if panel_data.achievement:
|
||||||
|
access_req = AccessRequirements()
|
||||||
|
access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world))
|
||||||
|
access_req.rooms.add(room_name)
|
||||||
|
|
||||||
|
self.mastery_reqs.append(access_req)
|
||||||
|
|
||||||
# Create groups of counting panel access requirements for the LEVEL 2 check.
|
# Create groups of counting panel access requirements for the LEVEL 2 check.
|
||||||
self.create_panel_hunt_events(world)
|
self.create_panel_hunt_events(world)
|
||||||
|
|
||||||
|
@ -470,6 +473,11 @@ class LingoPlayerLogic:
|
||||||
if panel == "THE MASTER":
|
if panel == "THE MASTER":
|
||||||
access_reqs.the_master = True
|
access_reqs.the_master = True
|
||||||
|
|
||||||
|
# Evil python magic (so sayeth NewSoupVi): this checks victory_condition against the panel's location name
|
||||||
|
# override if it exists, or the auto-generated location name if it's None.
|
||||||
|
if self.victory_condition == (panel_object.location_name or f"{room} - {panel}"):
|
||||||
|
access_reqs.postgame = True
|
||||||
|
|
||||||
self.panel_reqs[room][panel] = access_reqs
|
self.panel_reqs[room][panel] = access_reqs
|
||||||
|
|
||||||
return self.panel_reqs[room][panel]
|
return self.panel_reqs[room][panel]
|
||||||
|
|
|
@ -62,6 +62,9 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir
|
||||||
if access.the_master and not lingo_can_use_mastery_location(state, world):
|
if access.the_master and not lingo_can_use_mastery_location(state, world):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if access.postgame and state.has("Prevent Victory", world.player):
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@ class TestMasteryWhenVictoryIsTheEnd(LingoTestBase):
|
||||||
options = {
|
options = {
|
||||||
"mastery_achievements": "22",
|
"mastery_achievements": "22",
|
||||||
"victory_condition": "the_end",
|
"victory_condition": "the_end",
|
||||||
"shuffle_colors": "true"
|
"shuffle_colors": "true",
|
||||||
|
"shuffle_postgame": "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_requirement(self):
|
def test_requirement(self):
|
||||||
|
@ -43,7 +44,8 @@ class TestMasteryBlocksDependents(LingoTestBase):
|
||||||
options = {
|
options = {
|
||||||
"mastery_achievements": "24",
|
"mastery_achievements": "24",
|
||||||
"shuffle_colors": "true",
|
"shuffle_colors": "true",
|
||||||
"location_checks": "insanity"
|
"location_checks": "insanity",
|
||||||
|
"victory_condition": "level_2",
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_requirement(self):
|
def test_requirement(self):
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
from . import LingoTestBase
|
||||||
|
|
||||||
|
|
||||||
|
class TestPostgameVanillaTheEnd(LingoTestBase):
|
||||||
|
options = {
|
||||||
|
"shuffle_doors": "none",
|
||||||
|
"victory_condition": "the_end",
|
||||||
|
"shuffle_postgame": "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_requirement(self):
|
||||||
|
location_names = [location.name for location in self.multiworld.get_locations(self.player)]
|
||||||
|
|
||||||
|
self.assertTrue("The End (Solved)" in location_names)
|
||||||
|
self.assertTrue("Champion's Rest - YOU" in location_names)
|
||||||
|
self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names)
|
||||||
|
self.assertFalse("The Red - Achievement" in location_names)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPostgameComplexDoorsTheEnd(LingoTestBase):
|
||||||
|
options = {
|
||||||
|
"shuffle_doors": "complex",
|
||||||
|
"victory_condition": "the_end",
|
||||||
|
"shuffle_postgame": "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_requirement(self):
|
||||||
|
location_names = [location.name for location in self.multiworld.get_locations(self.player)]
|
||||||
|
|
||||||
|
self.assertTrue("The End (Solved)" in location_names)
|
||||||
|
self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names)
|
||||||
|
self.assertTrue("The Red - Achievement" in location_names)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPostgameLateColorHunt(LingoTestBase):
|
||||||
|
options = {
|
||||||
|
"shuffle_doors": "none",
|
||||||
|
"victory_condition": "the_end",
|
||||||
|
"sunwarp_access": "disabled",
|
||||||
|
"shuffle_postgame": "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_requirement(self):
|
||||||
|
location_names = [location.name for location in self.multiworld.get_locations(self.player)]
|
||||||
|
|
||||||
|
self.assertFalse("Champion's Rest - YOU" in location_names)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPostgameVanillaTheMaster(LingoTestBase):
|
||||||
|
options = {
|
||||||
|
"shuffle_doors": "none",
|
||||||
|
"victory_condition": "the_master",
|
||||||
|
"shuffle_postgame": "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_requirement(self):
|
||||||
|
location_names = [location.name for location in self.multiworld.get_locations(self.player)]
|
||||||
|
|
||||||
|
self.assertTrue("Orange Tower Seventh Floor - THE END" in location_names)
|
||||||
|
self.assertTrue("Orange Tower Seventh Floor - Mastery Achievements" in location_names)
|
||||||
|
self.assertTrue("The Red - Achievement" in location_names)
|
||||||
|
self.assertFalse("Mastery Panels" in location_names)
|
Loading…
Reference in New Issue