From 272a7822f8d50d0edd9acad3ab384cb9ebbf9370 Mon Sep 17 00:00:00 2001 From: "grandsonneo@gmail.com" Date: Mon, 8 Jan 2024 22:25:03 -0500 Subject: [PATCH] Potion of foresight/Increasing stats allows you to choose some of them to increase --- effects.json | 17 +++++--- entity.py | 8 ++++ game_inst.py | 21 +++++----- inventory_ui.py | 6 ++- items.py | 25 +++++++++++- json_obj.py | 15 ++++++- monster.py | 30 ++++++++++---- player.py | 104 +++++++++++++++++++++++++++++++++++++++--------- projectile.py | 4 +- 9 files changed, 185 insertions(+), 45 deletions(-) diff --git a/effects.json b/effects.json index 713d5d8..851dd3d 100644 --- a/effects.json +++ b/effects.json @@ -1,10 +1,10 @@ [ { - "name": "Hasted", - "type": "good", - "apply_msg": "You feel your movements speed up as time appears to slow down.", - "extend_msg": "You feel that your speed will last longer.", - "remove_msg": "You feel yourself slow down to normal speed." + "name": "Hasted", + "type": "good", + "apply_msg": "You feel your movements speed up as time appears to slow down.", + "extend_msg": "You feel that your speed will last longer.", + "remove_msg": "You feel yourself slow down to normal speed." }, { "name": "Slowed", @@ -47,6 +47,13 @@ "remove_msg": "Your fade into view once again.", "mon_apply_msg": " fades out of view!", "mon_remove_msg": " reappears!" + }, + { + "name": "Foresight", + "type": "good", + "apply_msg": "You suddenly feel like you can see what's just about to happen.", + "extend_msg": "You feel even more perceptive about the near future.", + "remove_msg": "You no longer feel as certain about what's about to happen." } ] \ No newline at end of file diff --git a/entity.py b/entity.py index 6b6a66b..bea8617 100644 --- a/entity.py +++ b/entity.py @@ -215,6 +215,9 @@ def has_clear_path(self, pos): def display_color(self): return 0 + def is_aware(self): + return True + def do_turn(self): pass @@ -259,6 +262,11 @@ def stealth_mod(self): def stealth_roll(self): return gauss_roll(self.stealth_mod()) + def get_perception(self): + per_mod = stat_mod(self.WIS) + perception = 10 + per_mod + return perception + def tick_status_effects(self, amount): for name in list(self.status.keys()): self.status[name] -= amount diff --git a/game_inst.py b/game_inst.py index 2aa7eef..4d32ed2 100644 --- a/game_inst.py +++ b/game_inst.py @@ -265,7 +265,8 @@ def place_items(self): [HealingPotion, 130], [EnlargementPotion, 20], [ShrinkingPotion, 20], - [SpeedPotion, 30] + [SpeedPotion, 30], + [ForesightPotion, 15] ] for _ in range(rng(1, 5)): @@ -323,11 +324,10 @@ def place_items(self): pos = board.random_passable() board.place_item_at(pos, Shield()) - if one_in(2): - for _ in range(triangular_roll(1, 9)): - if one_in(2): - pos = board.random_passable() - board.place_item_at(pos, Dart()) + num = rng(0, rng(0, 9)) + for _ in range(num): + pos = board.random_passable() + board.place_item_at(pos, Dart()) def place_monsters(self): eligible_types = {} @@ -486,7 +486,6 @@ def do_turn(self): player = self.get_player() used = player.energy_used - if used <= 0: return @@ -500,7 +499,9 @@ def do_turn(self): self.subtick_timer += used player.energy += used - for m in self.monsters: + + monsters = self.get_monsters() + for m in monsters: m.energy += used self.process_noise_events() @@ -509,12 +510,12 @@ def do_turn(self): self.subtick_timer -= 100 self.tick += 1 player.tick() - for m in self.monsters: + for m in monsters: if m.is_alive(): m.tick() - remaining = self.monsters.copy() + remaining = monsters.copy() random.shuffle(remaining) remaining.sort(key=lambda m: m.energy, reverse=True) diff --git a/inventory_ui.py b/inventory_ui.py index c63cb93..feca27e 100644 --- a/inventory_ui.py +++ b/inventory_ui.py @@ -7,6 +7,8 @@ def item_display_name(player, item): if item is player.armor: name += " (worn)" + elif item is player.weapon: + name += " (wielded)" return name def display_item(g, item): @@ -31,7 +33,8 @@ def display_item(g, item): menu.add_text("This is a finesse weapon.") if item.heavy: menu.add_text("This weapon is heavy; attacking with it takes a bit longer.") - + + use_text = "Use" can_throw = False @@ -40,6 +43,7 @@ def display_item(g, item): can_throw = item.thrown != False elif isinstance(item, ThrownItem): can_throw = True + elif isinstance(item, Armor): use_text = "Wear" diff --git a/items.py b/items.py index 9dcf49e..1a47a05 100644 --- a/items.py +++ b/items.py @@ -90,7 +90,7 @@ def use(self, player): return ItemUseResult.CONSUMED class SpeedPotion(Potion): - description = "A potion with a blue liquid that appears to have a slight glow. When consed, it grants a temporary speed boost." + description = "A potion with a blue liquid that appears to have a slight glow. When consumed, it grants a temporary speed boost." def __init__(self): super().__init__() @@ -129,6 +129,26 @@ def use(self, player): player.add_status("Invisible", dur) return ItemUseResult.CONSUMED +class ForesightPotion(Potion): + description = "A potion with a clear liquid that appears to glow cyan when agitated. Anyone who drinks it gains the ability to see a few seconds into the future for a short duration, allowing you to anticipate your enemies' actions." + + def __init__(self): + super().__init__() + self.name = "foresight potion" + + def display_color(self): + return curses.color_pair(COLOR_CYAN) + + def use(self, player): + player.add_msg("You drink the foresight potion.") + if player.has_status("Foresight"): + dur = rng(15, 40) + else: + dur = rng(30, 80) + player.use_energy(100) + player.add_status("Foresight", dur) + return ItemUseResult.CONSUMED + class Scroll(Item): description = "A scroll that contains magical writing on it." @@ -217,6 +237,7 @@ def __init__(self, name): self.damage = Dice(1, 1) self.finesse = False self.thrown = [6, 18] + self.destroy_chance = 6 def roll_damage(self): return self.damage.roll() @@ -233,6 +254,8 @@ def __init__(self): self.damage = Dice(1, 4) self.finesse = True self.symbol = ";" + self.destroy_chance = 3 + def display_color(self): return curses.color_pair(COLOR_DODGER_BLUE2) | curses.A_REVERSE diff --git a/json_obj.py b/json_obj.py index d317dd3..8fa6b54 100644 --- a/json_obj.py +++ b/json_obj.py @@ -135,7 +135,20 @@ def load(cls, d): obj.load_required(d, "potency", int) obj.load_optional(d, "slowing", False, bool) return obj - + +class AttackType(JSONObject): + + def load(cls, d): + obj = cls() + obj.load_required(d, "name", str) + obj.load_required(d, "attack_cost", 100) + + obj.load_optional(d, "use_dex", False, bool) + obj.load_optional(d, "reach", 1, int) + + dam = obj.get_required(d, "base_damage", str) + obj.set_field("base_damage", Dice(*parse_dice(dam))) + speed_names = ["tiny", "small", "medium", "large", "huge", "gargantuan"] diff --git a/monster.py b/monster.py index d7bcb4f..b1dded2 100644 --- a/monster.py +++ b/monster.py @@ -175,6 +175,9 @@ def on_hear_noise(self, noise): self.set_target(noise.pos) self.soundf = duration + def get_perception(self): + return super().get_perception() + self.get_skill("perception") + def check_alerted(self): if one_in(70): return True @@ -185,7 +188,7 @@ def check_alerted(self): dist = self.distance(player) per_mod = stat_mod(self.WIS) - perception = 10 + per_mod + perception = self.get_perception() range = self.type.blindsight_range if dist <= range: perception += 5 @@ -194,7 +197,6 @@ def check_alerted(self): #Player is invisible perception -= 5 - perception += self.get_skill("perception") stealth_roll = player.stealth_roll() @@ -420,9 +422,11 @@ def die(self): self.use_energy(1000) self.add_msg_if_u_see(self, f"{self.get_name(True)} dies!", "good") board.erase_collision_cache(self.pos) - if self.weapon and one_in(3): + if self.weapon and one_in(4): board.place_item_at(self.pos, self.weapon) - + if self.shield and one_in(4): + board.place_item_at(self.pos, Shield()) + def is_aware(self): return self.state in ["AWARE", "TRACKING"] @@ -561,6 +565,8 @@ def attack_pos(self, pos): if acid > 0: c.hit_with_acid(acid) elif u_see_attacker: + if c.is_player() and c.has_status("Foresight") and one_in(3): #No need to tell them every time + self.add_msg(f"You anticipate {self.get_name()}'s attack and instinctively avoid it!", "good") defender = c.get_name() if u_see_defender else "something" self.add_msg_if_u_see(self, f"{self.get_name(True)}'s attack misses {defender}.") @@ -596,6 +602,17 @@ def inflict_poison_to(self, c): c.add_status("Paralyzed", rng(1, 5)) paralyzed = True c.add_status("Slowed", rng(dmg, dmg*4), paralyzed) + + def add_status(self, name, dur, silent=False): + super().add_status(name, dur) + + g = self.g + if not silent: + eff_type = g.get_effect_type(name) + if self.has_status(name): + self.add_msg_if_u_see(self, eff_type.mon_extend_msg) + else: + self.add_msg_if_u_see(self, eff_type.mon_apply_msg) def random_guess_invis(self): g = self.g @@ -603,7 +620,7 @@ def random_guess_invis(self): chance = 1 target = None for pos in board.points_in_radius(self.pos, 3): - if self.sees_pos(pos) and one_in(chance): + if board.has_clear_path_to(self.pos, pos) and one_in(chance): chance += 1 target = pos if target: @@ -635,8 +652,7 @@ def on_hit(self, ent): if not self.is_ally(mon): continue - if not one_in(3): - self.set_state("AWARE") + self.set_state("AWARE") def move(self): g = self.g diff --git a/player.py b/player.py index 8d94017..9d4567f 100644 --- a/player.py +++ b/player.py @@ -1,7 +1,7 @@ from entity import Entity from utils import * from const import * -from items import UNARMED +from items import * from collections import deque import math @@ -49,17 +49,27 @@ def calc_evasion(self): if not self.is_alive(): bonus = 0 else: + foresight = self.has_status("Foresight") if self.has_status("Hasted"): bonus += 2 + + if foresight: + bonus += 2.5 if self.has_status("Enlarged"): bonus *= 0.7 elif self.has_status("Reduced"): bonus *= 1.3 bonus *= self.encumb_ev_mult() + if foresight: + #Split the bonus from foresight into half the bonus applied before armor encumbrance, and the other half applied after + bonus += 2.5 return bonus + 5 + def ranged_evasion(self): + return self.calc_evasion() + def add_to_inventory(self, item): self.inventory.append(item) @@ -112,32 +122,74 @@ def xp_to_next_level(self): amount = 100 * self.xp_level ** 1.5 return round(amount/10)*10 - def inc_random_stat(self): - rand = rng(1, 6) - - match rand: - case 1: + def inc_stat(self, name): + msg = "" + match name: + case "STR": self.STR += 1 msg = "You feel stronger." - case 2: + case "DEX": self.DEX += 1 msg = "You feel more agile." - case 3: + case "CON": self.CON += 1 self.recalc_max_hp() msg = "You feel your physical endurance improve." - case 4: + case "INT": self.INT += 1 msg = "You feel more intelligent." - case 5: + case "WIS": self.WIS += 1 msg = "You feel wiser." - case 6: + case "CHA": self.CHA += 1 msg = "You feel more charismatic." self.add_msg(msg, "good") + def get_stat_increase_candidates(self): + stats = [] + if self.STR < 20: + stats.append("STR") + if self.DEX < 20: + stats.append("DEX") + if self.CON < 20: + stats.append("CON") + if self.INT < 20: + stats.append("INT") + if self.WIS < 20: + stats.append("WIS") + if self.CHA < 20: + stats.append("CHA") + return stats + + + def inc_random_stat(self): + stats = self.get_stat_increase_candidates() + + if stats: + self.inc_stat(random.choice(stats)) + + def inc_stat_prompt(self): + stats = self.get_stat_increase_candidates() + + if stats: + g = self.g + if len(stats) == 1: + self.inc_stat(stats[0]) + else: + msg = "Increase which stat? " + msg += ", ".join(f"{i+1} - {stat}" for i, stat in enumerate(stats)) + msg += " (Enter the number corresponding to the stat)" + while True: + num = g.input_int(msg) + if 1 <= num <= len(stats): + self.inc_stat(stats[num-1]) + break + else: + self.add_msg(f"Please enter a number from 1 to {len(stats)}") + + def gain_xp(self, amount): self.xp += amount old_level = self.xp_level @@ -153,20 +205,31 @@ def gain_xp(self, amount): if old_level != self.xp_level: self.add_msg(f"You have reached experience level {self.xp_level}!", "good") - for _ in range(num*2): + for _ in range(num): self.inc_random_stat() + for _ in range(num): + self.inc_stat_prompt() def calc_fov(self): g = self.g board = g.get_board() self.fov = board.get_fov(self.pos) + def is_aware(self): + return not self.resting + def can_rest(self): if self.HP >= self.MAX_HP: return False + g = self.g num_visible = 0 + foresight = self.has_status("Foresight") for mon in self.visible_monsters(): - num_visible += 1 + perceived = True + if not foresight and self.is_resting: + perceived = one_in(2) and mon.stealth_roll() >= self.get_perception() + + num_visible += perceived if num_visible > 0: if num_visible == 1: @@ -194,7 +257,7 @@ def sees(self, other): def visible_monsters(self): g = self.g - for m in g.monsters: + for m in g.get_monsters(): if self.sees(m): yield m @@ -246,6 +309,8 @@ def calc_to_hit_bonus(self, mon, ranged=False): adv += 1 if not mon.sees(self): adv += 1 + if self.has_status("Foresight"): + adv += 1 if not self.sees(mon): mod -= 5 @@ -641,9 +706,6 @@ def stealth_mod(self): return stealth - def stealth_roll(self): - return gauss_roll(self.stealth_mod()) - def get_armor(self): if self.armor: armor = self.armor @@ -702,7 +764,13 @@ def throw_item(self, item): self.remove_from_inventory(item) self.shoot_projectile_at(mon.pos, proj) - board.place_item_at(proj.final_pos, item) + + destroy_chance = 10 if isinstance(item, Weapon) else 6 + if isinstance(item, ThrownItem): + destroy_chance = item.destroy_chance + + if not one_in(destroy_chance): + board.place_item_at(proj.final_pos, item) self.use_energy(100) return True \ No newline at end of file diff --git a/projectile.py b/projectile.py index 8472a4a..f228910 100644 --- a/projectile.py +++ b/projectile.py @@ -40,7 +40,7 @@ def on_hit(self, attacker, defender, margin): damage = defender.apply_armor(damage) msg = f"The {self.name} hits {defender.get_name()}" if attacker.is_player(): - msg += " for {damage} damage" + msg += f" for {damage} damage" msg += "." attacker.add_msg_if_u_see(defender, msg) if damage > 0 and crit: @@ -49,4 +49,4 @@ def on_hit(self, attacker, defender, margin): defender.take_damage(damage, attacker) if defender.is_alive() and defender.is_monster(): if attacker.is_player(): - attacker.alerted() \ No newline at end of file + defender.alerted() \ No newline at end of file