WebHost, Core: Move item and location descriptions to `WebWorld` responsibilities. (#2508)
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com> Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									8b6eae0a14
								
							
						
					
					
						commit
						e1ff5073b5
					
				|  | @ -121,6 +121,53 @@ class RLWeb(WebWorld): | |||
|     # ... | ||||
| ``` | ||||
| 
 | ||||
| * `location_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of locations  | ||||
| or location groups. | ||||
| 
 | ||||
|   ```python | ||||
|   # locations.py | ||||
|   location_descriptions = { | ||||
|       "Red Potion #6": "In a secret destructible block under the second stairway", | ||||
|       "L2 Spaceship": """ | ||||
|         The group of all items in the spaceship in Level 2. | ||||
|    | ||||
|         This doesn't include the item on the spaceship door, since it can be | ||||
|         accessed without the Spaceship Key. | ||||
|       """ | ||||
|   } | ||||
|    | ||||
|   # __init__.py | ||||
|   from worlds.AutoWorld import WebWorld | ||||
|   from .locations import location_descriptions | ||||
|    | ||||
|    | ||||
|   class MyGameWeb(WebWorld): | ||||
|       location_descriptions = location_descriptions | ||||
|   ``` | ||||
| 
 | ||||
| * `item_descriptions` (optional) WebWorlds can provide a map that contains human-friendly descriptions of items or item  | ||||
| groups. | ||||
| 
 | ||||
|   ```python | ||||
|   # items.py | ||||
|   item_descriptions = { | ||||
|       "Red Potion": "A standard health potion", | ||||
|       "Spaceship Key": """ | ||||
|         The key to the spaceship in Level 2. | ||||
|    | ||||
|         This is necessary to get to the Star Realm. | ||||
|       """, | ||||
|   } | ||||
|    | ||||
|   # __init__.py | ||||
|   from worlds.AutoWorld import WebWorld | ||||
|   from .items import item_descriptions | ||||
|    | ||||
|    | ||||
|   class MyGameWeb(WebWorld): | ||||
|       item_descriptions = item_descriptions | ||||
|   ``` | ||||
| 
 | ||||
| ### MultiWorld Object | ||||
| 
 | ||||
| The `MultiWorld` object references the whole multiworld (all items and locations for all players) and is accessible | ||||
|  | @ -178,36 +225,6 @@ Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED | |||
| The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being | ||||
| required, and will prevent progression and useful items from being placed at excluded locations. | ||||
| 
 | ||||
| #### Documenting Locations | ||||
| 
 | ||||
| Worlds can optionally provide a `location_descriptions` map which contains human-friendly descriptions of locations and | ||||
| location groups. These descriptions will show up in location-selection options on the options pages. | ||||
| 
 | ||||
| ```python | ||||
| # locations.py | ||||
| 
 | ||||
| location_descriptions = { | ||||
|     "Red Potion #6": "In a secret destructible block under the second stairway", | ||||
|     "L2 Spaceship": | ||||
|         """ | ||||
|         The group of all items in the spaceship in Level 2. | ||||
| 
 | ||||
|         This doesn't include the item on the spaceship door, since it can be accessed without the Spaceship Key. | ||||
|         """ | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ```python | ||||
| # __init__.py | ||||
| 
 | ||||
| from worlds.AutoWorld import World | ||||
| from .locations import location_descriptions | ||||
| 
 | ||||
| 
 | ||||
| class MyGameWorld(World): | ||||
|     location_descriptions = location_descriptions | ||||
| ``` | ||||
| 
 | ||||
| ### Items | ||||
| 
 | ||||
| Items are all things that can "drop" for your game. This may be RPG items like weapons, or technologies you normally | ||||
|  | @ -232,36 +249,6 @@ Other classifications include: | |||
| * `progression_skip_balancing`: the combination of `progression` and `skip_balancing`, i.e., a progression item that | ||||
|   will not be moved around by progression balancing; used, e.g., for currency or tokens, to not flood early spheres | ||||
| 
 | ||||
| #### Documenting Items | ||||
| 
 | ||||
| Worlds can optionally provide an `item_descriptions` map which contains human-friendly descriptions of items and item | ||||
| groups. These descriptions will show up in item-selection options on the options pages. | ||||
| 
 | ||||
| ```python | ||||
| # items.py | ||||
| 
 | ||||
| item_descriptions = { | ||||
|     "Red Potion": "A standard health potion", | ||||
|     "Spaceship Key": | ||||
|         """ | ||||
|         The key to the spaceship in Level 2. | ||||
| 
 | ||||
|         This is necessary to get to the Star Realm. | ||||
|         """ | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ```python | ||||
| # __init__.py | ||||
| 
 | ||||
| from worlds.AutoWorld import World | ||||
| from .items import item_descriptions | ||||
| 
 | ||||
| 
 | ||||
| class MyGameWorld(World): | ||||
|     item_descriptions = item_descriptions | ||||
| ``` | ||||
| 
 | ||||
| ### Events | ||||
| 
 | ||||
| An Event is a special combination of a Location and an Item, with both having an `id` of `None`. These can be used to | ||||
|  |  | |||
|  | @ -64,15 +64,6 @@ class TestBase(unittest.TestCase): | |||
|                 for item in multiworld.itempool: | ||||
|                     self.assertIn(item.name, world_type.item_name_to_id) | ||||
| 
 | ||||
|     def test_item_descriptions_have_valid_names(self): | ||||
|         """Ensure all item descriptions match an item name or item group name""" | ||||
|         for game_name, world_type in AutoWorldRegister.world_types.items(): | ||||
|             valid_names = world_type.item_names.union(world_type.item_name_groups) | ||||
|             for name in world_type.item_descriptions: | ||||
|                 with self.subTest("Name should be valid", game=game_name, item=name): | ||||
|                     self.assertIn(name, valid_names, | ||||
|                                   "All item descriptions must match defined item names") | ||||
| 
 | ||||
|     def test_itempool_not_modified(self): | ||||
|         """Test that worlds don't modify the itempool after `create_items`""" | ||||
|         gen_steps = ("generate_early", "create_regions", "create_items") | ||||
|  |  | |||
|  | @ -66,12 +66,3 @@ class TestBase(unittest.TestCase): | |||
|                         for location in locations: | ||||
|                             self.assertIn(location, world_type.location_name_to_id) | ||||
|                         self.assertNotIn(group_name, world_type.location_name_to_id) | ||||
| 
 | ||||
|     def test_location_descriptions_have_valid_names(self): | ||||
|         """Ensure all location descriptions match a location name or location group name""" | ||||
|         for game_name, world_type in AutoWorldRegister.world_types.items(): | ||||
|             valid_names = world_type.location_names.union(world_type.location_name_groups) | ||||
|             for name in world_type.location_descriptions: | ||||
|                 with self.subTest("Name should be valid", game=game_name, location=name): | ||||
|                     self.assertIn(name, valid_names, | ||||
|                                   "All location descriptions must match defined location names") | ||||
|  |  | |||
|  | @ -0,0 +1,23 @@ | |||
| import unittest | ||||
| 
 | ||||
| from worlds.AutoWorld import AutoWorldRegister | ||||
| 
 | ||||
| 
 | ||||
| class TestWebDescriptions(unittest.TestCase): | ||||
|     def test_item_descriptions_have_valid_names(self) -> None: | ||||
|         """Ensure all item descriptions match an item name or item group name""" | ||||
|         for game_name, world_type in AutoWorldRegister.world_types.items(): | ||||
|             valid_names = world_type.item_names.union(world_type.item_name_groups) | ||||
|             for name in world_type.web.item_descriptions: | ||||
|                 with self.subTest("Name should be valid", game=game_name, item=name): | ||||
|                     self.assertIn(name, valid_names, | ||||
|                                   "All item descriptions must match defined item names") | ||||
| 
 | ||||
|     def test_location_descriptions_have_valid_names(self) -> None: | ||||
|         """Ensure all location descriptions match a location name or location group name""" | ||||
|         for game_name, world_type in AutoWorldRegister.world_types.items(): | ||||
|             valid_names = world_type.location_names.union(world_type.location_name_groups) | ||||
|             for name in world_type.web.location_descriptions: | ||||
|                 with self.subTest("Name should be valid", game=game_name, location=name): | ||||
|                     self.assertIn(name, valid_names, | ||||
|                                   "All location descriptions must match defined location names") | ||||
|  | @ -3,13 +3,12 @@ from __future__ import annotations | |||
| import hashlib | ||||
| import logging | ||||
| import pathlib | ||||
| from random import Random | ||||
| import re | ||||
| import sys | ||||
| import time | ||||
| from random import Random | ||||
| from dataclasses import make_dataclass | ||||
| from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, | ||||
|                     Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union) | ||||
| from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple, | ||||
|                     TYPE_CHECKING, Type, Union) | ||||
| 
 | ||||
| from Options import ( | ||||
|     ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions, | ||||
|  | @ -55,17 +54,12 @@ class AutoWorldRegister(type): | |||
|         dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set | ||||
|                                    in dct.get("item_name_groups", {}).items()} | ||||
|         dct["item_name_groups"]["Everything"] = dct["item_names"] | ||||
|         dct["item_descriptions"] = {name: _normalize_description(description) for name, description | ||||
|                                     in dct.get("item_descriptions", {}).items()} | ||||
|         dct["item_descriptions"]["Everything"] = "All items in the entire game." | ||||
| 
 | ||||
|         dct["location_names"] = frozenset(dct["location_name_to_id"]) | ||||
|         dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set | ||||
|                                        in dct.get("location_name_groups", {}).items()} | ||||
|         dct["location_name_groups"]["Everywhere"] = dct["location_names"] | ||||
|         dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {}))) | ||||
|         dct["location_descriptions"] = {name: _normalize_description(description) for name, description | ||||
|                                     in dct.get("location_descriptions", {}).items()} | ||||
|         dct["location_descriptions"]["Everywhere"] = "All locations in the entire game." | ||||
| 
 | ||||
|         # move away from get_required_client_version function | ||||
|         if "game" in dct: | ||||
|  | @ -226,6 +220,12 @@ class WebWorld(metaclass=WebWorldRegister): | |||
|     option_groups: ClassVar[List[OptionGroup]] = [] | ||||
|     """Ordered list of option groupings. Any options not set in a group will be placed in a pre-built "Game Options".""" | ||||
| 
 | ||||
|     location_descriptions: Dict[str, str] = {} | ||||
|     """An optional map from location names (or location group names) to brief descriptions for users.""" | ||||
| 
 | ||||
|     item_descriptions: Dict[str, str] = {} | ||||
|     """An optional map from item names (or item group names) to brief descriptions for users.""" | ||||
| 
 | ||||
| 
 | ||||
| class World(metaclass=AutoWorldRegister): | ||||
|     """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. | ||||
|  | @ -252,23 +252,9 @@ class World(metaclass=AutoWorldRegister): | |||
|     item_name_groups: ClassVar[Dict[str, Set[str]]] = {} | ||||
|     """maps item group names to sets of items. Example: {"Weapons": {"Sword", "Bow"}}""" | ||||
| 
 | ||||
|     item_descriptions: ClassVar[Dict[str, str]] = {} | ||||
|     """An optional map from item names (or item group names) to brief descriptions for users. | ||||
| 
 | ||||
|     Individual newlines and indentation will be collapsed into spaces before these descriptions are | ||||
|     displayed. This may cover only a subset of items. | ||||
|     """ | ||||
| 
 | ||||
|     location_name_groups: ClassVar[Dict[str, Set[str]]] = {} | ||||
|     """maps location group names to sets of locations. Example: {"Sewer": {"Sewer Key Drop 1", "Sewer Key Drop 2"}}""" | ||||
| 
 | ||||
|     location_descriptions: ClassVar[Dict[str, str]] = {} | ||||
|     """An optional map from location names (or location group names) to brief descriptions for users. | ||||
| 
 | ||||
|     Individual newlines and indentation will be collapsed into spaces before these descriptions are | ||||
|     displayed. This may cover only a subset of locations. | ||||
|     """ | ||||
| 
 | ||||
|     data_version: ClassVar[int] = 0 | ||||
|     """ | ||||
|     Increment this every time something in your world's names/id mappings changes. | ||||
|  | @ -572,18 +558,3 @@ def data_package_checksum(data: "GamesPackage") -> str: | |||
|     assert sorted(data) == list(data), "Data not ordered" | ||||
|     from NetUtils import encode | ||||
|     return hashlib.sha1(encode(data).encode()).hexdigest() | ||||
| 
 | ||||
| 
 | ||||
| def _normalize_description(description): | ||||
|     """ | ||||
|     Normalizes a description in item_descriptions or location_descriptions. | ||||
| 
 | ||||
|     This allows authors to write descritions with nice indentation and line lengths in their world | ||||
|     definitions without having it affect the rendered format. | ||||
|     """ | ||||
|     # First, collapse the whitespace around newlines and the ends of the description. | ||||
|     description = re.sub(r' *\n *', '\n', description.strip()) | ||||
|     # Next, condense individual newlines into spaces. | ||||
|     description = re.sub(r'(?<!\n)\n(?!\n)', ' ', description) | ||||
|     return description | ||||
| 
 | ||||
|  |  | |||
|  | @ -1272,11 +1272,7 @@ _cut_content_items = [DS3ItemData(row[0], row[1], False, row[2]) for row in [ | |||
| ]] | ||||
| 
 | ||||
| item_descriptions = { | ||||
|     "Cinders": """ | ||||
|       All four Cinders of a Lord. | ||||
| 
 | ||||
|       Once you have these four, you can fight Soul of Cinder and win the game. | ||||
|     """, | ||||
|     "Cinders": "All four Cinders of a Lord.\n\nOnce you have these four, you can fight Soul of Cinder and win the game.", | ||||
| } | ||||
| 
 | ||||
| _all_items = _vanilla_items + _dlc_items | ||||
|  |  | |||
|  | @ -35,6 +35,8 @@ class DarkSouls3Web(WebWorld): | |||
| 
 | ||||
|     tutorials = [setup_en, setup_fr] | ||||
| 
 | ||||
|     item_descriptions = item_descriptions | ||||
| 
 | ||||
| 
 | ||||
| class DarkSouls3World(World): | ||||
|     """ | ||||
|  | @ -61,8 +63,6 @@ class DarkSouls3World(World): | |||
|             "Cinders of a Lord - Lothric Prince" | ||||
|         } | ||||
|     } | ||||
|     item_descriptions = item_descriptions | ||||
| 
 | ||||
| 
 | ||||
|     def __init__(self, multiworld: MultiWorld, player: int): | ||||
|         super().__init__(multiworld, player) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue