diff --git a/game/action.py b/game/action.py index a880e96..b298623 100644 --- a/game/action.py +++ b/game/action.py @@ -137,6 +137,9 @@ def __init__(self, item: Item): def perform(self, actor: Actor, level: Level, log: MessageLog) -> bool: assert isinstance(actor, Player) + if self.item == actor.inventory.weapon_slot: + log.append("That's already in use.") + return False actor.inventory.weapon_slot = self.item log.append(f"You are now wielding {self.item}.") return True diff --git a/game/combat.py b/game/combat.py index 0940bd3..f29ae18 100644 --- a/game/combat.py +++ b/game/combat.py @@ -1,8 +1,8 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import InitVar, dataclass, field -from game.dice import roll +from game.dice import parse_dice, roll from game.entity import Actor, Player from game.level import Level from game.messages import MessageLog @@ -41,15 +41,21 @@ def ac(self) -> int: return self.base_ac - self.plus_ac -@dataclass(frozen=True, slots=True) +@dataclass(eq=False, slots=True, kw_only=True) class Weapon: - # weapon damage die - damage: int + # weapon damage dice + base_dmg: tuple[int, int] = field(init=False) # to hit bonus plus_hit: int = 0 # damage bonus plus_dmg: int = 0 + # damage dice expression + dmg_dice: InitVar[str] + + def __post_init__(self, dmg_dice: str) -> None: + self.base_dmg = parse_dice(dmg_dice) + def melee_attack(attacker: Actor, defender: Actor, level: Level, log: MessageLog) -> None: assert isinstance(attacker, Player) != isinstance(defender, Player) @@ -63,10 +69,11 @@ def melee_attack(attacker: Actor, defender: Actor, level: Level, log: MessageLog thac0 = 21 - attacker.stats.hd hit = roll(1, d=20) + to_hit_bonus >= thac0 - armor_class if hit: - damage_die = attacker.stats.dmg + damage_dice = 1, attacker.stats.dmg if isinstance(attacker, Player) and attacker.inventory.weapon_slot: - damage_die = attacker.inventory.weapon_slot.weapon.damage - damage = roll(1, d=damage_die) + damage_bonus + damage_dice = attacker.inventory.weapon_slot.weapon.base_dmg + damage = roll(*damage_dice) + damage_bonus + damage = max(0, damage) defender.stats.hp = max(0, defender.stats.hp - damage) log.append(f"You hit the {defender.name}." if isinstance(attacker, Player) else f"The {attacker.name} hits you.") diff --git a/game/dice.py b/game/dice.py index 3808e9e..609bd7a 100644 --- a/game/dice.py +++ b/game/dice.py @@ -1,6 +1,7 @@ from __future__ import annotations import random +import re def roll(n: int, d: int) -> int: @@ -13,3 +14,9 @@ def roll(n: int, d: int) -> int: def percent(p: int) -> bool: return random.randrange(100) < p + + +def parse_dice(dice: str) -> tuple[int, int]: + assert re.match(r"^\d+d\d+$", dice) + n, d = dice.split('d') + return int(n), int(d) diff --git a/game/entity.py b/game/entity.py index b8779e1..3a43942 100644 --- a/game/entity.py +++ b/game/entity.py @@ -59,6 +59,12 @@ def __str__(self) -> str: class WeaponItem(Item): weapon: Weapon + def __str__(self) -> str: + if self.identified: + return f"a {self.weapon.plus_hit:+d},{self.weapon.plus_dmg:+d} {self.name}" + else: + return f"{article(self.name)} {self.name}" + def article(noun: str) -> str: return "an" if noun[0] in 'aeiou' else "a" diff --git a/game/game_loop.py b/game/game_loop.py index 089a4c6..34c6331 100644 --- a/game/game_loop.py +++ b/game/game_loop.py @@ -26,8 +26,14 @@ def new_game() -> tuple[Player, Level, MessageLog]: ) inventory.add_item(armor) inventory.armor_slot = armor - weapon = WeaponItem(x=0, y=0, glyph=Glyph.WEAPON, name='+1,+1 mace', - weapon=Weapon(plus_hit=1, plus_dmg=1, damage=9)) + weapon = WeaponItem( + x=0, + y=0, + glyph=Glyph.WEAPON, + name="mace", + weapon=Weapon(plus_hit=+1, plus_dmg=+1, dmg_dice='2d4'), + identified=True, + ) inventory.add_item(weapon) inventory.weapon_slot = weapon player = Player( diff --git a/game/items.py b/game/items.py index 61bc2cf..6aabae5 100644 --- a/game/items.py +++ b/game/items.py @@ -49,8 +49,11 @@ def spawn(self, x: int, y: int) -> Item: ] weapon_types = [ - ItemType(1, Glyph.WEAPON, 'dagger', Weapon(damage=6)), - ItemType(1, Glyph.WEAPON, 'long sword', Weapon(damage=10)), + ItemType(1, Glyph.WEAPON, "mace", Weapon(dmg_dice='2d4')), + ItemType(1, Glyph.WEAPON, "longsword", Weapon(dmg_dice='3d4')), + ItemType(1, Glyph.WEAPON, "dagger", Weapon(dmg_dice='1d6')), + ItemType(1, Glyph.WEAPON, "two-handed sword", Weapon(dmg_dice='4d4')), + ItemType(1, Glyph.WEAPON, "spear", Weapon(dmg_dice='2d3')), ] @@ -68,7 +71,7 @@ def get_item_types(self) -> tuple[list[ItemType], list[int]]: ItemCategory(27, potion_types), ItemCategory(27, scroll_types), ItemCategory(8, armor_types), - ItemCategory(9, weapon_types), + ItemCategory(8, weapon_types), ] diff --git a/game/procgen.py b/game/procgen.py index 9679f39..3ee0146 100644 --- a/game/procgen.py +++ b/game/procgen.py @@ -4,7 +4,7 @@ import random from game.constants import Glyph, Tile -from game.entity import ArmorItem, Item +from game.entity import ArmorItem, Item, WeaponItem from game.items import get_item_categories from game.level import Level from game.monsters import eligible_monsters @@ -266,6 +266,13 @@ def place_item(all_rooms: list[Room | Junction], level: Level) -> None: item.armor.plus_ac = -rng.randint(1, 3) elif r < 0.28: item.armor.plus_ac = +rng.randint(1, 3) + elif isinstance(item, WeaponItem): + r = rng.random() + if r < 0.10: + item.cursed = True + item.weapon.plus_hit = -rng.randint(1, 3) + elif r < 0.15: + item.weapon.plus_hit = +rng.randint(1, 3) level.entities.add(item) diff --git a/game/state.py b/game/state.py index 5c9bce8..0cda678 100644 --- a/game/state.py +++ b/game/state.py @@ -74,6 +74,9 @@ def event(self, event: tcod.event.Event, player: Player, level: Level, log: Mess case Command.READ: return UseItem(glyph=Glyph.SCROLL) case Command.WIELD: + if player.inventory.weapon_slot and player.inventory.weapon_slot.cursed: + log.append("You can't. It appears to be cursed.") + return Play() return UseItem(glyph=Glyph.WEAPON) case Command.WEAR: if player.inventory.armor_slot: