#==============================================================================
# ■ 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