bcdice/BCDice

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

Summary

Maintainability
B
4 hrs
Test Coverage
D
64%
# frozen_string_literal: true

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

      # ゲームシステム名
      NAME = 'フィルトウィズ'

      # ゲームシステム名の読みがな
      SORT_KEY = 'ふいるとういす'

      # ダイスボットの使い方
      HELP_MESSAGE = <<~MESSAGETEXT
        ・判定 (3FW@x#y<=z or z-3FW@x#y)
         3個の6面ダイスを振る判定。
         @x:xにクリティカル値を入力。省略可。(省略時クリティカル値4)
         #y:yにファンブル値を入力。省略可(省略時ファンブル値17)
         <=z or z-:zに目標値を入力。±の計算に対応。省略可。
        ・【必殺技!】 (HST)
         ホムンクルス特技【必殺技!】表。
        ・マジカルクッキング (COOKx)
         マジカルクッキングのシェフのおすすめコース。
         xにクッキングレベルを入力。(1-8)
        ・ナンバーワンくじ (LOTN or LOTP)
         LOTNでノーマルくじ、LOTPでプレミアムくじ。(GURPS-FW版)
        ----------夢幻の迷宮用----------
        ・共通書式
         a:aに地形(1-6の数字)を入力。省略可。(省略時ランダム決定)
          (1:洞窟 2:遺跡 3:山岳 4:水辺 5:森林 6:墓場)
         d:dに難易度を入力。(E:初級 N:中級 H:上級 L:悪夢 X:伝説)
        ・ランダムイベント表 (RANDda)
        ・ランダムエンカウント表 (RENCda)
        ・エネミーデータ表 (REDde)
         エネミーデータ参照表。
         GMがシークレットダイスで参照するとPLに知られずにエネミーデータを参照可能。
         e:3桁のイベントダイスを入力(D666の結果)。
        ・トラップ表 (TRAPd)
        ・財宝表 (TRSr±x)
         r:rに財宝ランクを入力。
         ±x:xに財宝ランク修正値を入力。省略可。
        ・迷宮追加オプション表(ROPd)
      MESSAGETEXT

      register_prefix('3FW', '[\+\-\d]*-3FW', 'LOT[NP]', 'HST', 'COOK[1-8]', 'RAND', 'RENC', 'RED', 'TRS', 'TRAP[ENHLX]', 'ROP[ENHLX]')

      def initialize(command)
        super(command)
        @d66_sort_type = D66SortType::NO_SORT; # d66の差し替え
      end

      # @param command [String] コマンド
      # @return [Result, nil] 固有コマンドの評価結果
      def eval_game_system_specific_command(command)
        # ダイスロールコマンド
        result = FW.roll(command, @randomizer)
        return result if result

        # 各種コマンド
        case command
        when "LOTN"
          roll_jump_table("ナンバーワンノーマルくじ", LOT_NORMAL_TABLES[1])
        when "LOTP"
          roll_jump_table("ナンバーワンプレミアムくじ", LOT_PREMIUM_TABLES[1])
        when /COOK([1-8])/
          lv = Regexp.last_match(1).to_i
          roll_jump_table("マジカルクッキング", COOK_TABLES[lv])
        when /TRAP[ENHLX]/
          roll_trap_table(command)
        when /TRS.*/i
          getTresureResult(command)
        when /RAND.*/
          roll_random_event_table(command)
        when /RENC.*/
          roll_random_event_table(command)
        when /RED.*/i
          fetch_enemy_data(command)
        when /ROP[ENHLX]/
          roll_random_option_table(command)
        else
          roll_tables(command, TABLES)
        end
      end

      # 表を振った結果を独自の書式で整形する
      # @param table_name [String] 表の名前
      # @param number [String] 出目の文字列
      # @param result [String] 結果の文章
      # @return [String]
      def format_table_roll_result(table_name, number, result)
        "#{table_name}(#{number}):#{result}"
      end

      # ジャンプする項目を含む表を振る
      # @param table_name [String] 表の名前
      # @param table [DiceTable::RangeTable] 振る対象の表
      # @return [Result]
      def roll_jump_table(table_name, table)
        # 出目の配列
        values = []

        loop do
          roll_result = table.roll(@randomizer)
          values.concat(roll_result.values)

          content = roll_result.content
          case content
          when String
            return Result.new(format_table_roll_result(table_name, values.join, content))
          when Proc
            # 次の繰り返しで指定された表を参照する
            table = content.call
          else
            raise TypeError
          end
        end
      end

      class FW
        attr_accessor :dice_count, :target, :critical, :fumble

        def initialize
          @target = nil
        end

        def self.roll(command, randomizer)
          fw = parse(command)

          return fw&.roll(randomizer)
        end

        def self.parse(command)
          if (m = /^(\d[+\-\d]*)-(\d+)FW(?:@(\d+))?(?:\#(\d+))?$/.match(command))
            new.tap do |fw|
              fw.dice_count = m[2].to_i
              fw.target = Arithmetic.eval(m[1], RoundType::FLOOR)
              fw.critical = m[3]&.to_i || 4
              fw.fumble = m[4]&.to_i || 17
            end
          elsif (m = /(\d+)FW(?:@(\d+))?(?:\#(\d+))?(?:<=([+\-\d]+))?/.match(command))
            new.tap do |fw|
              fw.dice_count = m[1].to_i
              fw.target = Arithmetic.eval(m[4], RoundType::FLOOR) if m[4]
              fw.critical = m[2]&.to_i || 4
              fw.fumble = m[3]&.to_i || 17
            end
          end
        end

        def roll(randomizer)
          dice_list = randomizer.roll_barabara(@dice_count, 6)
          dice = dice_list.sum()
          dice_str = dice_list.join(",")

          res = result(dice)

          sequence = [
            "(#{expr})",
            "#{dice}[#{dice_str}]",
            res.text,
          ].compact

          res.text = sequence.join(" > ")

          return res
        end

        private

        def expr
          ret = "#{@dice_count}FW"
          ret += "@#{@critical}" if @critical != 4
          ret += "##{@fumble}" if @fumble != 17
          ret += "<=#{@target}" if @target

          return ret
        end

        def result(total)
          if total <= @critical
            Result.critical("クリティカル!")
          elsif total >= @fumble
            Result.fumble("ファンブル!")
          elsif @target
            success = @target - total
            if total <= @target
              Result.success("成功(成功度:#{success})")
            else
              Result.failure("失敗(失敗度:#{success})")
            end
          else
            Result.new
          end
        end
      end

      TABLES = {
        "HST" => DiceTable::Table.new(
          "【必殺技!】",
          "1D6",
          [
            '〔命中〕判定に出目[1,1,1]でクリティカル。更に致傷力に「SLv×20」のボーナスを得る。',
            '〔命中〕判定と致傷力に「SLv×10」のボーナスを得る。',
            '致傷力に「SLv×10」のボーナスを得る。',
            '攻撃が命中するとバッドステータス「転倒」を与える。',
            '通常攻撃。',
            '〔命中〕判定に[6,6,6]でファンブル。更に、使用者がバッドステータス「転倒」を受ける。',
          ]
        )
      }.freeze

      class Row
        def initialize(body, *args)
          @body = body
          @args = args
        end

        def format(difficulty)
          args = @args.map { |e| e[difficulty.index] }
          Kernel.format(@body, *args)
        end
      end

      class Difficulty
        DIFFICULTYS = ["E", "N", "H", "L", "X"].freeze

        NAMES = {
          "E" => "初級",
          "N" => "中級",
          "H" => "上級",
          "L" => "悪夢",
          "X" => "伝説",
        }.freeze

        def initialize(sign)
          @sign = sign
        end

        def index
          @index ||= DIFFICULTYS.find_index(@sign)
        end

        def name
          @name ||= NAMES[@sign]
        end
      end

      TRAP_TABLE = [
        Row.new("トライディザスター:宝箱から広範囲に火炎・冷気・電撃が放たれる罠。PC全員に「%s」の「火炎」「冷気」「電撃」属性ダメージ。", ['3D6+3', '3D6+50', '3D6+70', '3D6+100', '300']),
        Row.new("ペトロブラスター:広範囲に石化光線を放つ罠。PC全員[抵抗-%s]判定を行い、失敗したPCはBS「石化」を受ける。", [2, 4, 6, 8, 10]),
        Row.new("クロスボウストリーム:宝箱から矢の嵐が放たれる罠。PC全員に「%s」の「刺突」属性ダメージ。「ドッジ-%s」で〔回避〕が可能。", ['3D6+20', '3D6+40', '3D6+60', '3D6+90', '200'], [4, 6, 8, 10, 20]),
        Row.new("フォーチュンイーター:PC全員の幸運を食らい、Ftを%s点減少させる。Ftが0の場合「%s」点の防護点無視ダメージ。", [1, 2, 3, 4, 5], ['3D6+30', '3D6+50', '3D6+70', '3D6+100', '300']),
        Row.new("スロット:解除に失敗しても害はないが、スロットが揃うまで開かない宝箱。スロットを1回まわすには%sGPが必要。行動を消費して[感覚-%s]判定に成功すればスロットは揃う。有利な特異点「ビビット反射」があれば判定に+4のボーナス。", [100, 300, 600, 1000, 10000], [4, 6, 8, 10, 15]),
        Row.new("テレポーター:PC全員(とエンカウントしているエネミー)を転送して道に迷わせる。「財宝ランク」が1段階減少する。"),
        Row.new("アイスコフィン:宝箱を開けようとしたキャラクターを氷漬けにする罠。対象1体に「%s」の「冷気」属性ダメージ。更にFPにも%s点の防護点無視ダメージ。", ['3D6+30', '3D6+50', '3D6+70', '3D6+100', '300'], [5, 10, 15, 20, 30]),
        Row.new("クロスボウ:宝箱を開けようとしたキャラクターに強力な矢が放たれる罠。対象1体に「%s」の「刺突」属性ダメージ。「ドッジ-%s」", ['3D6+20', '3D6+40', '3D6+60', '3D6+90', '200'], [4, 6, 8, 10, 20]),
        Row.new("毒針:宝箱を開けようとしたキャラクターに毒針を突き刺す罠対象1体に%s点の防護点無視ダメージ。更に[抵抗-%s]判定に失敗するとシナリオ終了まであらゆる判定に-2のペナルティ。", [15, 30, 45, 60, 150], [4, 6, 8, 10, 15]),
        Row.new("アラーム:即座にその地形のエンカウント表を振って、それに対応したエネミーが出現する。出現したエネミーはそのターンから行動順に組み込まれる。出現するエネミー以外の記述は無視する。"),
        Row.new("殺人鬼の斧:宝箱を開けようとしたキャラクターに斧が振り下ろされる罠。対象1体に「%s」の「打撃」「斬撃」属性ダメージ。「ドッジ-%s」か「シールド-%s」で〔回避〕が可能。", ['3D6+30', '3D6+50', '3D6+70', '3D6+100', '300'], [4, 6, 8, 10, 20], [4, 6, 8, 10, -20]),
        Row.new("死神:宝箱を開けようとしたキャラクターに死神を取り憑かせる罠。4ラウンド目が終了するまであらゆる判定に-3のペナルティを受け、4ラウンド目の終了と同時に「%s」の防護点無視ダメージ。", ['3D6+30', '3D6+50', '3D6+70', '3D6+100', '300']),
        Row.new("幻の宝:宝箱に偽の財宝を入れ、本物の財宝を入手させない罠。トラップが発動すると価値の無い偽の宝物「幻の宝」を入手してしまう。「幻の宝」はアイテム欄を3つ占有し、シナリオ終了まで捨てられない。アイテム欄に空きがない場合は、何かを捨てて誰かが必ず持たなくてはならない。"),
        Row.new("エクスプロージョン:宝箱が大爆発を起こし、中身を粉々にしてしまう罠。宝箱の中身は消滅する。PC全員に「%s」の「打撃」「火炎」属性ダメージ。", ['3D6+10', '3D6+30', '3D6+50', '3D6+80', '200']),
        Row.new("レインボーポイズン:宝箱から七色の毒ガスが放たれる罠。PC全員に「%s」の防護点無視ダメージ。更にシナリオ終了まであらゆる判定に-2のペナルティ。[抵抗-%s]判定に成功すれば無効。", ['3D6+10', '3D6+30', '3D6+50', '3D6+80', '200'], [4, 6, 8, 10, 15]),
        Row.new("デスクラウド:宝箱から致死性の毒ガスを放つ罠。PC全員を即死させる。[抵抗-%s]判定に成功すれば無効。", [2, 4, 6, 8, 12]),
      ].freeze

      # 夢幻の迷宮トラップ表
      def roll_trap_table(command)
        m = /^TRAP([ENHLX])$/.match(command)
        unless m
          return nil
        end

        difficality = Difficulty.new(m[1])
        number = @randomizer.roll_sum(3, 6)
        chosen = TRAP_TABLE[number - 3]

        return "トラップ表<#{difficality.name}>(#{number}):#{chosen.format(difficality)}"
      end

      class D66Table
        def initialize(name, rows)
          @name = name
          @rows = rows
        end

        def roll(randomizer, difficality)
          value = randomizer.roll_d66(D66SortType::NO_SORT)
          chosen = @rows[value]

          "#{@name}<#{difficality.name}>(#{value}):#{chosen.format(difficality)}"
        end
      end

      OPTION_TABLE = D66Table.new(
        "迷宮追加オプション表",
        {
          11 => Row.new("黄金の迷宮(財宝ランク+2):全てが黄金で彩られた迷宮。財宝ランクが大きく上昇する。"),
          12 => Row.new("密林の迷宮(財宝ランク+1):密林の中にひっそりとたたずむ迷宮。分類が「魔獣」「獣人」「霊獣」のエネミーが行うあらゆる判定に+2のボーナス。"),
          13 => Row.new("カラクリの迷宮:複雑なカラクリが周囲で絶え間なく動いている迷宮。分類「ギア」のエネミーが行うあらゆる判定に+2のボーナス。クリア時に「アタッチメント割引券」を全員が%s枚獲得。", [1, 2, 3, 5, 10]),
          14 => Row.new("フラウの舞踏会:あちこちに花畑のある迷宮。フラウが発生するランダムイベントが発生した際、「この迷宮を制覇して、私達が舞踏会を開けるようにしてね」とお願いされ、クリア時の報酬に%sが追加される。", ['「キノコの帽子」(装飾品)', '「猛毒の花」(装飾品)', '「フルブロウン」(鎧)', '「緊急召喚の宝珠」(装飾品)', '魔将樹の大剣(剣)']),
          15 => Row.new("アズマ風の迷宮:風流なアズマ風の迷宮。武器に「刀」を持つエネミーが行うあらゆる判定に+2のボーナス。クリア時に「アタッチメント割引券」を全員が%s枚獲得。", [1, 2, 3, 5, 10]),
          16 => Row.new("枯れた泉の迷宮:「全地形1-1」の回復の泉が全て枯れており、回復効果を得ることができない。「山岳1-6」の貴重な水源や、「水辺1-6」の毒の泉などはそのまま存在する。"),
          21 => Row.new("天空への道(財宝ランク+1):上へ上へと果てしなく登っていく迷宮。空気が薄くなって疲労しやすくなる。【特技】特技などによるFP消費が全て+3。"),
          22 => Row.new("灼熱焦土の迷宮(財宝ランク+1):とてつもなく暑く、あちこちで炎が燃え盛る迷宮。エネミーが行う「火炎」属性を含む攻撃の致傷力に+%sのボーナス。", [10, 20, 30, 50, 100]),
          23 => Row.new("灼熱焦土の迷宮(財宝ランク+1):とてつもなく寒く、気温が氷点下の迷宮。エネミーが行う「冷気」属性を含む攻撃の致傷力に+%sのボーナス。", [10, 20, 30, 50, 100]),
          24 => Row.new("盗賊王の迷宮:迷宮内での罠や鍵を解除する[感覚]判定に-3のペナルティ。4ラウンドまでに出現した宝箱の「財宝ランク」+1。"),
          25 => Row.new("ミミック狂暴化:「全地形2-5」のミミックの致傷力に+%sのボーナス。ミミックを見破った場合に得られるGPが%sGP増加する。", [20, 30, 50, 80, 150], [500, 1000, 3000, 5000, 20000]),
          26 => Row.new("トレジャーイーター狂暴化:「全地形2-6」のトレジャーイーターを見破る[知力]判定に-3のペナルティ。4ラウンドまでに出現した宝箱の「財宝ランク」+1。"),
          31 => Row.new("暗闇の迷宮:どこもかしこも真っ暗な迷宮。「猫の目」などがなければ視覚に関する[感覚]判定に-5のペナルティ。"),
          32 => Row.new("騒音の迷宮:常に大音量で謎の音楽(BGM)が鳴っている迷宮。聴覚に関する[感覚]判定に-5のペナルティ。"),
          33 => Row.new("未知の怪物の迷宮(財宝ランク+1):エネミーの姿がシルエットのみになる迷宮。エネミーのデータがいかなる手段でも判明させられなくなる。(通常通り〔HP〕〔FP〕〔先制〕は判明する)"),
          34 => Row.new("氾濫中の迷宮:大雨が降っており、川などが氾濫している迷宮。水泳を行う際の[敏捷]判定に-5のペナルティ。「森林3-6」の山火事イベントの効果は無視できる。"),
          35 => Row.new("間抜けの迷宮(財宝ランク+1):頭がおかしくなりそうな極彩色の迷宮。[知力][意志]判定に-2のペナルティ。[知力]や[意志]そのものが下がるわけではない。"),
          36 => Row.new("瘴気の迷宮(財宝ランク+1):生命力を奪う紫の霧で満ちた迷宮。〔HP〕の最大値に-%sのペナルティ。", [10, 20, 30, 40, 50]),
          41 => Row.new("加速する迷宮:狂ったように針の動く時計が多数された迷宮。「CT:安息の日」以外の【特技】が「CT:なし」になる。"),
          42 => Row.new("停滞する迷宮(財宝ランク+1):動かない時計が多数設置された迷宮。「CT:安息の日」以外のCTの存在する【特技】が「CT:シナリオ終了」になる。この効果はシナリオ終了まで持続する。"),
          43 => Row.new("猛毒の迷宮(財宝ランク+1):見るからに毒々しい紫色の沼があちこちにある迷宮。エネミーが行う、名称に「毒」もしくは「ポイズン」が入る【特技】や、名称に「毒」もしくは「ポイズン」が入るトラップの致傷力に+%sのボーナス。", [10, 20, 40, 50, 100]),
          44 => Row.new("死の迷宮(財宝ランク+2):死の運命から逃れることのできない、血まみれの迷宮。「生命保険証」の効果が適用されない。"),
          45 => Row.new("幸運の迷宮:何者かの加護を感じる迷宮。PC全員のFtの最大値と現在値に+1のボーナス。この効果はシナリオ終了まで持続する。"),
          46 => Row.new("不運の迷宮:PC全員のFt最大値と現在値に-1のペナルティ。この効果はシナリオ終了まで持続する。"),
          51 => Row.new("レアメタルの迷宮:非常にレアなエネミー「レアメタルキャンディー」「レアメタルクラウン」が生息している迷宮。キャンディークラウン(CL40)、ゴールデンクラウン(CL177)から獲得できる通常ドロップのGPが5倍になる。"),
          52 => Row.new("魔力の泉:PCとエネミーの双方が、〔FP〕を消費せずに【魔法】を使用できるようになる。最終的な消費〔FP〕が最大〔FP〕より大きい【魔法】は使用できない。"),
          53 => Row.new("ブルーの迷宮:陰鬱な気分になり、他のキャラクターと関わる気力を失う。PC全員が不利な特異点「嫌な奴」を1段階得る。"),
          54 => Row.new("レッドの迷宮:なぜか興奮して非常に好戦的になる。PC全員が不利な特異点「脳みそ筋肉」を得る。交戦中に「1:回復系」のイベントが発生しても戦闘を終了させることができない。"),
          55 => Row.new("ピンクの迷宮:なんだか身近な異性(同性も?)が気になって仕方なくなる。PC全員が不利な特異点「英雄色を好む」を得る。魔族も戦闘意欲を失い、「分類:魔族」のエネミーが出現するイベントは無視する。"),
          56 => Row.new("ハズレの迷宮(財宝ランク-1):ツギハギだらけの壁などでできた、ハリボテのような貧相な迷宮。宝箱の中身もなんだか貧相になる。"),
          61 => Row.new("ラダマンティスの迷宮(財宝ランク+2):第一魔将ラダマンティスの像が入口に設置された迷宮。全てのエネミーが行うあらゆる判定に+2のボーナス。また、「遺跡6-6」のイベントのダメージ+%s。", [20, 40, 60, 80, 150]),
          62 => Row.new("グレイヴディガーの迷宮(財宝ランク+2):第二魔将グレイヴディガーの像が入口に設置された迷宮。「分類:アンデッド」のエネミーが行うあらゆる判定に+5のボーナス。"),
          63 => Row.new("ハイペリオンの迷宮(財宝ランク+2):第三魔将ハイペリオンの像が入口に設置された迷宮。全てのエネミーが「ターン開始」時に〔HP〕を全回復する。"),
          64 => Row.new("ムスペルニヴルの迷宮(財宝ランク+2):勇ましくも美しい女性の像が設置された迷宮。エネミーが行う「火炎」もしくは「冷気」属性を含む攻撃の致傷力に+%sのボーナス。", [20, 40, 60, 80, 150]),
          65 => Row.new("ウェルスの迷宮:人懐っこそうなアズマ風の青年が設置された迷宮。シナリオ上で第五魔将の正体が明らかに鳴っている場合のみ、PC全員のFtの最大値と現在値に+5のボーナス。この効果はシナリオ終了まで持続する。"),
          66 => Row.new("バロールの迷宮(財宝ランク+2):第六魔将バロールの像が入口に設置された迷宮。「分類:ギア」のエネミーが行うあらゆる判定に+5のボーナス。"),
        }
      )

      # 夢幻の迷宮追加オプション表
      def roll_random_option_table(command)
        m = /^ROP([ENHLX])$/.match(command)
        unless m
          return nil
        end

        difficality = Difficulty.new(m[1])
        return OPTION_TABLE.roll(@randomizer, difficality)
      end

      # 夢幻の迷宮ランダムイベント表
      def roll_random_event_table(command)
        m = /^(RAND|RENC)([ENHLX])([1-6])?$/.match(command)
        unless m
          return nil
        end

        type = m[1] == "RAND" ? nil : 4
        difficulty = Difficulty.new(m[2])
        area = m[3]&.to_i || @randomizer.roll_once(6)

        table = EVENT_TABLES[area - 1]
        return table.roll(@randomizer, difficulty, type: type)
      end
    end
  end
end

require "bcdice/game_system/filled_with/lot_tables"
require "bcdice/game_system/filled_with/enemy_data_tables"
require "bcdice/game_system/filled_with/event_tables"
require "bcdice/game_system/filled_with/cook_tables"
require "bcdice/game_system/filled_with/tresure_tables"