模块:Schedule:修订间差异
来自星砂岛百科
更多操作
同步更新 |
修正 Character 行程内层标签页的中文文案 |
||
| (未显示同一用户的27个中间版本) | |||
| 第1行: | 第1行: | ||
local common = require('Module:Common') | local common = require('Module:Common') | ||
local character_common = require('Module:CharacterCommon') | |||
local p = {} | local p = {} | ||
local | local raw_schedule_records | ||
local candidate_registry | |||
local expanded_record_cache | |||
local mapping | local mapping | ||
local location_labels | local location_labels | ||
| 第17行: | 第20行: | ||
special = '特殊日程', | special = '特殊日程', | ||
living_together = '同居日程', | living_together = '同居日程', | ||
} | } | ||
| 第41行: | 第37行: | ||
['3'] = '秋季', | ['3'] = '秋季', | ||
['4'] = '冬季', | ['4'] = '冬季', | ||
} | |||
local season_plain_labels = { | |||
spring = '春季', | |||
summer = '夏季', | |||
autumn = '秋季', | |||
fall = '秋季', | |||
winter = '冬季', | |||
} | } | ||
| 第48行: | 第52行: | ||
['3'] = 'autumn', | ['3'] = 'autumn', | ||
['4'] = 'winter', | ['4'] = 'winter', | ||
} | |||
local season_template_keys = { | |||
['1'] = '1', | |||
['2'] = '2', | |||
['3'] = '3', | |||
['4'] = '4', | |||
spring = 'spring', | |||
summer = 'summer', | |||
autumn = 'fall', | |||
fall = 'fall', | |||
winter = 'winter', | |||
} | } | ||
| 第74行: | 第90行: | ||
ClearSky = '晴天', | ClearSky = '晴天', | ||
Snow = '雪天', | Snow = '雪天', | ||
} | |||
local weather_template_keys = { | |||
Rain = 'Rainy', | |||
ClearSky = 'Sunny', | |||
Snow = 'Snowy', | |||
} | |||
local weather_plain_labels = { | |||
Rain = '雨天', | |||
Rainy = '雨天', | |||
ClearSky = '晴天', | |||
Sunny = '晴天', | |||
Snow = '雪天', | |||
Snowy = '雪天', | |||
} | } | ||
local festival_labels = { | local festival_labels = { | ||
['Festival.SpringFestivalActivity_POI_AfterFestival'] = ' | ['Festival.BeachFestival_Formal'] = '沙滩节', | ||
['Festival.SpringFestival_Formal'] = '春节', | |||
} | |||
local festival_heading_labels = { | |||
['Festival.SpringFestivalActivity_POI_AfterFestival'] = '春节准备', | |||
['Festival.SpringFestivalActivity_POI_InFestival'] = '春节期间', | ['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, | |||
}, | |||
} | } | ||
| 第104行: | 第158行: | ||
['执行互动'] = true, | ['执行互动'] = true, | ||
} | } | ||
local function normalize_key(value) | local function normalize_key(value) | ||
return | return character_common.normalizeKey(value) | ||
end | end | ||
| 第257行: | 第299行: | ||
local display_name = common.trim(primary_action.display_name) | local display_name = common.trim(primary_action.display_name) | ||
local location_hint = common.trim(primary_action.location_hint) | 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) | local template_name = common.trim(primary_action.template) | ||
if display_name ~= '' and | if display_name ~= '' and location_display ~= '' then | ||
return display_name .. ' (' .. | return display_name .. ' (' .. location_display .. ')' | ||
end | end | ||
if display_name ~= '' then | if display_name ~= '' then | ||
return display_name | return display_name | ||
end | end | ||
if | if location_display ~= '' and template_name ~= '' then | ||
return template_name .. ' (' .. | return template_name .. ' (' .. location_display .. ')' | ||
end | end | ||
return template_name | 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 | end | ||
| 第280行: | 第361行: | ||
display_name = common.trim(raw_candidate.d), | display_name = common.trim(raw_candidate.d), | ||
location_hint = common.trim(raw_candidate.l), | 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, | weight = raw_candidate.w or 0, | ||
score = raw_candidate.s or 0, | score = raw_candidate.s or 0, | ||
| 第396行: | 第479行: | ||
variants = variants, | variants = variants, | ||
} | } | ||
end | end | ||
local function load_data() | local function load_data() | ||
if | if raw_schedule_records then | ||
return | return | ||
end | 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 {} | location_labels = common.loadJsonData('数据:Character/schedule_location_labels.json') or {} | ||
end | end | ||
local function | local function expand_record_by_key(record_key) | ||
if record_key == '' then | |||
return nil | |||
if | |||
end | end | ||
if expanded_record_cache[record_key] ~= nil then | |||
if | return expanded_record_cache[record_key] or nil | ||
return | |||
end | end | ||
local | local raw_record = raw_schedule_records[record_key] | ||
if | if type(raw_record) ~= 'table' then | ||
expanded_record_cache[record_key] = false | |||
return nil | |||
end | end | ||
local | local record | ||
if | if type(raw_record.v) == 'table' then | ||
record = expand_compact_record(record_key, raw_record, candidate_registry or {}) | |||
else | |||
record = raw_record | |||
end | 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) | |||
return | |||
end | end | ||
| 第600行: | 第666行: | ||
sunday = 7, | 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 | end | ||
| 第607行: | 第713行: | ||
if a_source ~= b_source then | if a_source ~= b_source then | ||
return a_source < b_source | 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 | end | ||
| 第619行: | 第737行: | ||
if a_weekday ~= b_weekday then | if a_weekday ~= b_weekday then | ||
return a_weekday < b_weekday | return a_weekday < b_weekday | ||
end | end | ||
| 第707行: | 第819行: | ||
return ranked | return ranked | ||
end | end | ||
| 第739行: | 第827行: | ||
end | end | ||
local function should_use_structured_condition(desc) | local function should_use_structured_condition(desc, summary) | ||
local cleaned = clean_condition_desc(desc) | local cleaned = clean_condition_desc(desc) | ||
if cleaned == '' then | if cleaned == '' then | ||
| 第749行: | 第837行: | ||
if mw.ustring.find(cleaned, '[A-Za-z]') and not mw.ustring.find(cleaned, '[一-龥]') then | if mw.ustring.find(cleaned, '[A-Za-z]') and not mw.ustring.find(cleaned, '[一-龥]') then | ||
return true | 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 | end | ||
return false | return false | ||
| 第789行: | 第884行: | ||
end | 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 '' | return '' | ||
end | end | ||
| 第794行: | 第959行: | ||
local function build_apply_text(variant) | local function build_apply_text(variant) | ||
local parts = {} | local parts = {} | ||
local | 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 weekdays = map_labels(variant.weekday_labels, weekday_labels) | ||
local season_day_text = format_season_day_text(month_values, day_values) | |||
local days = {} | local days = {} | ||
for _, day in ipairs( | 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 | ||
end | end | ||
if #weekdays > 0 then | if #weekdays > 0 then | ||
parts[#parts + 1] = join_parts(weekdays) | parts[#parts + 1] = join_parts(weekdays) | ||
end | end | ||
if | if season_day_text ~= '' then | ||
parts[#parts + 1] = | parts[#parts + 1] = season_day_text | ||
end | end | ||
| 第818行: | 第999行: | ||
end | end | ||
local function describe_condition_item(item) | 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 | if type(item) ~= 'table' then | ||
return '' | return '' | ||
| 第825行: | 第1,059行: | ||
if item.type == 'weather_today' then | if item.type == 'weather_today' then | ||
local dest_type = common.trim(item.dest_type) | 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 label = weather_labels[dest_type] or dest_type | ||
if | local weather_text = template_key and ('{{Weather|' .. template_key .. '}}') or label | ||
if weather_text ~= '' then | |||
if item.reverse then | if item.reverse then | ||
return '非' .. | return '非' .. weather_text | ||
end | end | ||
return | return weather_text | ||
end | end | ||
return '天气条件' | return '天气条件' | ||
| 第875行: | 第1,111行: | ||
if item.type == 'calendar_now_is_festival' then | if item.type == 'calendar_now_is_festival' then | ||
local festival_template = common.trim(item.festival_template) | local festival_template = common.trim(item.festival_template) | ||
local label = festival_labels[festival_template] or festival_template | local label = festival_labels[festival_template] or festival_heading_labels[festival_template] or festival_template | ||
if label ~= '' then | if label ~= '' then | ||
if item.reverse then | if item.reverse then | ||
| 第991行: | 第1,227行: | ||
local condition_desc = clean_condition_desc(variant.condition_desc) | local condition_desc = clean_condition_desc(variant.condition_desc) | ||
local summary = variant.condition_summary | local summary = variant.condition_summary | ||
local title_text = build_structured_condition_title(summary) | |||
local parts = {} | local parts = {} | ||
if type(summary) == 'table' then | if type(summary) == 'table' then | ||
| 第1,002行: | 第1,239行: | ||
local structured_text = table.concat(unique_texts(parts), ';') | local structured_text = table.concat(unique_texts(parts), ';') | ||
if title_text ~= '' then | |||
return title_text | |||
end | |||
if condition_desc == '' then | if condition_desc == '' then | ||
return structured_text | return structured_text | ||
| 第1,008行: | 第1,248行: | ||
return condition_desc | return condition_desc | ||
end | end | ||
if should_use_structured_condition(condition_desc) then | if should_use_structured_condition(condition_desc, summary) then | ||
return structured_text | return structured_text | ||
end | end | ||
| 第1,029行: | 第1,269行: | ||
return '默认' | return '默认' | ||
end | end | ||
return table.concat(parts, ' | return table.concat(parts, '|') | ||
end | end | ||
local function | local function resolve_season_plain_label(value) | ||
local | local normalized = mw.ustring.lower(common.trim(value)) | ||
if | if normalized == '' then | ||
return '' | return '' | ||
end | end | ||
local | local season_key = season_keys[normalized] or normalized | ||
if | if season_number_labels[season_key] then | ||
return | return season_number_labels[season_key] | ||
end | end | ||
if season_plain_labels[season_key] then | |||
if | return season_plain_labels[season_key] | ||
return | |||
end | end | ||
return common.trim(value) | |||
end | |||
local | local function build_variant_tab_label(variant) | ||
local text = build_variant_heading(variant) | |||
local | text = mw.ustring.gsub(text, '{{%s*[Ss]eason%s*|%s*([^|}]+)%s*|%s*([^}]+)%s*}}', function(key, day) | ||
if | local season_label = resolve_season_plain_label(key) | ||
return | 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 | end | ||
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 | return season_label ~= '' and season_label or common.trim(key) | ||
end) | |||
text = mw.ustring.gsub(text, '{{%s*[Ww]eather%s*|%s*([^}]+)%s*}}', function(key) | |||
return text | 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 | end | ||
| 第1,066行: | 第1,320行: | ||
if type(candidate) ~= 'table' then | if type(candidate) ~= 'table' then | ||
return '' | return '' | ||
end | |||
local merged_display_name = common.trim(candidate.merged_display_name) | |||
if merged_display_name ~= '' then | |||
return merged_display_name | |||
end | end | ||
local display_name = common.trim(candidate.display_name) | local display_name = common.trim(candidate.display_name) | ||
local | 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 | if display_name ~= '' and location_label ~= '' and not mw.ustring.find(display_name, location_label, 1, true) then | ||
return display_name .. '(' .. | return display_name .. '(' .. location_label .. ')' | ||
end | end | ||
if display_name ~= '' then | if display_name ~= '' then | ||
return display_name | return display_name | ||
end | end | ||
if | |||
return template_name | if location_label ~= '' then | ||
return location_label | |||
end | |||
local template_name = common.trim(candidate.template) | |||
if template_name ~= '' then | |||
return template_name | |||
end | end | ||
return | |||
return common.trim(candidate.location_hint) | |||
end | end | ||
local function | local function format_probability(weight, total_weight) | ||
local current = tonumber(weight or 0) or 0 | |||
return | local total = tonumber(total_weight or 0) or 0 | ||
if current <= 0 then | |||
return '—' | |||
end | |||
if total <= 0 then | |||
return tostring(current) | |||
end | end | ||
local | |||
local percent = current * 100 / total | |||
return | if math.abs(percent - math.floor(percent + 0.5)) < 0.05 then | ||
return string.format('%d%%', math.floor(percent + 0.5)) | |||
end | end | ||
return | return string.format('%.1f%%', percent) | ||
end | end | ||
local function | local format_slot_activity | ||
if type( | local format_slot_note | ||
return '' | |||
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 | end | ||
return | |||
return entries | |||
end | end | ||
| 第1,106行: | 第1,422行: | ||
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 | ||
end | end | ||
format_slot_activity = function(slot) | |||
if type(slot) ~= 'table' then | if type(slot) ~= 'table' then | ||
return '' | return '' | ||
| 第1,130行: | 第1,446行: | ||
end | end | ||
local | 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 | |||
if | |||
end | end | ||
end | end | ||
return | return common.trim(slot.summary) | ||
end | end | ||
format_slot_note = function(slot) | |||
if type(slot) ~= 'table' then | if type(slot) ~= 'table' then | ||
return '' | return '' | ||
| 第1,190行: | 第1,466行: | ||
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,471行: | ||
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,205行: | 第1,476行: | ||
end | end | ||
return '' | |||
return | |||
end | end | ||
local function render_variant_table(variant) | local function render_variant_table(variant) | ||
local root = mw.html.create('table'):addClass('wikitable') | local root = mw.html.create('table'):addClass('wikitable'):addClass('char-data-table'):addClass('char-schedule-table') | ||
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('概率') | ||
for _, slot in ipairs(variant.slots or {}) do | for _, slot in ipairs(variant.slots or {}) do | ||
local row = root:tag('tr') | local entries = build_slot_schedule_entries(slot) | ||
if #entries == 0 then | |||
entries = { | |||
row:tag('td'):wikitext( | { | ||
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 | end | ||
| 第1,259行: | 第1,523行: | ||
end | end | ||
local function render_group(source, variants) | 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 = {} | local items = {} | ||
for _, variant in ipairs(variants) do | for _, variant in ipairs(variants) do | ||
| 第1,271行: | 第1,545行: | ||
end | end | ||
if #items == 1 then | |||
return render_schedule_group(items[1], true) | |||
for | 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 | end | ||
return | return false | ||
end | end | ||
| 第1,296行: | 第1,593行: | ||
end | end | ||
local function render_schedule_by_key(key) | local function render_schedule_by_key(frame, key) | ||
local record = find_record(key) | local record = find_record(key) | ||
if not record then | if not record then | ||
return '' | return '<div class="char-empty char-schedule-empty">未找到角色行程数据</div>' | ||
end | end | ||
local variants = filter_display_variants(merge_variants(record.variants)) | local variants = filter_display_variants(merge_variants(record.variants)) | ||
if #variants == 0 then | if #variants == 0 then | ||
return '暂无日程数据。' | return '<div class="char-empty char-schedule-empty">暂无日程数据。</div>' | ||
end | end | ||
local | 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 | |||
local | tab_parts[#tab_parts + 1] = (source_labels[source] or source) .. '=' | ||
if | tab_parts[#tab_parts + 1] = block | ||
end | end | ||
end | end | ||
if # | if #tab_parts == 0 then | ||
return '<div class="char-empty char-schedule-empty">暂无可展示的行程数据。</div>' | |||
end | 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 | end | ||
out[#out + 1] = frame:extensionTag('tabber', table.concat(tab_parts, '\n'), { | |||
return table.concat(out, ' | class = 'char-tabber char-schedule-tabber', | ||
}) | |||
out[#out + 1] = '</div>' | |||
return table.concat(out, '\n') | |||
end | end | ||
| 第1,361行: | 第1,648行: | ||
function p.renderSchedule(frame) | function p.renderSchedule(frame) | ||
local key = common.getArg(frame, 1, '') | local key = common.getArg(frame, 1, '') | ||
local output = render_schedule_by_key(frame, key) | |||
if output == '' then | |||
return '' | |||
end | |||
return frame:preprocess(output) | |||
end | end | ||
return p | return p | ||
2026年4月1日 (三) 16:08的最新版本
概述
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