设为首页收藏本站|繁體中文

Project1

 找回密码
 注册会员
搜索
查看: 306|回复: 0
打印 上一主题 下一主题

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

[复制链接]

Lv3.寻梦者

梦石
1
星屑
1174
在线时间
58 小时
注册时间
2023-9-8
帖子
35
跳转到指定楼层
1
发表于 2026-5-11 02:36:14 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式

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

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

x
RUBY 代码复制
  1. #==============================================================================
  2. # ■ Undertale 风格敌人对话气泡
  3. #==============================================================================
  4. #作者:ruigi
  5. #==============================================================================
  6. # ■ 使用说明
  7. #==============================================================================
  8. # 【1】在敌人备注中写入对话标签
  9. #   格式:<对话:你的台词>
  10. #   如:<对话:敢不敢跟我比划比划!>
  11. #   可写入多行,每次随机抽取一句。
  12. #
  13. # 【2】手动换行(仅支持事件脚本)
  14. #   使用 <p> 标签,例如:<对话:第一行<p>第二行>
  15. #   自动换行已启用,超宽文字会自动折行。
  16. #
  17. # 【3】事件脚本指令(在战斗事件的[脚本]中使用)
  18. #   set_enemy_dialogue(索引, "台词")   # 为敌人设置自定义对话(下次行动时显示)
  19. #   force_enemy_dialogue(索引)         # 立即强制显示该敌人的对话气泡
  20. #   clear_enemy_dialogue(索引)         # 清除自定义对话,恢复从备注随机读取
  21. #   dismiss_enemy_dialogue             # 立即关闭当前显示的气泡
  22. #
  23. # 【4】可调参数(见模块 Enemy_Dialogue)
  24. #   ENABLED           : 总开关(false 完全关闭功能)
  25. #   TEXT_TYPEWRITER   : 打字机效果开关
  26. #   TYPE_SPEED        : 打字速度(每 N 帧显示一个字)
  27. #   EXTRA_PAUSE      : 打字完成后额外停留帧数
  28. #   DELAY_ACTION     : 是否等气泡消失后再发动攻击
  29. #   SIMULTANEOUS_BUBBLES        : 是否所有敌人同时显示气泡
  30. #   SIMULTANEOUS_BUBBLES_WAIT   : 全体气泡是否等待结束后才继续行动
  31. #   REPEAT_DIALOGUE : 是否每回合都可能说话
  32. #   Z 键(确认键)可跳过当前正在显示的对话(打字或停留)。
  33. #
  34. # 【5】资源文件
  35. #   请在 Graphics/System/ 下放置气泡图片,文件名见 DIALOGUE_BUBBLE_IMAGE。
  36. #==============================================================================
  37. module Enemy_Dialogue
  38.   ENABLED                = true
  39.   DIALOGUE_BUBBLE_IMAGE  = "Enemy_Dialogue_Bubble"
  40.   DURATION_IN_FRAMES     = 200
  41.  
  42.   X_OFFSET   = -30
  43.   Y_OFFSET   = -290
  44.   Z_VALUE    = 100
  45.  
  46.   TEXT_COLOR        = Color.new(255, 255, 255, 255)
  47.   TEXT_SIZE         = 20
  48.   TEXT_BOLD         = true
  49.   TEXT_SHADOW_COLOR = Color.new(0, 0, 0, 160)
  50.  
  51.   TEXT_TYPEWRITER  = true
  52.   TYPE_SPEED       = 2
  53.   EXTRA_PAUSE      = 40
  54.  
  55.   AUTO_WRAP = true
  56.   MANUAL_BREAK_TAG = /<p>/i
  57.   TEXT_PADDING_LEFT  = 12
  58.   TEXT_PADDING_RIGHT = 12
  59.   LINE_SPACING = 24
  60.  
  61.   BUBBLE_ZOOM_X  = 1.0
  62.   BUBBLE_ZOOM_Y  = 1.0
  63.   SHOW_IN_LOG    = false
  64.  
  65.   REPEAT_DIALOGUE = true
  66.   DELAY_ACTION    = true
  67.   SIMULTANEOUS_BUBBLES      = true
  68.   SIMULTANEOUS_BUBBLES_WAIT = true
  69. end
  70.  
  71. #==============================================================================
  72. # ■ RPG::Enemy
  73. #==============================================================================
  74. class RPG::Enemy < RPG::BaseItem
  75.   def dialogue_lines
  76.     @dialogue_lines ||= load_dialogue_lines
  77.   end
  78.  
  79.   def random_dialogue
  80.     lines = dialogue_lines
  81.     lines.empty? ? nil : lines[rand(lines.size)]
  82.   end
  83.  
  84.   private
  85.  
  86.   def load_dialogue_lines
  87.     lines = []
  88.     note.scan(/<对话[::]\s*(.+?)>/i) do |match|
  89.       lines.push(match[0].strip) unless match[0].strip.empty?
  90.     end
  91.     lines
  92.   end
  93. end
  94.  
  95. #==============================================================================
  96. # ■ Game_Enemy
  97. #==============================================================================
  98. class Game_Enemy < Game_Battler
  99.   attr_accessor :custom_dialogue_text
  100.   attr_accessor :dialogue_displayed
  101.   attr_accessor :just_spoke_in_group
  102.  
  103.   alias enemy_dialogue_initialize initialize
  104.   def initialize(index, enemy_id)
  105.     enemy_dialogue_initialize(index, enemy_id)
  106.     @custom_dialogue_text = nil
  107.     @dialogue_displayed   = false
  108.     @just_spoke_in_group  = false
  109.   end
  110.  
  111.   def active_dialogue_text
  112.     return @custom_dialogue_text if @custom_dialogue_text
  113.     enemy.random_dialogue
  114.   end
  115.  
  116.   def has_dialogue?
  117.     return true if @custom_dialogue_text
  118.     enemy.dialogue_lines.any?
  119.   end
  120.  
  121.   def custom_dialogue_active?
  122.     !@custom_dialogue_text.nil?
  123.   end
  124. end
  125.  
  126. #==============================================================================
  127. # ■ Window_Enemy_Dialogue_Bubble
  128. #==============================================================================
  129. class Window_Enemy_Dialogue_Bubble < Window_Base
  130.   attr_reader :enemy, :text
  131.   attr_reader :typing_finished
  132.  
  133.   def initialize(enemy, raw_text)
  134.     @enemy = enemy
  135.     @raw_text = raw_text
  136.     @processed_text = @raw_text.gsub(Enemy_Dialogue::MANUAL_BREAK_TAG, "\n")
  137.     @char_index = 0
  138.     @type_timer = 0
  139.     @typing_finished = false
  140.  
  141.     super(0, 0, 210, 80)
  142.     self.opacity          = 0
  143.     self.back_opacity     = 0
  144.     self.contents_opacity = 255
  145.     self.z = Enemy_Dialogue::Z_VALUE
  146.  
  147.     create_contents
  148.     max_width = max_text_width
  149.     @total_lines = calculate_lines(@processed_text, max_width)
  150.     line_height = Enemy_Dialogue::LINE_SPACING
  151.     w = [210, [280, max_width + 40].min].max
  152.     h = [80, @total_lines.size * line_height + 32].max
  153.  
  154.     self.width = w
  155.     self.height = h
  156.     create_contents
  157.  
  158.     create_bubble_sprite
  159.     update_display_text
  160.     set_position
  161.   end
  162.  
  163.   # 自动换行计算
  164.   def full_text_width
  165.     bmp = Bitmap.new(1, 1)
  166.     bmp.font.size = Enemy_Dialogue::TEXT_SIZE
  167.     bmp.font.bold = Enemy_Dialogue::TEXT_BOLD
  168.     w = bmp.text_size(@processed_text).width
  169.     bmp.dispose
  170.     w
  171.   end
  172.  
  173.   def max_text_width
  174.     self.width - 32 - Enemy_Dialogue::TEXT_PADDING_LEFT - Enemy_Dialogue::TEXT_PADDING_RIGHT
  175.   end
  176.  
  177.   def create_bubble_sprite
  178.     @bubble_sprite = Sprite.new
  179.     @bubble_sprite.bitmap = Cache.system(Enemy_Dialogue::DIALOGUE_BUBBLE_IMAGE)
  180.     @bubble_sprite.z      = self.z - 100
  181.     @bubble_sprite.zoom_x = Enemy_Dialogue::BUBBLE_ZOOM_X
  182.     @bubble_sprite.zoom_y = Enemy_Dialogue::BUBBLE_ZOOM_Y
  183.   end
  184.  
  185.   def create_contents
  186.     self.contents.dispose if self.contents
  187.     self.contents = Bitmap.new(width - 32, height - 32)
  188.     self.contents.clear
  189.   end
  190.  
  191.   def calculate_lines(text, max_width)
  192.     lines = []
  193.     paragraphs = text.split("\n")
  194.     bmp = self.contents
  195.     bmp.font.size = Enemy_Dialogue::TEXT_SIZE
  196.     bmp.font.bold = Enemy_Dialogue::TEXT_BOLD
  197.  
  198.     paragraphs.each do |para|
  199.       if para.empty?
  200.         lines << ""
  201.         next
  202.       end
  203.       current_line = ""
  204.       para.each_char do |ch|
  205.         test_line = current_line + ch
  206.         if bmp.text_size(test_line).width > max_width
  207.           lines << current_line
  208.           current_line = ch
  209.         else
  210.           current_line = test_line
  211.         end
  212.       end
  213.       lines << current_line unless current_line.empty?
  214.     end
  215.     lines
  216.   end
  217.  
  218.   def visible_text
  219.     if Enemy_Dialogue::TEXT_TYPEWRITER && !@typing_finished
  220.       @processed_text[0, @char_index]
  221.     else
  222.       @processed_text
  223.     end
  224.   end
  225.  
  226.   def update_display_text
  227.     self.contents.clear
  228.     text = visible_text
  229.     return if text.empty?
  230.     max_width = max_text_width
  231.     lines = calculate_lines(text, max_width)
  232.     bmp = self.contents
  233.     bmp.font.size   = Enemy_Dialogue::TEXT_SIZE
  234.     bmp.font.bold   = Enemy_Dialogue::TEXT_BOLD
  235.     bmp.font.color  = Enemy_Dialogue::TEXT_COLOR
  236.     bmp.font.shadow = false
  237.  
  238.     line_height = Enemy_Dialogue::LINE_SPACING
  239.     start_y = self.contents.height / 2 - (lines.size * line_height) / 2
  240.     lines.each_with_index do |line, i|
  241.       y = start_y + i * line_height
  242.       bmp.draw_text(Enemy_Dialogue::TEXT_PADDING_LEFT, y, max_width, line_height, line, 0)
  243.     end
  244.   end
  245.  
  246.   def set_position
  247.     sprite = find_enemy_sprite
  248.     if sprite
  249.       x = sprite.x + Enemy_Dialogue::X_OFFSET
  250.       y = sprite.y + Enemy_Dialogue::Y_OFFSET
  251.     else
  252.       x = Graphics.width - 160 + Enemy_Dialogue::X_OFFSET
  253.       y = 180 + @enemy.index * 48 + Enemy_Dialogue::Y_OFFSET
  254.     end
  255.     self.x = x
  256.     self.y = y
  257.     @bubble_sprite.x = x
  258.     @bubble_sprite.y = y
  259.   end
  260.  
  261.   def find_enemy_sprite
  262.     spriteset = SceneManager.scene.spriteset rescue nil
  263.     return nil unless spriteset
  264.     spriteset.enemy_sprites.each do |sprite|
  265.       return sprite if sprite.battler == @enemy
  266.     end
  267.     nil
  268.   end
  269.  
  270.   def update
  271.     super
  272.     @bubble_sprite.update if @bubble_sprite
  273.     update_typing if Enemy_Dialogue::TEXT_TYPEWRITER && !@typing_finished
  274.   end
  275.  
  276.   def update_typing
  277.     if @char_index < @processed_text.length
  278.       @type_timer += 1
  279.       if @type_timer >= Enemy_Dialogue::TYPE_SPEED
  280.         @type_timer = 0
  281.         @char_index += 1
  282.         update_display_text
  283.       end
  284.     else
  285.       @typing_finished = true
  286.     end
  287.   end
  288.  
  289.   # 新功能:立即完成打字(用于Z键跳过)
  290.   def skip_typing
  291.     return if @typing_finished
  292.     @char_index = @processed_text.length
  293.     @typing_finished = true
  294.     update_display_text
  295.   end
  296.  
  297.   def dispose
  298.     @bubble_sprite.dispose if @bubble_sprite
  299.     super
  300.   end
  301.  
  302.   def visible=(value)
  303.     super(value)
  304.     @bubble_sprite.visible = value if @bubble_sprite
  305.   end
  306. end
  307.  
  308. #==============================================================================
  309. # ■ Scene_Battle
  310. #==============================================================================
  311. class Scene_Battle < Scene_Base
  312.   attr_accessor :all_bubbles_shown_this_turn
  313.  
  314.   alias dialogue_execute_action execute_action
  315.   def execute_action
  316.     # 全体气泡
  317.     if Enemy_Dialogue::ENABLED && Enemy_Dialogue::SIMULTANEOUS_BUBBLES &&
  318.        @subject.is_a?(Game_Enemy) && !@all_bubbles_shown_this_turn
  319.       create_all_enemy_bubbles
  320.       @all_bubbles_shown_this_turn = true
  321.  
  322.       if Enemy_Dialogue::SIMULTANEOUS_BUBBLES_WAIT && @enemy_dialogue_bubbles
  323.         while wait_for_multi_bubbles?
  324.           update_basic
  325.           @enemy_dialogue_bubbles.each { |b| b.update if b }
  326.           Graphics.update
  327.           Input.update
  328.           # Z键跳过:立即完成所有打字并结束等待
  329.           if Input.trigger?(:C)
  330.             @enemy_dialogue_bubbles.each { |b| b.skip_typing }
  331.             dispose_all_enemy_bubbles
  332.             break
  333.           end
  334.         end
  335.       end
  336.     end
  337.  
  338.     # 单个延迟攻击
  339.     if Enemy_Dialogue::ENABLED && Enemy_Dialogue::DELAY_ACTION &&
  340.        @subject.is_a?(Game_Enemy) && !(@subject.just_spoke_in_group) &&
  341.        enemy_should_speak_individually?(@subject)
  342.       speak_then_act(@subject)
  343.       return
  344.     end
  345.  
  346.     dialogue_execute_action
  347.   end
  348.  
  349.   def wait_for_multi_bubbles?
  350.     return false unless @enemy_dialogue_bubbles
  351.     @multi_bubbles_extra_pause_started ||= false
  352.     if @enemy_dialogue_bubbles.all? { |b| b.typing_finished }
  353.       unless @multi_bubbles_extra_pause_started
  354.         @multi_bubbles_extra_pause_frames = Enemy_Dialogue::EXTRA_PAUSE
  355.         @multi_bubbles_extra_pause_started = true
  356.       end
  357.       @multi_bubbles_extra_pause_frames -= 1
  358.       if @multi_bubbles_extra_pause_frames <= 0
  359.         dispose_all_enemy_bubbles
  360.         @multi_bubbles_extra_pause_started = false
  361.         return false
  362.       end
  363.     end
  364.     true
  365.   end
  366.  
  367.   def create_all_enemy_bubbles
  368.     dispose_all_enemy_bubbles
  369.     @enemy_dialogue_bubbles = []
  370.     $game_troop.members.each do |enemy|
  371.       next if enemy.dead?
  372.       next unless enemy.is_a?(Game_Enemy)
  373.       text = enemy.active_dialogue_text
  374.       next unless text && !text.empty?
  375.       enemy.dialogue_displayed = true
  376.       enemy.just_spoke_in_group = true
  377.       bubble = Window_Enemy_Dialogue_Bubble.new(enemy, text)
  378.       bubble.viewport = @viewport
  379.       bubble.show
  380.       @enemy_dialogue_bubbles.push(bubble)
  381.     end
  382.   end
  383.  
  384.   def dispose_all_enemy_bubbles
  385.     return unless @enemy_dialogue_bubbles
  386.     @enemy_dialogue_bubbles.each { |b| b.dispose }
  387.     @enemy_dialogue_bubbles = nil
  388.     @multi_bubbles_extra_pause_started = false
  389.   end
  390.  
  391.   def enemy_should_speak_individually?(enemy)
  392.     return false unless enemy.has_dialogue?
  393.     return false if enemy.dialogue_displayed && !Enemy_Dialogue::REPEAT_DIALOGUE
  394.     true
  395.   end
  396.  
  397.   def speak_then_act(enemy)
  398.     text = enemy.active_dialogue_text
  399.     if text && !text.empty?
  400.       enemy.dialogue_displayed = true
  401.       show_single_bubble(enemy, text)
  402.  
  403.       if Enemy_Dialogue::TEXT_TYPEWRITER
  404.         # 打字阶段
  405.         until @enemy_dialogue_bubble.typing_finished
  406.           update_basic
  407.           @enemy_dialogue_bubble.update
  408.           Graphics.update
  409.           Input.update
  410.           if Input.trigger?(:C)                     # Z键跳过打字
  411.             @enemy_dialogue_bubble.skip_typing
  412.             break
  413.           end
  414.         end
  415.         # 额外停留阶段
  416.         extra_frames = Enemy_Dialogue::EXTRA_PAUSE
  417.         extra_frames.times do
  418.           update_basic
  419.           @enemy_dialogue_bubble.update if @enemy_dialogue_bubble
  420.           Graphics.update
  421.           Input.update
  422.           if Input.trigger?(:C)                     # Z键跳过停留
  423.             break
  424.           end
  425.         end
  426.         dispose_single_bubble
  427.       else
  428.         # 无打字机时,直接等待固定时长,也可以跳
  429.         while @enemy_dialogue_bubble
  430.           update_basic
  431.           @enemy_dialogue_bubble.update
  432.           Graphics.update
  433.           Input.update
  434.           if Input.trigger?(:C)
  435.             dispose_single_bubble
  436.             break
  437.           end
  438.         end
  439.       end
  440.  
  441.       if enemy.exist? && !enemy.dead?
  442.         dialogue_execute_action
  443.       end
  444.     end
  445.   end
  446.  
  447.   def show_single_bubble(enemy, text)
  448.     dispose_single_bubble
  449.     @enemy_dialogue_bubble = Window_Enemy_Dialogue_Bubble.new(enemy, text)
  450.     @enemy_dialogue_bubble.viewport = @viewport
  451.     @enemy_dialogue_bubble.show
  452.     @enemy_dialogue_frames = Enemy_Dialogue::DURATION_IN_FRAMES
  453.   end
  454.  
  455.   def dispose_single_bubble
  456.     return unless @enemy_dialogue_bubble
  457.     @enemy_dialogue_bubble.dispose
  458.     @enemy_dialogue_bubble = nil
  459.     @enemy_dialogue_frames = nil
  460.   end
  461.  
  462.   alias dialogue_update_basic update_basic
  463.   def update_basic
  464.     dialogue_update_basic
  465.     update_single_bubble_countdown if @enemy_dialogue_bubble && @enemy_dialogue_frames
  466.   end
  467.  
  468.   def update_single_bubble_countdown
  469.     @enemy_dialogue_frames -= 1
  470.     if @enemy_dialogue_frames <= 0
  471.       dispose_single_bubble
  472.     end
  473.   end
  474.  
  475.   alias dialogue_turn_end turn_end
  476.   def turn_end
  477.     $game_troop.members.each do |enemy|
  478.       next unless enemy.is_a?(Game_Enemy)
  479.       enemy.just_spoke_in_group = false
  480.     end
  481.     @all_bubbles_shown_this_turn = false
  482.     dialogue_turn_end
  483.   end
  484.  
  485.   alias dialogue_terminate terminate
  486.   def terminate
  487.     dispose_single_bubble
  488.     dispose_all_enemy_bubbles
  489.     dialogue_terminate
  490.   end
  491. end
  492.  
  493. #==============================================================================
  494. # ■ 事件脚本指令
  495. #==============================================================================
  496. class Game_Interpreter
  497.   def set_enemy_dialogue(enemy_index, text)
  498.     enemy = $game_troop.members[enemy_index]
  499.     return unless enemy && enemy.is_a?(Game_Enemy)
  500.     enemy.custom_dialogue_text = text
  501.     enemy.dialogue_displayed = false
  502.   end
  503.  
  504.   def force_enemy_dialogue(enemy_index)
  505.     enemy = $game_troop.members[enemy_index]
  506.     return unless enemy && enemy.is_a?(Game_Enemy)
  507.     text = enemy.active_dialogue_text
  508.     if text
  509.       enemy.dialogue_displayed = true
  510.       SceneManager.scene.show_single_bubble(enemy, text)
  511.     end
  512.   end
  513.  
  514.   def clear_enemy_dialogue(enemy_index)
  515.     enemy = $game_troop.members[enemy_index]
  516.     return unless enemy && enemy.is_a?(Game_Enemy)
  517.     enemy.custom_dialogue_text = nil
  518.   end
  519.  
  520.   # 立即移除当前气泡(事件脚本中可用)
  521.   def dismiss_enemy_dialogue
  522.     SceneManager.scene.dispose_single_bubble if SceneManager.scene.is_a?(Scene_Battle)
  523.   end
  524. end

WIR`SR0_TD`_{3XPOK1O)T0.png (40.36 KB, 下载次数: 15)

WIR`SR0_TD`_{3XPOK1O)T0.png

PV_]KCE%18Q6OF7YFY_UIUB.png (32.56 KB, 下载次数: 15)

PV_]KCE%18Q6OF7YFY_UIUB.png

3WZZKMMO7Y4E%4_EMYU@S$G.png (64.25 KB, 下载次数: 13)

3WZZKMMO7Y4E%4_EMYU@S$G.png

OXMOQ2J`6%5D]3KVP0G9HBG.png (49.17 KB, 下载次数: 13)

OXMOQ2J`6%5D]3KVP0G9HBG.png
您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

拿上你的纸笔,建造一个属于你的梦想世界,加入吧。
 注册会员
找回密码

站长信箱:[email protected]|手机版|小黑屋|无图版|Project1游戏制作

GMT+8, 2026-6-4 22:42

Powered by Discuz! X3.1

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表