bcdice/BCDice

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

Summary

Maintainability
D
1 day
Test Coverage
A
96%
# frozen_string_literal: true

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

      # ゲームシステム名
      NAME = 'ウォーハンマーRPG第4版'

      # ゲームシステム名の読みがな
      SORT_KEY = 'うおおはんまあ4'

      # ダイスボットの使い方
      HELP_MESSAGE = <<~INFO_MESSAGE_TEXT
        ■ クリティカル表 (whctH, whctA, whctB, whctL, whctHU, whctAU, whctBU, whctLU)
          "WH部位 頑健ボーナス以下フラグ"の形で指定します。
          部位は「H(頭部)」「A(腕)」「B(胴体)」「L(足)」の4カ所です。
          頑健ボーナスフラグは頑健ボーナス以下のダメージの時にUをつけます。
          例)whctH whctAU WHCTL

        ■ 命中部位表 (WHLT)
          命中部位をランダムに決定します。(クリティカル用)

        ■ やっちまった!表 (WHFT)
         やっちまった!表をロールして、結果を表示します。

        ■ 命中判定 (WHx)
          "WH(命中値)"の形で指定します。
          命中判定を行って、クリティカルかファンブルでなければ部位も表示します。
          例)wh60  wh(43-20)
      INFO_MESSAGE_TEXT

      register_prefix('WH')

      def initialize(command)
        super(command)

        @round_type = RoundType::CEIL
      end

      def eval_game_system_specific_command(command)
        roll_critical_table(command) || roll_attack(command) || roll_tables(command, TABLES)
      end

      def result_1d100(total, _dice_total, cmp_op, target)
        return Result.nothing if target == '?'
        return nil unless cmp_op == :<=

        t10 = total / 10
        d10 = target / 10
        sl = d10 - t10

        if total <= 5 && sl < 1
          # 自動成功時のSLは最低でも1
          sl = 1
        elsif total >= 96 && sl > -1
          # 自動失敗時のSLは最大でも-1
          sl = -1
        end

        result =
          if total <= 5
            Result.success("自動成功")
          elsif total >= 96
            Result.failure("自動失敗")
          elsif total <= target
            Result.success("成功")
          else
            Result.failure("失敗")
          end

        sl_text = format("(SL%+d)", sl)
        if (sl == 0) && (total > target)
          sl_text = "(SL-0)"
        end

        result.text += "#{sl_text} > #{result_detail(sl, total, target)}"

        return result
      end

      private

      def result_detail(sl, total, target)
        if sl == 0
          if total <= 5
            '小成功'
          elsif total >= 96
            '小失敗'
          elsif total <= target
            '小成功'
          else
            '小失敗'
          end
        elsif sl >= 6
          '超大成功'
        elsif sl >= 4
          '大成功'
        elsif sl >= 2
          '成功'
        elsif sl > 0
          '小成功'
        elsif sl >= -1
          '小失敗'
        elsif sl >= -3
          '失敗'
        elsif sl >= -5
          '大失敗'
        else # sl <= -6
          '超大失敗'
        end
      end

      # クリティカル表
      def roll_critical_table(command)
        m = /^WHCT([HABTL])(U)?$/.match(command)
        return nil unless m

        part = m[1]
        part = "B" if part == "T"

        under_ganken_bonus = !m[2].nil?

        CRITICAL_TABLES[part].roll(@randomizer, under_ganken_bonus)
      end

      class CriticalTable
        def initialize(name, items)
          @name = name
          @items = items
        end

        def roll(randomizer, under_ganken_bonus)
          dice = randomizer.roll_once(100)
          if under_ganken_bonus
            dice = (dice - 20).clamp(1, 100)
          end

          chosen = @items.find { |key, _| key >= dice }[1]

          "#{@name}(#{dice}) > #{chosen}"
        end
      end

      CRITICAL_TABLES = {
        "H" => CriticalTable.new(
          "頭部CT表",
          [
            [10, '勲章となる傷:耐久値-1:「出血状態」1つを得る。社交系テストSL+1(累積なし)。'],
            [20, '小さな裂傷:耐久値-1:「出血状態」1つを得る。'],
            [25, '眼窩の負傷:耐久値-1:「目がくらんだ状態」1つを得る。'],
            [30, '耳朶への強打:耐久値-1:「耳鳴り状態」1つを得る。'],
            [35, '朦朧化打撃:耐久値-2:「朦朧状態」1つを得る。'],
            [40, '眼の周りの青あざ:耐久値-2:「目がくらんだ状態」2つを得る。'],
            [45, '耳朶の裂傷:耐久値-2:「出血状態」2つに加えて、「耳鳴り状態」1つを得る。'],
            [50, '額への一打:耐久値-2:「出血状態」2つに加えて「目がくらんだ状態」1つを得る。なお後者は「出血状態」を取り除いた後でなければ取り除けない。'],
            [55, '顎骨折:耐久値-3:「朦朧状態」2つを得る。さらに「骨折(軽度)」の負傷を得る。'],
            [60, '眼窩の大怪我:耐久値-3:「出血状態」2つを得る。さらに「目がくらんだ状態」1つも得て、こちらは「治療行為」を受けない限り回復できない。'],
            [65, '耳朶の大怪我:耐久値-3:片耳の聴力を失う。聴覚に関するテストに-20。この効果をもう一度受けた場合、永続的な聴覚喪失者となる。これは魔法でしか治療できない。'],
            [70, '鼻砕き:耐久値-3:「出血状態」2つを得る。「手強い(+0)」〈肉体抵抗〉テストを行い、失敗したら「朦朧状態」1つも得る。社交系テストに+1/-1。'],
            [75, '顎粉砕:耐久値-4:「朦朧状態」3つを得る。「手強い(+0)」〈肉体抵抗〉テストを行い、失敗したら「気絶状態」を得る。「骨折(重度)」の負傷を得る。'],
            [80, '脳震盪打撃:耐久値-4:「出血状態」2つと「耳鳴り状態」1つに加えて1d10の「朦朧状態」を得る。さらに1d10日間継続する「疲労状態」1つも得る。この「疲労状態」継続中に頭部への「致命的負傷」を再度得たなら、「普通(+20)」の〈肉体抵抗〉テストを行い、失敗したなら追加で「気絶状態」になる。'],
            [85, '口粉砕:耐久値-4:「出血状態」2つを得る。1d10本の歯が失われる。「身体欠損(容易)」を被る。'],
            [90, '耳が屑肉に:耐久値-4:「耳鳴り状態」3つに加えて「出血状態」2つを得る。片耳の機能を失う。「身体欠損(普通)」を被る。'],
            [93, '眼球破裂:耐久値-5:「目がくらんだ状態」3つと、「出血状態」2つ、「朦朧状態」1つを得る。片目の機能を失う。「身体欠損(難しい)」を被る。'],
            [96, '顔が化け物のように:耐久値-5:「出血状態」3つ「目がくらんだ状態」3つ、「朦朧状態」2つを得る。片目の機能を失い、鼻梁も失う。「身体欠損(多難)」を被る。'],
            [99, '顎が屑肉に:耐久値-5:「出血状態」4つと「朦朧状態」3つを得る。「超多難(-30)」な〈肉体抵抗〉テストを行い、失敗したら「気絶状態」になる。「骨折(重度)」の負傷を得て、舌と1d10本の歯を失う。「身体欠損(多難)」を被る。'],
            [100, '斬首刑さながらに:即死:'],
          ]
        ),
        "A" => CriticalTable.new(
          "腕部CT表",
          [
            [10, '腕痺れ:耐久値-1:手に持っているものを落としてしまう。'],
            [20, '小さな裂傷:耐久値-1:「出血状態」1つを得る。'],
            [25, '捻挫:耐久値-1:「筋断裂(軽度)」の負傷を得る。'],
            [30, 'ひどい腕痺れ:耐久値-2:手に持っているものを落としてしまい、その腕は1d10-【頑健力】ボーナスのラウンドに渡って使えなくなる。'],
            [35, '筋断裂:耐久値-2:「出血状態」1つと、「筋断裂(軽度)」の負傷を得る。'],
            [40, '手の出血:耐久値-2:「出血状態」1つを得る。その手で何かを握る「アクション」を行う場合、先立って「普通(+20)」の【器用度】テストを行い、失敗したら取り落としてしまう。'],
            [45, '肩の捻挫:耐久値-2:手に持っているものを落としてしまい、その腕は1d10ラウンドに渡って使えなくなる。'],
            [50, '大きな傷口:耐久値-3:「出血状態」2つを得る。傷口を縫う「手術」を受ける前に同じ腕にダメージを受けた場合、傷口が開き「出血状態」1つを追加で得る。'],
            [55, '単純骨折:耐久値-3:手に持っているものを落としてしまい、「骨折(軽度)」の負傷を得る。「難しい(-10)」〈肉体抵抗〉テストを行い、失敗したなら「朦朧状態」1つを得る。'],
            [60, '靱帯断裂:耐久値-3:手に持っているものを落としてしまい、「筋断裂(重度)」の負傷を得る。'],
            [65, '深手:耐久値-3:「出血状態」2つを得る。「朦朧状態」1つを得て、「筋断裂(軽度)」の負傷を得る。「多難(-20)」な〈肉体抵抗〉テストを行い、失敗したなら「気絶状態」になる。'],
            [70, '動脈破損:耐久値-4:「出血状態」4つを得る。「手術」を受ける前に同じ腕にダメージを受けるたびに「出血状態」2つを得る。'],
            [75, '肘粉砕:耐久値-4:手に持っているものを落としてしまい、「筋断裂(重度)」の負傷を得る。'],
            [80, '肩の脱臼:耐久値-4:「多難(-20)」な〈肉体抵抗〉テストを行い、失敗したなら「朦朧状態」1つを得て「伏せ状態」になる。手に持っているものを落としてしまう。その腕を使うことはできず、欠損したものとして扱う。さらに「治療行為」を受けるまでの間「朦朧状態」1つを得る。'],
            [85, '指切断:耐久値-4:「身体欠損(普通)」を被る。「出血状態」1つを得る。'],
            [90, '手の半切断:耐久値-5:指一本を失い、「身体欠損(多難)」を被る。「出血状態」2つと「朦朧状態」1つを得る。君が「治療行為」を受けるまでの以降の各ラウンドに指を一本ずつ失う。すべての指を失ったら、その手の機能を失い「身体欠損(難しい)」を得る。'],
            [93, '力こぶがズタズタに:耐久値-5:手に持っていたものを落とすとともに「筋断裂(重度)」の負傷を得て、「出血状態」2つと「朦朧状態」1つを得る。'],
            [96, '片手が屑肉同然に:耐久値-5:その手の機能を失い、「身体欠損(多難)」を被る。「出血状態」2つを得る。「多難(-20)」な〈肉体抵抗〉テストを行い、失敗したなら「気絶状態」になる。'],
            [99, '靱帯断裂:耐久値-5:腕は無用の長物と化す―「身体欠損(超多難)」を被る。「出血状態」3つ、「朦朧状態」1つを得て、「伏せ状態」になる。「多難(-20)」な〈肉体抵抗〉テストを行い、失敗したなら「気絶状態」になる。'],
            [100, '残酷な腕の喪失:即死:'],
          ]
        ),
        "B" => CriticalTable.new(
          "胴体CT表",
          [
            [10, 'ただのかすり傷だって!:耐久値-1:「出血状態」1つを得る。'],
            [20, 'みぞおちへの一打:耐久値-1:「朦朧状態」1つを得る。「容易(+40)」な〈肉体抵抗〉テストを行い、失敗したなら「伏せ状態」になる。'],
            [25, '下腹部に直撃!:耐久値-1:「多難(-20)」な〈肉体抵抗〉テストを行い、失敗したなら「朦朧状態」3つを得る。'],
            [30, '背中を捻じる:耐久値-1:「筋断裂(軽度)」の負傷を得る。'],
            [35, '息ができない:耐久値-2:「朦朧状態」1つを得る。「普通(+20)」の〈肉体抵抗〉テストを行い、失敗したなら「伏せ状態」になる。また、1d10ラウンドに渡って「移動力」が半分になる。'],
            [40, '肋骨に青あざ:耐久値-2:1d10日間に渡って【敏捷力】に関連するテストに-10。'],
            [45, '鎖骨捻じれ:耐久値-2:左右どちらの鎖骨かランダムに決定する。そちらの手に持っている物を落としてしまい、1d10ラウンドに渡ってその腕は使用できない。'],
            [50, '裂傷:耐久値-2:「出血状態」2つを得る。'],
            [55, '肋骨にひびが:耐久値-3:「朦朧状態」1つを得る。また、「骨折(軽度)」の負傷を得る。'],
            [60, '大きな傷口:耐久値-3:「出血状態」3つを得る。「手術」を受けるまでの間、胴体に命中を受けるたびに追加で1つの「出血状態」を得る。'],
            [65, '激痛を伴う裂傷:耐久値-3:「出血状態」2つと「朦朧状態」1つを得る。「多難(-20)」な〈肉体抵抗〉テストを行い、失敗したなら「気絶状態」になる。またSL+4以上でテストに成功していない限り、金切り声を上げてしまう。'],
            [70, '動脈損傷:耐久値-3:「出血状態」4つを得る。「手術」を受けるまでの間、胴体に攻撃を受けるたびに追加で2つの「出血状態」を得る。'],
            [75, '背筋断裂:耐久値-4:「筋断裂(重度)」の負傷を得る。'],
            [80, '股関節骨折:耐久値-4:「朦朧状態」1つを得る。「手強い(+0)」〈肉体抵抗〉テストを行い、失敗したなら「伏せ状態」にもなる。「骨折(軽度)」の負傷を得る。'],
            [85, '胸郭の大怪我:耐久値-4:「出血状態」4つを得る。「手術」を受けるまでの間、胴体に命中を受けるたびに追加で2つの「出血状態」を得る。'],
            [90, '内臓の損傷:耐久値-4:「膿み傷」に罹患し、「出血状態」2つを得る。'],
            [93, '胸郭粉砕:耐久値-5:「朦朧状態」1つを得る。これは「治療行為」を受けない限り取り除くことは出来ない。さらに「骨折(重度)」の負傷を得る。'],
            [96, '鎖骨粉砕:耐久値-5:「気絶状態」になる。これは「治療行為」を受けない限り取り除くことは出来ない。さらに「骨折(重度)」の負傷を得る。'],
            [99, '内臓出血:耐久値-5:「出血状態」1つを得る。これは「手術」を受けない限り取り除くことは出来ない。さらに「血液腐れ病」に罹患する。'],
            [100, '胴部両断:即死:'],
          ]
        ),
        "L" => CriticalTable.new(
          "脚部CT表",
          [
            [10, '爪先をぶつける:耐久値-1:「普通(+20)」の〈肉体抵抗〉テストを行い、失敗したなら次のターンの終了時まで【敏捷力】に関するテストに-10。'],
            [20, '足首の捻挫:耐久値-1:以降1d10ラウンドに渡って【敏捷力】に関連するテストに-10。'],
            [25, '小さな裂傷:耐久値-1:「出血状態」1つを得る。'],
            [30, '足がかりを失う:耐久値-1:「手強い(+0)」〈肉体抵抗〉テストを行い、失敗したなら「伏せ状態」になる。'],
            [35, '太腿への打撃:耐久値-2:「出血状態」1つを得て「普通(+20)」の〈肉体抵抗〉テストを行い、失敗したなら「伏せ状態」になる。'],
            [40, '足首の筋断裂:耐久値-2:「筋断裂(軽度)」の負傷を得る。'],
            [45, '膝の捻挫:耐久値-2:以降1d10ラウンドに渡って【敏捷力】に関連するテストに-20。'],
            [50, '爪先のひどい裂傷:耐久値-2:「出血状態」1つを得る。「手強い(+0)」〈肉体抵抗〉テストを行い、失敗したなら片方の爪先を失う―「身体欠損(普通)」を被る。'],
            [55, 'ひどい裂傷:耐久値-3:「出血状態」2つを得る。「手強い(+0)」〈肉体抵抗〉テストを行い、失敗したなら「伏せ状態」になる。'],
            [60, '膝のひどい捻挫:耐久値-3:「筋断裂(重度)」の負傷を得る。'],
            [65, '脚への痛打:耐久値-3:「伏せ状態」に加えて「出血状態」2つを得て、「骨折(軽度)」の負傷を得る。さらに「多難(-20)」な〈肉体抵抗〉テストを行い、失敗したなら「朦朧状態」1つを追加で得る。'],
            [70, '太腿への深手:耐久値-3:「出血状態」3つを得る。「手強い(+0)」〈肉体抵抗〉テストを行い、失敗したなら「伏せ状態」になる。「手術」を受けるまでの間、この脚に命中を受けるたびに、追加で1つの「出血状態」を得る。'],
            [75, '靱帯断裂:耐久値-4:「伏せ状態」に加えて「朦朧状態」1つを得る。「多難(-20)」な〈肉体抵抗〉テストを行い、失敗したなら「気絶状態」になる。君のその脚は使えなくなる。'],
            [80, '脛への深手:耐久値-4:敵の武器が君の膝下に当たり、骨に食い込んで腱を切断する。「朦朧状態」1つを得て、「伏せ状態」になる。さらに「筋断裂(重度)」と「骨折(軽度)」の負傷を得る。'],
            [85, '膝粉砕:耐久値-4:「出血状態」1つと「朦朧状態」1つを得ることに加えて、「伏せ状態」になる。「骨折(重度)」の負傷を得る。'],
            [90, '膝関節脱臼:耐久値-4:「伏せ状態」になる。「多難(-20)」な〈肉体抵抗〉テストを行い、失敗したなら「朦朧状態」1つを得る。これは「治療行為」を受けない限り取り除くことができない。'],
            [93, '脚粉砕:耐久値-5:「普通(+20)」の〈肉体抵抗〉テストを行い、失敗したなら「伏せ状態」を得て、足の指1本を失う。さら、SLが-1あるごとに追加で1つの足指を失う―「身体欠損(普通)」を被る。「出血状態」2つを得る。'],
            [96, '足切断:耐久値-5:「身体欠損(多難)」を被る。「出血状態」3つと「朦朧状態」2つを得て「伏せ状態」になる。'],
            [99, 'アキレス腱切断:耐久値-5:苦痛に金切り声を上げながら倒れる。「出血状態」2つと「朦朧状態」2つを得て「伏せ状態」になる。「身体欠損(超多難)」を被る。'],
            [100, '骨盤粉砕:即死:'],
          ]
        ),
      }.freeze

      # 命中判定
      def roll_attack(command)
        m = /^WH(\d+)$/.match(command)
        return nil unless m

        target_number = m[1].to_i
        total = @randomizer.roll_once(100)
        result = result_1d100(total, total, :<=, target_number)&.text

        sequence = [
          "(#{command})",
          total,
          result,
          additional_result(total, target_number),
        ].compact

        return sequence.join(" > ")
      end

      def additional_result(total, target_number)
        tens, ones = split_d100(total)
        if (total > target_number) || (total > 95) # 自動失敗時のファンブル処理も
          if ones == tens
            "ファンブル"
          else
            # 一の位と十の位を入れ替えて参照する
            "(#{HIT_PARTS_TABLE.fetch(merge_d100(ones, tens)).content})"
          end
        elsif ones == tens
          "クリティカル"
        else
          # 一の位と十の位を入れ替えて参照する
          HIT_PARTS_TABLE.fetch(merge_d100(ones, tens)).content
        end
      end

      def split_d100(dice)
        if dice == 100
          return 0, 0
        else
          return dice / 10, dice % 10
        end
      end

      def merge_d100(tens, ones)
        if tens == 0 && ones == 0
          100
        else
          tens * 10 + ones
        end
      end

      HIT_PARTS_TABLE = DiceTable::RangeTable.new(
        "命中部位表",
        "1D100",
        [
          # [0, '二足'],
          [1..9, '頭部'],
          [10..24, '左腕(逆腕)'],
          [25..44, '右腕(利腕)'],
          [45..79, '胴体'],
          [80..89, '左脚'],
          [90..100, '右脚'],
        ]
      )

      TABLES = {
        "WHLT" => HIT_PARTS_TABLE,
        "WHFT" => DiceTable::RangeTable.new(
          "やっちまった!表",
          "1D100",
          [
            [1..20, '君は自分の体の一部に当ててしまった。「耐久度」1点を失う。'],
            [21..40, '君の武器が1ダメージを受ける。次のラウンドの行動順が一番最後になる。'],
            [41..60, '君は身体動作の目算を誤る。次のラウンドのアクションに-10。'],
            [61..70, '君はひどく躓きバランスを回復するのに手間取る。次の「移動」を失う。'],
            [71..80, '君は武器を取り回しを誤る。次の「アクション」を失う。'],
            [81..90, '君は筋を違えるか、足首をひねってしまう。「筋断裂(軽度)」の負傷を得る。「致命的負傷」としてカウントされる。'],
            [91..100, '君は射程内のランダムな味方を攻撃してしまう。命中判定の出目の1の位をSLとして用いること。攻撃可能な味方が居ない場合は、自己を攻撃してしまい「朦朧状態」1つを得る。'],
          ]
        )
      }.freeze
    end
  end
end