查看: 851|回复: 0

[原创发布] 仿Undertale 风格敌人对话气泡

[复制链接]
梦石
1
星屑
1184
在线时间
59 小时
注册时间
2023-9-8
回帖
19
发表于 2026-5-11 02:36:14 | 显示全部楼层 |阅读模式

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

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

×
[pre lang="ruby"]#==============================================================================
# ■ Undertale 风格敌人对话气泡
#==============================================================================
#作者:ruigi
#==============================================================================
# ■ 使用说明
#==============================================================================
# 【1】在敌人备注中写入对话标签
#   格式:<对话:你的台词>
#   如:<对话:敢不敢跟我比划比划!>
#   可写入多行,每次随机抽取一句。
#
# 【2】手动换行(仅支持事件脚本)
#   使用 <p> 标签,例如:<对话:第一行<p>第二行>
#   自动换行已启用,超宽文字会自动折行。
#
# 【3】事件脚本指令(在战斗事件的[脚本]中使用)
#   set_enemy_dialogue(索引, "台词")   # 为敌人设置自定义对话(下次行动时显示)
#   force_enemy_dialogue(索引)         # 立即强制显示该敌人的对话气泡
#   clear_enemy_dialogue(索引)         # 清除自定义对话,恢复从备注随机读取
#   dismiss_enemy_dialogue             # 立即关闭当前显示的气泡
#
# 【4】可调参数(见模块 Enemy_Dialogue)
#   ENABLED           : 总开关(false 完全关闭功能)
#   TEXT_TYPEWRITER   : 打字机效果开关
#   TYPE_SPEED        : 打字速度(每 N 帧显示一个字)
#   EXTRA_PAUSE      : 打字完成后额外停留帧数
#   DELAY_ACTION     : 是否等气泡消失后再发动攻击
#   SIMULTANEOUS_BUBBLES        : 是否所有敌人同时显示气泡
#   SIMULTANEOUS_BUBBLES_WAIT   : 全体气泡是否等待结束后才继续行动
#   REPEAT_DIALOGUE : 是否每回合都可能说话
#   Z 键(确认键)可跳过当前正在显示的对话(打字或停留)。
#
# 【5】资源文件
#   请在 Graphics/System/ 下放置气泡图片,文件名见 DIALOGUE_BUBBLE_IMAGE。
#==============================================================================
module Enemy_Dialogue
  ENABLED                = true
  DIALOGUE_BUBBLE_IMAGE  = "Enemy_Dialogue_Bubble"
  DURATION_IN_FRAMES     = 200

  X_OFFSET   = -30
  Y_OFFSET   = -290
  Z_VALUE    = 100

  TEXT_COLOR        = Color.new(255, 255, 255, 255)
  TEXT_SIZE         = 20
  TEXT_BOLD         = true
  TEXT_SHADOW_COLOR = Color.new(0, 0, 0, 160)

  TEXT_TYPEWRITER  = true
  TYPE_SPEED       = 2
  EXTRA_PAUSE      = 40

  AUTO_WRAP = true
  MANUAL_BREAK_TAG = /<p>/i
  TEXT_PADDING_LEFT  = 12
  TEXT_PADDING_RIGHT = 12
  LINE_SPACING = 24

  BUBBLE_ZOOM_X  = 1.0
  BUBBLE_ZOOM_Y  = 1.0
  SHOW_IN_LOG    = false

  REPEAT_DIALOGUE = true
  DELAY_ACTION    = true
  SIMULTANEOUS_BUBBLES      = true
  SIMULTANEOUS_BUBBLES_WAIT = true
end

#==============================================================================
# ■ RPG::Enemy
#==============================================================================
class RPG::Enemy < RPG::BaseItem
  def dialogue_lines
    @dialogue_lines ||= load_dialogue_lines
  end

  def random_dialogue
    lines = dialogue_lines
    lines.empty? ? nil : lines[rand(lines.size)]
  end

  private

  def load_dialogue_lines
    lines = []
    note.scan(/<对话[::]\s*(.+?)>/i) do |match|
      lines.push(match[0].strip) unless match[0].strip.empty?
    end
    lines
  end
end

#==============================================================================
# ■ Game_Enemy
#==============================================================================
class Game_Enemy < Game_Battler
  attr_accessor :custom_dialogue_text
  attr_accessor :dialogue_displayed
  attr_accessor :just_spoke_in_group

  alias enemy_dialogue_initialize initialize
  def initialize(index, enemy_id)
    enemy_dialogue_initialize(index, enemy_id)
    @custom_dialogue_text = nil
    @dialogue_displayed   = false
    @just_spoke_in_group  = false
  end

  def active_dialogue_text
    return @custom_dialogue_text if @custom_dialogue_text
    enemy.random_dialogue
  end

  def has_dialogue?
    return true if @custom_dialogue_text
    enemy.dialogue_lines.any?
  end

  def custom_dialogue_active?
    !@custom_dialogue_text.nil?
  end
end

#==============================================================================
# ■ Window_Enemy_Dialogue_Bubble
#==============================================================================
class Window_Enemy_Dialogue_Bubble < Window_Base
  attr_reader :enemy, :text
  attr_reader :typing_finished

  def initialize(enemy, raw_text)
    @enemy = enemy
    @raw_text = raw_text
    @processed_text = @raw_text.gsub(Enemy_Dialogue::MANUAL_BREAK_TAG, "\n")
    @char_index = 0
    @type_timer = 0
    @typing_finished = false

    super(0, 0, 210, 80)
    self.opacity          = 0
    self.back_opacity     = 0
    self.contents_opacity = 255
    self.z = Enemy_Dialogue::Z_VALUE

    create_contents
    max_width = max_text_width
    @total_lines = calculate_lines(@processed_text, max_width)
    line_height = Enemy_Dialogue::LINE_SPACING
    w = [210, [280, max_width + 40].min].max
    h = [80, @total_lines.size * line_height + 32].max

    self.width = w
    self.height = h
    create_contents

    create_bubble_sprite
    update_display_text
    set_position
  end

  # 自动换行计算
  def full_text_width
    bmp = Bitmap.new(1, 1)
    bmp.font.size = Enemy_Dialogue::TEXT_SIZE
    bmp.font.bold = Enemy_Dialogue::TEXT_BOLD
    w = bmp.text_size(@processed_text).width
    bmp.dispose
    w
  end

  def max_text_width
    self.width - 32 - Enemy_Dialogue::TEXT_PADDING_LEFT - Enemy_Dialogue::TEXT_PADDING_RIGHT
  end

  def create_bubble_sprite
    @bubble_sprite = Sprite.new
    @bubble_sprite.bitmap = Cache.system(Enemy_Dialogue::DIALOGUE_BUBBLE_IMAGE)
    @bubble_sprite.z      = self.z - 100
    @bubble_sprite.zoom_x = Enemy_Dialogue::BUBBLE_ZOOM_X
    @bubble_sprite.zoom_y = Enemy_Dialogue::BUBBLE_ZOOM_Y
  end

  def create_contents
    self.contents.dispose if self.contents
    self.contents = Bitmap.new(width - 32, height - 32)
    self.contents.clear
  end

  def calculate_lines(text, max_width)
    lines = []
    paragraphs = text.split("\n")
    bmp = self.contents
    bmp.font.size = Enemy_Dialogue::TEXT_SIZE
    bmp.font.bold = Enemy_Dialogue::TEXT_BOLD

    paragraphs.each do |para|
      if para.empty?
        lines << ""
        next
      end
      current_line = ""
      para.each_char do |ch|
        test_line = current_line + ch
        if bmp.text_size(test_line).width > max_width
          lines << current_line
          current_line = ch
        else
          current_line = test_line
        end
      end
      lines << current_line unless current_line.empty?
    end
    lines
  end

  def visible_text
    if Enemy_Dialogue::TEXT_TYPEWRITER && !@typing_finished
      @processed_text[0, @char_index]
    else
      @processed_text
    end
  end

  def update_display_text
    self.contents.clear
    text = visible_text
    return if text.empty?
    max_width = max_text_width
    lines = calculate_lines(text, max_width)
    bmp = self.contents
    bmp.font.size   = Enemy_Dialogue::TEXT_SIZE
    bmp.font.bold   = Enemy_Dialogue::TEXT_BOLD
    bmp.font.color  = Enemy_Dialogue::TEXT_COLOR
    bmp.font.shadow = false

    line_height = Enemy_Dialogue::LINE_SPACING
    start_y = self.contents.height / 2 - (lines.size * line_height) / 2
    lines.each_with_index do |line, i|
      y = start_y + i * line_height
      bmp.draw_text(Enemy_Dialogue::TEXT_PADDING_LEFT, y, max_width, line_height, line, 0)
    end
  end

  def set_position
    sprite = find_enemy_sprite
    if sprite
      x = sprite.x + Enemy_Dialogue::X_OFFSET
      y = sprite.y + Enemy_Dialogue::Y_OFFSET
    else
      x = Graphics.width - 160 + Enemy_Dialogue::X_OFFSET
      y = 180 + @enemy.index * 48 + Enemy_Dialogue::Y_OFFSET
    end
    self.x = x
    self.y = y
    @bubble_sprite.x = x
    @bubble_sprite.y = y
  end

  def find_enemy_sprite
    spriteset = SceneManager.scene.spriteset rescue nil
    return nil unless spriteset
    spriteset.enemy_sprites.each do |sprite|
      return sprite if sprite.battler == @enemy
    end
    nil
  end

  def update
    super
    @bubble_sprite.update if @bubble_sprite
    update_typing if Enemy_Dialogue::TEXT_TYPEWRITER && !@typing_finished
  end

  def update_typing
    if @char_index < @processed_text.length
      @type_timer += 1
      if @type_timer >= Enemy_Dialogue::TYPE_SPEED
        @type_timer = 0
        @char_index += 1
        update_display_text
      end
    else
      @typing_finished = true
    end
  end

  # 新功能:立即完成打字(用于Z键跳过)
  def skip_typing
    return if @typing_finished
    @char_index = @processed_text.length
    @typing_finished = true
    update_display_text
  end

  def dispose
    @bubble_sprite.dispose if @bubble_sprite
    super
  end

  def visible=(value)
    super(value)
    @bubble_sprite.visible = value if @bubble_sprite
  end
end

#==============================================================================
# ■ Scene_Battle
#==============================================================================
class Scene_Battle < Scene_Base
  attr_accessor :all_bubbles_shown_this_turn

  alias dialogue_execute_action execute_action
  def execute_action
    # 全体气泡
    if Enemy_Dialogue::ENABLED && Enemy_Dialogue::SIMULTANEOUS_BUBBLES &&
       @subject.is_a?(Game_Enemy) && !@all_bubbles_shown_this_turn
      create_all_enemy_bubbles
      @all_bubbles_shown_this_turn = true

      if Enemy_Dialogue::SIMULTANEOUS_BUBBLES_WAIT && @enemy_dialogue_bubbles
        while wait_for_multi_bubbles?
          update_basic
          @enemy_dialogue_bubbles.each { |b| b.update if b }
          Graphics.update
          Input.update
          # Z键跳过:立即完成所有打字并结束等待
          if Input.trigger?(:C)
            @enemy_dialogue_bubbles.each { |b| b.skip_typing }
            dispose_all_enemy_bubbles
            break
          end
        end
      end
    end

    # 单个延迟攻击
    if Enemy_Dialogue::ENABLED && Enemy_Dialogue::DELAY_ACTION &&
       @subject.is_a?(Game_Enemy) && !(@subject.just_spoke_in_group) &&
       enemy_should_speak_individually?(@subject)
      speak_then_act(@subject)
      return
    end

    dialogue_execute_action
  end

  def wait_for_multi_bubbles?
    return false unless @enemy_dialogue_bubbles
    @multi_bubbles_extra_pause_started ||= false
    if @enemy_dialogue_bubbles.all? { |b| b.typing_finished }
      unless @multi_bubbles_extra_pause_started
        @multi_bubbles_extra_pause_frames = Enemy_Dialogue::EXTRA_PAUSE
        @multi_bubbles_extra_pause_started = true
      end
      @multi_bubbles_extra_pause_frames -= 1
      if @multi_bubbles_extra_pause_frames <= 0
        dispose_all_enemy_bubbles
        @multi_bubbles_extra_pause_started = false
        return false
      end
    end
    true
  end

  def create_all_enemy_bubbles
    dispose_all_enemy_bubbles
    @enemy_dialogue_bubbles = []
    $game_troop.members.each do |enemy|
      next if enemy.dead?
      next unless enemy.is_a?(Game_Enemy)
      text = enemy.active_dialogue_text
      next unless text && !text.empty?
      enemy.dialogue_displayed = true
      enemy.just_spoke_in_group = true
      bubble = Window_Enemy_Dialogue_Bubble.new(enemy, text)
      bubble.viewport = @viewport
      bubble.show
      @enemy_dialogue_bubbles.push(bubble)
    end
  end

  def dispose_all_enemy_bubbles
    return unless @enemy_dialogue_bubbles
    @enemy_dialogue_bubbles.each { |b| b.dispose }
    @enemy_dialogue_bubbles = nil
    @multi_bubbles_extra_pause_started = false
  end

  def enemy_should_speak_individually?(enemy)
    return false unless enemy.has_dialogue?
    return false if enemy.dialogue_displayed && !Enemy_Dialogue::REPEAT_DIALOGUE
    true
  end

  def speak_then_act(enemy)
    text = enemy.active_dialogue_text
    if text && !text.empty?
      enemy.dialogue_displayed = true
      show_single_bubble(enemy, text)

      if Enemy_Dialogue::TEXT_TYPEWRITER
        # 打字阶段
        until @enemy_dialogue_bubble.typing_finished
          update_basic
          @enemy_dialogue_bubble.update
          Graphics.update
          Input.update
          if Input.trigger?(:C)                     # Z键跳过打字
            @enemy_dialogue_bubble.skip_typing
            break
          end
        end
        # 额外停留阶段
        extra_frames = Enemy_Dialogue::EXTRA_PAUSE
        extra_frames.times do
          update_basic
          @enemy_dialogue_bubble.update if @enemy_dialogue_bubble
          Graphics.update
          Input.update
          if Input.trigger?(:C)                     # Z键跳过停留
            break
          end
        end
        dispose_single_bubble
      else
        # 无打字机时,直接等待固定时长,也可以跳
        while @enemy_dialogue_bubble
          update_basic
          @enemy_dialogue_bubble.update
          Graphics.update
          Input.update
          if Input.trigger?(:C)
            dispose_single_bubble
            break
          end
        end
      end

      if enemy.exist? && !enemy.dead?
        dialogue_execute_action
      end
    end
  end

  def show_single_bubble(enemy, text)
    dispose_single_bubble
    @enemy_dialogue_bubble = Window_Enemy_Dialogue_Bubble.new(enemy, text)
    @enemy_dialogue_bubble.viewport = @viewport
    @enemy_dialogue_bubble.show
    @enemy_dialogue_frames = Enemy_Dialogue::DURATION_IN_FRAMES
  end

  def dispose_single_bubble
    return unless @enemy_dialogue_bubble
    @enemy_dialogue_bubble.dispose
    @enemy_dialogue_bubble = nil
    @enemy_dialogue_frames = nil
  end

  alias dialogue_update_basic update_basic
  def update_basic
    dialogue_update_basic
    update_single_bubble_countdown if @enemy_dialogue_bubble && @enemy_dialogue_frames
  end

  def update_single_bubble_countdown
    @enemy_dialogue_frames -= 1
    if @enemy_dialogue_frames <= 0
      dispose_single_bubble
    end
  end

  alias dialogue_turn_end turn_end
  def turn_end
    $game_troop.members.each do |enemy|
      next unless enemy.is_a?(Game_Enemy)
      enemy.just_spoke_in_group = false
    end
    @all_bubbles_shown_this_turn = false
    dialogue_turn_end
  end

  alias dialogue_terminate terminate
  def terminate
    dispose_single_bubble
    dispose_all_enemy_bubbles
    dialogue_terminate
  end
end

#==============================================================================
# ■ 事件脚本指令
#==============================================================================
class Game_Interpreter
  def set_enemy_dialogue(enemy_index, text)
    enemy = $game_troop.members[enemy_index]
    return unless enemy && enemy.is_a?(Game_Enemy)
    enemy.custom_dialogue_text = text
    enemy.dialogue_displayed = false
  end

  def force_enemy_dialogue(enemy_index)
    enemy = $game_troop.members[enemy_index]
    return unless enemy && enemy.is_a?(Game_Enemy)
    text = enemy.active_dialogue_text
    if text
      enemy.dialogue_displayed = true
      SceneManager.scene.show_single_bubble(enemy, text)
    end
  end

  def clear_enemy_dialogue(enemy_index)
    enemy = $game_troop.members[enemy_index]
    return unless enemy && enemy.is_a?(Game_Enemy)
    enemy.custom_dialogue_text = nil
  end

  # 立即移除当前气泡(事件脚本中可用)
  def dismiss_enemy_dialogue
    SceneManager.scene.dispose_single_bubble if SceneManager.scene.is_a?(Scene_Battle)
  end
end[/pre]
WIR`SR0_TD`_{3XPOK1O)T0.png
PV_]KCE%18Q6OF7YFY_UIUB.png
3WZZKMMO7Y4E%4_EMYU@S$G.png
OXMOQ2J`6%5D]3KVP0G9HBG.png
您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

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

在本版发帖返回顶部