diff --git a/docs/changelog.txt b/docs/changelog.txt index 46b21bedb..609ad09cc 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -59,6 +59,9 @@ Template for new versions: - `buildingplan`: make the construction dimensions readout visible again ## Misc Improvements +- `sort`: sort by need for training on squad assignment screen +- `sort`: filter mothers with infants, units with weak mental fortitude, and critically injured units on the squad assignment screen +- `sort`: display a rating relative to the current sort order next to the visible units on the squad assignment screen ## Documentation diff --git a/docs/plugins/sort.rst b/docs/plugins/sort.rst index f142534db..72f8c372e 100644 --- a/docs/plugins/sort.rst +++ b/docs/plugins/sort.rst @@ -14,54 +14,75 @@ Searching and sorting functionality is provided by `overlay` widgets, and widget Squad assignment overlay ------------------------ -The squad assignment screen can be sorted by name, by migrant wave, by stress, -by various military-related skills or by long-term military potential. - -If sorted by "any melee", then the citizen is sorted according to the "melee -skill effectiveness". This rating uses the highest skill they have in axes, short -swords, maces, warhammers or spears along with physical and mental attributes and -general fighting skill. Citizens with higher rating are expected to be more -effective in melee combat with their corresponding weapon. - -If sorted by "any ranged", then the citizen is sorted according to the "ranged -skill effectiveness". This rating uses crossbow and general archery skills -along with mental and physical attributes. Citizens with higher rating are -expected to be more effective in ranged combat. - -If sorted by "leadership", then the citizen is sorted according to the highest -skill they have in leader, teacher, or military tactics. - -If sorting is done by "mental stability" citizens are arranged based on their -mental stability rating. This rating is a measure that takes into account -facets and values of an individual and correlates to better stress values. -It is designed to be higher for more stress-resistant citizens. - -If sorting is done by "melee potential" citizens are arranged based on -their "melee combat potential" rating. This rating is a statistical measure -that takes into account genetic predispositions in physical and mental -attributes, as well as body size. Dwarves (and other humanoid creatures) with -higher rating are expected to be more effective in melee combat if they train -their attributes to their genetic maximum. - -If sorting is done by "ranged potential" citizens are arranged based on their -ranged combat potential rating. This rating is a statistical measure that takes into -account genetic predispositions in physical and mental attributes. Dwarves -(and other humanoid creatures) with higher rating are expected to be more -effective in ranged combat if they train their attributes to the maximum. - -You can search for a dwarf by name by typing in the Search field. You can also -type in the name of any job skill (military-related or not) and dwarves with -any experience in that skill will be shown. For example, to only see citizens -with military tactics skill, type in "tactics". +You can search for a dwarf by name by typing in the Search field. The search +field is always focused, so any lowercase letter you type will appear there. + +The squad assignment screen can be sorted by name, by arrival order, by stress, +by various military-related skills, or by long-term military potential. + +If sorted by "melee effectiveness" (the default), then the citizens are sorted +according to how well they will perform in battle when using the weapon they +have the most skill in. The effectiveness rating also takes into account +physical and mental attributes as well as general fighting (non-weapon) skills. + +The "ranged effectiveness" sort order does a similar sort for expected +effectiveness with a crossbow. This sort also takes into account relevant +physical and mental attributes. + +The "effectiveness" sorts are the ones you should be using if you need the best +squad you can make right now. The numbers to the left of the unit list indicate +exactly how effective that dwarf is expected to be. Light green numbers +indicate the best of the best, while red numbers indicate dwarves that will not +be effective in the military in their current state (though see "melee +potential" and "ranged potential" sorts below for predictions about future +effectiveness). + +The "arrival order" sort shows the order that your dwarves appeared at your +fort. The numbers on the left indicate the relative arrival order, and the +numbers for the most recent migration wave will be colored bright green. +Dwarves that arrived earlier will have numbers in yellow, and your original +dwarves (if any still survive) will have numbers in red. + +The "stress" sort order will bring your most stressed dwarves to the top, ready +for addition to a :wiki:`therapy squad ` to +help improve their mood. + +Similarly, sorting by "need for training" will show you the dwarves that are +feeling the most unfocused because they are having their military training +needs unmet. + +Both "stress" and "need for training" sorts use the dwarf happiness indicators +to show how dire the dwarf's situation is and how much their mood might be +improved if you add them to an appropriate squad. + +If sorting is done by "melee potential", then citizens are arranged based on +genetic predispositions in physical and mental attributes, as well as body +size. Dwarves (and other humanoid creatures) with higher ratings are expected +to be more effective in melee combat if they train their attributes to their +genetic maximum. + +Similarly, the "ranged potential" sort orders citizens by genetic +predispositions in physical and mental attributes that are relevant to ranged +combat. Dwarves (and other humanoid creatures) with higher rating are expected +to be more effective in ranged combat if they train their attributes to the +maximum. + +The squad assignment panel also offers options for filtering which dwarves are +shown. Each filter option can by cycled through "Include", "Only", and +"Exclude" settings. "Include" does no filtering, "Only" shows only units that +match the filter, and "Exclude" shows only units that do *not* match the filter. + +The following filters are provided: + +- Units that are assigned to other squads +- Elected and appointed officials (e.g. mayor, priests, tavern keepers, etc.) +- Nobility (e.g. monarch, barons, counts, etc.) +- Mothers with infants (you may not want mothers using their babies as shields) +- Weak mental fortitude (units that have facets and values that indicate that they will react poorly to the stresses of battle) +- Critically injured (units that have lost their ability to grasp weapons or walk) "Melee skill effectiveness", "ranged skill effectiveness", "melee combat potential" and "ranged combat potential" are explained in detail here: https://www.reddit.com/r/dwarffortress/comments/163kczo/enhancing_military_candidate_selection_part_3/ "Mental stability" is explained here: https://www.reddit.com/r/dwarffortress/comments/1617s11/enhancing_military_candidate_selection_part_2/ - -You can see all the job skill names that you can search for by running:: - - :lua @df.job_skill - -in `gui/launcher`. diff --git a/plugins/lua/sort.lua b/plugins/lua/sort.lua index e775f1ddf..67d8c9e0f 100644 --- a/plugins/lua/sort.lua +++ b/plugins/lua/sort.lua @@ -9,31 +9,34 @@ local widgets = require('gui.widgets') local CH_UP = string.char(30) local CH_DN = string.char(31) -local MELEE_WEAPON_SKILLS = { - df.job_skill.AXE, - df.job_skill.SWORD, - df.job_skill.MACE, - df.job_skill.HAMMER, - df.job_skill.SPEAR, -} - -local RANGED_WEAPON_SKILLS = { - df.job_skill.CROSSBOW, -} +local function get_rating(val, baseline, range, highest, high, med, low) + val = val - (baseline or 0) + range = range or 100 + local percentile = (math.min(range, val) * 100) // range + if percentile < (low or 25) then return percentile, COLOR_RED end + if percentile < (med or 50) then return percentile, COLOR_LIGHTRED end + if percentile < (high or 75) then return percentile, COLOR_YELLOW end + if percentile < (highest or 90) then return percentile, COLOR_GREEN end + return percentile, COLOR_LIGHTGREEN +end local function sort_noop(a, b) -- this function is used as a marker and never actually gets called error('sort_noop should not be called') end +local function get_name(unit) + return unit and dfhack.toSearchNormalized(dfhack.TranslateName(dfhack.units.getVisibleName(unit))) +end + local function sort_by_name_desc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local name1 = dfhack.TranslateName(dfhack.units.getVisibleName(unit1)) - local name2 = dfhack.TranslateName(dfhack.units.getVisibleName(unit2)) + local name1 = get_name(unit1) + local name2 = get_name(unit2) return utils.compare_name(name1, name2) end @@ -43,8 +46,8 @@ local function sort_by_name_asc(unit_id_1, unit_id_2) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local name1 = dfhack.TranslateName(dfhack.units.getVisibleName(unit1)) - local name2 = dfhack.TranslateName(dfhack.units.getVisibleName(unit2)) + local name1 = get_name(unit1) + local name2 = get_name(unit2) return utils.compare_name(name2, name1) end @@ -61,7 +64,46 @@ local function get_active_idx_cache() return active_idx_cache end -local function sort_by_migrant_wave_desc(unit_id_1, unit_id_2) +local function is_original_dwarf(unit) + return df.global.plotinfo.fortress_age == unit.curse.time_on_site // 10 +end + +local WAVE_END_GAP = 10000 + +local function get_most_recent_wave_oldest_active_idx(cache) + local oldest_unit + for idx=#active_units-1,0,-1 do + local unit = active_units[idx] + if not dfhack.units.isCitizen(unit) then goto continue end + if oldest_unit and unit.curse.time_on_site - oldest_unit.curse.time_on_site > WAVE_END_GAP then + return cache[oldest_unit.id] + else + oldest_unit = unit + end + ::continue:: + end +end + +-- return green for most recent wave, red for the first wave, yellow for all others +-- rating is a three digit number that indicates the (potentially approximate) order +local function get_arrival_rating(unit) + local cache = get_active_idx_cache() + local unit_active_idx = cache[unit.id] + if not unit_active_idx then return end + local most_recent_wave_oldest_active_idx = get_most_recent_wave_oldest_active_idx(cache) + if not most_recent_wave_oldest_active_idx then return end + local num_active_units = #active_units + local rating = num_active_units < 1000 and unit_active_idx or ((unit_active_idx * 1000) // #active_units) + if most_recent_wave_oldest_active_idx < unit_active_idx then + return rating, COLOR_LIGHTGREEN + end + if is_original_dwarf(unit) then + return rating, COLOR_RED + end + return rating, COLOR_YELLOW +end + +local function sort_by_arrival_desc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local cache = get_active_idx_cache() if not cache[unit_id_1] then return -1 end @@ -69,7 +111,7 @@ local function sort_by_migrant_wave_desc(unit_id_1, unit_id_2) return utils.compare(cache[unit_id_2], cache[unit_id_1]) end -local function sort_by_migrant_wave_asc(unit_id_1, unit_id_2) +local function sort_by_arrival_asc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local cache = get_active_idx_cache() if not cache[unit_id_1] then return -1 end @@ -77,17 +119,29 @@ local function sort_by_migrant_wave_asc(unit_id_1, unit_id_2) return utils.compare(cache[unit_id_1], cache[unit_id_2]) end +local function get_stress(unit) + return unit and + unit.status.current_soul and + unit.status.current_soul.personality.stress +end + +local function get_stress_rating(unit) + return get_rating(dfhack.units.getStressCategory(unit), 0, 100, 4, 3, 2, 1) +end + local function sort_by_stress_desc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local happiness1 = unit1.status.current_soul.personality.stress - local happiness2 = unit2.status.current_soul.personality.stress + local happiness1 = get_stress(unit1) + local happiness2 = get_stress(unit2) if happiness1 == happiness2 then return sort_by_name_desc(unit_id_1, unit_id_2) end + if not happiness2 then return -1 end + if not happiness1 then return 1 end return utils.compare(happiness2, happiness1) end @@ -97,22 +151,37 @@ local function sort_by_stress_asc(unit_id_1, unit_id_2) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local happiness1 = unit1.status.current_soul.personality.stress - local happiness2 = unit2.status.current_soul.personality.stress + local happiness1 = get_stress(unit1) + local happiness2 = get_stress(unit2) if happiness1 == happiness2 then return sort_by_name_desc(unit_id_1, unit_id_2) end + if not happiness2 then return 1 end + if not happiness1 then return -1 end return utils.compare(happiness1, happiness2) end -local function get_skill(unit_id, skill, unit) - unit = unit or df.unit.find(unit_id) +local function get_skill(skill, unit) return unit and unit.status.current_soul and - utils.binsearch(unit.status.current_soul.skills, skill, 'id') + (utils.binsearch(unit.status.current_soul.skills, skill, 'id')) end -local function melee_skill_effectiveness(unit, skill_list) +local function get_skill_rating(skill, unit) + local uskill = get_skill(skill, unit) + if not uskill then return nil end + return get_rating(uskill.rating, 0, 100, 10, 5, 1, 0) +end + +local MELEE_WEAPON_SKILLS = { + df.job_skill.AXE, + df.job_skill.SWORD, + df.job_skill.MACE, + df.job_skill.HAMMER, + df.job_skill.SPEAR, +} + +local function melee_skill_effectiveness(unit) -- Physical attributes local strength = dfhack.units.getPhysicalAttrValue(unit, df.physical_attribute_type.STRENGTH) local agility = dfhack.units.getPhysicalAttrValue(unit, df.physical_attribute_type.AGILITY) @@ -128,7 +197,7 @@ local function melee_skill_effectiveness(unit, skill_list) -- Skills -- Finding the highest skill local skill_rating = 0 - for _, skill in ipairs(skill_list) do + for _, skill in ipairs(MELEE_WEAPON_SKILLS) do local melee_skill = dfhack.units.getNominalSkill(unit, skill, true) skill_rating = math.max(skill_rating, melee_skill) end @@ -140,36 +209,44 @@ local function melee_skill_effectiveness(unit, skill_list) return rating end -local function make_sort_by_melee_skill_effectiveness_desc(list) +local function get_melee_skill_effectiveness_rating(unit) + return get_rating(melee_skill_effectiveness(unit), 350000, 2350000, 78, 64, 49, 35) +end + +local function make_sort_by_melee_skill_effectiveness_desc() return function(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = melee_skill_effectiveness(unit1, list) - local rating2 = melee_skill_effectiveness(unit2, list) + local rating1 = melee_skill_effectiveness(unit1) + local rating2 = melee_skill_effectiveness(unit2) if rating1 == rating2 then return sort_by_name_desc(unit_id_1, unit_id_2) end return utils.compare(rating2, rating1) end end -local function make_sort_by_melee_skill_effectiveness_asc(list) +local function make_sort_by_melee_skill_effectiveness_asc() return function(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = melee_skill_effectiveness(unit1, list) - local rating2 = melee_skill_effectiveness(unit2, list) + local rating1 = melee_skill_effectiveness(unit1) + local rating2 = melee_skill_effectiveness(unit2) if rating1 == rating2 then return sort_by_name_desc(unit_id_1, unit_id_2) end return utils.compare(rating1, rating2) end end --- FUnction could easily be adapted to different weapon types. -local function ranged_skill_effectiveness(unit, skill_list) +local RANGED_WEAPON_SKILLS = { + df.job_skill.CROSSBOW, +} + +-- Function could easily be adapted to different weapon types. +local function ranged_skill_effectiveness(unit) -- Physical attributes local agility = dfhack.units.getPhysicalAttrValue(unit, df.physical_attribute_type.AGILITY) @@ -181,7 +258,7 @@ local function ranged_skill_effectiveness(unit, skill_list) -- Skills -- Finding the highest skill local skill_rating = 0 - for _, skill in ipairs(skill_list) do + for _, skill in ipairs(RANGED_WEAPON_SKILLS) do local ranged_skill = dfhack.units.getNominalSkill(unit, skill, true) skill_rating = math.max(skill_rating, ranged_skill) end @@ -192,6 +269,10 @@ local function ranged_skill_effectiveness(unit, skill_list) return rating end +local function get_ranged_skill_effectiveness_rating(unit) + return get_rating(ranged_skill_effectiveness(unit), 0, 500000, 90, 62, 44, 27) +end + local function make_sort_by_ranged_skill_effectiveness_desc(list) return function(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end @@ -225,8 +306,8 @@ local function make_sort_by_skill_desc(sort_skill) if unit_id_1 == unit_id_2 then return 0 end if unit_id_1 == -1 then return -1 end if unit_id_2 == -1 then return 1 end - local s1 = get_skill(unit_id_1, sort_skill) - local s2 = get_skill(unit_id_2, sort_skill) + local s1 = get_skill(sort_skill, df.unit.find(unit_id_1)) + local s2 = get_skill(sort_skill, df.unit.find(unit_id_2)) if s1 == s2 then return sort_by_name_desc(unit_id_1, unit_id_2) end if not s2 then return -1 end if not s1 then return 1 end @@ -245,8 +326,8 @@ local function make_sort_by_skill_asc(sort_skill) if unit_id_1 == unit_id_2 then return 0 end if unit_id_1 == -1 then return -1 end if unit_id_2 == -1 then return 1 end - local s1 = get_skill(unit_id_1, sort_skill) - local s2 = get_skill(unit_id_2, sort_skill) + local s1 = get_skill(sort_skill, df.unit.find(unit_id_1)) + local s2 = get_skill(sort_skill, df.unit.find(unit_id_2)) if s1 == s2 then return sort_by_name_desc(unit_id_1, unit_id_2) end if not s2 then return 1 end if not s1 then return -1 end @@ -261,7 +342,7 @@ local function make_sort_by_skill_asc(sort_skill) end -- Statistical rating that is higher for dwarves that are mentally stable -local function mental_stability(unit) +local function get_mental_stability(unit) local ALTRUISM = unit.status.current_soul.personality.traits.ALTRUISM local ANXIETY_PROPENSITY = unit.status.current_soul.personality.traits.ANXIETY_PROPENSITY local BRAVERY = unit.status.current_soul.personality.traits.BRAVERY @@ -307,8 +388,8 @@ local function sort_by_mental_stability_desc(unit_id_1, unit_id_2) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = mental_stability(unit1) - local rating2 = mental_stability(unit2) + local rating1 = get_mental_stability(unit1) + local rating2 = get_mental_stability(unit2) if rating1 == rating2 then -- sorting by stress is opposite -- more mental stable dwarves should have less stress @@ -323,8 +404,8 @@ local function sort_by_mental_stability_asc(unit_id_1, unit_id_2) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = mental_stability(unit1) - local rating2 = mental_stability(unit2) + local rating1 = get_mental_stability(unit1) + local rating2 = get_mental_stability(unit2) if rating1 == rating2 then return sort_by_stress_desc(unit_id_1, unit_id_2) end @@ -334,7 +415,7 @@ end -- Statistical rating that is higher for more potent dwarves in long run melee military training -- Rating considers fighting melee opponents -- Wounds are not considered! -local function melee_combat_potential(unit) +local function get_melee_combat_potential(unit) -- Physical attributes local strength = unit.body.physical_attrs.STRENGTH.max_value local agility = unit.body.physical_attrs.AGILITY.max_value @@ -353,14 +434,18 @@ local function melee_combat_potential(unit) return rating end +local function get_melee_combat_potential_rating(unit) + return get_rating(get_melee_combat_potential(unit), 300000, 2600000, 81, 64, 46, 29) +end + local function sort_by_melee_combat_potential_desc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = melee_combat_potential(unit1) - local rating2 = melee_combat_potential(unit2) + local rating1 = get_melee_combat_potential(unit1) + local rating2 = get_melee_combat_potential(unit2) if rating1 == rating2 then return sort_by_mental_stability_desc(unit_id_1, unit_id_2) end @@ -373,8 +458,8 @@ local function sort_by_melee_combat_potential_asc(unit_id_1, unit_id_2) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = melee_combat_potential(unit1) - local rating2 = melee_combat_potential(unit2) + local rating1 = get_melee_combat_potential(unit1) + local rating2 = get_melee_combat_potential(unit2) if rating1 == rating2 then return sort_by_mental_stability_asc(unit_id_1, unit_id_2) end @@ -383,7 +468,7 @@ end -- Statistical rating that is higher for more potent dwarves in long run ranged military training -- Wounds are not considered! -local function ranged_combat_potential(unit) +local function get_ranged_combat_potential(unit) -- Physical attributes local agility = unit.body.physical_attrs.AGILITY.max_value @@ -393,18 +478,22 @@ local function ranged_combat_potential(unit) local kinesthetic_sense = unit.status.current_soul.mental_attrs.KINESTHETIC_SENSE.max_value -- ranged combat potential formula - local rating = agility * 5 + kinesthetic_sense * 5 + spatial_sense * 2 + focus * 2 + local rating = agility * 5 + kinesthetic_sense * 2 + spatial_sense * 5 + focus * 2 return rating end +local function get_ranged_combat_potential_rating(unit) + return get_rating(get_ranged_combat_potential(unit), 0, 70000, 73, 57, 41, 25) +end + local function sort_by_ranged_combat_potential_desc(unit_id_1, unit_id_2) if unit_id_1 == unit_id_2 then return 0 end local unit1 = df.unit.find(unit_id_1) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = ranged_combat_potential(unit1) - local rating2 = ranged_combat_potential(unit2) + local rating1 = get_ranged_combat_potential(unit1) + local rating2 = get_ranged_combat_potential(unit2) if rating1 == rating2 then return sort_by_mental_stability_desc(unit_id_1, unit_id_2) end @@ -417,47 +506,128 @@ local function sort_by_ranged_combat_potential_asc(unit_id_1, unit_id_2) local unit2 = df.unit.find(unit_id_2) if not unit1 then return -1 end if not unit2 then return 1 end - local rating1 = ranged_combat_potential(unit1) - local rating2 = ranged_combat_potential(unit2) + local rating1 = get_ranged_combat_potential(unit1) + local rating2 = get_ranged_combat_potential(unit2) if rating1 == rating2 then return sort_by_mental_stability_asc(unit_id_1, unit_id_2) end return utils.compare(rating1, rating2) end -local SORT_FNS = { - sort_by_any_melee_desc=make_sort_by_melee_skill_effectiveness_desc(MELEE_WEAPON_SKILLS), - sort_by_any_melee_asc=make_sort_by_melee_skill_effectiveness_asc(MELEE_WEAPON_SKILLS), - sort_by_any_ranged_desc=make_sort_by_ranged_skill_effectiveness_desc(RANGED_WEAPON_SKILLS), - sort_by_any_ranged_asc=make_sort_by_ranged_skill_effectiveness_asc(RANGED_WEAPON_SKILLS), - sort_by_teacher_desc=make_sort_by_skill_desc(df.job_skill.TEACHING), - sort_by_teacher_asc=make_sort_by_skill_asc(df.job_skill.TEACHING), - sort_by_tactics_desc=make_sort_by_skill_desc(df.job_skill.MILITARY_TACTICS), - sort_by_tactics_asc=make_sort_by_skill_asc(df.job_skill.MILITARY_TACTICS), - sort_by_axe_desc=make_sort_by_skill_desc(df.job_skill.AXE), - sort_by_axe_asc=make_sort_by_skill_asc(df.job_skill.AXE), - sort_by_sword_desc=make_sort_by_skill_desc(df.job_skill.SWORD), - sort_by_sword_asc=make_sort_by_skill_asc(df.job_skill.SWORD), - sort_by_mace_desc=make_sort_by_skill_desc(df.job_skill.MACE), - sort_by_mace_asc=make_sort_by_skill_asc(df.job_skill.MACE), - sort_by_hammer_desc=make_sort_by_skill_desc(df.job_skill.HAMMER), - sort_by_hammer_asc=make_sort_by_skill_asc(df.job_skill.HAMMER), - sort_by_spear_desc=make_sort_by_skill_desc(df.job_skill.SPEAR), - sort_by_spear_asc=make_sort_by_skill_asc(df.job_skill.SPEAR), - sort_by_crossbow_desc=make_sort_by_skill_desc(df.job_skill.CROSSBOW), - sort_by_crossbow_asc=make_sort_by_skill_asc(df.job_skill.CROSSBOW), +local function get_need(unit) + if not unit or not unit.status.current_soul then return end + for _, need in ipairs(unit.status.current_soul.personality.needs) do + if need.id == df.need_type.MartialTraining and need.focus_level < 0 then + return -need.focus_level + end + end +end + +local function get_need_rating(unit) + local focus_level = get_need(unit) + if not focus_level then return end + -- convert to stress ratings so we can use stress faces as labels + if focus_level > 100000 then return 0 end + if focus_level > 10000 then return 1 end + if focus_level > 1000 then return 2 end + if focus_level > 100 then return 3 end + return 6 +end + +local function sort_by_need_desc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = get_need(unit1) + local rating2 = get_need(unit2) + if rating1 == rating2 then + return sort_by_stress_desc(unit_id_1, unit_id_2) + end + if not rating2 then return -1 end + if not rating1 then return 1 end + return utils.compare(rating2, rating1) +end + +local function sort_by_need_asc(unit_id_1, unit_id_2) + if unit_id_1 == unit_id_2 then return 0 end + local unit1 = df.unit.find(unit_id_1) + local unit2 = df.unit.find(unit_id_2) + if not unit1 then return -1 end + if not unit2 then return 1 end + local rating1 = get_need(unit1) + local rating2 = get_need(unit2) + if rating1 == rating2 then + return sort_by_stress_asc(unit_id_1, unit_id_2) + end + if not rating2 then return 1 end + if not rating1 then return -1 end + return utils.compare(rating1, rating2) +end + +local sort_by_any_melee_desc=make_sort_by_melee_skill_effectiveness_desc() +local sort_by_any_melee_asc=make_sort_by_melee_skill_effectiveness_asc() +local sort_by_any_ranged_desc=make_sort_by_ranged_skill_effectiveness_desc() +local sort_by_any_ranged_asc=make_sort_by_ranged_skill_effectiveness_asc() +local sort_by_teacher_desc=make_sort_by_skill_desc(df.job_skill.TEACHING) +local sort_by_teacher_asc=make_sort_by_skill_asc(df.job_skill.TEACHING) +local sort_by_tactics_desc=make_sort_by_skill_desc(df.job_skill.MILITARY_TACTICS) +local sort_by_tactics_asc=make_sort_by_skill_asc(df.job_skill.MILITARY_TACTICS) +local sort_by_axe_desc=make_sort_by_skill_desc(df.job_skill.AXE) +local sort_by_axe_asc=make_sort_by_skill_asc(df.job_skill.AXE) +local sort_by_sword_desc=make_sort_by_skill_desc(df.job_skill.SWORD) +local sort_by_sword_asc=make_sort_by_skill_asc(df.job_skill.SWORD) +local sort_by_mace_desc=make_sort_by_skill_desc(df.job_skill.MACE) +local sort_by_mace_asc=make_sort_by_skill_asc(df.job_skill.MACE) +local sort_by_hammer_desc=make_sort_by_skill_desc(df.job_skill.HAMMER) +local sort_by_hammer_asc=make_sort_by_skill_asc(df.job_skill.HAMMER) +local sort_by_spear_desc=make_sort_by_skill_desc(df.job_skill.SPEAR) +local sort_by_spear_asc=make_sort_by_skill_asc(df.job_skill.SPEAR) +local sort_by_crossbow_desc=make_sort_by_skill_desc(df.job_skill.CROSSBOW) +local sort_by_crossbow_asc=make_sort_by_skill_asc(df.job_skill.CROSSBOW) + +local SORT_LIBRARY = { + {label='melee effectiveness', desc_fn=sort_by_any_melee_desc, asc_fn=sort_by_any_melee_asc, rating_fn=get_melee_skill_effectiveness_rating}, + {label='ranged effectiveness', desc_fn=sort_by_any_ranged_desc, asc_fn=sort_by_any_ranged_asc, rating_fn=get_ranged_skill_effectiveness_rating}, + {label='name', desc_fn=sort_by_name_desc, asc_fn=sort_by_name_asc}, + {label='teacher skill', desc_fn=sort_by_teacher_desc, asc_fn=sort_by_teacher_asc, rating_fn=curry(get_skill_rating, df.job_skill.TEACHING)}, + {label='tactics skill', desc_fn=sort_by_tactics_desc, asc_fn=sort_by_tactics_asc, rating_fn=curry(get_skill_rating, df.job_skill.MILITARY_TACTICS)}, + {label='arrival order', desc_fn=sort_by_arrival_desc, asc_fn=sort_by_arrival_asc, rating_fn=get_arrival_rating}, + {label='stress level', desc_fn=sort_by_stress_desc, asc_fn=sort_by_stress_asc, rating_fn=get_stress_rating, use_stress_faces=true}, + {label='need for training', desc_fn=sort_by_need_desc, asc_fn=sort_by_need_asc, rating_fn=get_need_rating, use_stress_faces=true}, + {label='axe skill', desc_fn=sort_by_axe_desc, asc_fn=sort_by_axe_asc, rating_fn=curry(get_skill_rating, df.job_skill.AXE)}, + {label='sword skill', desc_fn=sort_by_sword_desc, asc_fn=sort_by_sword_asc, rating_fn=curry(get_skill_rating, df.job_skill.SWORD)}, + {label='mace skill', desc_fn=sort_by_mace_desc, asc_fn=sort_by_mace_asc, rating_fn=curry(get_skill_rating, df.job_skill.MACE)}, + {label='hammer skill', desc_fn=sort_by_hammer_desc, asc_fn=sort_by_hammer_asc, rating_fn=curry(get_skill_rating, df.job_skill.HAMMER)}, + {label='spear skill', desc_fn=sort_by_spear_desc, asc_fn=sort_by_spear_asc, rating_fn=curry(get_skill_rating, df.job_skill.SPEAR)}, + {label='crossbow skill', desc_fn=sort_by_crossbow_desc, asc_fn=sort_by_crossbow_asc, rating_fn=curry(get_skill_rating, df.job_skill.CROSSBOW)}, + {label='melee potential', desc_fn=sort_by_melee_combat_potential_desc, asc_fn=sort_by_melee_combat_potential_asc, rating_fn=get_melee_combat_potential_rating}, + {label='ranged potential', desc_fn=sort_by_ranged_combat_potential_desc, asc_fn=sort_by_ranged_combat_potential_asc, rating_fn=get_ranged_combat_potential_rating}, } +local RATING_FNS = {} +local STRESS_FACE_FNS = {} +for _, opt in ipairs(SORT_LIBRARY) do + RATING_FNS[opt.desc_fn] = opt.rating_fn + RATING_FNS[opt.asc_fn] = opt.rating_fn + if opt.use_stress_faces then + STRESS_FACE_FNS[opt.desc_fn] = true + STRESS_FACE_FNS[opt.asc_fn] = true + end +end + -- ---------------------- -- SquadAssignmentOverlay -- SquadAssignmentOverlay = defclass(SquadAssignmentOverlay, overlay.OverlayWidget) SquadAssignmentOverlay.ATTRS{ - default_pos={x=23, y=5}, + default_pos={x=18, y=5}, default_enabled=true, viewscreens='dwarfmode/UnitSelector/SQUAD_FILL_POSITION', - frame={w=38, h=25}, + version='2', + frame={w=38, h=31}, frame_style=gui.FRAME_PANEL, frame_background=gui.CLEAR_PEN, autoarrange_subviews=true, @@ -474,6 +644,20 @@ end function SquadAssignmentOverlay:init() self.dirty = true + local sort_options = {} + for _, opt in ipairs(SORT_LIBRARY) do + table.insert(sort_options, { + label=opt.label..CH_DN, + value=opt.desc_fn, + pen=COLOR_GREEN, + }) + table.insert(sort_options, { + label=opt.label..CH_UP, + value=opt.asc_fn, + pen=COLOR_YELLOW, + }) + end + self:addviews{ widgets.EditField{ view_id='search', @@ -491,41 +675,8 @@ function SquadAssignmentOverlay:init() frame={t=0, l=0}, label='Sort by:', key='CUSTOM_SHIFT_S', - options={ - {label='melee effectiveness'..CH_DN, value=SORT_FNS.sort_by_any_melee_desc, pen=COLOR_GREEN}, - {label='melee effectiveness'..CH_UP, value=SORT_FNS.sort_by_any_melee_asc, pen=COLOR_YELLOW}, - {label='ranged effectiveness'..CH_DN, value=SORT_FNS.sort_by_any_ranged_desc, pen=COLOR_GREEN}, - {label='ranged effectiveness'..CH_UP, value=SORT_FNS.sort_by_any_ranged_asc, pen=COLOR_YELLOW}, - {label='name'..CH_DN, value=sort_by_name_desc, pen=COLOR_GREEN}, - {label='name'..CH_UP, value=sort_by_name_asc, pen=COLOR_YELLOW}, - {label='teacher skill'..CH_DN, value=SORT_FNS.sort_by_teacher_desc, pen=COLOR_GREEN}, - {label='teacher skill'..CH_UP, value=SORT_FNS.sort_by_teacher_asc, pen=COLOR_YELLOW}, - {label='tactics skill'..CH_DN, value=SORT_FNS.sort_by_tactics_desc, pen=COLOR_GREEN}, - {label='tactics skill'..CH_UP, value=SORT_FNS.sort_by_tactics_asc, pen=COLOR_YELLOW}, - {label='migrant wave'..CH_DN, value=sort_by_migrant_wave_desc, pen=COLOR_GREEN}, - {label='migrant wave'..CH_UP, value=sort_by_migrant_wave_asc, pen=COLOR_YELLOW}, - {label='stress level'..CH_DN, value=sort_by_stress_desc, pen=COLOR_GREEN}, - {label='stress level'..CH_UP, value=sort_by_stress_asc, pen=COLOR_YELLOW}, - {label='mental stability'..CH_DN, value=sort_by_mental_stability_desc, pen=COLOR_GREEN}, - {label='mental stability'..CH_UP, value=sort_by_mental_stability_asc, pen=COLOR_YELLOW}, - {label='axe skill'..CH_DN, value=SORT_FNS.sort_by_axe_desc, pen=COLOR_GREEN}, - {label='axe skill'..CH_UP, value=SORT_FNS.sort_by_axe_asc, pen=COLOR_YELLOW}, - {label='sword skill'..CH_DN, value=SORT_FNS.sort_by_sword_desc, pen=COLOR_GREEN}, - {label='sword skill'..CH_UP, value=SORT_FNS.sort_by_sword_asc, pen=COLOR_YELLOW}, - {label='mace skill'..CH_DN, value=SORT_FNS.sort_by_mace_desc, pen=COLOR_GREEN}, - {label='mace skill'..CH_UP, value=SORT_FNS.sort_by_mace_asc, pen=COLOR_YELLOW}, - {label='hammer skill'..CH_DN, value=SORT_FNS.sort_by_hammer_desc, pen=COLOR_GREEN}, - {label='hammer skill'..CH_UP, value=SORT_FNS.sort_by_hammer_asc, pen=COLOR_YELLOW}, - {label='spear skill'..CH_DN, value=SORT_FNS.sort_by_spear_desc, pen=COLOR_GREEN}, - {label='spear skill'..CH_UP, value=SORT_FNS.sort_by_spear_asc, pen=COLOR_YELLOW}, - {label='crossbow skill'..CH_DN, value=SORT_FNS.sort_by_crossbow_desc, pen=COLOR_GREEN}, - {label='crossbow skill'..CH_UP, value=SORT_FNS.sort_by_crossbow_asc, pen=COLOR_YELLOW}, - {label='melee potential'..CH_DN, value=sort_by_melee_combat_potential_desc, pen=COLOR_GREEN}, - {label='melee potential'..CH_UP, value=sort_by_melee_combat_potential_asc, pen=COLOR_YELLOW}, - {label='ranged potential'..CH_DN, value=sort_by_ranged_combat_potential_desc, pen=COLOR_GREEN}, - {label='ranged potential'..CH_UP, value=sort_by_ranged_combat_potential_asc, pen=COLOR_YELLOW}, - }, - initial_option=SORT_FNS.sort_by_any_melee_desc, + options=sort_options, + initial_option=sort_by_any_melee_desc, on_change=self:callback('refresh_list', 'sort'), }, widgets.CycleHotkeyLabel{ @@ -533,10 +684,10 @@ function SquadAssignmentOverlay:init() frame={t=2, l=0, w=11}, options={ {label='melee eff.', value=sort_noop}, - {label='melee eff.'..CH_DN, value=SORT_FNS.sort_by_any_melee_desc, pen=COLOR_GREEN}, - {label='melee eff.'..CH_UP, value=SORT_FNS.sort_by_any_melee_asc, pen=COLOR_YELLOW}, + {label='melee eff.'..CH_DN, value=sort_by_any_melee_desc, pen=COLOR_GREEN}, + {label='melee eff.'..CH_UP, value=sort_by_any_melee_asc, pen=COLOR_YELLOW}, }, - initial_option=SORT_FNS.sort_by_any_melee_desc, + initial_option=sort_by_any_melee_desc, option_gap=0, on_change=self:callback('refresh_list', 'sort_any_melee'), }, @@ -545,8 +696,8 @@ function SquadAssignmentOverlay:init() frame={t=2, r=8, w=12}, options={ {label='ranged eff.', value=sort_noop}, - {label='ranged eff.'..CH_DN, value=SORT_FNS.sort_by_any_ranged_desc, pen=COLOR_GREEN}, - {label='ranged eff.'..CH_UP, value=SORT_FNS.sort_by_any_ranged_asc, pen=COLOR_YELLOW}, + {label='ranged eff.'..CH_DN, value=sort_by_any_ranged_desc, pen=COLOR_GREEN}, + {label='ranged eff.'..CH_UP, value=sort_by_any_ranged_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_any_ranged'), @@ -567,8 +718,8 @@ function SquadAssignmentOverlay:init() frame={t=4, l=0, w=8}, options={ {label='teacher', value=sort_noop}, - {label='teacher'..CH_DN, value=SORT_FNS.sort_by_teacher_desc, pen=COLOR_GREEN}, - {label='teacher'..CH_UP, value=SORT_FNS.sort_by_teacher_asc, pen=COLOR_YELLOW}, + {label='teacher'..CH_DN, value=sort_by_teacher_desc, pen=COLOR_GREEN}, + {label='teacher'..CH_UP, value=sort_by_teacher_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_teacher'), @@ -578,22 +729,22 @@ function SquadAssignmentOverlay:init() frame={t=4, l=10, w=8}, options={ {label='tactics', value=sort_noop}, - {label='tactics'..CH_DN, value=SORT_FNS.sort_by_tactics_desc, pen=COLOR_GREEN}, - {label='tactics'..CH_UP, value=SORT_FNS.sort_by_tactics_asc, pen=COLOR_YELLOW}, + {label='tactics'..CH_DN, value=sort_by_tactics_desc, pen=COLOR_GREEN}, + {label='tactics'..CH_UP, value=sort_by_tactics_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_tactics'), }, widgets.CycleHotkeyLabel{ - view_id='sort_migrant_wave', - frame={t=4, r=0, w=13}, + view_id='sort_arrival', + frame={t=4, r=0, w=14}, options={ - {label='migrant wave', value=sort_noop}, - {label='migrant wave'..CH_DN, value=sort_by_migrant_wave_desc, pen=COLOR_GREEN}, - {label='migrant wave'..CH_UP, value=sort_by_migrant_wave_asc, pen=COLOR_YELLOW}, + {label='arrival order', value=sort_noop}, + {label='arrival order'..CH_DN, value=sort_by_arrival_desc, pen=COLOR_GREEN}, + {label='arrival order'..CH_UP, value=sort_by_arrival_asc, pen=COLOR_YELLOW}, }, option_gap=0, - on_change=self:callback('refresh_list', 'sort_migrant_wave'), + on_change=self:callback('refresh_list', 'sort_arrival'), }, widgets.CycleHotkeyLabel{ view_id='sort_stress', @@ -607,23 +758,23 @@ function SquadAssignmentOverlay:init() on_change=self:callback('refresh_list', 'sort_stress'), }, widgets.CycleHotkeyLabel{ - view_id='sort_mental_stability', - frame={t=6, r=0, w=17}, + view_id='sort_need', + frame={t=6, r=0, w=18}, options={ - {label='mental stability', value=sort_noop}, - {label='mental stability'..CH_DN, value=sort_by_mental_stability_desc, pen=COLOR_GREEN}, - {label='mental stability'..CH_UP, value=sort_by_mental_stability_asc, pen=COLOR_YELLOW}, + {label='need for training', value=sort_noop}, + {label='need for training'..CH_DN, value=sort_by_need_desc, pen=COLOR_GREEN}, + {label='need for training'..CH_UP, value=sort_by_need_asc, pen=COLOR_YELLOW}, }, option_gap=0, - on_change=self:callback('refresh_list', 'sort_mental_stability'), + on_change=self:callback('refresh_list', 'sort_need'), }, widgets.CycleHotkeyLabel{ view_id='sort_axe', frame={t=8, l=0, w=4}, options={ {label='axe', value=sort_noop}, - {label='axe'..CH_DN, value=SORT_FNS.sort_by_axe_desc, pen=COLOR_GREEN}, - {label='axe'..CH_UP, value=SORT_FNS.sort_by_axe_asc, pen=COLOR_YELLOW}, + {label='axe'..CH_DN, value=sort_by_axe_desc, pen=COLOR_GREEN}, + {label='axe'..CH_UP, value=sort_by_axe_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_axe'), @@ -633,8 +784,8 @@ function SquadAssignmentOverlay:init() frame={t=8, w=6}, options={ {label='sword', value=sort_noop}, - {label='sword'..CH_DN, value=SORT_FNS.sort_by_sword_desc, pen=COLOR_GREEN}, - {label='sword'..CH_UP, value=SORT_FNS.sort_by_sword_asc, pen=COLOR_YELLOW}, + {label='sword'..CH_DN, value=sort_by_sword_desc, pen=COLOR_GREEN}, + {label='sword'..CH_UP, value=sort_by_sword_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_sword'), @@ -644,8 +795,8 @@ function SquadAssignmentOverlay:init() frame={t=8, r=0, w=5}, options={ {label='mace', value=sort_noop}, - {label='mace'..CH_DN, value=SORT_FNS.sort_by_mace_desc, pen=COLOR_GREEN}, - {label='mace'..CH_UP, value=SORT_FNS.sort_by_mace_asc, pen=COLOR_YELLOW}, + {label='mace'..CH_DN, value=sort_by_mace_desc, pen=COLOR_GREEN}, + {label='mace'..CH_UP, value=sort_by_mace_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_mace'), @@ -655,8 +806,8 @@ function SquadAssignmentOverlay:init() frame={t=10, l=0, w=7}, options={ {label='hammer', value=sort_noop}, - {label='hammer'..CH_DN, value=SORT_FNS.sort_by_hammer_desc, pen=COLOR_GREEN}, - {label='hammer'..CH_UP, value=SORT_FNS.sort_by_hammer_asc, pen=COLOR_YELLOW}, + {label='hammer'..CH_DN, value=sort_by_hammer_desc, pen=COLOR_GREEN}, + {label='hammer'..CH_UP, value=sort_by_hammer_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_hammer'), @@ -666,8 +817,8 @@ function SquadAssignmentOverlay:init() frame={t=10, w=6}, options={ {label='spear', value=sort_noop}, - {label='spear'..CH_DN, value=SORT_FNS.sort_by_spear_desc, pen=COLOR_GREEN}, - {label='spear'..CH_UP, value=SORT_FNS.sort_by_spear_asc, pen=COLOR_YELLOW}, + {label='spear'..CH_DN, value=sort_by_spear_desc, pen=COLOR_GREEN}, + {label='spear'..CH_UP, value=sort_by_spear_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_spear'), @@ -677,8 +828,8 @@ function SquadAssignmentOverlay:init() frame={t=10, r=0, w=9}, options={ {label='crossbow', value=sort_noop}, - {label='crossbow'..CH_DN, value=SORT_FNS.sort_by_crossbow_desc, pen=COLOR_GREEN}, - {label='crossbow'..CH_UP, value=SORT_FNS.sort_by_crossbow_asc, pen=COLOR_YELLOW}, + {label='crossbow'..CH_DN, value=sort_by_crossbow_desc, pen=COLOR_GREEN}, + {label='crossbow'..CH_UP, value=sort_by_crossbow_asc, pen=COLOR_YELLOW}, }, option_gap=0, on_change=self:callback('refresh_list', 'sort_crossbow'), @@ -746,6 +897,45 @@ function SquadAssignmentOverlay:init() initial_option='include', on_change=function() self:refresh_list() end, }, + widgets.CycleHotkeyLabel{ + view_id='infant', + frame={l=0}, + key='CUSTOM_SHIFT_M', + label='Mothers with infants:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, + widgets.CycleHotkeyLabel{ + view_id='unstable', + frame={l=0}, + key='CUSTOM_SHIFT_F', + label='Weak mental fortitude:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, + widgets.CycleHotkeyLabel{ + view_id='maimed', + frame={l=0}, + key='CUSTOM_SHIFT_I', + label='Critically injured:', + options={ + {label='Include', value='include', pen=COLOR_GREEN}, + {label='Only', value='only', pen=COLOR_YELLOW}, + {label='Exclude', value='exclude', pen=COLOR_RED}, + }, + initial_option='include', + on_change=function() self:refresh_list() end, + }, } end @@ -784,6 +974,28 @@ local function is_nobility(unit) return false end +local function has_infant(unit) + for _, baby in ipairs(df.global.world.units.other.ANY_BABY2) do + if baby.relationship_ids.Mother == unit.id then + return true + end + end + return false +end + +local function is_unstable(unit) + -- stddev percentiles are 61, 48, 35, 23 + -- let's go with one stddev below the mean (35) as the cutoff + local _, color = get_rating(get_mental_stability(unit), -40, 80, 35, 0, 0, 0) + return color ~= COLOR_LIGHTGREEN +end + +local function is_maimed(unit) + return unit.flags2.vision_missing or + unit.status2.limbs_grasp_count < 2 or + unit.status2.limbs_stand_count == 0 +end + local function filter_matches(unit_id, filter) if unit_id == -1 then return true end local unit = df.unit.find(unit_id) @@ -794,6 +1006,12 @@ local function filter_matches(unit_id, filter) if filter.officials == 'exclude' and is_elected_or_appointed_official(unit) then return false end if filter.nobles == 'only' and not is_nobility(unit) then return false end if filter.nobles == 'exclude' and is_nobility(unit) then return false end + if filter.infant == 'only' and not has_infant(unit) then return false end + if filter.infant == 'exclude' and has_infant(unit) then return false end + if filter.unstable == 'only' and not is_unstable(unit) then return false end + if filter.unstable == 'exclude' and is_unstable(unit) then return false end + if filter.maimed == 'only' and not is_maimed(unit) then return false end + if filter.maimed == 'exclude' and is_maimed(unit) then return false end if #filter.search == 0 then return true end local search_key = dfhack.TranslateName(dfhack.units.getVisibleName(unit)) return normalize_search_key(search_key):find(dfhack.toSearchNormalized(filter.search)) @@ -803,14 +1021,20 @@ local function is_noop_filter(filter) return #filter.search == 0 and filter.military == 'include' and filter.officials == 'include' and - filter.nobles == 'include' + filter.nobles == 'include' and + filter.infant == 'include' and + filter.unstable == 'include' and + filter.maimed == 'include' end local function is_filter_equal(a, b) return a.search == b.search and a.military == b.military and a.officials == b.officials and - a.nobles == b.nobles + a.nobles == b.nobles and + a.infant == b.infant and + a.unstable == b.unstable and + a.maimed == b.maimed end local unit_selector = df.global.game.main_interface.unit_selector @@ -848,6 +1072,31 @@ local function filter_vector(filter, prev_filter) unit_selector.unid:erase(idx) end end + -- fix up scroll position if it would be off the end of the list + if unit_selector.scroll_position + 10 > #unit_selector.unid then + unit_selector.scroll_position = math.max(0, #unit_selector.unid - 10) + end +end + +local use_stress_faces = false +local rating_annotations = {} + +local function annotate_visible_units(sort_fn) + use_stress_faces = STRESS_FACE_FNS[sort_fn] + rating_annotations = {} + rating_fn = RATING_FNS[sort_fn] + local max_idx = math.min(#unit_selector.unid-1, unit_selector.scroll_position+9) + for idx = unit_selector.scroll_position, max_idx do + local annotation_idx = idx - unit_selector.scroll_position + 1 + local unit = df.unit.find(unit_selector.unid[idx]) + rating_annotations[annotation_idx] = nil + if unit and rating_fn then + local val, color = rating_fn(unit) + if val then + rating_annotations[annotation_idx] = {val=val, color=color} + end + end + end end local SORT_WIDGET_NAMES = { @@ -857,9 +1106,9 @@ local SORT_WIDGET_NAMES = { 'sort_name', 'sort_teacher', 'sort_tactics', - 'sort_migrant_wave', + 'sort_arrival', 'sort_stress', - 'sort_mental_stability', + 'sort_need', 'sort_axe', 'sort_sword', 'sort_mace', @@ -885,10 +1134,15 @@ function SquadAssignmentOverlay:refresh_list(sort_widget, sort_fn) military=self.subviews.military:getOptionValue(), officials=self.subviews.officials:getOptionValue(), nobles=self.subviews.nobles:getOptionValue(), + infant=self.subviews.infant:getOptionValue(), + unstable=self.subviews.unstable:getOptionValue(), + maimed=self.subviews.maimed:getOptionValue(), } filter_vector(filter, self.prev_filter or {}) self.prev_filter = filter utils.sort_vector(unit_selector.unid, nil, sort_fn) + annotate_visible_units(sort_fn) + self.saved_scroll_position = unit_selector.scroll_position end function SquadAssignmentOverlay:onInput(keys) @@ -906,11 +1160,106 @@ function SquadAssignmentOverlay:onRenderFrame(dc, frame_rect) if self.dirty then self:refresh_list() self.dirty = false + elseif self.saved_scroll_position ~= unit_selector.scroll_position then + annotate_visible_units(self.subviews.sort:getOptionValue()) + self.saved_scroll_position = unit_selector.scroll_position + end +end + +-- ---------------------- +-- SquadAnnotationOverlay +-- + +SquadAnnotationOverlay = defclass(SquadAnnotationOverlay, overlay.OverlayWidget) +SquadAnnotationOverlay.ATTRS{ + default_pos={x=56, y=5}, + default_enabled=true, + viewscreens='dwarfmode/UnitSelector/SQUAD_FILL_POSITION', + frame={w=5, h=35}, + frame_style=gui.FRAME_INTERIOR_MEDIUM, + frame_background=gui.CLEAR_PEN, +} + +function get_annotation_text(idx) + local elem = rating_annotations[idx] + if not elem or not tonumber(elem.val) then return ' - ' end + + return tostring(math.tointeger(elem.val)) +end + +function get_annotation_color(idx) + local elem = rating_annotations[idx] + return elem and elem.color or nil +end + +local to_pen = dfhack.pen.parse +local DASH_PEN = to_pen{ch='-', fg=COLOR_WHITE, keep_lower=true} + +local FACE_TILES = {} +for idx=0,6 do + FACE_TILES[idx] = {} + local face_off = (6 - idx) * 2 + for y=0,1 do + for x=0,1 do + local tile = dfhack.screen.findGraphicsTile('INTERFACE_BITS', 32 + face_off + x, 6 + y) + ensure_key(FACE_TILES[idx], y)[x] = tile + end + end +end + +local ASCII_FACE_TILES = {} +for idx,color in ipairs{COLOR_RED, COLOR_LIGHTRED, COLOR_YELLOW, COLOR_WHITE, COLOR_GREEN, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN} do + local face = {} + ensure_key(face, 0)[0] = to_pen{ch=1, fg=color} + ensure_key(face, 0)[1] = to_pen{ch='\\', fg=color} + ensure_key(face, 1)[0] = to_pen{ch='\\', fg=color} + ensure_key(face, 1)[1] = to_pen{ch='/', fg=color} + ASCII_FACE_TILES[idx-1] = face +end + +function get_stress_face_tile(idx, x, y) + local elem = rating_annotations[idx] + if not elem or not elem.val or elem.val < 0 then + return x == 0 and y == 1 and DASH_PEN or gui.CLEAR_PEN + end + local val = math.min(6, elem.val) + return (dfhack.screen.inGraphicsMode() and FACE_TILES or ASCII_FACE_TILES)[val][y][x] +end + +function SquadAnnotationOverlay:init() + for idx = 1, 10 do + self:addviews{ + widgets.Label{ + frame={t=idx*3+1, h=1, w=3}, + text={ + { + text=curry(get_annotation_text, idx), + pen=curry(get_annotation_color, idx), + width=3, + rjustify=true, + }, + }, + visible=function() return not use_stress_faces end, + }, + widgets.Label{ + frame={t=idx*3, r=0, h=2, w=2}, + auto_height=false, + text={ + {width=1, tile=curry(get_stress_face_tile, idx, 0, 0)}, + {width=1, tile=curry(get_stress_face_tile, idx, 1, 0)}, + NEWLINE, + {width=1, tile=curry(get_stress_face_tile, idx, 0, 1)}, + {width=1, tile=curry(get_stress_face_tile, idx, 1, 1)}, + }, + visible=function() return use_stress_faces end, + }, + } end end OVERLAY_WIDGETS = { squad_assignment=SquadAssignmentOverlay, + squad_annotation=SquadAnnotationOverlay, } --[[