模块:Schedule:修订间差异
来自星砂岛百科
更多操作
同步更新 |
同步更新 |
||
| 第1,029行: | 第1,029行: | ||
return '默认' | return '默认' | ||
end | end | ||
return table.concat(parts, ' | return table.concat(parts, '|') | ||
end | |||
local function format_location_list(values, delimiter) | |||
local texts = unique_texts(values) | |||
local compact = {} | |||
for _, text in ipairs(texts) do | |||
local value = common.trim(text) | |||
if value ~= '' then | |||
compact[#compact + 1] = value | |||
end | |||
end | |||
if #compact == 0 then | |||
return '' | |||
end | |||
return table.concat(compact, delimiter or '/') | |||
end | |||
local function build_slot_meta_parts(slot) | |||
if type(slot) ~= 'table' then | |||
return {} | |||
end | |||
local parts = {} | |||
if common.trim(slot.kind) == 'mission' then | |||
parts[#parts + 1] = '任务' | |||
end | |||
local ranked_candidates, top_candidates = get_top_candidates(slot.action_candidates) | |||
if #top_candidates > 1 then | |||
parts[#parts + 1] = '并列候选 ' .. tostring(#top_candidates) .. ' 项' | |||
elseif #ranked_candidates > 1 then | |||
parts[#parts + 1] = '候选 ' .. tostring(#ranked_candidates) .. ' 项' | |||
end | |||
return parts | |||
end | end | ||
| 第1,073行: | 第1,108行: | ||
if display_name ~= '' and location ~= '' then | if display_name ~= '' and location ~= '' then | ||
return display_name .. ' | return display_name .. '(地点:' .. location .. ')' | ||
end | end | ||
if display_name ~= '' then | if display_name ~= '' then | ||
| 第1,079行: | 第1,114行: | ||
end | end | ||
if location ~= '' and template_name ~= '' then | if location ~= '' and template_name ~= '' then | ||
return template_name .. ' | return template_name .. '(地点:' .. location .. ')' | ||
end | end | ||
return template_name | return template_name | ||
| 第1,106行: | 第1,141行: | ||
local end_time = common.trim(slot.end_time) | local end_time = common.trim(slot.end_time) | ||
if begin_time ~= '' and end_time ~= '' then | if begin_time ~= '' and end_time ~= '' then | ||
return begin_time .. ' | return begin_time .. '-' .. end_time | ||
end | end | ||
return begin_time ~= '' and begin_time or end_time | return begin_time ~= '' and begin_time or end_time | ||
| 第1,136行: | 第1,171行: | ||
texts[#texts + 1] = candidate_activity_text(candidate) | texts[#texts + 1] = candidate_activity_text(candidate) | ||
end | end | ||
return | return format_location_list(texts) | ||
end | end | ||
| 第1,170行: | 第1,205行: | ||
locations[#locations + 1] = candidate_location_text(candidate) | locations[#locations + 1] = candidate_location_text(candidate) | ||
end | end | ||
return | return format_location_list(locations) | ||
end | end | ||
| 第1,190行: | 第1,225行: | ||
local mission_goal = common.trim(slot.mission_goal) | local mission_goal = common.trim(slot.mission_goal) | ||
local mission_title = common.trim(slot.mission_title) | local mission_title = common.trim(slot.mission_title) | ||
if mission_goal ~= '' and not generic_mission_goals[mission_goal] then | if mission_goal ~= '' and not generic_mission_goals[mission_goal] then | ||
| 第1,196行: | 第1,230行: | ||
elseif mission_title ~= '' and mission_title ~= common.trim(slot.mission_display) then | elseif mission_title ~= '' and mission_title ~= common.trim(slot.mission_display) then | ||
notes[#notes + 1] = mission_title | notes[#notes + 1] = mission_title | ||
end | end | ||
| 第1,213行: | 第1,243行: | ||
end | end | ||
if #top_texts > 0 then | if #top_texts > 0 then | ||
notes[#notes + 1] = '并列候选:' .. | notes[#notes + 1] = '并列候选:' .. format_location_list(top_texts) | ||
end | end | ||
elseif #ranked_candidates > 1 then | elseif #ranked_candidates > 1 then | ||
| 第1,224行: | 第1,254行: | ||
end | end | ||
if #candidate_texts > 0 then | if #candidate_texts > 0 then | ||
notes[#notes + 1] = '候选:' .. | notes[#notes + 1] = '候选:' .. format_location_list(candidate_texts) | ||
end | end | ||
end | end | ||
| 第1,244行: | 第1,274行: | ||
local head = root:tag('tr') | local head = root:tag('tr') | ||
head:tag('th'):wikitext('时间') | head:tag('th'):wikitext('时间') | ||
head:tag('th'):wikitext(' | head:tag('th'):wikitext('安排') | ||
head:tag('th'):wikitext('地点') | head:tag('th'):wikitext('地点') | ||
head:tag('th'):wikitext(' | head:tag('th'):wikitext('说明') | ||
for _, slot in ipairs(variant.slots or {}) do | for _, slot in ipairs(variant.slots or {}) do | ||
local row = root:tag('tr') | local row = root:tag('tr') | ||
row:tag('td'):wikitext( | local time_text = format_slot_time(slot) | ||
row:tag('td'):wikitext( | local activity_text = format_slot_activity(slot) | ||
row:tag('td'):wikitext( | local location_text = format_slot_location(slot) | ||
row:tag('td'):wikitext( | local note_text = format_slot_note(slot) | ||
local meta_text = format_location_list(build_slot_meta_parts(slot), '|') | |||
local final_note = format_location_list({ meta_text, note_text }, ';') | |||
row:tag('td'):wikitext(time_text ~= '' and time_text or '—') | |||
row:tag('td'):wikitext(activity_text ~= '' and activity_text or '—') | |||
row:tag('td'):wikitext(location_text ~= '' and location_text or '—') | |||
row:tag('td'):wikitext(final_note ~= '' and final_note or '—') | |||
end | end | ||
| 第1,331行: | 第1,368行: | ||
if #summary > 0 then | if #summary > 0 then | ||
out[#out + 1] = "''来源统计:" .. table.concat(summary, ',') .. " | out[#out + 1] = "''来源统计:" .. table.concat(summary, ',') .. "。相同日程规则已在展示层合并;若同一时段存在多个可能动作或地点,会在“说明”列列出。''" | ||
end | end | ||
2026年3月27日 (五) 11:12的版本
概述
Schedule 用于读取并展示角色行程数据,供 {{Schedule}} 与人物页“日程”章节调用。
用法
{{#invoke:Schedule|getField|晨星|calendar_id}}
{{#invoke:Schedule|renderSchedule|晨星}}
示例
{{#invoke:Schedule|renderSchedule|晨星}}
函数
getField:读取行程记录的顶层字段,如calendar_id、regular_count、special_count。renderSchedule:输出角色的日程区块,按常规、特殊、同居三类分组展示。
数据来源
local common = require('Module:Common')
local p = {}
local schedule_data
local mapping
local location_labels
local source_order = {
regular = 1,
special = 2,
living_together = 3,
}
local source_labels = {
regular = '常规日程',
special = '特殊日程',
living_together = '同居日程',
}
local month_labels = {
spring = '春季',
summer = '夏季',
autumn = '秋季',
winter = '冬季',
}
local weekday_labels = {
monday = '周一',
tuesday = '周二',
wednesday = '周三',
thursday = '周四',
friday = '周五',
saturday = '周六',
sunday = '周日',
}
local season_number_labels = {
['1'] = '春季',
['2'] = '夏季',
['3'] = '秋季',
['4'] = '冬季',
}
local season_keys = {
['1'] = 'spring',
['2'] = 'summer',
['3'] = 'autumn',
['4'] = 'winter',
}
local weekday_number_labels = {
['0'] = '周日',
['1'] = '周一',
['2'] = '周二',
['3'] = '周三',
['4'] = '周四',
['5'] = '周五',
['6'] = '周六',
}
local weekday_keys = {
['0'] = 'sunday',
['1'] = 'monday',
['2'] = 'tuesday',
['3'] = 'wednesday',
['4'] = 'thursday',
['5'] = 'friday',
['6'] = 'saturday',
}
local weather_labels = {
Rain = '雨天',
ClearSky = '晴天',
Snow = '雪天',
}
local festival_labels = {
['Festival.SpringFestivalActivity_POI_AfterFestival'] = '春节前',
['Festival.SpringFestivalActivity_POI_InFestival'] = '春节期间',
}
local buildable_labels = {
BuildableRegion_Boat = '渡口建造',
}
local compare_op_labels = {
Equal = '等于',
GreatOrEqualThan = '大于等于',
GreatThan = '大于',
LessOrEqualThan = '小于等于',
LessThan = '小于',
Between = '介于',
}
local generic_condition_descs = {
['On Raining Days'] = true,
['雨天日程'] = true,
['雨雪天'] = true,
['雨雪天日程'] = true,
}
local generic_mission_goals = {
['执行互动'] = true,
}
local function default_mapping()
return {
name_to_id = {},
id_to_name = {},
aliases = {},
overrides = {
name_to_id = {},
aliases = {},
},
}
end
local function normalize_key(value)
return common.normalizeKey(value)
end
local function copy_array(values)
local out = {}
if type(values) ~= 'table' then
return out
end
for _, value in ipairs(values) do
out[#out + 1] = value
end
return out
end
local function normalize_number_array(values)
local out = {}
if type(values) ~= 'table' then
return out
end
for _, value in ipairs(values) do
local number = tonumber(value)
if number ~= nil then
out[#out + 1] = number
end
end
return out
end
local function has_ipairs_items(values)
if type(values) ~= 'table' then
return false
end
for _ in ipairs(values) do
return true
end
return false
end
local function append_unique(target, values)
if type(target) ~= 'table' or type(values) ~= 'table' then
return
end
local seen = {}
for _, value in ipairs(target) do
seen[tostring(value)] = true
end
for _, value in ipairs(values) do
local key = tostring(value)
if not seen[key] then
target[#target + 1] = value
seen[key] = true
end
end
end
local function sort_text_list(values, order_map)
table.sort(values, function(a, b)
local a_key = tostring(a)
local b_key = tostring(b)
local a_rank = order_map and order_map[a_key] or nil
local b_rank = order_map and order_map[b_key] or nil
if a_rank and b_rank and a_rank ~= b_rank then
return a_rank < b_rank
end
if a_rank and not b_rank then
return true
end
if b_rank and not a_rank then
return false
end
return a_key < b_key
end)
end
local function sort_number_list(values)
table.sort(values, function(a, b)
return tonumber(a) < tonumber(b)
end)
end
local function pick_primary_action(candidates)
if type(candidates) ~= 'table' or #candidates == 0 then
return {}
end
local ranked = {}
for _, candidate in ipairs(candidates) do
if type(candidate) == 'table' then
ranked[#ranked + 1] = candidate
end
end
table.sort(ranked, function(a, b)
local a_weight = tonumber(a.weight or 0) or 0
local b_weight = tonumber(b.weight or 0) or 0
if a_weight ~= b_weight then
return a_weight > b_weight
end
local a_score = tonumber(a.score or 0) or 0
local b_score = tonumber(b.score or 0) or 0
if a_score ~= b_score then
return a_score > b_score
end
return common.trim(a.template) > common.trim(b.template)
end)
return ranked[1] or {}
end
local function build_slot_summary(slot)
if type(slot) ~= 'table' then
return ''
end
local mission_template = common.trim(slot.mission_template)
if mission_template ~= '' then
local mission_display = common.trim(slot.mission_display)
if mission_display ~= '' then
return mission_display
end
local mission_title = common.trim(slot.mission_title)
if mission_title ~= '' then
return mission_title
end
return mission_template
end
local primary_action = slot.primary_action
if type(primary_action) ~= 'table' then
return ''
end
local display_name = common.trim(primary_action.display_name)
local location_hint = common.trim(primary_action.location_hint)
local template_name = common.trim(primary_action.template)
if display_name ~= '' and location_hint ~= '' then
return display_name .. ' (' .. location_hint .. ')'
end
if display_name ~= '' then
return display_name
end
if location_hint ~= '' and template_name ~= '' then
return template_name .. ' (' .. location_hint .. ')'
end
return template_name
end
local function expand_candidate_entry(raw_candidate)
if type(raw_candidate) ~= 'table' then
return {}
end
return {
template = common.trim(raw_candidate.t),
display_name = common.trim(raw_candidate.d),
location_hint = common.trim(raw_candidate.l),
weight = raw_candidate.w or 0,
score = raw_candidate.s or 0,
}
end
local function expand_compact_slot(raw_slot, candidate_registry)
if type(raw_slot) ~= 'table' then
return {}
end
local slot = {
begin_time = common.trim(raw_slot.b),
end_time = common.trim(raw_slot.e),
kind = common.trim(raw_slot.k),
}
if slot.kind == 'actions' then
local action_candidates = {}
if type(raw_slot.a) == 'table' then
for _, raw_index in ipairs(raw_slot.a) do
local index = tonumber(raw_index)
if index ~= nil then
local candidate = candidate_registry[index + 1]
if type(candidate) == 'table' then
action_candidates[#action_candidates + 1] = expand_candidate_entry(candidate)
end
end
end
end
slot.action_candidates = action_candidates
slot.primary_action = pick_primary_action(action_candidates)
elseif slot.kind == 'mission' then
slot.mission_template = common.trim(raw_slot.mt)
slot.mission_title = common.trim(raw_slot.tt)
slot.mission_goal = common.trim(raw_slot.g)
slot.mission_display = common.trim(raw_slot.md)
end
slot.summary = build_slot_summary(slot)
return slot
end
local function expand_compact_variant(raw_variant, candidate_registry)
if type(raw_variant) ~= 'table' then
return {}
end
local months = normalize_number_array(raw_variant.m)
local weekdays = normalize_number_array(raw_variant.w)
local days = normalize_number_array(raw_variant.d)
local month_keys = {}
local weekday_label_keys = {}
for _, value in ipairs(months) do
local key = season_keys[tostring(value)]
if key then
month_keys[#month_keys + 1] = key
end
end
for _, value in ipairs(weekdays) do
local key = weekday_keys[tostring(value)]
if key then
weekday_label_keys[#weekday_label_keys + 1] = key
end
end
local slots = {}
if type(raw_variant.sl) == 'table' then
for _, raw_slot in ipairs(raw_variant.sl) do
if type(raw_slot) == 'table' then
slots[#slots + 1] = expand_compact_slot(raw_slot, candidate_registry)
end
end
end
return {
source = common.trim(raw_variant.o),
schedule_id = common.trim(raw_variant.i),
months = months,
month_labels = month_keys,
weekdays = weekdays,
weekday_labels = weekday_label_keys,
days = days,
priority = '',
condition_desc = common.trim(raw_variant.c),
condition_summary = type(raw_variant.cs) == 'table' and raw_variant.cs or {},
condition_raw = {},
slots = slots,
}
end
local function expand_compact_record(record_key, raw_record, candidate_registry)
if type(raw_record) ~= 'table' then
return nil
end
local counts = type(raw_record.r) == 'table' and raw_record.r or {}
local variants = {}
if type(raw_record.v) == 'table' then
for _, raw_variant in ipairs(raw_record.v) do
if type(raw_variant) == 'table' then
variants[#variants + 1] = expand_compact_variant(raw_variant, candidate_registry)
end
end
end
return {
id = common.trim(raw_record.i),
name = common.trim(raw_record.n),
key = record_key,
calendar_id = common.trim(raw_record.c),
regular_count = tonumber(counts[1] or 0) or 0,
special_count = tonumber(counts[2] or 0) or 0,
living_together_count = tonumber(counts[3] or 0) or 0,
variants = variants,
}
end
local function expand_compact_schedule_data(raw_data)
if type(raw_data) ~= 'table' then
return {}
end
if type(raw_data.records) ~= 'table' or type(raw_data.candidate_registry) ~= 'table' then
return raw_data
end
local expanded = {}
for record_key, raw_record in pairs(raw_data.records) do
local normalized = normalize_key(record_key)
local record = expand_compact_record(normalized, raw_record, raw_data.candidate_registry)
if normalized ~= '' and record then
expanded[normalized] = record
end
end
return expanded
end
local function load_data()
if schedule_data then
return
end
schedule_data = expand_compact_schedule_data(common.loadJsonData('数据:Character/character_schedule_index.json') or {})
mapping = common.loadJsonData('数据:Character/character_mapping.json') or default_mapping()
location_labels = common.loadJsonData('数据:Character/schedule_location_labels.json') or {}
end
local function find_record(key)
load_data()
local resolved = common.trim(key)
if resolved == '' then
resolved = common.getCurrentTitleText()
end
local normalized = normalize_key(resolved)
if schedule_data[normalized] then
return schedule_data[normalized]
end
local override_id = mapping.overrides and mapping.overrides.name_to_id and mapping.overrides.name_to_id[normalized]
if override_id and schedule_data[normalize_key(override_id)] then
return schedule_data[normalize_key(override_id)]
end
local override_alias = mapping.overrides and mapping.overrides.aliases and mapping.overrides.aliases[normalized]
if override_alias and schedule_data[normalize_key(override_alias)] then
return schedule_data[normalize_key(override_alias)]
end
local mapped_id = mapping.name_to_id and mapping.name_to_id[normalized]
if mapped_id and schedule_data[normalize_key(mapped_id)] then
return schedule_data[normalize_key(mapped_id)]
end
local alias_id = mapping.aliases and mapping.aliases[normalized]
if alias_id and schedule_data[normalize_key(alias_id)] then
return schedule_data[normalize_key(alias_id)]
end
return nil
end
local function list_key(values)
if type(values) ~= 'table' then
return ''
end
local out = {}
for _, value in ipairs(values) do
out[#out + 1] = tostring(value)
end
return table.concat(out, ',')
end
local function build_condition_key(items)
if type(items) ~= 'table' then
return ''
end
local out = {}
for _, item in ipairs(items) do
if type(item) == 'table' then
out[#out + 1] = table.concat({
common.trim(item.type),
tostring(item.reverse),
common.trim(item.dest_type),
common.trim(item.key),
common.trim(item.tag),
common.trim(item.op),
common.trim(item.value),
tostring(item.check_day),
tostring(item.check_month),
tostring(item.check_weekday),
tostring(item.check_year),
common.trim(item.year_op),
common.trim(item.year_value),
}, '~')
end
end
return table.concat(out, '||')
end
local function build_slot_key(slots)
if type(slots) ~= 'table' then
return ''
end
local out = {}
for _, slot in ipairs(slots) do
if type(slot) == 'table' then
local primary_action = slot.primary_action
local primary_template = ''
if type(primary_action) == 'table' then
primary_template = common.trim(primary_action.template)
end
out[#out + 1] = table.concat({
common.trim(slot.begin_time),
common.trim(slot.end_time),
common.trim(slot.kind),
common.trim(slot.summary),
common.trim(slot.mission_template),
primary_template,
}, '~')
end
end
return table.concat(out, '||')
end
local function clone_variant(variant)
return {
source = common.trim(variant.source),
schedule_id = common.trim(variant.schedule_id),
months = copy_array(variant.months),
month_labels = copy_array(variant.month_labels),
weekdays = copy_array(variant.weekdays),
weekday_labels = copy_array(variant.weekday_labels),
days = copy_array(variant.days),
priority = variant.priority,
condition_desc = common.trim(variant.condition_desc),
condition_summary = variant.condition_summary or {},
condition_raw = variant.condition_raw or {},
slots = variant.slots or {},
}
end
local function merge_variants(variants)
local grouped = {}
local order = {}
if type(variants) ~= 'table' then
return order
end
for _, variant in ipairs(variants) do
if type(variant) == 'table' then
local key = table.concat({
common.trim(variant.source),
common.trim(variant.schedule_id),
list_key(variant.months),
list_key(variant.days),
common.trim(variant.condition_desc),
build_condition_key(variant.condition_summary),
build_slot_key(variant.slots),
}, '|')
if not grouped[key] then
grouped[key] = clone_variant(variant)
order[#order + 1] = grouped[key]
else
append_unique(grouped[key].weekdays, variant.weekdays or {})
append_unique(grouped[key].weekday_labels, variant.weekday_labels or {})
append_unique(grouped[key].months, variant.months or {})
append_unique(grouped[key].month_labels, variant.month_labels or {})
append_unique(grouped[key].days, variant.days or {})
end
end
end
for _, variant in ipairs(order) do
sort_number_list(variant.months)
sort_number_list(variant.weekdays)
sort_number_list(variant.days)
sort_text_list(variant.month_labels, {
spring = 1,
summer = 2,
autumn = 3,
winter = 4,
})
sort_text_list(variant.weekday_labels, {
monday = 1,
tuesday = 2,
wednesday = 3,
thursday = 4,
friday = 5,
saturday = 6,
sunday = 7,
})
end
table.sort(order, function(a, b)
local a_source = source_order[a.source] or 99
local b_source = source_order[b.source] or 99
if a_source ~= b_source then
return a_source < b_source
end
local a_month = tonumber(a.months[1] or 99) or 99
local b_month = tonumber(b.months[1] or 99) or 99
if a_month ~= b_month then
return a_month < b_month
end
local a_weekday = tonumber(a.weekdays[1] or 99) or 99
local b_weekday = tonumber(b.weekdays[1] or 99) or 99
if a_weekday ~= b_weekday then
return a_weekday < b_weekday
end
local a_day = tonumber(a.days[1] or 99) or 99
local b_day = tonumber(b.days[1] or 99) or 99
if a_day ~= b_day then
return a_day < b_day
end
local a_desc = common.trim(a.condition_desc)
local b_desc = common.trim(b.condition_desc)
if a_desc ~= b_desc then
return a_desc < b_desc
end
return common.trim(a.schedule_id) < common.trim(b.schedule_id)
end)
return order
end
local function map_labels(values, label_map)
if type(values) ~= 'table' then
return {}
end
local out = {}
for _, value in ipairs(values) do
local text = common.trim(value)
if text ~= '' then
out[#out + 1] = label_map[text] or text
end
end
return out
end
local function join_parts(values)
if type(values) ~= 'table' or #values == 0 then
return ''
end
return table.concat(values, '、')
end
local function unique_texts(values)
local out = {}
local seen = {}
if type(values) ~= 'table' then
return out
end
for _, value in ipairs(values) do
local text = common.trim(value)
if text ~= '' and not seen[text] then
out[#out + 1] = text
seen[text] = true
end
end
return out
end
local function rank_candidates(candidates)
local ranked = {}
if type(candidates) ~= 'table' then
return ranked
end
for _, candidate in ipairs(candidates) do
if type(candidate) == 'table' then
ranked[#ranked + 1] = candidate
end
end
table.sort(ranked, function(a, b)
local a_weight = tonumber(a.weight or 0) or 0
local b_weight = tonumber(b.weight or 0) or 0
if a_weight ~= b_weight then
return a_weight > b_weight
end
local a_score = tonumber(a.score or 0) or 0
local b_score = tonumber(b.score or 0) or 0
if a_score ~= b_score then
return a_score > b_score
end
return common.trim(a.template) > common.trim(b.template)
end)
return ranked
end
local function get_top_candidates(candidates)
local ranked = rank_candidates(candidates)
if #ranked == 0 then
return ranked, {}
end
local first = ranked[1]
local top_weight = tonumber(first.weight or 0) or 0
local top_score = tonumber(first.score or 0) or 0
local top = {}
for _, candidate in ipairs(ranked) do
local weight = tonumber(candidate.weight or 0) or 0
local score = tonumber(candidate.score or 0) or 0
if weight == top_weight and score == top_score then
top[#top + 1] = candidate
else
break
end
end
return ranked, top
end
local function clean_condition_desc(value)
local text = common.trim(value)
text = mw.ustring.gsub(text, '^%*+', '')
return common.trim(text)
end
local function should_use_structured_condition(desc)
local cleaned = clean_condition_desc(desc)
if cleaned == '' then
return true
end
if generic_condition_descs[cleaned] then
return true
end
if mw.ustring.find(cleaned, '[A-Za-z]') and not mw.ustring.find(cleaned, '[一-龥]') then
return true
end
return false
end
local function describe_compare_range(op, value, min_value, max_value, label_map, suffix)
local resolved_op = common.trim(op)
local resolved_value = common.trim(value)
local resolved_min = common.trim(min_value)
local resolved_max = common.trim(max_value)
local resolved_suffix = suffix or ''
local function map_value(raw)
local key = common.trim(raw)
if key == '' then
return ''
end
if label_map and label_map[key] then
return label_map[key]
end
return key .. resolved_suffix
end
if resolved_op == 'Equal' and resolved_value ~= '' then
return map_value(resolved_value)
end
if resolved_op == 'Between' and resolved_min ~= '' and resolved_max ~= '' then
local left = map_value(resolved_min)
local right = map_value(resolved_max)
if label_map then
return left .. ' - ' .. right
end
return resolved_min .. ' - ' .. resolved_max .. resolved_suffix
end
local compare_label = compare_op_labels[resolved_op] or resolved_op
if compare_label ~= '' and resolved_value ~= '' then
return compare_label .. map_value(resolved_value)
end
return ''
end
local function build_apply_text(variant)
local parts = {}
local months = map_labels(variant.month_labels, month_labels)
local weekdays = map_labels(variant.weekday_labels, weekday_labels)
local days = {}
for _, day in ipairs(variant.days or {}) do
local value = common.trim(day)
if value ~= '' then
days[#days + 1] = value .. '日'
end
end
if #months > 0 then
parts[#parts + 1] = join_parts(months)
end
if #weekdays > 0 then
parts[#parts + 1] = join_parts(weekdays)
end
if #days > 0 then
parts[#parts + 1] = join_parts(days)
end
return table.concat(parts, ' / ')
end
local function describe_condition_item(item)
if type(item) ~= 'table' then
return ''
end
if item.type == 'weather_today' then
local dest_type = common.trim(item.dest_type)
local label = weather_labels[dest_type] or dest_type
if label ~= '' then
if item.reverse then
return '非' .. label
end
return label
end
return '天气条件'
end
if item.type == 'entity_data_check_string' then
local key = common.trim(item.key)
local op = common.trim(item.op)
local value = common.trim(item.value)
if key == 'Schedule_Typhoon' and value == '1' then
if item.reverse then
return '非台风天气'
end
return '台风天气'
end
if key ~= '' and value ~= '' then
local prefix = item.reverse and '不满足:' or ''
if op ~= '' then
return prefix .. key .. ' ' .. op .. ' ' .. value
end
return prefix .. key .. ' = ' .. value
end
end
if item.type == 'npc_affection_star' then
local value = common.trim(item.value)
if value == '' then
return item.reverse and '好感条件不满足' or '好感条件'
end
local op = common.trim(item.op)
if op == 'GreatOrEqualThan' then
return item.reverse and ('好感度未达到 ' .. value .. ' 心') or ('好感度达到 ' .. value .. ' 心')
end
if op == 'Equal' then
return item.reverse and ('好感度不为 ' .. value .. ' 心') or ('好感度为 ' .. value .. ' 心')
end
local compare_label = compare_op_labels[op] or op
if compare_label ~= '' then
return item.reverse and ('好感条件不满足 ' .. compare_label .. ' ' .. value .. ' 心') or ('好感度 ' .. compare_label .. ' ' .. value .. ' 心')
end
return item.reverse and '好感条件不满足' or '好感条件'
end
if item.type == 'calendar_now_is_festival' then
local festival_template = common.trim(item.festival_template)
local label = festival_labels[festival_template] or festival_template
if label ~= '' then
if item.reverse then
return '非' .. label
end
return label
end
return item.reverse and '非节庆期间' or '节庆期间'
end
if item.type == 'buildable_region_check_completed' then
local template_name = common.trim(item.template)
local label = buildable_labels[template_name] or template_name
if label ~= '' then
if item.reverse then
return label .. '未完成'
end
return label .. '已完成'
end
return item.reverse and '建筑条件未完成' or '建筑条件已完成'
end
if item.type == 'check_has_owner' then
if item.reverse then
return '未被收养'
end
return '已被收养'
end
if item.type == 'time_check_date' then
local parts = {}
if item.check_month then
local month_text = describe_compare_range(
item.month_op,
item.month_value,
item.month_min_value,
item.month_max_value,
season_number_labels
)
if month_text ~= '' then
parts[#parts + 1] = month_text
else
parts[#parts + 1] = '月份'
end
end
if item.check_day then
local day_text = describe_compare_range(
item.day_op,
item.day_value,
item.day_min_value,
item.day_max_value,
nil,
'日'
)
if day_text ~= '' then
parts[#parts + 1] = day_text
else
parts[#parts + 1] = '日期'
end
end
if item.check_weekday then
local weekday_text = describe_compare_range(
item.weekday_op,
item.weekday_value,
item.weekday_min_value,
item.weekday_max_value,
weekday_number_labels
)
if weekday_text ~= '' then
parts[#parts + 1] = weekday_text
else
parts[#parts + 1] = '星期'
end
end
if item.check_year then
local year_text = ''
if common.trim(item.year_op) == 'Equal' and common.trim(item.year_value) ~= '' then
year_text = '第' .. common.trim(item.year_value) .. '年'
else
year_text = describe_compare_range(
item.year_op,
item.year_value,
item.year_min_value,
item.year_max_value
)
if year_text ~= '' then
year_text = '年份' .. year_text
end
end
if year_text ~= '' then
parts[#parts + 1] = year_text
else
parts[#parts + 1] = '年份'
end
end
if #parts > 0 then
local text = table.concat(parts, '')
if item.reverse then
return '非' .. text
end
return text
end
return item.reverse and '日期条件不满足' or '日期条件'
end
return '特殊条件'
end
local function build_condition_text(variant)
local condition_desc = clean_condition_desc(variant.condition_desc)
local summary = variant.condition_summary
local parts = {}
if type(summary) == 'table' then
for _, item in ipairs(summary) do
local text = describe_condition_item(item)
if text ~= '' then
parts[#parts + 1] = text
end
end
end
local structured_text = table.concat(unique_texts(parts), ';')
if condition_desc == '' then
return structured_text
end
if structured_text == '' then
return condition_desc
end
if should_use_structured_condition(condition_desc) then
return structured_text
end
return condition_desc
end
local function build_variant_heading(variant)
local parts = {}
local apply_text = build_apply_text(variant)
local condition_text = build_condition_text(variant)
if apply_text ~= '' then
parts[#parts + 1] = apply_text
end
if condition_text ~= '' then
parts[#parts + 1] = condition_text
end
if #parts == 0 then
return '默认'
end
return table.concat(parts, '|')
end
local function format_location_list(values, delimiter)
local texts = unique_texts(values)
local compact = {}
for _, text in ipairs(texts) do
local value = common.trim(text)
if value ~= '' then
compact[#compact + 1] = value
end
end
if #compact == 0 then
return ''
end
return table.concat(compact, delimiter or '/')
end
local function build_slot_meta_parts(slot)
if type(slot) ~= 'table' then
return {}
end
local parts = {}
if common.trim(slot.kind) == 'mission' then
parts[#parts + 1] = '任务'
end
local ranked_candidates, top_candidates = get_top_candidates(slot.action_candidates)
if #top_candidates > 1 then
parts[#parts + 1] = '并列候选 ' .. tostring(#top_candidates) .. ' 项'
elseif #ranked_candidates > 1 then
parts[#parts + 1] = '候选 ' .. tostring(#ranked_candidates) .. ' 项'
end
return parts
end
local function localize_location(value)
local text = common.trim(value)
if text == '' then
return ''
end
local direct = location_labels[text]
if direct then
return direct
end
if text == '家中' or text == '森林' or text == '森林三层' or text == '温泉' or text == '广场' or text == '宠物店' or text == '宠物店室内' or text == '宠物区域' then
return text
end
local festival_base = text:match('^(.-)_BeachFestival$')
if festival_base then
local base = localize_location(festival_base)
if base ~= '' and base ~= festival_base then
return base .. '(海滩节)'
end
end
local location, actor = text:match('^(.-)_([A-Za-z][A-Za-z0-9]+)$')
if location and actor and location_labels[location] then
return location_labels[location] .. '(' .. actor .. ')'
end
return text:gsub('_', ' / ')
end
local function format_candidate_text(candidate)
if type(candidate) ~= 'table' then
return ''
end
local display_name = common.trim(candidate.display_name)
local location = localize_location(candidate.location_hint)
local template_name = common.trim(candidate.template)
if display_name ~= '' and location ~= '' then
return display_name .. '(地点:' .. location .. ')'
end
if display_name ~= '' then
return display_name
end
if location ~= '' and template_name ~= '' then
return template_name .. '(地点:' .. location .. ')'
end
return template_name
end
local function candidate_activity_text(candidate)
if type(candidate) ~= 'table' then
return ''
end
local display_name = common.trim(candidate.display_name)
if display_name ~= '' then
return display_name
end
return common.trim(candidate.template)
end
local function candidate_location_text(candidate)
if type(candidate) ~= 'table' then
return ''
end
return localize_location(candidate.location_hint)
end
local function format_slot_time(slot)
local begin_time = common.trim(slot.begin_time)
local end_time = common.trim(slot.end_time)
if begin_time ~= '' and end_time ~= '' then
return begin_time .. '-' .. end_time
end
return begin_time ~= '' and begin_time or end_time
end
local function format_slot_activity(slot)
if type(slot) ~= 'table' then
return ''
end
if common.trim(slot.kind) == 'mission' then
local mission_display = common.trim(slot.mission_display)
if mission_display ~= '' then
return mission_display
end
local mission_title = common.trim(slot.mission_title)
if mission_title ~= '' then
return mission_title
end
return '任务'
end
local ranked_candidates, top_candidates = get_top_candidates(slot.action_candidates)
if #top_candidates > 1 then
local texts = {}
for _, candidate in ipairs(top_candidates) do
texts[#texts + 1] = candidate_activity_text(candidate)
end
return format_location_list(texts)
end
local primary_action = slot.primary_action
if type(primary_action) ~= 'table' then
if #ranked_candidates > 0 then
return candidate_activity_text(ranked_candidates[1])
end
return common.trim(slot.summary)
end
local display_name = common.trim(primary_action.display_name)
if display_name ~= '' then
return display_name
end
return common.trim(primary_action.template)
end
local function format_slot_location(slot)
if type(slot) ~= 'table' then
return ''
end
if common.trim(slot.kind) == 'mission' then
return ''
end
local _, top_candidates = get_top_candidates(slot.action_candidates)
if #top_candidates > 1 then
local locations = {}
for _, candidate in ipairs(top_candidates) do
locations[#locations + 1] = candidate_location_text(candidate)
end
return format_location_list(locations)
end
local primary_action = slot.primary_action
if type(primary_action) ~= 'table' then
return ''
end
return localize_location(primary_action.location_hint)
end
local function format_slot_note(slot)
if type(slot) ~= 'table' then
return ''
end
if common.trim(slot.kind) == 'mission' then
local notes = {}
local mission_goal = common.trim(slot.mission_goal)
local mission_title = common.trim(slot.mission_title)
if mission_goal ~= '' and not generic_mission_goals[mission_goal] then
notes[#notes + 1] = mission_goal
elseif mission_title ~= '' and mission_title ~= common.trim(slot.mission_display) then
notes[#notes + 1] = mission_title
end
return table.concat(notes, ';')
end
local notes = {}
local ranked_candidates, top_candidates = get_top_candidates(slot.action_candidates)
if #top_candidates > 1 then
local top_texts = {}
for _, candidate in ipairs(top_candidates) do
top_texts[#top_texts + 1] = format_candidate_text(candidate)
end
if #top_texts > 0 then
notes[#notes + 1] = '并列候选:' .. format_location_list(top_texts)
end
elseif #ranked_candidates > 1 then
local candidate_texts = {}
for _, candidate in ipairs(ranked_candidates) do
local text = format_candidate_text(candidate)
if text ~= '' then
candidate_texts[#candidate_texts + 1] = text
end
end
if #candidate_texts > 0 then
notes[#notes + 1] = '候选:' .. format_location_list(candidate_texts)
end
end
local primary_action = slot.primary_action
if type(primary_action) == 'table' then
local template_name = common.trim(primary_action.template)
local display_name = common.trim(primary_action.display_name)
if template_name ~= '' and display_name == '' then
notes[#notes + 1] = '模板:' .. template_name
end
end
return table.concat(notes, ';')
end
local function render_variant_table(variant)
local root = mw.html.create('table'):addClass('wikitable')
local head = root:tag('tr')
head:tag('th'):wikitext('时间')
head:tag('th'):wikitext('安排')
head:tag('th'):wikitext('地点')
head:tag('th'):wikitext('说明')
for _, slot in ipairs(variant.slots or {}) do
local row = root:tag('tr')
local time_text = format_slot_time(slot)
local activity_text = format_slot_activity(slot)
local location_text = format_slot_location(slot)
local note_text = format_slot_note(slot)
local meta_text = format_location_list(build_slot_meta_parts(slot), '|')
local final_note = format_location_list({ meta_text, note_text }, ';')
row:tag('td'):wikitext(time_text ~= '' and time_text or '—')
row:tag('td'):wikitext(activity_text ~= '' and activity_text or '—')
row:tag('td'):wikitext(location_text ~= '' and location_text or '—')
row:tag('td'):wikitext(final_note ~= '' and final_note or '—')
end
return tostring(root)
end
local function render_group(source, variants)
local items = {}
for _, variant in ipairs(variants) do
if variant.source == source and has_ipairs_items(variant.slots) then
items[#items + 1] = variant
end
end
if #items == 0 then
return ''
end
local out = {}
out[#out + 1] = '=== ' .. (source_labels[source] or source) .. ' ==='
for _, variant in ipairs(items) do
out[#out + 1] = '==== ' .. build_variant_heading(variant) .. ' ===='
out[#out + 1] = render_variant_table(variant)
end
return table.concat(out, '\n')
end
local function filter_display_variants(variants)
local out = {}
if type(variants) ~= 'table' then
return out
end
for _, variant in ipairs(variants) do
if type(variant) == 'table' and has_ipairs_items(variant.slots) then
out[#out + 1] = variant
end
end
return out
end
local function render_schedule_by_key(key)
local record = find_record(key)
if not record then
return ''
end
local variants = filter_display_variants(merge_variants(record.variants))
if #variants == 0 then
return '暂无日程数据。'
end
local out = {}
local summary_counts = {
regular = 0,
special = 0,
living_together = 0,
}
for _, variant in ipairs(variants) do
local source = common.trim(variant.source)
if summary_counts[source] ~= nil then
summary_counts[source] = summary_counts[source] + 1
end
end
local summary = {}
if summary_counts.regular > 0 then
summary[#summary + 1] = '常规 ' .. tostring(summary_counts.regular)
end
if summary_counts.special > 0 then
summary[#summary + 1] = '特殊 ' .. tostring(summary_counts.special)
end
if summary_counts.living_together > 0 then
summary[#summary + 1] = '同居 ' .. tostring(summary_counts.living_together)
end
if #summary > 0 then
out[#out + 1] = "''来源统计:" .. table.concat(summary, ',') .. "。相同日程规则已在展示层合并;若同一时段存在多个可能动作或地点,会在“说明”列列出。''"
end
for _, source in ipairs({ 'regular', 'special', 'living_together' }) do
local block = render_group(source, variants)
if block ~= '' then
out[#out + 1] = block
end
end
return table.concat(out, '\n\n')
end
function p.getField(frame)
local key = common.getArg(frame, 1, '')
local field = common.getArg(frame, 2, '')
if field == '' then
return ''
end
local record = find_record(key)
if not record then
return ''
end
return common.toText(record[field])
end
function p.renderSchedule(frame)
local key = common.getArg(frame, 1, '')
return render_schedule_by_key(key)
end
return p