维护提醒
BWIKI 全站将于 9 月 3 日(全天)进行维护,期间无法编辑任何页面或发布新的评论。
全站通知:
模块:Events
刷
历
编
跳到导航
跳到搜索
local Helper = require("Module:Helper")
local NPC = require("Module:NPC")
-- 懒加载数据模块
local EventsData = Helper.LazyLoad("Module:Events/data")
local EventsStrings = Helper.LazyLoad("Module:Events/data/strings")
local TalkData = Helper.LazyLoad("Module:Talk/data")
local p = {}
-- 前向声明事件相关函数
local processEventDialogue
local findEventQuestionReply
local processEventQuestionCommand
local processEventQuickQuestionCommand
local findNpcResponse
local formatDialogueText
local removeConsecutiveEmptyStrings
local findEventReactions
local formatEventReactions
local findGotoAfterLabel
local processGenderDialogue
local processCutsceneCommand
-- 全局变量
local squote = "Squote"
local questionHasNoSpecificResponse = false
local warpInDD = false
local forkProcessed = false
local hasProcessed = {}
-- 条件goto的标签描述表
-- 格式:[条件ID] = "描述文本"
local conditionalGotoLabels = {
-- 事件相关
LeahInternet = "二心事件中,提议将作品放到网上出售",
LeahArtShowSuggestion = "二心事件中,提议在镇上举办艺术展",
["51_default"] = "二心事件中,冒犯了莉亚",
["54_default"] = "二心事件中,提议在镇上举办艺术展",
choseWizard = "选择的职业为法师时",
choseWarrior = "选择的职业为战士时",
["27_default"] = "选择的职业为牧师时",
-- 对话答案相关
Answer_38 = "上一个选项中,让玛鲁说实话",
["7_default"] = "上一个选项中,让玛鲁推卸责任",
Answer_77 = "(如果选择了重金属风格)",
Answer_78 = "(如果选择了电子音乐风格)",
Answer_79 = "(如果选择了乡村音乐风格)",
Answer_81 = "(如果四心事件中选择了「山姆故意弄掉的」)",
Answer_958699 = "二心事件中,选择了“悬念”",
Answer_958700 = "二心事件中,选择了“浪漫”",
["1848481_default"] = "二心事件中,选择了“科幻”",
-- 可以继续添加其他事件的条件标签
-- 对于默认分支,使用 "事件ID_default" 格式
}
-- cutscene选项的描述表
-- 格式:[minigame_label] = "描述文本"
local cutsceneLabels = {
-- Abigail游戏相关
AbigailGame_wonGame = "游戏成功通关",
AbigailGame_lostGame = "游戏未能通关",
-- 可以继续添加其他cutscene的标签
-- 格式:minigame名称_label名称 = "描述文本"
}
-- 清理hasProcessed表的函数
local function clearProcessedForks()
hasProcessed = {}
end
-- 获取已处理的fork分支列表
local function getProcessedForks()
local forks = {}
for fork, _ in pairs(hasProcessed) do
table.insert(forks, fork)
end
return forks
end
-- 获取翻译文本
local function getTranslationText(translationKey)
if not translationKey or translationKey == "" then
return ""
end
-- 处理Strings/Events格式的翻译键
if translationKey:match("^Strings/Events:") then
local key = translationKey:match("^Strings/Events:(.+)$")
if key and EventsStrings[key] then
return EventsStrings[key]
end
end
-- 处理Strings/StringsFromCSFiles格式的翻译键
if translationKey:match("^Strings/StringsFromCSFiles:") then
local key = translationKey:match("^Strings/StringsFromCSFiles:(.+)$")
if key and TalkData and TalkData["Strings/StringsFromCSFiles"] and TalkData["Strings/StringsFromCSFiles"][key] then
return TalkData["Strings/StringsFromCSFiles"][key]
end
end
-- 处理Characters/Dialogue格式的对话引用
if translationKey:match("^Characters/Dialogue/") then
local npcAndKey = translationKey:match("^Characters/Dialogue/(.+):(.+)$")
if npcAndKey then
local npcName, dialogueKey = translationKey:match("^Characters/Dialogue/([^:]+):(.+)$")
if npcName and dialogueKey and TalkData and TalkData[npcName] then
local dialogue = TalkData[npcName][dialogueKey]
if dialogue then
return dialogue
end
end
end
end
-- 如果找不到翻译,返回原键作为占位符
return "(" .. translationKey .. ")"
end
-- 清理文本,处理转义字符和占位符
local function cleanText(text, isPlayerChoice)
if not isPlayerChoice then
isPlayerChoice = false
end
if not text or type(text) ~= "string" then
return text
end
-- 处理转义字符
text = text:gsub("\\\"", "\"")
text = text:gsub("\\n", "\n")
-- 处理玩家名称占位符
text = text:gsub("@", '<span class="player-name">玩家名</span>')
text = text:gsub("{0}", '<span class="player-name">玩家名</span>')
text = text:gsub("%%farm", '<span class="player-name">农场名</span>')
text = text:gsub("%%book", '<span class="player-name">书名</span>')
text = text:gsub(" < ", '喜欢') -- [[文件:Emojis046.png|24px|link=]]
text = text:gsub("“<”", '“喜欢”') -- [[文件:Emojis046.png|24px|link=]]
text = text:gsub("%%firstnameletter", '<span class="player-name">玩家名的第一个字符</span>')
-- 处理其他占位符
text = text:gsub("%%noturn", '<span class="trigger-chance">(不转身)</span>')
-- 注意:表情参数($h, $s, $u, $l, $a, $0-9等)现在在parsePortraitParameter中处理
text = text:gsub("%$[be]", "") -- 只清理$b和$e,保留表情参数
text = text:gsub("##", "\n")
text = text:gsub("%s*%[%s*%d+%s*%]", '<span class="refuse-item">(给予物品)</span>')
text = text:gsub("%%fork", '')
text = text:gsub("*", "<span>*</span>")
-- 处理物品给予标记
text = text:gsub("%[([%d%s]+)%]", '<span class="trigger-chance">(给予物品)</span>')
text = text:gsub("^%%", '<span class="refuse-item">(不回应)</span>')
-- 清理末尾的#字符
text = text:gsub("#$", "")
return text
end
-- 获取角色中文名称
local function getCharacterName(character)
if not character then return "" end
-- 处理特殊角色名
if character == "farmer" then
return Helper.ExpandTemplate("玩家", {})
end
character = character:gsub("^%l", string.upper)
return NPC.getChineseName(character)
end
-- 解析表情参数,将英语别名转换为数字
local function parsePortraitParameter(text)
if not text or type(text) ~= "string" then
return text, nil
end
local portrait = nil
local cleanedText = text
-- 首先处理数字表情参数(包括多位数字),使用更精确的匹配
local customPortrait = cleanedText:match("%$(%d+)%s*$") -- 匹配行末的数字参数
if not customPortrait then
customPortrait = cleanedText:match("%$(%d+)[^%d]") -- 匹配后面不是数字的数字参数
if customPortrait then
-- 验证这个匹配是完整的(不是更长数字的一部分)
local fullMatch = cleanedText:match("%$" .. customPortrait .. "(%d)")
if fullMatch then
customPortrait = nil -- 如果后面还有数字,说明这不是完整匹配
end
end
end
if not customPortrait then
customPortrait = cleanedText:match("^.*%$(%d+)$") -- 匹配整行末尾的数字参数
end
if customPortrait then
portrait = customPortrait
cleanedText = cleanedText:gsub("%$" .. customPortrait, "")
else
-- 如果没有找到数字参数,再处理字母别名
local portraitMap = {
["$h"] = "1", -- happy
["$s"] = "2", -- sad
["$u"] = "3", -- unique
["$l"] = "4", -- love
["$a"] = "5", -- angry
}
-- 查找表情参数(通常在行末)
for pattern, number in pairs(portraitMap) do
local escapedPattern = pattern:gsub("%$", "%%$")
if cleanedText:find(escapedPattern) then
portrait = number
cleanedText = cleanedText:gsub(escapedPattern, "")
break
end
end
end
return cleanedText, portrait
end
-- 包装函数
local function wrapInText(text, label)
if label then
return '<div class="choice-option" data-goto="' .. label .. '">' .. text .. '</div>'
else
return '<div class="choice-option">' .. text .. '</div>'
end
end
local function wrapInReply(text)
return '<div class="choice-reply">' .. text .. '</div>'
end
local function choice()
return '<div class="choice-container">'
end
local function text()
return '<div class="choice-option">'
end
local function reply()
return '<div class="choice-reply">'
end
local function div()
return '</div>'
end
-- 创建引用格式
local function createQuote(text, template, id, character)
if not template then
template = "Say"
end
local type = "left"
local portrait
-- 如果没有传入角色名,尝试从文本中提取
if not character then
return Helper.ExpandTemplate(template, {"voiceover", text})
else
portrait = NPC.getEnglishName(character:gsub('(对话气泡)', '')) or nil
end
if not id and character then
id = 0
end
if portrait then
portrait = portrait:lower()
end
if id and portrait then
return Helper.ExpandTemplate(template, {type, character, text, id, portrait})
else
return Helper.ExpandTemplate(template, {type, character, text})
end
end
-- 分析label中的好感度变化
local function analyzeLabelFriendship(labelName, allCommands, commandIndex)
if not labelName or not allCommands then
return "0", nil
end
-- 从指定位置开始查找label
for i = commandIndex, #allCommands do
local command = allCommands[i]
if command and command:match("^label%s+" .. labelName .. "$") then
-- 找到label,继续查找friendship命令
for j = i + 1, #allCommands do
local nextCommand = allCommands[j]
if not nextCommand then
break
end
-- 如果遇到下一个label,停止搜索
if nextCommand:match("^label%s+") then
break
end
-- 查找friendship命令:friendship <character> <amount>
local character, amount = nextCommand:match("^friendship%s+([^%s]+)%s+([+-]?%d+)$")
if character and amount then
return amount, character
end
end
break
end
end
return "0", nil
end
-- 创建玩家选择格式
local function createPlayerChoice(text, friendship, character)
if character then
character = '[[' .. character .. ']]'
return Helper.ExpandTemplate("Choose", {"> " .. text, friendship, character})
else
return Helper.ExpandTemplate("Choose", {"> " .. text, friendship})
end
end
-- 分离人物名称和对话内容
local function separateCharacterAndDialogue(text)
if not text or type(text) ~= "string" then
return nil, text
end
-- 匹配格式:角色名:对话内容
local characterName, dialogue = text:match("^([^:]+):(.*)$")
if characterName and dialogue then
return characterName:match("^%s*(.-)%s*$"), dialogue
end
return nil, text
end
-- 处理包含分隔符的对话文本,分割为独立对话
local function processDialogueWithSeparators(text, template, characterName)
if not text or type(text) ~= "string" then
return ""
end
local dialogueText = text
-- 如果没有传入角色名,尝试从对话文本中分离
if not characterName then
local extractedName, extractedDialogue = separateCharacterAndDialogue(text)
if extractedName then
characterName = extractedName
dialogueText = extractedDialogue
end
end
if (dialogueText == nil or dialogueText == '') and characterName then
dialogueText = characterName
characterName = nil
end
-- 分割对话:#$b# 和 #$e# 都作为分隔符
local parts = {}
local currentPart = ""
local i = 1
while i <= #dialogueText do
if dialogueText:sub(i, i + 3) == "#$b#" then
if currentPart ~= "" then
table.insert(parts, currentPart)
currentPart = ""
end
i = i + 4
elseif dialogueText:sub(i, i + 3) == "#$e#" then
if currentPart ~= "" then
table.insert(parts, currentPart)
currentPart = ""
end
i = i + 4
else
currentPart = currentPart .. dialogueText:sub(i, i)
i = i + 1
end
end
-- 添加最后一部分
if currentPart ~= "" then
table.insert(parts, currentPart)
end
-- 如果没有分隔符,直接处理整个文本
if #parts <= 1 then
if dialogueText:find("%^") then
-- 有性别分支,调用性别处理函数
return processGenderDialogue(dialogueText, template, characterName)
else
local cleanedText, portrait = parsePortraitParameter(dialogueText)
cleanedText = cleanText(cleanedText)
return createQuote(cleanedText, template, portrait, characterName)
end
end
-- 处理每个独立的对话片段
local results = {}
for _, part in ipairs(parts) do
local trimmed = part:match("^%s*(.-)%s*$")
if trimmed and trimmed ~= "" then
-- 检查这个片段是否包含性别分支(包括 ^ 和 ${...^...}$ 格式)
if trimmed:find("%^") then
-- 有性别分支,调用性别处理函数
table.insert(results, processGenderDialogue(trimmed, template, characterName))
else
-- 没有性别分支,直接处理
local cleanedText, portrait = parsePortraitParameter(trimmed)
cleanedText = cleanText(cleanedText)
table.insert(results, createQuote(cleanedText, template, portrait, characterName))
end
end
end
return table.concat(results, "\n")
end
-- 处理$y快速问答对话(从Talk.lua移植)
local function processQuickResponseDialogue(text, template, characterName)
if not text or type(text) ~= "string" then
return text
end
-- 检查是否包含$y命令
if not text:match("%$y%s+") then
return text
end
local result = {}
local currentPos = 1
while true do
-- 查找下一个$y命令
local yPos = text:find("%$y%s+", currentPos)
if not yPos then
-- 没有更多$y命令,添加剩余文本
local remaining = text:sub(currentPos)
if remaining and remaining ~= "" then
local cleanedRemaining, remainingPortrait = parsePortraitParameter(remaining)
cleanedRemaining = cleanText(cleanedRemaining)
table.insert(result, createQuote(cleanedRemaining, template, remainingPortrait, characterName))
end
break
end
-- 添加$y之前的文本
local beforeY = text:sub(currentPos, yPos - 1)
if beforeY and beforeY ~= "" and beforeY ~= "#" then
local cleanedBeforeY, beforeYPortrait = parsePortraitParameter(beforeY)
cleanedBeforeY = cleanText(cleanedBeforeY)
table.insert(result, createQuote(cleanedBeforeY, template, beforeYPortrait, characterName))
end
-- 解析$y命令:$y 'question_answer1_reply1_answer2_reply2...'
local afterY = text:sub(yPos)
local yStart, yEnd, yContentOnly = afterY:find("%$y%s+'([^']+)'")
if yContentOnly and yStart and yEnd then
-- 分割问答对
local parts = {}
for part in yContentOnly:gmatch("[^_]+") do
table.insert(parts, part)
end
if #parts >= 3 then
local question = parts[1]
-- 处理问题(包括表情参数)
local cleanedQuestion, questionPortrait = parsePortraitParameter(question)
cleanedQuestion = cleanText(cleanedQuestion)
table.insert(result, createQuote(cleanedQuestion, template, questionPortrait, characterName))
-- 处理选项和回应
table.insert(result, choice())
-- 首先生成所有选项
local optionLabels = {}
for i = 2, #parts, 2 do
local answer = parts[i]
if answer then
answer = cleanText(answer, true)
local labelName = "y_option_" .. ((i-2)/2 + 1) .. "_" .. tostring(math.random(10000, 99999))
table.insert(optionLabels, labelName)
-- 格式化选择
local choiceTemplate = createPlayerChoice(answer, "0")
table.insert(result, wrapInText(choiceTemplate, labelName))
end
end
table.insert(result, div())
-- 然后生成对应的event-label
for i = 2, #parts, 2 do
local reply = parts[i + 1]
local labelIndex = (i-2)/2 + 1
local labelName = optionLabels[labelIndex]
if reply and labelName then
-- 处理回应(包括表情参数)
local cleanedReply, replyPortrait = parsePortraitParameter(reply)
cleanedReply = cleanText(cleanedReply)
table.insert(result, '<div class="event-label" data-label="' .. labelName .. '" style="display:none;">')
table.insert(result, createQuote(cleanedReply, template, replyPortrait, characterName))
table.insert(result, '</div>')
end
end
end
-- 移动到下一个位置:使用精确的结束位置
currentPos = yPos + yEnd
else
-- 如果解析失败,跳过这个$y命令
currentPos = yPos + 2
end
end
return table.concat(result, "\n")
end
-- 处理条件对话(如 $p 81#文本1$s|文本2)
local function processConditionalDialogue(text, template, characterName)
if not text or type(text) ~= "string" then
return text
end
-- 检查是否包含 $p <number># 格式的条件对话
local conditionPattern = "%$p%s+(%d+)#([^|]+)|(.+)"
local questionId, trueText, falseText = text:match(conditionPattern)
if questionId and trueText and falseText then
local result = {}
if questionId == "81" then
table.insert(result, "(如果选择了选项三)")
end
-- 处理条件为真的情况
local trueProcessed = processDialogueWithSeparators(trueText, template, characterName)
table.insert(result, trueProcessed)
if questionId == "81" then
table.insert(result, "(如果选择了选项三以外的选项)")
end
-- 处理条件为假的情况
local falseProcessed = processDialogueWithSeparators(falseText, template, characterName)
table.insert(result, falseProcessed)
return table.concat(result, "\n")
end
return text
end
-- 统一的对话处理函数:先处理换行切割,再处理性别分支
local function processDialogueUnified(text, template, characterName)
if not text or type(text) ~= "string" then
return ""
end
-- 优先处理条件对话($p格式)
if text:match("%$p%s+%d+#") then
return processConditionalDialogue(text, template, characterName)
end
-- 优先处理$y快速问答对话
if text:match("%$y%s+") then
return processQuickResponseDialogue(text, template, characterName)
end
-- 直接调用换行切割处理,它会在内部处理性别分支
return processDialogueWithSeparators(text, template, characterName)
end
-- 格式化对话文本
formatDialogueText = function(text)
if not text then return "" end
return processDialogueUnified(text, nil, nil)
end
-- 包装label区域
local function wrapLabel(content, label)
return '<div class="event-label" data-label="' .. label .. '" style="display:none;">' .. content .. '</div>'
end
-- 分析goto命令,返回标签列表和是否为条件goto
local function analyzeGotoCommand(gotoCommand)
local remaining = gotoCommand:match("^goto%s+(.+)$")
if not remaining then
return {}, false
end
-- 检查是否包含引号(条件goto的标志)
local hasConditions = remaining:find('"') ~= nil
if hasConditions then
-- 条件goto:格式为 goto target1 "condition1" target2 "condition2" targetDefault
-- 根据文档:goto [<target> [condition]]+
local parts = {}
local inQuote = false
local current = ""
for i = 1, #remaining do
local char = remaining:sub(i, i)
if char == '"' then
inQuote = not inQuote
if not inQuote then
-- 结束引号,保存当前内容
table.insert(parts, current)
current = ""
end
elseif char == ' ' and not inQuote then
if current ~= "" then
table.insert(parts, current)
current = ""
end
elseif inQuote then
current = current .. char
else
current = current .. char
end
end
if current ~= "" then
table.insert(parts, current)
end
-- 提取所有标签(包括有条件和无条件的)
local labels = {}
local i = 1
while i <= #parts do
local part = parts[i]
-- 如果这个部分不是以引号开始,说明是标签
if not part:match('^"') then
table.insert(labels, part)
i = i + 1
-- 跳过下一个条件部分(如果存在)
if i <= #parts and parts[i]:match('^"') then
i = i + 1
end
else
-- 这是条件部分,跳过
i = i + 1
end
end
return labels, true
else
-- 简单goto:只有一个标签
return {remaining}, false
end
end
-- 从条件字符串中提取ID(如从"PLAYER_HAS_MAIL Current LeahInternet"提取"LeahInternet")
local function extractConditionId(condition)
if not condition then
return nil
end
-- 处理PLAYER_HAS_MAIL格式
local mailId = condition:match("PLAYER_HAS_MAIL%s+Current%s+(.+)")
if mailId then
return mailId
end
-- 处理PLAYER_HAS_DIALOGUE_ANSWER格式
local answerId = condition:match("PLAYER_HAS_DIALOGUE_ANSWER%s+Current%s+(.+)")
if answerId then
return "Answer_" .. answerId
end
-- 可以添加其他条件格式的处理
-- 例如:PLAYER_NPC_RELATIONSHIP等
return nil
end
-- 处理条件goto命令
local function processConditionalGoto(gotoCommand, eventId, allCommands, commandIndex)
local remaining = gotoCommand:match("^goto%s+(.+)$")
if not remaining then
return nil
end
-- 检查是否包含引号(条件goto的标志)
if not remaining:find('"') then
return nil
end
-- 解析条件goto:格式为 goto target1 "condition1" target2 "condition2" targetDefault
local parts = {}
local inQuote = false
local current = ""
for i = 1, #remaining do
local char = remaining:sub(i, i)
if char == '"' then
inQuote = not inQuote
if not inQuote then
-- 结束引号,保存当前内容
table.insert(parts, current)
current = ""
end
elseif char == ' ' and not inQuote then
if current ~= "" then
table.insert(parts, current)
current = ""
end
elseif inQuote then
current = current .. char
else
current = current .. char
end
end
if current ~= "" then
table.insert(parts, current)
end
-- 调试:输出解析的parts
-- print("DEBUG: parts = ", table.concat(parts, " | "))
-- 解析条件分支
local result = {}
local options = {}
local i = 1
while i <= #parts do
local label = parts[i]
-- 检查下一个部分是否是条件
-- 条件应该是从引号中解析出来的内容,通常包含空格和特定关键词
if i + 1 <= #parts and (parts[i + 1]:find("PLAYER_") or parts[i + 1]:find(" ")) then
-- 下一个部分是条件
local condition = parts[i + 1]
i = i + 2
-- 从条件中提取ID
local conditionId = extractConditionId(condition)
mw.log(condition)
if conditionId then
local description = conditionalGotoLabels[conditionId] or ("条件分支:" .. conditionId)
-- 查找标签后的goto命令和隐式跳转
local additionalLabels = {}
if allCommands and commandIndex then
additionalLabels = findGotoAfterLabel(allCommands, commandIndex + 1, label)
end
-- 构建完整的标签列表
local allLabels = {label}
for _, additionalLabel in ipairs(additionalLabels) do
table.insert(allLabels, additionalLabel)
end
local labelString = table.concat(allLabels, " ")
table.insert(options, '<div class="choice-option" data-goto="' .. labelString .. '">')
table.insert(options, '<p>' .. description .. '</p>')
table.insert(options, '</div>')
end
else
-- 没有条件,这是默认分支
local defaultKey = (eventId or "unknown") .. "_default"
local description = conditionalGotoLabels[defaultKey] or ("默认分支")
-- 查找标签后的goto命令和隐式跳转
local additionalLabels = {}
if allCommands and commandIndex then
additionalLabels = findGotoAfterLabel(allCommands, commandIndex + 1, label)
end
-- 构建完整的标签列表
local allLabels = {label}
for _, additionalLabel in ipairs(additionalLabels) do
table.insert(allLabels, additionalLabel)
end
local labelString = table.concat(allLabels, " ")
table.insert(options, '<div class="choice-option" data-goto="' .. labelString .. '">')
table.insert(options, '<p>' .. description .. '</p>')
table.insert(options, '</div>')
i = i + 1
end
end
-- 用choice-container包装所有选项
if #options > 0 then
table.insert(result, '<div class="choice-container">')
for _, option in ipairs(options) do
table.insert(result, option)
end
table.insert(result, '</div>')
end
return table.concat(result, "\n")
end
-- 移除连续的空字符串
removeConsecutiveEmptyStrings = function(arr)
local i = 1
while i <= #arr do
if arr[i] == "" then
local j = i
while j <= #arr and arr[j] == "" do
j = j + 1
end
if j > i + 1 then
for k = i + 1, j - 1 do
table.remove(arr, i + 1)
end
end
i = i + 1
else
i = i + 1
end
end
end
-- 移除空的div元素(包括event-label和choice-container)
local function removeEmptyDivs(text)
if not text or type(text) ~= "string" then
return text
end
-- 移除空的event-label div(只包含空白字符、换行符或完全为空)
-- 这些模式会匹配所有event-label类的div,包括带有show类的
-- 模式1:完全为空的event-label div
local pattern1 = '<div%s+class="event%-label[^"]*"[^>]*></div>'
text = text:gsub(pattern1, '')
-- 模式2:只包含空白字符的event-label div
local pattern2 = '<div%s+class="event%-label[^"]*"[^>]*>%s+</div>'
text = text:gsub(pattern2, '')
-- 模式3:只包含换行符和空白字符的event-label div
local pattern3 = '<div%s+class="event%-label[^"]*"[^>]*>[\r\n%s]*</div>'
text = text:gsub(pattern3, '')
-- 移除空的choice-container div
-- 模式4:完全为空的choice-container div
local pattern4 = '<div%s+class="choice%-container"[^>]*></div>'
text = text:gsub(pattern4, '')
-- 模式5:只包含空白字符的choice-container div
local pattern5 = '<div%s+class="choice%-container"[^>]*>%s+</div>'
text = text:gsub(pattern5, '')
-- 模式6:只包含换行符和空白字符的choice-container div
local pattern6 = '<div%s+class="choice%-container"[^>]*>[\r\n%s]*</div>'
text = text:gsub(pattern6, '')
return text
end
-- 处理性别分隔的对话(处理"^"分隔符)
-- 注意:这个函数现在处理已经切割后的单个对话片段,所以逻辑大大简化
processGenderDialogue = function(text, template, characterName)
if not template then
template = "Say"
end
if not text or type(text) ~= "string" then
return text
end
-- 检查是否包含 ${男性内容^女性内容}$ 格式
local hasInlineGender = text:find("%$%{[^%^}]+%^[^}]+%}%$")
if hasInlineGender then
-- 处理 ${男性内容^女性内容}$ 格式
-- 需要生成两个完整的句子
local maleText = text:gsub("%$%{([^%^}]+)%^[^}]+%}%$", "%1")
local femaleText = text:gsub("%$%{[^%^}]+%^([^}]+)%}%$", "%1")
-- 处理表情参数和清理文本,然后创建对话
local maleCleanedText, malePortrait = parsePortraitParameter(maleText)
maleCleanedText = cleanText(maleCleanedText) .. '<span class="trigger-chance">(男)</span>'
local maleResult = createQuote(maleCleanedText, template, malePortrait, characterName)
local femaleCleanedText, femalePortrait = parsePortraitParameter(femaleText)
femaleCleanedText = cleanText(femaleCleanedText) .. '<span class="trigger-chance">(女)</span>'
local femaleResult = createQuote(femaleCleanedText, template, femalePortrait, characterName)
return maleResult .. "\n" .. femaleResult
end
-- 如果没有性别分支,直接处理(这种情况理论上不应该发生,因为调用前已经检查过)
if not text:find("%^") then
local cleanedText, portrait = parsePortraitParameter(text)
cleanedText = cleanText(cleanedText)
return createQuote(cleanedText, template, portrait, characterName)
end
-- 简化的性别分支处理:现在只需要处理单个片段,不会有换行符
-- 直接处理 ^ 分隔符
local caretPos = text:find("%^")
-- 分离男性和女性内容
local beforeCaret = text:sub(1, caretPos - 1)
local afterCaret = text:sub(caretPos + 1)
-- 清理前后空白
local maleText = beforeCaret:match("^%s*(.-)%s*$") or ""
local femaleText = afterCaret:match("^%s*(.-)%s*$") or ""
mw.log(maleText, femaleText)
-- 处理表情参数和清理文本,然后创建对话
local maleCleanedText, malePortrait = parsePortraitParameter(maleText)
maleCleanedText = cleanText(maleCleanedText) .. '<span class="trigger-chance">(男)</span>'
local maleResult = createQuote(maleCleanedText, template, malePortrait, characterName)
local femaleCleanedText, femalePortrait = parsePortraitParameter(femaleText)
femaleCleanedText = cleanText(femaleCleanedText) .. '<span class="trigger-chance">(女)</span>'
local femaleResult = createQuote(femaleCleanedText, template, femalePortrait, characterName)
return maleResult .. "\n" .. femaleResult
end
-- 查找NPC回应
findNpcResponse = function(character, optionKey)
-- 这里需要实现查找NPC回应的逻辑
-- 暂时返回空,后续可以扩展
return nil
end
-- 检测标签路径是否会执行到事件结束
local function doesLabelPathEndEvent(commands, startIndex, targetLabel)
local visited = {} -- 防止无限循环
-- 递归检测函数
local function checkLabelRecursive(labelName)
if visited[labelName] then
return false -- 避免无限循环
end
visited[labelName] = true
for i = startIndex, #commands do
local command = commands[i]:match("^%s*(.-)%s*$")
-- 如果遇到label命令,检查是否是目标标签
if command:match("^label%s+") then
local currentLabelName = command:match("^label%s+(.+)$")
if currentLabelName == labelName then
-- 找到目标标签,继续查找后续命令
for j = i + 1, #commands do
local nextCommand = commands[j]:match("^%s*(.-)%s*$")
-- 如果遇到end命令,说明会执行到事件结束
if nextCommand:match("^end$") or nextCommand:match("^end%s+") then
return true
elseif nextCommand:match("^goto%s+") then
-- 如果遇到goto命令,递归检查goto的目标
local gotoLabels, isConditional = analyzeGotoCommand(nextCommand)
if not isConditional and #gotoLabels > 0 then
-- 无条件goto,递归检查所有目标标签
for _, gotoLabel in ipairs(gotoLabels) do
if checkLabelRecursive(gotoLabel) then
return true
end
end
return false -- 无条件goto但没有找到结束路径
else
-- 条件goto或其他情况,继续检查当前路径
-- 这里不返回,继续循环检查后续命令
end
elseif nextCommand:match("^choose%s+") then
-- 遇到choose命令,停止查找(分支选择)
return false
elseif nextCommand:match("^label%s+") then
-- 遇到新的label,这是隐式跳转,递归检查
local nextLabelName = nextCommand:match("^label%s+(.+)$")
return checkLabelRecursive(nextLabelName)
end
end
return false
end
end
end
return false
end
-- 开始递归检查
return checkLabelRecursive(targetLabel)
end
-- 查找标签后的goto命令,返回额外的标签(递归查找所有隐式跳转)
findGotoAfterLabel = function(commands, startIndex, targetLabel)
local additionalLabels = {}
local visited = {} -- 防止无限循环
-- 递归查找函数
local function findLabelsRecursive(labelName)
if visited[labelName] then
return -- 避免无限循环
end
visited[labelName] = true
for i = startIndex, #commands do
local command = commands[i]:match("^%s*(.-)%s*$")
-- 如果遇到label命令,检查是否是目标标签
if command:match("^label%s+") then
local currentLabelName = command:match("^label%s+(.+)$")
if currentLabelName == labelName then
-- 找到目标标签,继续查找后续的goto命令或检测隐式跳转
for j = i + 1, #commands do
local nextCommand = commands[j]:match("^%s*(.-)%s*$")
-- 如果遇到goto命令
if nextCommand:match("^goto%s+") then
local gotoLabels, isConditional = analyzeGotoCommand(nextCommand)
if not isConditional and #gotoLabels > 0 then
-- 无条件goto,添加标签并递归查找
for _, gotoLabel in ipairs(gotoLabels) do
table.insert(additionalLabels, gotoLabel)
findLabelsRecursive(gotoLabel) -- 递归查找
end
end
return -- 找到goto命令后停止查找
elseif nextCommand:match("^end$") or nextCommand:match("^end%s+") then
-- 遇到end命令,停止查找
return
elseif nextCommand:match("^choose%s+") then
-- 遇到choose命令,停止查找
return
elseif nextCommand:match("^label%s+") then
-- 遇到新的label,这是隐式跳转
local nextLabelName = nextCommand:match("^label%s+(.+)$")
table.insert(additionalLabels, nextLabelName)
findLabelsRecursive(nextLabelName) -- 递归查找
return
end
end
return
end
end
end
end
-- 开始递归查找
findLabelsRecursive(targetLabel)
return additionalLabels
end
-- 查找choose命令后的隐式goto内容(用于"_"标签)
local function findImplicitGotoContent(allCommands, chooseCommandIndex)
local implicitContent = {}
local i = chooseCommandIndex + 1
-- 从choose命令后开始查找,直到遇到关键词为止
while i <= #allCommands do
local command = allCommands[i]:match("^%s*(.-)%s*$")
-- 如果遇到这些关键词,停止收集
if command:match("^goto%s+") or
command:match("^label%s+") then
break
elseif command:match("^end$") or command:match("^end%s+") then
-- end命令也要包含在implicit_goto内容中,然后停止收集
table.insert(implicitContent, command)
i = i + 1
break
end
-- 收集其他命令作为隐式goto的内容(包括choose命令)
table.insert(implicitContent, command)
i = i + 1
end
return implicitContent, i - 1 -- 返回内容和最后处理的命令索引
end
-- 处理choose命令
local function processChooseCommand(command, allCommands, commandIndex)
-- choose "<question>" "<answer1>" <label1> "<answer2>" <label2> ...
local content = command:match("^choose%s+(.+)$")
if not content then
return nil, nil
end
local result = {}
local parts = {}
local inQuote = false
local current = ""
-- 解析choose命令的参数
for i = 1, #content do
local char = content:sub(i, i)
if char == '"' and (i == 1 or content:sub(i-1, i-1) ~= "\\") then
inQuote = not inQuote
if not inQuote then
table.insert(parts, current)
current = ""
end
elseif char == " " and not inQuote then
if current ~= "" then
table.insert(parts, current)
current = ""
end
elseif inQuote then
current = current .. char
else
current = current .. char
end
end
if current ~= "" then
table.insert(parts, current)
end
local skipToIndex = nil -- 需要跳过的命令索引
-- 检查隐式goto内容中是否有splitSpeak命令
local hasSplitSpeak = false
local splitSpeakDialogues = {}
local splitSpeakCommandIndex = nil
if #parts >= 3 then
local question = parts[1]
-- 显示问题(如果不为空)
if question and question ~= "" then
-- 检查是否为翻译键或Characters/Dialogue引用
if question:match("^Strings/") or question:match("^Characters/Dialogue/") then
question = getTranslationText(question)
end
table.insert(result, choice())
-- 此处的对话应当不会有性别分支
table.insert(result, processDialogueUnified(question, nil, nil))
else
table.insert(result, choice())
end
-- 检查是否有"_"标签的隐式goto
local hasImplicitGoto = false
local implicitGotoLabels = {}
local implicitGotoContent = nil
local implicitGotoEndIndex = nil
-- 扫描所有选项,查找"_"标签
local j = 2
while j < #parts do
local label = parts[j + 1]
if label == "_" then
hasImplicitGoto = true
break
end
j = j + 2
end
-- 如果有隐式goto,处理相关内容
if hasImplicitGoto and allCommands and commandIndex then
implicitGotoContent, implicitGotoEndIndex = findImplicitGotoContent(allCommands, commandIndex)
skipToIndex = implicitGotoEndIndex -- 设置需要跳过的索引
-- 在隐式goto内容中查找splitSpeak命令
if implicitGotoContent then
for cmdIndex, cmd in ipairs(implicitGotoContent) do
if cmd:match("^splitSpeak%s+") then
hasSplitSpeak = true
splitSpeakCommandIndex = commandIndex + cmdIndex
-- 解析splitSpeak命令
local splitSpeakContent = cmd:match("^splitSpeak%s+(.+)$")
if splitSpeakContent then
local splitParts = {}
local splitInQuote = false
local splitCurrent = ""
-- 解析splitSpeak的参数
for i = 1, #splitSpeakContent do
local char = splitSpeakContent:sub(i, i)
if char == '"' and (i == 1 or splitSpeakContent:sub(i-1, i-1) ~= "\\") then
splitInQuote = not splitInQuote
if not splitInQuote then
table.insert(splitParts, splitCurrent)
splitCurrent = ""
end
elseif char == " " and not splitInQuote then
if splitCurrent ~= "" then
table.insert(splitParts, splitCurrent)
splitCurrent = ""
end
elseif splitInQuote then
splitCurrent = splitCurrent .. char
else
splitCurrent = splitCurrent .. char
end
end
if splitCurrent ~= "" then
table.insert(splitParts, splitCurrent)
end
-- 第一个参数是角色名,后面的是对话内容
if #splitParts >= 2 then
local character = splitParts[1]
for i = 2, #splitParts do
local dialogue = splitParts[i]
if dialogue:match("^Strings/") or dialogue:match("^Characters/Dialogue/") then
dialogue = getTranslationText(dialogue)
end
table.insert(splitSpeakDialogues, {character = character, text = dialogue})
end
end
end
break
end
end
end
-- 为所有"_"标签使用同一个标签名,因为它们的内容是相同的
local sharedImplicitLabel = "implicit_goto_" .. commandIndex
j = 2
while j < #parts do
local label = parts[j + 1]
if label == "_" then
implicitGotoLabels[j] = sharedImplicitLabel
end
j = j + 2
end
end
-- 处理选项
j = 2
local optionIndex = 1 -- 用于splitSpeak对话的索引
while j < #parts do
local answer = parts[j]
local label = parts[j + 1]
if answer and label then
-- 检查是否为翻译键或Characters/Dialogue引用
if answer:match("^Strings/") or answer:match("^Characters/Dialogue/") then
answer = getTranslationText(answer)
end
local finalLabel = label
local additionalLabels = {}
-- 如果有splitSpeak且是"_"标签,创建特殊的标签来处理splitSpeak对话
if hasSplitSpeak and label == "_" then
finalLabel = "splitspeak_option_" .. optionIndex .. "_" .. commandIndex
-- splitSpeak后面会继续接着通用对话,所以需要添加implicit_goto标签
if implicitGotoLabels[j] then
table.insert(additionalLabels, implicitGotoLabels[j])
end
-- 查找隐式goto内容后的goto命令
if implicitGotoEndIndex and allCommands then
-- 检查隐式内容结束后是否有goto命令
if implicitGotoEndIndex + 1 <= #allCommands then
local nextCommand = allCommands[implicitGotoEndIndex + 1]:match("^%s*(.-)%s*$")
if nextCommand:match("^goto%s+") then
local gotoLabels, isConditional = analyzeGotoCommand(nextCommand)
if not isConditional then
for _, gotoLabel in ipairs(gotoLabels) do
table.insert(additionalLabels, gotoLabel)
-- 递归查找后续的goto
local moreLabels = findGotoAfterLabel(allCommands, implicitGotoEndIndex + 2, gotoLabel)
for _, moreLabel in ipairs(moreLabels) do
table.insert(additionalLabels, moreLabel)
end
end
end
-- 如果有goto命令,也需要跳过它
skipToIndex = implicitGotoEndIndex + 1
end
end
end
elseif label == "_" and implicitGotoLabels[j] then
-- 如果是"_"标签但没有splitSpeak,使用生成的隐式标签
finalLabel = implicitGotoLabels[j]
-- 查找隐式goto内容后的goto命令
if implicitGotoEndIndex and allCommands then
-- 检查隐式内容结束后是否有goto命令
if implicitGotoEndIndex + 1 <= #allCommands then
local nextCommand = allCommands[implicitGotoEndIndex + 1]:match("^%s*(.-)%s*$")
if nextCommand:match("^goto%s+") then
local gotoLabels, isConditional = analyzeGotoCommand(nextCommand)
if not isConditional then
for _, gotoLabel in ipairs(gotoLabels) do
table.insert(additionalLabels, gotoLabel)
-- 递归查找后续的goto
local moreLabels = findGotoAfterLabel(allCommands, implicitGotoEndIndex + 2, gotoLabel)
for _, moreLabel in ipairs(moreLabels) do
table.insert(additionalLabels, moreLabel)
end
end
end
-- 如果有goto命令,也需要跳过它
skipToIndex = implicitGotoEndIndex + 1
end
end
end
else
-- 普通标签,查找标签后的goto命令和隐式跳转
if allCommands and commandIndex then
additionalLabels = findGotoAfterLabel(allCommands, commandIndex + 1, label)
end
end
-- 构建完整的标签列表
local allLabels = {finalLabel}
for _, additionalLabel in ipairs(additionalLabels) do
table.insert(allLabels, additionalLabel)
end
-- 检查这个选项是否会执行到事件结束,如果是则添加event-reactions
if allCommands and commandIndex then
local willEndEvent = false
-- 检查主标签是否会执行到事件结束
if doesLabelPathEndEvent(allCommands, commandIndex + 1, finalLabel) then
willEndEvent = true
else
-- 检查所有附加标签是否会执行到事件结束
for _, additionalLabel in ipairs(additionalLabels) do
if doesLabelPathEndEvent(allCommands, commandIndex + 1, additionalLabel) then
willEndEvent = true
break
end
end
end
-- 如果会执行到事件结束,添加event-reactions
if willEndEvent then
table.insert(allLabels, "event-reactions")
end
end
local labelString = table.concat(allLabels, " ")
-- 分析label中的好感度变化
local friendship, character = analyzeLabelFriendship(label == "_" and finalLabel or label, allCommands, commandIndex)
local characterName = nil
if character then
characterName = getCharacterName(character)
end
local choiceTemplate = createPlayerChoice(answer, friendship, characterName)
-- 使用完整的标签列表作为goto目标
table.insert(result, wrapInText(choiceTemplate, labelString))
end
j = j + 2
optionIndex = optionIndex + 1 -- 递增选项索引
end
table.insert(result, div())
-- 如果有splitSpeak,为每个选项创建对应的event-label
if hasSplitSpeak and #splitSpeakDialogues > 0 then
for i = 1, #splitSpeakDialogues do
local dialogue = splitSpeakDialogues[i]
local labelName = "splitspeak_option_" .. i .. "_" .. commandIndex
table.insert(result, '<div class="event-label" data-label="' .. labelName .. '" style="display:none;">')
-- 处理对话文本
local processedDialogue = processDialogueUnified(dialogue.text, nil, getCharacterName(dialogue.character))
table.insert(result, processedDialogue)
table.insert(result, '</div>')
end
end
-- 如果有隐式goto内容,添加对应的event-label(放在splitSpeak后面)
if hasImplicitGoto and implicitGotoContent then
-- 使用一个集合来跟踪已经创建的标签,避免重复
local createdLabels = {}
for labelIndex, implicitLabel in pairs(implicitGotoLabels) do
-- 如果这个标签已经创建过,跳过
if not createdLabels[implicitLabel] then
createdLabels[implicitLabel] = true
-- 处理隐式goto的内容
local implicitDialogue = {}
for cmdIndex, cmd in ipairs(implicitGotoContent) do
-- 跳过splitSpeak命令,因为它已经单独处理了
if cmd:match("^splitSpeak%s+") then
-- 跳过splitSpeak命令,不处理
-- 处理speak命令
elseif cmd:match("^speak%s+") then
local character, text = cmd:match("^speak%s+([^%s]+)%s+\"(.*)\"$")
if character and text then
if text:match("^Strings/") or text:match("^Characters/Dialogue/") then
text = getTranslationText(text)
end
table.insert(implicitDialogue, processDialogueUnified(text, nil, getCharacterName(character)))
end
-- 处理message命令
elseif cmd:match("^message%s+") then
local messageText = cmd:match("^message%s+\"(.*)\"$")
if messageText then
if messageText:match("^Strings/") or messageText:match("^Characters/Dialogue/") then
messageText = getTranslationText(messageText)
else
messageText = cleanText(messageText)
end
if not messageText:find(":") then
messageText = messageText -- "(旁白)"
end
table.insert(implicitDialogue, processDialogueUnified(messageText, nil, nil))
end
-- 处理dialogue命令
elseif cmd:match("^dialogue%s+") then
local character, text = cmd:match("^dialogue%s+([^%s]+)%s+\"(.*)\"$")
if character and text then
if text:match("^Strings/") or text:match("^Characters/Dialogue/") then
text = getTranslationText(text)
else
text = cleanText(text)
end
table.insert(implicitDialogue, processDialogueUnified(text, nil, getCharacterName(character)))
end
-- 处理textAboveHead命令
elseif cmd:match("^textAboveHead%s+") then
local actor, headText = cmd:match("^textAboveHead%s+([^%s]+)%s+\"(.*)\"$")
if actor and headText then
if headText:match("^Strings/") or headText:match("^Characters/Dialogue/") then
headText = getTranslationText(headText)
else
headText = cleanText(headText)
end
table.insert(implicitDialogue, processDialogueUnified(headText, nil, getCharacterName(actor) .. "(对话气泡)"))
end
-- 处理choose命令
elseif cmd:match("^choose%s+") then
-- 处理choose命令,需要传递完整的命令列表以便正确分析好感度
-- 隐式goto内容从第一个choose命令后开始,所以实际索引是 commandIndex + cmdIndex
local actualCommandIndex = commandIndex + cmdIndex
local choiceResult, _ = processChooseCommand(cmd, allCommands, actualCommandIndex)
if choiceResult then
for _, line in ipairs(choiceResult) do
table.insert(implicitDialogue, line)
end
end
-- 处理end命令
elseif cmd:match("^end$") or cmd:match("^end%s+") then
table.insert(implicitDialogue, "[ 事件结束 ]")
-- 可以添加其他命令的处理
end
end
-- 创建event-label包装隐式goto内容
if #implicitDialogue > 0 then
table.insert(result, '<div class="event-label" data-label="' .. implicitLabel .. '" style="display:none;">')
for _, dialogue in ipairs(implicitDialogue) do
table.insert(result, dialogue)
end
table.insert(result, '</div>')
end
end
end
end
end
return result, skipToIndex
end
-- 处理cutscene命令
local function processCutsceneCommand(command, allCommands, commandIndex)
-- cutscene <minigame> [<label1> <label2> ...]
local content = command:match("^cutscene%s+(.+)$")
if not content then
return nil
end
local parts = {}
-- 简单按空格分割参数(cutscene命令的参数不包含引号)
for word in content:gmatch("%S+") do
table.insert(parts, word)
end
-- 如果只有一个参数(如 cutscene marucomet),这只是播放动画,不需要创建选项
if #parts < 2 then
return nil
end
local minigame = parts[1]
local result = {}
-- 创建选项容器
table.insert(result, '<div class="choice-container">')
-- 处理每个标签选项
for i = 2, #parts do
local label = parts[i]
local cutsceneKey = minigame .. "_" .. label
local description = cutsceneLabels[cutsceneKey] or ("选择:" .. label)
-- 查找标签后的goto命令和隐式跳转
local additionalLabels = {}
if allCommands and commandIndex then
additionalLabels = findGotoAfterLabel(allCommands, commandIndex + 1, label)
end
-- 构建完整的标签列表
local allLabels = {label}
for _, additionalLabel in ipairs(additionalLabels) do
table.insert(allLabels, additionalLabel)
end
-- 检查这个选项是否会执行到事件结束,如果是则添加event-reactions
if allCommands and commandIndex then
local willEndEvent = false
-- 检查主标签是否会执行到事件结束
if doesLabelPathEndEvent(allCommands, commandIndex + 1, label) then
willEndEvent = true
else
-- 检查所有附加标签是否会执行到事件结束
for _, additionalLabel in ipairs(additionalLabels) do
if doesLabelPathEndEvent(allCommands, commandIndex + 1, additionalLabel) then
willEndEvent = true
break
end
end
end
-- 如果会执行到事件结束,添加event-reactions
if willEndEvent then
table.insert(allLabels, "event-reactions")
end
end
local labelString = table.concat(allLabels, " ")
-- 创建选项
table.insert(result, '<div class="choice-option" data-goto="' .. labelString .. '">')
table.insert(result, '<p>' .. description .. '</p>')
table.insert(result, '</div>')
end
table.insert(result, '</div>')
return result
end
-- 处理conditionSpeak命令
local function processConditionSpeakCommand(command)
-- conditionSpeak <npc> [<dialogue> [condition]]+
local content = command:match("^conditionSpeak%s+(.+)$")
if not content then
return nil
end
local parts = {}
local inQuote = false
local current = ""
-- 解析参数,处理引号
for i = 1, #content do
local char = content:sub(i, i)
if char == '"' and (i == 1 or content:sub(i-1, i-1) ~= "\\") then
inQuote = not inQuote
if not inQuote then
table.insert(parts, current)
current = ""
end
elseif char == " " and not inQuote then
if current ~= "" then
table.insert(parts, current)
current = ""
end
elseif inQuote then
current = current .. char
else
current = current .. char
end
end
if current ~= "" then
table.insert(parts, current)
end
if #parts >= 2 then
local character = parts[1]:match("^%s*(.-)%s*$")
local dialogue = parts[2]
if character and dialogue then
-- 检查是否为翻译键或Characters/Dialogue引用
if dialogue:match("^Strings/") or dialogue:match("^Characters/Dialogue/") then
dialogue = getTranslationText(dialogue)
else
dialogue = cleanText(dialogue)
end
-- 处理性别分支
return processDialogueUnified(dialogue, nil, getCharacterName(character))
end
end
return nil
end
-- 处理事件对话,过滤掉指令,只保留文本内容
processEventDialogue = function(eventScript, eventLocation, eventId)
if not eventScript or type(eventScript) ~= "string" then
return eventScript
end
-- 事件脚本由斜杠分隔的命令组成
local commands = {}
local currentPos = 1
local inQuotes = false
-- Label管理变量
local currentLabel = nil
local labelStack = {}
local currentCommand = ""
-- 解析命令,考虑引号内的内容
for i = 1, #eventScript do
local char = eventScript:sub(i, i)
if char == '"' and (i == 1 or eventScript:sub(i - 1, i - 1) ~= "\\") then
inQuotes = not inQuotes
currentCommand = currentCommand .. char
elseif char == "/" and not inQuotes then
if currentCommand ~= "" then
table.insert(commands, currentCommand)
currentCommand = ""
end
else
currentCommand = currentCommand .. char
end
end
-- 添加最后一个命令
if currentCommand ~= "" then
table.insert(commands, currentCommand)
end
local dialogueTexts = {}
local currentLocation = eventLocation
local i = 1
local hasChoiceContainer = false
local preChoiceContentAdded = false
-- 检查是否有choose命令或条件goto命令
for _, cmd in ipairs(commands) do
local trimmedCmd = cmd:match("^%s*(.-)%s*$")
if trimmedCmd:match("^choose%s+") then
hasChoiceContainer = true
break
elseif trimmedCmd:match("^goto%s+") and trimmedCmd:find('"') then
-- 条件goto命令也会产生选择
hasChoiceContainer = true
break
end
end
-- 处理命令序列
while i <= #commands do
local trimmedCommand = commands[i]:match("^%s*(.-)%s*$")
-- 处理speak命令:speak <character> "<text>"
if trimmedCommand:match("^speak%s+") then
local character, text = trimmedCommand:match("^speak%s+([^%s]+)%s+\"(.*)\"$")
if character and text then
-- 检查是否为翻译键或Characters/Dialogue引用
if text:match("^Strings/") or text:match("^Characters/Dialogue/") then
text = getTranslationText(text)
end
-- 处理性别分支
table.insert(dialogueTexts, processDialogueUnified(text, nil, getCharacterName(character)))
end
i = i + 1
-- 处理choose命令(1.6.16新命令)
elseif trimmedCommand:match("^choose%s+") then
-- 如果不在label内且有choose命令且还没有添加预选择内容包装,现在添加
if not currentLabel and hasChoiceContainer and not preChoiceContentAdded then
-- 将之前的内容包装在默认显示的event-label中
if #dialogueTexts > 0 then
local preChoiceContent = table.concat(dialogueTexts, "\n")
-- 检查内容是否为空(去除空白字符后)
if preChoiceContent:match("^%s*$") == nil then
dialogueTexts = {}
table.insert(dialogueTexts, '<div class="event-label show" style="display: block;">')
table.insert(dialogueTexts, preChoiceContent)
table.insert(dialogueTexts, '</div>')
else
dialogueTexts = {}
end
end
preChoiceContentAdded = true
end
local choiceResult, skipToIndex = processChooseCommand(trimmedCommand, commands, i)
if choiceResult then
-- 如果在label内,choice-container应该包含在当前label内
-- 如果不在label内,choice-container会被包装在预选择内容中或直接添加
for _, line in ipairs(choiceResult) do
table.insert(dialogueTexts, line)
end
end
-- 如果有需要跳过的索引,跳过隐式goto内容
if skipToIndex and skipToIndex > i then
i = skipToIndex + 1
else
i = i + 1
end
-- 处理goto命令(1.6.16新命令)
elseif trimmedCommand:match("^goto%s+") then
-- 检查是否是条件goto(包含引号)
if trimmedCommand:find('"') then
-- 如果不在label内且有条件goto命令且还没有添加预选择内容包装,现在添加
if not currentLabel and hasChoiceContainer and not preChoiceContentAdded then
-- 将之前的内容包装在默认显示的event-label中
if #dialogueTexts > 0 then
local preChoiceContent = table.concat(dialogueTexts, "\n")
-- 检查内容是否为空(去除空白字符后)
if preChoiceContent:match("^%s*$") == nil then
dialogueTexts = {}
table.insert(dialogueTexts, '<div class="event-label show" style="display: block;">')
table.insert(dialogueTexts, preChoiceContent)
table.insert(dialogueTexts, '</div>')
else
dialogueTexts = {}
end
end
preChoiceContentAdded = true
end
local conditionalResult = processConditionalGoto(trimmedCommand, eventId, commands, i)
if conditionalResult then
table.insert(dialogueTexts, conditionalResult)
end
else
-- 简单goto命令 - 不显示跳转信息,因为它会在选项的data-label中处理
-- 这里什么都不做,让goto命令静默执行
end
i = i + 1
-- 处理label命令(1.6.16新命令)
elseif trimmedCommand:match("^label%s+") then
local labelName = trimmedCommand:match("^label%s+(.+)$")
if labelName then
-- 检查是否是skippedItem标签,如果是则跳过到下一个/end
if labelName == "skippedItem" then
-- 跳过skippedItem标签的所有内容直到找到/end
local skipIndex = i + 1
while skipIndex <= #commands do
local skipCommand = commands[skipIndex]:match("^%s*(.-)%s*$")
if skipCommand:match("^end$") or skipCommand:match("^end%s+") then
-- 找到end命令,检查是否有end dialogue内容需要处理
local endContent = skipCommand:match("^end%s+(.+)$")
if endContent and endContent:match("^dialogue%s+") then
-- 将end dialogue内容添加到前面的内容中
local character, text = endContent:match("^dialogue%s+([^%s]+)%s+\"(.*)\"$")
if character and text then
-- 检查是否为翻译键或Characters/Dialogue引用
if text:match("^Strings/") or text:match("^Characters/Dialogue/") then
text = getTranslationText(text)
else
text = text:gsub("\\\"", "\"")
text = text:gsub("\\n", "\n")
end
table.insert(dialogueTexts, processDialogueUnified(text, nil, getCharacterName(character)))
end
end
-- 添加事件结束标记到前面的内容中
table.insert(dialogueTexts, "[ 事件结束 ]")
-- 跳过到end命令之后
i = skipIndex + 1
break
end
skipIndex = skipIndex + 1
end
-- 如果没找到end命令,跳过到最后
if skipIndex > #commands then
i = #commands + 1
end
else
-- 正常处理其他标签
-- 如果之前有未关闭的label,需要检查是否应该添加隐式goto
if currentLabel then
-- 检查前一个命令是否是end、goto或choose,如果不是,则添加隐式goto
local shouldAddImplicitGoto = true
if i > 1 then
local prevCommand = commands[i - 1]:match("^%s*(.-)%s*$")
if prevCommand:match("^end$") or prevCommand:match("^end%s+") or
prevCommand:match("^goto%s+") or prevCommand:match("^choose%s+") then
shouldAddImplicitGoto = false
end
end
-- 总是关闭之前的label并开始新的label
-- 隐式跳转将在findGotoAfterLabel中检测
table.insert(dialogueTexts, '</div>')
currentLabel = labelName
table.insert(dialogueTexts, '<div class="event-label" data-label="' .. labelName .. '" style="display:none;">')
else
-- 开始一个新的label区域
currentLabel = labelName
table.insert(dialogueTexts, '<div class="event-label" data-label="' .. labelName .. '" style="display:none;">')
end
i = i + 1
end
else
i = i + 1
end
-- 处理conditionSpeak命令(1.6.16新命令)
elseif trimmedCommand:match("^conditionSpeak%s+") then
local conditionResult = processConditionSpeakCommand(trimmedCommand)
if conditionResult then
table.insert(dialogueTexts, conditionResult)
end
i = i + 1
-- 处理splitSpeak命令(兼容旧版本,但通常与choose命令一起使用)
elseif trimmedCommand:match("^splitSpeak%s+") then
-- splitSpeak通常在choose命令中处理,这里只是为了兼容性
-- 如果单独出现,可以作为普通对话处理
local splitSpeakContent = trimmedCommand:match("^splitSpeak%s+(.+)$")
if splitSpeakContent then
local splitParts = {}
local splitInQuote = false
local splitCurrent = ""
-- 解析splitSpeak的参数
for j = 1, #splitSpeakContent do
local char = splitSpeakContent:sub(j, j)
if char == '"' and (j == 1 or splitSpeakContent:sub(j-1, j-1) ~= "\\") then
splitInQuote = not splitInQuote
if not splitInQuote then
table.insert(splitParts, splitCurrent)
splitCurrent = ""
end
elseif char == " " and not splitInQuote then
if splitCurrent ~= "" then
table.insert(splitParts, splitCurrent)
splitCurrent = ""
end
elseif splitInQuote then
splitCurrent = splitCurrent .. char
else
splitCurrent = splitCurrent .. char
end
end
if splitCurrent ~= "" then
table.insert(splitParts, splitCurrent)
end
-- 第一个参数是角色名,后面的是对话内容
if #splitParts >= 2 then
local character = splitParts[1]
for j = 2, #splitParts do
local dialogue = splitParts[j]
if dialogue:match("^Strings/") or dialogue:match("^Characters/Dialogue/") then
dialogue = getTranslationText(dialogue)
end
table.insert(dialogueTexts, processDialogueUnified(dialogue, nil, getCharacterName(character)))
end
end
end
i = i + 1
-- 处理message命令:message "<text>"
elseif trimmedCommand:match("^message%s+") then
local messageText = trimmedCommand:match("^message%s+\"(.*)\"$")
if messageText then
-- 检查是否为翻译键或Characters/Dialogue引用
if messageText:match("^Strings/") or messageText:match("^Characters/Dialogue/") then
messageText = getTranslationText(messageText)
else
messageText = cleanText(messageText)
end
if not messageText:find(":") then
messageText = messageText -- "(旁白)"
end
-- 处理性别分支
table.insert(dialogueTexts, processDialogueUnified(messageText, nil, nil))
end
i = i + 1
-- 处理textAboveHead命令:textAboveHead <actor> "<text>"
elseif trimmedCommand:match("^textAboveHead%s+") then
local actor, headText = trimmedCommand:match("^textAboveHead%s+([^%s]+)%s+\"(.*)\"$")
if actor and headText then
-- 检查是否为翻译键或Characters/Dialogue引用
if headText:match("^Strings/") or headText:match("^Characters/Dialogue/") then
headText = getTranslationText(headText)
else
headText = cleanText(headText)
end
-- 处理性别分支
table.insert(dialogueTexts, processDialogueUnified(headText, nil, getCharacterName(actor) .. "(对话气泡)"))
end
i = i + 1
-- 处理dialogue命令:dialogue <character> "<text>"
elseif trimmedCommand:match("^dialogue%s+") then
local character, text = trimmedCommand:match("^dialogue%s+([^%s]+)%s+\"(.*)\"$")
if character and text then
-- 检查是否为翻译键或Characters/Dialogue引用
if text:match("^Strings/") or text:match("^Characters/Dialogue/") then
text = getTranslationText(text)
else
text = cleanText(text)
end
-- 处理性别分支
table.insert(dialogueTexts, processDialogueUnified(text, nil, getCharacterName(character)))
end
i = i + 1
-- 处理cutscene命令:cutscene <minigame> [<label1> <label2> ...]
elseif trimmedCommand:match("^cutscene%s+") then
local cutsceneResult = processCutsceneCommand(trimmedCommand, commands, i)
-- 只有当cutscene命令返回选项时才进行预选择内容包装
if cutsceneResult then
-- 如果不在label内且有cutscene命令且还没有添加预选择内容包装,现在添加
if not currentLabel and hasChoiceContainer and not preChoiceContentAdded then
-- 将之前的内容包装在默认显示的event-label中
if #dialogueTexts > 0 then
local preChoiceContent = table.concat(dialogueTexts, "\n")
-- 检查内容是否为空(去除空白字符后)
if preChoiceContent:match("^%s*$") == nil then
dialogueTexts = {}
table.insert(dialogueTexts, '<div class="event-label show" style="display: block;">')
table.insert(dialogueTexts, preChoiceContent)
table.insert(dialogueTexts, '</div>')
else
dialogueTexts = {}
end
end
preChoiceContentAdded = true
end
for _, line in ipairs(cutsceneResult) do
table.insert(dialogueTexts, line)
end
end
-- 如果cutsceneResult为nil,说明这是一个简单的动画播放命令,不需要特殊处理
i = i + 1
-- 处理end命令
elseif trimmedCommand:match("^end$") or trimmedCommand:match("^end%s+") then
-- 检查是否已经在skippedItem处理中添加了事件结束标记
local alreadyHasEndMarker = false
if #dialogueTexts > 0 then
local lastText = dialogueTexts[#dialogueTexts]
if lastText == "[ 事件结束 ]" then
alreadyHasEndMarker = true
end
end
if not alreadyHasEndMarker then
local endContent = trimmedCommand:match("^end%s+(.+)$")
table.insert(dialogueTexts, "[ 事件结束 ]")
-- 处理end dialogue命令
if endContent and endContent:match("^dialogue%s+") then
-- 解析dialogue命令:dialogue <character> "<text>"
local character, text = endContent:match("^dialogue%s+([^%s]+)%s+\"(.*)\"$")
if character and text then
-- 检查是否为翻译键或Characters/Dialogue引用
if text:match("^Strings/") or text:match("^Characters/Dialogue/") then
text = getTranslationText(text)
else
-- 处理普通文本的转义字符
text = text:gsub("\\\"", "\"")
text = text:gsub("\\n", "\n")
end
-- 创建一个新的event-label来包装事件结束后的对话
table.insert(dialogueTexts, '<div class="event-label" style="display: block;">')
table.insert(dialogueTexts, '<p><strong>事件后续对话</strong></p>')
table.insert(dialogueTexts, processDialogueUnified(text, nil, getCharacterName(character)))
table.insert(dialogueTexts, '</div>')
end
end
else
-- 如果已经有事件结束标记,只处理end dialogue命令
local endContent = trimmedCommand:match("^end%s+(.+)$")
if endContent and endContent:match("^dialogue%s+") then
-- 解析dialogue命令:dialogue <character> "<text>"
local character, text = endContent:match("^dialogue%s+([^%s]+)%s+\"(.*)\"$")
if character and text then
-- 检查是否为翻译键或Characters/Dialogue引用
if text:match("^Strings/") or text:match("^Characters/Dialogue/") then
text = getTranslationText(text)
else
-- 处理普通文本的转义字符
text = text:gsub("\\\"", "\"")
text = text:gsub("\\n", "\n")
end
-- 创建一个新的event-label来包装事件结束后的对话
table.insert(dialogueTexts, '<div class="event-label" style="display: block;">')
table.insert(dialogueTexts, '<p><strong>事件后续对话</strong></p>')
table.insert(dialogueTexts, processDialogueUnified(text, nil, getCharacterName(character)))
table.insert(dialogueTexts, '</div>')
end
end
end
-- 如果有当前label,立即关闭它,确保事件结束标签在容器内
if currentLabel then
table.insert(dialogueTexts, '</div>')
currentLabel = nil
end
i = i + 1
else
i = i + 1
end
end
-- 关闭任何未关闭的label区域
if currentLabel then
table.insert(dialogueTexts, '</div>')
currentLabel = nil
end
-- 如果没有choose命令但有对话内容,包装在默认显示的event-label中
if not hasChoiceContainer and #dialogueTexts > 0 then
local allContent = table.concat(dialogueTexts, "\n")
-- 检查内容是否为空(去除空白字符后)
if allContent:match("^%s*$") == nil then
dialogueTexts = {}
table.insert(dialogueTexts, '<div class="event-label show" style="display: block;">')
table.insert(dialogueTexts, allContent)
table.insert(dialogueTexts, '</div>')
else
dialogueTexts = {}
end
end
-- 如果没有找到对话文本,返回原始内容的简化版本
if #dialogueTexts == 0 then
return "(事件脚本,包含复杂指令)"
end
removeConsecutiveEmptyStrings(dialogueTexts)
local result = table.concat(dialogueTexts, "\n")
-- 移除空的div元素
result = removeEmptyDivs(result)
-- 包装在事件容器中
-- 移动了
if result and result ~= "" then
result = result
end
return result
end
-- 根据事件编号和地点查找并处理事件对话
p.event = function(frame)
local args = frame.args or frame
local eventId = args[1]
local location = args[2]
local mainOnly = args[3] or nil
if not mainOnly or mainOnly == "" then
mainOnly = false
elseif mainOnly == "否" then
mainOnly = true
else
mainOnly = false
end
if not eventId or eventId == "" then
return "错误:请提供事件编号"
end
local mainResult = ""
local foundLocation = ""
local foundEventScript = ""
if not location or location == "" then
-- 在所有地点查找事件
if not EventsData then
return "错误:EventsData未加载"
end
for locationName, locationData in pairs(EventsData) do
if type(locationData) == "table" then
for eventKey, eventScript in pairs(locationData) do
-- 检查事件key是否包含指定的事件编号
if eventKey:match("^" .. eventId .. "/") or eventKey == eventId then
local result = processEventDialogue(eventScript, locationName, eventId)
if result and result ~= "(事件脚本,包含复杂指令)" then
mainResult = "<!-- 事件 " .. eventId .. " (位置: " .. locationName .. ") -->" .. result
foundLocation = locationName
foundEventScript = eventScript
break
end
end
end
if mainResult ~= "" then
break
end
end
end
if mainResult == "" then
return "未找到编号为 " .. eventId .. " 的事件"
end
else
-- 在指定地点查找事件
if not EventsData or not EventsData[location] then
return "未找到地点为 " .. location .. " 的事件"
end
for key, eventScript in pairs(EventsData[location]) do
-- 检查事件key是否包含指定的事件编号
if key:match("^" .. eventId .. "/") or key == eventId then
local result = processEventDialogue(eventScript, location, eventId)
if result and result ~= "(事件脚本,包含复杂指令)" then
mainResult = "<!-- 事件 " .. eventId .. " (位置: " .. location .. ") -->" .. result
foundLocation = location
foundEventScript = eventScript
break
end
end
end
if mainResult == "" then
return "在地点 " .. location .. " 未找到编号为 " .. eventId .. " 的事件"
end
end
-- 查找事件反应对话
local reactions = findEventReactions(eventId)
local reactionsText = formatEventReactions(eventId, reactions, foundEventScript)
if mainOnly then
if reactionsText and reactionsText ~= '' then
reactionsText = "\n<div class=\"event-label show\" data-label=\"event-reactions\" style=\"display:block;\">\n" .. reactionsText .. "\n</div>" -- <p>'''事件的后续'''</p>\n
end
frame:callParserFunction('#vardefine', {'event_reaction_' .. eventId, reactionsText})
return '<div class="event-container" data-event="' .. (eventId or "unknown") .. '">\n' .. mainResult .. '\n</div>'
end
if reactionsText and reactionsText ~= '' then
-- 检查主事件是否包含选择容器
local hasChoices = mainResult:find('<div class="choice%-container">')
if hasChoices then
-- 有选择的事件,event-reactions默认隐藏
reactionsText = "\n<div class=\"event-label\" data-label=\"event-reactions\" style=\"display:none;\">\n<p>'''事件的后续'''</p>\n" .. reactionsText .. "\n</div>"
else
-- 没有选择的事件,event-reactions直接显示
reactionsText = "\n<div class=\"event-label show\" data-label=\"event-reactions\" style=\"display:block;\">\n<p>'''事件的后续'''</p>\n" .. reactionsText .. "\n</div>"
end
end
-- 合并主事件对话和反应对话
local finalResult = '<div class="event-container" data-event="' .. (eventId or "unknown") .. '">\n' .. mainResult .. reactionsText .. '\n</div>'
-- 移除空的div元素
finalResult = removeEmptyDivs(finalResult)
return finalResult
end
-- 分析事件脚本中人物出现次数
local function analyzeEventCharacters(eventScript)
if not eventScript or type(eventScript) ~= "string" then
return {}
end
local characterCounts = {}
-- 解析事件脚本中的speak命令
for character in eventScript:gmatch("speak%s+([^%s]+)%s+") do
if character and character ~= "" then
characterCounts[character] = (characterCounts[character] or 0) + 1
end
end
-- 解析conditionSpeak命令
for character in eventScript:gmatch("conditionSpeak%s+([^%s]+)%s+") do
if character and character ~= "" then
characterCounts[character] = (characterCounts[character] or 0) + 1
end
end
-- 解析其他可能包含人物名的命令(如warp、faceDirection等)
for character in eventScript:gmatch("warp%s+([^%s]+)%s+") do
if character and character ~= "" and character ~= "farmer" then
characterCounts[character] = (characterCounts[character] or 0) + 0.5 -- 权重较低
end
end
for character in eventScript:gmatch("faceDirection%s+([^%s]+)%s+") do
if character and character ~= "" and character ~= "farmer" then
characterCounts[character] = (characterCounts[character] or 0) + 0.3 -- 权重更低
end
end
return characterCounts
end
-- 查找所有村民对事件的反应对话
findEventReactions = function(eventId)
if not eventId or not TalkData then
return {}
end
local reactions = {}
-- 遍历所有NPC数据
for npcName, npcData in pairs(TalkData) do
-- 跳过Events/位置数据,只处理NPC数据
if not npcName:match("^Events/") and type(npcData) == "table" then
for key, dialogue in pairs(npcData) do
-- 查找eventSeen_事件编号相关的对话
if key:match("^eventSeen_" .. eventId .. "$") or key:match("^eventSeen_" .. eventId .. "_") then
if not reactions[npcName] then
reactions[npcName] = {}
end
reactions[npcName][key] = dialogue
end
end
end
end
-- mw.logObject(reactions)
return reactions
end
-- 格式化事件反应对话
formatEventReactions = function(eventId, reactions, eventScript)
if not reactions or not next(reactions) then
return ""
end
local output = {}
-- table.insert(output, )
-- 分析事件脚本中的人物出现次数
local characterCounts = {}
if eventScript then
characterCounts = analyzeEventCharacters(eventScript)
end
-- 按时间分组收集对话
local timeGroups = {}
for npcName, dialogues in pairs(reactions) do
local appearanceCount = characterCounts[npcName] or 0
for key, dialogue in pairs(dialogues) do
-- 确定时间分组
local timeGroup = "触发后"
local timeFrame = key:match("^eventSeen_" .. eventId .. "_memory_(.+)$")
if timeFrame then
if timeFrame == "oneday" then
timeGroup = "1 天后"
elseif timeFrame == "oneweek" then
timeGroup = "1 周后"
elseif timeFrame == "oneyear" then
timeGroup = "1 年后"
elseif timeFrame == "twoweeks" then
timeGroup = "2 周后"
elseif timeFrame == "fourweeks" then
timeGroup = "4 周后"
else
timeGroup = "" .. timeFrame .. "后"
end
end
if not timeGroups[timeGroup] then
timeGroups[timeGroup] = {}
end
table.insert(timeGroups[timeGroup], {
npcName = npcName,
dialogue = dialogue,
appearanceCount = appearanceCount
})
end
end
-- 定义时间组的排序顺序
local timeOrder = {
["触发后"] = 1,
["1 天后"] = 2,
["1 周后"] = 3,
["2 周后"] = 4,
["4 周后"] = 5,
["1 年后"] = 6
}
-- 收集并排序时间组
local sortedTimeGroups = {}
for timeGroup, npcs in pairs(timeGroups) do
table.insert(sortedTimeGroups, {
timeGroup = timeGroup,
npcs = npcs
})
end
table.sort(sortedTimeGroups, function(a, b)
local orderA = timeOrder[a.timeGroup] or 999
local orderB = timeOrder[b.timeGroup] or 999
if orderA ~= orderB then
return orderA < orderB
else
return a.timeGroup < b.timeGroup
end
end)
-- 输出每个时间组
for _, timeGroupInfo in ipairs(sortedTimeGroups) do
local timeGroup = timeGroupInfo.timeGroup
local npcs = timeGroupInfo.npcs
table.insert(output, "" .. timeGroup .. "") -- 时间组的标题
-- 在每个时间组内按出现次数排序NPC
table.sort(npcs, function(a, b)
if a.appearanceCount ~= b.appearanceCount then
return a.appearanceCount > b.appearanceCount -- 出现次数多的排在前面
else
return a.npcName < b.npcName -- 出现次数相同时按名字排序
end
end)
-- 输出该时间组的所有NPC对话
for _, npcInfo in ipairs(npcs) do
local processedDialogue = processDialogueUnified(npcInfo.dialogue, nil, getCharacterName(npcInfo.npcName))
table.insert(output, processedDialogue)
end
table.insert(output, "")
end
return table.concat(output, "\n")
end
-- 调试函数
p.debug = function(frame)
return p.event {
args = {"38"} -- 测试事件ID 3091462 13 195013 3910975 51 54 9581348 6184644 3091462 1 10
}
end
return p