查看: 32|回复: 1

[原创发布] 射击系统脚本

[复制链接]
梦石
1
星屑
1219
在线时间
60 小时
注册时间
2023-9-8
回帖
20
发表于 5 小时前 | 显示全部楼层 |阅读模式

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

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

×
  1. #==============================================================================
  2. # ▼ 射击系统脚本 (Shooting System)
  3. # 作者:Ruigi
  4. # -- 放在 Main 之前
  5. #==============================================================================
  6. # 标签(写在技能/物品的「备注」栏):
  7. #   <gun>                  标记为射击类
  8. #   <guntime:数字>         射击模式持续时间(帧),默认 300
  9. #   <bullet:物品编号>      每次射击消耗的物品ID
  10. #   <kind:动画编号>        持枪状态动画(持续循环播放)
  11. #   <shoot:动画编号>       射击命中敌人时对目标播放的动画
  12. #   <Impact:动画编号>      准星位置播放的弹孔/火花(无论命中与否都播放)
  13. #   <Precision:数字>       准星平滑度(1 = 不滑,越大越滑越难瞄)
  14. #   <Automatic:秒数>       自动挡模式(支持小数)
  15. #   <Xrecoil:幅度>         水平后坐力(像素)
  16. #   <Yrecoil:幅度>         垂直后坐力(像素)
  17. #
  18. # ★ 射击技能的「使用效果」要设置伤害公式(如 a.atk * 4 - b.def * 2)
  19. #==============================================================================

  20. module Shooting_Config
  21.   # ---- 基础 ----
  22.   CROSSHAIR_IMAGE       = "crosshair"
  23.   CROSSHAIR_SIZE        = [48, 48]
  24.   DEFAULT_GUNTIME       = 300
  25.   SHOOT_KEY             = :C
  26.   HIT_SOUND             = "gun1"
  27.   EXIT_KEYS             = [:B]
  28.   USE_MOUSE_LEFT        = true
  29.   SHOT_COOLDOWN         = 8
  30.   KEYBOARD_SPEED        = 6
  31.   CHECK_BULLET_ON_ENTER = true
  32.   HIT_RECT_PADDING      = 0

  33.   # ---- 爆头机制 ----
  34.   HEADSHOT_ENABLED    = true
  35.   HEADSHOT_RATIO      = 0.35
  36.   HEADSHOT_MULTIPLIER = 2.0
  37.   HEADSHOT_SOUND      = "Critical"
  38.   HEADSHOT_TEXT       = "爆头!"

  39.   # ---- 准星平滑 ----
  40.   DEFAULT_PRECISION = 3

  41.   # ---- 后坐力 ----
  42.   RECOIL_DECAY = 0.82

  43.   # ---- 弹孔 / 空枪反馈 ----
  44.   IMPACT_OFFSET_X = 0
  45.   IMPACT_OFFSET_Y = 0
  46.   IMPACT_Z        = 8000

  47.   # ★ 每次射击(命中或未命中)都重播一次 <kind:> 动画
  48.   #   true  = 每次开火都重播(更有"枪口抖动"感)
  49.   #   false = 只在未命中时重播(默认,避免命中时视觉过乱)
  50.   REPLAY_KIND_ON_HIT = false

  51.   # ---- 子弹 HUD ----
  52.   HUD_X             = 0
  53.   HUD_Y             = 0
  54.   HUD_WIDTH         = 200
  55.   HUD_Z             = 9000
  56.   HUD_SHOW_NAME     = true
  57.   HUD_LOW_COLOR     = true
  58.   HUD_LOW_THRESHOLD = 3

  59.   # ---- 自动挡 ----
  60.   AUTO_HUD_LABEL = "[自动]"

  61.   # ---- 调试 ----
  62.   DEBUG_AUTO = false
  63. end

  64. #==============================================================================
  65. # ■ Spriteset_Battle - 开放精灵数组读取
  66. #==============================================================================
  67. class Spriteset_Battle
  68.   attr_reader :actor_sprites, :enemy_sprites
  69. end

  70. #==============================================================================
  71. # ■ NoteNum - 宽容的标签数字解析
  72. #==============================================================================
  73. module NoteNum
  74.   FULLWIDTH_DIGITS = {
  75.     "0"=>"0","1"=>"1","2"=>"2","3"=>"3","4"=>"4",
  76.     "5"=>"5","6"=>"6","7"=>"7","8"=>"8","9"=>"9"
  77.   }

  78.   def self.extract(note, tag)
  79.     m = note.match(/<#{tag}\s*:\s*([^>\r\n]*)/i)
  80.     return nil unless m
  81.     raw = m[1].strip
  82.     num = raw[/[0-90-9..,,]+/]
  83.     return nil unless num
  84.     s = num.each_char.map { |c| FULLWIDTH_DIGITS[c] || c }.join
  85.     s = s.tr(".,,", ".")
  86.     s.to_f
  87.   end
  88. end

  89. #==============================================================================
  90. # ■ RPG::UsableItem - 标签解析
  91. #==============================================================================
  92. class RPG::UsableItem < RPG::BaseItem
  93.   def shooting?
  94.     return @shooting unless @shooting.nil?
  95.     @shooting = !note[/<gun>/i].nil?
  96.   end

  97.   def gun_time
  98.     return @gun_time unless @gun_time.nil?
  99.     v = NoteNum.extract(note, "guntime")
  100.     @gun_time = v ? v.to_i : Shooting_Config::DEFAULT_GUNTIME
  101.   end

  102.   def bullet_id
  103.     return @bullet_id unless @bullet_id.nil?
  104.     v = NoteNum.extract(note, "bullet")
  105.     @bullet_id = v ? v.to_i : 0
  106.   end

  107.   def kind_animation_id
  108.     return @kind_anim unless @kind_anim.nil?
  109.     v = NoteNum.extract(note, "kind")
  110.     @kind_anim = v ? v.to_i : 0
  111.   end

  112.   def shoot_animation_id
  113.     return @shoot_anim unless @shoot_anim.nil?
  114.     v = NoteNum.extract(note, "shoot")
  115.     @shoot_anim = v ? v.to_i : 0
  116.   end

  117.   def impact_animation_id
  118.     return @impact_anim unless @impact_anim.nil?
  119.     v = NoteNum.extract(note, "Impact")
  120.     @impact_anim = v ? v.to_i : 0
  121.   end

  122.   def precision
  123.     return @precision unless @precision.nil?
  124.     v = NoteNum.extract(note, "Precision")
  125.     @precision = v ? v.to_i : Shooting_Config::DEFAULT_PRECISION
  126.   end

  127.   def automatic_interval
  128.     return @auto_interval unless @auto_interval.nil?
  129.     v = NoteNum.extract(note, "Automatic")
  130.     @auto_interval = v && v > 0 ? v : 0.0
  131.   end

  132.   def automatic_interval_frames
  133.     return 0 if automatic_interval <= 0
  134.     [(automatic_interval * 60).round, 1].max
  135.   end

  136.   def automatic?
  137.     automatic_interval > 0
  138.   end

  139.   def x_recoil
  140.     return @x_recoil unless @x_recoil.nil?
  141.     v = NoteNum.extract(note, "Xrecoil")
  142.     @x_recoil = v && v > 0 ? v : 0.0
  143.   end

  144.   def y_recoil
  145.     return @y_recoil unless @y_recoil.nil?
  146.     v = NoteNum.extract(note, "Yrecoil")
  147.     @y_recoil = v && v > 0 ? v : 0.0
  148.   end
  149. end

  150. #==============================================================================
  151. # ■ Mouse - 鼠标辅助
  152. #==============================================================================
  153. module Mouse
  154.   WIN32_AVAILABLE = defined?(::Win32API) ? true : false

  155.   if WIN32_AVAILABLE
  156.     @get_cursor_pos   = ::Win32API.new("user32", "GetCursorPos",    "P",  "I")
  157.     @screen_to_client = ::Win32API.new("user32", "ScreenToClient",  "LP", "I")
  158.     @find_window      = ::Win32API.new("user32", "FindWindow",      "PP", "L")
  159.     @get_async_key    = ::Win32API.new("user32", "GetAsyncKeyState", "i", "i")
  160.   end

  161.   @cached_pos = [320, 240]
  162.   @prev_left  = false

  163.   def self.available?
  164.     WIN32_AVAILABLE
  165.   end

  166.   def self.find_hwnd
  167.     return 0 unless WIN32_AVAILABLE
  168.     ["RGSS Player", "RGSS3 Player", "RPG Maker VX Ace", "Game"].each do |t|
  169.       hwnd = @find_window.call(nil, t)
  170.       return hwnd if hwnd != 0
  171.     end
  172.     0
  173.   end

  174.   def self.position
  175.     return @cached_pos unless WIN32_AVAILABLE
  176.     hwnd = find_hwnd
  177.     return @cached_pos if hwnd == 0
  178.     pos = [0, 0].pack("ll")
  179.     @get_cursor_pos.call(pos)
  180.     @screen_to_client.call(hwnd, pos)
  181.     @cached_pos = pos.unpack("ll")
  182.   end

  183.   def self.left_held?
  184.     return false unless WIN32_AVAILABLE
  185.     (@get_async_key.call(0x01) & 0x8000) != 0
  186.   end

  187.   def self.left_click?
  188.     now = left_held?
  189.     result = now && !@prev_left
  190.     @prev_left = now
  191.     result
  192.   end
  193. end

  194. #==============================================================================
  195. # ■ Sprite_Crosshair - 准星精灵
  196. #==============================================================================
  197. class Sprite_Crosshair < Sprite
  198.   attr_accessor :precision

  199.   def initialize(viewport = nil)
  200.     super(viewport)
  201.     @active    = false
  202.     @precision = Shooting_Config::DEFAULT_PRECISION
  203.     @target_x  = 320
  204.     @target_y  = 240
  205.     @current_x = 320
  206.     @current_y = 240
  207.     @recoil_x  = 0.0
  208.     @recoil_y  = 0.0

  209.     begin
  210.       self.bitmap = Cache.picture(Shooting_Config::CROSSHAIR_IMAGE)
  211.     rescue
  212.       self.bitmap = nil
  213.     end

  214.     if self.bitmap
  215.       self.zoom_x = Shooting_Config::CROSSHAIR_SIZE[0].to_f / self.bitmap.width
  216.       self.zoom_y = Shooting_Config::CROSSHAIR_SIZE[1].to_f / self.bitmap.height
  217.     else
  218.       self.bitmap = Bitmap.new(32, 32)
  219.       c = Color.new(255, 0, 0)
  220.       self.bitmap.draw_line(16, 0,  16, 12, c)
  221.       self.bitmap.draw_line(16, 20, 16, 32, c)
  222.       self.bitmap.draw_line(0,  16, 12, 16, c)
  223.       self.bitmap.draw_line(20, 16, 32, 16, c)
  224.       self.bitmap.draw_line(6,  6,  12, 12, c)
  225.       self.bitmap.draw_line(26, 6,  20, 12, c)
  226.       self.bitmap.draw_line(6,  26, 12, 20, c)
  227.       self.bitmap.draw_line(26, 26, 20, 20, c)
  228.     end

  229.     self.visible = false
  230.     self.z = 9999
  231.   end

  232.   def activate
  233.     @active = true
  234.     self.visible = true
  235.     if Mouse.available?
  236.       @target_x, @target_y = Mouse.position
  237.     else
  238.       @target_x = 320
  239.       @target_y = 240
  240.     end
  241.     @current_x = @target_x
  242.     @current_y = @target_y
  243.     @recoil_x  = 0.0
  244.     @recoil_y  = 0.0
  245.     update_position
  246.   end

  247.   def deactivate
  248.     @active = false
  249.     self.visible = false
  250.   end

  251.   def active?
  252.     @active
  253.   end

  254.   def screen_pos
  255.     [@current_x + @recoil_x, @current_y + @recoil_y]
  256.   end

  257.   def add_recoil(x_amp, y_amp)
  258.     @recoil_x += (rand * 2.0 - 1.0) * x_amp if x_amp && x_amp > 0
  259.     @recoil_y += (rand * 2.0 - 1.0) * y_amp if y_amp && y_amp > 0
  260.   end

  261.   def update_position
  262.     return unless @active

  263.     if Mouse.available?
  264.       @target_x, @target_y = Mouse.position
  265.     else
  266.       @target_x -= Shooting_Config::KEYBOARD_SPEED if Input.press?(:LEFT)
  267.       @target_x += Shooting_Config::KEYBOARD_SPEED if Input.press?(:RIGHT)
  268.       @target_y -= Shooting_Config::KEYBOARD_SPEED if Input.press?(:UP)
  269.       @target_y += Shooting_Config::KEYBOARD_SPEED if Input.press?(:DOWN)
  270.       @target_x = [[@target_x, 0].max, 640].min
  271.       @target_y = [[@target_y, 0].max, 480].min
  272.     end

  273.     factor = 1.0 / [@precision, 1].max
  274.     @current_x += (@target_x - @current_x) * factor
  275.     @current_y += (@target_y - @current_y) * factor
  276.     @current_x = @target_x if (@target_x - @current_x).abs < 0.5
  277.     @current_y = @target_y if (@target_y - @current_y).abs < 0.5

  278.     @recoil_x *= Shooting_Config::RECOIL_DECAY
  279.     @recoil_y *= Shooting_Config::RECOIL_DECAY
  280.     @recoil_x = 0.0 if @recoil_x.abs < 0.1
  281.     @recoil_y = 0.0 if @recoil_y.abs < 0.1

  282.     draw_x = @current_x + @recoil_x
  283.     draw_y = @current_y + @recoil_y

  284.     w = self.bitmap ? self.bitmap.width  * zoom_x : 0
  285.     h = self.bitmap ? self.bitmap.height * zoom_y : 0
  286.     self.x = draw_x - w / 2
  287.     self.y = draw_y - h / 2
  288.   end
  289. end

  290. #==============================================================================
  291. # ■ Sprite_Impact - 弹孔动画精灵
  292. #==============================================================================
  293. class Sprite_Impact < Sprite_Base
  294.   def initialize(viewport = nil)
  295.     super(viewport)
  296.     self.visible = false
  297.     self.z = Shooting_Config::IMPACT_Z
  298.     @playing = false
  299.   end

  300.   def play(x, y, animation_id)
  301.     return if animation_id.nil? || animation_id <= 0
  302.     animation = $data_animations[animation_id]
  303.     return unless animation
  304.     self.x = x + Shooting_Config::IMPACT_OFFSET_X
  305.     self.y = y + Shooting_Config::IMPACT_OFFSET_Y
  306.     @playing = true
  307.     self.visible = true
  308.     start_animation(animation)
  309.   end

  310.   def update
  311.     super
  312.     if @playing && !animation?
  313.       @playing = false
  314.       self.visible = false
  315.     end
  316.   end
  317. end

  318. #==============================================================================
  319. # ■ Window_BulletCounter - 左上角子弹数量 HUD
  320. #==============================================================================
  321. class Window_BulletCounter < Window_Base
  322.   def initialize
  323.     super(Shooting_Config::HUD_X, Shooting_Config::HUD_Y,
  324.           Shooting_Config::HUD_WIDTH, fitting_height(1))
  325.     self.z = Shooting_Config::HUD_Z
  326.     self.visible = false
  327.     self.openness = 255
  328.     [url=home.php?mod=space&uid=363652]@Bullet[/url]    = nil
  329.     @count     = 0
  330.     @automatic = false
  331.   end

  332.   def show_bullet(item, automatic = false)
  333.     return unless item
  334.     @bullet = item
  335.     @automatic = automatic
  336.     refresh
  337.     self.visible = true
  338.   end

  339.   def hide_bullet
  340.     self.visible = false
  341.     @bullet = nil
  342.   end

  343.   def update_count
  344.     return unless @bullet
  345.     new_count = $game_party.item_number(@bullet)
  346.     return if new_count == @count
  347.     @count = new_count
  348.     refresh
  349.   end

  350.   def refresh
  351.     contents.clear
  352.     return unless @bullet

  353.     @count = $game_party.item_number(@bullet)
  354.     draw_icon(@bullet.icon_index, 0, 0)

  355.     offset = 28
  356.     if @automatic && Shooting_Config::AUTO_HUD_LABEL && !Shooting_Config::AUTO_HUD_LABEL.empty?
  357.       change_color(Color.new(120, 200, 255))
  358.       draw_text(offset, 0, 48, line_height, Shooting_Config::AUTO_HUD_LABEL)
  359.       change_color(normal_color)
  360.       offset += 48
  361.     end

  362.     if Shooting_Config::HUD_SHOW_NAME
  363.       change_color(normal_color)
  364.       draw_text(offset, 0, 120 - (offset - 28), line_height, @bullet.name)
  365.       count_x = 148
  366.       count_w = contents.width - count_x
  367.     else
  368.       count_x = offset
  369.       count_w = contents.width - count_x
  370.     end

  371.     if Shooting_Config::HUD_LOW_COLOR && @count <= Shooting_Config::HUD_LOW_THRESHOLD
  372.       change_color(Color.new(255, 80, 80))
  373.     else
  374.       change_color(system_color)
  375.     end
  376.     draw_text(count_x, 0, count_w, line_height, "x#{@count}", 2)
  377.     change_color(normal_color)
  378.   end
  379. end

  380. #==============================================================================
  381. # ■ Game_Battler - 射击状态管理 + 爆头伤害加成
  382. #==============================================================================
  383. class Game_Battler < Game_BattlerBase
  384.   attr_accessor :gun_active
  385.   attr_accessor :gun_time_remaining
  386.   attr_accessor :gun_item
  387.   attr_accessor :shooting_headshot

  388.   alias shooting_init_game_battler initialize
  389.   def initialize
  390.     shooting_init_game_battler
  391.     @gun_active = false
  392.     @gun_time_remaining = 0
  393.     @gun_item = nil
  394.     @shooting_headshot = false
  395.   end

  396.   def start_gun(item)
  397.     @gun_active = true
  398.     @gun_time_remaining = item.gun_time
  399.     @gun_item = item
  400.   end

  401.   def stop_gun
  402.     @gun_active = false
  403.     @gun_time_remaining = 0
  404.     @gun_item = nil
  405.   end

  406.   def update_gun
  407.     return unless @gun_active
  408.     @gun_time_remaining -= 1
  409.     stop_gun if @gun_time_remaining <= 0
  410.   end

  411.   def gun_active?
  412.     @gun_active
  413.   end

  414.   alias shooting_make_damage_value make_damage_value
  415.   def make_damage_value(user, item)
  416.     shooting_make_damage_value(user, item)
  417.     if @shooting_headshot && Shooting_Config::HEADSHOT_ENABLED && @result.hp_damage > 0
  418.       multiplier = Shooting_Config::HEADSHOT_MULTIPLIER
  419.       @result.hp_damage = (@result.hp_damage * multiplier).to_i
  420.     end
  421.   end
  422. end

  423. #==============================================================================
  424. # ■ Game_Enemy - 命中判定后备矩形
  425. #==============================================================================
  426. # 注意:不覆盖 screen_x / screen_y / screen_z —— 保留引擎原版定义,
  427. #       让 $data_troops 里手动摆放的敌人坐标继续生效。
  428. #       这里只新增一个 screen_rect 后备方法,仅在找不到精灵时使用。
  429. class Game_Enemy < Game_Battler
  430.   def screen_rect
  431.     # 引擎原版的 screen_x / screen_y 是精灵的绘制坐标(左上角)
  432.     # 真实尺寸由精灵决定,找不到精灵时给一个宽松的默认矩形
  433.     Rect.new(screen_x, screen_y, 64, 64)
  434.   end
  435. end

  436. #==============================================================================
  437. # ■ Scene_Battle - 射击模式集成
  438. #==============================================================================
  439. class Scene_Battle < Scene_Base

  440.   alias shooting_start start
  441.   def start
  442.     shooting_start
  443.     @shooting_mode = false
  444.     @shooting_item = nil
  445.     @crosshair     = Sprite_Crosshair.new
  446.     @impact        = Sprite_Impact.new
  447.     @shot_cooldown = 0
  448.     @bullet_window = Window_BulletCounter.new
  449.     @dbg_counter   = 0
  450.   end

  451.   alias shooting_terminate terminate
  452.   def terminate
  453.     shooting_terminate
  454.     @bullet_window.dispose if @bullet_window
  455.     @impact.dispose        if @impact
  456.   end

  457.   alias shooting_update update
  458.   def update
  459.     if @shooting_mode
  460.       update_basic
  461.       @spriteset.update if @spriteset
  462.       @impact.update    if @impact
  463.       update_shooting
  464.     else
  465.       shooting_update
  466.       @impact.update    if @impact
  467.       update_shooting
  468.     end
  469.   end

  470.   #--------------------------------------------------------------------------
  471.   # ● 每帧射击相关更新
  472.   #--------------------------------------------------------------------------
  473.   def update_shooting
  474.     @crosshair.update_position if @crosshair.active?
  475.     @shot_cooldown -= 1 if @shot_cooldown > 0

  476.     $game_party.members.each { |m| m.update_gun }
  477.     $game_troop.members.each { |m| m.update_gun }

  478.     @bullet_window.update_count if @shooting_mode

  479.     return unless @shooting_mode

  480.     update_gun_animation

  481.     if Shooting_Config::DEBUG_AUTO
  482.       @dbg_counter += 1
  483.       if @dbg_counter % 30 == 0
  484.         p "[AUTO DBG] automatic?=#{auto_mode?} interval=#{@shooting_item.automatic_interval rescue 'nil'} " +
  485.           "frames=#{@shooting_item.automatic_interval_frames rescue 'nil'} " +
  486.           "cooldown=#{@shot_cooldown} " +
  487.           "kbd_press=#{Input.press?(Shooting_Config::SHOOT_KEY)} " +
  488.           "mouse_held=#{Shooting_Config::USE_MOUSE_LEFT && Mouse.available? ? Mouse.left_held? : false}"
  489.       end
  490.     end

  491.     if @shooting_item && $game_party.members.none? { |m| m.gun_active? }
  492.       exit_shooting_mode
  493.       return
  494.     end

  495.     if @shot_cooldown <= 0
  496.       if auto_mode?
  497.         perform_shot if shoot_key_held?
  498.       else
  499.         perform_shot if shoot_key_triggered?
  500.       end
  501.     end

  502.     Shooting_Config::EXIT_KEYS.each do |key|
  503.       if Input.trigger?(key)
  504.         exit_shooting_mode
  505.         return
  506.       end
  507.     end
  508.   end

  509.   def auto_mode?
  510.     @shooting_item && @shooting_item.automatic?
  511.   end

  512.   def shoot_key_held?
  513.     return true if Input.press?(Shooting_Config::SHOOT_KEY)
  514.     if Shooting_Config::USE_MOUSE_LEFT && Mouse.available?
  515.       return true if Mouse.left_held?
  516.     end
  517.     false
  518.   end

  519.   def shoot_key_triggered?
  520.     return true if Input.trigger?(Shooting_Config::SHOOT_KEY)
  521.     if Shooting_Config::USE_MOUSE_LEFT && Mouse.available?
  522.       return true if Mouse.left_click?
  523.     end
  524.     false
  525.   end

  526.   def shot_interval
  527.     return Shooting_Config::SHOT_COOLDOWN unless @shooting_item
  528.     frames = @shooting_item.automatic_interval_frames
  529.     frames > 0 ? frames : Shooting_Config::SHOT_COOLDOWN
  530.   end

  531.   #--------------------------------------------------------------------------
  532.   # ● 拦截 process_action
  533.   #--------------------------------------------------------------------------
  534.   alias shooting_process_action process_action
  535.   def process_action
  536.     return if scene_changing?
  537.     if !@subject || !@subject.current_action
  538.       @subject = BattleManager.next_subject
  539.     end
  540.     return turn_end unless @subject

  541.     if @subject.current_action
  542.       @subject.current_action.prepare
  543.       if @subject.current_action.valid?
  544.         @status_window.open
  545.         item = @subject.current_action.item

  546.         if item && item.shooting? && @subject.is_a?(Game_Actor)
  547.           if Shooting_Config::CHECK_BULLET_ON_ENTER && !has_bullet?(item)
  548.             Sound.play_buzzer
  549.             @log_window.add_text(sprintf("%s 的弹药不足!", @subject.name)) if @log_window
  550.             @subject.remove_current_action
  551.             @status_window.close rescue nil
  552.             return
  553.           end

  554.           @subject.use_item(item)
  555.           refresh_status
  556.           @subject.remove_current_action
  557.           enter_shooting_mode(item)
  558.           return
  559.         end

  560.         execute_action
  561.       end
  562.       @subject.remove_current_action
  563.     end
  564.     process_action_end unless @subject.current_action
  565.   end

  566.   def has_bullet?(item)
  567.     return true if item.bullet_id <= 0
  568.     bullet = $data_items[item.bullet_id]
  569.     return false if bullet.nil?
  570.     $game_party.item_number(bullet) > 0
  571.   end

  572.   #--------------------------------------------------------------------------
  573.   # ● 进入 / 退出射击模式
  574.   #--------------------------------------------------------------------------
  575.   def enter_shooting_mode(item)
  576.     @shooting_mode = true
  577.     @shooting_item = item
  578.     @shot_cooldown = 0
  579.     @crosshair.precision = item.precision
  580.     @crosshair.activate
  581.     $game_party.members.each { |m| m.start_gun(item) }
  582.     play_actor_animation(item.kind_animation_id)

  583.     if item.bullet_id > 0
  584.       bullet = $data_items[item.bullet_id]
  585.       @bullet_window.show_bullet(bullet, item.automatic?) if bullet
  586.     end

  587.     if Shooting_Config::DEBUG_AUTO
  588.       p "=== 进入射击模式:#{item.name} ==="
  589.       p "  shooting?     = #{item.shooting?}"
  590.       p "  bullet_id     = #{item.bullet_id}"
  591.       p "  precision     = #{item.precision}"
  592.       p "  automatic?    = #{item.automatic?}"
  593.       p "  auto_interval = #{item.automatic_interval}"
  594.       p "  auto_frames   = #{item.automatic_interval_frames}"
  595.       p "  impact_anim   = #{item.impact_animation_id}"
  596.       p "  kind_anim     = #{item.kind_animation_id}"
  597.       p "  x_recoil      = #{item.x_recoil}"
  598.       p "  y_recoil      = #{item.y_recoil}"
  599.       p "  note          = #{item.note.inspect}"
  600.     end
  601.   end

  602.   def exit_shooting_mode
  603.     @shooting_mode = false
  604.     @shooting_item = nil
  605.     @crosshair.deactivate
  606.     $game_party.members.each { |m| m.stop_gun }
  607.     @bullet_window.hide_bullet
  608.   end

  609.   #--------------------------------------------------------------------------
  610.   # ● 执行射击
  611.   #--------------------------------------------------------------------------
  612.   def perform_shot
  613.     return unless @shooting_item

  614.     bullet_id = @shooting_item.bullet_id
  615.     if bullet_id > 0
  616.       bullet_item = $data_items[bullet_id]
  617.       if bullet_item.nil? || $game_party.item_number(bullet_item) <= 0
  618.         Sound.play_buzzer
  619.         @log_window.add_text("弹药耗尽!") if @log_window
  620.         exit_shooting_mode
  621.         return
  622.       end
  623.     end

  624.     # 拍快照:本次射击用当前准星位置判定(含已有后坐力)
  625.     shot_x, shot_y = @crosshair.screen_pos

  626.     # 扣弹药
  627.     if bullet_id > 0
  628.       $game_party.lose_item($data_items[bullet_id], 1)
  629.     end

  630.     # 准星位置播放弹孔/火花动画(无论命中与否)
  631.     impact_id = @shooting_item.impact_animation_id
  632.     if impact_id > 0 && @impact
  633.       @impact.play(shot_x, shot_y, impact_id)
  634.     end

  635.     # 施加后坐力(视觉立即偏移,影响下一发)
  636.     @crosshair.add_recoil(@shooting_item.x_recoil, @shooting_item.y_recoil)

  637.     # 用快照位置判定目标
  638.     target = find_enemy_at(shot_x, shot_y)

  639.     # =========================================================
  640.     # 空枪:播放开火音效 + 重播 kind 动画(开火动作反馈)
  641.     # =========================================================
  642.     unless target
  643.       Sound.play_use_skill rescue nil

  644.       # ★ 未命中时重新播放角色身上的持枪动画
  645.       play_actor_animation(@shooting_item.kind_animation_id)

  646.       @log_window.add_text("未命中...") if @log_window
  647.       @shot_cooldown = shot_interval
  648.       if bullet_id > 0 && $game_party.item_number($data_items[bullet_id]) <= 0
  649.         exit_shooting_mode
  650.       end
  651.       return
  652.     end

  653.     # =========================================================
  654.     # 命中:敌人播 <shoot:>,可选重播 kind
  655.     # =========================================================
  656.     headshot = headshot_hit_at?(target, shot_x, shot_y)

  657.     Sound.play_use_skill rescue nil

  658.     # 可选:命中时也重播 kind 动画(由配置控制)
  659.     if Shooting_Config::REPLAY_KIND_ON_HIT
  660.       play_actor_animation(@shooting_item.kind_animation_id)
  661.     end

  662.     play_enemy_animation(target, @shooting_item.shoot_animation_id)

  663.     if Shooting_Config::HIT_SOUND && !Shooting_Config::HIT_SOUND.empty?
  664.       Audio.se_play("Audio/SE/" + Shooting_Config::HIT_SOUND, 80, 100) rescue nil
  665.     end

  666.     if headshot
  667.       if Shooting_Config::HEADSHOT_SOUND && !Shooting_Config::HEADSHOT_SOUND.empty?
  668.         Audio.se_play("Audio/SE/" + Shooting_Config::HEADSHOT_SOUND, 90, 100) rescue nil
  669.       end
  670.       if @log_window && Shooting_Config::HEADSHOT_TEXT && !Shooting_Config::HEADSHOT_TEXT.empty?
  671.         @log_window.add_text(Shooting_Config::HEADSHOT_TEXT) rescue nil
  672.       end
  673.     end

  674.     target.shooting_headshot = headshot
  675.     apply_shot_effect(target, @shooting_item)
  676.     target.shooting_headshot = false

  677.     @shot_cooldown = shot_interval

  678.     if bullet_id > 0 && $game_party.item_number($data_items[bullet_id]) <= 0
  679.       exit_shooting_mode
  680.     end
  681.   end

  682.   #--------------------------------------------------------------------------
  683.   # ● 命中检测 / 爆头判定
  684.   #--------------------------------------------------------------------------
  685.   def find_enemy_at(mx, my)
  686.     $game_troop.alive_members.each do |enemy|
  687.       sprite = find_enemy_sprite(enemy)
  688.       rect   = enemy_hit_rect(enemy, sprite)
  689.       return enemy if mx >= rect.x && mx <= rect.x + rect.width &&
  690.                       my >= rect.y && my <= rect.y + rect.height
  691.     end
  692.     nil
  693.   end

  694.   def enemy_hit_rect(enemy, sprite)
  695.     pad = Shooting_Config::HIT_RECT_PADDING
  696.     if sprite && sprite.bitmap
  697.       w = sprite.bitmap.width  * sprite.zoom_x
  698.       h = sprite.bitmap.height * sprite.zoom_y
  699.       x = sprite.x - sprite.ox * sprite.zoom_x - pad
  700.       y = sprite.y - sprite.oy * sprite.zoom_y - pad
  701.       Rect.new(x, y, w + pad * 2, h + pad * 2)
  702.     else
  703.       r = enemy.screen_rect
  704.       Rect.new(r.x - pad, r.y - pad, r.width + pad * 2, r.height + pad * 2)
  705.     end
  706.   end

  707.   def headshot_hit_at?(enemy, mx, my)
  708.     return false unless Shooting_Config::HEADSHOT_ENABLED
  709.     sprite = find_enemy_sprite(enemy)
  710.     rect   = enemy_hit_rect(enemy, sprite)
  711.     return false unless mx >= rect.x && mx <= rect.x + rect.width
  712.     return false unless my >= rect.y && my <= rect.y + rect.height
  713.     head_bottom = rect.y + rect.height * Shooting_Config::HEADSHOT_RATIO
  714.     my <= head_bottom
  715.   end

  716.   def apply_shot_effect(target, item)
  717.     user = $game_party.members[0]
  718.     return unless user
  719.     target.item_apply(user, item)
  720.     if @log_window
  721.       @log_window.display_action_results(target, item) rescue nil
  722.     end
  723.   end

  724.   def play_actor_animation(anim_id)
  725.     return if anim_id <= 0 || !@spriteset
  726.     animation = $data_animations[anim_id]
  727.     return unless animation
  728.     $game_party.members.each do |actor|
  729.       sprite = find_actor_sprite(actor)
  730.       sprite.start_animation(animation) if sprite
  731.     end
  732.   end

  733.   def play_enemy_animation(target, anim_id)
  734.     return if anim_id <= 0 || !@spriteset
  735.     animation = $data_animations[anim_id]
  736.     return unless animation
  737.     sprite = find_enemy_sprite(target)
  738.     sprite.start_animation(animation) if sprite
  739.   end

  740.   def update_gun_animation
  741.     return unless @shooting_item
  742.     anim_id = @shooting_item.kind_animation_id
  743.     return if anim_id <= 0 || !@spriteset
  744.     animation = $data_animations[anim_id]
  745.     return unless animation

  746.     $game_party.members.each do |actor|
  747.       sprite = find_actor_sprite(actor)
  748.       next unless sprite
  749.       next if sprite.animation?
  750.       sprite.start_animation(animation)
  751.     end
  752.   end

  753.   def find_actor_sprite(actor)
  754.     return nil unless @spriteset
  755.     return nil unless @spriteset.respond_to?(:actor_sprites)
  756.     @spriteset.actor_sprites.find { |s| s.battler == actor }
  757.   end

  758.   def find_enemy_sprite(target)
  759.     return nil unless @spriteset
  760.     return nil unless @spriteset.respond_to?(:enemy_sprites)
  761.     @spriteset.enemy_sprites.find { |s| s.battler == target }
  762.   end
  763. end

  764. #==============================================================================
  765. # ■ 战斗窗口中射击技能/物品的图标标记
  766. #==============================================================================
  767. class Window_BattleSkill < Window_SkillList
  768.   alias shooting_draw_item draw_item
  769.   def draw_item(index)
  770.     shooting_draw_item(index)
  771.     skill = @data[index]
  772.     return unless skill
  773.     if skill.shooting?
  774.       rect = item_rect(index)
  775.       draw_icon(132, rect.x + rect.width - 24, rect.y, true) rescue nil
  776.     end
  777.   end
  778. end

  779. class Window_BattleItem < Window_ItemList
  780.   alias shooting_draw_item draw_item
  781.   def draw_item(index)
  782.     shooting_draw_item(index)
  783.     item = @data[index]
  784.     return unless item
  785.     if item.shooting?
  786.       rect = item_rect(index)
  787.       draw_icon(132, rect.x + rect.width - 24, rect.y, true) rescue nil
  788.     end
  789.   end
  790. end
复制代码

tzskx-qy3a6.gif
cvwhc-b96v5.gif
@5MY}AKSZNK])~1FE_HG@BP.png
[AIS}7P3NDET~T7D75E8G(H.png
梦石
1
星屑
1219
在线时间
60 小时
注册时间
2023-9-8
回帖
20
 楼主| 发表于 5 小时前 | 显示全部楼层
本帖最后由 Ruigi 于 2026-10-12 01:11 编辑

以下是使用方法。。。
NRYK4O{NX@}Y12F%(T`}(DD.png
W3UC4RA0S_HF@APA7N179J2.png
)3$S6SF[M5EGC[ABSRN%5`W.png
回复

使用道具 举报

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

本版积分规则

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

在本版发帖返回顶部