查看: 3380|回复: 0

[交流讨论] 基础脚本:通用备注扩展(及此脚本制作简单状态切换被动

[复制链接]
梦石
0
星屑
782
在线时间
71 小时
注册时间
2017-12-2
回帖
12
发表于 2021-3-15 00:24:08 | 显示全部楼层 |阅读模式

加入我们,或者,欢迎回来。

您需要 登录 才可以下载或查看,没有账号?注册会员

×
本帖最后由 KurozawaRuby 于 2021-3-15 00:31 编辑

很久之前就想写的一个东西,最近找到了历史遗留的老代码,看了十几秒后删了重写了一份,发上来给大家玩。
因为也没实现什么实际功能,也需要使用者有一定的脚本能力,就发个讨论吧。

我们经常会碰到需要为游戏系统中的角色,技能,物品等新增一些额外属性的时候,之前挺多脚本的做法是在备注区(note)走正则匹配,这样做每加一个属性就需要新增一个正则和一个读取属性的方法,比较麻烦。也有大大使用元编程等手段自动生成了这些代码,但作为一个稍懂一点ruby的码农,怎么能不造个自己的轮子呢?

于是就有了这个备注扩展脚本,它可以在备注栏定义值类型、函数类型和ruby原始代码等的变量,通过变量名的Symbol获取,帮助我们走出正则解析等的迷宫,专注于游戏系统内的逻辑实现。

备注扩展脚本会在加载数据库(DataManager.load_databas)完成后初始化备注扩展的属性,扩展的属性被追加到RPG::BaseItem, RPG::Map, RPG::Tileset这三个类中,你可以通过 ext(:name) 的方式去获取。

我们可以在备注中添加 '---' 分隔符来兼容其它的脚本,如果备注中包含以三个英文减号'---'开头的行,则会将其作为分隔行。默认情况下,脚本不会解析没有分隔行的备注。有分隔行时,脚本默认会解析第一个分隔行之前的部分,而将该分隔行之后的部分作为原始备注设置回去,因此其它脚本在运行时进行正则匹配,就无法感知到备注扩展的内容。当然,我们也可以通过修改脚本内设置的常量来改变这样(需要有分隔行才解析,解析分隔行之前的部分)的策略。

变量定义规则:
首先,以'#'号开头的,且不在多行解析范围内的行为注释行。

以赋值符号 '='  '=>'  '=$'  '=@' 为界,左边的作为变量名(不允许带空格),或函数定义解析。右边作为值解析,如果右边不为空,则视为单行定义。如果右边为空,则为视为多行定义,直到下一个空行(或分隔行)为止。

比如备注区备注
  1. name1 = test

  2. name2 =
  3. t
  4. e
  5. s
  6. t
  7. ---
  8. 原始注释
复制代码

上面的代码定义了name1和name2,值都是'test'(默认设定是删除多行文本间的换行符的,这个也可以修改常量配置),对应的data对象使用 ext(:name1) 就可以拿到它的值了,而其它脚本在运行时读取的注释,只有下面的“原始注释”了


脚本支持 值类型,函数类型,模板类型,原始ruby代码 四种定义,分别对应 '='  '=>'  '=$'  '=@' 四种符号。

值类型(=):
最简单的类型,直接写就行了,单行定义时会自适应数字(整数和浮点数)和字符串,不需要你自己转换了。字符串还可以支持"#{}"的表达式,不过表达式的值是通过eval在解析时直接确定的,所以不能通过ext方法访问krubynote的值或是游戏开关变量等全局变量的值了。如果你需要那样的功能,你可以使用模板(或者函数或者原始ruby,都是一样的,只是模板最简单)。
  1. 简介 = 这个物品的名字是#{name}
  2. 最大数量 = 100
  3. 百分比 = 20.0
  4. 整数部分为0的小数 = .2

  5. ---
  6. 原始注释
复制代码


函数类型(=>)
函数类型写法比较特殊,我是按照我自己写的比较偷懒的习惯,函数名+空格+参数列表空格分隔放在赋值符号"=>"的左边,右边放ruby代码,特殊情况是单行有参数的函数可以直接写等号,看起来更好看(多行或没有参数就只能写=>了,不然解析成值类型了)
  1. add a b = a + b

  2. fun => puts self

  3. condition user =>
  4.   user.hp_rate > 20

  5. ---
  6. 原始注释
复制代码

函数类型定义拿到的是lambda,需要你传参调用(使用.call或[]调用)
比如上例调用时
  1. # 只做演示,真正使用时别忘了判空
  2. ext(:add).call(1, 2)
  3. ext(:fun)[]
  4. ext(:condition)[user]
复制代码



模板类型(=$):
运行时确定"#{}"表达式的值的字符串类型,最开始设计就是为了写复杂的简介,以下是例子:

  1. 简介 =$
  2. #{name}是比较强力的技能,冷却时间为#{ext(:cd)}回合,伤害公式为#{damage.formula}

  3. cd = 5
  4. ---
  5. 原始注释
复制代码

这样只要照着下面重写一下description方法,这个简介就生效了,你也可以就着F1文档补充一点代码,把其它的属性写全(当然,底下实现技能冷却时间还需要你自己再写写代码才行,而且默认简介框的大小。。。):
  1. class RPG::BaseItem
  2.   def description
  3.     ext(:简介) || @description
  4.   end
  5. end
复制代码



原始ruby代码(=@):
最简单粗暴强力的功能,上面所有的功能都可以写ruby代码解决,但万一没注意漏个逗号什么的调起来报错信息会很蛋疼(函数类型也有类似问题)。
  1. list =@ [1,2,3]
  2. fun =@ -> { puts self }
  3. dict =@
  4. {
  5.     a: "1",
  6.     b: 2
  7. }

  8. ---
  9. 原始注释
复制代码


代码:
请注意根据自己的需要调整最开始4个常量哦。
最后一段重写DataManager.load_database时,最开始是直接用我之前整的after:类宏的,后来觉得发上来尽量减少依赖,就写成alias的形式了 https://rpg.blue/forum.php?mod=viewthread&tid=484700

[pre lang="ruby"]#encoding: utf-8
# -------------------------------------------------------------------
# - KRuby module  by KurozawaRuby
# -------------------------------------------------------------------
module KRuby
end

# -------------------------------------------------------------------
# - KRuby::Note module
# -   备注扩展 by KurozawaRuby
# - 在备注内可按照格式方便地定义一些变量,变量支持数字、字符串、函数类型。
# - 在脚本内,备注变量内通过 RPG::BaseItem#ext 方法根据变量名获取。
# - 可以较好地兼容其他备注相关脚本,需要用户对脚本有一定了解。
# -------------------------------------------------------------------
# - 备注扩展框架以备注中第一个三个英文减号 '---' 开头的行作为分隔行,默认将
# - 分隔行之前的行作为扩展备注进行解析,分隔行之后的为原始备注以兼容。
# - 你可自行修改 KRuby::Note 下 USE_DEFAULT 和 APPEAR_START 等常量。
# -
# - 备注扩展变量在游戏初始化加载数据(DataManager.load_database)后解析。
# - 扩展变量定义的基本语句为:
# - 单行: <name>=<value>
# - 多行: <name>=\n(换行符
# -       <multiline_value>\n(换行符
# -       (空行
# - ex: num = 1
# -     str = 这个物品的名字是#{name},它是...
# -     test =
# -     测试
# -     多行
# -
# - 以第一个等号 '=' 为标志,等号之前的为变量名,不允许带空格(最好是英文数字
# - 下划线,因为代码中我们通过变量名Symbol访问),等号之后的:
# -   1. 有内容,为单行,先判断内容格式是否符合数字,如果是简单数字格式则自动
# -      转为浮点数或整数,如果没有则为字符串
# -   2. 无内容,为多行,扩展变量值为下行开始至第一个空行内的字符串内容。
# - * 等号左右可以加空格,不影响。
# - * 字符串不需要使用引号,且可以使用"#{}"表达式,self为该RPG::BaseItem对象
# -
# - 扩展变量共有三类 值类型:val 模板类型:tpl 函数类型:fun
# -   上述的基本语句定义的就是值类型的变量,它在解析完成后固定,因此在其中使用
# - "#{$game_variables[12]}"等语句无法达成在游戏运行时根据变量变化的效果,如
# - 果需要实现类似功能,你需要使用模板或函数。
# -
# - 模板类型:
# - 在等号之后添加'$'符号就可以定义模板('='和'$'之间不能有空格)同样支持多行,
# - 通过 ext 方法获取的值为字符串类型(在运行动态生成,可以使用全局变量)
# - ex: template =$ variable 14 = #{$game_variables[12]}
# -
# - 函数类型:
# - 在等号之后添加'>'符号就可以定义函数('='和'>'之间不能有空格)同样支持多行,
# - 函数定义(参数名和代码请符合ruby的规范):
# - <funcution_name> <param_1>... <param_n> => <function_body>
# -   函数名、参数列表按照空格分隔(注意参数列表也是空格分隔,而非逗号),放在
# -  '=>' 符号的左边,符号的右边(多行的情况下同样为下一行至第一个空行)作为
# -  ruby 代码解析成 lambda
# -   函数类型通过 ext 方法获取的值为 lambda 对象,必须手动传参调用。
# - * 单行函数在有参数的情况下,可以省略'>',写成更好看的形式:
# -   ex: add a b = a + b
# -       test => $game_variables[12] > 12
# -       skill_condition user = user.hp_rate < 0.1
# -
# - 其它ruby类型(值类型:val的扩展):
# - 在等号之后添加'@'符号可以定义其它ruby类型的值,此时 '=@' 的右边(多行的
# - 情况下同样为下一行至第一个空行)作为 ruby 代码在解析时直接执行,self为
# - 该RPG::BaseItem对象
# - 通过 ext 方法获取的值为 ruby 代码执行的返回值。
# - 这样的写法有时候ruby代码写的语法不对就很难定位,请先在irb中测试好
# - 代码同样在 DataManager.load_database 后执行,与值类型:val相同
# -
# - ex: dict =@ { a:1, b:2 }
# -     arr =@ [1, 'str', 2.0]
# -
# -------------------------------------------------------------------
module KRuby::Note
  

  # -----------------------------------------------------------------
  # - KRuby::Note::USE_DEFAULT
  # - 备注里没有出现 '---' 分隔符的情况下
  # - true: 使用 Note 解析备注
  # - false: 不适用 Note
  # -----------------------------------------------------------------
  USE_DEFAULT = false

  # -----------------------------------------------------------------
  # - KRuby::Note::APPEAR_START
  # - 备注出现 '---' 分隔符时
  # - true: 解析第一个分隔符之前的部分
  # - false: 解析第一个分隔符之后的部分
  # -----------------------------------------------------------------
  APPEAR_START = true
  
  # -----------------------------------------------------------------
  # - KRuby::Note::DEL_LINE_SEP
  # - 删除多行文本的换行符
  # -----------------------------------------------------------------
  DEL_LINE_SEP = true
  
  # -----------------------------------------------------------------
  # - 在同一个备注里重复定义相同的名字时报错(false时覆盖先前的)
  # -----------------------------------------------------------------
  FAIL_ON_REDEF = true

  # -----------------------------------------------------------------
  # - KRuby::Note::Parser 备注解析器
  # - 使用:
  # - # item 为 RPG::BaseItem 等包含备注的类的对象
  # - Parser.new(item).do_final
  # -----------------------------------------------------------------
  class Parser
    def initialize(item)
      @item = item
      @data = {}
      @org = ''
      @state = KRuby::Note::APPEAR_START ? :ext : :org
      @line = 0
      @def_line = 0
      @after_sep = false
      @name = nil
      @val = ''
      @type = nil
    end
   
    def << (str)
      @line += 1
      case @state
      when :ext
        on_ext(str)
      when :org
        on_org(str)
      when :next
        on_next(str)
      else
        raise "在第#{@line}行,解析器的状态[#{state}]未定义"
      end
    end

    def do_final
      on_final
      [@org, @data]
    end

    private
    def on_org(str)
      return on_sep(:ext) if str.start_with?('---') && !@after_sep
      @org += str + "\n"
    end

    def on_ext(str)
      @def_line = @line
      return on_sep(:org) if str.start_with?('---') && !@after_sep
      on_ext_real(str)
    end

    def on_sep(state)
      return if @after_sep
      @after_sep = true
      @state = state
    end

    def on_ext_real(str)
      return if str.empty?

      str = str.lstrip
      return if str.start_with?('#')
      
      idx = str.index('=')
      raise "无法解析第#{@line}行:非注释行且没有找到'='" unless idx

      @name = str[0...idx].strip
      str = str[idx+1..-1]

      case str[0]
      when '>'
        @val = str[1..-1]
        @type = :fun
      when '$'
        check_name
        @val = str[1..-1].lstrip
        @type = :tpl
      when '@'
        check_name
        @val = str[1..-1]
        @type = :ruby
      else
        @val = str.lstrip
        @type = :val
      end

      if @val.empty?
        @val = ''
        @state = :next
        return
      end

      if @name.index(' ')
        @type = :fun
      end

      parse_new_val(true)
    end

    def on_next(str)
      if str.start_with?('---') && !@after_sep
        parse_new_val
        return on_sep(:org)
      end
      return parse_new_val if str.empty?
      @val += str
      @val += "\n" if @type == :fun || !KRuby::Note::DEL_LINE_SEP
    end

    def check_name
      if @name.index(' ')
        raise "无法解析第#{@line}行:变量名[#{@name}]包含空格"
      end
    end

    def parse_new_val(single_line = false)
      @state = :ext
      case @type
      when :fun
        new_fun
      when :val
        single_line ? new_val_single : new_val
      when :ruby
        new_val(@item.instance_eval(@val))
      when :tpl
        new_tpl
      else
        raise "无法解析第#{@line}行:未知的类型[#{@type}]"
      end
      reset
    end

    def new_fun
      fns = @name.split(' ')
      name = fns[0]
      params = fns[1..-1]
      codes = @val
      @name = name.to_sym
      new_ext(create_lambda(params, codes), :fun)
    end

    def new_val(val = nil)
      unless val
        val = calc_val
      end
      new_ext(val, :val)
    end

    def new_val_single
      if @val =~ /^(\d*(\.?)\d+)$/
        new_val($2.empty? ? $1.to_i : $1.to_f)
      else
        new_val
      end
    end

    def new_tpl
      tpl = eval("proc { \n#{tpl_code(@val)}\n }")

      new_ext(-> { @item.instance_eval(&tpl) }, :tpl)
    end

    def calc_val
      if @val.index('#{')
        @item.instance_eval(tpl_code(@val))
      else
        @val
      end
    end

    def reset
      @name = nil
      @val = ''
      @type = nil
    end

    def new_ext(value, type)
      @name = @name.to_sym
      info = KRuby::Note::ExtInfo.new(@name, value, type)
      old = @data[@name]
      if old
        on_redef
      end
      @data[@name] = info
    end

    def on_redef
      raise "已经定义过的变量名#{@name}在第#{@def_line}行被重新定义" if KRuby::Note::FAIL_ON_REDEF
    end

    def on_final
      parse_new_val if @name
    end

    def create_lambda(params, codes)
      eval "-> #{params.join(', ')} {\n#{codes}\n}"
    end

    def tpl_code(txt)
      %Q(<<-hIRJi5_FOSy___\n#{txt}\nhIRJi5_FOSy___\n.chomp)
    end
  end

  # -----------------------------------------------------------------
  # - KRuby::Note::ExtInfo 备注扩展信息
  # -   name    扩展的字段名, Symbol, 请尽量使用小写字母和下划线
  # -   type    扩展字段的类型, Symbol, 枚举值:
  # -               :val 已经计算出来的值类型
  # -               :tpl 模板类型
  # -               :fun 函数类型
  # -   value   扩展字段的值 值类型时是 String|Fixnum|Float, 否则是 Lambda
  # -----------------------------------------------------------------
  class ExtInfo
    attr_reader :name, :type
    def initialize(name, value, type=:val)
      @name = name
      @value = value
      @type = type
    end

    # ---------------------------------------------------------------
    # - 获取 value 模板类型需要调用 Lambda 计算结果
    # ---------------------------------------------------------------
    def value
      type == :tpl ? @value[] : @value
    end
  end

  # -----------------------------------------------------------------
  # - KRuby::Note::DataExt 扩展模块
  # -   用于混入 RPG::BaseItem 等有备注 @note 的类
  # -   提供 init 和 ext 两个方法
  # -----------------------------------------------------------------
  module DataExt
    def init
      @exts = {}
      return unless @note
      return if @note.empty?

      begin
        org, result = parse_note
        @note = org
        @exts = result
      rescue => e
        # TODO KRuby.puts_err e
        raise "处理[#{self.class}|id:#{@id}]的备注时出现错误: #{e}"
      end
    end

    def ext(name)
      item = @exts[name]
      item ? item.value : nil
    end

    private
    def parse_note
      lines = @note.split("\n")
      has_sep = lines.any? { |line| line.start_with?('---') }
      return [@note, {}] unless has_sep || KRuby::Note::USE_DEFAULT

      p = KRuby::Note::Parser.new(self)
      lines.each { |line| p << line.chomp }
      p.do_final
    end

    [
      RPG::BaseItem,
      RPG::Map,
      RPG::Tileset,
    ].each do |clazz|
      clazz.send :include, self
    end
  end
end

# -----------------------------------------------------------------
# - DataManager load_database方法加钩子
# -   用于初始化备注扩展变量
# -----------------------------------------------------------------
class << DataManager
  alias :load_database_krubynote :load_database
  def load_database
    load_database_krubynote
    puts "> 开始解析krubynote"
    start = Time.now
    [
      $data_actors,
      $data_classes,
      $data_skills,
      $data_items,
      $data_weapons,
      $data_armors,
      $data_enemies,
      $data_states,
      $data_tilesets
    ].each do |arr|
      arr.compact.each(&:init)
    end
    ms = (Time.now - start) * 1000
    puts "> krubynote解析完成,耗时 #{ms.to_i}ms"
    puts $data_actors[1].ext(:desc)
  end
end[/pre]





赠品:
使用这个库,整一个自动切换状态的被动技能吧:
(老代码警告,意义不明的写法警告)

  1. # 技能自动状态的简单逻辑,依赖 KRuby::Note
  2. class Game_Battler
  3.   alias refresh_org_autoswitch_skill refresh
  4.   def refresh
  5.     refresh_org_autoswitch_skill
  6.     skills.each { |s| switch_auto_skill(s) }
  7.   end
  8.   
  9.   private
  10.   def switch_skill(id, state)
  11.     @skill_switches ||= {}
  12.     last = @skill_switches[id]
  13.     @skill_switches[id] = state
  14.     state ^ last
  15.   end

  16.   def switch_auto_skill(skill, init = false)
  17.     fn = skill.ext(:switch_condition)
  18.     return unless fn

  19.     res = fn[self]
  20.     return unless switch_skill(skill.id, res) || init

  21.     ids = skill.ext(:switch_states)
  22.     return unless ids

  23.     ids.each do |id|
  24.       next if id == 0
  25.       add = id > 0
  26.       id = -id unless add
  27.       add ^ res ?  remove_state(id) : add_state(id)
  28.     end
  29.   end
  30. end

  31. class Game_Actor
  32.   alias learn_skill_org_autoswitch_skill learn_skill
  33.   def learn_skill(skill_id)
  34.     learn_skill_org_autoswitch_skill(skill_id)
  35.     switch_auto_skill($data_skills[skill_id], true)
  36.   end
  37. end
复制代码

使用:技能里备注
Snipaste_2021-03-15_00-13-49.png




顺便:
模板类型和带#{}符号的值类型会使用HEREDOC的模板代码进行eval,使用的标志为 'hIRJi5_FOSy___' 所有多行文本内出现内容为 'hIRJi5_FOSy___' 的行时解析会出问题(我感觉是不可能出现,但是还是说一声吧)。

评分

参与人数 3赞 +3 收起 理由
wc3215410 + 1 塞糖
负有尽 + 1 塞糖
WLian + 1

查看全部评分

java.lang.NullPointerException
您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.

在本版发帖返回顶部