diff --git a/data/worlds/chaosdorf/areas/cave.toml b/data/worlds/chaosdorf/areas/cave.toml index cf465e5..1eeec9d 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 b70fabb..d8c9277 100644 --- a/data/worlds/chaosdorf/world.toml +++ b/data/worlds/chaosdorf/world.toml @@ -1,6 +1,6 @@ -name = "Chaosdorf" +name = "chaosdorf" language = "en" -areas = ["cave", "lounge", "kitchen", "hackcenter", "floor", "elab", "conference-room", "everyones-toilet", "flinta-toilet", "premium-toilet", "wetlab"] +areas = ["cave", "lounge", "kitchen", "hackcenter", "floor", "elab", "conference room", "everyones toilet", "flinta toilet", "premium toilet", "wetlab"] spawn = "cave" intro_text = """ You awake confused from a deep sleep. 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" diff --git a/src/fantasy_forge/area.py b/src/fantasy_forge/area.py index fd34bed..18f7228 100644 --- a/src/fantasy_forge/area.py +++ b/src/fantasy_forge/area.py @@ -1,5 +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 @@ -7,14 +10,24 @@ 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.""" __important_attributes__ = ("name",) contents: dict[str, Entity] - + def __init__(self: Self, messages: Messages, config_dict: dict[str, Any]): + """ + 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__(messages, config_dict) self.contents: dict = {} diff --git a/src/fantasy_forge/armour.py b/src/fantasy_forge/armour.py index ec3b670..d807e13 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,16 +18,35 @@ class Armour(Item): + """An Armour object.""" + armour_type: str defense: int __important_attributes__ = ("name", "armour_type", "defense") + __attributes__ = {**Item.__attributes__, "armour_type": str, "defense": int} def __init__(self, messages: Messages, config_dict): + """ + 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) + """ + # set armour type 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__(messages, config_dict) def to_dict(self: Self) -> dict: diff --git a/src/fantasy_forge/character.py b/src/fantasy_forge/character.py index 0c379e3..7ba6b4f 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 @@ -26,6 +31,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 @@ -33,6 +39,16 @@ class Character(Entity): _alive: bool def __init__(self: Self, messages: Messages, config_dict: dict[str, Any]) -> 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(messages, BASE_INVENTORY_CAPACITY) self.main_hand = None @@ -40,6 +56,7 @@ def __init__(self: Self, messages: Messages, config_dict: dict[str, Any]) -> Non @property def alive(self: Self) -> bool: + """Return True if the character is alive.""" self._alive = self.health > 0 return self._alive @@ -58,6 +75,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): @@ -80,6 +98,7 @@ def _on_death(self: Self, player: Player): for loot_item in self.inventory: player.area.contents[loot_item.name] = loot_item player.seen_entities[loot_item.name] = loot_item + self.messages.to( [player], "attack-drop-single", diff --git a/src/fantasy_forge/container.py b/src/fantasy_forge/container.py index 7005c4c..914ad26 100644 --- a/src/fantasy_forge/container.py +++ b/src/fantasy_forge/container.py @@ -1,14 +1,40 @@ +"""Container class + +A container is an item in the world which holds an inventory. +""" + from typing import Any, Self -from fantasy_forge.entity import Entity from fantasy_forge.inventory import Inventory -from fantasy_forge.world import World +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__( + 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) -class Container(Entity, Inventory): - __important_attributes__ = ("name", "capacity") - def __init__(self: Self, world: World, config_dict: dict[str, Any]): - capacity: int = config_dict.get("capacity", 10) - Entity.__init__(world, capacity) - Inventory.__init__(world, config_dict) +if TYPE_CHECKING: + from fluent.runtime import FluentLocalization diff --git a/src/fantasy_forge/enemy.py b/src/fantasy_forge/enemy.py index 405164a..078e129 100644 --- a/src/fantasy_forge/enemy.py +++ b/src/fantasy_forge/enemy.py @@ -1,4 +1,7 @@ -from typing import Any, Self +"""Enemy class + +An enemy is a hostile character which will attack the player on contact. +""" from fantasy_forge.character import Character, bare_hands from fantasy_forge.item import Item @@ -7,11 +10,26 @@ class Enemy(Character): """An enemy is a person which will fight back.""" - def __init__(self: Self, messages: Messages, config_dict: dict[str, Any]): + """ + 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__(messages, config_dict) for item_dict in config_dict.get("loot", []): self.inventory.add(Item(messages, item_dict)) 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 ced096c..5d58633 100644 --- a/src/fantasy_forge/entity.py +++ b/src/fantasy_forge/entity.py @@ -1,28 +1,42 @@ +"""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} messages: Messages name: str description: str obvious: bool # obvious entities are seen when entering the room + l10n: FluentLocalization def __init__( self: Self, messages: Messages, config_dict: dict[str, Any], ) -> 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.messages = messages self.name = config_dict.pop("name") self.description = config_dict.pop("description", "") self.obvious = config_dict.pop("obvious", False) - + def on_look(self: Self, actor: Player): actor.shell.stdout.write(self.description + "\n") @@ -48,6 +62,7 @@ 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 diff --git a/src/fantasy_forge/gateway.py b/src/fantasy_forge/gateway.py index 7b4d8ea..1f1bbb9 100644 --- a/src/fantasy_forge/gateway.py +++ b/src/fantasy_forge/gateway.py @@ -1,10 +1,14 @@ +"""Gateway class + +A gateway connects two areas. +""" + from __future__ import annotations from typing import TYPE_CHECKING, Any, Optional, Self from fantasy_forge.area import Area from fantasy_forge.entity import Entity -from fantasy_forge.item import Item from fantasy_forge.key import Key from fantasy_forge.messages import Messages from fantasy_forge.world import World @@ -14,6 +18,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, + } target_str: str # This is not an area because the target might not be loaded yet. target: Optional[Area] @@ -25,12 +35,23 @@ def __init__( messages: Messages, config_dict: dict[str, Any], ): + """ + 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_str = config_dict.pop("target") self.target = None self.locked = config_dict.pop("locked", False) self.key_list = config_dict.pop("key_list", []) super().__init__(messages, config_dict) - + def on_look(self: Self, actor: Player): if self.key_list and self.locked: self.messages.to([actor], "gateway-on-look-locked") @@ -79,6 +100,7 @@ def on_lock(self: Self, actor: Player, 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 85e4877..89f181b 100644 --- a/src/fantasy_forge/inventory.py +++ b/src/fantasy_forge/inventory.py @@ -14,30 +14,33 @@ class InventoryTooSmall(Exception): pass -class Inventory: - """An Inventory contains multiple items.""" +class Inventory(Entity): + """An Inventory contains multiple entities.""" messages: Messages capacity: int - contents: dict[str, Item] + contents: dict[str, Entity] + + __important_attributes__ = ("name", "capacity") + __attributes__ = {**Entity.__attributes__, "capacity": int} def __init__(self: Self, messages: Messages, capacity: int): self.messages = messages self.capacity = capacity self.contents = {} + self.l10n = l10n + + def __len__(self: Self) -> int: + """Returns current capacity.""" + return len(self.contents) def __iter__(self: Self) -> Iterator[Item]: """Iterates over items in inventory.""" yield from self.contents.values() 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 + """Returns if entity is in inventory.""" + return other in self.contents def calculate_weight(self: Self) -> int: weight = 0 @@ -71,14 +74,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: @@ -98,6 +101,12 @@ def on_look(self: Self) -> str: }, ) + def to_dict(self) -> dict: + """Returns inventory as a dictionary.""" + entity_dict: dict = super().to_dict() + inventory_dict: dict = {**entity_dict, "capacity": self.capacity} + return inventory_dict + if TYPE_CHECKING: from fantasy_forge.messages import Messages diff --git a/src/fantasy_forge/item.py b/src/fantasy_forge/item.py index 392bb8f..e01dff9 100644 --- a/src/fantasy_forge/item.py +++ b/src/fantasy_forge/item.py @@ -8,6 +8,8 @@ class Item(Entity): """An Item is an entity which can be picked up by the player.""" + + __attributes__ = {**Entity.__attributes__, "moveable": bool, "carryable": bool, "weight": int} __important_attributes__ = ("name", "moveable", "carryable", "weight") moveable: bool @@ -15,6 +17,17 @@ class Item(Entity): weight: int def __init__(self: Self, messages: Messages, config_dict: dict[str, Any]) -> 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) + 'weight' (int): weight of the item (important for inventory capacity) + + 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) self.weight = config_dict.pop("weight", 1) diff --git a/src/fantasy_forge/key.py b/src/fantasy_forge/key.py index 8559b63..d233ab1 100644 --- a/src/fantasy_forge/key.py +++ b/src/fantasy_forge/key.py @@ -1,6 +1,6 @@ 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.messages import Messages @@ -10,18 +10,47 @@ 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 + def __init__(self: Self, messages: Messages, config_dict: dict[str, Any]) -> 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) + 'weight' (int): weight of the item (important for inventory capacity) + + 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 self.carryable = True # keys are carryable by default + self.weight = 0 # keys are weightless by default + super().__init__(messages, config_dict) 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.""" + return hash(self.key_id) + + +if TYPE_CHECKING: + from fluent.runtime import FluentLocalization 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/localization.py b/src/fantasy_forge/localization.py new file mode 100644 index 0000000..a71c6a4 --- /dev/null +++ b/src/fantasy_forge/localization.py @@ -0,0 +1,41 @@ +from pathlib import Path +from typing import Any + +import huepy +from fluent.runtime import FluentLocalization, FluentResourceLoader +from fluent.runtime.types import FluentNone + +from fantasy_forge.utils import LOCALE_FOLDER + +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/main.py b/src/fantasy_forge/main.py index e7f7e99..3f90ee9 100644 --- a/src/fantasy_forge/main.py +++ b/src/fantasy_forge/main.py @@ -1,14 +1,16 @@ import logging +import toml import shutil + from argparse import ArgumentParser from importlib import resources from pathlib import Path from sys import argv from typing import Any -import toml from xdg_base_dirs import xdg_config_home +from fantasy_forge.area import Area from fantasy_forge.player import Player from fantasy_forge.world import World @@ -57,10 +59,11 @@ 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) # set player name and load world world = World.load(args.world) + name_input = input( world.l10n.format_value("character-name-prompt", {"default_name": args.name}) + " " @@ -77,6 +80,10 @@ def main(): print() player = Player(world, player_name, description) + # enter spawn area + spawn: Area = world.spawn_point + 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 b219544..8637d7d 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 @@ -10,7 +12,10 @@ from fantasy_forge.item import Item from fantasy_forge.shell import Shell from fantasy_forge.weapon import Weapon -from fantasy_forge.world import World + +if TYPE_CHECKING: + from fantasy_forge.world import World + BASE_PLAYER_HEALTH = 100 @@ -38,9 +43,11 @@ def __init__( description=description, health=health, ), + world.l10n, ) self.world = world self.area = Area.empty(world.messages) + # put us in the void # We will (hopefully) never see this, but it's important for the # transition to the next area. @@ -55,8 +62,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 @@ -137,7 +144,7 @@ def equip(self, item_name: str): entity=item_name, ) 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 @@ -180,7 +187,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: self.messages.to( @@ -240,7 +247,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) self.messages.to( @@ -312,6 +319,10 @@ def go(self, gateway_name: str): def enter_gateway(self: Self, gateway: Gateway): """Uses gateway to enter a new area.""" + # TODO: refactor + # The Player holds no reference to the current world, + # so a external function should do the area change + if gateway.locked: self.messages.to( [self], diff --git a/src/fantasy_forge/shell.py b/src/fantasy_forge/shell.py index 313d76d..7b6f753 100644 --- a/src/fantasy_forge/shell.py +++ b/src/fantasy_forge/shell.py @@ -74,7 +74,7 @@ def default(self, line: str): ) 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 @@ -141,7 +141,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, @@ -181,7 +181,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, @@ -258,25 +258,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/utils.py b/src/fantasy_forge/utils.py index 11bca1d..8efd46d 100644 --- a/src/fantasy_forge/utils.py +++ b/src/fantasy_forge/utils.py @@ -1,19 +1,21 @@ -from fantasy_forge.area import Area -from fantasy_forge.item import Item +from pathlib import Path +from string import whitespace +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 + +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) diff --git a/src/fantasy_forge/weapon.py b/src/fantasy_forge/weapon.py index 6fcb0f4..7e3f3c3 100644 --- a/src/fantasy_forge/weapon.py +++ b/src/fantasy_forge/weapon.py @@ -1,4 +1,6 @@ -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.messages import Messages @@ -8,9 +10,23 @@ 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 def __init__(self: Self, messages: Messages, config_dict: dict[str, Any]) -> 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__(messages, config_dict) diff --git a/src/fantasy_forge/world.py b/src/fantasy_forge/world.py index 873d7a1..9b3cbee 100644 --- a/src/fantasy_forge/world.py +++ b/src/fantasy_forge/world.py @@ -1,18 +1,19 @@ from __future__ import annotations import logging -from importlib import resources -from pathlib import Path -from typing import Any, Optional, Self - -import huepy import toml -from fluent.runtime import FluentLocalization, FluentResourceLoader -from fluent.runtime.types import FluentNone + +from collections import defaultdict +from pathlib import Path +from typing import Any, Optional, Self, TYPE_CHECKING +from importlib import resources from fantasy_forge.area import Area +from fantasy_forge.load_assets import ASSET_TYPES +from fantasy_forge.utils import WORLDS_FOLDER from fantasy_forge.messages import Messages + logger = logging.getLogger(__name__) # logger.setLevel(logging.DEBUG) @@ -21,12 +22,14 @@ class World: """A world contains many rooms. It's where the game happens.""" l10n: FluentLocalization - areas: dict[str, Area] - messages: Messages + name: str + areas: dict[str, Area] spawn_str: str # area name to spawn in spawn: Optional[Area] intro_text: str + messages: Messages + assets: dict[str, list[ASSET_TYPE]] # store of all loaded assets def __init__( self: Self, @@ -43,8 +46,14 @@ def __init__( self.spawn = None self.intro_text = intro_text self.messages = Messages(l10n) - - @staticmethod + + self.assets = defaultdict(list) + self._load_assets() + + # populate areas dict + for area in self.assets["Area"]: + self.areas[area.name] = area +@staticmethod def load(name: str) -> World: with resources.as_file(resources.files()) as resource_path: locale_path = resource_path / "l10n/{locale}" @@ -76,6 +85,36 @@ def load(name: str) -> World: areas[area_name] = Area.load(world.messages, path, area_name) world.resolve() 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 %s", 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 %s", toml_path.name) + continue + + self.assets[asset_type.__name__].append(asset) def resolve(self): for area in self.areas.values(): @@ -84,17 +123,10 @@ def resolve(self): self.spawn = self.areas[self.spawn_str] + @property + def spawn_point(self) -> Area: + """Returns spawnpoint as area.""" + return self.areas[self.spawn] -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