模块:Schedule
来自星砂岛百科
更多操作
概述
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 character_common = require('Module:CharacterCommon')
local p = {}
local raw_schedule_records
local candidate_registry
local expanded_record_cache
local mapping
local location_labels
local source_order = {
regular = 1,
special = 2,
living_together = 3,
}
local source_labels = {
regular = '常规日程',
special = '特殊日程',
living_together = '同居日程',
}
local weekday_labels = {
monday = '周一',
tuesday = '周二',
wednesday = '周三',
thursday = '周四',
friday = '周五',
saturday = '周六',
sunday = '周日',
}
local season_number_labels = {
['1'] = '春季',
['2'] = '夏季',
['3'] = '秋季',
['4'] = '冬季',
}
local season_plain_labels = {
spring = '春季',
summer = '夏季',
autumn = '秋季',
fall = '秋季',
winter = '冬季',
}
local season_keys = {
['1'] = 'spring',
['2'] = 'summer',
['3'] = 'autumn',
['4'] = 'winter',
}
local season_template_keys = {
['1'] = '1',
['2'] = '2',
['3'] = '3',
['4'] = '4',
spring = 'spring',
summer = 'summer',
autumn = 'fall',
fall = 'fall',
winter = '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 weather_template_keys = {
Rain = 'Rainy',
ClearSky = 'Sunny',
Snow = 'Snowy',
}
local weather_plain_labels = {
Rain = '雨天',
Rainy = '雨天',
ClearSky = '晴天',
Sunny = '晴天',
Snow = '雪天',
Snowy = '雪天',
}
local festival_labels = {
['Festival.BeachFestival_Formal'] = '沙滩节',
['Festival.SpringFestival_Formal'] = '春节',
}
local festival_heading_labels = {
['Festival.SpringFestivalActivity_POI_AfterFestival'] = '春节准备',
['Festival.SpringFestivalActivity_POI_InFestival'] = '春节期间',
}
local festival_date_ranges = {
['Festival.BeachFestival_Formal'] = {
month = 2,
day_min = 27,
day_max = 27,
},
['Festival.SpringFestivalActivity_POI_AfterFestival'] = {
month = 4,
day_min = 25,
day_max = 28,
},
['Festival.SpringFestivalActivity_POI_InFestival'] = {
month = 1,
day_min = 1,
day_max = 4,
},
}
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 normalize_key(value)
return character_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 location_label = common.trim(primary_action.location_label)
local location_display = location_label ~= '' and location_label or location_hint
local template_name = common.trim(primary_action.template)
if display_name ~= '' and location_display ~= '' then
return display_name .. ' (' .. location_display .. ')'
end
if display_name ~= '' then
return display_name
end
if location_display ~= '' and template_name ~= '' then
return template_name .. ' (' .. location_display .. ')'
end
return template_name
end
local function resolve_location_label(value)
local text = common.trim(value)
if text == '' or type(location_labels) ~= 'table' then
return ''
end
local direct = common.trim(location_labels[text] or '')
if direct ~= '' then
return direct
end
local festival_base = mw.ustring.match(text, '^(.+)_BeachFestival$')
if festival_base then
local base = common.trim(location_labels[festival_base] or '')
if base ~= '' then
return base .. '(海滩节)'
end
end
local location_key, actor_key = mw.ustring.match(text, '^(.*)_([A-Za-z][A-Za-z0-9]+)$')
if location_key and actor_key then
local location_label = common.trim(location_labels[location_key] or '')
local actor_label = common.trim(location_labels[actor_key] or '')
if location_label ~= '' and actor_label ~= '' then
return actor_label .. '(' .. location_label .. ')'
end
if location_label ~= '' then
return location_label .. '(' .. actor_key .. ')'
end
if actor_label ~= '' then
return actor_label
end
end
return ''
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),
location_label = common.trim(raw_candidate.ll or resolve_location_label(raw_candidate.l)),
merged_display_name = common.trim(raw_candidate.md),
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 load_data()
if raw_schedule_records then
return
end
local raw_data = common.loadJsonData('数据:Character/character_schedule_index.json') or {}
if type(raw_data.records) == 'table' then
raw_schedule_records = raw_data.records
candidate_registry = type(raw_data.candidate_registry) == 'table' and raw_data.candidate_registry or {}
else
raw_schedule_records = raw_data
candidate_registry = {}
end
expanded_record_cache = {}
mapping = character_common.loadCharacterMapping()
location_labels = common.loadJsonData('数据:Character/schedule_location_labels.json') or {}
end
local function expand_record_by_key(record_key)
if record_key == '' then
return nil
end
if expanded_record_cache[record_key] ~= nil then
return expanded_record_cache[record_key] or nil
end
local raw_record = raw_schedule_records[record_key]
if type(raw_record) ~= 'table' then
expanded_record_cache[record_key] = false
return nil
end
local record
if type(raw_record.v) == 'table' then
record = expand_compact_record(record_key, raw_record, candidate_registry or {})
else
record = raw_record
end
expanded_record_cache[record_key] = record or false
return record
end
local function find_record(key)
load_data()
local _, record_key = character_common.findRecordWithKey(raw_schedule_records, mapping, key)
return expand_record_by_key(record_key)
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
local function condition_priority(variant)
local has_weekday = type(variant.weekdays) == 'table' and #variant.weekdays > 0
local has_rain = false
local has_clear = false
if type(variant.condition_summary) == 'table' then
for _, item in ipairs(variant.condition_summary) do
if type(item) == 'table' then
local item_type = common.trim(item.type)
local is_reverse = item.reverse == true
local dest_type = common.trim(item.dest_type)
if item_type == 'weather_today' then
if not is_reverse and dest_type == 'Rain' then
has_rain = true
elseif is_reverse and (dest_type == 'Rain' or dest_type == 'ClearSky') then
has_clear = true
elseif not is_reverse and dest_type == 'Sunny' then
has_clear = true
end
end
end
end
end
if has_clear and not has_weekday then
return 0
end
if has_rain and not has_weekday then
return 1
end
if has_weekday and not has_rain then
return 2
end
if has_weekday and has_rain then
return 3
end
return 4
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_condition_priority = condition_priority(a)
local b_condition_priority = condition_priority(b)
if a_condition_priority ~= b_condition_priority then
return a_condition_priority < b_condition_priority
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_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_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 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, summary)
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
if type(summary) == 'table' then
for _, item in ipairs(summary) do
if type(item) == 'table' and (item.type == 'calendar_now_is_festival' or item.type == 'weather_today') then
return true
end
end
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 resolve_season_template_key(value)
local normalized = mw.ustring.lower(common.trim(value))
if normalized == '' then
return nil
end
return season_template_keys[normalized] or season_template_keys[season_keys[normalized] or '']
end
local function format_season_token(value)
local key = resolve_season_template_key(value)
if key then
return '{{Season|' .. key .. '}}'
end
return ''
end
local function format_season_day_text(months, days)
if #months == 1 and #days > 0 then
local key = resolve_season_template_key(months[1])
if key then
local season_days = {}
for _, day in ipairs(days) do
local value = common.trim(day)
if value ~= '' then
season_days[#season_days + 1] = '{{Season|' .. key .. '|' .. value .. '}}'
end
end
if #season_days > 0 then
return join_parts(season_days)
end
end
end
return ''
end
local function format_condition_date_text(month_value, day_value)
local season_key = resolve_season_template_key(month_value)
local day_text = common.trim(day_value)
if season_key ~= nil and day_text ~= '' then
return '{{Season|' .. season_key .. '|' .. day_text .. '}}'
end
if season_key ~= nil then
return '{{Season|' .. season_key .. '}}'
end
if day_text ~= '' then
return day_text .. '日'
end
return ''
end
local function format_condition_date_range_text(month_value, day_min_value, day_max_value)
local season_key = resolve_season_template_key(month_value)
local min_text = common.trim(day_min_value)
local max_text = common.trim(day_max_value)
if season_key ~= nil and min_text ~= '' and max_text ~= '' then
if min_text == max_text then
return '{{Season|' .. season_key .. '|' .. min_text .. '}}'
end
return '{{Season|' .. season_key .. '|' .. min_text .. '}} - {{Season|' .. season_key .. '|' .. max_text .. '}}'
end
if min_text ~= '' and max_text ~= '' then
if min_text == max_text then
return min_text .. '日'
end
return min_text .. '日-' .. max_text .. '日'
end
return ''
end
local function build_apply_text(variant)
local parts = {}
local month_values = variant.month_labels or {}
local day_values = variant.days or {}
local month_parts = {}
local weekdays = map_labels(variant.weekday_labels, weekday_labels)
local season_day_text = format_season_day_text(month_values, day_values)
local days = {}
if season_day_text == '' then
for _, month in ipairs(month_values) do
local token = format_season_token(month)
if token ~= '' then
month_parts[#month_parts + 1] = token
end
end
for _, day in ipairs(day_values) do
local value = common.trim(day)
if value ~= '' then
days[#days + 1] = value .. '日'
end
end
if #month_parts > 0 then
parts[#parts + 1] = join_parts(month_parts)
end
if #days > 0 then
parts[#parts + 1] = join_parts(days)
end
end
if #weekdays > 0 then
parts[#parts + 1] = join_parts(weekdays)
end
if season_day_text ~= '' then
parts[#parts + 1] = season_day_text
end
return table.concat(parts, ' / ')
end
local describe_condition_item
local function build_structured_condition_title(summary)
if type(summary) ~= 'table' then
return ''
end
local festival_template = ''
local festival_text = ''
local weather_text = ''
local exact_date_text = ''
local ranged_date_text = ''
for _, item in ipairs(summary) do
if type(item) == 'table' then
if item.type == 'calendar_now_is_festival' and not item.reverse then
festival_template = common.trim(item.festival_template)
festival_text = festival_labels[festival_template] or festival_heading_labels[festival_template] or festival_template
elseif item.type == 'weather_today' and weather_text == '' then
local dest_type = common.trim(item.dest_type)
local template_key = weather_template_keys[dest_type]
local label = weather_labels[dest_type] or dest_type
weather_text = template_key and ('{{Weather|' .. template_key .. '}}') or label
elseif item.type == 'time_check_date' then
if exact_date_text == '' and item.check_month and item.check_day and common.trim(item.month_op) == 'Equal' and common.trim(item.day_op) == 'Equal' then
exact_date_text = format_condition_date_text(item.month_value, item.day_value)
elseif ranged_date_text == '' and item.check_month and item.check_day and common.trim(item.month_op) == 'Equal' and common.trim(item.day_op) == 'Between' and not item.reverse then
ranged_date_text = format_condition_date_range_text(item.month_value, item.day_min_value, item.day_max_value)
end
end
end
end
local fallback_range = festival_date_ranges[festival_template]
if exact_date_text == '' and fallback_range and fallback_range.day_min == fallback_range.day_max then
exact_date_text = format_condition_date_text(fallback_range.month, fallback_range.day_min)
end
if festival_text ~= '' and exact_date_text ~= '' then
return exact_date_text .. '|' .. festival_text
end
if festival_text ~= '' and ranged_date_text == '' and fallback_range then
ranged_date_text = format_condition_date_range_text(fallback_range.month, fallback_range.day_min, fallback_range.day_max)
end
if festival_text ~= '' and ranged_date_text ~= '' then
return ranged_date_text .. '|' .. festival_text
end
if weather_text ~= '' then
return weather_text
end
return ''
end
describe_condition_item = function(item)
if type(item) ~= 'table' then
return ''
end
if item.type == 'weather_today' then
local dest_type = common.trim(item.dest_type)
local template_key = weather_template_keys[dest_type]
local label = weather_labels[dest_type] or dest_type
local weather_text = template_key and ('{{Weather|' .. template_key .. '}}') or label
if weather_text ~= '' then
if item.reverse then
return '非' .. weather_text
end
return weather_text
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_heading_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 title_text = build_structured_condition_title(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 title_text ~= '' then
return title_text
end
if condition_desc == '' then
return structured_text
end
if structured_text == '' then
return condition_desc
end
if should_use_structured_condition(condition_desc, summary) 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 resolve_season_plain_label(value)
local normalized = mw.ustring.lower(common.trim(value))
if normalized == '' then
return ''
end
local season_key = season_keys[normalized] or normalized
if season_number_labels[season_key] then
return season_number_labels[season_key]
end
if season_plain_labels[season_key] then
return season_plain_labels[season_key]
end
return common.trim(value)
end
local function build_variant_tab_label(variant)
local text = build_variant_heading(variant)
text = mw.ustring.gsub(text, '{{%s*[Ss]eason%s*|%s*([^|}]+)%s*|%s*([^}]+)%s*}}', function(key, day)
local season_label = resolve_season_plain_label(key)
local day_text = common.trim(day)
if season_label ~= '' and day_text ~= '' then
return season_label .. day_text .. '日'
end
if season_label ~= '' then
return season_label
end
return common.trim(key)
end)
text = mw.ustring.gsub(text, '{{%s*[Ss]eason%s*|%s*([^}]+)%s*}}', function(key)
local season_label = resolve_season_plain_label(key)
return season_label ~= '' and season_label or common.trim(key)
end)
text = mw.ustring.gsub(text, '{{%s*[Ww]eather%s*|%s*([^}]+)%s*}}', function(key)
local weather_key = common.trim(key)
return weather_plain_labels[weather_key] or weather_labels[weather_key] or weather_key
end)
text = mw.ustring.gsub(text, '%[%[([^%]|]+)|([^%]]+)%]%]', '%2')
text = mw.ustring.gsub(text, '%[%[([^%]]+)%]%]', '%1')
text = mw.ustring.gsub(text, "'''", '')
text = mw.ustring.gsub(text, '<[^>]+>', '')
text = common.trim(text)
return text ~= '' and text or '默认'
end
local function format_candidate_text(candidate)
if type(candidate) ~= 'table' then
return ''
end
local merged_display_name = common.trim(candidate.merged_display_name)
if merged_display_name ~= '' then
return merged_display_name
end
local display_name = common.trim(candidate.display_name)
local location_label = common.trim(candidate.location_label)
if location_label == '' then
location_label = resolve_location_label(candidate.location_hint)
end
if display_name ~= '' and location_label ~= '' and not mw.ustring.find(display_name, location_label, 1, true) then
return display_name .. '(' .. location_label .. ')'
end
if display_name ~= '' then
return display_name
end
if location_label ~= '' then
return location_label
end
local template_name = common.trim(candidate.template)
if template_name ~= '' then
return template_name
end
return common.trim(candidate.location_hint)
end
local function format_probability(weight, total_weight)
local current = tonumber(weight or 0) or 0
local total = tonumber(total_weight or 0) or 0
if current <= 0 then
return '—'
end
if total <= 0 then
return tostring(current)
end
local percent = current * 100 / total
if math.abs(percent - math.floor(percent + 0.5)) < 0.05 then
return string.format('%d%%', math.floor(percent + 0.5))
end
return string.format('%.1f%%', percent)
end
local format_slot_activity
local format_slot_note
local function build_slot_schedule_entries(slot)
local entries = {}
if type(slot) ~= 'table' then
return entries
end
if common.trim(slot.kind) == 'mission' then
local mission_activity = format_slot_activity(slot)
local mission_note = format_slot_note(slot)
entries[#entries + 1] = {
activity = mission_note ~= '' and (mission_activity .. '<br /><small>' .. mission_note .. '</small>') or mission_activity,
probability = '—',
}
return entries
end
local ranked_candidates = rank_candidates(slot.action_candidates)
if #ranked_candidates == 0 then
local activity_text = format_slot_activity(slot)
if activity_text ~= '' then
entries[#entries + 1] = {
activity = activity_text,
probability = '—',
}
end
return entries
end
local total_weight = 0
for _, candidate in ipairs(ranked_candidates) do
total_weight = total_weight + (tonumber(candidate.weight or 0) or 0)
end
for _, candidate in ipairs(ranked_candidates) do
local activity_text = format_candidate_text(candidate)
if activity_text ~= '' then
entries[#entries + 1] = {
activity = activity_text,
probability = format_probability(candidate.weight, total_weight),
}
end
end
return entries
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
format_slot_activity = function(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 primary_action = type(slot.primary_action) == 'table' and slot.primary_action or nil
if primary_action then
local activity_text = format_candidate_text(primary_action)
if activity_text ~= '' then
return activity_text
end
end
return common.trim(slot.summary)
end
format_slot_note = function(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
return ''
end
local function render_variant_table(variant)
local root = mw.html.create('table'):addClass('wikitable'):addClass('char-data-table'):addClass('char-schedule-table')
local head = root:tag('tr')
head:tag('th'):wikitext('时间')
head:tag('th'):wikitext('安排')
head:tag('th'):wikitext('概率')
for _, slot in ipairs(variant.slots or {}) do
local entries = build_slot_schedule_entries(slot)
if #entries == 0 then
entries = {
{
activity = format_slot_activity(slot),
probability = '—',
}
}
end
local slot_time = format_slot_time(slot)
local time_text = slot_time ~= '' and slot_time or '—'
if #entries == 1 then
local row = root:tag('tr')
row:tag('td'):wikitext(time_text)
row:tag('td'):wikitext(common.trim(entries[1].activity) ~= '' and entries[1].activity or '—')
row:tag('td'):wikitext(common.trim(entries[1].probability) ~= '' and entries[1].probability or '—')
else
for index, entry in ipairs(entries) do
local row = root:tag('tr')
if index == 1 then
row:tag('td')
:attr('rowspan', tostring(#entries))
:wikitext(time_text)
end
row:tag('td'):wikitext(common.trim(entry.activity) ~= '' and entry.activity or '—')
row:tag('td'):wikitext(common.trim(entry.probability) ~= '' and entry.probability or '—')
end
end
end
return tostring(root)
end
local function render_schedule_group(variant, include_heading)
local out = { '<div class="char-schedule-group">' }
if include_heading ~= false then
out[#out + 1] = '<div class="char-schedule-heading"><b>' .. build_variant_heading(variant) .. '</b></div>'
end
out[#out + 1] = '<div class="char-table-wrap">' .. render_variant_table(variant) .. '</div>'
out[#out + 1] = '</div>'
return table.concat(out, '\n')
end
local function render_group(frame, 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
if #items == 1 then
return render_schedule_group(items[1], true)
end
local tab_parts = {}
for index, variant in ipairs(items) do
if index > 1 then
tab_parts[#tab_parts + 1] = '|-|'
end
tab_parts[#tab_parts + 1] = build_variant_tab_label(variant) .. '=\n' .. render_schedule_group(variant, true)
end
return frame:extensionTag('tabber', table.concat(tab_parts, '\n'), {
class = 'char-tabber char-schedule-group-tabber',
})
end
local function has_multi_candidate_slots(variants)
if type(variants) ~= 'table' then
return false
end
for _, variant in ipairs(variants) do
for _, slot in ipairs(variant.slots or {}) do
if #build_slot_schedule_entries(slot) > 1 then
return true
end
end
end
return false
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(frame, key)
local record = find_record(key)
if not record then
return '<div class="char-empty char-schedule-empty">未找到角色行程数据</div>'
end
local variants = filter_display_variants(merge_variants(record.variants))
if #variants == 0 then
return '<div class="char-empty char-schedule-empty">暂无日程数据。</div>'
end
local tab_parts = {}
for _, source in ipairs({ 'regular', 'special', 'living_together' }) do
local block = render_group(frame, source, variants)
if block ~= '' then
if #tab_parts > 0 then
tab_parts[#tab_parts + 1] = '|-|'
end
tab_parts[#tab_parts + 1] = (source_labels[source] or source) .. '='
tab_parts[#tab_parts + 1] = block
end
end
if #tab_parts == 0 then
return '<div class="char-empty char-schedule-empty">暂无可展示的行程数据。</div>'
end
local out = { '<div class="char-schedule-root">' }
if has_multi_candidate_slots(variants) then
out[#out + 1] = '<div class="char-note char-schedule-note">同一时段如出现多行,表示该时段存在多个候选动作;概率为该时段候选动作的相对权重。</div>'
end
out[#out + 1] = frame:extensionTag('tabber', table.concat(tab_parts, '\n'), {
class = 'char-tabber char-schedule-tabber',
})
out[#out + 1] = '</div>'
return table.concat(out, '\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, '')
local output = render_schedule_by_key(frame, key)
if output == '' then
return ''
end
return frame:preprocess(output)
end
return p