<commit_before>import {Game} from "../models/game";
import {GameObject} from "../models/game-object";
import {Artifact} from "../models/artifact";
import {Room} from "../models/room";
import {RoomExit} from "../models/room";

/**
 * Monster class. Represents all properties of a single monster
 */
export class Monster extends GameObject {

  // constants
  static PLAYER: number = 0;
  static FRIEND_ALWAYS: string = "friend";
  static FRIEND_NEUTRAL: string = "neutral";
  static FRIEND_NEVER: string = "hostile";
  static FRIEND_RANDOM: string = "random";
  // reaction to player
  static RX_UNKNOWN: string = "unknown";
  static RX_FRIEND: string = "friend";
  static RX_NEUTRAL: string = "neutral";
  static RX_HOSTILE: string = "hostile";
  // status
  static STATUS_ALIVE: number = 1;
  static STATUS_DEAD: number = 2;
  // combat codes
  static COMBAT_CODE_SPECIAL: number = 1;  // uses generic combat verbs like "attacks"
  static COMBAT_CODE_NORMAL: number = 0;  // uses a weapon, or natural weapons if defined in database
  static COMBAT_CODE_WEAPON_IF_AVAILABLE: number = -1;  // uses a weapon if there is one available; otherwise natural
  static COMBAT_CODE_NEVER_FIGHT: number = -2;
  // attack verbs (indexed by weapon type, first index (0) is for natural weapons)
  static COMBAT_VERBS_ATTACK = [
    ['lunges', 'tears', 'claws'],
    ['swings', 'chops', 'swings'],
    ['shoots'],
    ['swings'],
    ['stabs', 'lunges', 'jabs'],
    ['swings', 'chops', 'stabs'],
  ];
  // miss verbs (indexed by weapon type, first index (0) is for natural weapons)
  static COMBAT_VERBS_MISS = [
    ['missed', 'missed'],
    ['dodged', 'missed'],
    ['missed', 'missed'],
    ['dodged', 'missed'],
    ['dodged', 'missed'],
    ['parried', 'missed'],
  ];

  // data properties for all monsters
  // don't use default values here because they won't be overwritten when loading the data object.
  room_id: number;
  gender: string;
  hardiness: number;
  agility: number;
  count: number;
  friendliness: string;
  friend_odds: number;
  combat_code: number;
  combat_verbs: string[] = [];  // custom messages that replace the normal attack messages
  health_messages: string[] = [];  // custom messages that replace the normal health status messages
  courage: number;
  gold: number;
  weapon_id: number;
  weapon_dice: number;
  weapon_sides: number;
  armor_class: number;

  // data properties for player only
  charisma: number;
  spell_abilities: any;
  spell_abilities_original: any;
  weapon_abilities: { [key: number]: number; };
  armor_expertise: number;

  // game-state properties
  seen: boolean = false;
  reaction: string = Monster.RX_UNKNOWN;
  status: number = Monster.STATUS_ALIVE;
  original_group_size: number;
  damage: number = 0;
  weight_carried: number = 0;
  armor_worn: Artifact[];
  weapon: Artifact;
  inventory: Artifact[];
  speed_time: number = 0; // time remaining on speed spell
  speed_multiplier: number = 1; // multiplier for to hit: 2 when speed spell is active; 1 otherwise
  dead_body_id: number; // the ID of the auto-generated dead body artifact for non-player monsters
  profit: number = 0; // the money the player makes for selling items when they leave the adventure
  group_monster_index: number = 0;  // for combat logic involving group monsters, which group member is active
  weapons: Artifact[]; // list of weapons, used when selling at game exit

  /**
   * Moves the monster to a specific room.
   */
  public moveToRoom(room_id: number, monsters_follow: boolean = true): void {
    this.room_id = room_id;

    // when the player moves, set the current room reference
    if (this.id === Monster.PLAYER) {
      let game = Game.getInstance();
      game.rooms.current_room = game.rooms.getRoomById(room_id);

      // check if monsters should move
      if (monsters_follow) {
        for (let m of game.monsters.visible) {
          if (m.reaction === Monster.RX_UNKNOWN) {
            m.checkReaction();
          }
          // friends always move
          if (m.reaction === Monster.RX_FRIEND && m.id !== Monster.PLAYER) {
            m.moveToRoom(room_id);
          }
          // enemies move based on courage check (this is used when the player flees)
          else if (m.reaction === Monster.RX_HOSTILE && m.checkCourage()) {
            m.moveToRoom(room_id);
          }
        }
      }
    }
  }

  /**
   * Monster flees out a random exit
   */
  public chooseRandomExit(): Room {
    let game = Game.getInstance();

    // choose a random exit
    // exclude any locked/hidden exits and the game exit
    let good_exits: RoomExit[] = game.rooms.current_room.exits.filter(x => x.room_to !== RoomExit.EXIT && x.isOpen());

    if (good_exits.length === 0) {
      return null;
    } else {
      let random_exit = good_exits[Game.getInstance().diceRoll(1, good_exits.length) - 1];
      return game.rooms.getRoomById(random_exit.room_to);
    }
  }

  /**
   * Checks the monster's reaction to the player
   */
  public checkReaction(): void {
    switch (this.friendliness) {
      case Monster.FRIEND_ALWAYS:
        this.reaction = Monster.RX_FRIEND;
        break;
      case Monster.FRIEND_NEUTRAL:
        this.reaction = Monster.RX_NEUTRAL;
        break;
      case Monster.FRIEND_NEVER:
        this.reaction = Monster.RX_HOSTILE;
        break;
      case Monster.FRIEND_RANDOM:
        // calculate reaction based on random odds

        this.reaction = Monster.RX_FRIEND;
        let friend_odds = this.friend_odds + ((Game.getInstance().player.charisma - 10) * 2);
        // first roll determines a neutral vs. friendly monster
        let roll1 = Game.getInstance().diceRoll(1, 100);
        if (roll1 > friend_odds) {
          this.reaction = Monster.RX_NEUTRAL;
          // second roll determines a hostile vs. neutral monster
          let roll2 = Game.getInstance().diceRoll(1, 100);
          if (roll2 > friend_odds) {
            this.reaction = Monster.RX_HOSTILE;
          }
        }
        break;
    }
  }

  /**
   * Executes a courage check on this monster. Typically used to determine
   * if a monster should flee combat, or follow the player when he/she flees.
   * @returns {boolean}
   */
  public checkCourage(): boolean {
    let fear = Game.getInstance().diceRoll(1, 100);
    let effective_courage = this.courage;
    if (this.damage > this.hardiness * 0.2 || this.count < this.original_group_size) {
      // wounded, or members of a group have been killed
      effective_courage *= 0.75;
    } else if (this.damage > this.hardiness * 0.6) {
      // badly wounded
      effective_courage *= 0.5;
    }
    return effective_courage >= fear;
  }

  /**
   * Recheck monster's reaction when the player attacks it or otherwise
   * does something nasty.
   */
  public hurtFeelings(): void {

    // this logic is only meaningful for neutral and friendly monsters
    if (this.reaction !== Monster.RX_HOSTILE) {

      // clear the automatic reactions and set a default percentage
      switch (this.friendliness) {
        case Monster.FRIEND_ALWAYS:
          this.friend_odds = 100;
          break;
        case Monster.FRIEND_NEUTRAL:
          this.friend_odds = 50;
          break;<commit_msg>Give player a minimum 15% chance to flee from hostile monsters<commit_after>import {Game} from "../models/game";
import {GameObject} from "../models/game-object";
import {Artifact} from "../models/artifact";
import {Room} from "../models/room";
import {RoomExit} from "../models/room";

/**
 * Monster class. Represents all properties of a single monster
 */
export class Monster extends GameObject {

  // constants
  static PLAYER: number = 0;
  static FRIEND_ALWAYS: string = "friend";
  static FRIEND_NEUTRAL: string = "neutral";
  static FRIEND_NEVER: string = "hostile";
  static FRIEND_RANDOM: string = "random";
  // reaction to player
  static RX_UNKNOWN: string = "unknown";
  static RX_FRIEND: string = "friend";
  static RX_NEUTRAL: string = "neutral";
  static RX_HOSTILE: string = "hostile";
  // status
  static STATUS_ALIVE: number = 1;
  static STATUS_DEAD: number = 2;
  // combat codes
  static COMBAT_CODE_SPECIAL: number = 1;  // uses generic combat verbs like "attacks"
  static COMBAT_CODE_NORMAL: number = 0;  // uses a weapon, or natural weapons if defined in database
  static COMBAT_CODE_WEAPON_IF_AVAILABLE: number = -1;  // uses a weapon if there is one available; otherwise natural
  static COMBAT_CODE_NEVER_FIGHT: number = -2;
  // attack verbs (indexed by weapon type, first index (0) is for natural weapons)
  static COMBAT_VERBS_ATTACK = [
    ['lunges', 'tears', 'claws'],
    ['swings', 'chops', 'swings'],
    ['shoots'],
    ['swings'],
    ['stabs', 'lunges', 'jabs'],
    ['swings', 'chops', 'stabs'],
  ];
  // miss verbs (indexed by weapon type, first index (0) is for natural weapons)
  static COMBAT_VERBS_MISS = [
    ['missed', 'missed'],
    ['dodged', 'missed'],
    ['missed', 'missed'],
    ['dodged', 'missed'],
    ['dodged', 'missed'],
    ['parried', 'missed'],
  ];

  // data properties for all monsters
  // don't use default values here because they won't be overwritten when loading the data object.
  room_id: number;
  gender: string;
  hardiness: number;
  agility: number;
  count: number;
  friendliness: string;
  friend_odds: number;
  combat_code: number;
  combat_verbs: string[] = [];  // custom messages that replace the normal attack messages
  health_messages: string[] = [];  // custom messages that replace the normal health status messages
  courage: number;
  gold: number;
  weapon_id: number;
  weapon_dice: number;
  weapon_sides: number;
  armor_class: number;

  // data properties for player only
  charisma: number;
  spell_abilities: any;
  spell_abilities_original: any;
  weapon_abilities: { [key: number]: number; };
  armor_expertise: number;

  // game-state properties
  seen: boolean = false;
  reaction: string = Monster.RX_UNKNOWN;
  status: number = Monster.STATUS_ALIVE;
  original_group_size: number;
  damage: number = 0;
  weight_carried: number = 0;
  armor_worn: Artifact[];
  weapon: Artifact;
  inventory: Artifact[];
  speed_time: number = 0; // time remaining on speed spell
  speed_multiplier: number = 1; // multiplier for to hit: 2 when speed spell is active; 1 otherwise
  dead_body_id: number; // the ID of the auto-generated dead body artifact for non-player monsters
  profit: number = 0; // the money the player makes for selling items when they leave the adventure
  group_monster_index: number = 0;  // for combat logic involving group monsters, which group member is active
  weapons: Artifact[]; // list of weapons, used when selling at game exit

  /**
   * Moves the monster to a specific room.
   */
  public moveToRoom(room_id: number, monsters_follow: boolean = true): void {
    this.room_id = room_id;

    // when the player moves, set the current room reference
    if (this.id === Monster.PLAYER) {
      let game = Game.getInstance();
      game.rooms.current_room = game.rooms.getRoomById(room_id);

      // check if monsters should move
      if (monsters_follow) {
        for (let m of game.monsters.visible) {
          if (m.reaction === Monster.RX_UNKNOWN) {
            m.checkReaction();
          }
          // friends always move
          if (m.reaction === Monster.RX_FRIEND && m.id !== Monster.PLAYER) {
            m.moveToRoom(room_id);
          }
          // enemies move based on courage check (this is used when the player flees)
          else if (m.reaction === Monster.RX_HOSTILE && m.checkCourage(true)) {
            m.moveToRoom(room_id);
          }
        }
      }
    }
  }

  /**
   * Monster flees out a random exit
   */
  public chooseRandomExit(): Room {
    let game = Game.getInstance();

    // choose a random exit
    // exclude any locked/hidden exits and the game exit
    let good_exits: RoomExit[] = game.rooms.current_room.exits.filter(x => x.room_to !== RoomExit.EXIT && x.isOpen());

    if (good_exits.length === 0) {
      return null;
    } else {
      let random_exit = good_exits[Game.getInstance().diceRoll(1, good_exits.length) - 1];
      return game.rooms.getRoomById(random_exit.room_to);
    }
  }

  /**
   * Checks the monster's reaction to the player
   */
  public checkReaction(): void {
    switch (this.friendliness) {
      case Monster.FRIEND_ALWAYS:
        this.reaction = Monster.RX_FRIEND;
        break;
      case Monster.FRIEND_NEUTRAL:
        this.reaction = Monster.RX_NEUTRAL;
        break;
      case Monster.FRIEND_NEVER:
        this.reaction = Monster.RX_HOSTILE;
        break;
      case Monster.FRIEND_RANDOM:
        // calculate reaction based on random odds

        this.reaction = Monster.RX_FRIEND;
        let friend_odds = this.friend_odds + ((Game.getInstance().player.charisma - 10) * 2);
        // first roll determines a neutral vs. friendly monster
        let roll1 = Game.getInstance().diceRoll(1, 100);
        if (roll1 > friend_odds) {
          this.reaction = Monster.RX_NEUTRAL;
          // second roll determines a hostile vs. neutral monster
          let roll2 = Game.getInstance().diceRoll(1, 100);
          if (roll2 > friend_odds) {
            this.reaction = Monster.RX_HOSTILE;
          }
        }
        break;
    }
  }

  /**
   * Executes a courage check on this monster. Typically used to determine
   * if a monster should flee combat, or follow the player when he/she flees.
   * @param {following} boolean
   *   Whether the monster is fleeing (false) or deciding to follow a fleeing player (true)
   * @returns {boolean}
   */
  public checkCourage(following: boolean = false): boolean {
    let fear = Game.getInstance().diceRoll(1, 100);
    let effective_courage = this.courage;
    if (this.damage > this.hardiness * 0.2 || this.count < this.original_group_size) {
      // wounded, or members of a group have been killed
      effective_courage *= 0.75;
    } else if (this.damage > this.hardiness * 0.6) {
      // badly wounded
      effective_courage *= 0.5;
    }
    // player always has a 15% chance to get away when fleeing
    // (new rule - not in EDX. it's really annoying otherwise.)
    if (following) {
      effective_courage = Math.min(effective_courage, 85);
    }
    return effective_courage >= fear;
  }

  /**
   * Recheck monster's reaction when the player attacks it or otherwise
   * does something nasty.
   */
  public hurtFeelings(): void {

    // this logic is only meaningful for neutral and friendly monsters
    if (this.reaction !== Monster.RX_HOSTILE) {

      // clear the automatic reactions and set a default percentage
      switch (this.friendliness) {
        case Monster.FRIEND_ALWAYS:
          this.friend_odds = 100;
          break;
        case Monster.FRIEND_NEUTRAL:
          this.friend_odds = 50;
          break;
{'fraction_non_alphanumeric': 0.11063973063973064, 'fraction_numerical': 0.007542087542087542, 'mean_word_length': 2.833505420753743, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7124209', 'n_tokens_mistral': 4746, 'n_tokens_neox': 4498, 'n_words': 1423}