Skip to content

Commit

Permalink
Implement armor system
Browse files Browse the repository at this point in the history
Add all armor types and handle base/bonus AC. Implement the generation
of enchanted and cursed armor. Handle cursed armor and identification
mechanics. Review and refine armor-related actions.
  • Loading branch information
leomartius committed Sep 24, 2024
1 parent def8152 commit e1b6bba
Show file tree
Hide file tree
Showing 8 changed files with 51 additions and 8 deletions.
7 changes: 7 additions & 0 deletions game/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ def perform(self, actor: Actor, level: Level, log: MessageLog) -> bool:
if level.get_item_at(actor.x, actor.y):
log.append("There is something there already.")
return False
if self.item.cursed and actor.inventory.is_equipped(self.item):
log.append("You can't. It appears to be cursed.")
return False
actor.inventory.remove_item(self.item)
self.item.x, self.item.y = actor.x, actor.y
level.entities.add(self.item)
Expand Down Expand Up @@ -142,6 +145,7 @@ def __init__(self, item: Item):
def perform(self, actor: Actor, level: Level, log: MessageLog) -> bool:
assert isinstance(actor, Player)
actor.inventory.armor_slot = self.item
self.item.identified = True
log.append(f"You are now wearing {self.item}.")
return True

Expand All @@ -168,6 +172,9 @@ class TakeOffAction(Action):
def perform(self, actor: Actor, level: Level, log: MessageLog) -> bool:
assert isinstance(actor, Player)
if armor := actor.inventory.armor_slot:
if armor.cursed:
log.append("You can't. It appears to be cursed.")
return False
actor.inventory.armor_slot = None
log.append(f"You used to be wearing {armor}.")
return True
Expand Down
12 changes: 9 additions & 3 deletions game/combat.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ def __post_init__(self) -> None:
self.hp = self.max_hp


@dataclass(frozen=True, slots=True)
@dataclass(eq=False, slots=True, kw_only=True)
class Armor:
# armor class
ac: int
# base armor class
base_ac: int
# armor class bonus
plus_ac: int = 0

@property
def ac(self) -> int:
return self.base_ac - self.plus_ac


@dataclass(frozen=True, slots=True)
Expand Down
8 changes: 8 additions & 0 deletions game/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class Player(Actor):

@dataclass(eq=False, slots=True, kw_only=True)
class Item(Entity):
cursed: bool = False
identified: bool = False
gold: int | None = None
consumable: Consumable | None = None

Expand All @@ -46,6 +48,12 @@ def __str__(self) -> str:
class ArmorItem(Item):
armor: Armor

def __str__(self) -> str:
if self.identified:
return f"{self.armor.plus_ac:+d} {self.name} [armor class {self.armor.ac}]"
else:
return self.name


@dataclass(eq=False, slots=True, kw_only=True)
class WeaponItem(Item):
Expand Down
4 changes: 3 additions & 1 deletion game/game_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
def new_game() -> tuple[Player, Level, MessageLog]:
level = generate_level(map_width, map_height, 1)
inventory = Inventory()
armor = ArmorItem(x=0, y=0, glyph=Glyph.ARMOR, name='+1 ring mail', armor=Armor(ac=6))
armor = ArmorItem(
x=0, y=0, glyph=Glyph.ARMOR, name="ring mail", armor=Armor(base_ac=7, plus_ac=+1), identified=True
)
inventory.add_item(armor)
inventory.armor_slot = armor
weapon = WeaponItem(x=0, y=0, glyph=Glyph.WEAPON, name='+1,+1 mace',
Expand Down
4 changes: 4 additions & 0 deletions game/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ def remove_item(self, item: Item) -> None:
if item is self.weapon_slot:
self.weapon_slot = None
self.items.remove(item)

def is_equipped(self, item: Item) -> bool:
assert item in self.items
return item in (self.armor_slot, self.weapon_slot)
12 changes: 9 additions & 3 deletions game/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,14 @@ def spawn(self, x: int, y: int) -> Item:
]

armor_types = [
ItemType(20, Glyph.ARMOR, 'leather armor', Armor(ac=8)),
ItemType(12, Glyph.ARMOR, 'chain mail', Armor(ac=5)),
ItemType(20, Glyph.ARMOR, "leather armor", Armor(base_ac=8)),
ItemType(15, Glyph.ARMOR, "ring mail", Armor(base_ac=7)),
ItemType(15, Glyph.ARMOR, "studded leather armor", Armor(base_ac=7)),
ItemType(13, Glyph.ARMOR, "scale mail", Armor(base_ac=6)),
ItemType(12, Glyph.ARMOR, "chain mail", Armor(base_ac=5)),
ItemType(10, Glyph.ARMOR, "splint mail", Armor(base_ac=4)),
ItemType(10, Glyph.ARMOR, "banded mail", Armor(base_ac=4)),
ItemType(5, Glyph.ARMOR, "plate mail", Armor(base_ac=3)),
]

weapon_types = [
Expand All @@ -61,7 +67,7 @@ def get_item_types(self) -> tuple[list[ItemType], list[int]]:
item_categories = [
ItemCategory(27, potion_types),
ItemCategory(27, scroll_types),
ItemCategory(9, armor_types),
ItemCategory(8, armor_types),
ItemCategory(9, weapon_types),
]

Expand Down
9 changes: 8 additions & 1 deletion game/procgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import random

from game.constants import Glyph, Tile
from game.entity import Item
from game.entity import ArmorItem, Item
from game.items import get_item_categories
from game.level import Level
from game.monsters import eligible_monsters
Expand Down Expand Up @@ -259,6 +259,13 @@ def place_item(all_rooms: list[Room | Junction], level: Level) -> None:
item_types, weights = category.get_item_types()
item_type = rng.choices(item_types, weights).pop()
item = item_type.spawn(x, y)
if isinstance(item, ArmorItem):
r = rng.random()
if r < 0.20:
item.cursed = True
item.armor.plus_ac = -rng.randint(1, 3)
elif r < 0.28:
item.armor.plus_ac = +rng.randint(1, 3)
level.entities.add(item)


Expand Down
3 changes: 3 additions & 0 deletions game/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def event(self, event: tcod.event.Event, player: Player, level: Level, log: Mess
case Command.WIELD:
return UseItem(glyph=Glyph.WEAPON)
case Command.WEAR:
if player.inventory.armor_slot:
log.append("You are already wearing some. You'll have to take it off first.")
return Play()
return UseItem(glyph=Glyph.ARMOR)
case Command.TAKEOFF:
action = TakeOffAction()
Expand Down

0 comments on commit e1b6bba

Please sign in to comment.