From fbdec86de8f80ef2c0d8e1bcb8836d9baeda6f61 Mon Sep 17 00:00:00 2001 From: dodo Date: Fri, 13 Dec 2024 21:07:14 +0100 Subject: [PATCH 01/21] take out localization loading into seperate file --- src/fantasy_forge/inventory.py | 3 ++- src/fantasy_forge/localization.py | 40 ++++++++++++++++++++++++++++++ src/fantasy_forge/world.py | 41 +++++++------------------------ 3 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 src/fantasy_forge/localization.py diff --git a/src/fantasy_forge/inventory.py b/src/fantasy_forge/inventory.py index 4989f19..98967ae 100644 --- a/src/fantasy_forge/inventory.py +++ b/src/fantasy_forge/inventory.py @@ -3,7 +3,8 @@ from typing import Iterator, Self from fantasy_forge.item import Item -from fantasy_forge.world import World, highlight_interactive +from fantasy_forge.localization import highlight_interactive +from fantasy_forge.world import World class Inventory: diff --git a/src/fantasy_forge/localization.py b/src/fantasy_forge/localization.py new file mode 100644 index 0000000..3bb9621 --- /dev/null +++ b/src/fantasy_forge/localization.py @@ -0,0 +1,40 @@ +from pathlib import Path +from typing import Any + +import huepy +from fluent.runtime import FluentLocalization, FluentResourceLoader +from fluent.runtime.types import FluentNone + +LOCALE_FOLDER: Path = Path("data/l10n/") +DEFAULT_LOCALE: str = "en" + + +def highlight_interactive(text: Any) -> FluentNone: + """INTER() for the localization""" + return FluentNone(huepy.bold(huepy.green(str(text)))) + + +def highlight_number(text: Any) -> FluentNone: + """NUM() for the localization""" + return FluentNone(huepy.bold(huepy.orange(str(text)))) + + +def check_exists(obj: Any): + """EXISTS() for the localization""" + return str(not isinstance(obj, FluentNone)).lower() + + +def get_fluent_locale(locale: str = DEFAULT_LOCALE) -> FluentLocalization: + locale_path: Path = LOCALE_FOLDER / locale + fluent_loader: FluentResourceLoader = FluentResourceLoader(str(locale_path)) + l10n = FluentLocalization( + locales=[locale], + resource_ids=["main.ftl"], + resource_loader=fluent_loader, + functions={ + "INTER": highlight_interactive, + "NUM": highlight_number, + "EXISTS": check_exists, + }, + ) + return l10n diff --git a/src/fantasy_forge/world.py b/src/fantasy_forge/world.py index 64422e6..4c5e4f1 100644 --- a/src/fantasy_forge/world.py +++ b/src/fantasy_forge/world.py @@ -2,14 +2,12 @@ import logging from pathlib import Path -from typing import Any, Self +from typing import TYPE_CHECKING, Self -import huepy import toml -from fluent.runtime import FluentLocalization, FluentResourceLoader -from fluent.runtime.types import FluentNone from fantasy_forge.area import Area +from fantasy_forge.localization import get_fluent_locale logger = logging.getLogger(__name__) # logger.setLevel(logging.DEBUG) @@ -37,8 +35,6 @@ def spawn_point(self) -> Area: @staticmethod def load(name: str) -> World: - locale_path = "data/l10n/{locale}" - fluent_loader = FluentResourceLoader(locale_path) path = Path("data/worlds") / name if not path.exists(): logger.debug(f"Path {path} not found, using {name}") @@ -46,35 +42,16 @@ def load(name: str) -> World: areas: dict[str, Area] = dict() with (path / "world.toml").open() as world_file: world_toml = toml.load(world_file) + language: str = world_toml["language"] + world_name: str = world_toml["name"] + l10n: FluentLocalization = get_fluent_locale(language) logger.debug("language") - logger.debug(world_toml["language"]) - l10n = FluentLocalization( - locales=[world_toml["language"]], - resource_ids=["main.ftl"], - resource_loader=fluent_loader, - functions={ - "INTER": highlight_interactive, - "NUM": highlight_number, - "EXISTS": check_exists, - }, - ) + logger.debug(language) world_spawn: str = world_toml["spawn"] - world = World(l10n, world_toml["name"], areas, world_spawn) + world = World(l10n, world_name, areas, world_spawn) for area_name in world_toml["areas"]: areas[area_name] = Area.load(world, path, area_name) return world - -def highlight_interactive(text: Any) -> FluentNone: - """INTER() for the localization""" - return FluentNone(huepy.bold(huepy.green(str(text)))) - - -def highlight_number(text: Any) -> FluentNone: - """NUM() for the localization""" - return FluentNone(huepy.bold(huepy.orange(str(text)))) - - -def check_exists(obj: Any): - """EXISTS() for the localization""" - return str(not isinstance(obj, FluentNone)).lower() +if TYPE_CHECKING: + from fluent.runtime import FluentLocalization \ No newline at end of file From f7239e649a07e0330cfd45a63744c6f31dd98628 Mon Sep 17 00:00:00 2001 From: dodo Date: Sat, 14 Dec 2024 03:01:41 +0100 Subject: [PATCH 02/21] modules now use FluentLocalization directly --- src/fantasy_forge/area.py | 36 +++++++-------- src/fantasy_forge/armour.py | 18 ++++---- src/fantasy_forge/character.py | 41 +++++++++-------- src/fantasy_forge/container.py | 17 ++++--- src/fantasy_forge/enemy.py | 18 ++++---- src/fantasy_forge/entity.py | 12 +++-- src/fantasy_forge/gateway.py | 33 +++++++------- src/fantasy_forge/inventory.py | 19 ++++---- src/fantasy_forge/item.py | 17 ++++--- src/fantasy_forge/key.py | 13 ++++-- src/fantasy_forge/player.py | 81 ++++++++++++++++------------------ src/fantasy_forge/weapon.py | 15 +++++-- 12 files changed, 174 insertions(+), 146 deletions(-) diff --git a/src/fantasy_forge/area.py b/src/fantasy_forge/area.py index f80a172..b82a729 100644 --- a/src/fantasy_forge/area.py +++ b/src/fantasy_forge/area.py @@ -14,8 +14,8 @@ class Area(Entity): __important_attributes__ = ("name",) contents: dict[str, Entity] - def __init__(self: Self, world: World, config_dict: dict[str, Any]): - super().__init__(world, config_dict) + def __init__(self: Self, config_dict: dict[str, Any], l10n: FluentLocalization): + super().__init__(config_dict, l10n) self.contents: dict = {} def __iter__(self: Self) -> Iterator: @@ -23,7 +23,7 @@ def __iter__(self: Self) -> Iterator: yield obj def on_look(self: Self) -> str: - return self.world.l10n.format_value( + return self.l10n.format_value( "look-around-begin", { "area-name": self.name, @@ -37,39 +37,39 @@ def to_dict(self: Self) -> dict: return area_dict @staticmethod - def from_dict(world: World, area_dict: dict) -> Area: + def from_dict(area_dict: dict, l10n: FluentLocalization) -> Area: contents_list: list[Entity] = [] for entity_dict in area_dict.get("contents", []): match entity_dict.get("kind", "entity"): case "item": from fantasy_forge.item import Item - contents_list.append(Item(world, entity_dict)) + contents_list.append(Item(entity_dict, l10n)) case "gateway": from fantasy_forge.gateway import Gateway - contents_list.append(Gateway(world, entity_dict)) + contents_list.append(Gateway(entity_dict, l10n)) case "key": from fantasy_forge.key import Key - contents_list.append(Key(world, entity_dict)) + contents_list.append(Key(entity_dict, l10n)) case "enemy": from fantasy_forge.enemy import Enemy - contents_list.append(Enemy(world, entity_dict)) + contents_list.append(Enemy(entity_dict, l10n)) case "weapon": from fantasy_forge.weapon import Weapon - contents_list.append(Weapon(world, entity_dict)) + contents_list.append(Weapon(entity_dict, l10n)) case "armour": from fantasy_forge.armour import Armour - contents_list.append(Armour(world, entity_dict)) + contents_list.append(Armour(entity_dict, l10n)) case default: - contents_list.append(Entity(world, entity_dict)) + contents_list.append(Entity(entity_dict, l10n)) contents = {entity.name: entity for entity in contents_list} - area = Area(world, area_dict) + area = Area(area_dict, l10n) area.contents = contents return area @@ -78,19 +78,19 @@ def load(world, root_path: Path, name: str): path = root_path / "areas" / f"{name}.toml" with path.open() as area_file: area_toml = toml.load(area_file) - return Area.from_dict(world, area_toml) + return Area.from_dict(area_toml, world.l10n) @staticmethod - def empty(world: World) -> Area: + def empty(l10n: FluentLocalization) -> Area: """Return an empty area, this is a placeholder.""" return Area( - world, dict( - name=world.l10n.format_value("void-name"), - description=world.l10n.format_value("void-description"), + name=l10n.format_value("void-name"), + description=l10n.format_value("void-description"), ), + l10n, ) if TYPE_CHECKING: - from fantasy_forge.world import World + from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/armour.py b/src/fantasy_forge/armour.py index cc1305f..bb637df 100644 --- a/src/fantasy_forge/armour.py +++ b/src/fantasy_forge/armour.py @@ -1,9 +1,8 @@ from __future__ import annotations -from typing import Self +from typing import TYPE_CHECKING, Self from fantasy_forge.item import Item -from fantasy_forge.world import World # Armour types: # 'head': for caps, hats, helmets @@ -19,12 +18,12 @@ class Armour(Item): __important_attributes__ = ("name", "armour_type", "defense") - def __init__(self, world, config_dict): + def __init__(self, config_dict, l10n: FluentLocalization): a_type: str = config_dict.pop("armour_type") assert a_type in ARMOUR_TYPES self.armour_type = a_type self.defense = config_dict.pop("defense") - super().__init__(world, config_dict) + super().__init__(config_dict, l10n) def to_dict(self: Self) -> dict: armour_dict: dict = super().to_dict() @@ -33,9 +32,10 @@ def to_dict(self: Self) -> dict: return armour_dict @staticmethod - def from_dict(world: World, armour_dict: dict) -> Armour: - armour: Armour = Armour( - world, - armour_dict - ) + def from_dict(armour_dict: dict, l10n: FluentLocalization) -> Armour: + armour: Armour = Armour(armour_dict, l10n) return armour + + +if TYPE_CHECKING: + from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/character.py b/src/fantasy_forge/character.py index 9e901c1..5922318 100644 --- a/src/fantasy_forge/character.py +++ b/src/fantasy_forge/character.py @@ -1,15 +1,15 @@ from __future__ import annotations -from typing import Any, Self +from typing import TYPE_CHECKING, Any, Self from fantasy_forge.entity import Entity from fantasy_forge.inventory import Inventory from fantasy_forge.weapon import Weapon -from fantasy_forge.world import World BASE_INVENTORY_CAPACITY = 10 BASE_DAMAGE = 1 + def bare_hands(world: World): return Weapon( world, @@ -20,6 +20,7 @@ def bare_hands(world: World): }, ) + class Character(Entity): """A character in the world.""" @@ -30,11 +31,13 @@ class Character(Entity): main_hand: Weapon | None _alive: bool - def __init__(self: Self, world: World, config_dict: dict[str, Any]) -> None: + def __init__( + self: Self, config_dict: dict[str, Any], l10n: FluentLocalization + ) -> None: self.health = config_dict.pop("health") - self.inventory = Inventory(world, BASE_INVENTORY_CAPACITY) + self.inventory = Inventory(BASE_INVENTORY_CAPACITY, l10n) self.main_hand = None - super().__init__(world, config_dict) + super().__init__(config_dict, l10n) @property def alive(self: Self) -> bool: @@ -46,9 +49,9 @@ def attack(self: Self, target: Character) -> None: weapon = bare_hands(self.world) else: weapon = self.main_hand - target.on_attack(weapon) + target.on_attack(weapon) print( - self.world.l10n.format_value( + self.l10n.format_value( "attack-character-message", { "source": self.name, @@ -63,20 +66,20 @@ def on_attack(self: Self, weapon: Weapon): def _on_death(self: Self, player: Player): """ - Automatic on death call. + Automatic on death call. """ assert not self.alive, "On_death called while entity is alive" print( - self.world.l10n.format_value( - "attack-character-dead-message", - { - "target": self.name, - }, - ) + self.l10n.format_value( + "attack-character-dead-message", + { + "target": self.name, + }, ) + ) # Populate area with loot print( - self.world.l10n.format_value( + self.l10n.format_value( "attack-drop-begin", { "target": self.name, @@ -88,7 +91,7 @@ def _on_death(self: Self, player: Player): player.area.contents[loot_item.name] = loot_item player.seen_entities[loot_item.name] = loot_item print( - self.world.l10n.format_value( + self.l10n.format_value( "attack-drop-single", {"item": loot_item.name}, ) @@ -96,9 +99,9 @@ def _on_death(self: Self, player: Player): # if the target is dead, remove it from the area and drop their inventory del player.area.contents[self.name] del player.seen_entities[self.name] - - # remove entity from area - + # remove entity from area +if TYPE_CHECKING: + from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/container.py b/src/fantasy_forge/container.py index 7b6a9e3..0d2c666 100644 --- a/src/fantasy_forge/container.py +++ b/src/fantasy_forge/container.py @@ -1,14 +1,21 @@ -from typing import Self, Any +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Self from fantasy_forge.entity import Entity from fantasy_forge.inventory import Inventory -from fantasy_forge.world import World class Container(Entity, Inventory): __important_attributes__ = ("name", "capacity") - def __init__(self: Self, world: World, config_dict: dict[str, Any]): + def __init__( + self: Self, config_dict: dict[str, Any], l10n: FluentLocalization + ) -> None: capacity: int = config_dict.get("capacity", 10) - Entity.__init__(world, capacity) - Inventory.__init__(world, config_dict) + Entity.__init__(capacity, l10n) + Inventory.__init__(config_dict, l10n) + + +if TYPE_CHECKING: + from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/enemy.py b/src/fantasy_forge/enemy.py index 53885a3..d7f6c8f 100644 --- a/src/fantasy_forge/enemy.py +++ b/src/fantasy_forge/enemy.py @@ -1,20 +1,22 @@ -from typing import Any, Self - -from fantasy_forge.character import bare_hands, Character -from fantasy_forge.item import Item -from fantasy_forge.world import World +from __future__ import annotations +from typing import TYPE_CHECKING, Any, Self +from fantasy_forge.character import Character, bare_hands +from fantasy_forge.item import Item class Enemy(Character): """An enemy is a person which will fight back.""" - def __init__(self: Self, world: World, config_dict: dict[str, Any]): - super().__init__(world, config_dict) + def __init__(self: Self, config_dict: dict[str, Any], l10n: FluentLocalization): + super().__init__(config_dict, l10n) for item_dict in config_dict.get("loot", []): - self.inventory.add(Item(world, item_dict)) + self.inventory.add(Item(item_dict, l10n)) def __str__(self: Self) -> str: return self.name + +if TYPE_CHECKING: + from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/entity.py b/src/fantasy_forge/entity.py index d5c4f4d..638f2fd 100644 --- a/src/fantasy_forge/entity.py +++ b/src/fantasy_forge/entity.py @@ -8,27 +8,25 @@ class Entity: __important_attributes__ = ("name",) - world: World name: str description: str obvious: bool # obvious entities are seen when entering the room + l10n: FluentLocalization def __init__( - self: Self, - world: World, - config_dict: dict[str, Any], + self: Self, config_dict: dict[str, Any], l10n: FluentLocalization ) -> None: - self.world = world self.name = config_dict.pop("name") self.description = config_dict.pop("description", "") self.obvious = config_dict.pop("obvious", False) + self.l10n = l10n def on_look(self: Self) -> str: return self.description def on_use(self: Self, other: Entity | None = None): print( - self.world.l10n.format_value( + self.l10n.format_value( "cannot-use-message", { "self": self.name, @@ -56,4 +54,4 @@ def to_dict(self: Self) -> dict: if TYPE_CHECKING: - from fantasy_forge.world import World + from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/gateway.py b/src/fantasy_forge/gateway.py index e8a9f66..6d2d77e 100644 --- a/src/fantasy_forge/gateway.py +++ b/src/fantasy_forge/gateway.py @@ -1,11 +1,10 @@ from __future__ import annotations -from typing import Any, Self +from typing import TYPE_CHECKING, Any, Self from fantasy_forge.entity import Entity from fantasy_forge.item import Item from fantasy_forge.key import Key -from fantasy_forge.world import World class Gateway(Entity): @@ -17,20 +16,16 @@ class Gateway(Entity): locked: bool key_list: list[str] - def __init__( - self: Self, - world: World, - config_dict: dict[str, Any], - ): + def __init__(self: Self, config_dict: dict[str, Any], l10n: FluentLocalization): self.target = config_dict.pop("target") self.locked = config_dict.pop("locked", False) self.key_list = config_dict.pop("key_list", []) - super().__init__(world, config_dict) + super().__init__(config_dict, l10n) def on_look(self: Self) -> str: text = [] if self.key_list and self.locked: - text.append(self.world.l10n.format_value("gateway-on-look-locked")) + text.append(self.l10n.format_value("gateway-on-look-locked")) text.append(self.description) return "\n".join(text) @@ -40,7 +35,7 @@ def on_use(self: Self, other: Item | None = None): return if not self.key_list: print( - self.world.l10n.format_value( + self.l10n.format_value( "gateway-no-keys", { "name": self.name, @@ -49,10 +44,12 @@ def on_use(self: Self, other: Item | None = None): ) if not isinstance(other, Key): print( - self.world.l10n.format_value("gateway-key-needed", - { - "name": self.name, - },) + self.l10n.format_value( + "gateway-key-needed", + { + "name": self.name, + }, + ) ) return if self.locked: @@ -65,7 +62,7 @@ def on_unlock(self: Self, key: Key): self.locked = False key.used = True print( - self.world.l10n.format_value( + self.l10n.format_value( "gateway-unlock-message", { "name": self.name, @@ -78,7 +75,7 @@ def on_lock(self: Self, key: Key): self.locked = True key.used = True print( - self.world.l10n.format_value( + self.l10n.format_value( "gateway-lock-message", { "name": self.name, @@ -90,3 +87,7 @@ def to_dict(self: Self) -> dict: gateway_dict: dict = super().to_dict() gateway_dict["target"] = self.target return gateway_dict + + +if TYPE_CHECKING: + from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/inventory.py b/src/fantasy_forge/inventory.py index 98967ae..a9b0da1 100644 --- a/src/fantasy_forge/inventory.py +++ b/src/fantasy_forge/inventory.py @@ -1,23 +1,22 @@ from __future__ import annotations -from typing import Iterator, Self +from typing import TYPE_CHECKING, Iterator, Self from fantasy_forge.item import Item from fantasy_forge.localization import highlight_interactive -from fantasy_forge.world import World class Inventory: """An Inventory contains multiple items.""" - world: World capacity: int contents: dict[str, Item] + l10n: FluentLocalization - def __init__(self: Self, world: World, capacity: int): - self.world = world + def __init__(self: Self, capacity: int, l10n: FluentLocalization): self.capacity = capacity self.contents = {} + self.l10n = l10n def __len__(self: Self) -> int: """Returns current capacity.""" @@ -43,7 +42,7 @@ def add(self: Self, item: Item) -> None: self.contents[item.name] = item else: raise Exception( - self.world.l10n.format_value( + self.l10n.format_value( "inventory-capacity-message", { "capacity": self.capacity, @@ -63,9 +62,9 @@ def pop(self: Self, item_name: str) -> Item | None: def on_look(self: Self) -> str: if not self.contents: - return self.world.l10n.format_value("inventory-look-empty-message") + return self.l10n.format_value("inventory-look-empty-message") else: - return self.world.l10n.format_value( + return self.l10n.format_value( "inventory-look-message", { "items": ", ".join( @@ -73,3 +72,7 @@ def on_look(self: Self) -> str: ), }, ) + + +if TYPE_CHECKING: + from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/item.py b/src/fantasy_forge/item.py index 96af36a..05d9108 100644 --- a/src/fantasy_forge/item.py +++ b/src/fantasy_forge/item.py @@ -1,9 +1,8 @@ from __future__ import annotations -from typing import Any, Self +from typing import TYPE_CHECKING, Any, Self from fantasy_forge.entity import Entity -from fantasy_forge.world import World class Item(Entity): @@ -14,10 +13,12 @@ class Item(Entity): moveable: bool carryable: bool - def __init__(self: Self, world: World, config_dict: dict[str, Any]) -> None: + def __init__( + self: Self, config_dict: dict[str, Any], l10n: FluentLocalization + ) -> None: self.moveable = config_dict.pop("moveable", True) self.carryable = config_dict.pop("carryable", True) - super().__init__(world, config_dict) + super().__init__(config_dict, l10n) def __repr__(self: Self) -> str: return f"Item({self.name}, {self.description}, moveable={self.moveable}, carryable={self.carryable})" @@ -33,6 +34,10 @@ def to_dict(self: Self) -> dict: return item_dict @staticmethod - def from_dict(world: World, item_dict: dict) -> Item: - item: Item = Item(world, item_dict) + def from_dict(item_dict: dict, l10n: FluentLocalization) -> Item: + item: Item = Item(item_dict, l10n) return item + + +if TYPE_CHECKING: + from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/key.py b/src/fantasy_forge/key.py index 41d9652..253cdb6 100644 --- a/src/fantasy_forge/key.py +++ b/src/fantasy_forge/key.py @@ -1,9 +1,8 @@ from __future__ import annotations -from typing import Any, Self +from typing import TYPE_CHECKING, Any, Self from fantasy_forge.item import Item -from fantasy_forge.world import World class Key(Item): @@ -14,14 +13,20 @@ class Key(Item): key_id: str used: bool # wether the key was used already - def __init__(self: Self, world: World, config_dict: dict[str, Any]) -> None: + def __init__( + self: Self, config_dict: dict[str, Any], l10n: FluentLocalization + ) -> None: self.key_id = config_dict.pop("key_id") self.moveable = True # keys are moveable by default self.carryable = True # keys are carryable by default - super().__init__(world, config_dict) + super().__init__(config_dict, l10n) self.used = False def __eq__(self: Self, other: Key): """Compares key ids.""" return self.key_id == other.key_id + + +if TYPE_CHECKING: + from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/player.py b/src/fantasy_forge/player.py index c2122aa..0de2a78 100644 --- a/src/fantasy_forge/player.py +++ b/src/fantasy_forge/player.py @@ -3,7 +3,7 @@ from fantasy_forge.area import Area from fantasy_forge.armour import ARMOUR_TYPES, Armour -from fantasy_forge.character import bare_hands, Character +from fantasy_forge.character import Character, bare_hands from fantasy_forge.entity import Entity from fantasy_forge.gateway import Gateway from fantasy_forge.item import Item @@ -14,8 +14,6 @@ BASE_PLAYER_HEALTH = 100 - - class Player(Character): __important_attributes__ = ("name", "area", "health") @@ -25,14 +23,15 @@ class Player(Character): def __init__(self: Self, world: World, name: str, health: int = BASE_PLAYER_HEALTH): super().__init__( - world, dict( name=name, description=world.l10n.format_value("player-description"), health=health, ), + world.l10n, ) - self.area = Area.empty(world) + self.world = world + self.area = Area.empty(world.l10n) # put us in the void # We will (hopefully) never see this, but it's important for the # transition to the next area. @@ -40,7 +39,7 @@ def __init__(self: Self, world: World, name: str, health: int = BASE_PLAYER_HEAL self.seen_entities = {} # define armour slots - self.armour_slots: dict[str, Armour|None] = {} + self.armour_slots: dict[str, Armour | None] = {} for armour_type in ARMOUR_TYPES: self.armour_slots[armour_type] = None @@ -64,7 +63,7 @@ def look_around(self): if entity is self: continue print( - self.world.l10n.format_value( + self.l10n.format_value( "look-around-single", { "object": entity.name, @@ -78,7 +77,7 @@ def look_at(self, name: str): entity = self.seen_entities.get(name) if entity is None: print( - self.world.l10n.format_value( + self.l10n.format_value( "entity-not-seen", { "entity": name, @@ -87,7 +86,7 @@ def look_at(self, name: str): ) return print( - self.world.l10n.format_value( + self.l10n.format_value( "look-at-message", { "object": entity.name, @@ -101,7 +100,7 @@ def pick_up(self, item_name: str): item = self.seen_entities.get(item_name) if item is None: print( - self.world.l10n.format_value( + self.l10n.format_value( "entity-does-not-exist", {"entity": item_name}, ) @@ -109,9 +108,9 @@ def pick_up(self, item_name: str): return if item_name not in self.area.contents: if item_name in self.inventory.contents: - print(self.world.l10n.format_value("item-is-in-inventory")) + print(self.l10n.format_value("item-is-in-inventory")) return - print(self.world.l10n.format_value("item-vanished")) + print(self.l10n.format_value("item-vanished")) self.seen_entities.pop(item_name) return if isinstance(item, Item) and item.carryable: @@ -119,7 +118,7 @@ def pick_up(self, item_name: str): # picking up items keeps them in seen_entities self.area.contents.pop(item_name) print( - self.world.l10n.format_value( + self.l10n.format_value( "pick-up-item-message", { "item": item.name, @@ -127,7 +126,7 @@ def pick_up(self, item_name: str): ) ) else: - print(self.world.l10n.format_value("pick-up-failed-message")) + print(self.l10n.format_value("pick-up-failed-message")) def equip(self, item_name: str): """Equips item.""" @@ -135,7 +134,7 @@ def equip(self, item_name: str): # check if item was already seen if item is None: print( - self.world.l10n.format_value( + self.l10n.format_value( "entity-does-not-exist", {"entity": item_name}, ) @@ -146,7 +145,7 @@ def equip(self, item_name: str): item_name not in self.area.contents and item_name not in self.inventory.contents ): - print(self.world.l10n.format_value("item-vanished")) + print(self.l10n.format_value("item-vanished")) self.seen_entities.pop(item_name) return @@ -157,7 +156,7 @@ def equip(self, item_name: str): # picking up items keeps them in seen_entities self.area.contents.pop(item_name) print( - self.world.l10n.format_value( + self.l10n.format_value( "pick-up-item-message", { "item": item.name, @@ -170,7 +169,7 @@ def equip(self, item_name: str): self.equip_armour(item) else: print( - self.world.l10n.format_value( + self.l10n.format_value( "cannot-equip", { "weapon": item.name, @@ -182,7 +181,7 @@ def equip_weapon(self, weapon: Weapon): """Equips weapon.""" self.main_hand = weapon print( - self.world.l10n.format_value( + self.l10n.format_value( "equip-item-message", { "player": self.name, @@ -197,7 +196,7 @@ def equip_armour(self, armour: Armour) -> None: # check if armour slot is already filled if current_armour is not None: print( - self.world.l10n.format_value( + self.l10n.format_value( "unequip-item-message", { "player": self.name, @@ -208,7 +207,7 @@ def equip_armour(self, armour: Armour) -> None: self.armour_slots[armour.armour_type] = armour print( - self.world.l10n.format_value( + self.l10n.format_value( "equip-item-message", { "player": self.name, @@ -228,7 +227,7 @@ def unequip(self, item_name: str): if armour_item is item: self.armour_slots[armour_type] = None print( - self.world.l10n.format_value( + self.l10n.format_value( "unequip-item-message", { "player": self.name, @@ -243,20 +242,18 @@ def attack(self, target_name: str) -> None: target = self.seen_entities.get(target_name) if target is None: print( - self.world.l10n.format_value( + self.l10n.format_value( "entity-does-not-exist", {"entity": target_name}, ) ) return if target_name not in self.area.contents: - print(self.world.l10n.format_value("item-vanished")) + print(self.l10n.format_value("item-vanished")) self.seen_entities.pop(target_name) return if not isinstance(target, Character): - print( - self.world.l10n.format_value("cannot-attack", {"target": target_name}) - ) + print(self.l10n.format_value("cannot-attack", {"target": target_name})) return super().attack(target) @@ -264,7 +261,7 @@ def attack(self, target_name: str) -> None: # give the enemy an option for revenge target.attack(self) print( - self.world.l10n.format_value( + self.l10n.format_value( "attack-character-alive-message", { "target": target.name, @@ -273,24 +270,24 @@ def attack(self, target_name: str) -> None: ) ) else: - target._on_death(self) + target._on_death(self) if self.alive: print( - self.world.l10n.format_value( + self.l10n.format_value( "player-health-remaining", {"health": self.health}, ) ) else: # self._on_death(self) - print(self.world.l10n.format_value("player-died")) + print(self.l10n.format_value("player-died")) exit() def use(self, subject_name: str, other_name: str | None = None): subject = self.seen_entities.get(subject_name) if subject is None: print( - self.world.l10n.format_value( + self.l10n.format_value( "entity-not-seen", { "entity": subject_name, @@ -306,7 +303,7 @@ def use(self, subject_name: str, other_name: str | None = None): other = self.seen_entities.get(other_name) if other is None: print( - self.world.l10n.format_value( + self.l10n.format_value( "entity-not-seen", { "entity": other_name, @@ -326,26 +323,26 @@ def go(self, gateway_name: str): gateway = self.seen_entities.get(gateway_name) if gateway is None: print( - self.world.l10n.format_value( + self.l10n.format_value( "entity-does-not-exist", {"entity": gateway_name}, ) ) return if gateway_name not in self.area.contents: - print(self.world.l10n.format_value("item-vanished")) + print(self.l10n.format_value("item-vanished")) self.seen_entities.pop(gateway_name) return if isinstance(gateway, Gateway): self.enter_gateway(gateway) else: - print(self.world.l10n.format_value("go-failed-message")) + print(self.l10n.format_value("go-failed-message")) def enter_gateway(self: Self, gateway: Gateway): """Uses gateway to enter a new area.""" if gateway.locked: print( - self.world.l10n.format_value( + self.l10n.format_value( "gateway-locked-message", { "gateway": gateway.name, @@ -369,7 +366,7 @@ def enter_area(self, new_area: Area): # enter the new one self.area.contents[self.name] = self print( - self.world.l10n.format_value( + self.l10n.format_value( "enter-area-message", { "area": self.area.name, @@ -379,7 +376,7 @@ def enter_area(self, new_area: Area): for entity in self.area.contents.values(): if entity.obvious: print( - self.world.l10n.format_value( + self.l10n.format_value( "look-around-single", { "object": entity.name, @@ -398,7 +395,7 @@ def drop(self, item_name: str): item = self.inventory.pop(item_name) if item is None: print( - self.world.l10n.format_value( + self.l10n.format_value( "drop-not-found", { "item": item_name, @@ -410,7 +407,7 @@ def drop(self, item_name: str): if self.main_hand is item: # clears main hand if item was dropped from it self.main_hand = None print( - self.world.l10n.format_value( + self.l10n.format_value( "dropped", { "item": item_name, @@ -437,7 +434,7 @@ def main_loop(self): ] ) print( - self.world.l10n.format_value( + self.l10n.format_value( quit_message, {}, ) diff --git a/src/fantasy_forge/weapon.py b/src/fantasy_forge/weapon.py index e32fc06..a137e17 100644 --- a/src/fantasy_forge/weapon.py +++ b/src/fantasy_forge/weapon.py @@ -1,7 +1,8 @@ -from typing import Any, Self +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Self from fantasy_forge.item import Item -from fantasy_forge.world import World class Weapon(Item): @@ -11,6 +12,12 @@ class Weapon(Item): damage: int - def __init__(self: Self, world: World, config_dict: dict[str, Any]) -> None: + def __init__( + self: Self, config_dict: dict[str, Any], l10n: FluentLocalization + ) -> None: self.damage = config_dict.pop("damage") - super().__init__(world, config_dict) + super().__init__(config_dict, l10n) + + +if TYPE_CHECKING: + from fluent.runtime import FluentLocalization From 3a19e9f649dd99f7f685df5b19647eada83f7318 Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 5 Jan 2025 15:26:54 +0100 Subject: [PATCH 03/21] common paths --- src/fantasy_forge/localization.py | 3 ++- src/fantasy_forge/utils.py | 22 ++++++---------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/fantasy_forge/localization.py b/src/fantasy_forge/localization.py index 3bb9621..a71c6a4 100644 --- a/src/fantasy_forge/localization.py +++ b/src/fantasy_forge/localization.py @@ -5,7 +5,8 @@ from fluent.runtime import FluentLocalization, FluentResourceLoader from fluent.runtime.types import FluentNone -LOCALE_FOLDER: Path = Path("data/l10n/") +from fantasy_forge.utils import LOCALE_FOLDER + DEFAULT_LOCALE: str = "en" diff --git a/src/fantasy_forge/utils.py b/src/fantasy_forge/utils.py index 11bca1d..d050e64 100644 --- a/src/fantasy_forge/utils.py +++ b/src/fantasy_forge/utils.py @@ -1,19 +1,9 @@ -from fantasy_forge.area import Area -from fantasy_forge.item import Item +from pathlib import Path +SOURCE_FOLDER: Path = Path(__file__).parent.resolve() # fantasy_forge/src/fantasy_forge -def pickup_menu(area: Area) -> Item | None: - # filter items contained in area - pickup_items: list[Item] = list( - filter(lambda c: isinstance(c, Item), area.contents) - ) +ROOT_FOLDER: Path = SOURCE_FOLDER.parent.parent.resolve() # fantasy_forge - for idx, item in enumerate(pickup_items): - print(f"[{idx:>2}] {item.name}") - print("[ q] Quit") - selection: str = input(area.world.l10n.format_value("pick-up-item-menu") + " ") - if selection.upper() == "Q": - return None - if selection.isnumeric(): - selection_index = int(selection) - return pickup_items[selection_index] +DATA_FOLDER: Path = ROOT_FOLDER / "data" # fantasy_forge/data +WORLDS_FOLDER: Path = DATA_FOLDER / "worlds" # fantasy_forge/data/worlds +LOCALE_FOLDER: Path = DATA_FOLDER / "l10n" # fantasy_forge/data/l10n From f1b5c74c7a4cb3b603537e1cd7d22adc9b81d270 Mon Sep 17 00:00:00 2001 From: dodo Date: Sat, 14 Dec 2024 02:26:24 +0100 Subject: [PATCH 04/21] new function for asset loading --- data/worlds/chaosdorf/areas/cave.toml | 2 +- ...ference-room.toml => conference room.toml} | 2 +- ...ones-toilet.toml => everyones toilet.toml} | 0 ...{flinta-toilet.toml => flinta toilet.toml} | 0 data/worlds/chaosdorf/areas/floor.toml | 8 +- data/worlds/chaosdorf/areas/lounge.toml | 2 +- ...remium-toilet.toml => premium toilet.toml} | 0 data/worlds/chaosdorf/world.toml | 6 +- src/fantasy_forge/load_assets.py | 31 +++++++ src/fantasy_forge/player.py | 9 +- src/fantasy_forge/world.py | 92 +++++++++++++++---- 11 files changed, 121 insertions(+), 31 deletions(-) rename data/worlds/chaosdorf/areas/{conference-room.toml => conference room.toml} (96%) rename data/worlds/chaosdorf/areas/{everyones-toilet.toml => everyones toilet.toml} (100%) rename data/worlds/chaosdorf/areas/{flinta-toilet.toml => flinta toilet.toml} (100%) rename data/worlds/chaosdorf/areas/{premium-toilet.toml => premium toilet.toml} (100%) create mode 100644 src/fantasy_forge/load_assets.py diff --git a/data/worlds/chaosdorf/areas/cave.toml b/data/worlds/chaosdorf/areas/cave.toml index 2fb6791..b0af6b9 100644 --- a/data/worlds/chaosdorf/areas/cave.toml +++ b/data/worlds/chaosdorf/areas/cave.toml @@ -1,4 +1,4 @@ -name = "Cave" +name = "cave" description = "a comfy room with sofas and a tv" [[contents]] diff --git a/data/worlds/chaosdorf/areas/conference-room.toml b/data/worlds/chaosdorf/areas/conference room.toml similarity index 96% rename from data/worlds/chaosdorf/areas/conference-room.toml rename to data/worlds/chaosdorf/areas/conference room.toml index edcfc72..ebc8fd8 100644 --- a/data/worlds/chaosdorf/areas/conference-room.toml +++ b/data/worlds/chaosdorf/areas/conference room.toml @@ -1,4 +1,4 @@ -name = "confeerence room" +name = "confererence room" description = "a room with a giant tv, a conference table, and a window." [[contents]] diff --git a/data/worlds/chaosdorf/areas/everyones-toilet.toml b/data/worlds/chaosdorf/areas/everyones toilet.toml similarity index 100% rename from data/worlds/chaosdorf/areas/everyones-toilet.toml rename to data/worlds/chaosdorf/areas/everyones toilet.toml diff --git a/data/worlds/chaosdorf/areas/flinta-toilet.toml b/data/worlds/chaosdorf/areas/flinta toilet.toml similarity index 100% rename from data/worlds/chaosdorf/areas/flinta-toilet.toml rename to data/worlds/chaosdorf/areas/flinta toilet.toml diff --git a/data/worlds/chaosdorf/areas/floor.toml b/data/worlds/chaosdorf/areas/floor.toml index 80cb99f..9de2b36 100644 --- a/data/worlds/chaosdorf/areas/floor.toml +++ b/data/worlds/chaosdorf/areas/floor.toml @@ -10,19 +10,19 @@ obvious = true [[contents]] kind = "gateway" name = "everyones toilet door" -target = "everyones-toilet" +target = "everyones toilet" obvious = true [[contents]] kind = "gateway" name = "flinta toilet door" -target = "flinta-toilet" +target = "flinta toilet" obvious = true [[contents]] kind = "gateway" name = "premium toilet door" -target = "premium-toilet" +target = "premium toilet" obvious = true [[contents]] @@ -34,7 +34,7 @@ obvious = true [[contents]] kind = "gateway" name = "conference room door" -target = "conference-room" +target = "conference room" obvious = true [[contents]] diff --git a/data/worlds/chaosdorf/areas/lounge.toml b/data/worlds/chaosdorf/areas/lounge.toml index b3ae3db..5245c8f 100644 --- a/data/worlds/chaosdorf/areas/lounge.toml +++ b/data/worlds/chaosdorf/areas/lounge.toml @@ -1,4 +1,4 @@ -name = "Lounge" +name = "lounge" description = "a place to eat and socialize" [[contents]] diff --git a/data/worlds/chaosdorf/areas/premium-toilet.toml b/data/worlds/chaosdorf/areas/premium toilet.toml similarity index 100% rename from data/worlds/chaosdorf/areas/premium-toilet.toml rename to data/worlds/chaosdorf/areas/premium toilet.toml diff --git a/data/worlds/chaosdorf/world.toml b/data/worlds/chaosdorf/world.toml index 584d8a0..a8fbf5d 100644 --- a/data/worlds/chaosdorf/world.toml +++ b/data/worlds/chaosdorf/world.toml @@ -1,4 +1,4 @@ -name = "Chaosdorf" +name = "chaosdorf" language = "en" -areas = ["cave", "lounge", "kitchen", "hackcenter", "floor", "elab", "conference-room", "everyones-toilet", "flinta-toilet", "premium-toilet", "wetlab"] -spawn = "cave" \ No newline at end of file +areas = ["cave", "lounge", "kitchen", "hackcenter", "floor", "elab", "conference room", "everyones toilet", "flinta toilet", "premium toilet", "wetlab"] +spawn = "cave" diff --git a/src/fantasy_forge/load_assets.py b/src/fantasy_forge/load_assets.py new file mode 100644 index 0000000..e0fb844 --- /dev/null +++ b/src/fantasy_forge/load_assets.py @@ -0,0 +1,31 @@ +import logging + +from fantasy_forge.area import Area +from fantasy_forge.armour import Armour +from fantasy_forge.character import Character +from fantasy_forge.container import Container +from fantasy_forge.enemy import Enemy +from fantasy_forge.entity import Entity +from fantasy_forge.gateway import Gateway +from fantasy_forge.inventory import Inventory +from fantasy_forge.item import Item +from fantasy_forge.key import Key +from fantasy_forge.player import Player +from fantasy_forge.weapon import Weapon + +logger = logging.getLogger(__name__) + +ASSET_TYPES: dict[str, type] = { + "areas": Area, + "armour": Armour, + "characters": Character, + "containers": Container, + "enemies": Enemy, + "entities": Entity, + "gateways": Gateway, + "inventories": Inventory, + "items": Item, + "keys": Key, + "players": Player, + "weapons": Weapon, +} diff --git a/src/fantasy_forge/player.py b/src/fantasy_forge/player.py index 0de2a78..e9f3b4a 100644 --- a/src/fantasy_forge/player.py +++ b/src/fantasy_forge/player.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import random -from typing import Self +from typing import TYPE_CHECKING, Self from fantasy_forge.area import Area from fantasy_forge.armour import ARMOUR_TYPES, Armour @@ -9,7 +11,6 @@ from fantasy_forge.item import Item from fantasy_forge.shell import Shell from fantasy_forge.weapon import Weapon -from fantasy_forge.world import World BASE_PLAYER_HEALTH = 100 @@ -439,3 +440,7 @@ def main_loop(self): {}, ) ) + + +if TYPE_CHECKING: + from fantasy_forge.world import World diff --git a/src/fantasy_forge/world.py b/src/fantasy_forge/world.py index 4c5e4f1..8dccf30 100644 --- a/src/fantasy_forge/world.py +++ b/src/fantasy_forge/world.py @@ -1,13 +1,16 @@ from __future__ import annotations import logging +from collections import defaultdict from pathlib import Path -from typing import TYPE_CHECKING, Self +from typing import IO, TYPE_CHECKING, Self import toml from fantasy_forge.area import Area +from fantasy_forge.load_assets import ASSET_TYPES from fantasy_forge.localization import get_fluent_locale +from fantasy_forge.utils import WORLDS_FOLDER logger = logging.getLogger(__name__) # logger.setLevel(logging.DEBUG) @@ -15,19 +18,32 @@ class World: """A world contains many rooms. It's where the game happens.""" + l10n: FluentLocalization - areas: dict[str, Area] name: str + areas: dict[str, Area] spawn: str # area name to spawn in + assets: dict[str, list[ASSET_TYPE]] # store of all loaded assets def __init__( - self: Self, l10n: FluentLocalization, name: str, areas: dict[str, Area], spawn: str + self: Self, + l10n: FluentLocalization, + name: str, + areas: dict[str, Area], + spawn: str, ): self.l10n = l10n self.name = name self.areas = areas self.spawn = spawn + self.assets = defaultdict(list) + self._load_assets() + + # populate areas dict + for area in self.assets["Area"]: + self.areas[area.name] = area + @property def spawn_point(self) -> Area: """Returns spawnpoint as area.""" @@ -35,23 +51,61 @@ def spawn_point(self) -> Area: @staticmethod def load(name: str) -> World: - path = Path("data/worlds") / name - if not path.exists(): - logger.debug(f"Path {path} not found, using {name}") - path = Path(name) + world_path = WORLDS_FOLDER / name + + if not world_path.exists(): + logger.debug(f"Path {world_path} not found, using {name}") + world_path = Path(name) + + world_toml_path: Path = world_path / "world.toml" + with world_toml_path.open() as world_file: + world_toml_data: dict = toml.load(world_file) + + world_name: str = world_toml_data["name"] + assert world_name == name + areas: dict[str, Area] = dict() - with (path / "world.toml").open() as world_file: - world_toml = toml.load(world_file) - language: str = world_toml["language"] - world_name: str = world_toml["name"] - l10n: FluentLocalization = get_fluent_locale(language) - logger.debug("language") - logger.debug(language) - world_spawn: str = world_toml["spawn"] - world = World(l10n, world_name, areas, world_spawn) - for area_name in world_toml["areas"]: - areas[area_name] = Area.load(world, path, area_name) + + # load language for localization + language: str = world_toml_data["language"] + l10n: FluentLocalization = get_fluent_locale(language) + logger.debug("language") + logger.debug(language) + + world_spawn: str = world_toml_data["spawn"] + world = World(l10n, world_name, areas, world_spawn) return world + def _load_assets(self): + world_path = WORLDS_FOLDER / self.name + + # iterate through world dir + toml_path: Path + for toml_path in world_path.glob("**/*.toml"): + asset_type: type + parent: str = toml_path.parent.name + + # infer type from parent directory + if parent in ASSET_TYPES.keys(): + asset_type = ASSET_TYPES[parent] + else: + logger.info("skipped " + toml_path.name) + continue + + # read toml + io: IO + with toml_path.open(encoding="utf-8") as io: + toml_data: dict = toml.load(io) + + # parse asset from toml data + if hasattr(asset_type, "from_dict"): + asset = asset_type.from_dict(toml_data, self.l10n) + else: + logger.info("skipped " + toml_path.name) + continue + + self.assets[asset_type.__name__].append(asset) + + if TYPE_CHECKING: - from fluent.runtime import FluentLocalization \ No newline at end of file + from fluent.runtime import FluentLocalization From 5201af6fc652e39b5e4e24b1cbbe47cbc96d059e Mon Sep 17 00:00:00 2001 From: dodo Date: Sat, 14 Dec 2024 14:48:42 +0100 Subject: [PATCH 05/21] adds test world --- data/worlds/test/areas/test_area.toml | 2 ++ data/worlds/test/characters/Enemy/test_enemy.toml | 3 +++ data/worlds/test/characters/Player/test_player.toml | 3 +++ data/worlds/test/characters/test_character.toml | 3 +++ data/worlds/test/containers/test_container.toml | 3 +++ data/worlds/test/entities/test_entity.toml | 3 +++ data/worlds/test/gateways/test_gateway.toml | 4 ++++ data/worlds/test/inventories/test_inventory.toml | 3 +++ data/worlds/test/items/Armour/test_armour.toml | 7 +++++++ data/worlds/test/items/Key/test_key.toml | 4 ++++ data/worlds/test/items/Weapon/test_weapon.toml | 3 +++ data/worlds/test/items/test_item.toml | 5 +++++ data/worlds/test/world.toml | 4 ++++ 13 files changed, 47 insertions(+) create mode 100644 data/worlds/test/areas/test_area.toml create mode 100644 data/worlds/test/characters/Enemy/test_enemy.toml create mode 100644 data/worlds/test/characters/Player/test_player.toml create mode 100644 data/worlds/test/characters/test_character.toml create mode 100644 data/worlds/test/containers/test_container.toml create mode 100644 data/worlds/test/entities/test_entity.toml create mode 100644 data/worlds/test/gateways/test_gateway.toml create mode 100644 data/worlds/test/inventories/test_inventory.toml create mode 100644 data/worlds/test/items/Armour/test_armour.toml create mode 100644 data/worlds/test/items/Key/test_key.toml create mode 100644 data/worlds/test/items/Weapon/test_weapon.toml create mode 100644 data/worlds/test/items/test_item.toml create mode 100644 data/worlds/test/world.toml diff --git a/data/worlds/test/areas/test_area.toml b/data/worlds/test/areas/test_area.toml new file mode 100644 index 0000000..313f6d4 --- /dev/null +++ b/data/worlds/test/areas/test_area.toml @@ -0,0 +1,2 @@ +name = "test area" +description = "an area could be a dungeon, a room, a castle" diff --git a/data/worlds/test/characters/Enemy/test_enemy.toml b/data/worlds/test/characters/Enemy/test_enemy.toml new file mode 100644 index 0000000..da5b134 --- /dev/null +++ b/data/worlds/test/characters/Enemy/test_enemy.toml @@ -0,0 +1,3 @@ +name = "test enemy" +description = "an enemy is a character which will attack the player" +health = 100 diff --git a/data/worlds/test/characters/Player/test_player.toml b/data/worlds/test/characters/Player/test_player.toml new file mode 100644 index 0000000..8599c8d --- /dev/null +++ b/data/worlds/test/characters/Player/test_player.toml @@ -0,0 +1,3 @@ +name = "test player" +description = "the playable character" +health = 100 diff --git a/data/worlds/test/characters/test_character.toml b/data/worlds/test/characters/test_character.toml new file mode 100644 index 0000000..de35f15 --- /dev/null +++ b/data/worlds/test/characters/test_character.toml @@ -0,0 +1,3 @@ +name = "test character" +description = "a character is a person in the world" +health = 100 diff --git a/data/worlds/test/containers/test_container.toml b/data/worlds/test/containers/test_container.toml new file mode 100644 index 0000000..04fe086 --- /dev/null +++ b/data/worlds/test/containers/test_container.toml @@ -0,0 +1,3 @@ +name = "test container" +description = "a container is an item which has an inventory" +capacity = 10 diff --git a/data/worlds/test/entities/test_entity.toml b/data/worlds/test/entities/test_entity.toml new file mode 100644 index 0000000..6233c6f --- /dev/null +++ b/data/worlds/test/entities/test_entity.toml @@ -0,0 +1,3 @@ +name = "test entity" +description = "an entity is an abstract thing" +obvious = true diff --git a/data/worlds/test/gateways/test_gateway.toml b/data/worlds/test/gateways/test_gateway.toml new file mode 100644 index 0000000..a874ae6 --- /dev/null +++ b/data/worlds/test/gateways/test_gateway.toml @@ -0,0 +1,4 @@ +name = "test gateway" +description = "a gateway is a portal to another area" +target = "test area" +locked = false diff --git a/data/worlds/test/inventories/test_inventory.toml b/data/worlds/test/inventories/test_inventory.toml new file mode 100644 index 0000000..95f681f --- /dev/null +++ b/data/worlds/test/inventories/test_inventory.toml @@ -0,0 +1,3 @@ +name = "test inventory" +description = "an inventory can contain items" +capacity = 10 diff --git a/data/worlds/test/items/Armour/test_armour.toml b/data/worlds/test/items/Armour/test_armour.toml new file mode 100644 index 0000000..78c4cd9 --- /dev/null +++ b/data/worlds/test/items/Armour/test_armour.toml @@ -0,0 +1,7 @@ +name = "test armour" +description = "armour is an item which can be worn by the player" +obvious = true +moveable = true +carryable = true +armour_type = "head" +defense = 10 diff --git a/data/worlds/test/items/Key/test_key.toml b/data/worlds/test/items/Key/test_key.toml new file mode 100644 index 0000000..de5ed20 --- /dev/null +++ b/data/worlds/test/items/Key/test_key.toml @@ -0,0 +1,4 @@ +name = "test key" +description = "a key is an item which can be used by the player to open doors" +obvious = true +key_id = "dungeon key 1" diff --git a/data/worlds/test/items/Weapon/test_weapon.toml b/data/worlds/test/items/Weapon/test_weapon.toml new file mode 100644 index 0000000..45c7ad4 --- /dev/null +++ b/data/worlds/test/items/Weapon/test_weapon.toml @@ -0,0 +1,3 @@ +name = "test weapon" +description = "a weapon is an item which can be used by the player to damage his enemies" +damage = 10 diff --git a/data/worlds/test/items/test_item.toml b/data/worlds/test/items/test_item.toml new file mode 100644 index 0000000..ab88ce2 --- /dev/null +++ b/data/worlds/test/items/test_item.toml @@ -0,0 +1,5 @@ +name = "test item" +description = "an item is a entity in the world, which the player can interact with" +obvious = true +moveable = true +carryable = true diff --git a/data/worlds/test/world.toml b/data/worlds/test/world.toml new file mode 100644 index 0000000..f54640b --- /dev/null +++ b/data/worlds/test/world.toml @@ -0,0 +1,4 @@ +name = "test" +language = "en" +areas = ["test area"] +spawn = "test area" From 2f19577254c3c8ca42a842a77fa92f3548ba55f6 Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 5 Jan 2025 14:08:27 +0100 Subject: [PATCH 06/21] uses inherited l10n localization in player --- src/fantasy_forge/player.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/fantasy_forge/player.py b/src/fantasy_forge/player.py index e9f3b4a..e21ae29 100644 --- a/src/fantasy_forge/player.py +++ b/src/fantasy_forge/player.py @@ -12,6 +12,9 @@ from fantasy_forge.shell import Shell from fantasy_forge.weapon import Weapon +if TYPE_CHECKING: + from fantasy_forge.world import World + BASE_PLAYER_HEALTH = 100 @@ -32,7 +35,7 @@ def __init__(self: Self, world: World, name: str, health: int = BASE_PLAYER_HEAL world.l10n, ) self.world = world - self.area = Area.empty(world.l10n) + self.area = Area.empty(self.l10n) # put us in the void # We will (hopefully) never see this, but it's important for the # transition to the next area. @@ -341,6 +344,7 @@ def go(self, gateway_name: str): def enter_gateway(self: Self, gateway: Gateway): """Uses gateway to enter a new area.""" + # TODO: separate world from Player if gateway.locked: print( self.l10n.format_value( @@ -418,6 +422,7 @@ def drop(self, item_name: str): def main_loop(self): """Runs the game.""" + # TODO: enter spawn area in Shell preloop self.enter_area(self.world.spawn_point) Shell(self).cmdloop() # afterwards, leave the current area @@ -440,7 +445,3 @@ def main_loop(self): {}, ) ) - - -if TYPE_CHECKING: - from fantasy_forge.world import World From 32bd2e758d1a40b6ac510f3640cd602a905c9e9d Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 5 Jan 2025 14:25:17 +0100 Subject: [PATCH 07/21] fixes typo in player --- src/fantasy_forge/player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fantasy_forge/player.py b/src/fantasy_forge/player.py index e21ae29..a176cd9 100644 --- a/src/fantasy_forge/player.py +++ b/src/fantasy_forge/player.py @@ -144,7 +144,7 @@ def equip(self, item_name: str): ) ) return - # item must be in the area or the inventory to be equiped + # item must be in the area or the inventory to be equipped if ( item_name not in self.area.contents and item_name not in self.inventory.contents @@ -261,7 +261,7 @@ def attack(self, target_name: str) -> None: return super().attack(target) - if target.alive: + if target.alive and hasattr(target, "attack"): # give the enemy an option for revenge target.attack(self) print( @@ -425,7 +425,7 @@ def main_loop(self): # TODO: enter spawn area in Shell preloop self.enter_area(self.world.spawn_point) Shell(self).cmdloop() - # afterwards, leave the current area + # afterward, leave the current area self.leave_area() quit_message = random.choice( [ From 099a2c246343de1cab9fcf82686aa27bcbeab342 Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 5 Jan 2025 14:45:32 +0100 Subject: [PATCH 08/21] removes obsolete todos --- src/fantasy_forge/player.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/fantasy_forge/player.py b/src/fantasy_forge/player.py index a176cd9..a5614eb 100644 --- a/src/fantasy_forge/player.py +++ b/src/fantasy_forge/player.py @@ -223,8 +223,6 @@ def equip_armour(self, armour: Armour) -> None: def unequip(self, item_name: str): """Unequips weapon or armour.""" item = self.seen_entities.get(item_name) - # TODO: Validate Item was already seen - # TODO: Validate Item is in inventory if self.main_hand is item: self.main_hand = None for armour_type, armour_item in self.armour_slots.items(): From 56cce8ca5233f05333f94b5d041c407795dd89c8 Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 5 Jan 2025 14:45:52 +0100 Subject: [PATCH 09/21] uses inherited localization in shell --- src/fantasy_forge/shell.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/fantasy_forge/shell.py b/src/fantasy_forge/shell.py index bfef5ef..d2a5420 100644 --- a/src/fantasy_forge/shell.py +++ b/src/fantasy_forge/shell.py @@ -1,10 +1,11 @@ from __future__ import annotations -import fuzzywuzzy.process import logging from cmd import Cmd from typing import TYPE_CHECKING +import fuzzywuzzy.process + from fantasy_forge.armour import Armour from fantasy_forge.character import Character from fantasy_forge.gateway import Gateway @@ -21,12 +22,12 @@ class Shell(Cmd): prompt = "> " def __new__(cls, player: Player) -> Shell: - match player.world.l10n.locales[0]: + match player.l10n.locales[0]: case "en": shell_type = ShellEn case default: raise RuntimeError( - player.world.l10n.format_value( + player.l10n.format_value( "unknown-language-error", { "language": default, @@ -49,20 +50,22 @@ def completenames(self, text, *ignored): return [name + " " for name in super().completenames(text, *ignored)] def default(self, line: str): - if len(line) < 3: """Display an error message, because the command was invalid.""" - print(self.player.world.l10n.format_value("shell-invalid-command")) + print(self.player.l10n.format_value("shell-invalid-command")) else: """Check for potential typos and recommend closest command""" commands = [x[3:] for x in self.get_names() if x.startswith("do_")] possibilities = fuzzywuzzy.process.extract(line, commands) closest_cmd, closest_ratio = possibilities[0] - print(self.player.world.l10n.format_value("shell-invalid-command"), f"Did you mean '{closest_cmd}'?") + print( + self.player.l10n.format_value("shell-invalid-command"), + f"Did you mean '{closest_cmd}'?", + ) def do_EOF(self, arg: str) -> bool: - """This is called if an EOF occures while parsing the command.""" + """This is called if an EOF occurs while parsing the command.""" return True @@ -183,12 +186,17 @@ def do_inventory(self, arg: str): def do_armour(self, arg: str): """shows the players armour""" for armour_type, armour_item in self.player.armour_slots.items(): - print(self.player.world.l10n.format_value("armour-detail", { - "type": armour_type, - "item": armour_item, - "item-name": getattr(armour_item, "name", None), - "item-defense":getattr(armour_item, "defense", None), - })) + print( + self.player.l10n.format_value( + "armour-detail", + { + "type": armour_type, + "item": armour_item, + "item-name": getattr(armour_item, "name", None), + "item-defense": getattr(armour_item, "defense", None), + }, + ) + ) def do_use(self, arg: str): """ From 544d26acfdd86af51b98359e76291836f548bb17 Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 5 Jan 2025 14:55:57 +0100 Subject: [PATCH 10/21] places player spawn in the main module --- src/fantasy_forge/main.py | 11 ++++++++--- src/fantasy_forge/player.py | 10 +++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/fantasy_forge/main.py b/src/fantasy_forge/main.py index 24c61ac..e8fa4a1 100644 --- a/src/fantasy_forge/main.py +++ b/src/fantasy_forge/main.py @@ -1,12 +1,12 @@ import logging from argparse import ArgumentParser -from sys import argv +from fantasy_forge.area import Area from fantasy_forge.player import Player from fantasy_forge.world import World -def parse_args(argv=argv[1:]): +def parse_args(): parser = ArgumentParser(description="Fantasy Forge: A text-based RPG") parser.add_argument("--version", action="version", version="%(prog)s 0.1.0") parser.add_argument("--world", help="The world to play in", default="chaosdorf") @@ -17,7 +17,7 @@ def parse_args(argv=argv[1:]): default="fantasy_forge.log", ) parser.add_argument("--loglevel", help="Severity Level for logging", default="INFO") - return parser.parse_args(argv) + return parser.parse_args() def main(): @@ -31,6 +31,7 @@ def main(): # load world world = World.load(args.world) + if args.name == "": player_name = input(world.l10n.format_value("character-name-prompt") + " ") else: @@ -38,6 +39,10 @@ def main(): player = Player(world, player_name) + # enter spawn area + spawn: Area = world.spawn_point + player.enter_area(spawn) + # main loop logger.info("starting mainloop for player %s" % player) player.main_loop() diff --git a/src/fantasy_forge/player.py b/src/fantasy_forge/player.py index a5614eb..b4ccad3 100644 --- a/src/fantasy_forge/player.py +++ b/src/fantasy_forge/player.py @@ -22,10 +22,16 @@ class Player(Character): __important_attributes__ = ("name", "area", "health") area: Area # the area we are currently in + world: World seen_entities: dict[str, Entity] armour_slots: dict[str, Armour] - def __init__(self: Self, world: World, name: str, health: int = BASE_PLAYER_HEALTH): + def __init__( + self: Self, + world: World, + name: str, + health: int = BASE_PLAYER_HEALTH, + ): super().__init__( dict( name=name, @@ -420,8 +426,6 @@ def drop(self, item_name: str): def main_loop(self): """Runs the game.""" - # TODO: enter spawn area in Shell preloop - self.enter_area(self.world.spawn_point) Shell(self).cmdloop() # afterward, leave the current area self.leave_area() From c008dbf619f1924a278babea09ec976d88519b79 Mon Sep 17 00:00:00 2001 From: dodo Date: Sat, 14 Dec 2024 15:08:04 +0100 Subject: [PATCH 11/21] adds Entity.from_dict method --- src/fantasy_forge/armour.py | 5 ----- src/fantasy_forge/entity.py | 5 +++++ src/fantasy_forge/inventory.py | 6 ++++++ src/fantasy_forge/item.py | 5 ----- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/fantasy_forge/armour.py b/src/fantasy_forge/armour.py index bb637df..7d254e5 100644 --- a/src/fantasy_forge/armour.py +++ b/src/fantasy_forge/armour.py @@ -31,11 +31,6 @@ def to_dict(self: Self) -> dict: armour_dict["defense"] = self.defense return armour_dict - @staticmethod - def from_dict(armour_dict: dict, l10n: FluentLocalization) -> Armour: - armour: Armour = Armour(armour_dict, l10n) - return armour - if TYPE_CHECKING: from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/entity.py b/src/fantasy_forge/entity.py index 638f2fd..29fc8c3 100644 --- a/src/fantasy_forge/entity.py +++ b/src/fantasy_forge/entity.py @@ -52,6 +52,11 @@ def to_dict(self: Self) -> dict: entity_dict: dict = {"name": self.name, "description": self.description} return entity_dict + @classmethod + def from_dict(cls, entity_dict: dict, l10n: FluentLocalization) -> Self: + entity = cls(entity_dict, l10n) + return entity + if TYPE_CHECKING: from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/inventory.py b/src/fantasy_forge/inventory.py index a9b0da1..97cfbe0 100644 --- a/src/fantasy_forge/inventory.py +++ b/src/fantasy_forge/inventory.py @@ -73,6 +73,12 @@ def on_look(self: Self) -> str: }, ) + @classmethod + def from_dict(cls, inventory_dict: dict, l10n: FluentLocalization) -> Self: + capacity = inventory_dict.get("capacity", 10) + inventory: Inventory = cls(capacity, l10n) + return inventory + if TYPE_CHECKING: from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/item.py b/src/fantasy_forge/item.py index 05d9108..a811abe 100644 --- a/src/fantasy_forge/item.py +++ b/src/fantasy_forge/item.py @@ -33,11 +33,6 @@ def to_dict(self: Self) -> dict: item_dict["carryable"] = self.carryable return item_dict - @staticmethod - def from_dict(item_dict: dict, l10n: FluentLocalization) -> Item: - item: Item = Item(item_dict, l10n) - return item - if TYPE_CHECKING: from fluent.runtime import FluentLocalization From c2c7473618c23848c4a0447bb4cfc9fe71cb1bea Mon Sep 17 00:00:00 2001 From: dodo Date: Thu, 26 Dec 2024 13:30:10 +0100 Subject: [PATCH 12/21] fixes inheritance --- src/fantasy_forge/container.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fantasy_forge/container.py b/src/fantasy_forge/container.py index 0d2c666..9df8d48 100644 --- a/src/fantasy_forge/container.py +++ b/src/fantasy_forge/container.py @@ -13,8 +13,8 @@ def __init__( self: Self, config_dict: dict[str, Any], l10n: FluentLocalization ) -> None: capacity: int = config_dict.get("capacity", 10) - Entity.__init__(capacity, l10n) - Inventory.__init__(config_dict, l10n) + Inventory.__init__(self, capacity, l10n) + Entity.__init__(self, config_dict, l10n) if TYPE_CHECKING: From c5b21727892255d6422f3a817ef9dedb8eeb2e49 Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 5 Jan 2025 15:26:31 +0100 Subject: [PATCH 13/21] adds logger --- src/fantasy_forge/area.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/fantasy_forge/area.py b/src/fantasy_forge/area.py index b82a729..667ca83 100644 --- a/src/fantasy_forge/area.py +++ b/src/fantasy_forge/area.py @@ -1,5 +1,7 @@ from __future__ import annotations +import logging + from pathlib import Path from typing import TYPE_CHECKING, Any, Iterator, Self @@ -7,6 +9,7 @@ from fantasy_forge.entity import Entity +logger = logging.getLogger(__name__) class Area(Entity): """An Area is a place in the world, containing NPCs, Items and connections to other areas.""" @@ -67,6 +70,7 @@ def from_dict(area_dict: dict, l10n: FluentLocalization) -> Area: contents_list.append(Armour(entity_dict, l10n)) case default: + logger.info("could not determine %s used Entity instead" % default) contents_list.append(Entity(entity_dict, l10n)) contents = {entity.name: entity for entity in contents_list} area = Area(area_dict, l10n) From 5f5406fb035be87b489784a68410fe881bf7380d Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 5 Jan 2025 15:26:44 +0100 Subject: [PATCH 14/21] Key is now hashable --- src/fantasy_forge/key.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/fantasy_forge/key.py b/src/fantasy_forge/key.py index 253cdb6..1cbd6dc 100644 --- a/src/fantasy_forge/key.py +++ b/src/fantasy_forge/key.py @@ -27,6 +27,10 @@ def __eq__(self: Self, other: Key): """Compares key ids.""" return self.key_id == other.key_id + def __hash__(self) -> int: + """Returns hash of key id.""" + return hash(self.key_id) + if TYPE_CHECKING: from fluent.runtime import FluentLocalization From be20bfc06abd42a282e0c195bb475cf0da778511 Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 5 Jan 2025 17:04:55 +0100 Subject: [PATCH 15/21] removes __repr__ overloading Use __important_attributes__ instead. --- src/fantasy_forge/armour.py | 1 + src/fantasy_forge/character.py | 1 + src/fantasy_forge/container.py | 1 + src/fantasy_forge/entity.py | 6 +++++- src/fantasy_forge/gateway.py | 1 + src/fantasy_forge/inventory.py | 9 ++++----- src/fantasy_forge/item.py | 5 +---- src/fantasy_forge/key.py | 1 + src/fantasy_forge/weapon.py | 1 + 9 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/fantasy_forge/armour.py b/src/fantasy_forge/armour.py index 7d254e5..89b82f5 100644 --- a/src/fantasy_forge/armour.py +++ b/src/fantasy_forge/armour.py @@ -17,6 +17,7 @@ class Armour(Item): defense: int __important_attributes__ = ("name", "armour_type", "defense") + __attributes__ = {**Item.__attributes__, "armour_type": str, "defense": int} def __init__(self, config_dict, l10n: FluentLocalization): a_type: str = config_dict.pop("armour_type") diff --git a/src/fantasy_forge/character.py b/src/fantasy_forge/character.py index 5922318..95e3230 100644 --- a/src/fantasy_forge/character.py +++ b/src/fantasy_forge/character.py @@ -25,6 +25,7 @@ class Character(Entity): """A character in the world.""" __important_attributes__ = ("name", "health", "alive") + __attributes__ = {**Entity.__attributes__, "health": int, "alive": bool} health: int inventory: Inventory diff --git a/src/fantasy_forge/container.py b/src/fantasy_forge/container.py index 9df8d48..4ea11c8 100644 --- a/src/fantasy_forge/container.py +++ b/src/fantasy_forge/container.py @@ -8,6 +8,7 @@ class Container(Entity, Inventory): __important_attributes__ = ("name", "capacity") + __attributes__ = {**Entity.__attributes__, **Inventory.__attributes__} def __init__( self: Self, config_dict: dict[str, Any], l10n: FluentLocalization diff --git a/src/fantasy_forge/entity.py b/src/fantasy_forge/entity.py index 29fc8c3..d431ada 100644 --- a/src/fantasy_forge/entity.py +++ b/src/fantasy_forge/entity.py @@ -2,11 +2,15 @@ from typing import TYPE_CHECKING, Any, Self - class Entity: """An Entity is an abstract object in the world.""" __important_attributes__ = ("name",) + __attributes__: dict[str, type] = { + "name": str, + "description": str, + "obvious": bool + } name: str description: str diff --git a/src/fantasy_forge/gateway.py b/src/fantasy_forge/gateway.py index 6d2d77e..1632831 100644 --- a/src/fantasy_forge/gateway.py +++ b/src/fantasy_forge/gateway.py @@ -11,6 +11,7 @@ class Gateway(Entity): """A Gateway is a one-way connection to an area.""" __important_attributes__ = ("name", "target", "locked") + __attributes__ = {**Entity.__attributes__, "target": str, "locked": bool, "key_list": list} target: str # This is not an area because the target might not be loaded yet. locked: bool diff --git a/src/fantasy_forge/inventory.py b/src/fantasy_forge/inventory.py index 97cfbe0..1f3c70f 100644 --- a/src/fantasy_forge/inventory.py +++ b/src/fantasy_forge/inventory.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Iterator, Self +from fantasy_forge.entity import Entity from fantasy_forge.item import Item from fantasy_forge.localization import highlight_interactive @@ -13,6 +14,9 @@ class Inventory: contents: dict[str, Item] l10n: FluentLocalization + __important_attributes__ = ("name", "capacity") + __attributes__ = {**Entity.__attributes__, "capacity": int} + def __init__(self: Self, capacity: int, l10n: FluentLocalization): self.capacity = capacity self.contents = {} @@ -30,11 +34,6 @@ def __contains__(self: Self, other: str) -> bool: """Returns if item is in inventory.""" return other in self.contents.keys() - def __repr__(self: Self) -> str: - output: str = f"Inventory({len(self)}/{self.capacity})\n" - output += "[" + ", ".join(self.contents.keys()) + "]" - return output - def add(self: Self, item: Item) -> None: """Adds Item to inventory with respect to capacity.""" assert item.name not in self.contents diff --git a/src/fantasy_forge/item.py b/src/fantasy_forge/item.py index a811abe..eb5b6ce 100644 --- a/src/fantasy_forge/item.py +++ b/src/fantasy_forge/item.py @@ -4,11 +4,11 @@ from fantasy_forge.entity import Entity - class Item(Entity): """An Item is an entity which can be picked up by the player.""" __important_attributes__ = ("name", "moveable", "carryable") + __attributes__ = {**Entity.__attributes__, "moveable": bool, "carryable": bool} moveable: bool carryable: bool @@ -20,9 +20,6 @@ def __init__( self.carryable = config_dict.pop("carryable", True) super().__init__(config_dict, l10n) - def __repr__(self: Self) -> str: - return f"Item({self.name}, {self.description}, moveable={self.moveable}, carryable={self.carryable})" - def on_pickup(self: Self): # TODO pass diff --git a/src/fantasy_forge/key.py b/src/fantasy_forge/key.py index 1cbd6dc..b19050c 100644 --- a/src/fantasy_forge/key.py +++ b/src/fantasy_forge/key.py @@ -9,6 +9,7 @@ class Key(Item): """A Key can be used to unlock Gateways or Container.""" __important_attributes__ = ("name", "key_id") + __attributes__ = {**Item.__attributes__, "key_id": str, "used": bool} key_id: str used: bool # wether the key was used already diff --git a/src/fantasy_forge/weapon.py b/src/fantasy_forge/weapon.py index a137e17..5e69130 100644 --- a/src/fantasy_forge/weapon.py +++ b/src/fantasy_forge/weapon.py @@ -9,6 +9,7 @@ class Weapon(Item): """A Weapon is an item, which can deal damage to players or NPCs.""" __important_attributes__ = ("name", "damage") + __attributes__ = {**Item.__attributes__, "damage": int} damage: int From 70bcf0fe88b039d058540737cf35c8306e31c5ed Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 5 Jan 2025 19:24:21 +0100 Subject: [PATCH 16/21] adds clean_filename function --- src/fantasy_forge/utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/fantasy_forge/utils.py b/src/fantasy_forge/utils.py index d050e64..8efd46d 100644 --- a/src/fantasy_forge/utils.py +++ b/src/fantasy_forge/utils.py @@ -1,4 +1,5 @@ from pathlib import Path +from string import whitespace SOURCE_FOLDER: Path = Path(__file__).parent.resolve() # fantasy_forge/src/fantasy_forge @@ -7,3 +8,14 @@ DATA_FOLDER: Path = ROOT_FOLDER / "data" # fantasy_forge/data WORLDS_FOLDER: Path = DATA_FOLDER / "worlds" # fantasy_forge/data/worlds LOCALE_FOLDER: Path = DATA_FOLDER / "l10n" # fantasy_forge/data/l10n + +def clean_filename(filename: str) -> str: + result = [] + for char in filename.casefold(): + if char.isalnum(): + result.append(char) + elif char in whitespace: + result.append("_") + else: + continue + return "".join(result) From a3d40be79a5a9ddc85b5ab97deba2c721d43a25e Mon Sep 17 00:00:00 2001 From: dodo Date: Mon, 6 Jan 2025 14:48:59 +0100 Subject: [PATCH 17/21] adds inventory.to_dict --- src/fantasy_forge/inventory.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/fantasy_forge/inventory.py b/src/fantasy_forge/inventory.py index 1f3c70f..831d44f 100644 --- a/src/fantasy_forge/inventory.py +++ b/src/fantasy_forge/inventory.py @@ -72,6 +72,11 @@ def on_look(self: Self) -> str: }, ) + def to_dict(self) -> dict: + """Returns inventory as a dictionary.""" + inventory_dict: dict = {"capacity": self.capacity} + return inventory_dict + @classmethod def from_dict(cls, inventory_dict: dict, l10n: FluentLocalization) -> Self: capacity = inventory_dict.get("capacity", 10) From aee865e5b4c3e273dc703c295f7de0024e91f788 Mon Sep 17 00:00:00 2001 From: dodo Date: Mon, 6 Jan 2025 16:51:31 +0100 Subject: [PATCH 18/21] Inventory now inherits from Entity --- src/fantasy_forge/container.py | 2 +- src/fantasy_forge/inventory.py | 38 ++++++++++++++-------------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/fantasy_forge/container.py b/src/fantasy_forge/container.py index 4ea11c8..7b8c92f 100644 --- a/src/fantasy_forge/container.py +++ b/src/fantasy_forge/container.py @@ -6,7 +6,7 @@ from fantasy_forge.inventory import Inventory -class Container(Entity, Inventory): +class Container(Inventory): __important_attributes__ = ("name", "capacity") __attributes__ = {**Entity.__attributes__, **Inventory.__attributes__} diff --git a/src/fantasy_forge/inventory.py b/src/fantasy_forge/inventory.py index 831d44f..3758f4b 100644 --- a/src/fantasy_forge/inventory.py +++ b/src/fantasy_forge/inventory.py @@ -3,15 +3,14 @@ from typing import TYPE_CHECKING, Iterator, Self from fantasy_forge.entity import Entity -from fantasy_forge.item import Item from fantasy_forge.localization import highlight_interactive -class Inventory: - """An Inventory contains multiple items.""" +class Inventory(Entity): + """An Inventory contains multiple entities.""" capacity: int - contents: dict[str, Item] + contents: dict[str, Entity] l10n: FluentLocalization __important_attributes__ = ("name", "capacity") @@ -26,19 +25,19 @@ def __len__(self: Self) -> int: """Returns current capacity.""" return len(self.contents) - def __iter__(self: Self) -> Iterator[Item]: - """Iterates over items in inventory.""" + def __iter__(self: Self) -> Iterator[Entity]: + """Iterates over entities in inventory.""" yield from self.contents.values() def __contains__(self: Self, other: str) -> bool: - """Returns if item is in inventory.""" + """Returns if entity is in inventory.""" return other in self.contents.keys() - def add(self: Self, item: Item) -> None: + def add(self: Self, entity: Entity) -> None: """Adds Item to inventory with respect to capacity.""" - assert item.name not in self.contents + assert entity.name not in self.contents if len(self) < self.capacity: - self.contents[item.name] = item + self.contents[entity.name] = entity else: raise Exception( self.l10n.format_value( @@ -49,14 +48,14 @@ def add(self: Self, item: Item) -> None: ) ) - def get(self: Self, item_name: str) -> Item | None: + def get(self: Self, entity_name: str) -> Entity | None: """Gets item by name.""" - return self.contents.get(item_name) + return self.contents.get(entity_name) - def pop(self: Self, item_name: str) -> Item | None: + def pop(self: Self, entity_name: str) -> Entity | None: """Pops item from inventory.""" - if item_name in self: - return self.contents.pop(item_name) + if entity_name in self: + return self.contents.pop(entity_name) return None def on_look(self: Self) -> str: @@ -74,15 +73,10 @@ def on_look(self: Self) -> str: def to_dict(self) -> dict: """Returns inventory as a dictionary.""" - inventory_dict: dict = {"capacity": self.capacity} + entity_dict: dict = super().to_dict() + inventory_dict: dict = {**entity_dict, "capacity": self.capacity} return inventory_dict - @classmethod - def from_dict(cls, inventory_dict: dict, l10n: FluentLocalization) -> Self: - capacity = inventory_dict.get("capacity", 10) - inventory: Inventory = cls(capacity, l10n) - return inventory - if TYPE_CHECKING: from fluent.runtime import FluentLocalization From 4f6cbc34d579602a18e390dfa789fad2ab50c582 Mon Sep 17 00:00:00 2001 From: dodo Date: Mon, 6 Jan 2025 16:55:04 +0100 Subject: [PATCH 19/21] Container now inherits from Inventory and Item --- src/fantasy_forge/container.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/fantasy_forge/container.py b/src/fantasy_forge/container.py index 7b8c92f..aaffc27 100644 --- a/src/fantasy_forge/container.py +++ b/src/fantasy_forge/container.py @@ -2,21 +2,18 @@ from typing import TYPE_CHECKING, Any, Self -from fantasy_forge.entity import Entity +from fantasy_forge.item import Item from fantasy_forge.inventory import Inventory -class Container(Inventory): - __important_attributes__ = ("name", "capacity") - __attributes__ = {**Entity.__attributes__, **Inventory.__attributes__} +class Container(Inventory, Item): + __attributes__ = {**Inventory.__attributes__, **Item.__attributes__} def __init__( self: Self, config_dict: dict[str, Any], l10n: FluentLocalization ) -> None: - capacity: int = config_dict.get("capacity", 10) - Inventory.__init__(self, capacity, l10n) - Entity.__init__(self, config_dict, l10n) - + Inventory.__init__(self, config_dict, l10n) + Item.__init__(self, config_dict, l10n) if TYPE_CHECKING: from fluent.runtime import FluentLocalization From eb72f74be5275d0efed28aa2a4167fbb5da0c610 Mon Sep 17 00:00:00 2001 From: dodo Date: Wed, 8 Jan 2025 22:20:40 +0100 Subject: [PATCH 20/21] docstrings for constructors --- src/fantasy_forge/area.py | 8 ++++++++ src/fantasy_forge/armour.py | 14 ++++++++++++++ src/fantasy_forge/character.py | 9 +++++++++ src/fantasy_forge/container.py | 14 ++++++++++++++ src/fantasy_forge/enemy.py | 12 ++++++++++++ src/fantasy_forge/entity.py | 6 ++++++ src/fantasy_forge/gateway.py | 11 +++++++++++ src/fantasy_forge/item.py | 10 ++++++++++ src/fantasy_forge/key.py | 13 +++++++++++++ src/fantasy_forge/weapon.py | 13 +++++++++++++ 10 files changed, 110 insertions(+) diff --git a/src/fantasy_forge/area.py b/src/fantasy_forge/area.py index 667ca83..a53cdf7 100644 --- a/src/fantasy_forge/area.py +++ b/src/fantasy_forge/area.py @@ -18,6 +18,14 @@ class Area(Entity): contents: dict[str, Entity] def __init__(self: Self, config_dict: dict[str, Any], l10n: FluentLocalization): + """ + config_dict contents + + inherited from Entity + 'name' (str): name of the entity + 'description' (str): description of the entity (default: "") + 'obvious'(bool): whether the entity will be spotted immediately (default: False) + """ super().__init__(config_dict, l10n) self.contents: dict = {} diff --git a/src/fantasy_forge/armour.py b/src/fantasy_forge/armour.py index 89b82f5..68d1ebe 100644 --- a/src/fantasy_forge/armour.py +++ b/src/fantasy_forge/armour.py @@ -20,6 +20,20 @@ class Armour(Item): __attributes__ = {**Item.__attributes__, "armour_type": str, "defense": int} def __init__(self, config_dict, l10n: FluentLocalization): + """ + config_dict contents + 'armour_type' (str): armour slot ("head", "torso", "legs", "feet") + 'defense' (int): defense points gained by armour + + inherited from Item: + 'moveable' (bool): can the item be moved by the player (default: True) + 'carryable' (bool): can the item be put in the inventory by the player (default: True) + + inherited from Entity + 'name' (str): name of the entity + 'description' (str): description of the entity (default: "") + 'obvious'(bool): whether the entity will be spotted immediately (default: False) + """ a_type: str = config_dict.pop("armour_type") assert a_type in ARMOUR_TYPES self.armour_type = a_type diff --git a/src/fantasy_forge/character.py b/src/fantasy_forge/character.py index 95e3230..32dea5f 100644 --- a/src/fantasy_forge/character.py +++ b/src/fantasy_forge/character.py @@ -35,6 +35,15 @@ class Character(Entity): def __init__( self: Self, config_dict: dict[str, Any], l10n: FluentLocalization ) -> None: + """ + config_dict contents + 'health' (int): health points + + inherited from Entity: + 'name' (str): name of the entity + 'description' (str): description of the entity (default: "") + 'obvious'(bool): whether the entity will be spotted immediately (default: False) + """ self.health = config_dict.pop("health") self.inventory = Inventory(BASE_INVENTORY_CAPACITY, l10n) self.main_hand = None diff --git a/src/fantasy_forge/container.py b/src/fantasy_forge/container.py index aaffc27..91d8568 100644 --- a/src/fantasy_forge/container.py +++ b/src/fantasy_forge/container.py @@ -12,6 +12,20 @@ class Container(Inventory, Item): def __init__( self: Self, config_dict: dict[str, Any], l10n: FluentLocalization ) -> None: + """ + config_dict contents + inherited from Inventory + 'capacity' (int): maximum capacity of the inventory + + inherited from Item + 'moveable' (bool): can the item be moved by the player (default: True) + 'carryable' (bool): can the item be put in the inventory by the player (default: True) + + inherited from Entity + 'name' (str): name of the entity + 'description' (str): description of the entity (default: "") + 'obvious'(bool): whether the entity will be spotted immediately (default: False) + """ Inventory.__init__(self, config_dict, l10n) Item.__init__(self, config_dict, l10n) diff --git a/src/fantasy_forge/enemy.py b/src/fantasy_forge/enemy.py index d7f6c8f..49baefd 100644 --- a/src/fantasy_forge/enemy.py +++ b/src/fantasy_forge/enemy.py @@ -10,6 +10,18 @@ class Enemy(Character): """An enemy is a person which will fight back.""" def __init__(self: Self, config_dict: dict[str, Any], l10n: FluentLocalization): + """ + config_dict contents + 'loot' (list[Item]): items dropped after death + + inherited from Character + 'health' (int): health points + + inherited from Entity: + 'name' (str): name of the entity + 'description' (str): description of the entity (default: "") + 'obvious'(bool): whether the entity will be spotted immediately (default: False) + """ super().__init__(config_dict, l10n) for item_dict in config_dict.get("loot", []): self.inventory.add(Item(item_dict, l10n)) diff --git a/src/fantasy_forge/entity.py b/src/fantasy_forge/entity.py index d431ada..19cafa5 100644 --- a/src/fantasy_forge/entity.py +++ b/src/fantasy_forge/entity.py @@ -20,6 +20,12 @@ class Entity: def __init__( self: Self, config_dict: dict[str, Any], l10n: FluentLocalization ) -> None: + """ + config_dict contents + 'name' (str): name of the entity + 'description' (str): description of the entity (default: "") + 'obvious'(bool): whether the entity will be spotted immediately (default: False) + """ self.name = config_dict.pop("name") self.description = config_dict.pop("description", "") self.obvious = config_dict.pop("obvious", False) diff --git a/src/fantasy_forge/gateway.py b/src/fantasy_forge/gateway.py index 1632831..7a81176 100644 --- a/src/fantasy_forge/gateway.py +++ b/src/fantasy_forge/gateway.py @@ -18,6 +18,17 @@ class Gateway(Entity): key_list: list[str] def __init__(self: Self, config_dict: dict[str, Any], l10n: FluentLocalization): + """ + config_dict contents + 'target' (str): name of the target area + 'locked' (bool): whether the gateway is locked (default: False) + 'key_list' (list[str]): a list of keys to use for this gateway (default: []) + + inherited from Entity + 'name' (str): name of the entity + 'description' (str): description of the entity (default: "") + 'obvious'(bool): whether the entity will be spotted immediately (default: False) + """ self.target = config_dict.pop("target") self.locked = config_dict.pop("locked", False) self.key_list = config_dict.pop("key_list", []) diff --git a/src/fantasy_forge/item.py b/src/fantasy_forge/item.py index eb5b6ce..cd02a84 100644 --- a/src/fantasy_forge/item.py +++ b/src/fantasy_forge/item.py @@ -16,6 +16,16 @@ class Item(Entity): def __init__( self: Self, config_dict: dict[str, Any], l10n: FluentLocalization ) -> None: + """ + config_dict contents + 'moveable' (bool): can the item be moved by the player (default: True) + 'carryable' (bool): can the item be put in the inventory by the player (default: True) + + inherited from Entity + 'name' (str): name of the entity + 'description' (str): description of the entity (default: "") + 'obvious'(bool): whether the entity will be spotted immediately (default: False) + """ self.moveable = config_dict.pop("moveable", True) self.carryable = config_dict.pop("carryable", True) super().__init__(config_dict, l10n) diff --git a/src/fantasy_forge/key.py b/src/fantasy_forge/key.py index b19050c..8a43e18 100644 --- a/src/fantasy_forge/key.py +++ b/src/fantasy_forge/key.py @@ -17,6 +17,19 @@ class Key(Item): def __init__( self: Self, config_dict: dict[str, Any], l10n: FluentLocalization ) -> None: + """ + config_dict contents + key_id (str): identifiable key id + + inherited from Item + 'moveable' (bool): can the item be moved by the player (default: True) + 'carryable' (bool): can the item be put in the inventory by the player (default: True) + + inherited from Entity + 'name' (str): name of the entity + 'description' (str): description of the entity (default: "") + 'obvious'(bool): whether the entity will be spotted immediately (default: False) + """ self.key_id = config_dict.pop("key_id") self.moveable = True # keys are moveable by default diff --git a/src/fantasy_forge/weapon.py b/src/fantasy_forge/weapon.py index 5e69130..fd234ed 100644 --- a/src/fantasy_forge/weapon.py +++ b/src/fantasy_forge/weapon.py @@ -16,6 +16,19 @@ class Weapon(Item): def __init__( self: Self, config_dict: dict[str, Any], l10n: FluentLocalization ) -> None: + """ + config_dict contents + 'damage' (int): damage dealt using the weapon + + inherited from Item + 'moveable' (bool): can the item be moved by the player (default: True) + 'carryable' (bool): can the item be put in the inventory by the player (default: True) + + inherited from Entity + 'name' (str): name of the entity + 'description' (str): description of the entity (default: "") + 'obvious'(bool): whether the entity will be spotted immediately (default: False) + """ self.damage = config_dict.pop("damage") super().__init__(config_dict, l10n) From 15bb0f8d43f0b9e145e2dbae3f457c7e1d272f6d Mon Sep 17 00:00:00 2001 From: dodo Date: Wed, 8 Jan 2025 19:02:35 +0100 Subject: [PATCH 21/21] adds more docstrings --- src/fantasy_forge/area.py | 17 +++++++------ src/fantasy_forge/armour.py | 7 ++++++ src/fantasy_forge/character.py | 9 ++++++- src/fantasy_forge/container.py | 11 +++++++- src/fantasy_forge/enemy.py | 5 ++++ src/fantasy_forge/entity.py | 19 +++++++++----- src/fantasy_forge/gateway.py | 20 ++++++++++++--- src/fantasy_forge/inventory.py | 2 +- src/fantasy_forge/key.py | 7 ++++-- src/fantasy_forge/main.py | 4 +-- src/fantasy_forge/player.py | 17 ++++++++----- src/fantasy_forge/shell.py | 46 +++++++++++++++++----------------- src/fantasy_forge/world.py | 11 +++++--- 13 files changed, 119 insertions(+), 56 deletions(-) diff --git a/src/fantasy_forge/area.py b/src/fantasy_forge/area.py index a53cdf7..386eee2 100644 --- a/src/fantasy_forge/area.py +++ b/src/fantasy_forge/area.py @@ -1,7 +1,8 @@ +"""An Area is a place in the world, containing NPCs, Items and connections to other areas.""" + from __future__ import annotations import logging - from pathlib import Path from typing import TYPE_CHECKING, Any, Iterator, Self @@ -11,6 +12,7 @@ logger = logging.getLogger(__name__) + class Area(Entity): """An Area is a place in the world, containing NPCs, Items and connections to other areas.""" @@ -20,7 +22,7 @@ class Area(Entity): def __init__(self: Self, config_dict: dict[str, Any], l10n: FluentLocalization): """ config_dict contents - + inherited from Entity 'name' (str): name of the entity 'description' (str): description of the entity (default: "") @@ -78,7 +80,7 @@ def from_dict(area_dict: dict, l10n: FluentLocalization) -> Area: contents_list.append(Armour(entity_dict, l10n)) case default: - logger.info("could not determine %s used Entity instead" % default) + logger.info("could not determine %s used Entity instead", default) contents_list.append(Entity(entity_dict, l10n)) contents = {entity.name: entity for entity in contents_list} area = Area(area_dict, l10n) @@ -87,6 +89,7 @@ def from_dict(area_dict: dict, l10n: FluentLocalization) -> Area: @staticmethod def load(world, root_path: Path, name: str): + """Loads an area from toml-file.""" path = root_path / "areas" / f"{name}.toml" with path.open() as area_file: area_toml = toml.load(area_file) @@ -96,10 +99,10 @@ def load(world, root_path: Path, name: str): def empty(l10n: FluentLocalization) -> Area: """Return an empty area, this is a placeholder.""" return Area( - dict( - name=l10n.format_value("void-name"), - description=l10n.format_value("void-description"), - ), + { + "name": l10n.format_value("void-name"), + "description": l10n.format_value("void-description"), + }, l10n, ) diff --git a/src/fantasy_forge/armour.py b/src/fantasy_forge/armour.py index 68d1ebe..49d62ed 100644 --- a/src/fantasy_forge/armour.py +++ b/src/fantasy_forge/armour.py @@ -1,3 +1,8 @@ +"""Armour class + +An Armour is an item which can be worn by Characters and grants protection. +""" + from __future__ import annotations from typing import TYPE_CHECKING, Self @@ -13,6 +18,8 @@ class Armour(Item): + """An Armour object.""" + armour_type: str defense: int diff --git a/src/fantasy_forge/character.py b/src/fantasy_forge/character.py index 32dea5f..45aee13 100644 --- a/src/fantasy_forge/character.py +++ b/src/fantasy_forge/character.py @@ -1,3 +1,8 @@ +"""Character class + +A character is a living entity, which can interact with other entities or characters. +""" + from __future__ import annotations from typing import TYPE_CHECKING, Any, Self @@ -38,7 +43,7 @@ def __init__( """ config_dict contents 'health' (int): health points - + inherited from Entity: 'name' (str): name of the entity 'description' (str): description of the entity (default: "") @@ -51,6 +56,7 @@ def __init__( @property def alive(self: Self) -> bool: + """Return True if the character is alive.""" self._alive = self.health > 0 return self._alive @@ -72,6 +78,7 @@ def attack(self: Self, target: Character) -> None: ) def on_attack(self: Self, weapon: Weapon): + """Handles an incoming attack.""" self.health -= weapon.damage def _on_death(self: Self, player: Player): diff --git a/src/fantasy_forge/container.py b/src/fantasy_forge/container.py index 91d8568..657d69d 100644 --- a/src/fantasy_forge/container.py +++ b/src/fantasy_forge/container.py @@ -1,12 +1,20 @@ +"""Container class + +A container is an item in the world which holds an inventory. +""" + from __future__ import annotations from typing import TYPE_CHECKING, Any, Self -from fantasy_forge.item import Item from fantasy_forge.inventory import Inventory +from fantasy_forge.item import Item class Container(Inventory, Item): + """Container object.""" + + __important_attributes__ = (*Item.__important_attributes__, "capacity") __attributes__ = {**Inventory.__attributes__, **Item.__attributes__} def __init__( @@ -29,5 +37,6 @@ def __init__( Inventory.__init__(self, config_dict, l10n) Item.__init__(self, config_dict, l10n) + if TYPE_CHECKING: from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/enemy.py b/src/fantasy_forge/enemy.py index 49baefd..4f50f56 100644 --- a/src/fantasy_forge/enemy.py +++ b/src/fantasy_forge/enemy.py @@ -1,3 +1,8 @@ +"""Enemy class + +An enemy is a hostile character which will attack the player on contact. +""" + from __future__ import annotations from typing import TYPE_CHECKING, Any, Self diff --git a/src/fantasy_forge/entity.py b/src/fantasy_forge/entity.py index 19cafa5..88b60fe 100644 --- a/src/fantasy_forge/entity.py +++ b/src/fantasy_forge/entity.py @@ -1,16 +1,19 @@ +"""Entity class + +An entity is an abstract object in the world. +Each entity is identifiable by its name. +""" + from __future__ import annotations from typing import TYPE_CHECKING, Any, Self + class Entity: - """An Entity is an abstract object in the world.""" + """An Entity object""" __important_attributes__ = ("name",) - __attributes__: dict[str, type] = { - "name": str, - "description": str, - "obvious": bool - } + __attributes__: dict[str, type] = {"name": str, "description": str, "obvious": bool} name: str description: str @@ -32,9 +35,11 @@ def __init__( self.l10n = l10n def on_look(self: Self) -> str: + """Returns description of entity.""" return self.description def on_use(self: Self, other: Entity | None = None): + """Handles usage of entity.""" print( self.l10n.format_value( "cannot-use-message", @@ -59,11 +64,13 @@ def __str__(self: Self) -> str: return self.name def to_dict(self: Self) -> dict: + """Returns entity attributes as a dictionary.""" entity_dict: dict = {"name": self.name, "description": self.description} return entity_dict @classmethod def from_dict(cls, entity_dict: dict, l10n: FluentLocalization) -> Self: + """Creates new entity from a dictionary.""" entity = cls(entity_dict, l10n) return entity diff --git a/src/fantasy_forge/gateway.py b/src/fantasy_forge/gateway.py index 7a81176..2b26b5e 100644 --- a/src/fantasy_forge/gateway.py +++ b/src/fantasy_forge/gateway.py @@ -1,9 +1,13 @@ +"""Gateway class + +A gateway connects two areas. +""" + from __future__ import annotations from typing import TYPE_CHECKING, Any, Self from fantasy_forge.entity import Entity -from fantasy_forge.item import Item from fantasy_forge.key import Key @@ -11,7 +15,12 @@ class Gateway(Entity): """A Gateway is a one-way connection to an area.""" __important_attributes__ = ("name", "target", "locked") - __attributes__ = {**Entity.__attributes__, "target": str, "locked": bool, "key_list": list} + __attributes__ = { + **Entity.__attributes__, + "target": str, + "locked": bool, + "key_list": list, + } target: str # This is not an area because the target might not be loaded yet. locked: bool @@ -35,13 +44,15 @@ def __init__(self: Self, config_dict: dict[str, Any], l10n: FluentLocalization): super().__init__(config_dict, l10n) def on_look(self: Self) -> str: + """Returns description if unlocked.""" text = [] if self.key_list and self.locked: text.append(self.l10n.format_value("gateway-on-look-locked")) text.append(self.description) return "\n".join(text) - def on_use(self: Self, other: Item | None = None): + def on_use(self: Self, other: Entity | None = None): + """Handles gateway use.""" if other is None: super().on_use() return @@ -70,6 +81,7 @@ def on_use(self: Self, other: Item | None = None): self.on_lock(other) def on_unlock(self: Self, key: Key): + """Handles gateway unlock.""" if key.key_id in self.key_list: self.locked = False key.used = True @@ -83,6 +95,7 @@ def on_unlock(self: Self, key: Key): ) def on_lock(self: Self, key: Key): + """Handles gateway lock.""" if key.key_id in self.key_list: self.locked = True key.used = True @@ -96,6 +109,7 @@ def on_lock(self: Self, key: Key): ) def to_dict(self: Self) -> dict: + """Returns gateway as a dictionary.""" gateway_dict: dict = super().to_dict() gateway_dict["target"] = self.target return gateway_dict diff --git a/src/fantasy_forge/inventory.py b/src/fantasy_forge/inventory.py index 3758f4b..647f0cc 100644 --- a/src/fantasy_forge/inventory.py +++ b/src/fantasy_forge/inventory.py @@ -31,7 +31,7 @@ def __iter__(self: Self) -> Iterator[Entity]: def __contains__(self: Self, other: str) -> bool: """Returns if entity is in inventory.""" - return other in self.contents.keys() + return other in self.contents def add(self: Self, entity: Entity) -> None: """Adds Item to inventory with respect to capacity.""" diff --git a/src/fantasy_forge/key.py b/src/fantasy_forge/key.py index 8a43e18..8a531d0 100644 --- a/src/fantasy_forge/key.py +++ b/src/fantasy_forge/key.py @@ -37,9 +37,12 @@ def __init__( super().__init__(config_dict, l10n) self.used = False - def __eq__(self: Self, other: Key): + def __eq__(self: Self, other: object) -> bool: """Compares key ids.""" - return self.key_id == other.key_id + if hasattr(other, "key_id"): + return self.key_id == other.key_id + + return hash(self) == hash(other) def __hash__(self) -> int: """Returns hash of key id.""" diff --git a/src/fantasy_forge/main.py b/src/fantasy_forge/main.py index e8fa4a1..b1053b1 100644 --- a/src/fantasy_forge/main.py +++ b/src/fantasy_forge/main.py @@ -27,7 +27,7 @@ def main(): logger = logging.getLogger(__name__) numeric_level = getattr(logging, args.loglevel.upper(), None) logging.basicConfig(filename=args.logfile, level=numeric_level, filemode="w") - logger.info("load world %s" % args.world) + logger.info("load world %s", args.world) # load world world = World.load(args.world) @@ -44,5 +44,5 @@ def main(): player.enter_area(spawn) # main loop - logger.info("starting mainloop for player %s" % player) + logger.info("starting mainloop for player %s", player) player.main_loop() diff --git a/src/fantasy_forge/player.py b/src/fantasy_forge/player.py index b4ccad3..456996f 100644 --- a/src/fantasy_forge/player.py +++ b/src/fantasy_forge/player.py @@ -24,7 +24,7 @@ class Player(Character): area: Area # the area we are currently in world: World seen_entities: dict[str, Entity] - armour_slots: dict[str, Armour] + armour_slots: dict[str, Armour | None] def __init__( self: Self, @@ -56,8 +56,8 @@ def __init__( @property def defense(self) -> int: defense_sum: int = 0 - armour_item: Armour - for armour_item in self.armour_slots.keys(): + armour_item: Armour | None + for armour_item in self.armour_slots.values(): if armour_item is not None: defense_sum += armour_item.defense return defense_sum @@ -202,7 +202,7 @@ def equip_weapon(self, weapon: Weapon): def equip_armour(self, armour: Armour) -> None: """Equips armour piece.""" - current_armour: Armour = self.armour_slots.pop(armour.armour_type) + current_armour: Armour | None = self.armour_slots.pop(armour.armour_type) # check if armour slot is already filled if current_armour is not None: print( @@ -228,7 +228,9 @@ def equip_armour(self, armour: Armour) -> None: def unequip(self, item_name: str): """Unequips weapon or armour.""" - item = self.seen_entities.get(item_name) + item: Entity | None = self.seen_entities.get(item_name) + if item is None: + return if self.main_hand is item: self.main_hand = None for armour_type, armour_item in self.armour_slots.items(): @@ -348,7 +350,10 @@ def go(self, gateway_name: str): def enter_gateway(self: Self, gateway: Gateway): """Uses gateway to enter a new area.""" - # TODO: separate world from Player + # TODO: refactor + # The Player holds no reference to the current world, + # so a external function should do the area change + if gateway.locked: print( self.l10n.format_value( diff --git a/src/fantasy_forge/shell.py b/src/fantasy_forge/shell.py index d2a5420..2750ed0 100644 --- a/src/fantasy_forge/shell.py +++ b/src/fantasy_forge/shell.py @@ -84,11 +84,11 @@ def do_look(self, arg: str): """ if arg.strip() == "around": self.player.look_around() - logger.debug("%s looks around" % self.player.name) + logger.debug("%s looks around", self.player.name) elif arg.strip().startswith("at"): entity_name = arg.strip().removeprefix("at").strip() self.player.look_at(entity_name) - logger.debug("%s looks at %s" % (self.player.name, entity_name)) + logger.debug("%s looks at %s", self.player.name, entity_name) else: self.default(arg) @@ -120,7 +120,7 @@ def do_pick(self, arg: str): if arg.startswith("up "): arg = arg.removeprefix("up ") self.player.pick_up(arg.strip()) - logger.debug("%s picks up %s" % (self.player.name, arg.strip())) + logger.debug("%s picks up %s", self.player.name, arg.strip()) def complete_pick( self, @@ -160,7 +160,7 @@ def complete_pick( def do_go(self, arg: str): """go """ self.player.go(arg) - logger.debug("%s goes to %s" % (self.player.name, arg)) + logger.debug("%s goes to %s", self.player.name, arg) def complete_go( self, @@ -240,25 +240,25 @@ def complete_use( if " " in completions: completions.remove(" ") return completions - else: - # we're looking for the subject - subject_name = " ".join(args).strip() - completions = [ - text + name.removeprefix(subject_name).strip() + " " - for name in self.player.seen_entities.keys() - if name.startswith(subject_name) - ] - if " " in completions: - completions.remove(" ") - # we might already be in the "with" - if any( - ( - entity_name == subject_name.rstrip("ihtw").strip() - for entity_name in self.player.seen_entities - ) - ): - completions.append("with ") - return completions + + # we're looking for the subject + subject_name = " ".join(args).strip() + completions = [ + text + name.removeprefix(subject_name).strip() + " " + for name in self.player.seen_entities.keys() + if name.startswith(subject_name) + ] + if " " in completions: + completions.remove(" ") + # we might already be in the "with" + if any( + ( + entity_name == subject_name.rstrip("ihtw").strip() + for entity_name in self.player.seen_entities + ) + ): + completions.append("with ") + return completions def do_attack(self, arg: str) -> None: """Attack another entity.""" diff --git a/src/fantasy_forge/world.py b/src/fantasy_forge/world.py index 8dccf30..c830266 100644 --- a/src/fantasy_forge/world.py +++ b/src/fantasy_forge/world.py @@ -54,7 +54,10 @@ def load(name: str) -> World: world_path = WORLDS_FOLDER / name if not world_path.exists(): - logger.debug(f"Path {world_path} not found, using {name}") + logger.debug( + "Path %(world_path) not found, using %(name)", + {"world_path": world_path, "name": name}, + ) world_path = Path(name) world_toml_path: Path = world_path / "world.toml" @@ -64,7 +67,7 @@ def load(name: str) -> World: world_name: str = world_toml_data["name"] assert world_name == name - areas: dict[str, Area] = dict() + areas: dict[str, Area] = {} # load language for localization language: str = world_toml_data["language"] @@ -89,7 +92,7 @@ def _load_assets(self): if parent in ASSET_TYPES.keys(): asset_type = ASSET_TYPES[parent] else: - logger.info("skipped " + toml_path.name) + logger.info("skipped %s", toml_path.name) continue # read toml @@ -101,7 +104,7 @@ def _load_assets(self): if hasattr(asset_type, "from_dict"): asset = asset_type.from_dict(toml_data, self.l10n) else: - logger.info("skipped " + toml_path.name) + logger.info("skipped %s", toml_path.name) continue self.assets[asset_type.__name__].append(asset)