bcdice/BCDice

View on GitHub
lib/bcdice/game_system/OracleEngine.rb

Summary

Maintainability
A
2 hrs
Test Coverage
A
93%
# frozen_string_literal: true

require 'bcdice/format'

module BCDice
  module GameSystem
    class OracleEngine < Base
      # ゲームシステムの識別子
      ID = 'OracleEngine'

      # ゲームシステム名
      NAME = 'オラクルエンジン'

      # ゲームシステム名の読みがな
      SORT_KEY = 'おらくるえんしん'

      # ダイスボットの使い方
      HELP_MESSAGE = <<MESSAGETEXT
  ・クラッチロール (xCL+y>=z)
  ダイスをx個振り、1個以上目標シフトzに到達したか判定します。修正yは全てのダイスにかかります。
  成功した時は目標シフトを、失敗した時はダイスの最大値-1シフトを返します
  zが指定されないときは、ダイスをx個を振り、それに修正yしたものを返します。
  通常、最低シフトは1、最大シフトは6です。目標シフトもそろえられます。
  また、CLの後に7を入れ、(xCL7+y>=z)と入力すると最大シフトが7になります。
 ・判定 (xR6+y@c#f$b>=z)
  ダイスをx個振り、大きいもの2つだけを見て達成値を算出し、成否を判定します。修正yは達成値にかかります。
  ダイスブレイクとしてbを、クリティカル値としてcを、ファンブル値としてfを指定できます。
  それぞれ指定されない時、0,12,2になります。
  クリティカル値の上限はなし、下限は2。ファンブル値の上限は12、下限は0。
  zが指定されないとき、達成値の算出のみ行います。
 ・ダメージロールのダイスブレイク (xD6+y$b)
  ダイスをx個振り、合計値を出します。修正yは合計値にかかります。
  ダイスブレイクとしてbを指定します。合計値は0未満になりません。
MESSAGETEXT

      # ダイスボットで使用するコマンドを配列で列挙する
      register_prefix('\d+CL', '\d+R6', '\d+D6.*\$[\+\-]?\d+')

      def initialize(command)
        super(command)
        @sort_add_dice = true
        @sort_barabara_dice = true
      end

      def eval_game_system_specific_command(command)
        case command
        when /\d+CL.*/i
          clutch_roll(command)
        when /\d+D6.*\$[+-]?\d.*/
          damage_roll(command)
        when /\d+R6/
          r_roll(command)
        end
      end

      # クラッチロール
      def clutch_roll(string)
        debug("clutch_roll begin", string)

        parser = Command::Parser.new(/\d+CL[67]?/, round_type: round_type)
                                .restrict_cmp_op_to(nil, :>=)

        @cmd = parser.parse(string)

        unless @cmd
          return nil
        end

        @times, @max_shift = @cmd.command.split("CL").map(&:to_i)
        @max_shift ||= 6
        @cmd.target_number = clamp(@cmd.target_number, 1, @max_shift) if @cmd.cmp_op

        if @times == 0
          return nil
        end

        dice_list = @randomizer.roll_barabara(@times, 6).map { |x| clamp(x + @cmd.modify_number, 1, @max_shift) }.sort

        sequence = [
          expr_clutch(),
          "[#{dice_list.join(', ')}]",
          result_clutch(dice_list.last)
        ]

        return sequence.join(' > ')
      end

      def expr_clutch()
        max_shift = @max_shift == 7 ? 7 : nil
        cmp_op = Format.comparison_operator(@cmd.cmp_op)
        modify_number = Format.modifier(@cmd.modify_number)

        "(#{@times}CL#{max_shift}#{modify_number}#{cmp_op}#{@cmd.target_number})"
      end

      def result_clutch(after_shift)
        if @cmd.cmp_op != :>=
          "シフト#{after_shift}"
        elsif after_shift >= @cmd.target_number
          "成功 シフト#{@cmd.target_number}"
        else
          after_shift -= 1
          after_shift = 1 if after_shift < 1
          "失敗 シフト#{after_shift}"
        end
      end

      def clamp(i, min, max)
        if i < min
          min
        elsif i > max
          max
        else
          i
        end
      end

      # 判定
      def r_roll(string)
        parser = Command::Parser.new(/\d+R6/, round_type: round_type)
                                .restrict_cmp_op_to(nil, :>=)
                                .enable_critical
                                .enable_fumble
                                .enable_dollar
        @cmd = parser.parse(string)
        unless @cmd
          return nil
        end

        @times = @cmd.command.to_i

        if @times == 0
          return nil
        end

        @critical = normalize_critical(@cmd.critical || 12, string)
        @fumble = normalize_fumble(@cmd.fumble || 2, string)
        @break = (@cmd.dollar || 0).abs

        dice_list = @randomizer.roll_barabara(@times, 6).sort
        dice_broken = dice_list.pop(@break)

        # ブレイク後のダイスから最大値2つの合計がダイスの値
        dice_total = dice_list.dup.pop(2).inject(0, :+)
        total = dice_total + @cmd.modify_number

        sequence = [
          expr_r(),
          dice_result_r(dice_total, dice_list, dice_broken),
          result_r(dice_total, total)
        ]

        return sequence.join(' > ')
      end

      def expr_r()
        modify_number = Format.modifier(@cmd.modify_number)
        critical = @critical == 12 ? "" : "c[#{@critical}]"
        fumble = @fumble == 2 ? "" : "f[#{@fumble}]"
        brak = @break == 0 ? "" : "b[#{@break}]"
        cmp_op = Format.comparison_operator(@cmd.cmp_op)

        "(#{@times}R6#{modify_number}#{critical}#{fumble}#{brak}#{cmp_op}#{@cmd.target_number})"
      end

      def dice_result_r(dice_total, dice_list, break_list)
        modify_number_text = Format.modifier(@cmd.modify_number)

        if break_list.empty?
          "#{dice_total}[#{dice_list.join(', ')}]#{modify_number_text}"
        else
          "#{dice_total}[#{dice_list.join(', ')}]×[#{break_list.join(', ')}]#{modify_number_text}"
        end
      end

      def result_r(dice_total, total)
        if dice_total <= @fumble
          "ファンブル!"
        elsif dice_total >= @critical
          "クリティカル!"
        elsif @cmd.cmp_op == :>=
          if total >= @cmd.target_number
            "#{total} 成功"
          else
            "#{total} 失敗"
          end
        else
          total.to_s
        end
      end

      def normalize_critical(critical, string)
        if /@[+-]/.match(string)
          critical = 12 + critical
        end

        if critical < 2
          critical = 2
        end

        return critical
      end

      def normalize_fumble(fumble, string)
        if /#[+-]/.match(string)
          fumble = 2 + fumble
        end

        return clamp(fumble, 0, 12)
      end

      # ダメージロール
      def damage_roll(string)
        parser = Command::Parser.new(/\d+D6/, round_type: round_type)
                                .restrict_cmp_op_to(nil)
                                .enable_dollar
        @cmd = parser.parse(string)
        return nil unless @cmd

        @times = @cmd.command.to_i
        @break = (@cmd.dollar || 0).abs

        if @times == 0
          return nil
        end

        dice_list = @randomizer.roll_barabara(@times, 6).sort
        dice_broken = dice_list.pop(@break)

        total_n = dice_list.inject(0, :+) + @cmd.modify_number
        total_n = 0 if total_n < 0

        sequence = [
          expr_damage(),
          result_damage(dice_list, dice_broken),
          total_n
        ]

        return sequence.join(' > ')
      end

      def expr_damage()
        modify_number = Format.modifier(@cmd.modify_number)
        brak = @break == 0 ? "" : "b[#{@break}]"

        "(#{@times}D6#{modify_number}#{brak})"
      end

      def result_damage(dice_list, break_list)
        dice_total = dice_list.inject(0, :+)
        modify_number_text = Format.modifier(@cmd.modify_number)

        if break_list.empty?
          "#{dice_total}[#{dice_list.join(', ')}]#{modify_number_text}"
        else
          "#{dice_total}[#{dice_list.join(', ')}]×[#{break_list.join(', ')}]#{modify_number_text}"
        end
      end
    end
  end
end