Advertisement
Guest User

[Dwarf Fortress] Fleeting Frames' relationsindicator v 1.0

a guest
Sep 15th, 2017
410
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. local helptext = [=[
  2. relations-indicator
  3. ===================
  4. v1.0
  5. Displays the pregnancy, fertility and romantic status of unit(s).
  6. For singular unit, in (v)iew-(g)eneral mode.
  7. For multiple units, in Citizens, Pets/Livestock and Others lists.
  8. By default, displays just pregnancies in those,
  9. as well as binding itself to be called there via keybinding plugin.
  10. Can be called with following arguments
  11. Relating to instructions
  12. -help, help, ?, -?
  13. Displays this text and exits
  14. explain colors
  15. Explains the meanings of various colors used and exits
  16. bindings
  17. Lists the keybindings relations-indicator uses with bind and exits
  18. bind
  19. adds context-specific keybindings
  20. unbind
  21. removes context-specific keybindings and exits]=]
  22.  
  23. -- ======================================== --
  24. -- Color shades --
  25. -- ======================================== --
  26.  
  27. local COLOR_DARKGRAY = 8 -- British spelling
  28. local COLOR_LIGHTGRAY = 7 -- ditto
  29.  
  30. local straightMaleShade = COLOR_LIGHTCYAN -- feels like this should be pregnant color, but tweak uses green
  31. local straightFemaleShade = COLOR_LIGHTMAGENTA -- DT colors. Could alternatively flicker with symbol.
  32. local gayMaleShade = COLOR_YELLOW -- Blue makes more sense, but dark blue on black is hard to see.
  33. local gayFemaleShade = COLOR_LIGHTRED -- originally love or straight female color.
  34. local pregnantColor = COLOR_LIGHTGREEN -- base tweak color, might wap with yellow
  35. local infertileColor = COLOR_LIGHTGRAY
  36. local offsiteShade = COLOR_WHITE -- issue: orientation-offsite? Blinking can solve this, ofc.
  37. local deadColor = COLOR_DARKGRAY -- still, should avoid blinking with symbols if possible.
  38. -- 8 shades, but I only have 7 available. Plus default blue is not so great.
  39.  
  40.  
  41. local function colorExplanation()
  42. function printlnc(text, color)
  43. dfhack.color(color)
  44. dfhack.println(text)
  45. dfhack.color(COLOR_RESET)
  46. end
  47. dfhack.println("relations-indicator marks the following traits with following colour:")
  48. dfhack.print("Straight Male ") printlnc("Cyan", straightMaleShade)
  49. dfhack.print("Gay Male ") printlnc("Yellow", gayMaleShade)
  50. dfhack.print("Straight Female ") printlnc("Magenta", straightFemaleShade)
  51. dfhack.print("Gay Female ") printlnc("Red", gayFemaleShade)
  52. dfhack.print("Pregnant ") printlnc("Green", pregnantColor)
  53. dfhack.println("")
  54. dfhack.println("For the first four, darker shade indicates unwillingness to marry.")
  55. dfhack.println("")
  56. dfhack.println("The below three by default replace the first four in top-down hiearchy:")
  57. dfhack.println("")
  58. dfhack.print("Dead partner ") printlnc("Dark gray ", deadColor)
  59. dfhack.print("Infertile/aromantic or infertile/aromantic hetero partner ") printlnc("Light gray", infertileColor)
  60. dfhack.print("Offsite partner ") printlnc("White", offsiteShade)
  61. end
  62.  
  63. -- ======================================== --
  64. -- Indicator Symbols --
  65. -- ======================================== --
  66.  
  67. local loversSymbol = "\148"
  68. local marriedSymbol = "\3"
  69. local singleMaleSymbol = "\11"
  70. local singleFemaleSymbol = "\12"
  71. local fertileBirdSymbol = "\8" -- used only in unitlist
  72. local pregnantCreatureSymbol = "\20" -- also only in unitlist
  73. local diedS, diedE = "\197", "\197" --Used just for viewing a single unit.
  74. local offsite = "..." --Also for just a single unit only.
  75. local blinkingdelay = 650
  76. --How often does it blink between colors and/or symbols.
  77.  
  78. -- ======================================== --
  79. -- Affection thresholds --
  80. -- ======================================== --
  81.  
  82. local friendlinessthreshold = 0
  83. local loversthreshold = 14
  84. local marriagethreshold = -40
  85.  
  86. -- ======================================== --
  87. -- Keybindings used --
  88. -- ======================================== --
  89.  
  90. local keybinding_list = {}
  91. table.insert(keybinding_list, "U@dwarfmode/Default relations-indicator")
  92.  
  93. table.insert(keybinding_list, "V@dwarfmode/Default relations-indicator")
  94. table.insert(keybinding_list, "Z@unitlist/Citizens relations-indicator")
  95. table.insert(keybinding_list, "Z@dfhack/unitlabors relations-indicator")
  96. table.insert(keybinding_list, "Z@unitlist/Livestock relations-indicator")
  97. table.insert(keybinding_list, "Z@unitlist/Citizens relations-indicator")
  98. table.insert(keybinding_list, "Z@unitlist/Others relations-indicator")
  99.  
  100.  
  101. local function relationsIndicatorPowerState(enable)
  102. for bindingi=1, #keybinding_list do
  103. dfhack.run_command_silent("keybinding " .. (enable and "add " or "clear ") .. keybinding_list[bindingi])
  104. end
  105. end
  106.  
  107. local args = {...}
  108.  
  109. local argsline = table.concat(args, " ")
  110. --utils.processArgs is neat but requires - and not necessary here
  111. if argsline:find("help") or
  112. argsline:find("?") then
  113. dfhack.println(helptext)
  114. qerror("")
  115. --Not, strictly speaking, a proper exit but it'll do
  116. end
  117. if argsline:find("explain colors") then colorExplanation() qerror("") end
  118.  
  119. if argsline:find("bindings") then
  120. for bindingi=1, #keybinding_list do
  121. dfhack.println(keybinding_list[bindingi])
  122. end
  123. dfhack.println("(Internal) Z@dfhack/lua/manipulator relations-indicator")
  124. qerror("")
  125. end
  126.  
  127. if argsline:find("unbind") then relationsIndicatorPowerState(false) qerror("") end
  128. if argsline:find("bind") then
  129. relationsIndicatorPowerState(true)
  130. end
  131.  
  132. -- ======================================== --
  133. -- Loading required modules --
  134. -- ======================================== --
  135.  
  136. local gui = require 'gui'
  137.  
  138. local screenconstructor = dfhack.script_environment("gui/indicator_screen")
  139.  
  140. -- ======================================== --
  141. -- Utility functions --
  142. -- ======================================== --
  143.  
  144.  
  145. local function getLen(data)
  146. -- Can't # a hashed table, must use pairs.
  147. local len = 0
  148. for i, val in pairs(data) do
  149. len = len +1
  150. end
  151. return len
  152. end
  153.  
  154. function blinkergenerator(tableofblinkers)
  155. -- takes numerically indexed table of values
  156. -- returns either single value if table has a single value, or function that alternates which one it returns
  157. -- local manyblinkers = #tableofblinkers
  158. if #tableofblinkers == 1 then
  159. return tableofblinkers[1]
  160. else
  161. function blinkingfunction()
  162. local blinkertable = tableofblinkers
  163. local blinkernr = #tableofblinkers
  164.  
  165. return blinkertable[1+math.floor(dfhack.getTickCount()/blinkingdelay) % blinkernr]
  166. end
  167. return blinkingfunction
  168. end
  169. end
  170.  
  171. function getFlag(object, flag)
  172. -- Utility function for safely requesting info from userdata
  173. -- Returns nil if the object doesn't have flag attribute, else returns it's value
  174. -- Because well, ordinarily, {}[flag] returns nil.
  175. -- However, if object is unit - or some other type, it may instead throw an error
  176. local a = {}
  177. if not object or not flag then return nil end
  178. --Crash is still possible for attempting to pairs a nil
  179. for index, value in pairs(object) do
  180. a[index] = value
  181. end
  182. local returnvalue = a[flag]
  183. a = nil --lua automatically garbage cleans tables without variable that links to them.
  184. return returnvalue
  185. end
  186.  
  187. function getBottomMostViewscreenWithFocus(text, targetscreen)
  188. --Finds and returns the screen closest to root screen whose path includes text
  189. --duplicated from getScreen, admittedly.
  190. if targetscreen and
  191. dfhack.gui.getFocusString(targetscreen):find(text) then
  192. return targetscreen
  193. elseif targetscreen and targetscreen.child then --Attempting to call nil.child will crash this
  194. return getBottomMostViewscreenWithFocus(text, targetscreen.child)
  195. end
  196. -- if there's no child, it didn't find a screen with text in focus and returns nil
  197. end
  198.  
  199. function writeoverTable(modifiedtable, copiedtable)
  200. --Takes two tables as input
  201. --Removes all the values in first table
  202. --Then places all the values in second table into it
  203. --Returns the first table after that
  204. for index, value in pairs(modifiedtable) do
  205. modifiedtable[index] = nil
  206. end
  207. for index, value in pairs(copiedtable) do
  208. modifiedtable[index] = value
  209. end
  210. return modifiedtable
  211. end
  212.  
  213. function hasKeyOverlap (searchtable, searchingtable)
  214. -- Looks for a key in searchtable whose name contains the name of a key in searchtable
  215. -- returns true if finds it.
  216. for aindex, avalue in pairs(searchingtable) do
  217. for bindex, bvalue in pairs(searchtable) do
  218. if tostring(bindex):find(tostring(aindex)) then
  219. return true
  220. end
  221. end
  222. end
  223. end
  224.  
  225. -- ======================================== --
  226. -- Tier 1 (df) functions --
  227. -- ======================================== --
  228.  
  229. function isItBird(unit)
  230. return (df.global.world.raws.creatures.all[unit.race].caste[0].flags.LAYS_EGGS)
  231. --because it lists all available flags as true or false doesn't need to invoke getFlag
  232. end
  233.  
  234. function isItSmart(unit)
  235. if (df.global.world.raws.creatures.all[unit.race].caste[0].flags.CAN_LEARN and
  236. not df.global.world.raws.creatures.all[unit.race].caste[0].flags.SLOW_LEARNER) then
  237. return true
  238. else
  239. return false
  240. end
  241.  
  242. end
  243.  
  244. function getGenderInInfertileColor(unit)
  245. local symbolColor = {}
  246. symbolColor.text = unit.sex == 1 and
  247. singleMaleSymbol or
  248. ( unit.sex == 0 and
  249. singleFemaleSymbol or "")
  250. symbolColor.color = infertileColor
  251. return symbolColor
  252. end
  253. -- ======================================== --
  254. -- 43.05 vs earlier structure differences --
  255. -- ======================================== --
  256.  
  257. function getPregnancyTimer(unit)
  258. -- Takes local unit, returns time until birth in steps.
  259. -- In case of historical unit always returns -1; don't know about their pregnancy structures.
  260. -- utilizes getFlag
  261. if getFlag(unit, "info") then return -1 end -- so assume they're always not pregnant. They usually aren't
  262. return (getFlag(unit, "pregnancy_timer") or getFlag(unit.relations, "pregnancy_timer"))
  263. --Differences between 43.05 and earlier dfhack.
  264. end
  265.  
  266. function getLocalRelationship(unit, SpouseOrLover)
  267. --Takes local unit, boolean
  268. -- returns spouse id in the case of true for second value,
  269. -- lover in the case of false or if nil is used and spouse isn't present.
  270. -- utilizes getFlag
  271. if getFlag(unit, "info") then return nil end
  272. --Not intended to be used on historical figure structure, as that is different.
  273. --Also, using nil when number is expected will throw an error, so this points out those mistakes
  274. local is4305p = getFlag(unit, "relationship_ids") and true or false
  275. local relationships = is4305p and unit.relationship_ids or unit.relations
  276. local spousevalue = is4305p and relationships.Spouse or relationships.spouse_id
  277. local lovervalue = is4305p and relationships.Lover or relationships.lover_id
  278. --Again, differences between 43.05 and earlier dfhack.
  279. --Further issue: 43.03 uses spouse_id, 43.05 uses Spouse
  280. --This is not as extensible, but since I only get spouse or lover for now...
  281. if SpouseOrLover == true then return spousevalue end
  282. if SpouseOrLover == false then return lovervalue end
  283. if spousevalue > -1 then
  284. return spousevalue
  285. else
  286. return lovervalue
  287. end
  288. end
  289.  
  290. -- ======================================== --
  291. -- Tier 2 functions --
  292. -- ======================================== --
  293.  
  294. function isItGelded(unit)
  295. -- Either local or historical unit
  296. -- returns true or nil
  297. -- utilizes getFlag
  298. if getFlag(unit, "status") then
  299. --local unit
  300. if unit.flags3.gelded then
  301. --usually only pets and guys are gelded, but can be set on anyone
  302. return true
  303. elseif unit.curse.add_tags2.STERILE then
  304. --occurs for vampires and husks
  305. return true
  306. elseif not getFlag(unit.status, "current_soul") then
  307. --occurs for animated corpses
  308. --Could also use unit.curse.add_tags1.OPPOSED_TO_LIFE
  309. --Though I'm not certain you need a soul to breed, lack of soul messes up checking personality
  310. return true
  311. end
  312. elseif getFlag(unit, "info") then
  313. --historical unit
  314. if (getFlag(unit.info, "wounds") and getFlag(unit.info, "wounds").anon_3 ==1 ) then
  315. --suspected gelding flag. 0 is dead, -1 is default?
  316. return true
  317. elseif (getFlag(unit.info,"curse") and getFlag(unit.info.curse, "active_interactions") ) then
  318. for i, interaction in pairs(unit.info.curse.active_interactions) do
  319. --Here, could just check that it's name has VAMPIRE in it in vanilla, but could have modded vamps
  320. --Interestingly, soul is not mentioned in historical unit data. Presumably it is hiding. Fallback plan
  321. for j, text in pairs(interaction.str) do
  322. if text:find("STERILE") or
  323. text:find("OPPOSED_TO_LIFE") then
  324. --side effect: False positive on syndromes that remove those tags.
  325. --ex: modded-in gonads that remove sterility from otherwise-sterile creature
  326. --TODO: fix
  327. return true
  328. end
  329. end
  330. end
  331. end
  332. end
  333. return nil
  334. end
  335.  
  336. function getPregnancyText(unit)
  337. -- Takes local unit, returns single line string that is "" for no pregnancy
  338. -- utilizes getFlag, getPregnancyTimer
  339. if not getFlag(unit, "status") or
  340. unit.caste > 0 then
  341. return ""
  342. else
  343. local howfar = getPregnancyTimer(unit)
  344.  
  345. local pregnancy_duration = df.global.world.raws.creatures.all[unit.race].caste[0].flags.CAN_LEARN and 9 or 6
  346. if howfar < 1 then return "" else
  347. local returnstring = ""
  348. if isItBird(unit) then
  349. if (howfar/33600)>1 then returnstring = "Was wooed " .. tostring(math.floor(pregnancy_duration-howfar/33600)) .. " months ago" end
  350. if (howfar/33600)<1 then returnstring = "Last seed withers in " .. tostring(math.floor(howfar/1200)) .. " days " end
  351. if (howfar/33600)>(pregnancy_duration-1) then returnstring = "Was wooed recently" end
  352.  
  353. else
  354. if (howfar/33600)>0 then returnstring = "Sick in the morning recently" end
  355. if (howfar/33600)<(pregnancy_duration-1) then returnstring = "Missed a period" end
  356. if (howfar/33600)<(pregnancy_duration-2) then returnstring = tostring(math.floor(pregnancy_duration-howfar/33600)) .. " months pregnant" end
  357. if (howfar/33600)<1 then returnstring = "Will give birth in " .. tostring(math.floor(howfar/1200)) .. " days" end
  358. end
  359. return returnstring;
  360. end
  361. end
  362. end
  363.  
  364. function getSpouseAndLover(unit)
  365. --Takes a local unit, returns either local or historical spouse and lover of that unit if present
  366. --if the spouse/lover is historically set but culled (single parents who can marry), returns false for those
  367. --else, returns for that value
  368. -- utilizes getLocalRelationship, df.historical_figure.find
  369. local historical_unit = df.historical_figure.find(unit.hist_figure_id)
  370. local spouse,lover, unithistoricalrelations
  371. local spouseid = getLocalRelationship(unit, true)
  372. local loverid = getLocalRelationship(unit, false)
  373. if spouseid > -1 then
  374. spouse = df.unit.find(spouseid)
  375. --shortcut
  376. end
  377. if not spouse and --shortcut failed due spouse never arriving, or having left the site
  378. historical_unit then --pets brought on embark aren't historical
  379. --got to dig into historical unit values
  380. unithistoricalrelations = historical_unit.histfig_links
  381. for index, relation in pairs(unithistoricalrelations) do
  382. --no local spouse? Mark both global spouse and lover
  383. if (relation._type == df.histfig_hf_link_spousest) then
  384. spouse=df.historical_figure.find(relation.target_hf)
  385. if not spouse then spouse = false
  386. --there was an id, but there wasn't a histfig with that id (due culling)
  387. end
  388. -- small distinction between nil and false: is it better to have loved and lost, or never loved?
  389. elseif (relation._type == df.histfig_hf_link_loverst) then
  390. lover=df.historical_figure.find(relation.target_hf)
  391. if not lover then lover = false
  392. end
  393. end
  394. end
  395. end
  396. if loverid > -1 then
  397. lover=df.unit.find(loverid) --can be nil for offsite lover
  398. if not lover then lover = false end --false instead of nil to indicate having checked
  399. end
  400. if not lover and historical_unit then
  401. --No local lover? Maybe lover is global
  402. unithistoricalrelations = historical_unit.histfig_links
  403. for index, relation in pairs(unithistoricalrelations) do
  404. if (relation._type == df.histfig_hf_link_loverst) then
  405. lover=df.historical_figure.find(relation.target_hf)
  406. if not lover then lover = false end
  407. end
  408. end
  409. end
  410. return spouse, lover
  411. end
  412.  
  413. function areCompatible(unitA,unitB)
  414. --Checks if two local units make compatible pair and returns true if they are, false if not
  415. --Utilizes getFlag, requires them to be historical to check relationships
  416.  
  417. -- Do I check if one is a child?
  418. -- I think not. Arranged marriages can be planned a decade before they happen.
  419. -- Still, age is most common disqualifying factor
  420. if unitA.race ~= unitB.race then return false end
  421. --multi-racial fortress are nice, but humans still can't into dwarves; not like this.
  422. local is4305p = getFlag(unitA, "relationship_ids") and true or false
  423. local relationshipsA = is4305p and unitA or unitA.relations
  424. local relationshipsB = is4305p and unitB or unitB.relations
  425. --age is stored in relations for 4303 but on base level in 43.05, so...
  426. local ageA = relationshipsA.birth_year+relationshipsA.birth_time/403200
  427. local ageB = relationshipsB.birth_year+relationshipsB.birth_time/403200
  428. --exact age matters
  429. if (ageA-ageB) > 10 or (ageB-ageA) > 10 then
  430. --over 10 year age difference
  431. return false
  432. end
  433.  
  434. -- Lets check if one of them is married.
  435. -- If they are, can do hanky panky with spouse alone
  436. local spouseA, loverA = getSpouseAndLover(unitA)
  437. local spouseB, loverB = getSpouseAndLover(unitB)
  438. if spouseA or loverA or
  439. spouseB or loverB then
  440. if spouseA == unitB or
  441. loverA == unitB then
  442. return true
  443. else
  444. return false
  445. end
  446. end
  447.  
  448. --Lets check if they have compatible orientations for marrying each other.
  449. local attractionA = getAttraction(unitA)
  450. local attractionB = getAttraction(unitB)
  451. if not ((2 == attractionA[(unitB.sex == 1 and "guy" or "girl")]) and
  452. (2 == attractionB[(unitA.sex == 1 and "guy" or "girl")])) then
  453. -- Admittedly, this means that someone who only romances has no suitable pairings.
  454. return false
  455. end
  456.  
  457. --Lets check if personalities are compatible.
  458. local unwillingToFriendA, unwillingToLoveA, unwillingToMarryA = getAromantism(unitA)
  459. local unwillingToFriendB, unwillingToLoveB, unwillingToMarryB = getAromantism(unitB)
  460. if unwillingToFriendA or unwillingToLoveA or unwillingToMarryA or
  461. unwillingToFriendB or unwillingToLoveB or unwillingToMarryB then
  462. --If either one as baggage about progressing through a relationship, no babies
  463. return false
  464. end
  465. --Checking for relationships requires digging into historical unit values.
  466. local hfA = unitA.hist_figure_id > -1 and df.historical_figure.find(unitA.hist_figure_id) or nil
  467. local hfB = unitB.hist_figure_id > -1 and df.historical_figure.find(unitB.hist_figure_id) or nil
  468. if hfA and hfB then --basic sanity check.
  469. -- Function to check for being a sibling.
  470. -- Half-siblings...Possible with hacking, and I bet they block
  471. function gethfParent(hfunit, retrieveMother)
  472. --Returns historical mother or father of a historical unit if possible
  473. --otherwise returns nil
  474. for index, relationship_link in pairs(hfunit.histfig_links) do
  475. if retrieveMother and relationship_link._type == df.histfig_hf_link_motherst or
  476. (not retrieveMother and relationship_link._type == df.histfig_hf_link_fatherst) then
  477. return df.historical_figure.find(relationship_link.target_hf)
  478. end
  479. end
  480. end
  481.  
  482. local momA = gethfParent(hfA, true)
  483. local momB = gethfParent(hfB, true)
  484. local dadA = gethfParent(hfA)
  485. local dadB = gethfParent(hfB)
  486. if momA and momB and momA == momB or --existence of moms must be checked since nil == nil
  487. (dadA and dadB and dadA == dadB) then
  488. --siblings or half-siblings are not allowed
  489. return false
  490. end
  491.  
  492. --Function to check for grudge:
  493. -- (As it is not used outside parent function, not encapsulating elsewhere despite size)
  494. function hasGrudgeTowards(hfUnitChecked, hfUnitFuckThisCreatureInParticular)
  495. -- Triple-loops checking info.relationships.list[#].anon_3[#].
  496. -- Admittedly, doing this repeatedly for every unit is inefficient.
  497. -- Better would be finding all grudges in fortress at start and cross-checking that.
  498. if hfUnitChecked.info.relationships then
  499. --Invaders, for instance, may have it absent
  500. --Though I wonder if it is even possible to marry off invaders, even after peace settlement
  501. for index, relationship in pairs (hfUnitChecked.info.relationships.list) do
  502. if hfUnitFuckThisCreatureInParticular.id == relationship.histfig_id then
  503. --Found a relationship between the two units. Now for grudge!
  504. for feelingindex, feelingtype in pairs(relationship.anon_3) do
  505. --A dwarf can have multiple feelings/relationship types with someone.
  506. if feelingtype == 2 then
  507. --[[List of options I've noticed with changing that value:
  508. 0: Hero
  509. 1: Friend
  510. 2: Grudge
  511. 3: Bonded
  512. 6: Good for Business
  513. 7: Friendly Terms? (unsure)
  514. 10: Comrade
  515. 17: Loyal Soldier
  516. 18: Considers Monster (hm, could be interesting RP-wise)
  517. 26: Protector of the Weak
  518.  
  519. Others seemed to default to Friendly terms
  520. with just few points on 7 as second relation.
  521.  
  522. Perhaps anon_1 and anon_5 may also matter.
  523. --]]
  524. return true
  525. end
  526. end
  527. --Found unit without grudge.
  528. return false
  529. end
  530. end
  531. end
  532. end
  533.  
  534. if hasGrudgeTowards(hfA, hfB) or
  535. hasGrudgeTowards(hfB, hfA) then
  536. --Either one having a grudge? Welp, no marriage for you two.
  537. return false
  538. end
  539. end
  540. -- No other disqualifing factors? It's a go.
  541.  
  542. return true
  543. end
  544.  
  545. function getInclinationIfPresent(unit, inclinationnumber)
  546. --takes ensouled unit and numerical index of inclination
  547. --returns the value of inclination or 0 or -1000 in case of divorce from local and seeing the world.
  548. -- utilizes getFlag
  549. local values
  550. if getFlag(unit,"status") then
  551. values = unit.status.current_soul.personality.values
  552. elseif getFlag(unit.info,"personality") then
  553. --can be nil for local units who have never updated their hfunit
  554. --comes up in the case of divorce.
  555. values = unit.info.personality.values
  556. else
  557. return -1000 --buggy placeholder: divorced partners are super-incapable of progressing their relationship.
  558. end
  559. -- Do need to check hfunits, since both parties of a ship must be willing to embark on the waters of marriage
  560. for index, value in pairs(values) do
  561. if value.type == inclinationnumber then
  562. return value.strength
  563. end
  564. end
  565. return 0
  566. end
  567.  
  568. function getAttraction(unit)
  569. --unit can't be nil. The nothingness where there should be something doesn't have sex, y'see.
  570. --Outputs a table of levels of sexual attraction an unit has.
  571. -- utilizes getFlag
  572. local attraction = {guy = 0, girl = 0}
  573. local orientation
  574. if unit.sex~=-1 then
  575. if getFlag(unit, "status") then
  576. --local unit
  577. orientation= getFlag(unit.status, "current_soul") and unit.status.current_soul.orientation_flags or false
  578. --alas, creatures can be soulless.
  579. else
  580. --historical unit
  581. orientation = unit.orientation_flags
  582.  
  583. end
  584. end
  585. if orientation then
  586. if orientation.romance_male then
  587. attraction.guy = 1
  588. elseif orientation.marry_male then
  589. attraction.guy = 2
  590. end
  591. if orientation.romance_female then
  592. attraction.girl = 1
  593. elseif orientation.marry_female then
  594. attraction.girl = 2
  595. end
  596. end
  597. return attraction
  598. end
  599.  
  600. -- ======================================== --
  601. -- Tier 3 functions --
  602. -- ======================================== --
  603.  
  604. function getAromantism(unit)
  605. --Takes local unit
  606. --returns following series of values
  607. local unwillingToFriend, unwillingToLove, unwillingToMarry
  608. --utilizes getFlag, getIncinationIfPresent
  609. --utilizes these internally:
  610. local smittedness, friendly, bonding
  611.  
  612. --failure conditions : hating friendship and having no eligble friends (not certain, might be enough to just have one-sided relation), hating romance, hating marriage.
  613. -- unit.status.current_soul.personality.values.
  614. -- Type 29: romance, type 2: family, type 3: friendship, type 18: harmony.
  615. -- hfunit.info.personality.values - nil for embark dwarves, present on visitors, like visitors can have nil spouse relation but histfig value set
  616. -- poults born on-site from embark turkeys don't have hfid or more than 0 values.
  617. -- unit.status.current_soul.personality.traits .LOVE_PROPENSITY (if this is high and romance is low can still marry, Putnam suggests from 2015 suggests it is almost the only trait that matters)
  618. -- unknown: Type 3 - friendship, .LUST_PROPENSITY, FRIENDLINESS
  619. -- unknown: how much traits must differ in general for marriage to happen instead of a grudge.
  620. -- also, grudges could be prevented or perhaps later removed by social skills, shared skills and preferences.
  621.  
  622.  
  623. --local smittedness, friendly, bonding, unwillingToFriend, unWillingToLove, unwillingToMarry
  624.  
  625. smittedness = unit.status.current_soul.personality.traits.LOVE_PROPENSITY
  626. -- again, always present on units that are on-site; but obviously histfigs will have to be handled differently
  627.  
  628. friendly =unit.status.current_soul.personality.traits.FRIENDLINESS
  629. --FRIENDLINESS. I think I've seen ever dyed-in-the-wool quarrels have friends.
  630.  
  631. bonding = unit.status.current_soul.personality.traits.EMOTIONALLY_OBSESSIVE -- how easily they make connections
  632. -- per Sarias's research in 2013, dwarves must be friends before becoming lovers.
  633. -- local cheerfulness -- happier dwarves make relationships more easily, buuut everyone is at -99999 anyway
  634. -- local lustfulness -- science required to see if it affects whether relationship can form
  635. -- local trust -- relationships should be about trust, but lying scumbags can get married too irl. Who knows?
  636. -- Eventually, for specific units would have to check how well they match up and return granual value.
  637.  
  638. unwillingToFriend =(getInclinationIfPresent(unit, 3)+friendly+bonding) < friendlinessthreshold and true or false
  639. -- avg 100, min -50, max 250
  640. -- currently requires ~roughly two lowest possible and 1 second lowest possible values.
  641. -- Starting seven do have friends, which can override this.
  642.  
  643. -- While I've failed to friend off dwarves due personality,
  644. -- those dwarves have managed friendships with at least 1 other person,
  645. -- and several times have managed a marriage with someone else.
  646. -- 18-year old Erush Shoduksǎkzul has 3 friends, having 1 lower FRIENDSHIP, bonding
  647.  
  648. unwillingToLove = (getInclinationIfPresent(unit, 29)+smittedness) < loversthreshold and true or false
  649. --not using bonding, maybe should. They already have emotional bond with others, though.
  650. --50 is average. 14 might be too low, allowing second lowest value on 1 with other average
  651. --20 is maximum for non-mentioned propensity and hating even the idea of romance, but can sometimes prevent two 1 worse than average values
  652.  
  653. -- What numercal indicators can I fit into 1 tile, anyway? Blinking, I guess. TWBT would enable gradual color
  654. -- blinking has problems at low fps, but viewing unit and unit list have game paused.
  655. -- Tests should be done with 50 fps/10 gfps, since those are lowest maximums I know of.
  656. -- 30 GFPS can blend-ish, but 10 is blinky. Maybe use -blinking_at input with default value only_if_above_29
  657. -- should check if it blinks on returning to low fps.
  658. unwillingToMarry = getInclinationIfPresent(unit, 2) < marriagethreshold and true or false
  659. --as long as they don't find family loathsome, it's a-ok.
  660.  
  661. return unwillingToFriend, unwillingToLove, unwillingToMarry
  662. end
  663.  
  664. function getSpouseAndLoverLink(unit)
  665. -- Currently takes local unit only
  666. -- Returns eight values: spouse and lover names, spouse and lover sites, spouse and lover aliveness, spouse and lover gender
  667. -- Doesn't check the hf relationships if the hf_only unit doesn't have a spouse in histfig links
  668. -- might be an issue if a visitor comes on map, romances someone, and then leaves. Never heard of that happening, but hey
  669. -- utilizes getFlag, getSpouseAndLover, dfhack.units.isAlive, dfhack.TranslateName, df.historical_figure.find
  670. local spouse,lover
  671. local spousesite, loversite = df.global.ui.site_id, df.global.ui.site_id
  672. --blanket current site, unless I assign them differently later
  673. --this is fine as I have indicator for off-site spouse, not on-site spouse
  674. local spouselives, loverlives
  675. spouse, lover = getSpouseAndLover(unit)
  676. if spouse and getFlag(spouse,"info") then spousesite = spouse.info.unk_14.site end
  677. if lover and getFlag(lover,"info") then loversite = lover.info.unk_14.site end
  678. local spousename, lovername
  679. if spouse == false then
  680. for index, relation in pairs(df.historical_figure.find(unit.hist_figure_id).histfig_links) do
  681. --Spouse has been culled? Maybe they're single parent.
  682. if (relation._type == df.histfig_hf_link_childst) then
  683. spousename = "Single parent"
  684. spouselives = true --More like that they're not dead. Visual thing.
  685. end
  686. end
  687. elseif spouse then
  688. spousename = dfhack.TranslateName(spouse.name)
  689. -- here, as spouse without name doesn't have gender either
  690. if getFlag(spouse, "flags1") then --local unit
  691. spouselives = dfhack.units.isAlive(spouse)
  692. else
  693. spouselives = spouse.died_year < 0
  694. end
  695. end
  696. if lover then
  697. lovername = dfhack.TranslateName(lover.name)
  698. if getFlag(lover, "flags1") then --local unit
  699. loverlives = dfhack.units.isAlive(lover)
  700. else
  701. loverlives = lover.died_year < 0
  702. end
  703. end
  704. -- lovers can't have children, so it's entirely pointless to speak of lost love.
  705. return spousename, lovername, spousesite, loversite, spouselives, loverlives, spouse, lover
  706. end
  707.  
  708. function getSymbolizedAnimalPreference(unit, unwrapped)
  709. --Returns symbolized pregnancy if animal is pregnant.
  710. --Else, returns gender symbol, color: string and number
  711. --color is a function whose return value blinks between modes if appropriate.
  712. --unwrapped doesn't pass the colors through blinkergenerator.
  713. --utilizes getAttraction, isItBird, isItGelded
  714. local attraction = getAttraction(unit)
  715. local symbolColor = {text, color}
  716. local prefColorTable
  717. if getPregnancyTimer(unit) > 0 then
  718. symbolColor.text = isItBird(unit) and fertileBirdSymbol or pregnantCreatureSymbol
  719. symbolColor.color = pregnantColor
  720. else
  721. symbolColor.text = unit.sex == 1 and
  722. singleMaleSymbol or
  723. ( unit.sex == 0 and
  724. singleFemaleSymbol or "")
  725. if unit.sex == -1 or --flesh balls
  726. (attraction.guy == 0 and attraction.girl == 0) or --asexual
  727. isItGelded(unit) then --some tomcats
  728. symbolColor.color = infertileColor
  729. return symbolColor
  730. --strictly speaking, not necessary, due light gray being default color
  731. end
  732. prefColorTable = {}
  733. if unit.sex == 0 then
  734. if attraction.guy > 0 then table.insert(prefColorTable, straightFemaleShade) end
  735. if attraction.girl > 0 then table.insert(prefColorTable, gayFemaleShade) end
  736. else
  737. if attraction.girl > 0 then table.insert(prefColorTable, straightMaleShade) end
  738. if attraction.guy > 0 then table.insert(prefColorTable, gayMaleShade) end
  739. end
  740. end
  741. if unwrapped then
  742. if prefColorTable and #prefColorTable > 0 then
  743. symbolColor.color = prefColorTable
  744. end
  745. return symbolColor
  746. else
  747. if prefColorTable then symbolColor.color = blinkergenerator(prefColorTable) end
  748. if getPregnancyTimer(unit) > 0 then
  749. symbolColor.onhovertext = {
  750. color = pregnantColor,
  751. text = tostring(math.floor((isItSmart(unit) and 9 or 6) -getPregnancyTimer(unit)/33600))
  752. }
  753. end
  754. return symbolColor
  755. end
  756. end
  757.  
  758. function getSymbolizedSpouse(unit)
  759. -- Currently takes local unit only
  760. -- Returns {} with text and color which are string or function and number or function
  761. -- utilizes getSpouseAndLoverLink, getAttraction, getInclinationIfPresent, isItSmart,isItBird, isItGelded
  762. local spousename, lovername, spousesite, loversite, spouselives, loverlives, spouse, lover = getSpouseAndLoverLink(unit)
  763. local symbolColor = {text, color}
  764.  
  765.  
  766. local attraction = getAttraction(unit)
  767. --plain sexual attraction table
  768. --it'd be more compact to code if instead of guy and girl values would use 0 and 1 indices
  769. --could call attraction[unit.sex] == 2 to see if they're willing to engage in gay marriage, for example
  770. --however, code is more self-explanatory with variables having names that explain what they're for
  771. local unwillingToFriend, unwillingToLove, unwillingToMarry
  772. if (attraction.guy+attraction.girl) == 0 then
  773. unwillingToFriend, unwillingToLove, unwillingToMarry = true, true, true
  774. else
  775. unwillingToFriend, unwillingToLove, unwillingToMarry = getAromantism(unit)
  776. end
  777. --series of disqualifying boolean values; though if orientation is already zero better not check
  778.  
  779. local symbolTable = {}
  780. local colorTable = {}
  781. if getPregnancyTimer(unit) > 0 and
  782. isItSmart(unit) then --necessary due otherwise doubling up on the indicator with animal preferences.
  783. --Normally, would lose nothing by having pregnancy highest hiearchy, could just check first and skip the above
  784. --However, in cases of modding or father dying in battle (such as in fucduck's elven outpost) info is lost
  785. if isItBird(unit) then
  786. --possible with bird-women adventurers joining the fortress
  787. table.insert(symbolTable,fertileBirdSymbol)
  788. else
  789. table.insert(symbolTable,pregnantCreatureSymbol)
  790. end
  791. table.insert(colorTable,pregnantColor)
  792. end
  793.  
  794. if not isItSmart(unit) then --it's an animal
  795. local animalprefs = getSymbolizedAnimalPreference(unit,true)
  796. table.insert(symbolTable, animalprefs.text)
  797. if type(animalprefs.color) == "table" then
  798. table.insert(colorTable, animalprefs.color[0])
  799. table.insert(colorTable, animalprefs.color[1]) --two gender prefs at most.
  800. table.insert(symbolTable, animalprefs.text) --going to mess up timing otherwise.
  801. else
  802. table.insert(colorTable, animalprefs.color)
  803. end
  804. end
  805. if not lovername and
  806. (not spousename or spousename == "Single parent") then
  807. if isItSmart(unit) then --otherwise already handled earlier, don't need to add anything.
  808. table.insert(symbolTable,
  809. ((unit.sex == 0) and singleFemaleSymbol or (unit.sex == 1 and singleMaleSymbol or "")))
  810. --creatures without gender don't get a symbol.
  811. if (attraction.guy == 0 and attraction.girl == 0) or --asexual
  812. isItGelded(unit) or -- gelded. Aw.
  813. unwillingToLove or --aromantic. Requires soul.
  814. sex == -1 then --creatures like iron men and bronze colossi indicate that genderless creatures can't breed
  815.  
  816. table.insert(colorTable, infertileColor)
  817.  
  818. else
  819. if unit.sex == 0 then
  820. if attraction.girl > 0 then
  821. table.insert(colorTable,
  822. (gayFemaleShade - 8*( (unwillingToMarry or attraction.girl == 1) and 1 or 0 )))
  823. --darker shade for lover-only relationships
  824. end
  825. if attraction.guy > 0 then
  826. table.insert(colorTable,
  827. (straightFemaleShade - 8*( (unwillingToMarry or attraction.guy == 1) and 1 or 0 )))
  828. end
  829. else
  830. if attraction.girl > 0 then
  831. table.insert(colorTable,
  832. (straightMaleShade - 8*( (unwillingToMarry or attraction.girl == 1) and 1 or 0 )))
  833. end
  834. if attraction.guy > 0 then
  835. table.insert(colorTable,
  836. (gayMaleShade - 8*( (unwillingToMarry or attraction.guy == 1) and 1 or 0 )))
  837. end
  838. end
  839. if #colorTable>#symbolTable and --Our table has more colors than symbols. Noprobs,
  840. #symbolTable>1 then --unless there's pregnant single present
  841. table.insert(symbolTable, symbolTable[#symbolTable])
  842. --Pregnant singles screw up timing, unless we double up on last symbol.
  843. end
  844. end
  845. end
  846. else
  847. if spousename and spousename ~= "Single parent" then
  848. table.insert(symbolTable, marriedSymbol)
  849. --table for on-site, alive, not infertile spouse
  850. -- Using hiearchy:
  851. -- Dead > Infertile (unless gay) > Offsite > Normal bright color.
  852. local spousecolor = (unit.sex == 0) and
  853. (spouse.sex==1 and straightFemaleShade or gayFemaleShade) or
  854. (spouse.sex==0 and straightMaleShade or gayMaleShade)
  855. -- live marriage
  856. spousecolor = df.global.ui.site_id == spousesite and spousecolor or offsiteShade
  857. --spouse is offsite. Can have problems with divorcing.
  858. spousecolor = (isItGelded(unit) or isItGelded(spouse)) and
  859. not (unit.sex == spouse.sex)
  860. and infertileColor or spousecolor
  861. --spouse is infertile and not gay-only marriage
  862. spousecolor = spouselives and spousecolor or deadColor
  863. table.insert(colorTable, spousecolor)
  864. end
  865. if lovername then
  866. table.insert(symbolTable, loversSymbol)
  867. -- Two types of lovers: ones willing to progress to marriage, ones unwilling
  868. -- The unwilling ones get darker shade
  869. -- Both parties must be willing
  870. -- Lovers and spouses may also be off-site, dead or infertile.
  871. -- While can display all with blinky things, should minimize needless UI churn.
  872. -- Dead > Infertile (unless gay) > Offsite > Unwilling to progress to marriage > Normal bright color.
  873. local lovercolor = (unit.sex == 0) and
  874. (lover.sex==0 and gayFemaleShade or straightFemaleShade) or
  875. (lover.sex==0 and straightMaleShade or gayMaleShade)
  876. --baseline is willing to marry
  877. lovercolor = (attraction[((lover.sex==1) and "guy" or "girl")] < 2 or
  878. getAttraction(lover)[((unit.sex==1) and "guy" or "girl")] < 2 or
  879. unwillingToMarry or
  880. (getInclinationIfPresent(lover, 2) < marriagethreshold and true or false)) and
  881. (lovercolor - 8) or lovercolor
  882. -- if the unit or their lover has personality or attraction failure, the relationship will not progress
  883. lovercolor = df.global.ui.site_id == loversite and lovercolor or offsiteShade
  884. --lover is offsite. Can have problems with divorcing.
  885. --Happens mostly in case of visitors or married migrants.
  886. --Issue: Either a possible false hope of offsite lover eventually arriving.
  887. -- or a false indicator of lover being on-site.
  888. --Blinking and blurring could solve this, but not hiearchy.
  889. lovercolor = (isItGelded(unit) or isItGelded(lover)) and
  890. not (unit.sex == lover.sex)
  891. and infertileColor or lovercolor
  892. --lover is infertile and not gay-only marriage
  893. lovercolor = loverlives and lovercolor or deadColor
  894. --lover is dead
  895. table.insert(colorTable, lovercolor)
  896. end
  897.  
  898. end
  899.  
  900. symbolColor.text = blinkergenerator(symbolTable)
  901.  
  902. symbolColor.color = blinkergenerator(colorTable)
  903.  
  904. if getPregnancyTimer(unit) > 0 then
  905. symbolColor.onhovertext = {
  906. color = pregnantColor,
  907. text = tostring(math.floor((isItSmart(unit) and 9 or 6) -getPregnancyTimer(unit)/33600))
  908. --Needs to be converted to string since #text is called for width
  909. }
  910. end
  911.  
  912. return symbolColor
  913.  
  914. end
  915.  
  916. function getSpouseText(unit)
  917. -- Takes local unit, returns single line string that is "" if there's no romantic relation
  918. local spousename, lovername, spousesite, loversite, spouselives, loverlives = getSpouseAndLoverLink(unit)
  919. --An unit can have both lover and spouse in vanilla with retirement shenanigans
  920. local returnstring = ""
  921. if spousename then
  922. --spouse matters more so goes first.
  923. returnstring = returnstring .. marriedSymbol
  924. if spousesite ~= df.global.ui.site_id then
  925. returnstring = returnstring .. offsite
  926. end
  927. if spouselives then
  928. returnstring = returnstring .. spousename
  929. else
  930. returnstring = returnstring .. diedS .. spousename ..diedE
  931. end
  932. end
  933. if lovername then
  934. returnstring = returnstring .. loversSymbol
  935. if loversite ~= df.global.ui.site_id then
  936. returnstring = returnstring .. offsite
  937. end
  938. if loverlives then
  939. returnstring = returnstring .. lovername
  940. else
  941. returnstring = returnstring .. diedS .. lovername ..diedE
  942. end
  943. end
  944.  
  945. return returnstring
  946. end
  947.  
  948. function getSuitableMatches(unit, unitlist, joblist)
  949. --Takes a local unit, local unitlist and local joblist
  950. --Returns an unitlist that includes unit and then all it's suitable candidates.
  951. --And joblist that has only those same indices, as unitlist viewscreen uses that data.
  952. --utilizes areCompatible
  953. local matchlist, jobmatchlist = {}, {}
  954. --The unit we've checking always comes first
  955. for index=0, #unitlist-1 do
  956. if (unit == unitlist[index]) or -- self
  957. areCompatible(unit, unitlist[index]) then --suitable match
  958. matchlist[index] = true
  959. jobmatchlist[index] = true
  960. end
  961. end
  962. return matchlist, jobmatchlist
  963. end
  964.  
  965. -- ======================================== --
  966. -- Dynamic text {} output functions --
  967. -- ======================================== --
  968.  
  969.  
  970. function getViewUnitPairs(unit)
  971. local returnpairs = {}
  972. local pregnantRichText = {}
  973. pregnantRichText.text = getPregnancyText(unit)
  974. --bit of fluff for pregnancy
  975. pregnantRichText.color = pregnantColor
  976. table.insert(returnpairs, pregnantRichText)
  977. --First line is for pregnancy, second line is for spouse/lover
  978. local spouseRichText = {}
  979. --Also gets lover text
  980. spouseRichText.text = getSpouseText(unit)
  981. function nabNonPregnantColor(unit)
  982. --I want spouse text to be coloured appropriately for the relationship.
  983. local previouscolor, returncolor
  984. local basecolor = getSymbolizedSpouse(unit).color
  985. if type(basecolor) == "number" then
  986. return basecolor
  987. else
  988. previouscolor = getSymbolizedSpouse(unit).color()
  989. function returnfunction()
  990. --In case the romantic relationship is blinky - typically that means pregnancy and/or lover.
  991. local unit = unit
  992. returncolor = getSymbolizedSpouse(unit).color()
  993. --Might as well use code already in place
  994. if returncolor == pregnantColor then
  995. --Of course, if the unit is pregnant, that's shown above, not here.
  996. --Visual bug: Can still start out as pregnant color.
  997. return previouscolor
  998. else
  999. previouscolor = returncolor
  1000. return returncolor
  1001. end
  1002. end
  1003. return returnfunction
  1004. end
  1005. end
  1006.  
  1007. if spouseRichText.text then
  1008. spouseRichText.color = nabNonPregnantColor(unit)
  1009. end
  1010. table.insert(returnpairs, spouseRichText)
  1011. return returnpairs
  1012. end
  1013.  
  1014. local HavePrinted, oldCitizens, oldJobs, oldIndices = false
  1015. function showUnitPairs(unit)
  1016. local unitscreen = getBottomMostViewscreenWithFocus("unitlist", df.global.gview.view.child.child)
  1017. if not HavePrinted then
  1018. local unittable,jobstable = getSuitableMatches(unit, unitscreen.units.Citizens,unitscreen.jobs.Citizens)
  1019. oldCitizens, oldJobs, oldIndices = {}, {}, {}
  1020. local index = 0
  1021. while getFlag(unitscreen.units.Citizens, index) do
  1022. if (unit == unitscreen.units.Citizens[index]) or -- self
  1023. areCompatible(unit, unitscreen.units.Citizens[index]) then
  1024. index = 1+index
  1025. else
  1026. table.insert(oldCitizens, unitscreen.units.Citizens[index])
  1027. table.insert(oldIndices, index)
  1028. oldJobs[#oldIndices] = unitscreen.jobs.Citizens[index]
  1029. unitscreen.units.Citizens:erase(index)
  1030. unitscreen.jobs.Citizens:erase(index)
  1031. end
  1032. end
  1033. HavePrinted = true
  1034. for ci = 0, #unitscreen.units.Citizens -1 do
  1035. if (unit == unitscreen.units.Citizens[ci]) then
  1036. unitscreen.cursor_pos.Citizens = ci
  1037. break;
  1038. end
  1039. end
  1040. end
  1041. end
  1042.  
  1043. function hideUnitPairs()
  1044. if HavePrinted then
  1045. local unitscreen = getBottomMostViewscreenWithFocus("unitlist", df.global.gview.view.child.child)
  1046. for i=#oldCitizens, 1, -1 do
  1047. unitscreen.units.Citizens:insert(oldIndices[i], oldCitizens[i])
  1048. unitscreen.jobs.Citizens:insert(oldIndices[i], oldJobs[i])
  1049. end
  1050. HavePrinted = false
  1051. end
  1052. end
  1053.  
  1054.  
  1055. local pagelength, currentpage, visitorpopupdims, symbolizedList = df.global.gps.dimy - 9, 0, {x = -30, y = 4}
  1056. -- pagelength needs to be exposed due manipulators having two lines shorter pages than standard view.
  1057. -- Also due resizing.
  1058. -- currentpage needs to be exposed due traversing the lists.
  1059. function getUnitListPairs()
  1060. local unitscreen = getBottomMostViewscreenWithFocus("unitlist", df.global.gview.view.child.child)
  1061. --note: Counts from 0, unlike lua tables
  1062. local returntable = {}
  1063. local cursorposition, unitlist, iter
  1064. if unitscreen.page == 0 then --Citizen list
  1065. cursorposition = unitscreen.cursor_pos.Citizens
  1066. currentpage = math.floor(cursorposition / pagelength)
  1067. cursorposition = cursorposition % pagelength --cursor position within a page
  1068. unitlist = unitscreen.units.Citizens
  1069. for iter = (0+currentpage*pagelength),
  1070. ( (((1+currentpage)*pagelength-1)<(#unitlist -1)) and
  1071. ((1+currentpage)*pagelength-1) or
  1072. (#unitlist -1)) do
  1073. table.insert(returntable, getSymbolizedSpouse(unitlist[iter]))
  1074. returntable[#returntable].onclick = function()
  1075. local tile = nil
  1076. if dfhack.gui.getCurFocus():find("unitlist") then tile = dfhack.screen.readTile(39,df.global.gps.dimy-2) end
  1077. --search plugin support
  1078. if not tile or (tile and tile.ch == 95 and tile.fg == 2 or tile.ch == 0) then
  1079. if HavePrinted then hideUnitPairs() else
  1080. showUnitPairs(unitlist[iter]) end
  1081. writeoverTable(symbolizedList, getUnitListPairs())
  1082. end
  1083. end
  1084. end
  1085. elseif unitscreen.page == 1 or unitscreen.page == 2 then --Livestock or Others
  1086. local pageName = (unitscreen.page == 1) and "Livestock" or "Others"
  1087. cursorposition = unitscreen.cursor_pos[pageName]
  1088. currentpage = math.floor(cursorposition / pagelength)
  1089. cursorposition = cursorposition % pagelength --cursor position within a page
  1090. unitlist = unitscreen.units[pageName]
  1091. for iter = (0+currentpage*pagelength),
  1092. ( (((1+currentpage)*pagelength-1)<(#unitlist -1)) and
  1093. ((1+currentpage)*pagelength-1) or
  1094. (#unitlist -1)) do
  1095. local unit = unitlist[iter]
  1096. -- What goes on with combination of pet and intelligent?
  1097. -- Tests reveals failure to bear children and love for even histfigged intelligent dogs
  1098. -- Perhaps only dumb pets of non-your civ can screw.
  1099. -- Of course, might want accurate indicator then anyway, as you might make them your civ members
  1100.  
  1101. --Near as I can tell, for historical figures:
  1102. -- pet 1 0
  1103. -- smart 1 - Fertile
  1104. -- 0 Fer Fertile(trogs, trolls)
  1105.  
  1106. if isItSmart(unit) then
  1107. if (df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET or
  1108. df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET_EXOTIC)
  1109. -- Intelligent pets seem unable to breed
  1110. or unit.hist_figure_id < 0 then
  1111. --I think marriage requires being historical,
  1112. -- collaborated by historical turkeys being able to marry during retirement
  1113. table.insert(returntable,getGenderInInfertileColor(unit))
  1114. end
  1115. if unit.hist_figure_id > -1 then
  1116. table.insert(returntable, getSymbolizedSpouse(unit))
  1117. returntable[#returntable].onclick = function()
  1118. --Something to display visitor dating pool
  1119. --creates several tables per call, but one doesn't usually call it.
  1120. local fortressmatches = getSuitableMatches(unit, unitscreen.units.Citizens, unitscreen.jobs.Others)
  1121. --Lets find the candidates for a given visitor
  1122. -- this is a table of index, true values, not units.
  1123. print("entered onclick " .. getLen(fortressmatches))
  1124. if getLen(fortressmatches) > 0 then
  1125. local fortressmatchlist = {}
  1126. for index, value in pairs(fortressmatches) do
  1127. table.insert(fortressmatchlist, getSymbolizedSpouse(unitscreen.units.Citizens[index]))
  1128. fortressmatchlist[#fortressmatchlist].notEndOfLine = true
  1129. table.insert(fortressmatchlist, {text = dfhack.TranslateName(unitscreen.units.Citizens[index].name)})
  1130. --Lets convert the unit to nicely colored name to display.
  1131. end
  1132. print(#fortressmatchlist)
  1133. local popupscreen = screenconstructor.getScreen(
  1134. fortressmatchlist,
  1135. {x = math.floor((df.global.gps.dimx-screenconstructor.getLongestLength(fortressmatchlist,"text"))/2),
  1136. y = math.floor((df.global.gps.dimy-screenconstructor.getHeight(fortressmatchlist))/2)},
  1137. {x = math.floor((df.global.gps.dimx-screenconstructor.getLongestLength(fortressmatchlist,"text"))/2 -1),
  1138. y = math.floor((df.global.gps.dimy-screenconstructor.getHeight(fortressmatchlist))/2 -1),
  1139. width = 2+ screenconstructor.getLongestLength(fortressmatchlist,"text"),
  1140. height = 2+ screenconstructor.getHeight(fortressmatchlist)}
  1141. )
  1142. popupscreen:show()
  1143. popupscreen.onInput = function() popupscreen:dismiss() end
  1144. --There's no input on which I wont want to dismiss the screen.
  1145. end
  1146. end
  1147. end
  1148. else
  1149. --It doesn't matter if a pet/troglodyte has killed someone or not, they'll breed either way.
  1150. if unit.hist_figure_id > -1 then
  1151. --Nonetheless, historical pets can marry in retired forts
  1152. table.insert(returntable, getSymbolizedSpouse(unit))
  1153. --getSymbolizedSpouse calls on below functions for not smart creatures
  1154. else
  1155. table.insert(returntable, getSymbolizedAnimalPreference(unit))
  1156. end
  1157. end
  1158.  
  1159. --[[ Deprecated logic based on previously believed data.
  1160. if unit.hist_figure_id > 0 then
  1161. --Can be married. Visitor, murderer, pet...
  1162. if isItSmart(unit) and
  1163. (df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET or
  1164. df.global.world.raws.creatures.all[unit.race].caste[0].flags.PET_EXOTIC) then
  1165. --
  1166. table.insert(returntable,getGenderInInfertileColor(unit))
  1167.  
  1168. end
  1169. --Distinction between being smart or not (aka free love) is handled inside.
  1170. table.insert(returntable, getSymbolizedSpouse(unit))
  1171. else
  1172. if isItSmart(unit) then
  1173. --Wild animal men and non-historical gremlins
  1174. --Infertile until they're able to marry - like after having killed someone important
  1175. table.insert(returntable,getGenderInInfertileColor(unit))
  1176. else
  1177. --normal turkeys brought on embark and wild animals.
  1178. table.insert(returntable, getSymbolizedAnimalPreference(unit))
  1179.  
  1180. end
  1181. end]]--
  1182. end
  1183.  
  1184. end
  1185. return returntable
  1186. end
  1187.  
  1188.  
  1189. -- ======================================== --
  1190. -- Initialization and placement --
  1191. -- ======================================== --
  1192.  
  1193. local unitscreen = getBottomMostViewscreenWithFocus("unitlist",df.global.gview.view.child.child)
  1194. --gets unitlist viewscreen
  1195. local viewscreen
  1196. if unitscreen then
  1197. symbolizedList = getUnitListPairs()
  1198. local list_rect = {x = 1, y = 4, width = 1, height = pagelength}
  1199. local newscreen = screenconstructor.getScreen(symbolizedList, list_rect, nil)
  1200. newscreen:show()
  1201. local listtable = {}
  1202. listtable[0] = "Citizens"
  1203. listtable[1] = "Livestock"
  1204. listtable[2] = "Others"
  1205. listtable[3] = "Dead"
  1206. local oldwhichlist,lenlist, doNotUseBase, manitimeout, manicursorpos = listtable[unitscreen.page]
  1207.  
  1208. local manipulatorkeytable = {}
  1209. local upkeys = {CURSOR_UP = true}
  1210. local downkeys = {CURSOR_DOWN = true}
  1211. newscreen.onclick = function()
  1212. local tile = nil
  1213. if dfhack.gui.getCurFocus():find("unitlist") then tile = dfhack.screen.readTile(39,df.global.gps.dimy-2) end
  1214. --search plugin support
  1215. --if prevents failing to work in manipulator/main
  1216. if not tile or (tile and tile.ch == 95 and tile.fg == 2 or tile.ch == 0) then
  1217. if HavePrinted then hideUnitPairs() end --restoring units on random clicks
  1218. writeoverTable(symbolizedList, getUnitListPairs()) --restoring appearance too
  1219. end
  1220. end
  1221. local baseonInput = newscreen.onInput
  1222. local function onListInput(self, keys)
  1223. lenlist = #(unitscreen.units[oldwhichlist])
  1224.  
  1225. if keys.LEAVESCREEN and 2 == dfhack.screen.readTile(29,df.global.gps.dimy-2).fg then
  1226. --Search plugin support. Tbh, would have been ultimately easier to disable dismiss_on_zoom
  1227. local dismissfunc = self.dismiss
  1228. self.dismiss = function () end
  1229. dfhack.timeout(1, "frames", function() self.dismiss = dismissfunc end )
  1230. end
  1231.  
  1232. if not doNotUseBase then baseonInput(self, keys) end
  1233. local manipulatorscript = getBottomMostViewscreenWithFocus("dfhack/lua/manipulator",df.global.gview.view.child.child)
  1234. --Lua manipulator viewscreen is present
  1235. local whichlist = listtable[unitscreen.page]
  1236. --duplicated from indicator_screen (ugh). Feels like there should be a way to better determine this.
  1237.  
  1238. if (currentpage ~= math.floor(unitscreen.cursor_pos[whichlist]/ pagelength) and whichlist ~= "Dead") or
  1239. --up-down paging
  1240. (oldwhichlist and oldwhichlist ~= whichlist) or --left-right paging
  1241. (lenlist ~= #(unitscreen.units[oldwhichlist])) then --search plugin support
  1242. oldwhichlist = whichlist
  1243. doNotUseBase = true
  1244. lenlist = #(unitscreen.units[oldwhichlist])
  1245. writeoverTable(symbolizedList, getUnitListPairs())
  1246. dfhack.timeout(2, "frames", function() doNotUseBase = false end)
  1247. --Something weird happens with writeoverTable here, where it sometimes parses input twice.
  1248. --In the absence of other solutions, merely avoid relaying input for two frames.
  1249. end
  1250.  
  1251. function mv_cursor(keys)
  1252. -- Function for the purpose of moving cursor alongside the manipulator.
  1253. -- They don't do this natively - a bugging disrepacy.
  1254. if keys.CURSOR_UP or keys.CURSOR_DOWN then
  1255. unitscreen.cursor_pos[whichlist] = --set new cursor position
  1256. (unitscreen.cursor_pos[whichlist] +(keys.CURSOR_DOWN and 1 or -1)) --to 1 up or down from previous
  1257. % #(unitscreen.units[whichlist]) --with overflow accounted for.
  1258. end
  1259. end
  1260.  
  1261. --manipulator/main.lua scrolls differently than default interface, got to handle it
  1262. --TODO: fix the mess with numpad keys and manipulator/main.lua
  1263. if manipulatorscript and manipulatorscript.breakdown_level ~= 2 then
  1264. --Finds manipulator here on both Alt-L and Escape back out
  1265. --breakdown level check prevents that.
  1266. if #(unitscreen.units[whichlist]) > (df.global.gps.dimy -11) then
  1267. --multi-page manipulator unitlist handling
  1268. if pagelength ~= lenlist then
  1269. --Instead of using a sublist that is refreshed manipulator/main uses whole list that is moved
  1270. pagelength = lenlist
  1271. writeoverTable(symbolizedList, getUnitListPairs())
  1272. self:adjustDims(true,nil,nil,nil, pagelength)
  1273. self.frame_body.clip_y2 = df.global.gps.dimy - 8
  1274. manicursorpos = unitscreen.cursor_pos[whichlist] > (df.global.gps.dimy - 12) and
  1275. (df.global.gps.dimy - 12) or unitscreen.cursor_pos[whichlist]
  1276. self.frame_body.y1 = 4 - (unitscreen.cursor_pos[whichlist] > (df.global.gps.dimy - 12) and
  1277. unitscreen.cursor_pos[whichlist] - (df.global.gps.dimy - 12) or
  1278. 0)
  1279. end
  1280. --scrolling occurs if manipulator's cursor position has reached the edge and tries to keep going.
  1281. --Manipulator's initial cursor position is either current cursor position or bottom, whichever is smaller
  1282. --Successive changes can divorce the two, so need to have internal check.
  1283.  
  1284. --Two functions to follow the cursor position of manipulator
  1285. function manidown()
  1286. if manicursorpos ~= (df.global.gps.dimy - 12) then
  1287. manicursorpos = 1 + manicursorpos
  1288. elseif manicursorpos == (df.global.gps.dimy - 12) then
  1289. if (unitscreen.cursor_pos[whichlist] +1 ) ~= #(unitscreen.units[whichlist]) then
  1290. self.frame_body.y1 = -1 + self.frame_body.y1
  1291. else
  1292. self.frame_body.y1 = 4
  1293. manicursorpos = 0
  1294. end
  1295. end
  1296. end
  1297. function maniup()
  1298. if manicursorpos ~= 0 then
  1299. manicursorpos = -1 + manicursorpos
  1300. elseif manicursorpos == 0 then
  1301. if unitscreen.cursor_pos[whichlist] ~= 0 then
  1302. self.frame_body.y1 = 1 + self.frame_body.y1
  1303. else
  1304. self.frame_body.y1 = (df.global.gps.dimy -7) - #(unitscreen.units[whichlist])
  1305. manicursorpos = (df.global.gps.dimy - 12)
  1306. end
  1307. end
  1308. end
  1309.  
  1310. --manipulator/main allows shift+up/down scrolling, which has unique behaviour
  1311. if hasKeyOverlap(keys, downkeys) then
  1312. if not hasKeyOverlap(keys, {["_FAST"] = true}) then
  1313. manidown()
  1314. else
  1315. for i=1, 10 do
  1316. if (unitscreen.cursor_pos[whichlist] +1 ) ~= #(unitscreen.units[whichlist]) then
  1317. manidown()
  1318. mv_cursor(downkeys)
  1319. else
  1320. if i == 1 then
  1321. manidown()
  1322. mv_cursor(downkeys)
  1323. end
  1324. break;
  1325. end
  1326. end
  1327. end
  1328. end
  1329. if hasKeyOverlap(keys, upkeys) then
  1330. if not hasKeyOverlap(keys, {["_FAST"] = true}) then
  1331. maniup()
  1332. else
  1333. for i=1, 10 do
  1334. if unitscreen.cursor_pos[whichlist] ~= 0 then
  1335. maniup()
  1336. mv_cursor(upkeys)
  1337. else
  1338. if i == 1 then
  1339. maniup()
  1340. mv_cursor(upkeys)
  1341. end
  1342. break;
  1343. end
  1344. end
  1345. end
  1346. end
  1347. end
  1348. mv_cursor(keys) --adjust outside cursor, does nothing on shift-scrolling
  1349. if df.global.gps.mouse_x == 1 and --clicked on the indicator line
  1350. (keys._MOUSE_L or keys._MOUSE_L_DOWN) then
  1351. if manipulatorscript and not manitimeout then
  1352. manitimeout = true
  1353. --timeout necessary due otherwise causing errors with multiple rapid commands
  1354. self._native.parent.breakdown_level = 2
  1355. self._native.parent.parent.child = self._native.parent.child
  1356. self._native.parent = self._native.parent.parent
  1357. dfhack.timeout(2, "frames", function()
  1358. dfhack.run_command("gui/indicator_screen execute_hook manipulator/main")
  1359. end)
  1360. dfhack.timeout(2, "frames", function() manitimeout = false end)
  1361. end
  1362. end
  1363. elseif manipulatorscript and manipulatorscript.breakdown_level == 2 then
  1364. if keys.LEAVESCREEN then
  1365. hideUnitPairs()
  1366. dfhack.run_command("relations-indicator")
  1367. end
  1368. if keys.UNITJOB_ZOOM_CRE then
  1369. dfhack.timeout(4, "frames", function() dfhack.run_command("relations-indicator") end)
  1370. end
  1371. end
  1372. end
  1373. newscreen.onInput = onListInput
  1374.  
  1375. local baseOnResize = newscreen.onResize
  1376.  
  1377. function onListResize(self)
  1378. -- Unlike with View-unit, the data might change depending on the size of the window.
  1379. baseOnResize(self)
  1380. if pagelength ~= (df.global.gps.dimy - 9) then
  1381. --If window length changed, better refresh the data.
  1382. pagelength = df.global.gps.dimy - 9
  1383. writeoverTable(symbolizedList, getUnitListPairs())
  1384. self:adjustDims(true,list_rect.x, list_rect.y,list_rect.width, pagelength)
  1385. --Not adjusting height here would result in situation where making screen shorter works, but taller not.
  1386. end
  1387. end
  1388.  
  1389. newscreen.onResize = onListResize
  1390.  
  1391. -- ======================================== --
  1392. -- View-unit section --
  1393. -- ======================================== --
  1394.  
  1395.  
  1396. else
  1397. local function viewunit()
  1398. viewscreen = dfhack.gui.getCurViewscreen()
  1399. local unit = dfhack.gui.getSelectedUnit()
  1400. local symbolizedSingle = getViewUnitPairs(unit)
  1401. local view_rect = {x = (-30 -(screenconstructor.isWideView() and 24 or 0)), y = 17, width = 28, height = 2}
  1402. local newscreen = screenconstructor.getScreen(symbolizedSingle, view_rect, nil)
  1403. newscreen:show()
  1404. if not dfhack.gui.getFocusString(viewscreen)
  1405. :find("dwarfmode/ViewUnits/Some/General") then
  1406. --Can enter in inventory view with v if one previously exited inventory view
  1407. newscreen:removeFromView() --Gotta hide in that case
  1408. end
  1409. local baseonInput = newscreen.onInput
  1410. local function onViewInput(self, keys)
  1411. --handling changing the menu width and units:
  1412. --Capturing the state before it changes:
  1413. local oldUnit, oldScreen
  1414. local sameUnit, sameScreen = true, true
  1415. --Tab changing menu width is handled below.
  1416. --storing pre-keypress identifiers for unit and viewscreen state
  1417. if not keys.CHANGETAB then
  1418. oldUnit = df.global.ui_selected_unit
  1419. oldScreen = dfhack.gui.getFocusString(viewscreen)
  1420. --Merely checking viewscreen match will not work, given that sideview only modifies existing screen
  1421. end
  1422.  
  1423. baseonInput(self,keys) --Doing baseline housekeeping and passing input to parent
  1424.  
  1425. --Finding out if anything changed after parent got the input
  1426. if not keys.CHANGETAB then
  1427. sameUnit = (oldUnit == df.global.ui_selected_unit)
  1428. --could also use dfhack.gui.getSelectedUnit()
  1429. sameScreen = (oldScreen == dfhack.gui.getFocusString(viewscreen))
  1430. end
  1431.  
  1432. if keys.CHANGETAB then
  1433. --Tabbing moves around the sideview, so got to readjust screen position.
  1434. view_rect.x = -30 -(screenconstructor.isWideView() and 24 or 0)
  1435. self:adjustDims(true, view_rect.x)
  1436. --unlike text tables, position tables aren't dynamic, to allow them to be incomplete
  1437. end
  1438.  
  1439. --If unit changed, got to replace the indicator text
  1440. if not sameUnit then
  1441. writeoverTable(symbolizedSingle,getViewUnitPairs(df.global.world.units.active[df.global.ui_selected_unit]))
  1442. elseif not sameScreen then
  1443. --Don't want to display the screen if there isn't an unit present, but don't want to spam blinks either
  1444. if not dfhack.gui.getFocusString(viewscreen)
  1445. :find("dwarfmode/ViewUnits/Some/General") then
  1446. --Different screen doesn't mean it's different in same way - need to check here too.
  1447. self:removeFromView()
  1448. else
  1449. --It's general, so better fix it...Thoug well - should change mostly nothing
  1450. self:adjustDims(true, view_rect.x, view_rect.y, view_rect.width, view_rect.height)
  1451. self.signature = true
  1452. end
  1453. end
  1454.  
  1455. end
  1456.  
  1457. newscreen.onInput = onViewInput
  1458.  
  1459. end
  1460. if dfhack.gui.getCurFocus():find("dwarfmode/ViewUnits/Some")
  1461. then viewunit()
  1462. else dfhack.timeout(2, "frames", function()
  1463. if getBottomMostViewscreenWithFocus("dwarfmode/ViewUnits/Some", df.global.gview.view.child) then
  1464. viewunit()
  1465. end
  1466. end)
  1467. end
  1468. end
Advertisement
RAW Paste Data Copied
Advertisement