diff --git a/player.py b/player.py index 3723a0c..faa2762 100644 --- a/player.py +++ b/player.py @@ -1,25 +1,165 @@ -import function_list from random import randint -from enum_list import * +from enum_list import DamageType, DamageMod, ItemType, Slots # Import specific enums + +# Define Item classes here, as they are fundamental building blocks +class Item: + """The base class for all items""" + def __init__(self, name, description, value, magical, itemType, attunement): + self.name = name + self.description = description + self.value = value + self.magical = magical + self.itemType = itemType + self.attunement = attunement + + def __repr__(self): + return f"{self.name}\n=====\n{self.description}\nValue: {self.value}\n" + +class Money(Item): + """The currency item used in the world of Kanjin""" + def __init__(self, name, amt, magical, ItemType): + self.name = name + self.amt = amt + self.magical = magical + self.itemType = ItemType + super().__init__(name=self.name, + description=f"A small round coin made of {self.name.lower()} " + f"with the imperial city logo stamped on the face.", + value=self.amt, + magical=self.magical, + itemType=ItemType.Money, + attunement=False) + +class Weapon(Item): + """The base class for all weapons""" + def __init__(self, name, description, value, slot, damage1H, damage2H, versatiledmg, + dmgType, dmgMod, versatile, thrown, ItemType, magical, attunement): + self.slot = slot + self.damage1H = damage1H + self.damage2H = damage2H + self.versatiledmg = versatiledmg + self.dmgType = dmgType + self.dmgMod = dmgMod + self.versatile = versatile + self.thrown = thrown + self.itemType = ItemType + super().__init__(name, description, value, magical, ItemType, attunement) + + def __str__(self): + if self.damage2H is None and self.damage1H is not None: + return f"{self.name}\n=====" \ + f"\n{self.description}" \ + f"\nValue: {self.value}" \ + f"\nDamage: One Handed - {self.damage1H}" \ + f"\nDamage Type: {self.dmgType.name.title()}" \ + f"\nMagical: {self.magical}" \ + f"\nAttunement: {'Required' if self.attunement else 'Not Required'}" + elif self.damage1H is None and self.damage2H is not None: + return f"{self.name}\n=====" \ + f"\n{self.description}" \ + f"\nValue: {self.value}" \ + f"\nDamage: Two Handed - {self.damage2H}" \ + f"\nDamage Type: {self.dmgType.name.title()}" \ + f"\nMagical: {self.magical}" \ + f"\nAttunement: {'Required' if self.attunement else 'Not Required'}" + elif self.versatile: + return f"{self.name}\n=====" \ + f"\n{self.description}" \ + f"\nValue: {self.value}" \ + f"\nDamage: Versatile - {self.versatiledmg}" \ + f"\nDamage Type: {self.dmgType.name.title()}" \ + f"\nMagical: {self.magical}" \ + f"\nAttunement: {'Required' if self.attunement else 'Not Required'}" + else: + return f"{self.name}\n=====" \ + f"\n{self.description}\n" \ + f"Value: {self.value}\n" \ + f"Magical: {self.magical}\n" \ + f"Attunement: {'Required' if self.attunement else 'Not Required'}" + +class Armor(Item): + """The base class for all armor""" + def __init__(self, name, description, value, slot, grade, ac, disadvantage, dmgRes, ItemType, magical, attunement): + self.slot = slot + self.grade = grade + self.ac = ac + self.stealthDis = disadvantage + self.dmgRes = dmgRes + self.itemType = ItemType + super().__init__(name, description, value, magical, ItemType, attunement) + + def __repr__(self): + if self.stealthDis: + return f"{self.name}\n" \ + f"=====\n" \ + f"{self.description}\n" \ + f"Value: {self.value}\n" \ + f"AC: {self.ac}\n" \ + f"Magical: {self.magical}" + elif not self.stealthDis: + return f"{self.name}\n=====" \ + f"\n{self.description}\n" \ + f"Value: {self.value}\n" \ + f"AC: {self.ac}\n" \ + f"Disadvantage on Stealth checks: {self.stealthDis}\n" \ + f"Magical: {self.magical}\n" + return None + class Inventory: def __init__(self): - # Changed to a dictionary to store item objects as keys and their data (count, equipped, etc.) as values + # Dictionary to store item objects as keys and their data (count) as values self.items = {} + # Dictionary to store equipped items by slot + self.equipped_items = { + Slots.MainHand: None, + Slots.OffHand: None, + Slots.TwoHanded: None, + Slots.Helm: None, + Slots.Chest: None, + Slots.Wrists: None, + Slots.Feet: None, + Slots.Neck: None, + Slots.Cloak: None, + Slots.LeftRing: None, + Slots.RightRing: None, + Slots.Other: None, # For miscellaneous equipped items + Slots.Attunement: 0 # Counter for attuned items + } + + def _get_slot_display_name(self, slot_val): + """Helper to get a user-friendly string name for a slot, handling both Enum and string inputs.""" + if isinstance(slot_val, Slots): + return slot_val.name.replace('TwoHanded', 'Two Handed').replace('MainHand', + 'Main Hand').replace('OffHand', + 'Off Hand').replace('LeftRing', 'Left Ring').replace( + 'RightRing', 'Right Ring') + elif isinstance(slot_val, str): + # If it's already a string, assume it's the name and format it + return (slot_val.replace('TwoHanded', 'Two Handed').replace('MainHand', 'Main Hand') + .replace('OffHand','Off Hand').replace('LeftRing', 'Left Ring') + .replace('RightRing', 'Right Ring')) + return str(slot_val) # Fallback for unexpected types def add_item(self, item, count=1): + """Adds an item to the inventory.""" if item in self.items: self.items[item]["Count"] += count print(f"Added {count} more {item.name}. Total: {self.items[item]['Count']}.") else: - self.items[item] = {"Count": count, "object": item.itemType, "equipped": False} + self.items[item] = {"Count": count, "object": item.itemType} print(f"Added {count} {item.name} to inventory.") def remove_item(self, item, count=1): + """Removes an item from the inventory.""" if item in self.items: if self.items[item]["Count"] <= count: print(f"Removed all {self.items[item]['Count']} {item.name} from inventory.") del self.items[item] + # If the item was equipped, unequip it + for slot, equipped_item in self.equipped_items.items(): + if equipped_item == item: + self.unequip_item(slot) else: self.items[item]["Count"] -= count print(f"Removed {count} {item.name} from inventory. Remaining: {self.items[item]['Count']}.") @@ -29,53 +169,207 @@ class Inventory: def current_inventory(self): """Prints a list of items in your backpack that aren't equipped to your person.""" print(f'In your rucksack you have:') - if not self.items: - print(" Your rucksack is empty.") - return - + found_un_equipped = False for item_obj, item_data in self.items.items(): - # Assuming 'equipped' is a key in item_data and Equipment.inventory holds equipped items - # This logic needs careful alignment with how you manage equipped items. - # For now, it checks if the item object itself is in any of the equipped slots. - if not item_data["equipped"] and item_obj not in Equipment.inventory.values(): + # Check if the item is in the backpack AND not currently equipped in any slot + if item_obj not in self.equipped_items.values(): + found_un_equipped = True count = item_data["Count"] print(f'{item_obj.name} x {count}\n' f' {item_obj.description}\n') + if not found_un_equipped: + print(" Your rucksack is empty.") - @staticmethod - def current_equipment(): + def current_equipment(self): """Prints a list of items that you have equipped in your slots.""" print(f'You are currently wearing:') equipment_found = False - for slot, equipment in Equipment.inventory.items(): - if equipment is not None: + for slot, equipment in self.equipped_items.items(): + if equipment is not None and slot != Slots.Attunement: # Don't print the attunement counter equipment_found = True - if slot == "Attunement1" or slot == "Attunement2" or slot == "Attunement3": # Assuming these are not to be printed - continue - elif equipment.itemType == ItemType.Weapon: + slot_name = slot.name.replace("TwoHanded", "Two Handed").replace("MainHand", "Main Hand").replace("OffHand", "Off Hand").replace("LeftRing", "Left Ring").replace("RightRing", "Right Ring") + if equipment.itemType == ItemType.Weapon: if equipment.versatile: - print(f'{slot}: {equipment.name} - {equipment.versatiledmg}\n' + print(f'{slot_name}: {equipment.name} - {equipment.versatiledmg}\n' f' {equipment.description}\n') - continue elif equipment.damage1H is not None: - print(f'{slot}: {equipment.name} - {equipment.damage1H}\n' + print(f'{slot_name}: {equipment.name} - {equipment.damage1H}\n' f' {equipment.description}\n') elif equipment.damage2H is not None: - print(f'{slot}: {equipment.name} - {equipment.damage2H}\n' + print(f'{slot_name}: {equipment.name} - {equipment.damage2H}\n' f' {equipment.description}\n') elif equipment.itemType == ItemType.Armor: - print(f'{slot}: {equipment.name} - AC {equipment.ac}\n' + print(f'{slot_name}: {equipment.name} - AC {equipment.ac}\n' f' {equipment.description}\n') elif equipment.itemType == ItemType.Item: - print(f'{slot}: {equipment.name}\n' + print(f'{slot_name}: {equipment.name}\n' f' {equipment.description}\n') if not equipment_found: print(" Nothing equipped.") + print(f"Attunement slots used: {self.equipped_items[Slots.Attunement]}/3") + + + def equip_item(self, item_obj, slot: Slots): + """Equips an item to a specific slot.""" + if item_obj not in self.items or self.items[item_obj]["Count"] == 0: + print(f"You don't have {item_obj.name} to equip.") + return + + # Check attunement limits before equipping (moved up for early exit) + if item_obj.attunement and self.equipped_items[Slots.Attunement] >= 3: + print(f"You cannot equip {item_obj.name}. You have reached your attunement limit (3/3).") + return + + # Handle unequipping existing item in the target slot first + if self.equipped_items[slot] is not None: + self.unequip_item(slot) + + # Now, handle equipping based on item type and specific properties + if item_obj.itemType == ItemType.Weapon: + if item_obj.slot == Slots.TwoHanded: + # If equipping a two-handed weapon, unequip anything in main/off-hand + if self.equipped_items[Slots.MainHand] is not None: + self.unequip_item(Slots.MainHand) + if self.equipped_items[Slots.OffHand] is not None: + self.unequip_item(Slots.OffHand) + self.equipped_items[Slots.TwoHanded] = item_obj + print(f"Equipped {item_obj.name} to {self._get_slot_display_name(Slots.TwoHanded)}.") + elif item_obj.versatile: + # If equipping a versatile weapon, it goes in MainHand, and potentially affects OffHand + if self.equipped_items[Slots.TwoHanded] is not None: + self.unequip_item(Slots.TwoHanded) # Unequip two-handed if present + self.equipped_items[Slots.MainHand] = item_obj + # A versatile weapon can be used two-handed, implying it might occupy the off-hand conceptually + # For simplicity, we'll just equip it to MainHand here. If you want it to occupy off-hand, + # you'd need more complex logic for 1H vs 2H use. + print(f"Equipped {item_obj.name} to {self._get_slot_display_name(Slots.MainHand)}.") + else: # Standard one-handed weapon (or other weapon types not specifically handled above) + self.equipped_items[slot] = item_obj # Equip to the specified slot (MainHand or OffHand) + print(f"Equipped {item_obj.name} to {self._get_slot_display_name(slot)}.") + elif item_obj.itemType == ItemType.Armor: + # For armor, ensure the target slot is appropriate for armor (e.g., Chest, Helm) + # The Armor class now has a 'slot' attribute, so we can use that for validation/assignment + if item_obj.slot == slot: # Ensure the item's intended slot matches the target slot + self.equipped_items[slot] = item_obj + print(f"Equipped {item_obj.name} to {self._get_slot_display_name(slot)}.") + else: + # Corrected this line to use _get_slot_display_name for both slot and item_obj.slot + print(f"Cannot equip {item_obj.name} to {self._get_slot_display_name(slot)}. It belongs in the {self._get_slot_display_name(item_obj.slot)} slot.") + return # Exit if slot mismatch + elif item_obj.itemType == ItemType.Item: + # For general items (rings, cloaks, etc.), equip to the specified slot + self.equipped_items[slot] = item_obj + print(f"Equipped {item_obj.name} to {self._get_slot_display_name(slot)}.") + else: + print(f"Cannot equip {item_obj.name}. Unknown item type or invalid slot for this item.") + return # Exit if item type is not recognized for equipping + + self.update_attunement() # Update attunement after successful equip + + + def unequip_item(self, slot: Slots): + """Unequips an item from a specific slot.""" + if self.equipped_items[slot] is not None: + unequipped_item = self.equipped_items[slot] + self.equipped_items[slot] = None + print(f"Unequipped {unequipped_item.name} from {self._get_slot_display_name(slot)}.") # Use helper function + self.update_attunement() + else: + print(f"Nothing is equipped in {self._get_slot_display_name(slot)}.") # Use helper function + + def update_attunement(self): + """Recalculates the number of attuned items.""" + attune_count = 0 + for slot, item in self.equipped_items.items(): + if slot != Slots.Attunement and item is not None and item.attunement: + attune_count += 1 + self.equipped_items[Slots.Attunement] = attune_count + + +# --- Item Instances (Moved here for clarity, but still defined once) --- +# Different types of coins +GP1 = Money("Gold", 1, False, ItemType.Money) +SP1 = Money("Silver", 1, False, ItemType.Money) +CP1 = Money("Copper", 1, False, ItemType.Money) + +# starting equip +rock = Weapon( + name="Rock", + description="A fist-sized rock, suitable for bludgeoning.", + value=None, + slot=Slots.MainHand, + damage1H='1d6', + damage2H=None, + versatiledmg=None, + dmgType=DamageType.Bludgeoning, + dmgMod=DamageMod.Strength, + versatile=False, + thrown=True, + magical=False, + ItemType=ItemType.Weapon, + attunement=False # Rocks typically aren't attuned +) + +dagger = Weapon( + name="Dagger", + description="Pointy stabby-stab", + value=None, + slot=Slots.MainHand, # Can be main hand or off hand + damage1H='1d4', # Daggers are usually 1d4 + damage2H=None, + versatiledmg=None, # Daggers are not versatile in the D&D sense (they are light, finesse) + dmgType=DamageType.Piercing, # Changed to piercing + dmgMod=DamageMod.Dexterity, + versatile=False, # Changed to False, as D&D versatile means 1H or 2H damage + thrown=True, + magical=False, + ItemType=ItemType.Weapon, + attunement=False +) + +polearm = Weapon( + name="Polearm", + description="Long Pointy stabby-stab", + value=None, + slot=Slots.TwoHanded, + damage1H=None, + damage2H="1d10", # Polearms are typically 1d10 + versatiledmg=None, + dmgType=DamageType.Piercing, + dmgMod=DamageMod.Strength, + versatile=False, + thrown=False, # Polearms are not typically thrown + magical=False, + ItemType=ItemType.Weapon, + attunement=False +) + +tornRags = Armor( + name="Torn Rags", + description="A ripped and worn-out outfit.", + value=None, + slot=Slots.Chest, + grade="Light", + ac=10, # Base AC for light armor without proficiency is 10 + Dex mod + disadvantage=False, # Light armor usually doesn't give disadvantage + dmgRes="None", + magical=False, + ItemType=ItemType.Armor, + attunement=False +) + +paper = Item( + name="Paper", + description="A blank piece of paper", + value=None, + magical=False, + itemType=ItemType.Item, + attunement=False +) + class Player: """Character Creation""" - - # Default Character def __init__(self, name, gender, age, race, job): # Identity self.name = name @@ -85,27 +379,29 @@ class Player: self.job = job # Defence - self.ac = 0 - self.elemental_resistance = {DamageType.Fire: False, DamageType.Water: False, DamageType.Lightning: False} - self.physical_resistance = {DamageType.Slashing: False, DamageType.Crushing: False, DamageType.Piercing: False} + self.ac = 0 # This will be calculated based on equipped armor + self.elemental_resistance = {DamageType.Fire: False, + DamageType.Cold: False, + DamageType.Lightning: False, + DamageType.Thunder: False, + DamageType.Poison: False, + DamageType.Acid: False, + DamageType.Necrotic: False, + DamageType.Radiant: False, + DamageType.Force: False, + DamageType.Psychic: False + } + self.physical_resistance = {DamageType.Slashing: False, + DamageType.Bludgeoning: False, + DamageType.Piercing: False} # Levels self.level = 1 self.exp = 0 self.maxEXP = 100 - # Equipment - # Held items - self.inventory = Inventory() - self.weapon = rock - self.armor = tornRags - - # Example of adding starting items to inventory - self.inventory.add_item(rock, 1) - self.inventory.add_item(tornRags, 1) - # Assuming you want to auto-equip these initially - self.equip_item(rock, "Main Hand") - self.equip_item(tornRags, "Chest") + # Equipment and Inventory + self.inventory = Inventory() # Each player gets their own inventory instance # Ability Scores self.stats = {"Strength": 0, @@ -122,6 +418,13 @@ class Player: "Wisdom": 0, "Charisma": 0} + # Initial items and equipment + self.inventory.add_item(rock, 1) + self.inventory.add_item(tornRags, 1) + # Equip initial items (using the inventory's equip method) + self.inventory.equip_item(rock, Slots.MainHand) + self.inventory.equip_item(tornRags, Slots.Chest) + def getModifier(self): """Floor calculation to work out skill check modifiers""" @@ -133,7 +436,9 @@ class Player: self.mods["Charisma"] = -5 + self.stats["Charisma"] // 2 def set_stats_race(self): - if self.race.lower() == "human": + """Applies racial ability score increases.""" + # Access name attribute of Race object + if self.race.name.lower() == "human": print("As a Human, two different ability scores of your choice increase by 1.") selection1 = input("Choose the first ability score to increase by 1.\n" "1) Strength\n" @@ -207,162 +512,63 @@ class Player: "6) Charisma\n >> ").lower() continue - elif self.race.lower() == "elf": + elif self.race.name.lower() == "elf": + print("As an Elf, your dexterity increases by 2.") self.stats["Dexterity"] += 2 - - elif self.race.lower() == "dwarf": + elif self.race.name.lower() == "dwarf": + print("As a Dwarf, your constitution increases by 2.") self.stats["Constitution"] += 2 def set_stats_job(self): - if self.job.lower() == "warrior": - self.hitDie = randint(1, 12) - self.maxHP = (12 + self.mods["Constitution"]) - self.currentHP = (12 + self.mods["Constitution"]) - - elif self.job.lower() == "cleric": - self.hitDie = randint(1, 10) - self.maxHP = (10 + self.mods["Constitution"]) - self.currentHP = (10 + self.mods["Constitution"]) - - elif self.job.lower() == "mage": - self.hitDie = randint(1, 8) - self.maxHP = (8 + self.mods["Constitution"]) - self.currentHP = (8 + self.mods["Constitution"]) + """Applies job-specific stats like hit points.""" + # Using self.job.name to access the job name from the Job object + if self.job.name.lower() == "barbarian": + self.hitDie = randint(self.job.hitdie[0], self.job.hitdie[1]) # Use job's hitdie range + self.maxHP = (self.job.hitdie[1] + self.mods["Constitution"]) # Use max hitdie value for initial HP + self.currentHP = self.maxHP + elif self.job.name.lower() == "cleric": + self.hitDie = randint(self.job.hitdie[0], self.job.hitdie[1]) + self.maxHP = (self.job.hitdie[1] + self.mods["Constitution"]) + self.currentHP = self.maxHP + elif self.job.name.lower() == "wizard": + self.hitDie = randint(self.job.hitdie[0], self.job.hitdie[1]) + self.maxHP = (self.job.hitdie[1] + self.mods["Constitution"]) + self.currentHP = self.maxHP def current_stats(self): """Prints a display of the user's current statistics.""" print("Your current stats are:") print(f'Hit Points: {self.currentHP}/{self.maxHP}') - print(f'Strength: {self.stats["Strength"]}') - print(f'Dexterity: {self.stats["Dexterity"]}') - print(f'Constitution: {self.stats["Constitution"]}') - print(f'Intelligence: {self.stats["Intelligence"]}') - print(f'Wisdom: {self.stats["Wisdom"]}') - print(f'Charisma: {self.stats["Charisma"]}') - - def current_equip(self): - """Prints a list of items currently equipped.""" - print(f"You have equipped:\n {self.armor.name}\n" - f" {self.armor.description}" - f"\n{self.weapon.name}\n" - f" {self.weapon.description}") + print(f'Strength: {self.stats["Strength"]} ({self.mods["Strength"]:+})') + print(f'Dexterity: {self.stats["Dexterity"]} ({self.mods["Dexterity"]:+})') + print(f'Constitution: {self.stats["Constitution"]} ({self.mods["Constitution"]:+})') + print(f'Intelligence: {self.stats["Intelligence"]} ({self.mods["Intelligence"]:+})') + print(f'Wisdom: {self.stats["Wisdom"]} ({self.mods["Wisdom"]:+})') + print(f'Charisma: {self.stats["Charisma"]} ({self.mods["Charisma"]:+})') def drop_item(self, item_name): - # Find the item object by name + """Drops an item from the player's inventory.""" item_to_drop = None for item_obj in self.inventory.items: if item_obj.name.lower() == item_name.lower(): item_to_drop = item_obj break - if item_to_drop: - self.inventory.remove_item(item_to_drop, count=1) # Remove one by default + self.inventory.remove_item(item_to_drop, count=1) else: print(f"You don't have '{item_name}' in your inventory.") - def loot_object(self, object_name): - # This method assumes a specific structure for Map.scenes and lootable_items - for scene_name, room_data in Map.scenes.items(): - if Engine.seed == scene_name: - found_item = False - for item_key_str, item_value_dict in room_data.get("lootable_items", {}).items(): - # item_value_dict is like {Item_obj: count} - for item_obj, count in item_value_dict.items(): - if object_name.lower() in item_obj.name.lower(): - if count > 0: - self.inventory.add_item(item_obj, count) - room_data["lootable_items"][item_key_str][item_obj] = 0 # Set count to 0 in room - found_item = True - break - if found_item: - break - if not found_item: - print("You don't see one of those to loot.") - return # Exit after checking the current room - - def add_inventory(self, object_name): - # This method assumes a specific structure for Map.scenes and available_items - for scene_name, room_data in Map.scenes.items(): - if Engine.seed == scene_name: - found_item = False - for item_key_str, item_value_dict in room_data.get("available_items", {}).items(): - # item_value_dict is like {Item_obj: count} - for item_obj, count_available in item_value_dict.items(): - if object_name.lower() in item_obj.name.lower(): - if count_available == 0: - print(f"There are no {item_obj.name} left here.") - found_item = True - break - - response = input(f'There are {count_available} {item_obj.name} available.\n' - f'How many will you take? (enter "all" or a number)\n>> ').lower() - - if response == "0" or response == "none": - print("You decide not to take any.") - found_item = True - break - elif response == "all": - self.inventory.add_item(item_obj, count_available) - room_data["available_items"][item_key_str][item_obj] = 0 - found_item = True - break - elif response.isdigit(): - num_to_take = int(response) - if num_to_take <= count_available: - self.inventory.add_item(item_obj, num_to_take) - room_data["available_items"][item_key_str][item_obj] -= num_to_take - found_item = True - break - else: - print("You can't take more than exist.") - found_item = True - break - else: - print("Invalid input. Please enter 'all' or a number.") - found_item = True - break - if found_item: - break - if not found_item: - print("You don't see any lying around.") - return # Exit after checking the current room - - def equip_item(self, item_obj, slot): - """Equips an item to a specific slot.""" - if item_obj in self.inventory.items and self.inventory.items[item_obj]["Count"] > 0: - # Unequip existing item in that slot if any - if Equipment.inventory[slot] is not None: - unequipped_item = Equipment.inventory[slot] - print(f"Unequipping {unequipped_item.name} from {slot}.") - self.inventory.items[unequipped_item]["equipped"] = False # Mark as unequipped in inventory - - Equipment.inventory[slot] = item_obj - self.inventory.items[item_obj]["equipped"] = True - print(f"Equipped {item_obj.name} to {slot}.") - else: - print(f"You don't have {item_obj.name} to equip.") - - def unequip_item(self, slot): - """Unequips an item from a specific slot.""" - if Equipment.inventory[slot] is not None: - unequipped_item = Equipment.inventory[slot] - self.inventory.items[unequipped_item]["equipped"] = False - Equipment.inventory[slot] = None - print(f"Unequipped {unequipped_item.name} from {slot}.") - else: - print(f"Nothing is equipped in {slot}.") - def new_char(self): """Outputs final stats, armour, and inventory""" - self.allocation() - self.set_stats_race() - self.set_stats_job() - self.getModifier() - self.current_stats() + self.allocation() # 1. Sets base self.stats from rolls. + self.set_stats_race() # 2. Applies racial bonuses to self.stats. + self.getModifier() # 3. Calculates ALL self.mods based on the FINAL self.stats (base + racial). + self.set_stats_job() # 4. Calculates maxHP using the now-accurate self.mods["Constitution"]. + self.current_stats() # 5. Displays the final stats. print(' ') - Inventory.current_equipment() # Call the static method of Inventory + self.inventory.current_equipment() # Corrected to call instance method print(' ') - self.inventory.current_inventory() # Call the instance method of Player's inventory + self.inventory.current_inventory() print(' ') def health_check(self): @@ -378,61 +584,61 @@ class Player: return damage def allocation(self): + """Handles the interactive allocation of rolled stats to attributes.""" + # Mapping for shorthand attribute names + attribute_map = { + "strength": "Strength", "str": "Strength", + "dexterity": "Dexterity", "dex": "Dexterity", + "constitution": "Constitution", "con": "Constitution", + "intelligence": "Intelligence", "int": "Intelligence", + "wisdom": "Wisdom", "wis": "Wisdom", + "charisma": "Charisma", "cha": "Charisma" + } + while True: rolls = [] stats = [] attributes = ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"] while len(stats) != 6: - # roll 4d6 - for roll in range(4): - r = randint(1, 6) - # This rolling logic (if r < 6: r = randint(1, 6)) seems to aim for higher rolls. - # Standard D&D 4d6 drop lowest usually means you just roll 4 and drop the lowest. - # I'll keep your original logic but note it's non-standard. - if r < 6: - r = randint(1, 6) - rolls.append(r) - # Drop the lowest number and then add to single value - del rolls[rolls.index(min(rolls))] - val = sum(rolls) + # Roll 4d6, drop lowest + four_d_six = sorted([randint(1, 6) for _ in range(4)]) + val = sum(four_d_six[1:]) # Sum the top 3 stats.append(str(val)) - rolls = [] + rolls = [] # Reset for next roll print(f"Please assign a stat to a selected attribute by entering the number then the attribute.\n" - f"For example, '10 strength' will assign 10 to your strength, if you have a 10 available.\n\n" - f"Please select a stat and an attribute.\n\n" + f"For example, '10 strength' or '10 str' will assign 10 to your strength, if you have a 10 available.\n\n" f"Your rolled stats are:\n {', '.join(stats)}\n\n" f"Your attributes are:\n {', '.join(attributes)}\n") - # Allocate - current_attributes = list(attributes) # Make a copy to modify - current_stats_to_assign = list(stats) # Make a copy + current_attributes = list(attributes) + current_stats_to_assign = list(stats) - while len(current_stats_to_assign) != 0: - input_text = input(">> ").title() + # Reset player stats for new allocation attempt + self.stats = {"Strength": 0, "Dexterity": 0, "Constitution": 0, "Intelligence": 0, "Wisdom": 0, "Charisma": 0} + + while len(current_stats_to_assign) > 0: + input_text = input(">> ").lower().strip() # Convert to lower and strip whitespace words = input_text.split() - if len(words) == 2: - try: - chosen_stat_str = words[0] - chosen_attribute = words[1] - if chosen_stat_str in current_stats_to_assign and chosen_attribute in current_attributes: - self.stats[chosen_attribute] += int(chosen_stat_str) - current_stats_to_assign.remove(chosen_stat_str) - current_attributes.remove(chosen_attribute) - if len(current_stats_to_assign) == 1: - self.stats[current_attributes[0]] += int(current_stats_to_assign[0]) - print("") - break - print(f"The remaining stats are:\n {', '.join(current_stats_to_assign)}\n\n" - f"The remaining attributes are:\n {', '.join(current_attributes)}") - else: - print( - "Error. Either the number or attribute was not available/recognised. Please try again with 'Number' + 'Attribute'.") - except ValueError: - print("Error. Invalid number format. Please try again with 'Number' + 'Attribute'.") - except KeyError: - print("Error. Attribute not recognized. Please try again with 'Number' + 'Attribute'.") + if len(words) == 2: + chosen_stat_str = words[0] + chosen_attribute_input = words[1] + + # Resolve shorthand to full attribute name + chosen_attribute_full = attribute_map.get(chosen_attribute_input, None) + + if chosen_stat_str in current_stats_to_assign and chosen_attribute_full in current_attributes: + self.stats[chosen_attribute_full] = int(chosen_stat_str) # Assign, not add + current_stats_to_assign.remove(chosen_stat_str) + current_attributes.remove(chosen_attribute_full) + if len(current_stats_to_assign) == 0: + break # All stats assigned + print(f"The remaining stats are:\n {', '.join(current_stats_to_assign)}\n\n" + f"The remaining attributes are:\n {', '.join(current_attributes)}") + else: + print( + "Error. Either the number or attribute was not available/recognised. Please try again with 'Number' + 'Attribute'.") else: print("Error. Input was not recognised. Please try again with 'Number' + 'Attribute'.") @@ -448,237 +654,9 @@ class Player: "WARNING: If you select 'N', your dice will be randomly rolled again. Proceed?\n" " >> ").lower() if select == "n": - # Reset stats for re-allocation - self.stats = {"Strength": 0, "Dexterity": 0, "Constitution": 0, "Intelligence": 0, "Wisdom": 0, - "Charisma": 0} - continue + continue # Loop back to re-roll and re-allocate elif select == 'y': break else: print("Error. Input was not recognised. Please select 'Y' or 'N'.") # Loop back to ask again or handle as desired - -class Item: - """The base class for all items""" - - def __init__(self, name, description, value, magical, itemType, attunement): - self.name = name - self.description = description - self.value = value - self.magical = magical - self.itemType = itemType - self.attunement = attunement - - def __repr__(self): - return f"{self.name}\n=====\n{self.description}\nValue: {self.value}\n" - - -class Money(Item): - """The currency item used in the world of Kanjin""" - - def __init__(self, name, amt, magical, ItemType): - self.name = name - self.amt = amt - self.magical = magical - self.itemType = ItemType - super().__init__(name=self.name, - description=f"A small round coin made of {self.name.lower()} " - f"with the imperial city logo stamped on the face.", - value=self.amt, - magical=self.magical, - ItemType=ItemType.Money, - attunement=False) - - -class Weapon(Item): - """The base class for all weapons""" - - def __init__(self, name, description, value, slot, damage1H, damage2H, versatiledmg, - dmgType, dmgMod, versatile, thrown, ItemType, magical, attunement): - self.slot = slot - self.damage1H = damage1H - self.damage2H = damage2H - self.versatiledmg = versatiledmg - self.dmgType = dmgType - self.dmgMod = dmgMod - self.versatile = versatile - self.thrown = thrown - self.itemType = ItemType - super().__init__(name, description, value, magical, ItemType, attunement) - - def __str__(self): - if self.damage2H is None and self.damage1H is not None: - return f"{self.name}\n=====" \ - f"\n{self.description}" \ - f"\nValue: {self.value}" \ - f"\nDamage: One Handed - {self.damage1H}" \ - f"\nDamage Type: {self.dmgType.name.title()}" \ - f"\nMagical: {self.magical}" \ - f"\nAttunement: {'Required' if self.attunement else 'Not Required'}" - elif self.damage1H is None and self.damage2H is not None: - return f"{self.name}\n=====" \ - f"\n{self.description}" \ - f"\nValue: {self.value}" \ - f"\nDamage: Two Handed - {self.damage2H}" \ - f"\nDamage Type: {self.dmgType.name.title()}" \ - f"\nMagical: {self.magical}" \ - f"\nAttunement: {'Required' if self.attunement else 'Not Required'}" - elif self.versatile: - return f"{self.name}\n=====" \ - f"\n{self.description}" \ - f"\nValue: {self.value}" \ - f"\nDamage: Versatile - {self.versatiledmg}" \ - f"\nDamage Type: {self.dmgType.name.title()}" \ - f"\nMagical: {self.magical}" \ - f"\nAttunement: {'Required' if self.attunement else 'Not Required'}" - else: - return f"{self.name}\n=====" \ - f"\n{self.description}" \ - f"\nValue: {self.value}" \ - f"\nMagical: {self.magical}" \ - f"\nAttunement: {'Required' if self.attunement else 'Not Required'}" - - -class Armor(Item): - """The base class for all armor""" - - def __init__(self, name, description, value, grade, ac, disadvantage, dmgRes, ItemType, magical, attunement): - self.grade = grade - self.ac = ac - self.stealthDis = disadvantage - self.dmgRes = dmgRes - self.itemType = ItemType - super().__init__(name, description, value, magical, ItemType, attunement) - - def __repr__(self): - if self.stealthDis: - return f"{self.name}\n" \ - f"=====\n" \ - f"{self.description}\n" \ - f"Value: {self.value}\n" \ - f"AC: {self.ac}\n" \ - f"Magical: {self.magical}" - elif not self.stealthDis: - return f"{self.name}\n=====" \ - f"\n{self.description}\n" \ - f"Value: {self.value}\n" \ - f"AC: {self.ac}\n" \ - f"Disadvantage on Stealth checks: {self.stealthDis}\n" \ - f"Magical: {self.magical}\n" - - -# Different types of coins -GP1 = Money("Gold", 1, False, ItemType.Money) -SP1 = Money("Silver", 1, False, ItemType.Money) -CP1 = Money("Copper", 1, False, ItemType.Money) - -# starting equip -rock = Weapon( - name="Rock", - description="A fist-sized rock, suitable for bludgeoning.", - value=None, - slot="Main Hand", - damage1H='1d6', - damage2H=None, - versatiledmg=None, - dmgType=DamageType.Crushing, - dmgMod=DamageMod.Strength, - versatile=False, - thrown=True, - magical=False, - ItemType=ItemType.Weapon, - attunement=True -) - -dagger = Weapon( - name="Dagger", - description="Pointy stabby-stab", - value=None, - slot="Main Hand", - damage1H=None, - damage2H=None, - versatiledmg='1d6', - dmgType=DamageType.Slashing, - dmgMod=DamageMod.Dexterity, - versatile=True, - thrown=True, - magical=False, - ItemType=ItemType.Weapon, - attunement=True -) - -polearm = Weapon( - name="Polearm", - description="Long Pointy stabby-stab", - value=None, - slot="Two Handed", - damage1H=None, - damage2H="2d6", - versatiledmg=None, - dmgType=DamageType.Piercing, - dmgMod=DamageMod.Strength, - versatile=False, - thrown=True, - magical=False, - ItemType=ItemType.Weapon, - attunement=True -) - -tornRags = Armor( - name="Torn Rags", - description="A ripped and worn-out outfit.", - value=None, - grade="Light", - ac=0, - disadvantage=True, - dmgRes="None", - magical=False, - ItemType=ItemType.Armor, - attunement=True -) -paper = Item( - name="Paper", - description="A blank piece of paper", - value=None, - magical=False, - ItemType=ItemType.Item, - attunement=True -) - -Inventory = { - rock: {"Count": 1, - "object": Weapon, - }, - dagger: {"Count": 1, - "object": Weapon, - }, - polearm: {"Count": 1, - "object": Weapon - }, - paper: {"Count": 1, - "object": Item - }, - tornRags: {"Count": 1, - "object": Armor - }, - GP1: {"Count": 10, - "object": Money - }, -} - -Equipment = { - "Main Hand": rock, - "Off Hand": None, - "Two Handed": None, - "Helm": None, - "Chest": tornRags, - "Wrists": None, - "Feet": None, - "Neck": None, - "Cloak": None, - "Left Ring": None, - "Right Ring": None, - "Other": paper, - "Attunement": 0 -} -