Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Template for new versions:
- `gui/export-world-map`: New GUI tool to configure world map exports from the embark selection screen.

## New Features
- `gui/unit-info-viewer`: the unit character sheet now shows the viewed unit's current and long-term mood, color-coded by stress level

## Fixes
- `bodyswap`: fix "invalid argument count" when the target unit has no nemesis record
Expand Down
3 changes: 3 additions & 0 deletions docs/gui/unit-info-viewer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ panels, color-coded to highlight rust and the highest skill levels:
- If a skill is rusty, then the level marker is colored light red
- If a skill is at Legendary level or higher, it is colored light cyan
- Other skills are colored plain white

It also shows the viewed unit's current and long-term mood at the top of
their character sheet, color-coded by stress level.
2 changes: 2 additions & 0 deletions gui/unit-info-viewer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ local gui = require('gui')
local widgets = require('gui.widgets')

local skills_progress = reqscript('internal/unit-info-viewer/skills-progress')
local overall_mood = reqscript('internal/unit-info-viewer/overall-mood')

--------------------------------------------------
---------------------- Time ----------------------
Expand Down Expand Up @@ -549,6 +550,7 @@ end

OVERLAY_WIDGETS = {
skillprogress=skills_progress.SkillProgressOverlay,
mood=overall_mood.OverallMoodOverlay,
}

if dfhack_flags.module then
Expand Down
132 changes: 132 additions & 0 deletions internal/unit-info-viewer/overall-mood.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
--@ module=true

local gui = require('gui')
local widgets = require('gui.widgets')
local overlay = require('plugins.overlay')

local view_sheets = df.global.game.main_interface.view_sheets

-- the unit sheet is a right-side panel whose layout shifts with the window
-- size, so anchor to the "Overview" tab label, which sits on the bottom tab
-- row and is rendered on every sub-tab of the sheet. the tabs are near the
-- top of the sheet, so only scan that band
local function find_overview_tab()
local dscreen = dfhack.screen
local sw, sh = dscreen.getWindowSize()
for y = 4, math.min(sh - 2, 25) do
local line = {}
for x = sw - 100, sw - 1 do
local tile = dscreen.readTile(x, y)
line[#line+1] = tile and tile.ch > 0 and tile.ch < 128
and string.char(tile.ch) or ' '
end
local s = table.concat(line):find('Overview')
if s then
return y, sw - 101 + s
end
end
end

-- display names and colors follow the stress categories in spectate and
-- dwarfmonitor: index 1 is most stressed, 7 is least
local STRESS_LEVELS = {
{name='Miserable', pen=COLOR_LIGHTRED},
{name='Unhappy', pen=COLOR_RED},
{name='Displeased', pen=COLOR_YELLOW},
{name='Content', pen=COLOR_WHITE},
{name='Pleased', pen=COLOR_CYAN},
{name='Happy', pen=COLOR_LIGHTBLUE},
{name='Ecstatic', pen=COLOR_LIGHTGREEN},
}

local function get_level(unit, stress)
if not stress then
return STRESS_LEVELS[dfhack.units.getStressCategory(unit) + 1]
end
return STRESS_LEVELS[dfhack.units.getStressCategoryRaw(stress) + 1]
end

OverallMoodOverlay = defclass(OverallMoodOverlay, overlay.OverlayWidget)
OverallMoodOverlay.ATTRS {
desc="Show the viewed unit's current and long-term mood on their sheet.",
default_pos={x=-96, y=10},
default_enabled=true,
version=1,
viewscreens={
'dwarfmode/ViewSheets/UNIT/Overview',
'dungeonmode/ViewSheets/UNIT/Overview',
},
frame={w=27, h=2},
-- anchor on the first tick the sheet is open so the widget never flashes
-- at the default position; the scan itself is gated by a cheap key check
overlay_onupdate_max_freq_seconds=0,
}

function OverallMoodOverlay:init()
self:addviews{
widgets.Label{
view_id='mood',
frame={t=0, l=0},
auto_height=false,
text='',
},
}
end

function OverallMoodOverlay:onRenderBody(dc)
local text, key = {}, ''
local unit = df.unit.find(view_sheets.active_id)
local soul = unit and unit.status.current_soul
if soul then
local cur = get_level(unit)
local longterm = get_level(unit, soul.personality.longterm_stress)
key = cur.name .. '|' .. longterm.name
text = {
{text='Mood: '},
{text=cur.name, pen=cur.pen},
NEWLINE,
{text='Long-term mood: '},
{text=longterm.name, pen=longterm.pen},
}
end
-- only rewrite the label when it changes; setText forces a relayout,
-- which makes the text flicker every frame
if key ~= self.last_text_key then
self.last_text_key = key
self.subviews.mood:setText(text)
end
OverallMoodOverlay.super.onRenderBody(self, dc)
end

function OverallMoodOverlay:overlay_onupdate()
-- the overlay framework persists user-set positions in config.pos; if the
-- user has repositioned us away from the default, don't fight them
local config = overlay.get_state().config[self.name]
if config and config.pos and
(config.pos.x ~= self.default_pos.x or
config.pos.y ~= self.default_pos.y) then
return
end
-- only rescan when the window size or sheet unit changes, and only after a
-- successful anchor so a transient miss doesn't stick
local _, sh = dfhack.screen.getWindowSize()
local scan_key = sh * 100000 + view_sheets.active_id
if scan_key == self.scan_key and self.anchored then return end
self.scan_key = scan_key
local y, x = find_overview_tab()
if not y then
self.anchored = false
return
end
-- the tab label sits on the lower tab row; put the mood text at the bottom
-- of the vital-stats section (just above its divider), aligned with the
-- section's left content margin
local t = y + 13
x = x - 2
if self.anchored and self.frame.t == t and self.frame.l == x then return end
self.frame.t = t
self.frame.r = nil
self.frame.l = x
self.anchored = true
self:updateLayout(gui.ViewRect{rect=gui.get_interface_rect()})
end
144 changes: 144 additions & 0 deletions test/overlay/unit-info-viewer-mood.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
local gui = require('gui')
local overlay = require('plugins.overlay')

local view_sheets = df.global.game.main_interface.view_sheets

config = {
target = 'unit-info-viewer',
mode = 'fortress',
}

local was_overlay_enabled = overlay.isEnabled()

local function get_widget()
overlay.setEnabled(true)
overlay.rescan()
overlay.overlay_command({'enable', 'gui/unit-info-viewer.mood'})
if not overlay.isOverlayEnabled('gui/unit-info-viewer.mood') then
qerror('could not enable gui/unit-info-viewer.mood overlay')
end
return overlay.get_state().db['gui/unit-info-viewer.mood'].widget
end

local function sheet_is_open()
return dfhack.gui.matchFocusString('dwarfmode/ViewSheets/UNIT/Overview',
dfhack.gui.getDFViewscreen(true))
end

local function get_citizen()
for _,u in ipairs(df.global.world.units.active) do
if dfhack.units.isCitizen(u) and u.status.current_soul then
return u
end
end
end

local function open_sheet(unit)
view_sheets.open = true
view_sheets.context = df.view_sheets_context_type.REGULAR_PLAY
view_sheets.active_sheet = df.view_sheet_type.UNIT
view_sheets.active_id = unit.id
view_sheets.active_sub_tab = 0
view_sheets.viewing_unid:insert('#', unit.id)
end

local function close_sheet()
view_sheets.open = false
view_sheets.active_id = -1
view_sheets.active_sheet = df.view_sheet_type.NONE
view_sheets.viewing_unid:resize(0)
end

local function with_unit_sheet(test_fn)
local unit = get_citizen()
expect.true_(unit ~= nil, 'test fort must have at least one citizen')
local was_open = sheet_is_open()
local saved = {
open = view_sheets.open,
context = view_sheets.context,
sheet = view_sheets.active_sheet,
id = view_sheets.active_id,
sub_tab = view_sheets.active_sub_tab,
}
local saved_unids = {}
for i = 0, #view_sheets.viewing_unid - 1 do
saved_unids[i] = view_sheets.viewing_unid[i]
end
dfhack.with_finalize(
function()
view_sheets.viewing_unid:resize(0)
if was_open then
view_sheets.open = saved.open
view_sheets.context = saved.context
view_sheets.active_sheet = saved.sheet
view_sheets.active_id = saved.id
view_sheets.active_sub_tab = saved.sub_tab
for i = 0, #saved_unids - 1 do
view_sheets.viewing_unid:insert('#', saved_unids[i])
end
else
close_sheet()
end
overlay.setEnabled(was_overlay_enabled)
end,
function()
open_sheet(unit)
test_fn(get_widget(), unit)
end)
end

local function label_text(widget)
widget:onRenderBody()
local parts = {}
for _,tok in ipairs(widget.subviews.mood.text) do
parts[#parts+1] = type(tok) == 'table' and tok.text or tostring(tok)
end
return table.concat(parts)
end

function test.mood_shows_stress_categories()
with_unit_sheet(function(widget, unit)
local soul = unit.status.current_soul
local cur = soul.personality.stress
local lt = soul.personality.longterm_stress
local text = label_text(widget)
expect.true_(text:find('Mood:') ~= nil, 'mood line missing: ' .. text)
expect.true_(text:find('Long%-term mood:') ~= nil,
'long-term line missing: ' .. text)
expect.true_(#text > 0)
end)
end

function test.mood_reflects_stress_change()
with_unit_sheet(function(widget, unit)
local soul = unit.status.current_soul
local saved = soul.personality.stress
local function restore()
soul.personality.stress = saved
end
dfhack.with_finalize(restore, function()
soul.personality.stress = 100000
local text = label_text(widget)
expect.true_(text:find('Miserable') ~= nil,
'expected Miserable at stress=100000: ' .. text)
soul.personality.stress = -150000
text = label_text(widget)
expect.true_(text:find('Ecstatic') ~= nil,
'expected Ecstatic at stress=-150000: ' .. text)
end)
end)
end

function test.mood_hides_for_soulless_unit()
with_unit_sheet(function(widget, unit)
local soul = unit.status.current_soul
local saved = unit.status.current_soul
dfhack.with_finalize(function()
unit.status.current_soul = saved
end, function()
unit.status.current_soul = nil
local text = label_text(widget)
expect.eq('', text, 'expected empty text for soul-less unit')
end)
end)
end
Loading