#============================================================================== # ■ 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
WIR`SR0_TD`_{3XPOK1O)T0.png (40.36 KB, 下载次数: 15)
PV_]KCE%18Q6OF7YFY_UIUB.png (32.56 KB, 下载次数: 15)
3WZZKMMO7Y4E%4_EMYU@S$G.png (64.25 KB, 下载次数: 13)
OXMOQ2J`6%5D]3KVP0G9HBG.png (49.17 KB, 下载次数: 13)
| 欢迎光临 Project1 (https://rpg.blue/) | Powered by Discuz! X3.1 |