diff --git a/effects.json b/effects.json index 6f48fa4..944f482 100644 --- a/effects.json +++ b/effects.json @@ -33,6 +33,13 @@ "apply_msg": "You suddenly become unable to move!", "extend_msg": "You feel even more paralyzed.", "remove_msg": "You can finally move again." + }, + { + "name": "Invisible", + "type": "good", + "apply_msg": "You become invisible.", + "extend_msg": "You become even more invisible.", + "remove_msg": "Your fade into view once again." } ] \ No newline at end of file diff --git a/entity.py b/entity.py index b02579f..5f7a1dc 100644 --- a/entity.py +++ b/entity.py @@ -18,6 +18,19 @@ def __init__(self): self.poison = 0 self.status = {} + def roll_to_hit(self, other): + if x_in_y(MIN_HIT_MISS_PROB, 100): + return 1000 if one_in(2) else -1000 + + roll = gauss_roll(self.calc_to_hit_bonus(other)) + evasion = other.calc_evasion() + + return roll - evasion + + def get_all_status_effects(self): + for name in self.status: + yield name + def has_status(self, name): g = self.g g.check_effect_type(name) @@ -28,6 +41,12 @@ def add_status(self, name, dur): self.status[name] = 0 self.status[name] += dur*100 + def adjust_duration(self, name, amount): + if self.has_status(name): + self.status[name] += amount*100 + if self.status[name] <= 0: + self.remove_status(name) + def remove_status(self, name): if self.has_status(name): del self.status[name] @@ -35,6 +54,10 @@ def remove_status(self, name): def use_energy(self, amount): self.energy -= amount + def use_move_energy(self): + cost = div_rand(10000, self.get_speed()) + self.use_energy(cost) + def is_player(self): return False @@ -42,7 +65,7 @@ def is_monster(self): return False def is_invisible(self): - return False + return self.has_status("Invisible") def calc_evasion(self): dex = self.DEX @@ -71,6 +94,19 @@ def get_armor(self): @abstractmethod def get_name(self, capitalize=False): return "Unknown Entity" + + @abstractmethod + def calc_to_hit_bonus(self, other): + mod = 0 + if not self.sees(other): + mod -= 5 + if not other.sees(self): + mod += 5 + return mod + + @abstractmethod + def base_damage_roll(self): + return 1 def add_msg(self, text, typ="neutral"): g = self.g @@ -146,7 +182,7 @@ def sees_pos(self, pos): return board.has_line_of_sight(self.pos, pos) def sees(self, other): - if self is other: + if self is other: #We always see ourselves return True return self.sees_pos(other.pos) @@ -181,6 +217,9 @@ def hit_with_acid(self, strength, corr): if x_in_y(dam * corr, 2000): #TODO: Corrode armor pass - + + def stealth_mod(self): + return stat_mod(self.DEX) - \ No newline at end of file + def stealth_roll(self): + return gauss_roll(self.stealth_mod()) \ No newline at end of file diff --git a/game_inst.py b/game_inst.py index d845ece..9906081 100644 --- a/game_inst.py +++ b/game_inst.py @@ -102,7 +102,6 @@ def init_colors(self): def init_game(self): self.screen = curses.initscr() - self.init_colors() curses.noecho() @@ -120,6 +119,7 @@ def load_json_data(self): def next_level(self): player = self.get_player() + self.level += 1 if player.debug_wizard: @@ -154,15 +154,20 @@ def choose_mon_spawn_pos(self): return pos return None + + def create_weapon(self, id): + typ = self.get_weapon_type(id) + return Weapon.from_type(typ) def place_items(self): board = self.get_board() potions = [ + [InvisibilityPotion, 10_000_000_000], [HealingPotion, 120], - [EnlargementPotion, 25], - [ShrinkingPotion, 25], - [SpeedPotion, 35] + [EnlargementPotion, 20], + [ShrinkingPotion, 20], + [SpeedPotion, 30] ] for _ in range(rng(1, 5)): @@ -179,12 +184,10 @@ def place_items(self): ] for _ in range(rng(1, 4)): - if one_in(2): + if x_in_y(2, 5): pos = board.random_passable() name = random_weighted(weapons) - typ = self.get_weapon_type(name) - weapon = Weapon.from_type(typ) - board.place_item_at(pos, weapon) + board.place_item_at(pos, self.create_weapon(name)) def place_monsters(self): eligible_types = {} @@ -510,7 +513,18 @@ def draw_stats(self): for i, string in enumerate(strings2): self.draw_string(i + offset, width + 6, string) - + offset += len(strings2) + 1 + status = sorted(list(player.get_all_status_effects())) + if status: + for i, string in enumerate(status): + eff = self.get_effect_type(string) + color = curses.color_pair(MSG_TYPES[eff.type]) + + self.draw_string(i + offset, width + 6, string, color) + + if i >= 12: + break + def draw_monsters(self, offset_y): player = self.get_player() for m in player.visible_monsters(): @@ -531,7 +545,8 @@ def draw_messages(self, offset_y): messages = self.msg_log.get_messages(8) board = self.get_board() y = board.height + offset_y - rows, cols = screen.getmaxyx() + rows, _ = screen.getmaxyx() + cols = board.width + 4 groups = [] total_lines = 0 @@ -620,7 +635,10 @@ def process_input(self): code = self.getch() char = chr(code) - + if code == -1: + curses.flushinp() + return False + if char == "w": return player.move_dir(0, -1) if char == "s": @@ -698,7 +716,13 @@ def print_monster_info(self, monster): info.add_line("It hasn't noticed your presence yet.") if monster.has_flag("PACK_TRAVEL"): info.add_line("This creature travels in packs and takes advantage of its nearby allies to attack targets more easily.") - + blindsight = monster.type.blindsight_range + if blindsight > 0: + string = f"This creature has blindsight to a radius of {blindsight} tiles" + if not monster.can_see(): + string += " (blind beyond this range)" + string += "." + info.add_line(string) mon_speed = monster.get_speed() player_speed = player.get_speed() diff = mon_speed / player_speed @@ -712,7 +736,7 @@ def print_monster_info(self, monster): player_to_hit = gauss_roll_prob(player.calc_to_hit_bonus(monster), monster.calc_evasion()) - monster_to_hit = gauss_roll_prob(monster.get_to_hit_bonus(), player.calc_evasion()) + monster_to_hit = gauss_roll_prob(monster.calc_to_hit_bonus(player), player.calc_evasion()) player_to_hit = player_to_hit*(1-MIN_HIT_MISS_PROB/100) + MIN_HIT_MISS_PROB/2 monster_to_hit = monster_to_hit*(1-MIN_HIT_MISS_PROB/100) + MIN_HIT_MISS_PROB/2 diff --git a/items.py b/items.py index 139e9bf..ebe9327 100644 --- a/items.py +++ b/items.py @@ -55,7 +55,7 @@ def use(self, player): player.use_energy(50) dur = rng(150, 300) - player.remove_status("Reduced") + player.remove_status("Reduced", True) player.add_status("Enlarged", dur) player.use_energy(100) player.recalc_max_hp() @@ -79,7 +79,7 @@ def use(self, player): player.use_energy(50) dur = rng(150, 300) - player.remove_status("Enlarged") + player.remove_status("Enlarged", True) player.add_status("Reduced", dur) player.use_energy(100) player.recalc_max_hp() @@ -104,6 +104,26 @@ def use(self, player): player.use_energy(100) player.add_status("Hasted", dur) return True + +class InvisibilityPotion(Potion): + description = "A potion with a rather transparent liquid." + + def __init__(self): + super().__init__() + self.name = "invisibility potion" + + def display_color(self): + return COLOR_BLUE + + def use(self, player): + player.add_msg("You drink the invisibility potion.") + if player.has_status("Invisible"): + dur = rng(20, 40) + else: + dur = rng(60, 100) + player.use_energy(100) + player.add_status("Invisible", dur) + return True #TODO: Melee and ranged weapons/JSON for them diff --git a/json_obj.py b/json_obj.py index a9f32ec..908c213 100644 --- a/json_obj.py +++ b/json_obj.py @@ -164,6 +164,7 @@ def load(cls, d): obj.load_optional(d, "blindsight_range", 0, int) obj.load_optional(d, "poison", False, (bool, dict)) + obj.load_optional(d, "weapon", None) if obj.poison != False: if type(obj.poison) != dict: diff --git a/monster.py b/monster.py index adc2e09..583db3a 100644 --- a/monster.py +++ b/monster.py @@ -2,6 +2,7 @@ from player import Player from utils import * from const import * +from items import Weapon from pathfinding import find_path from collections import deque import random, curses @@ -22,6 +23,7 @@ def __init__(self): self.to_hit = 0 self.soundf = 0 self.damage = Dice(0,0,0) + self.weapon = None self.type = None def is_monster(self): @@ -55,6 +57,13 @@ def from_type(cls, typ): m.HP = m.MAX_HP = typ.HP m.to_hit = typ.to_hit m.damage = typ.base_damage + + g = m.g + + weap = typ.weapon + if weap: + weap = g.create_weapon(weap) + m.weapon = weap return m def has_flag(self, name): @@ -70,10 +79,6 @@ def base_speed(self): def get_diff_level(self): return self.type.diff - def use_move_energy(self): - cost = div_rand(10000, self.get_speed()) - self.use_energy(cost) - def get_name(self, capitalize=False): the = "The" if capitalize else "the" return the + " " + self.name @@ -85,21 +90,13 @@ def calc_path_to(self, pos): g = self.g board = g.get_board() - is_pack = self.has_flag("PACK_TRAVEL") - def passable_func(p): return p == pos or board.passable(p) def cost_func(p): cost = 1 - if (c := g.monster_at(p)): + if (c := g.monster_at(p)) and not self.will_attack(c): cost += 2 - if is_pack: - num = 0 - for m in g.monsters_in_radius(p, 1): - if self is not m and self.is_ally(m): - num += 1 - cost /= num + 1 return cost @@ -117,8 +114,10 @@ def path_towards(self, pos): return True if self.path: - if pos != self.path[-1] or not self.move_to(self.path.popleft()): - #Either target tile changed, or path is blocked; recalculate path + can_path = pos == self.path[-1] and self.distance(self.path[0]) <= 1 + + if not (can_path and self.move_to(self.path.popleft())): + #Either target tile changed, path is blocked, or we're off-course; recalculate path self.calc_path_to(pos) if self.path: if self.move_to(self.path.popleft()): @@ -135,7 +134,7 @@ def path_towards(self, pos): def base_pursue_duration(self): #How long to continue tracking after losing sight of the player - return 4 * self.INT + 8 + return 5 * self.INT + 10 def set_target(self, pos): if self.has_target(): @@ -187,6 +186,10 @@ def check_alerted(self): if dist <= range: perception += 5 + if not self.sees(player): + #Player is invisible + perception -= 5 + perception += self.get_skill("perception") stealth_roll = player.stealth_roll() @@ -211,7 +214,7 @@ def tick(self): self.poison -= amount if self.poison < 0: self.poison = 0 - elif x_in_y(self.CON, 160) and not self.has_flag("NO_REGEN"): + elif one_in(16) and not self.has_flag("NO_REGEN"): self.heal(1) def do_turn(self): @@ -225,6 +228,9 @@ def do_turn(self): self.energy = 0 def sees(self, other): + if self is other: + return True + if not super().sees(other): return False @@ -302,7 +308,20 @@ def move_towards(self, target): return self.move_dir(dx, 0) or (not switched and self.move_dir(0, dy)) else: return self.move_dir(0, dy) or (not switched and self.move_dir(dx, 0)) - + + def will_attack(self, c): + return c.is_player() + + def can_reach_attack(self, target): + #Is attacking our target viable from our current position? + + dist = self.distance(target) + if dist <= 1: + return True + + reach = self.reach_dist() + return dist <= reach and self.has_clear_path_to(target) + def move_to_target(self): if not self.has_target(): return @@ -323,8 +342,8 @@ def move_to_target(self): target = self.target_pos dist = self.distance(target) - if (c := g.entity_at(target)) and c.is_player() and dist <= self.reach_dist(): - if dist <= 1 or ((x_in_y(3, dist + 2) or one_in(3)) and self.has_clear_path_to(target)): + if (c := g.entity_at(target)) and self.will_attack(c): + if self.can_reach_attack(target) and ((x_in_y(3, dist + 2) or one_in(3))): if self.attack_pos(target): return @@ -379,6 +398,8 @@ 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: + board.place_item_at(self.pos, self.weapon) def is_aware(self): return self.state in ["AWARE", "TRACKING"] @@ -392,8 +413,8 @@ def alerted(self): for mon in g.monsters_in_radius(self.pos, 6): if self.is_ally(mon): mon.set_state("AWARE") - if self.state == "IDLE": - self.set_state("AWARE") + mon.target_entity(player) + self.set_state("AWARE") self.target_entity(player) @@ -403,7 +424,16 @@ def move_dir(self, dx, dy): return True return False - def get_to_hit_bonus(self): + def base_damage_dice(self): + damage = self.damage + if self.weapon: + damage = self.weapon.damage + return damage + + def base_damage_roll(self): + return self.base_damage_dice().roll() + + def calc_to_hit_bonus(self, c): g = self.g board = g.get_board() @@ -430,8 +460,8 @@ def get_to_hit_bonus(self): if allies > 0: #Pack tactics gives a bonus to-hit if there are allies nearby bonus = 2.5*allies mod += bonus - - return mod + + return mod + super().calc_to_hit_bonus(c) def calc_evasion(self): ev = super().calc_evasion() @@ -448,40 +478,48 @@ def calc_evasion(self): def get_armor(self): return self.type.armor + + def get_hit_msg(self, c): + g = self.g + player = g.get_player() + + u_see_attacker = player.sees(self) + u_see_defender = player.sees(c) + + monster_name = self.get_name() if u_see_attacker else "something" + target_name = c.get_name() if u_see_defender else "something" + + msg = self.type.attack_msg + msg = msg.replace("", monster_name) + msg = msg.replace("", target_name) + + if msg.startswith(monster_name): + msg = msg.capitalize() + + return msg + "." def attack_pos(self, pos): g = self.g board = g.get_board() + player = g.get_player() if not (c := g.entity_at(pos)): return False - assert c.is_player() #TODO: Remove when it's possible for monsters to attack other monsters + att_roll = self.roll_to_hit(c) + u_see_attacker = player.sees(self) + u_see_defender = player.sees(c) + print_msg = u_see_attacker or u_see_defender - mod = self.get_to_hit_bonus() - - roll = gauss_roll(mod) - margin = roll - c.calc_evasion() - - if x_in_y(MIN_HIT_MISS_PROB, 100): - margin = 1000 if one_in(2) else -1000 - - if margin >= 0: - damage = self.damage.roll() + if att_roll >= 0: + damage = self.base_damage_roll() stat = self.DEX if self.type.use_dex_melee else self.STR damage += div_rand(stat - 10, 2) damage = max(damage, 1) msg_type = "bad" if c.is_player() else "neutral" - monster_name = self.get_name() - target_name = c.get_name() - msg = self.type.attack_msg - msg = msg.replace("", monster_name) - msg = msg.replace("", target_name) - - if msg.startswith(monster_name): - msg = msg.capitalize() - - self.add_msg_if_u_see(self, f"{msg}.", msg_type) + if print_msg: + self.add_msg_if_u_see(self, self.get_hit_msg(c), msg_type) + c.take_damage(damage) poison_typ = self.type.poison if poison_typ: @@ -495,7 +533,7 @@ def attack_pos(self, pos): typ = "bad" if c.is_player() else "neutral" dmg = rng(0, amount) if dmg > 0: - c.add_msg_u_or_mons("You are poisoned!", f"{self.get_name(True)} appears to be poisoned.", typ) + c.add_msg_u_or_mons("You are poisoned!", f"{self.get_name(True)} is poisoned!", typ) c.poison += dmg if poison_typ.slowing and x_in_y(dmg, dmg+3): paralyzed = False @@ -505,12 +543,33 @@ def attack_pos(self, pos): c.add_status("Slowed", rng(dmg, dmg*4), paralyzed) - else: + elif print_msg: self.add_msg_if_u_see(self, f"{self.get_name(True)}'s attack misses {c.get_name()}.") self.use_energy(100) return True + def random_guess_invis(self): + g = self.g + board = g.get_board() + possibilities = [] + for pos in board.points_in_radius(self.pos, 3): + if self.sees_pos(pos): + possibilities.append(pos) + if possibilities: + p = random.choice(possibilities) + self.set_target(p) + + def determine_invis(self, c): + g = self.g + player = g.get_player() + + dist = self.distance(c) + if dist <= 1 and one_in(4): + return True + + return self.perception_roll() >= player.stealth_roll() + def perception_roll(self): return self.roll_wisdom() + self.get_skill("perception") @@ -526,7 +585,18 @@ def move(self): if self.sees(player): self.target_entity(player) if self.id == "bat" and one_in(5): - self.set_rand_target() + self.set_rand_target() + elif self.sees_pos(player.pos): + #Target is in LOS, but invisible + reached_target = self.target_pos == self.pos + perceived_invis = self.determine_invis(player) + + if self.target_pos != player.pos and reached_target: + if perceived_invis: + self.target_entity(player) + else: + self.random_guess_invis() + else: self.set_state("TRACKING") self.target_entity(player) diff --git a/monsters.json b/monsters.json index ec69dbd..a3720d2 100644 --- a/monsters.json +++ b/monsters.json @@ -98,9 +98,10 @@ "use_dex_melee": true, "size": "small", "speed": 100, - "base_damage": "1d4", + "weapon": "dagger", "attack_msg": " hits with a dagger", - "flags": [ "SEES", "PACK_TRAVEL" ] + "flags": [ "SEES", "PACK_TRAVEL" ], + "weapon": "dagger" }, { "id": "giant_rat", diff --git a/player.py b/player.py index 461bf9b..9335721 100644 --- a/player.py +++ b/player.py @@ -58,6 +58,31 @@ 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: + self.STR += 1 + msg = "You feel stronger." + case 2: + self.DEX += 1 + msg = "You feel more agile." + case 3: + self.CON += 1 + self.recalc_max_hp() + msg = "You feel your physical endurance improve." + case 4: + self.INT += 1 + msg = "You feel more intelligent." + case 5: + self.WIS += 1 + msg = "You feel wiser." + case 6: + self.CHA += 1 + msg = "You feel more charismatic." + + self.add_msg(msg, "good") + def gain_xp(self, amount): self.xp += amount old_level = self.xp_level @@ -70,34 +95,11 @@ def gain_xp(self, amount): if self.xp_level % 3 == 0: num += 1 - if old_level != self.xp_level: self.add_msg(f"You have reached experience level {self.xp_level}!", "good") for _ in range(num*2): - rand = rng(1, 6) - match rand: - case 1: - self.STR += 1 - msg = "You feel stronger." - case 2: - self.DEX += 1 - msg = "You feel more agile." - case 3: - self.CON += 1 - self.recalc_max_hp() - msg = "You feel your physical endurance improve." - case 4: - self.INT += 1 - msg = "You feel more intelligent." - case 5: - self.WIS += 1 - msg = "You feel wiser." - case 6: - self.CHA += 1 - msg = "You feel more charismatic." - - self.add_msg(msg, "good") + self.inc_random_stat() def calc_fov(self): g = self.g @@ -108,8 +110,10 @@ def sees_pos(self, other): return other in self.fov def sees(self, other): - if self is other: + if self is other: #We always see ourselves return True + if other.is_invisible(): + return False return self.sees_pos(other.pos) @@ -141,14 +145,24 @@ def calc_to_hit_bonus(self, mon): stat_bonus = stat_mod(stat) mod = level_bonus + stat_bonus - if not mon.is_aware(): - mod += 5 + adv = 0 + + if not mon.is_aware(): + adv += 1 + if not mon.sees(self): + adv += 1 + + if not self.sees(mon): + mod -= 5 + if self.has_status("Reduced"): mod += 1.5 if self.is_unarmed(): mod += 1 + + mod += 5 * math.sqrt(adv) return mod def regen_rate(self): @@ -226,14 +240,15 @@ def on_move(self, oldpos): if not mon.is_aware(): continue reach = mon.reach_dist() - if reach > 1 and not mon.has_clear_path_to(self.pos): + if not mon.can_reach_attack(self.pos): continue if old_dist <= reach and self.distance(mon) > reach and mon.sees(self): player_roll = triangular_roll(0, self.get_speed()) monster_roll = triangular_roll(0, mon.get_speed()) if monster_roll >= player_roll and one_in(2): - self.add_msg(f"{mon.get_name(True)} makes an opportunity attack as you move away!", "warning") + if player.sees(self): + self.add_msg(f"{mon.get_name(True)} makes an attack as you move away!", "warning") oldenergy = mon.energy mon.attack_pos(self.pos) mon.energy = oldenergy @@ -293,27 +308,24 @@ def attack_pos(self, pos): return True sneak_attack = False - mod = self.calc_to_hit_bonus(mon) + finesse = self.weapon.finesse + if not mon.is_aware(): - if x_in_y(self.DEX, 60): + finesse_bonus = 5 if finesse else 0 + if x_in_y(self.DEX + finesse_bonus, 70): sneak_attack = True - roll = gauss_roll(mod) - evasion = mon.calc_evasion() - margin = roll - evasion + att_roll = self.roll_to_hit(mon) - if x_in_y(MIN_HIT_MISS_PROB, 100): - margin = 1000 if one_in(2) else -1000 - - if margin >= 0: + if att_roll >= 0: damage = self.base_damage_roll() + div_rand(self.STR - 10, 2) crit = False - if margin >= 5: + if att_roll >= 5: crit = one_in(10) if sneak_attack: eff_level = self.xp_level - if self.weapon.finesse: + if finesse: eff_level = mult_rand_frac(eff_level, 4, 3) eff_level += rng(0, 3) msg = [ @@ -342,14 +354,17 @@ def attack_pos(self, pos): self.combat_noise(damage, sneak_attack) - self.add_msg(f"You hit {mon.get_name()} for {damage} damage.") - if crit: - self.add_msg("Critical hit!", "good") - mon.take_damage(damage) - if mon.is_alive(): - self.add_msg(f"It has {mon.HP}/{mon.MAX_HP} HP.") + if damage <= 0: + self.add_msg(f"You hit {mon.get_name()} but deal no damage.") else: - self.on_defeat_monster(mon) + self.add_msg(f"You hit {mon.get_name()} for {damage} damage.") + if crit: + self.add_msg("Critical hit!", "good") + mon.take_damage(damage) + if mon.is_alive(): + self.add_msg(f"It has {mon.HP}/{mon.MAX_HP} HP.") + else: + self.on_defeat_monster(mon) else: self.add_msg(f"Your attack misses {mon.get_name()}.") @@ -357,6 +372,7 @@ def attack_pos(self, pos): if self.has_status("Hasted"): attack_cost = 75 self.use_energy(attack_cost) + self.adjust_duration("Invisible", -rng(0, 10)) mon.alerted() self.maybe_alert_monsters(15) @@ -370,19 +386,22 @@ def on_defeat_monster(self, mon): if len(g.get_monsters()) <= 0: g.place_stairs() - self.add_msg("The stairs proceeding downward to the next level begin to open up...") + self.add_msg("The stairs proceeding downward begin to open up...") if g.level == 1: self.add_msg("You have completed the first level! Move onto the '>', then press the '>' key to go downstairs.", "info") def move_dir(self, dx, dy): + g = self.g + oldpos = self.pos.copy() + pos = self.pos + target = Point(pos.x + dx, pos.y + dy) + if super().move_dir(dx, dy): - self.use_energy(div_rand(10000, self.get_speed())) + self.use_move_energy() self.on_move(oldpos) return True - g = self.g - pos = self.pos - target = Point(pos.x + dx, pos.y + dy) + if g.monster_at(target): return self.attack_pos(target) @@ -413,6 +432,25 @@ def tick_poison(self, subt): if dmg > 0 and (one_in(4) or x_in_y(self.poison, self.MAX_HP)): self.add_msg("You feel sick due to the poison in your system.", "bad") + def remove_status(self, name, silent=False): + g = self.g + + super().remove_status(name) + + if not silent: + eff = g.get_effect_type(name) + typ = eff.type + msg_type = "neutral" + if typ in ["good", "info"]: + msg_type = "info" + elif typ == "bad": + msg_type = "good" + + self.add_msg(eff.remove_msg, msg_type) + + if name == "Enlarged" or name == "Reduced": + self.recalc_max_hp() + def do_turn(self, used): g = self.g @@ -429,28 +467,13 @@ def do_turn(self, used): self.status[name] -= used if self.status[name] <= 0: self.remove_status(name) - - eff = g.get_effect_type(name) - typ = eff.type - msg_type = "neutral" - if typ in ["good", "info"]: - msg_type = "info" - elif typ == "bad": - msg_type = "good" - - self.add_msg(eff.remove_msg, msg_type) - - if name == "Enlarged" or name == "Reduced": - self.recalc_max_hp() for mon in self.visible_monsters(): - name = mon.type.name if mon.check_alerted(): mon.alerted() - mon.target_entity(self) def stealth_mod(self): - stealth = stat_mod(self.DEX) + stealth = super().stealth_mod() if self.has_status("Reduced"): stealth += 3 elif self.has_status("Enlarged"):