打开/关闭菜单
打开/关闭外观设置菜单
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。

本站正在进行早期测试,目前仍存在许多内容的缺失。

模块:Schedule:修订间差异

来自星砂岛百科
Sizau-bot留言 | 贡献
同步 Character 行程更新
 
Sizau-bot留言 | 贡献
修复 Schedule 行程误判为空
第41行: 第41行:
     ['3'] = '秋季',
     ['3'] = '秋季',
     ['4'] = '冬季',
     ['4'] = '冬季',
}
local season_keys = {
    ['1'] = 'spring',
    ['2'] = 'summer',
    ['3'] = 'autumn',
    ['4'] = 'winter',
}
}


第51行: 第58行:
     ['5'] = '周五',
     ['5'] = '周五',
     ['6'] = '周六',
     ['6'] = '周六',
}
local weekday_keys = {
    ['0'] = 'sunday',
    ['1'] = 'monday',
    ['2'] = 'tuesday',
    ['3'] = 'wednesday',
    ['4'] = 'thursday',
    ['5'] = 'friday',
    ['6'] = 'saturday',
}
}


第113行: 第130行:
     end
     end
     return out
     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
end


第156行: 第197行:
         return tonumber(a) < tonumber(b)
         return tonumber(a) < tonumber(b)
     end)
     end)
end
local function pick_primary_action(candidates)
    if type(candidates) ~= 'table' or #candidates == 0 then
        return {}
    end
    local ranked = {}
    for _, candidate in ipairs(candidates) do
        if type(candidate) == 'table' then
            ranked[#ranked + 1] = candidate
        end
    end
    table.sort(ranked, function(a, b)
        local a_weight = tonumber(a.weight or 0) or 0
        local b_weight = tonumber(b.weight or 0) or 0
        if a_weight ~= b_weight then
            return a_weight > b_weight
        end
        local a_score = tonumber(a.score or 0) or 0
        local b_score = tonumber(b.score or 0) or 0
        if a_score ~= b_score then
            return a_score > b_score
        end
        return common.trim(a.template) > common.trim(b.template)
    end)
    return ranked[1] or {}
end
local function build_slot_summary(slot)
    if type(slot) ~= 'table' then
        return ''
    end
    local mission_template = common.trim(slot.mission_template)
    if mission_template ~= '' then
        local mission_display = common.trim(slot.mission_display)
        if mission_display ~= '' then
            return mission_display
        end
        local mission_title = common.trim(slot.mission_title)
        if mission_title ~= '' then
            return mission_title
        end
        return mission_template
    end
    local primary_action = slot.primary_action
    if type(primary_action) ~= 'table' then
        return ''
    end
    local display_name = common.trim(primary_action.display_name)
    local location_hint = common.trim(primary_action.location_hint)
    local template_name = common.trim(primary_action.template)
    if display_name ~= '' and location_hint ~= '' then
        return display_name .. ' (' .. location_hint .. ')'
    end
    if display_name ~= '' then
        return display_name
    end
    if location_hint ~= '' and template_name ~= '' then
        return template_name .. ' (' .. location_hint .. ')'
    end
    return template_name
end
local function expand_candidate_entry(raw_candidate)
    if type(raw_candidate) ~= 'table' then
        return {}
    end
    return {
        template = common.trim(raw_candidate.t),
        display_name = common.trim(raw_candidate.d),
        location_hint = common.trim(raw_candidate.l),
        weight = raw_candidate.w or 0,
        score = raw_candidate.s or 0,
    }
end
local function expand_compact_slot(raw_slot, candidate_registry)
    if type(raw_slot) ~= 'table' then
        return {}
    end
    local slot = {
        begin_time = common.trim(raw_slot.b),
        end_time = common.trim(raw_slot.e),
        kind = common.trim(raw_slot.k),
    }
    if slot.kind == 'actions' then
        local action_candidates = {}
        if type(raw_slot.a) == 'table' then
            for _, raw_index in ipairs(raw_slot.a) do
                local index = tonumber(raw_index)
                if index ~= nil then
                    local candidate = candidate_registry[index + 1]
                    if type(candidate) == 'table' then
                        action_candidates[#action_candidates + 1] = expand_candidate_entry(candidate)
                    end
                end
            end
        end
        slot.action_candidates = action_candidates
        slot.primary_action = pick_primary_action(action_candidates)
    elseif slot.kind == 'mission' then
        slot.mission_template = common.trim(raw_slot.mt)
        slot.mission_title = common.trim(raw_slot.tt)
        slot.mission_goal = common.trim(raw_slot.g)
        slot.mission_display = common.trim(raw_slot.md)
    end
    slot.summary = build_slot_summary(slot)
    return slot
end
local function expand_compact_variant(raw_variant, candidate_registry)
    if type(raw_variant) ~= 'table' then
        return {}
    end
    local months = normalize_number_array(raw_variant.m)
    local weekdays = normalize_number_array(raw_variant.w)
    local days = normalize_number_array(raw_variant.d)
    local month_keys = {}
    local weekday_label_keys = {}
    for _, value in ipairs(months) do
        local key = season_keys[tostring(value)]
        if key then
            month_keys[#month_keys + 1] = key
        end
    end
    for _, value in ipairs(weekdays) do
        local key = weekday_keys[tostring(value)]
        if key then
            weekday_label_keys[#weekday_label_keys + 1] = key
        end
    end
    local slots = {}
    if type(raw_variant.sl) == 'table' then
        for _, raw_slot in ipairs(raw_variant.sl) do
            if type(raw_slot) == 'table' then
                slots[#slots + 1] = expand_compact_slot(raw_slot, candidate_registry)
            end
        end
    end
    return {
        source = common.trim(raw_variant.o),
        schedule_id = common.trim(raw_variant.i),
        months = months,
        month_labels = month_keys,
        weekdays = weekdays,
        weekday_labels = weekday_label_keys,
        days = days,
        priority = '',
        condition_desc = common.trim(raw_variant.c),
        condition_summary = type(raw_variant.cs) == 'table' and raw_variant.cs or {},
        condition_raw = {},
        slots = slots,
    }
end
local function expand_compact_record(record_key, raw_record, candidate_registry)
    if type(raw_record) ~= 'table' then
        return nil
    end
    local counts = type(raw_record.r) == 'table' and raw_record.r or {}
    local variants = {}
    if type(raw_record.v) == 'table' then
        for _, raw_variant in ipairs(raw_record.v) do
            if type(raw_variant) == 'table' then
                variants[#variants + 1] = expand_compact_variant(raw_variant, candidate_registry)
            end
        end
    end
    return {
        id = common.trim(raw_record.i),
        name = common.trim(raw_record.n),
        key = record_key,
        calendar_id = common.trim(raw_record.c),
        regular_count = tonumber(counts[1] or 0) or 0,
        special_count = tonumber(counts[2] or 0) or 0,
        living_together_count = tonumber(counts[3] or 0) or 0,
        variants = variants,
    }
end
local function expand_compact_schedule_data(raw_data)
    if type(raw_data) ~= 'table' then
        return {}
    end
    if type(raw_data.records) ~= 'table' or type(raw_data.candidate_registry) ~= 'table' then
        return raw_data
    end
    local expanded = {}
    for record_key, raw_record in pairs(raw_data.records) do
        local normalized = normalize_key(record_key)
        local record = expand_compact_record(normalized, raw_record, raw_data.candidate_registry)
        if normalized ~= '' and record then
            expanded[normalized] = record
        end
    end
    return expanded
end
end


第163行: 第424行:
     end
     end


     schedule_data = common.loadJsonData('数据:Character/character_schedule_index.json') or {}
     schedule_data = expand_compact_schedule_data(common.loadJsonData('数据:Character/character_schedule_index.json') or {})
     mapping = common.loadJsonData('数据:Character/character_mapping.json') or default_mapping()
     mapping = common.loadJsonData('数据:Character/character_mapping.json') or default_mapping()
     location_labels = common.loadJsonData('数据:Character/schedule_location_labels.json') or {}
     location_labels = common.loadJsonData('数据:Character/schedule_location_labels.json') or {}
第997行: 第1,258行:
     local items = {}
     local items = {}
     for _, variant in ipairs(variants) do
     for _, variant in ipairs(variants) do
         if variant.source == source and type(variant.slots) == 'table' and #variant.slots > 0 then
         if variant.source == source and has_ipairs_items(variant.slots) then
             items[#items + 1] = variant
             items[#items + 1] = variant
         end
         end
第1,023行: 第1,284行:


     for _, variant in ipairs(variants) do
     for _, variant in ipairs(variants) do
         if type(variant) == 'table' and type(variant.slots) == 'table' and #variant.slots > 0 then
         if type(variant) == 'table' and has_ipairs_items(variant.slots) then
             out[#out + 1] = variant
             out[#out + 1] = variant
         end
         end

2026年3月18日 (三) 22:44的版本

概述

Schedule 用于读取并展示角色行程数据,供 {{Schedule}} 与人物页“日程”章节调用。

用法

{{#invoke:Schedule|getField|晨星|calendar_id}}
{{#invoke:Schedule|renderSchedule|晨星}}

示例

{{#invoke:Schedule|renderSchedule|晨星}}

函数

  • getField:读取行程记录的顶层字段,如 calendar_idregular_countspecial_count
  • renderSchedule:输出角色的日程区块,按常规、特殊、同居三类分组展示。

数据来源


local common = require('Module:Common')

local p = {}

local schedule_data
local mapping
local location_labels

local source_order = {
    regular = 1,
    special = 2,
    living_together = 3,
}

local source_labels = {
    regular = '常规日程',
    special = '特殊日程',
    living_together = '同居日程',
}

local month_labels = {
    spring = '春季',
    summer = '夏季',
    autumn = '秋季',
    winter = '冬季',
}

local weekday_labels = {
    monday = '周一',
    tuesday = '周二',
    wednesday = '周三',
    thursday = '周四',
    friday = '周五',
    saturday = '周六',
    sunday = '周日',
}

local season_number_labels = {
    ['1'] = '春季',
    ['2'] = '夏季',
    ['3'] = '秋季',
    ['4'] = '冬季',
}

local season_keys = {
    ['1'] = 'spring',
    ['2'] = 'summer',
    ['3'] = 'autumn',
    ['4'] = 'winter',
}

local weekday_number_labels = {
    ['0'] = '周日',
    ['1'] = '周一',
    ['2'] = '周二',
    ['3'] = '周三',
    ['4'] = '周四',
    ['5'] = '周五',
    ['6'] = '周六',
}

local weekday_keys = {
    ['0'] = 'sunday',
    ['1'] = 'monday',
    ['2'] = 'tuesday',
    ['3'] = 'wednesday',
    ['4'] = 'thursday',
    ['5'] = 'friday',
    ['6'] = 'saturday',
}

local weather_labels = {
    Rain = '雨天',
    ClearSky = '晴天',
    Snow = '雪天',
}

local festival_labels = {
    ['Festival.SpringFestivalActivity_POI_AfterFestival'] = '春节前',
    ['Festival.SpringFestivalActivity_POI_InFestival'] = '春节期间',
}

local buildable_labels = {
    BuildableRegion_Boat = '渡口建造',
}

local compare_op_labels = {
    Equal = '等于',
    GreatOrEqualThan = '大于等于',
    GreatThan = '大于',
    LessOrEqualThan = '小于等于',
    LessThan = '小于',
    Between = '介于',
}

local generic_condition_descs = {
    ['On Raining Days'] = true,
    ['雨天日程'] = true,
    ['雨雪天'] = true,
    ['雨雪天日程'] = true,
}

local generic_mission_goals = {
    ['执行互动'] = true,
}

local function default_mapping()
    return {
        name_to_id = {},
        id_to_name = {},
        aliases = {},
        overrides = {
            name_to_id = {},
            aliases = {},
        },
    }
end

local function normalize_key(value)
    return common.normalizeKey(value)
end

local function copy_array(values)
    local out = {}
    if type(values) ~= 'table' then
        return out
    end
    for _, value in ipairs(values) do
        out[#out + 1] = value
    end
    return out
end

local function normalize_number_array(values)
    local out = {}
    if type(values) ~= 'table' then
        return out
    end
    for _, value in ipairs(values) do
        local number = tonumber(value)
        if number ~= nil then
            out[#out + 1] = number
        end
    end
    return out
end

local function has_ipairs_items(values)
    if type(values) ~= 'table' then
        return false
    end
    for _ in ipairs(values) do
        return true
    end
    return false
end

local function append_unique(target, values)
    if type(target) ~= 'table' or type(values) ~= 'table' then
        return
    end

    local seen = {}
    for _, value in ipairs(target) do
        seen[tostring(value)] = true
    end
    for _, value in ipairs(values) do
        local key = tostring(value)
        if not seen[key] then
            target[#target + 1] = value
            seen[key] = true
        end
    end
end

local function sort_text_list(values, order_map)
    table.sort(values, function(a, b)
        local a_key = tostring(a)
        local b_key = tostring(b)
        local a_rank = order_map and order_map[a_key] or nil
        local b_rank = order_map and order_map[b_key] or nil
        if a_rank and b_rank and a_rank ~= b_rank then
            return a_rank < b_rank
        end
        if a_rank and not b_rank then
            return true
        end
        if b_rank and not a_rank then
            return false
        end
        return a_key < b_key
    end)
end

local function sort_number_list(values)
    table.sort(values, function(a, b)
        return tonumber(a) < tonumber(b)
    end)
end

local function pick_primary_action(candidates)
    if type(candidates) ~= 'table' or #candidates == 0 then
        return {}
    end

    local ranked = {}
    for _, candidate in ipairs(candidates) do
        if type(candidate) == 'table' then
            ranked[#ranked + 1] = candidate
        end
    end

    table.sort(ranked, function(a, b)
        local a_weight = tonumber(a.weight or 0) or 0
        local b_weight = tonumber(b.weight or 0) or 0
        if a_weight ~= b_weight then
            return a_weight > b_weight
        end

        local a_score = tonumber(a.score or 0) or 0
        local b_score = tonumber(b.score or 0) or 0
        if a_score ~= b_score then
            return a_score > b_score
        end

        return common.trim(a.template) > common.trim(b.template)
    end)

    return ranked[1] or {}
end

local function build_slot_summary(slot)
    if type(slot) ~= 'table' then
        return ''
    end

    local mission_template = common.trim(slot.mission_template)
    if mission_template ~= '' then
        local mission_display = common.trim(slot.mission_display)
        if mission_display ~= '' then
            return mission_display
        end

        local mission_title = common.trim(slot.mission_title)
        if mission_title ~= '' then
            return mission_title
        end

        return mission_template
    end

    local primary_action = slot.primary_action
    if type(primary_action) ~= 'table' then
        return ''
    end

    local display_name = common.trim(primary_action.display_name)
    local location_hint = common.trim(primary_action.location_hint)
    local template_name = common.trim(primary_action.template)

    if display_name ~= '' and location_hint ~= '' then
        return display_name .. ' (' .. location_hint .. ')'
    end
    if display_name ~= '' then
        return display_name
    end
    if location_hint ~= '' and template_name ~= '' then
        return template_name .. ' (' .. location_hint .. ')'
    end
    return template_name
end

local function expand_candidate_entry(raw_candidate)
    if type(raw_candidate) ~= 'table' then
        return {}
    end

    return {
        template = common.trim(raw_candidate.t),
        display_name = common.trim(raw_candidate.d),
        location_hint = common.trim(raw_candidate.l),
        weight = raw_candidate.w or 0,
        score = raw_candidate.s or 0,
    }
end

local function expand_compact_slot(raw_slot, candidate_registry)
    if type(raw_slot) ~= 'table' then
        return {}
    end

    local slot = {
        begin_time = common.trim(raw_slot.b),
        end_time = common.trim(raw_slot.e),
        kind = common.trim(raw_slot.k),
    }

    if slot.kind == 'actions' then
        local action_candidates = {}
        if type(raw_slot.a) == 'table' then
            for _, raw_index in ipairs(raw_slot.a) do
                local index = tonumber(raw_index)
                if index ~= nil then
                    local candidate = candidate_registry[index + 1]
                    if type(candidate) == 'table' then
                        action_candidates[#action_candidates + 1] = expand_candidate_entry(candidate)
                    end
                end
            end
        end
        slot.action_candidates = action_candidates
        slot.primary_action = pick_primary_action(action_candidates)
    elseif slot.kind == 'mission' then
        slot.mission_template = common.trim(raw_slot.mt)
        slot.mission_title = common.trim(raw_slot.tt)
        slot.mission_goal = common.trim(raw_slot.g)
        slot.mission_display = common.trim(raw_slot.md)
    end

    slot.summary = build_slot_summary(slot)
    return slot
end

local function expand_compact_variant(raw_variant, candidate_registry)
    if type(raw_variant) ~= 'table' then
        return {}
    end

    local months = normalize_number_array(raw_variant.m)
    local weekdays = normalize_number_array(raw_variant.w)
    local days = normalize_number_array(raw_variant.d)
    local month_keys = {}
    local weekday_label_keys = {}

    for _, value in ipairs(months) do
        local key = season_keys[tostring(value)]
        if key then
            month_keys[#month_keys + 1] = key
        end
    end
    for _, value in ipairs(weekdays) do
        local key = weekday_keys[tostring(value)]
        if key then
            weekday_label_keys[#weekday_label_keys + 1] = key
        end
    end

    local slots = {}
    if type(raw_variant.sl) == 'table' then
        for _, raw_slot in ipairs(raw_variant.sl) do
            if type(raw_slot) == 'table' then
                slots[#slots + 1] = expand_compact_slot(raw_slot, candidate_registry)
            end
        end
    end

    return {
        source = common.trim(raw_variant.o),
        schedule_id = common.trim(raw_variant.i),
        months = months,
        month_labels = month_keys,
        weekdays = weekdays,
        weekday_labels = weekday_label_keys,
        days = days,
        priority = '',
        condition_desc = common.trim(raw_variant.c),
        condition_summary = type(raw_variant.cs) == 'table' and raw_variant.cs or {},
        condition_raw = {},
        slots = slots,
    }
end

local function expand_compact_record(record_key, raw_record, candidate_registry)
    if type(raw_record) ~= 'table' then
        return nil
    end

    local counts = type(raw_record.r) == 'table' and raw_record.r or {}
    local variants = {}
    if type(raw_record.v) == 'table' then
        for _, raw_variant in ipairs(raw_record.v) do
            if type(raw_variant) == 'table' then
                variants[#variants + 1] = expand_compact_variant(raw_variant, candidate_registry)
            end
        end
    end

    return {
        id = common.trim(raw_record.i),
        name = common.trim(raw_record.n),
        key = record_key,
        calendar_id = common.trim(raw_record.c),
        regular_count = tonumber(counts[1] or 0) or 0,
        special_count = tonumber(counts[2] or 0) or 0,
        living_together_count = tonumber(counts[3] or 0) or 0,
        variants = variants,
    }
end

local function expand_compact_schedule_data(raw_data)
    if type(raw_data) ~= 'table' then
        return {}
    end

    if type(raw_data.records) ~= 'table' or type(raw_data.candidate_registry) ~= 'table' then
        return raw_data
    end

    local expanded = {}
    for record_key, raw_record in pairs(raw_data.records) do
        local normalized = normalize_key(record_key)
        local record = expand_compact_record(normalized, raw_record, raw_data.candidate_registry)
        if normalized ~= '' and record then
            expanded[normalized] = record
        end
    end

    return expanded
end

local function load_data()
    if schedule_data then
        return
    end

    schedule_data = expand_compact_schedule_data(common.loadJsonData('数据:Character/character_schedule_index.json') or {})
    mapping = common.loadJsonData('数据:Character/character_mapping.json') or default_mapping()
    location_labels = common.loadJsonData('数据:Character/schedule_location_labels.json') or {}
end

local function find_record(key)
    load_data()

    local resolved = common.trim(key)
    if resolved == '' then
        resolved = common.getCurrentTitleText()
    end

    local normalized = normalize_key(resolved)
    if schedule_data[normalized] then
        return schedule_data[normalized]
    end

    local override_id = mapping.overrides and mapping.overrides.name_to_id and mapping.overrides.name_to_id[normalized]
    if override_id and schedule_data[normalize_key(override_id)] then
        return schedule_data[normalize_key(override_id)]
    end

    local override_alias = mapping.overrides and mapping.overrides.aliases and mapping.overrides.aliases[normalized]
    if override_alias and schedule_data[normalize_key(override_alias)] then
        return schedule_data[normalize_key(override_alias)]
    end

    local mapped_id = mapping.name_to_id and mapping.name_to_id[normalized]
    if mapped_id and schedule_data[normalize_key(mapped_id)] then
        return schedule_data[normalize_key(mapped_id)]
    end

    local alias_id = mapping.aliases and mapping.aliases[normalized]
    if alias_id and schedule_data[normalize_key(alias_id)] then
        return schedule_data[normalize_key(alias_id)]
    end

    return nil
end

local function list_key(values)
    if type(values) ~= 'table' then
        return ''
    end

    local out = {}
    for _, value in ipairs(values) do
        out[#out + 1] = tostring(value)
    end
    return table.concat(out, ',')
end

local function build_condition_key(items)
    if type(items) ~= 'table' then
        return ''
    end

    local out = {}
    for _, item in ipairs(items) do
        if type(item) == 'table' then
            out[#out + 1] = table.concat({
                common.trim(item.type),
                tostring(item.reverse),
                common.trim(item.dest_type),
                common.trim(item.key),
                common.trim(item.tag),
                common.trim(item.op),
                common.trim(item.value),
                tostring(item.check_day),
                tostring(item.check_month),
                tostring(item.check_weekday),
                tostring(item.check_year),
                common.trim(item.year_op),
                common.trim(item.year_value),
            }, '~')
        end
    end
    return table.concat(out, '||')
end

local function build_slot_key(slots)
    if type(slots) ~= 'table' then
        return ''
    end

    local out = {}
    for _, slot in ipairs(slots) do
        if type(slot) == 'table' then
            local primary_action = slot.primary_action
            local primary_template = ''
            if type(primary_action) == 'table' then
                primary_template = common.trim(primary_action.template)
            end
            out[#out + 1] = table.concat({
                common.trim(slot.begin_time),
                common.trim(slot.end_time),
                common.trim(slot.kind),
                common.trim(slot.summary),
                common.trim(slot.mission_template),
                primary_template,
            }, '~')
        end
    end
    return table.concat(out, '||')
end

local function clone_variant(variant)
    return {
        source = common.trim(variant.source),
        schedule_id = common.trim(variant.schedule_id),
        months = copy_array(variant.months),
        month_labels = copy_array(variant.month_labels),
        weekdays = copy_array(variant.weekdays),
        weekday_labels = copy_array(variant.weekday_labels),
        days = copy_array(variant.days),
        priority = variant.priority,
        condition_desc = common.trim(variant.condition_desc),
        condition_summary = variant.condition_summary or {},
        condition_raw = variant.condition_raw or {},
        slots = variant.slots or {},
    }
end

local function merge_variants(variants)
    local grouped = {}
    local order = {}

    if type(variants) ~= 'table' then
        return order
    end

    for _, variant in ipairs(variants) do
        if type(variant) == 'table' then
            local key = table.concat({
                common.trim(variant.source),
                common.trim(variant.schedule_id),
                list_key(variant.months),
                list_key(variant.days),
                common.trim(variant.condition_desc),
                build_condition_key(variant.condition_summary),
                build_slot_key(variant.slots),
            }, '|')

            if not grouped[key] then
                grouped[key] = clone_variant(variant)
                order[#order + 1] = grouped[key]
            else
                append_unique(grouped[key].weekdays, variant.weekdays or {})
                append_unique(grouped[key].weekday_labels, variant.weekday_labels or {})
                append_unique(grouped[key].months, variant.months or {})
                append_unique(grouped[key].month_labels, variant.month_labels or {})
                append_unique(grouped[key].days, variant.days or {})
            end
        end
    end

    for _, variant in ipairs(order) do
        sort_number_list(variant.months)
        sort_number_list(variant.weekdays)
        sort_number_list(variant.days)
        sort_text_list(variant.month_labels, {
            spring = 1,
            summer = 2,
            autumn = 3,
            winter = 4,
        })
        sort_text_list(variant.weekday_labels, {
            monday = 1,
            tuesday = 2,
            wednesday = 3,
            thursday = 4,
            friday = 5,
            saturday = 6,
            sunday = 7,
        })
    end

    table.sort(order, function(a, b)
        local a_source = source_order[a.source] or 99
        local b_source = source_order[b.source] or 99
        if a_source ~= b_source then
            return a_source < b_source
        end

        local a_month = tonumber(a.months[1] or 99) or 99
        local b_month = tonumber(b.months[1] or 99) or 99
        if a_month ~= b_month then
            return a_month < b_month
        end

        local a_weekday = tonumber(a.weekdays[1] or 99) or 99
        local b_weekday = tonumber(b.weekdays[1] or 99) or 99
        if a_weekday ~= b_weekday then
            return a_weekday < b_weekday
        end

        local a_day = tonumber(a.days[1] or 99) or 99
        local b_day = tonumber(b.days[1] or 99) or 99
        if a_day ~= b_day then
            return a_day < b_day
        end

        local a_desc = common.trim(a.condition_desc)
        local b_desc = common.trim(b.condition_desc)
        if a_desc ~= b_desc then
            return a_desc < b_desc
        end

        return common.trim(a.schedule_id) < common.trim(b.schedule_id)
    end)

    return order
end

local function map_labels(values, label_map)
    if type(values) ~= 'table' then
        return {}
    end

    local out = {}
    for _, value in ipairs(values) do
        local text = common.trim(value)
        if text ~= '' then
            out[#out + 1] = label_map[text] or text
        end
    end
    return out
end

local function join_parts(values)
    if type(values) ~= 'table' or #values == 0 then
        return ''
    end
    return table.concat(values, '、')
end

local function unique_texts(values)
    local out = {}
    local seen = {}
    if type(values) ~= 'table' then
        return out
    end

    for _, value in ipairs(values) do
        local text = common.trim(value)
        if text ~= '' and not seen[text] then
            out[#out + 1] = text
            seen[text] = true
        end
    end
    return out
end

local function rank_candidates(candidates)
    local ranked = {}
    if type(candidates) ~= 'table' then
        return ranked
    end

    for _, candidate in ipairs(candidates) do
        if type(candidate) == 'table' then
            ranked[#ranked + 1] = candidate
        end
    end

    table.sort(ranked, function(a, b)
        local a_weight = tonumber(a.weight or 0) or 0
        local b_weight = tonumber(b.weight or 0) or 0
        if a_weight ~= b_weight then
            return a_weight > b_weight
        end

        local a_score = tonumber(a.score or 0) or 0
        local b_score = tonumber(b.score or 0) or 0
        if a_score ~= b_score then
            return a_score > b_score
        end

        return common.trim(a.template) > common.trim(b.template)
    end)

    return ranked
end

local function get_top_candidates(candidates)
    local ranked = rank_candidates(candidates)
    if #ranked == 0 then
        return ranked, {}
    end

    local first = ranked[1]
    local top_weight = tonumber(first.weight or 0) or 0
    local top_score = tonumber(first.score or 0) or 0
    local top = {}

    for _, candidate in ipairs(ranked) do
        local weight = tonumber(candidate.weight or 0) or 0
        local score = tonumber(candidate.score or 0) or 0
        if weight == top_weight and score == top_score then
            top[#top + 1] = candidate
        else
            break
        end
    end

    return ranked, top
end

local function clean_condition_desc(value)
    local text = common.trim(value)
    text = mw.ustring.gsub(text, '^%*+', '')
    return common.trim(text)
end

local function should_use_structured_condition(desc)
    local cleaned = clean_condition_desc(desc)
    if cleaned == '' then
        return true
    end
    if generic_condition_descs[cleaned] then
        return true
    end
    if mw.ustring.find(cleaned, '[A-Za-z]') and not mw.ustring.find(cleaned, '[一-龥]') then
        return true
    end
    return false
end

local function describe_compare_range(op, value, min_value, max_value, label_map, suffix)
    local resolved_op = common.trim(op)
    local resolved_value = common.trim(value)
    local resolved_min = common.trim(min_value)
    local resolved_max = common.trim(max_value)
    local resolved_suffix = suffix or ''

    local function map_value(raw)
        local key = common.trim(raw)
        if key == '' then
            return ''
        end
        if label_map and label_map[key] then
            return label_map[key]
        end
        return key .. resolved_suffix
    end

    if resolved_op == 'Equal' and resolved_value ~= '' then
        return map_value(resolved_value)
    end

    if resolved_op == 'Between' and resolved_min ~= '' and resolved_max ~= '' then
        local left = map_value(resolved_min)
        local right = map_value(resolved_max)
        if label_map then
            return left .. ' - ' .. right
        end
        return resolved_min .. ' - ' .. resolved_max .. resolved_suffix
    end

    local compare_label = compare_op_labels[resolved_op] or resolved_op
    if compare_label ~= '' and resolved_value ~= '' then
        return compare_label .. map_value(resolved_value)
    end

    return ''
end

local function build_apply_text(variant)
    local parts = {}
    local months = map_labels(variant.month_labels, month_labels)
    local weekdays = map_labels(variant.weekday_labels, weekday_labels)
    local days = {}

    for _, day in ipairs(variant.days or {}) do
        local value = common.trim(day)
        if value ~= '' then
            days[#days + 1] = value .. '日'
        end
    end

    if #months > 0 then
        parts[#parts + 1] = join_parts(months)
    end
    if #weekdays > 0 then
        parts[#parts + 1] = join_parts(weekdays)
    end
    if #days > 0 then
        parts[#parts + 1] = join_parts(days)
    end

    return table.concat(parts, ' / ')
end

local function describe_condition_item(item)
    if type(item) ~= 'table' then
        return ''
    end

    if item.type == 'weather_today' then
        local dest_type = common.trim(item.dest_type)
        local label = weather_labels[dest_type] or dest_type
        if label ~= '' then
            if item.reverse then
                return '非' .. label
            end
            return label
        end
        return '天气条件'
    end

    if item.type == 'entity_data_check_string' then
        local key = common.trim(item.key)
        local op = common.trim(item.op)
        local value = common.trim(item.value)
        if key == 'Schedule_Typhoon' and value == '1' then
            if item.reverse then
                return '非台风天气'
            end
            return '台风天气'
        end
        if key ~= '' and value ~= '' then
            local prefix = item.reverse and '不满足:' or ''
            if op ~= '' then
                return prefix .. key .. ' ' .. op .. ' ' .. value
            end
            return prefix .. key .. ' = ' .. value
        end
    end

    if item.type == 'npc_affection_star' then
        local value = common.trim(item.value)
        if value == '' then
            return item.reverse and '好感条件不满足' or '好感条件'
        end
        local op = common.trim(item.op)
        if op == 'GreatOrEqualThan' then
            return item.reverse and ('好感度未达到 ' .. value .. ' 心') or ('好感度达到 ' .. value .. ' 心')
        end
        if op == 'Equal' then
            return item.reverse and ('好感度不为 ' .. value .. ' 心') or ('好感度为 ' .. value .. ' 心')
        end
        local compare_label = compare_op_labels[op] or op
        if compare_label ~= '' then
            return item.reverse and ('好感条件不满足 ' .. compare_label .. ' ' .. value .. ' 心') or ('好感度 ' .. compare_label .. ' ' .. value .. ' 心')
        end
        return item.reverse and '好感条件不满足' or '好感条件'
    end

    if item.type == 'calendar_now_is_festival' then
        local festival_template = common.trim(item.festival_template)
        local label = festival_labels[festival_template] or festival_template
        if label ~= '' then
            if item.reverse then
                return '非' .. label
            end
            return label
        end
        return item.reverse and '非节庆期间' or '节庆期间'
    end

    if item.type == 'buildable_region_check_completed' then
        local template_name = common.trim(item.template)
        local label = buildable_labels[template_name] or template_name
        if label ~= '' then
            if item.reverse then
                return label .. '未完成'
            end
            return label .. '已完成'
        end
        return item.reverse and '建筑条件未完成' or '建筑条件已完成'
    end

    if item.type == 'check_has_owner' then
        if item.reverse then
            return '未被收养'
        end
        return '已被收养'
    end

    if item.type == 'time_check_date' then
        local parts = {}

        if item.check_month then
            local month_text = describe_compare_range(
                item.month_op,
                item.month_value,
                item.month_min_value,
                item.month_max_value,
                season_number_labels
            )
            if month_text ~= '' then
                parts[#parts + 1] = month_text
            else
                parts[#parts + 1] = '月份'
            end
        end

        if item.check_day then
            local day_text = describe_compare_range(
                item.day_op,
                item.day_value,
                item.day_min_value,
                item.day_max_value,
                nil,
                '日'
            )
            if day_text ~= '' then
                parts[#parts + 1] = day_text
            else
                parts[#parts + 1] = '日期'
            end
        end

        if item.check_weekday then
            local weekday_text = describe_compare_range(
                item.weekday_op,
                item.weekday_value,
                item.weekday_min_value,
                item.weekday_max_value,
                weekday_number_labels
            )
            if weekday_text ~= '' then
                parts[#parts + 1] = weekday_text
            else
                parts[#parts + 1] = '星期'
            end
        end

        if item.check_year then
            local year_text = ''
            if common.trim(item.year_op) == 'Equal' and common.trim(item.year_value) ~= '' then
                year_text = '第' .. common.trim(item.year_value) .. '年'
            else
                year_text = describe_compare_range(
                    item.year_op,
                    item.year_value,
                    item.year_min_value,
                    item.year_max_value
                )
                if year_text ~= '' then
                    year_text = '年份' .. year_text
                end
            end
            if year_text ~= '' then
                parts[#parts + 1] = year_text
            else
                parts[#parts + 1] = '年份'
            end
        end

        if #parts > 0 then
            local text = table.concat(parts, '')
            if item.reverse then
                return '非' .. text
            end
            return text
        end
        return item.reverse and '日期条件不满足' or '日期条件'
    end

    return '特殊条件'
end

local function build_condition_text(variant)
    local condition_desc = clean_condition_desc(variant.condition_desc)
    local summary = variant.condition_summary
    local parts = {}
    if type(summary) == 'table' then
        for _, item in ipairs(summary) do
            local text = describe_condition_item(item)
            if text ~= '' then
                parts[#parts + 1] = text
            end
        end
    end

    local structured_text = table.concat(unique_texts(parts), ';')
    if condition_desc == '' then
        return structured_text
    end
    if structured_text == '' then
        return condition_desc
    end
    if should_use_structured_condition(condition_desc) then
        return structured_text
    end
    return condition_desc
end

local function build_variant_heading(variant)
    local parts = {}
    local apply_text = build_apply_text(variant)
    local condition_text = build_condition_text(variant)

    if apply_text ~= '' then
        parts[#parts + 1] = apply_text
    end
    if condition_text ~= '' then
        parts[#parts + 1] = condition_text
    end

    if #parts == 0 then
        return '默认'
    end
    return table.concat(parts, ' | ')
end

local function localize_location(value)
    local text = common.trim(value)
    if text == '' then
        return ''
    end

    local direct = location_labels[text]
    if direct then
        return direct
    end

    local festival_base = text:match('^(.-)_BeachFestival$')
    if festival_base then
        local base = localize_location(festival_base)
        if base ~= '' and base ~= festival_base then
            return base .. '(海滩节)'
        end
    end

    local location, actor = text:match('^(.-)_([A-Za-z][A-Za-z0-9]+)$')
    if location and actor and location_labels[location] then
        return location_labels[location] .. '(' .. actor .. ')'
    end

    return text:gsub('_', ' / ')
end

local function format_candidate_text(candidate)
    if type(candidate) ~= 'table' then
        return ''
    end

    local display_name = common.trim(candidate.display_name)
    local location = localize_location(candidate.location_hint)
    local template_name = common.trim(candidate.template)

    if display_name ~= '' and location ~= '' then
        return display_name .. '(' .. location .. ')'
    end
    if display_name ~= '' then
        return display_name
    end
    if location ~= '' and template_name ~= '' then
        return template_name .. '(' .. location .. ')'
    end
    return template_name
end

local function candidate_activity_text(candidate)
    if type(candidate) ~= 'table' then
        return ''
    end
    local display_name = common.trim(candidate.display_name)
    if display_name ~= '' then
        return display_name
    end
    return common.trim(candidate.template)
end

local function candidate_location_text(candidate)
    if type(candidate) ~= 'table' then
        return ''
    end
    return localize_location(candidate.location_hint)
end

local function format_slot_time(slot)
    local begin_time = common.trim(slot.begin_time)
    local end_time = common.trim(slot.end_time)
    if begin_time ~= '' and end_time ~= '' then
        return begin_time .. ' - ' .. end_time
    end
    return begin_time ~= '' and begin_time or end_time
end

local function format_slot_activity(slot)
    if type(slot) ~= 'table' then
        return ''
    end

    if common.trim(slot.kind) == 'mission' then
        local mission_display = common.trim(slot.mission_display)
        if mission_display ~= '' then
            return mission_display
        end

        local mission_title = common.trim(slot.mission_title)
        if mission_title ~= '' then
            return mission_title
        end

        return '任务'
    end

    local ranked_candidates, top_candidates = get_top_candidates(slot.action_candidates)
    if #top_candidates > 1 then
        local texts = {}
        for _, candidate in ipairs(top_candidates) do
            texts[#texts + 1] = candidate_activity_text(candidate)
        end
        return table.concat(unique_texts(texts), ' / ')
    end

    local primary_action = slot.primary_action
    if type(primary_action) ~= 'table' then
        if #ranked_candidates > 0 then
            return candidate_activity_text(ranked_candidates[1])
        end
        return common.trim(slot.summary)
    end

    local display_name = common.trim(primary_action.display_name)
    if display_name ~= '' then
        return display_name
    end

    return common.trim(primary_action.template)
end

local function format_slot_location(slot)
    if type(slot) ~= 'table' then
        return ''
    end

    if common.trim(slot.kind) == 'mission' then
        return ''
    end

    local _, top_candidates = get_top_candidates(slot.action_candidates)
    if #top_candidates > 1 then
        local locations = {}
        for _, candidate in ipairs(top_candidates) do
            locations[#locations + 1] = candidate_location_text(candidate)
        end
        return table.concat(unique_texts(locations), ' / ')
    end

    local primary_action = slot.primary_action
    if type(primary_action) ~= 'table' then
        return ''
    end

    return localize_location(primary_action.location_hint)
end

local function format_slot_note(slot)
    if type(slot) ~= 'table' then
        return ''
    end

    if common.trim(slot.kind) == 'mission' then
        local notes = {}
        local mission_goal = common.trim(slot.mission_goal)
        local mission_title = common.trim(slot.mission_title)
        local mission_template = common.trim(slot.mission_template)

        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

        if mission_template ~= '' then
            notes[#notes + 1] = '模板:' .. mission_template
        end

        return table.concat(notes, ';')
    end

    local notes = {}
    local ranked_candidates, top_candidates = get_top_candidates(slot.action_candidates)
    if #top_candidates > 1 then
        local top_texts = {}
        for _, candidate in ipairs(top_candidates) do
            top_texts[#top_texts + 1] = format_candidate_text(candidate)
        end
        if #top_texts > 0 then
            notes[#notes + 1] = '并列候选:' .. table.concat(unique_texts(top_texts), ' / ')
        end
    elseif #ranked_candidates > 1 then
        local candidate_texts = {}
        for _, candidate in ipairs(ranked_candidates) do
            local text = format_candidate_text(candidate)
            if text ~= '' then
                candidate_texts[#candidate_texts + 1] = text
            end
        end
        if #candidate_texts > 0 then
            notes[#notes + 1] = '候选:' .. table.concat(candidate_texts, ' / ')
        end
    end

    local primary_action = slot.primary_action
    if type(primary_action) == 'table' then
        local template_name = common.trim(primary_action.template)
        local display_name = common.trim(primary_action.display_name)
        if template_name ~= '' and display_name == '' then
            notes[#notes + 1] = '模板:' .. template_name
        end
    end

    return table.concat(notes, ';')
end

local function render_variant_table(variant)
    local root = mw.html.create('table'):addClass('wikitable')
    local head = root:tag('tr')
    head:tag('th'):wikitext('时间')
    head:tag('th'):wikitext('行为')
    head:tag('th'):wikitext('地点')
    head:tag('th'):wikitext('备注')

    for _, slot in ipairs(variant.slots or {}) do
        local row = root:tag('tr')
        row:tag('td'):wikitext(format_slot_time(slot))
        row:tag('td'):wikitext(format_slot_activity(slot))
        row:tag('td'):wikitext(format_slot_location(slot))
        row:tag('td'):wikitext(format_slot_note(slot))
    end

    return tostring(root)
end

local function render_group(source, variants)
    local items = {}
    for _, variant in ipairs(variants) do
        if variant.source == source and has_ipairs_items(variant.slots) then
            items[#items + 1] = variant
        end
    end

    if #items == 0 then
        return ''
    end

    local out = {}
    out[#out + 1] = '=== ' .. (source_labels[source] or source) .. ' ==='
    for _, variant in ipairs(items) do
        out[#out + 1] = '==== ' .. build_variant_heading(variant) .. ' ===='
        out[#out + 1] = render_variant_table(variant)
    end

    return table.concat(out, '\n')
end

local function filter_display_variants(variants)
    local out = {}
    if type(variants) ~= 'table' then
        return out
    end

    for _, variant in ipairs(variants) do
        if type(variant) == 'table' and has_ipairs_items(variant.slots) then
            out[#out + 1] = variant
        end
    end

    return out
end

local function render_schedule_by_key(key)
    local record = find_record(key)
    if not record then
        return ''
    end

    local variants = filter_display_variants(merge_variants(record.variants))
    if #variants == 0 then
        return '暂无日程数据。'
    end

    local out = {}
    local summary_counts = {
        regular = 0,
        special = 0,
        living_together = 0,
    }
    for _, variant in ipairs(variants) do
        local source = common.trim(variant.source)
        if summary_counts[source] ~= nil then
            summary_counts[source] = summary_counts[source] + 1
        end
    end
    local summary = {}
    if summary_counts.regular > 0 then
        summary[#summary + 1] = '常规 ' .. tostring(summary_counts.regular)
    end
    if summary_counts.special > 0 then
        summary[#summary + 1] = '特殊 ' .. tostring(summary_counts.special)
    end
    if summary_counts.living_together > 0 then
        summary[#summary + 1] = '同居 ' .. tostring(summary_counts.living_together)
    end

    if #summary > 0 then
        out[#out + 1] = "''来源统计:" .. table.concat(summary, ',') .. "。同一条日程下拆开的星期规则已在展示层合并,多候选动作保留在备注列。''"
    end

    for _, source in ipairs({ 'regular', 'special', 'living_together' }) do
        local block = render_group(source, variants)
        if block ~= '' then
            out[#out + 1] = block
        end
    end

    return table.concat(out, '\n\n')
end

function p.getField(frame)
    local key = common.getArg(frame, 1, '')
    local field = common.getArg(frame, 2, '')
    if field == '' then
        return ''
    end

    local record = find_record(key)
    if not record then
        return ''
    end

    return common.toText(record[field])
end

function p.renderSchedule(frame)
    local key = common.getArg(frame, 1, '')
    return render_schedule_by_key(key)
end

return p