diff options
| -rw-r--r-- | __init__.py | 33 | ||||
| -rw-r--r-- | data/LL1.yaml | 16 | ||||
| -rw-r--r-- | data/generated.dat | bin | 136277 -> 136563 bytes | |||
| -rw-r--r-- | data/ids.yaml | 3 | ||||
| -rw-r--r-- | options.py | 6 | ||||
| -rw-r--r-- | player_logic.py | 34 | ||||
| -rw-r--r-- | rules.py | 3 | ||||
| -rw-r--r-- | test/TestMastery.py | 6 | ||||
| -rw-r--r-- | test/TestPostgame.py | 62 |
9 files changed, 145 insertions, 18 deletions
| diff --git a/__init__.py b/__init__.py index 8d6a7fc..3b67617 100644 --- a/__init__.py +++ b/__init__.py | |||
| @@ -3,7 +3,7 @@ Archipelago init file for Lingo | |||
| 3 | """ | 3 | """ |
| 4 | from logging import warning | 4 | from logging import warning |
| 5 | 5 | ||
| 6 | from BaseClasses import Item, ItemClassification, Tutorial | 6 | from BaseClasses import CollectionState, Item, ItemClassification, Tutorial |
| 7 | from Options import OptionError | 7 | from Options import OptionError |
| 8 | from worlds.AutoWorld import WebWorld, World | 8 | from worlds.AutoWorld import WebWorld, World |
| 9 | from .datatypes import Room, RoomEntrance | 9 | from .datatypes import Room, RoomEntrance |
| @@ -68,6 +68,37 @@ class LingoWorld(World): | |||
| 68 | def create_regions(self): | 68 | def create_regions(self): |
| 69 | create_regions(self) | 69 | create_regions(self) |
| 70 | 70 | ||
| 71 | if not self.options.shuffle_postgame: | ||
| 72 | state = CollectionState(self.multiworld) | ||
| 73 | state.collect(LingoItem("Prevent Victory", ItemClassification.progression, None, self.player), True) | ||
| 74 | |||
| 75 | # Note: relies on the assumption that real_items is a definitive list of real progression items in this | ||
| 76 | # world, and is not modified after being created. | ||
| 77 | for item in self.player_logic.real_items: | ||
| 78 | state.collect(self.create_item(item), True) | ||
| 79 | |||
| 80 | # Exception to the above: a forced good item is not considered a "real item", but needs to be here anyway. | ||
| 81 | if self.player_logic.forced_good_item != "": | ||
| 82 | state.collect(self.create_item(self.player_logic.forced_good_item), True) | ||
| 83 | |||
| 84 | all_locations = self.multiworld.get_locations(self.player) | ||
| 85 | state.sweep_for_events(locations=all_locations) | ||
| 86 | |||
| 87 | unreachable_locations = [location for location in all_locations | ||
| 88 | if not state.can_reach_location(location.name, self.player)] | ||
| 89 | |||
| 90 | for location in unreachable_locations: | ||
| 91 | if location.name in self.player_logic.event_loc_to_item.keys(): | ||
| 92 | continue | ||
| 93 | |||
| 94 | self.player_logic.real_locations.remove(location.name) | ||
| 95 | location.parent_region.locations.remove(location) | ||
| 96 | |||
| 97 | if len(self.player_logic.real_items) > len(self.player_logic.real_locations): | ||
| 98 | raise OptionError(f"{self.player_name}'s Lingo world does not have enough locations to fit the number" | ||
| 99 | f" of required items without shuffling the postgame. Either enable postgame" | ||
| 100 | f" shuffling, or choose different options.") | ||
| 101 | |||
| 71 | def create_items(self): | 102 | def create_items(self): |
| 72 | pool = [self.create_item(name) for name in self.player_logic.real_items] | 103 | pool = [self.create_item(name) for name in self.player_logic.real_items] |
| 73 | 104 | ||
| diff --git a/data/LL1.yaml b/data/LL1.yaml index e12ca02..3035446 100644 --- a/data/LL1.yaml +++ b/data/LL1.yaml | |||
| @@ -879,6 +879,8 @@ | |||
| 879 | panel: DRAWL + RUNS | 879 | panel: DRAWL + RUNS |
| 880 | - room: Owl Hallway | 880 | - room: Owl Hallway |
| 881 | panel: READS + RUST | 881 | panel: READS + RUST |
| 882 | - room: Ending Area | ||
| 883 | panel: THE END | ||
| 882 | paintings: | 884 | paintings: |
| 883 | - id: eye_painting | 885 | - id: eye_painting |
| 884 | disable: True | 886 | disable: True |
| @@ -2322,7 +2324,7 @@ | |||
| 2322 | orientation: east | 2324 | orientation: east |
| 2323 | - id: hi_solved_painting | 2325 | - id: hi_solved_painting |
| 2324 | orientation: west | 2326 | orientation: west |
| 2325 | Orange Tower Seventh Floor: | 2327 | Ending Area: |
| 2326 | entrances: | 2328 | entrances: |
| 2327 | Orange Tower Sixth Floor: | 2329 | Orange Tower Sixth Floor: |
| 2328 | room: Orange Tower | 2330 | room: Orange Tower |
| @@ -2334,6 +2336,18 @@ | |||
| 2334 | check: True | 2336 | check: True |
| 2335 | tag: forbid | 2337 | tag: forbid |
| 2336 | non_counting: True | 2338 | non_counting: True |
| 2339 | location_name: Orange Tower Seventh Floor - THE END | ||
| 2340 | doors: | ||
| 2341 | End: | ||
| 2342 | event: True | ||
| 2343 | panels: | ||
| 2344 | - THE END | ||
| 2345 | Orange Tower Seventh Floor: | ||
| 2346 | entrances: | ||
| 2347 | Ending Area: | ||
| 2348 | room: Ending Area | ||
| 2349 | door: End | ||
| 2350 | panels: | ||
| 2337 | THE MASTER: | 2351 | THE MASTER: |
| 2338 | # We will set up special rules for this in code. | 2352 | # We will set up special rules for this in code. |
| 2339 | id: Countdown Panels/Panel_master_master | 2353 | id: Countdown Panels/Panel_master_master |
| diff --git a/data/generated.dat b/data/generated.dat index 3ed6cb2..4a751b2 100644 --- a/data/generated.dat +++ b/data/generated.dat | |||
| Binary files differ | |||
| diff --git a/data/ids.yaml b/data/ids.yaml index 1fa06d2..c49a8df 100644 --- a/data/ids.yaml +++ b/data/ids.yaml | |||
| @@ -272,8 +272,9 @@ panels: | |||
| 272 | PAINTING (4): 445081 | 272 | PAINTING (4): 445081 |
| 273 | PAINTING (5): 445082 | 273 | PAINTING (5): 445082 |
| 274 | ROOM: 445083 | 274 | ROOM: 445083 |
| 275 | Orange Tower Seventh Floor: | 275 | Ending Area: |
| 276 | THE END: 444620 | 276 | THE END: 444620 |
| 277 | Orange Tower Seventh Floor: | ||
| 277 | THE MASTER: 444621 | 278 | THE MASTER: 444621 |
| 278 | MASTERY: 444622 | 279 | MASTERY: 444622 |
| 279 | Behind A Smile: | 280 | Behind A Smile: |
| diff --git a/options.py b/options.py index 333b3e1..5a076e5 100644 --- a/options.py +++ b/options.py | |||
| @@ -194,6 +194,11 @@ class EarlyColorHallways(Toggle): | |||
| 194 | display_name = "Early Color Hallways" | 194 | display_name = "Early Color Hallways" |
| 195 | 195 | ||
| 196 | 196 | ||
| 197 | class ShufflePostgame(Toggle): | ||
| 198 | """When off, locations that could not be reached without also reaching your victory condition are removed.""" | ||
| 199 | display_name = "Shuffle Postgame" | ||
| 200 | |||
| 201 | |||
| 197 | class TrapPercentage(Range): | 202 | class TrapPercentage(Range): |
| 198 | """Replaces junk items with traps, at the specified rate.""" | 203 | """Replaces junk items with traps, at the specified rate.""" |
| 199 | display_name = "Trap Percentage" | 204 | display_name = "Trap Percentage" |
| @@ -263,6 +268,7 @@ class LingoOptions(PerGameCommonOptions): | |||
| 263 | mastery_achievements: MasteryAchievements | 268 | mastery_achievements: MasteryAchievements |
| 264 | level_2_requirement: Level2Requirement | 269 | level_2_requirement: Level2Requirement |
| 265 | early_color_hallways: EarlyColorHallways | 270 | early_color_hallways: EarlyColorHallways |
| 271 | shuffle_postgame: ShufflePostgame | ||
| 266 | trap_percentage: TrapPercentage | 272 | trap_percentage: TrapPercentage |
| 267 | trap_weights: TrapWeights | 273 | trap_weights: TrapWeights |
| 268 | puzzle_skip_percentage: PuzzleSkipPercentage | 274 | puzzle_skip_percentage: PuzzleSkipPercentage |
| diff --git a/player_logic.py b/player_logic.py index 1621620..35080ac 100644 --- a/player_logic.py +++ b/player_logic.py | |||
| @@ -19,22 +19,25 @@ class AccessRequirements: | |||
| 19 | doors: Set[RoomAndDoor] | 19 | doors: Set[RoomAndDoor] |
| 20 | colors: Set[str] | 20 | colors: Set[str] |
| 21 | the_master: bool | 21 | the_master: bool |
| 22 | postgame: bool | ||
| 22 | 23 | ||
| 23 | def __init__(self): | 24 | def __init__(self): |
| 24 | self.rooms = set() | 25 | self.rooms = set() |
| 25 | self.doors = set() | 26 | self.doors = set() |
| 26 | self.colors = set() | 27 | self.colors = set() |
| 27 | self.the_master = False | 28 | self.the_master = False |
| 29 | self.postgame = False | ||
| 28 | 30 | ||
| 29 | def merge(self, other: "AccessRequirements"): | 31 | def merge(self, other: "AccessRequirements"): |
| 30 | self.rooms |= other.rooms | 32 | self.rooms |= other.rooms |
| 31 | self.doors |= other.doors | 33 | self.doors |= other.doors |
| 32 | self.colors |= other.colors | 34 | self.colors |= other.colors |
| 33 | self.the_master |= other.the_master | 35 | self.the_master |= other.the_master |
| 36 | self.postgame |= other.postgame | ||
| 34 | 37 | ||
| 35 | def __str__(self): | 38 | def __str__(self): |
| 36 | return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors})," \ | 39 | return f"AccessRequirements(rooms={self.rooms}, doors={self.doors}, colors={self.colors}," \ |
| 37 | f" the_master={self.the_master}" | 40 | f" the_master={self.the_master}, postgame={self.postgame})" |
| 38 | 41 | ||
| 39 | 42 | ||
| 40 | class PlayerLocation(NamedTuple): | 43 | class PlayerLocation(NamedTuple): |
| @@ -190,16 +193,6 @@ class LingoPlayerLogic: | |||
| 190 | if color_shuffle: | 193 | if color_shuffle: |
| 191 | self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR] | 194 | self.real_items += [name for name, item in ALL_ITEM_TABLE.items() if item.type == ItemType.COLOR] |
| 192 | 195 | ||
| 193 | # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. | ||
| 194 | for room_name, room_data in PANELS_BY_ROOM.items(): | ||
| 195 | for panel_name, panel_data in room_data.items(): | ||
| 196 | if panel_data.achievement: | ||
| 197 | access_req = AccessRequirements() | ||
| 198 | access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world)) | ||
| 199 | access_req.rooms.add(room_name) | ||
| 200 | |||
| 201 | self.mastery_reqs.append(access_req) | ||
| 202 | |||
| 203 | # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need | 196 | # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need |
| 204 | # to prevent the actual victory condition from becoming a check. | 197 | # to prevent the actual victory condition from becoming a check. |
| 205 | self.mastery_location = "Orange Tower Seventh Floor - THE MASTER" | 198 | self.mastery_location = "Orange Tower Seventh Floor - THE MASTER" |
| @@ -207,7 +200,7 @@ class LingoPlayerLogic: | |||
| 207 | 200 | ||
| 208 | if victory_condition == VictoryCondition.option_the_end: | 201 | if victory_condition == VictoryCondition.option_the_end: |
| 209 | self.victory_condition = "Orange Tower Seventh Floor - THE END" | 202 | self.victory_condition = "Orange Tower Seventh Floor - THE END" |
| 210 | self.add_location("Orange Tower Seventh Floor", "The End (Solved)", None, [], world) | 203 | self.add_location("Ending Area", "The End (Solved)", None, [], world) |
| 211 | self.event_loc_to_item["The End (Solved)"] = "Victory" | 204 | self.event_loc_to_item["The End (Solved)"] = "Victory" |
| 212 | elif victory_condition == VictoryCondition.option_the_master: | 205 | elif victory_condition == VictoryCondition.option_the_master: |
| 213 | self.victory_condition = "Orange Tower Seventh Floor - THE MASTER" | 206 | self.victory_condition = "Orange Tower Seventh Floor - THE MASTER" |
| @@ -231,6 +224,16 @@ class LingoPlayerLogic: | |||
| 231 | [RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world) | 224 | [RoomAndPanel("Pilgrim Antechamber", "PILGRIM")], world) |
| 232 | self.event_loc_to_item["PILGRIM (Solved)"] = "Victory" | 225 | self.event_loc_to_item["PILGRIM (Solved)"] = "Victory" |
| 233 | 226 | ||
| 227 | # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. | ||
| 228 | for room_name, room_data in PANELS_BY_ROOM.items(): | ||
| 229 | for panel_name, panel_data in room_data.items(): | ||
| 230 | if panel_data.achievement: | ||
| 231 | access_req = AccessRequirements() | ||
| 232 | access_req.merge(self.calculate_panel_requirements(room_name, panel_name, world)) | ||
| 233 | access_req.rooms.add(room_name) | ||
| 234 | |||
| 235 | self.mastery_reqs.append(access_req) | ||
| 236 | |||
| 234 | # Create groups of counting panel access requirements for the LEVEL 2 check. | 237 | # Create groups of counting panel access requirements for the LEVEL 2 check. |
| 235 | self.create_panel_hunt_events(world) | 238 | self.create_panel_hunt_events(world) |
| 236 | 239 | ||
| @@ -470,6 +473,11 @@ class LingoPlayerLogic: | |||
| 470 | if panel == "THE MASTER": | 473 | if panel == "THE MASTER": |
| 471 | access_reqs.the_master = True | 474 | access_reqs.the_master = True |
| 472 | 475 | ||
| 476 | # Evil python magic (so sayeth NewSoupVi): this checks victory_condition against the panel's location name | ||
| 477 | # override if it exists, or the auto-generated location name if it's None. | ||
| 478 | if self.victory_condition == (panel_object.location_name or f"{room} - {panel}"): | ||
| 479 | access_reqs.postgame = True | ||
| 480 | |||
| 473 | self.panel_reqs[room][panel] = access_reqs | 481 | self.panel_reqs[room][panel] = access_reqs |
| 474 | 482 | ||
| 475 | return self.panel_reqs[room][panel] | 483 | return self.panel_reqs[room][panel] |
| diff --git a/rules.py b/rules.py index d91c53f..ed84c56 100644 --- a/rules.py +++ b/rules.py | |||
| @@ -62,6 +62,9 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir | |||
| 62 | if access.the_master and not lingo_can_use_mastery_location(state, world): | 62 | if access.the_master and not lingo_can_use_mastery_location(state, world): |
| 63 | return False | 63 | return False |
| 64 | 64 | ||
| 65 | if access.postgame and state.has("Prevent Victory", world.player): | ||
| 66 | return False | ||
| 67 | |||
| 65 | return True | 68 | return True |
| 66 | 69 | ||
| 67 | 70 | ||
| diff --git a/test/TestMastery.py b/test/TestMastery.py index 3ebe40a..c9c79a9 100644 --- a/test/TestMastery.py +++ b/test/TestMastery.py | |||
| @@ -5,7 +5,8 @@ class TestMasteryWhenVictoryIsTheEnd(LingoTestBase): | |||
| 5 | options = { | 5 | options = { |
| 6 | "mastery_achievements": "22", | 6 | "mastery_achievements": "22", |
| 7 | "victory_condition": "the_end", | 7 | "victory_condition": "the_end", |
| 8 | "shuffle_colors": "true" | 8 | "shuffle_colors": "true", |
| 9 | "shuffle_postgame": "true", | ||
| 9 | } | 10 | } |
| 10 | 11 | ||
| 11 | def test_requirement(self): | 12 | def test_requirement(self): |
| @@ -43,7 +44,8 @@ class TestMasteryBlocksDependents(LingoTestBase): | |||
| 43 | options = { | 44 | options = { |
| 44 | "mastery_achievements": "24", | 45 | "mastery_achievements": "24", |
| 45 | "shuffle_colors": "true", | 46 | "shuffle_colors": "true", |
| 46 | "location_checks": "insanity" | 47 | "location_checks": "insanity", |
| 48 | "victory_condition": "level_2", | ||
| 47 | } | 49 | } |
| 48 | 50 | ||
| 49 | def test_requirement(self): | 51 | def test_requirement(self): |
| diff --git a/test/TestPostgame.py b/test/TestPostgame.py new file mode 100644 index 0000000..d2e2232 --- /dev/null +++ b/test/TestPostgame.py | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | from . import LingoTestBase | ||
| 2 | |||
| 3 | |||
| 4 | class TestPostgameVanillaTheEnd(LingoTestBase): | ||
| 5 | options = { | ||
| 6 | "shuffle_doors": "none", | ||
| 7 | "victory_condition": "the_end", | ||
| 8 | "shuffle_postgame": "false", | ||
| 9 | } | ||
| 10 | |||
| 11 | def test_requirement(self): | ||
| 12 | location_names = [location.name for location in self.multiworld.get_locations(self.player)] | ||
| 13 | |||
| 14 | self.assertTrue("The End (Solved)" in location_names) | ||
| 15 | self.assertTrue("Champion's Rest - YOU" in location_names) | ||
| 16 | self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names) | ||
| 17 | self.assertFalse("The Red - Achievement" in location_names) | ||
| 18 | |||
| 19 | |||
| 20 | class TestPostgameComplexDoorsTheEnd(LingoTestBase): | ||
| 21 | options = { | ||
| 22 | "shuffle_doors": "complex", | ||
| 23 | "victory_condition": "the_end", | ||
| 24 | "shuffle_postgame": "false", | ||
| 25 | } | ||
| 26 | |||
| 27 | def test_requirement(self): | ||
| 28 | location_names = [location.name for location in self.multiworld.get_locations(self.player)] | ||
| 29 | |||
| 30 | self.assertTrue("The End (Solved)" in location_names) | ||
| 31 | self.assertFalse("Orange Tower Seventh Floor - THE MASTER" in location_names) | ||
| 32 | self.assertTrue("The Red - Achievement" in location_names) | ||
| 33 | |||
| 34 | |||
| 35 | class TestPostgameLateColorHunt(LingoTestBase): | ||
| 36 | options = { | ||
| 37 | "shuffle_doors": "none", | ||
| 38 | "victory_condition": "the_end", | ||
| 39 | "sunwarp_access": "disabled", | ||
| 40 | "shuffle_postgame": "false", | ||
| 41 | } | ||
| 42 | |||
| 43 | def test_requirement(self): | ||
| 44 | location_names = [location.name for location in self.multiworld.get_locations(self.player)] | ||
| 45 | |||
| 46 | self.assertFalse("Champion's Rest - YOU" in location_names) | ||
| 47 | |||
| 48 | |||
| 49 | class TestPostgameVanillaTheMaster(LingoTestBase): | ||
| 50 | options = { | ||
| 51 | "shuffle_doors": "none", | ||
| 52 | "victory_condition": "the_master", | ||
| 53 | "shuffle_postgame": "false", | ||
| 54 | } | ||
| 55 | |||
| 56 | def test_requirement(self): | ||
| 57 | location_names = [location.name for location in self.multiworld.get_locations(self.player)] | ||
| 58 | |||
| 59 | self.assertTrue("Orange Tower Seventh Floor - THE END" in location_names) | ||
| 60 | self.assertTrue("Orange Tower Seventh Floor - Mastery Achievements" in location_names) | ||
| 61 | self.assertTrue("The Red - Achievement" in location_names) | ||
| 62 | self.assertFalse("Mastery Panels" in location_names) | ||
