From 596ab0e1b85dff4107a65847247d4b7ec1eafa12 Mon Sep 17 00:00:00 2001 From: expwnent Date: Thu, 26 Jun 2014 08:36:57 -0400 Subject: [PATCH] Added many forum scripts, and a makeshift onReport/onStrike event that will be merged into eventful/EventManager later. --- plugins/lua/onReport.lua | 74 +++ plugins/lua/onStrike.lua | 255 ++++++++ plugins/lua/repeatUtil.lua | 34 ++ plugins/lua/syndromeUtil.lua | 154 +++++ scripts/ShowUnitSyndromes.rb | 1013 +++++++++++++++++++++++++++++++ scripts/Spawnunit.lua | 260 ++++++++ scripts/blooddel.lua | 48 ++ scripts/feeding-timers.lua | 37 ++ scripts/force.lua | 84 +++ scripts/forumdwarves.lua | 125 ++++ scripts/fullheal.lua | 127 ++++ scripts/growthbug.lua | 26 + scripts/hackWish.lua | 251 ++++++++ scripts/itemsyndrome.lua | 223 +++++++ scripts/moddableGods.lua | 152 +++++ scripts/onReportExample.lua | 19 + scripts/onStrikeExample.lua | 16 + scripts/printArgs.lua | 9 + scripts/projectileExpansion.lua | 190 ++++++ scripts/removewear.lua | 56 ++ scripts/repeat.lua | 73 +++ scripts/shapechange.lua | 97 +++ scripts/skillChange.lua | 47 ++ scripts/skillroll.lua | 60 ++ scripts/teleport.lua | 36 ++ scripts/unit-info-viewer.lua | 793 ++++++++++++++++++++++++ 26 files changed, 4259 insertions(+) create mode 100644 plugins/lua/onReport.lua create mode 100644 plugins/lua/onStrike.lua create mode 100644 plugins/lua/repeatUtil.lua create mode 100644 plugins/lua/syndromeUtil.lua create mode 100644 scripts/ShowUnitSyndromes.rb create mode 100644 scripts/Spawnunit.lua create mode 100644 scripts/blooddel.lua create mode 100644 scripts/feeding-timers.lua create mode 100644 scripts/force.lua create mode 100644 scripts/forumdwarves.lua create mode 100644 scripts/fullheal.lua create mode 100644 scripts/growthbug.lua create mode 100644 scripts/hackWish.lua create mode 100644 scripts/itemsyndrome.lua create mode 100644 scripts/moddableGods.lua create mode 100644 scripts/onReportExample.lua create mode 100644 scripts/onStrikeExample.lua create mode 100644 scripts/printArgs.lua create mode 100644 scripts/projectileExpansion.lua create mode 100644 scripts/removewear.lua create mode 100644 scripts/repeat.lua create mode 100644 scripts/shapechange.lua create mode 100644 scripts/skillChange.lua create mode 100644 scripts/skillroll.lua create mode 100644 scripts/teleport.lua create mode 100644 scripts/unit-info-viewer.lua diff --git a/plugins/lua/onReport.lua b/plugins/lua/onReport.lua new file mode 100644 index 000000000..713c32388 --- /dev/null +++ b/plugins/lua/onReport.lua @@ -0,0 +1,74 @@ +--onReport.lua +--author expwnent +--contains the "ON_REPORT" event: triggered when there is a new report in df.global.world.status.reports + +--example +--local onReport = require 'plugins.onReport' +--onReport.triggers.someName = function (reportId) +-- --do stuff with that id +--end + +local _ENV = mkmodule('onReport') +local utils = require 'utils' +local repeatUtil = require 'plugins.repeatUtil' + +lastReport = lastReport or -1 +triggers = triggers or {} + +monitorFrequency = monitorFrequency or nil +eventToDwarf = eventToDwarf or {} + +function updateEventToDwarf(reportId) + if not eventToDwarf[reportId] then + eventToDwarf[reportId] = {} + end + for _,unit in ipairs(df.global.world.units.all) do + for _,reportType in ipairs(unit.reports.log) do + for _,report in ipairs(reportType) do + if report == reportId then + eventToDwarf[reportId][unit.id] = true + end + end + end + end +end + +function monitor() + local reports = df.global.world.status.reports + if df.global.world.status.next_report_id-1 <= lastReport then + return + end +-- if #reports == 0 or reports[#reports-1].id <= lastReport then +-- return +-- end + _,_,start = utils.binsearch(reports,lastReport,"id") + while start < #reports and reports[start].id <= lastReport do + start = start+1 + end + for i=start,#reports-1,1 do + updateEventToDwarf(reports[i].id) + for _,callBack in pairs(triggers) do + callBack(reports[i].id) + end + lastReport = reports[i].id + end +end + +monitorEvery = function(n) + if n <= 0 then + print('cannot monitor onReport every '..n..' ticks.') + return + end + if monitorFrequency and monitorFrequency < n then + print('NOT decreasing frequency of onReport monitoring from every '..monitorFrequency..' ticks to every '..n..' ticks') + return + end + print('monitor onReport every '..n..' ticks') + monitorFrequency = n + repeatUtil.scheduleEvery('onReportMonitoring', n, 'ticks', monitor) +end + +monitorEvery(1) + +return _ENV + diff --git a/plugins/lua/onStrike.lua b/plugins/lua/onStrike.lua new file mode 100644 index 000000000..33f9dffed --- /dev/null +++ b/plugins/lua/onStrike.lua @@ -0,0 +1,255 @@ + +local _ENV = mkmodule('onStrike') + +local onReport = require 'plugins.onReport' +local utils = require 'utils' + +debug = debug or true + +triggers = triggers or {} + +function getReportString(reportId) + local report = df.report.find(reportId) + local result = report.text + local i = 1 + local report2 = df.report.find(reportId+i) + while report2 and report2.flags.continuation do + result = result .. ' ' .. report2.text + i = i+1 + report2 = df.report.find(reportId+i) + end + return result +end + +onReport.triggers.onStrike = function(reportId) + local report = df.report.find(reportId) + if report["type"] ~= df.announcement_type.COMBAT_STRIKE_DETAILS then + return + end + if report.flags.continuation then + return + end +-- print('\n') + local fighters = {} + for unitId,_ in pairs(onReport.eventToDwarf[reportId]) do + table.insert(fighters,unitId) + end + local reportString = getReportString(reportId) + if #fighters ~= 2 then + if debug then + local ok = string.find(reportString,' skids along ') + if not ok then + print('onStrike: #fighters = ' .. #fighters) + print(reportString) + df.global.pause_state = true + end + end + return + end + local info = {} + local count = 0 + fighters[1] = df.unit.find(fighters[1]) + fighters[2] = df.unit.find(fighters[2]) + local function getWound(fighterA,fighterB) + local wound + for i=#fighterB.body.wounds-1,0,-1 do + local w = fighterB.body.wounds[i] + if w.unit_id == fighterA.id and w.age <= 1 then + wound = w + break + end + end + --[[name,_ = tryParse(reportString,getNames(fighterA)) + if not name then + wound = nil + end + --]] + return wound + end + local wound1 = getWound(fighters[1],fighters[2]) + local wound2 = getWound(fighters[2],fighters[1]) + local flying = string.find(reportString,'The flying') + local name1 + local name2 + if flying then + name1 = findAny(reportString,getNames(fighters[2])) + name2 = findAny(reportString,getNames(fighters[1])) + else + name1 = tryParse(reportString,getNames(fighters[1])) + name2 = tryParse(reportString,getNames(fighters[2])) + end + if name1 and wound1 and name2 and wound2 then + if debug then + print('ambiguous wounds: ' .. reportString) + print('fighter1 = ' .. fighters[1].id) + print('fighter2 = ' .. fighters[2].id) + df.global.pause_state = true + end + return + elseif not wound1 and not wound2 then + local ok = fighters[1].flags1.dead or fighters[2].flags1.dead or string.find(reportString,' grabs ') or string.find(reportString,'snatches at') or string.find(reportString,'glances away!') or string.find(reportString, ' shakes ') + if not ok and debug then + print('neither wound works: ' .. reportString) + print('fighter1 = ' .. fighters[1].id) + print('fighter2 = ' .. fighters[2].id) + df.global.pause_state = true + end + return + elseif not name1 and not name2 and not string.find(reportString,'The flying ') then + if debug then + print('WTF?') + print('fighter1 = ' .. fighters[1].id) + print('fighter2 = ' .. fighters[2].id) + df.global.pause_state = true + end + return + elseif name1 and wound1 then + else + local temp = fighters[1] + fighters[1] = fighters[2] + fighters[2] = temp + end + local wound = wound1 or wound2 + + --is it a weapon attack? + local isWeaponAttack + if getWeapon(fighters[1]) and string.find(reportString,getWeaponString(fighters[1])) then + isWeaponAttack = true + else + isWeaponAttack = false + end + isWeaponAttack = isWeaponAttack or flying + local weapon + if isWeaponAttack then + weapon = getWeapon(fighters[1]) + end +-- print('triggers') + for _,trigger in pairs(triggers) do +-- print('trigger') + trigger(fighters[1],fighters[2],weapon,wound) + end +end + +function myConcat(table1,table2) + local result = {} + for _,v in pairs(table1) do + table.insert(result,v) + end + for _,v in pairs(table2) do + table.insert(result,v) + end + return result +end + +function getUnitAttackStrings(unit) + local result = {} + for _,attack in ipairs(unit.body.body_plan.attacks) do + table.insert(result,attack.verb_3rd..' ') + end + return result +end + +function getUnitAttack(unit,parsedAttack) + for _,attack in ipairs(unit.body.body_plan.attacks) do + if attack.verb_3rd..' ' == parsedAttack then + return result + end + end + return nil +end + +function getWeapon(unit) + function dumb(item) +-- print('\n') +-- print(item) +-- printall(item) + if item.mode ~= df.unit_inventory_item.T_mode.Weapon then + --print('item.mode ' .. item.mode .. ' /= Weapon ' .. df.unit_inventory_item.T_mode.Weapon) + return false + end + if item.item._type ~= df.item_weaponst then + --print('item.item._type ' .. item.item._type .. ' /= df.item_weaponst ' .. df.item_weaponst) + return false + end + return true + end + for _,item in ipairs(unit.inventory) do + if dumb(item) then + return item.item + end + end + return nil +end + +function getWeaponAttackStrings(unit) + local result = {} + local weapon = getWeapon(unit) + if not weapon then + print('no weapon') + return result + end + for _,attack in ipairs(weapon.subtype.attacks) do + table.insert(result,attack.verb_3rd..' ') + end + return result +end + +function getWeaponAttack(unit,parsedAttack) + local weapon = getWeapon(unit) + if not weapon then + return nil + end + for _,attack in ipairs(weapon.subtype.attacks) do + if attack.verb_3rd..' ' == parsedAttack then + return attack + end + end + return nil +end + +function getNames(unit) + local result = {} + table.insert(result,unit.name.first_name .. ' ') + table.insert(result,'The '..dfhack.units.getProfessionName(unit)..' ') + table.insert(result,'the '..dfhack.units.getProfessionName(unit)..' ') + table.insert(result,'The Stray '..dfhack.units.getProfessionName(unit)..' ') + table.insert(result,'the stray '..dfhack.units.getProfessionName(unit)..' ') + return result +end + +function getWeaponString(unit,suffix) + local weapon = getWeapon(unit) + if not weapon then + return '' + end + local material = getMaterialString(weapon) + return material .. ' ' .. weapon.subtype.name .. (suffix or '') +end + +function getMaterialString(item) + local material = dfhack.matinfo.decode(item.mat_type,item.mat_index) + return material.material.state_name[df.matter_state.Solid] +end + +function findAny(parseString,strs) + for _,str in ipairs(strs) do + if string.find(parseString,str) then + return str + end + end + return nil +end + +function tryParse(parseString,strs) + for _,str in ipairs(strs) do + if string.sub(parseString,1,#str) == str then + --print('\n"' .. str .. '" matches "' .. parseString .. '"\n') + return str,string.sub(parseString,#str+1,#parseString) + end + --print('\n"' .. str .. '" doesn\'t match "' .. parseString .. '"\n') + end + return nil,nil +end + +return _ENV + diff --git a/plugins/lua/repeatUtil.lua b/plugins/lua/repeatUtil.lua new file mode 100644 index 000000000..c3275be44 --- /dev/null +++ b/plugins/lua/repeatUtil.lua @@ -0,0 +1,34 @@ +-- lua/plugins/repeatUtil.lua +-- author expwnent +-- vaguely based on a script by Putnam + +local _ENV = mkmodule("repeatUtil") + +repeating = repeating or {} + +dfhack.onStateChange.repeatUtilStateChange = function(code) + if code == SC_WORLD_UNLOADED then + repeating = {} + end +end + +function cancel(name) + if not repeating[name] then + return false + end + dfhack.timeout_active(repeating[name],nil) + repeating[name] = nil + return true +end + +function scheduleEvery(name,time,timeUnits,func) + cancel(name) + local function helper() + func() + repeating[name] = dfhack.timeout(time,timeUnits,helper) + end + helper() +end + +return _ENV + diff --git a/plugins/lua/syndromeUtil.lua b/plugins/lua/syndromeUtil.lua new file mode 100644 index 000000000..132172e46 --- /dev/null +++ b/plugins/lua/syndromeUtil.lua @@ -0,0 +1,154 @@ +--syndrome.lua +--author expwnent +--some utilities for adding syndromes to units + +local _ENV = mkmodule("syndromeUtil") +local Utils = require("utils") + +function findUnitSyndrome(unit,syn_id) + for index,syndrome in ipairs(unit.syndromes.active) do + if syndrome.type == syn_id then + return syndrome + end + end + return nil +end + +--usage: syndrome.ResetPolicy.DoNothing, syndrome.ResetPolicy.ResetDuration, etc +ResetPolicy = ResetPolicy or Utils.reverse({ + "DoNothing", + "ResetDuration", + "AddDuration", + "NewInstance" +}) + +function eraseSyndrome(unit,syndromeId,oldestFirst) + local i1 + local iN + local d + if oldestFirst then + i1 = 0 + iN = #unit.syndromes.active-1 + d = 1 + else + i1 = #unit.syndromes.active-1 + iN = 0 + d = -1 + end + local syndromes = unit.syndromes.active + for i=i1,iN,d do + if syndromes[i].type == syndromeId then + syndromes:erase(i) + return true + end + end + return false +end + +function eraseSyndromes(unit,syndromeId) + local count=0 + while eraseSyndrome(unit,syndromeId,true) do + count = count+1 + end + return count +end +--target is a df.unit, syndrome is a df.syndrome, resetPolicy is one of syndrome.ResetPolicy +--if the target has an instance of the syndrome already, the reset policy takes effect +--returns true if the unit did not have the syndrome before calling and false otherwise +function infectWithSyndrome(target,syndrome,resetPolicy) + local oldSyndrome = findUnitSyndrome(target,syndrome.id) + if oldSyndrome == nil or resetPolicy == nil or resetPolicy == ResetPolicy.NewInstance then + local unitSyndrome = df.unit_syndrome:new() + unitSyndrome.type = syndrome.id + unitSyndrome.year = df.global.cur_year + unitSyndrome.year_time = df.global.cur_year_tick + unitSyndrome.ticks = 0 + unitSyndrome.wound_id = -1 + unitSyndrome.flags.bits.active = 1 + for k,v in ipairs(syndrome.ce) do + local symptom = df.unit_syndrome.T_symptoms:new() + symptom.quantity = 0 + symptom.delay = 0 + symptom.ticks = 0 + symptom.flags.bits.active = true + unitSyndrome.symptoms:insert("#",symptom) + end + target.syndromes.active:insert("#",unitSyndrome) + elseif resetPolicy == ResetPolicy.DoNothing then + elseif resetPolicy == ResetPolicy.ResetDuration then + for k,symptom in ipairs(oldSyndrome.symptoms) do + symptom.ticks = 0 + end + oldSyndrome.ticks = 0 + elseif resetPolicy == ResetPolicy.AddDuration then + for k,symptom in ipairs(oldSyndrome.symptoms) do + --really it's syndrome.ce[k].end, but lua doesn't like that because keywords + if syndrome.ce[k]["end"] ~= -1 then + symptom.ticks = symptom.ticks - syndrome.ce[k]["end"] + end + end + else qerror("Bad reset policy: " .. resetPolicy) + end + return (oldSyndrome == nil) +end + +function isValidTarget(unit,syndrome) + --mostly copied from itemsyndrome, which is based on autoSyndrome + if + #syndrome.syn_affected_class==0 + and #syndrome.syn_affected_creature==0 + and #syndrome.syn_affected_caste==0 + and #syndrome.syn_immune_class==0 + and #syndrome.syn_immune_creature==0 + and #syndrome.syn_immune_caste==0 + then + return true + end + local affected = false + local unitRaws = df.creature_raw.find(unit.race) + local casteRaws = unitRaws.caste[unit.caste] + local unitRaceName = unitRaws.creature_id + local casteName = casteRaws.caste_id + local unitClasses = casteRaws.creature_class + for _,unitClass in ipairs(unitClasses) do + for _,syndromeClass in ipairs(syndrome.syn_affected_class) do + if unitClass.value==syndromeClass.value then + affected = true + end + end + end + for caste,creature in ipairs(syndrome.syn_affected_creature) do + local affectedCreature = creature.value + local affectedCaste = syndrome.syn_affectedCaste[caste].value + if affectedCreature == unitRaceName and (affectedCaste == casteName or affectedCaste == "ALL") then + affected = true + end + end + for _,unitClass in ipairs(unitClasses) do + for _,syndromeClass in ipairs(syndrome.syn_immune_class) do + if unitClass.value == syndromeClass.value then + affected = false + end + end + end + for caste,creature in ipairs(syndrome,syn_immune_creature) do + local immuneCreature = creature.value + local immuneCaste = syndrome.syn_immune_caste[caste].value + if immuneCreature == unitRaceName and (immuneCaste == casteName or immuneCaste == "ALL") then + affected = false + end + end + return affected +end + +function infectWithSyndromeIfValidTarget(target,syndrome,resetPolicy) + if isValidTarget(unit,syndrome) then + infectWithSyndrome(target,syndrome,resetPolicy) + return true + else + return false + end +end + +return _ENV + diff --git a/scripts/ShowUnitSyndromes.rb b/scripts/ShowUnitSyndromes.rb new file mode 100644 index 000000000..9cec79b9b --- /dev/null +++ b/scripts/ShowUnitSyndromes.rb @@ -0,0 +1,1013 @@ +# Show syndromes affecting units and the remaining and maximum duration (v6). Call with help on the command line for further options. +# original author: drayath +# edited by expwnent + +#TODO: When showing effects on a unit, show the actual change to the unit +# E.g. if +150%, +500 strength show actual total bonus based on the unit stats. +# For this also need to know +# how does size_delays affect the start/peak/end time +# how does size_dilute affect the Severity, does it also affect the phy/mental stat adjustments? +# how does peak affect the Severity, does it also affect the phy/mental stat adjustments? +#TODO: Add interaction info needs to display a bit more data, but the required structures are not yet decoded + +#TODO: Several of the unk_xxx fields have been identified here, and can get some more by comparing the raws with the printed interaction and effect information. Pass these onto the dfhack guys. + +def print_help() + puts "Use one or more of the following options:" + puts " showall: Show units even if not affected by any syndrome" + puts " showeffects: shows detailed effects of each syndrome" + puts " showdisplayeffects: show effects that only change the look of the unit" + puts " ignorehiddencurse: Hides syndromes the user should not be able to know about (TODO)" + puts " selected: Show selected unit" + puts " dwarves: Show dwarves" + puts " livestock: Show livestock" + puts " wildanimals: Show wild animals" + puts " hostile: Show hostiles (e.g. invaders, thieves, forgotten beasts etc)" + puts " world: Show all defined syndromes in the world" + puts " export: Write the output to a file instead of the console." + puts "" + puts "Will show all syndromes affecting each units with the maximum and present duration." +end + +class Output + attr_accessor :fileLogger, :indent_level + + def initialize(filename) + indent_level = "" + if filename==nil + @fileLogger = nil + else + @fileLogger = File.new(filename + ".html", "w") + @fileLogger.puts("") + end + end + + RED = "red" + GREEN = "green" + BLUE = "blue" + DEFAULT = "black" + HIGHLIGHT = "black\" size=\"+1" + + def colorize(text, color_code) + if @fileLogger == nil + return text + else + new_text = "#{text}" + if color_code == HIGHLIGHT + new_text = "" + new_text + "" + end + + return new_text + end + end + + def inactive(text) + if @fileLogger == nil + return "###" + text + else + return "#{text}" + end + end + + def indent() + if @fileLogger == nil + @indent_level = "#{@indent_level} - " + else + @fileLogger.puts("") + end + end + + def break() + if @fileLogger == nil + puts("\n") + else + @fileLogger.puts("

") + end + end + + def close() + if @fileLogger != nil + @fileLogger.puts("") + @fileLogger.flush + @fileLogger.close + @fileLogger = nil + end + end + + def log(text, color=nil) + if @fileLogger == nil + puts("#{@indent_level}#{text}") + elsif color==nil + @fileLogger.puts(text+"
") + elsif @indent_level == "" + @fileLogger.puts(colorize(text, color)) + else + @fileLogger.puts("
  • " + colorize(text, color)+"
  • ") + end + end +end + +def get_mental_att(att_index) + + case att_index + when 0 + return "Analytical Ability" + when 1 + return "Focus" + when 2 + return "Willpower" + when 3 + return "Creativity" + when 4 + return "Intuition" + when 5 + return "Patience" + when 6 + return "Memory" + when 7 + return "Linguistics" + when 8 + return "Spacial Sense" + when 9 + return "Musicality" + when 10 + return "Kinesthetic Sense" + when 11 + return "Empathy" + when 12 + return "Social Awareness" + else + return "Unknown" + end +end + +def get_physical_att(att_index) + + case att_index + when 0 + return "strength" + when 1 + return "agility" + when 2 + return "toughness" + when 3 + return "endurance" + when 4 + return "recuperation" + when 5 + return "disease resistance" + else + return "unknown" + end +end + +def get_effect_target(target) + + values = [] + + limit = target.key.length - 1 + for i in (0..limit) + + if(target.mode[i].to_s() != "") + + items = "" + + #case target.mode[i].to_s() + #when "BY_TYPE" + # item = "type(" + #when "BY_TOKEN" + # item = "token(" + #when "BY_CATEGORY" + # item = "category(" + #end + + if(target.key[i].to_s()!="") + item = "#{item}#{target.key[i].to_s().capitalize()}" + end + + if target.tissue[i].to_s() != "ALL" + if(target.key[i].to_s()!="" and target.tissue[i].to_s()!="") + item = "#{item}:" + end + + if(target.tissue[i].to_s()!="") + item = "#{item}#{target.tissue[i].to_s().capitalize()}" + end + end + + #item = item + ")" + + values.push(item) + end + + end + + if values.length == 0 or (values.length == 1 and values[0] == "All") + return "" + else + return ", target=" + values.join(", ") + end +end + +def get_att_pairs(values, percents, physical) + + items = [] + + color = Output::DEFAULT + + limit = values.length - 1 + for i in (0..limit) + if (values[i] != 0 or percents[i] != 100) + + if physical + item = "#{get_physical_att(i)}(" + else + item = "#{get_mental_att(i)}(" + end + + if(values[i]!=0) + item = item + "%+d" % values[i] + end + + if (values[i]!=0 and percents[i]!=100) + item = item + ", " + end + + if (percents[i]!=100 or values[i]==0) + item = item + "%d" % percents[i] + "%" + end + + item = item + ")" + + if color != Output::RED and values[i] >= 0 and percents[i] > 100 + color = Output::GREEN + elsif values[i] <0 || percents[i] < 100 + color = Output::RED + end + + items.push(item) + end + end + + return items.join(", "), color +end + +def get_display_name(name, verb) + if name != nil and name != "" + return name.capitalize() + end + + if verb == nil or verb == "" + return "Mystery" + end + + if verb.length > 100 + verb = verb.slice(0, 100).capitalize() + end + + pos = verb.index(".") + if pos == nil + return verb.slice(0, verb.rindex(" ")).capitalize() + else + return verb.slice(0, pos).capitalize() + end +end + +def get_interaction(interaction) + + # name, USAGE_HINT, range, wait period are probably all we really want to show. + + #result = "a=#{interaction.unk_6c} b=#{interaction.unk_7c} c=#{interaction.unk_8c} d=#{interaction.unk_a8} e=#{interaction.unk_c4} f=#{interaction.unk_e4} " + #result = result + "g=#{interaction.unk_e0} h=#{interaction.unk_e4} i=#{interaction.unk_100} j=#{interaction.unk_11c} k=#{interaction.unk_138} l=#{interaction.unk_154} " + #result = result + "m=#{interaction.unk_170} n=#{interaction.unk_18c} o=#{interaction.unk_1a8} p=#{interaction.unk_1c4} q=#{interaction.unk_1e8} r=#{interaction.unk_25c} " + #result = result + "s=#{interaction.unk_278}" + + if interaction.unk_25c == "" + name = "mystery" + else + name = interaction.unk_25c + end + + return "ability=#{get_display_name(interaction.unk_25c, interaction.unk_e4)}, delay=#{interaction.unk_278}, actionType=TODO, range=TODO, maxTargets=TODO" +end + +def get_effect_flags(flags) + + values = [] + + if(flags.SIZE_DELAYS) then values.push("size delays") end + if(flags.SIZE_DILUTES) then values.push("size dilutes") end + if(flags.VASCULAR_ONLY) then values.push("vascular only") end + if(flags.MUSCULAR_ONLY) then values.push("muscles only") end + if(flags.RESISTABLE) then values.push("resistable") end + if(flags.LOCALIZED) then values.push("localized") end + + return values.join(",") +end + +def get_tag1_flags(logger, flags, add) + + values = [] + + good = false + bad = false + + if add + good_color = Output::GREEN + bad_color = Output::RED + else + good_color = Output::RED + bad_color = Output::GREEN + end + + if(flags.EXTRAVISION) + values.push(logger.colorize("extravision", good_color)) + good = true + end + + if(flags.OPPOSED_TO_LIFE) + values.push(logger.colorize("attack the living", bad_color)) + bad = true + end + + if(flags.NOT_LIVING) + values.push(logger.colorize("undead", Output::DEFAULT)) + end + + if(flags.NOEXERT) + values.push(logger.colorize("does not tire", good_color)) + good = true + end + + if(flags.NOPAIN) + values.push(logger.colorize("does not feel pain", good_color)) + good = true + end + + if(flags.NOBREATHE) + values.push(logger.colorize("does not breathe", good_color)) + good = true + end + + if(flags.HAS_BLOOD) + values.push(logger.colorize("has blood", Output::DEFAULT)) + end + + if(flags.NOSTUN) + values.push(logger.colorize("can't be stunned", good_color)) + good = true + end + + if(flags.NONAUSEA) + values.push(logger.colorize("does not get nausea", good_color)) + good = true + end + + if(flags.NO_DIZZINESS) + values.push(logger.colorize("does not get dizzy", good_color)) + good = true + end + + if(flags.NO_FEVERS) + values.push(logger.colorize("does not get fever", good_color)) + good = true + end + + if(flags.TRANCES) + values.push(logger.colorize("can enter trance", good_color)) + good = true + end + + if(flags.NOEMOTION) + values.push(logger.colorize("feels no emotion", good_color)) + good = true + end + + if(flags.LIKES_FIGHTING) + values.push(logger.colorize("like fighting", Output::DEFAULT)) + end + + if(flags.PARALYZEIMMUNE) + values.push(logger.colorize("can't be paralyzed", good_color)) + good = true + end + if(flags.NOFEAR) + values.push(logger.colorize("does not feel fear", good_color)) + good = true + end + + if(flags.NO_EAT) + values.push(logger.colorize("does not eat", good_color)) + good = true + end + + if(flags.NO_DRINK) + values.push(logger.colorize("does not drink", good_color)) + good = true + end + + if(flags.NO_SLEEP) + values.push(logger.colorize("does not sleep", good_color)) + good = true + end + if(flags.MISCHIEVOUS) + values.push(logger.colorize("mischievous", Output::DEFAULT)) + end + + if(flags.NO_PHYS_ATT_GAIN) + values.push(logger.colorize("physical stats cant improve", good_color)) + good = true + end + + if(flags.NO_PHYS_ATT_RUST) + values.push(logger.colorize("physical stats do not rust", good_color)) + good = true + end + + if(flags.NOTHOUGHT) + values.push(logger.colorize("stupid", bad_color)) + bad = true + end + + if(flags.NO_THOUGHT_CENTER_FOR_MOVEMENT) + values.push(logger.colorize("no brain needed to move", good_color)) + good = true + end + + if(flags.CAN_SPEAK) + values.push(logger.colorize("can speak", good_color)) + good = true + end + + if(flags.CAN_LEARN) + values.push(logger.colorize("can learn", good_color)) + good = true + end + + if(flags.UTTERANCES) + values.push(logger.colorize("utterances", Output::DEFAULT)) + end + + if(flags.CRAZED) + values.push(logger.colorize("crazed", bad_color)) + bad = true + end + + if(flags.BLOODSUCKER) + values.push(logger.colorize("drinks blood", bad_color)) + bad = true + end + + if(flags.NO_CONNECTIONS_FOR_MOVEMENT) + values.push(logger.colorize("can move without nerves", good_color)) + good = true + end + + if(flags.SUPERNATURAL) + values.push(logger.colorize("supernatural", good_color)) + good = true + end + + if add + if bad + color = Output::RED + elsif good + color = Output::GREEN + else + color = Output::DEFAULT + end + else + if good + color = Output::RED + elsif bad + color = Output::GREEN + else + color = Output::DEFAULT + end + end + + return values.join(", "), color +end + +def get_tag2_flags(logger, flags, add) + values = [] + + good = false + bad = false + + if add + good_color = Output::GREEN + bad_color = Output::RED + else + good_color = Output::RED + bad_color = Output::GREEN + end + + if(flags.NO_AGING) + good = true + values.push(logger.colorize("does not age", good_color)) + end + + if(flags.MORTAL) + bad = true + values.push(logger.colorize("mortal", bad_color)) + end + + if(flags.STERILE) + values.push(logger.colorize("can't have children", Output::DEFAULT)) + end + + if(flags.FIT_FOR_ANIMATION) + values.push(logger.colorize("can be animated", Output::DEFAULT)) + end + + if(flags.FIT_FOR_RESURRECTION) + good = true + values.push(logger.colorize("can be resurrected", Output::DEFAULT)) + end + + if add + if bad + color = Output::RED + elsif good + color = Output::GREEN + else + color = Output::DEFAULT + end + else + if good + color = Output::RED + elsif bad + color = Output::GREEN + else + color = Output::DEFAULT + end + end + + return values.join(", "), color +end + +def find_creature_name(id, casteid) + creature = df.world.raws.creatures.all.find{ |c| c.creature_id == id } + + if creature == nil + return id, casteid + end + + creature_name = creature.name[0].capitalize() + + if casteid == "DEFAULT" + return creature_name, "" + end + + caste = creature.caste.find{ |c| c.caste_id == casteid } + + if caste == nil + return creature_name, casteid + elsif creature.name[0].downcase() == caste.caste_name[0].downcase() + return creature_name, "" + else + castename = caste.caste_name[0].downcase().chomp(creature.name[0].downcase()).strip() + + if castename.start_with?(creature.name[0]) + castename = castename.slice(creature.name[0].length, castename.length - creature.name[0].length).strip() + end + + if castename.start_with?("of the") + castename = castename.slice("of the".length, castename.length - "of the".length).strip() + end + + return creature_name, castename.downcase() + end +end + +def get_effect(logger, ce, ticks, showdisplayeffects) + + flags = get_effect_flags(ce.flags) + if flags != "" + flags = " (#{flags})" + end + + if ce.end == -1 + duration = " [permanent]" + elsif ce.start >= ce.peak or ce.peak <= 1 + duration = " [#{ce.start}-#{ce.end}]" + else + duration = " [#{ce.start}-#{ce.peak}-#{ce.end}]" + end + + case ce.getType().to_s() + when "PAIN" + name = "Pain" + desc = "power=#{ce.sev}#{get_effect_target(ce.target)}" + color = Output::RED + when "SWELLING" + name = "Swelling" + desc = "power=#{ce.sev}#{get_effect_target(ce.target)}" + color = Output::RED + when "OOZING" + name = "Oozing" + desc = "power=#{ce.sev}#{get_effect_target(ce.target)}" + color = Output::RED + when "BRUISING" + name = "Bruising" + desc = "power=#{ce.sev}#{get_effect_target(ce.target)}" + color = Output::RED + when "BLISTERS" + name = "Blisters" + desc = "power=#{ce.sev}#{get_effect_target(ce.target)}" + color = Output::RED + when "NUMBNESS" + name = "Numbness" + desc = "power=#{ce.sev}#{get_effect_target(ce.target)}" + color = Output::GREEN + when "PARALYSIS" + name = "Paralysis" + desc = "power=#{ce.sev}#{get_effect_target(ce.target)}" + color = Output::RED + when "FEVER" + name = "Fever" + desc = "power=#{ce.sev}" + color = Output::RED + when "BLEEDING" + name = "Bleeding" + desc = "power=#{ce.sev}#{get_effect_target(ce.target)}" + color = Output::RED + when "COUGH_BLOOD" + name = "Cough Blood" + desc = "power=#{ce.sev}" + color = Output::RED + when "VOMIT_BLOOD" + name = "Vomit Blood" + desc = "power=#{ce.sev}" + color = Output::RED + when "NAUSEA" + name = "Nausea" + desc = "power=#{ce.sev}" + color = Output::RED + when "UNCONSCIOUSNESS" + name = "Unconsciousness" + desc = "power=#{ce.sev}" + color = Output::RED + when "NECROSIS" + name = "Necrosis" + desc = "power=#{ce.sev}#{get_effect_target(ce.target)}" + color = Output::RED + when "IMPAIR_FUNCTION" + name = "Impairs" + desc = "power=#{ce.sev}#{get_effect_target(ce.target)}" + color = Output::RED + when "DROWSINESS" + name = "Drowsiness" + desc = "power=#{ce.sev}" + color = Output::RED + when "DIZZINESS" + name = "Dizziness" + desc = "power=#{ce.sev}" + color = Output::RED + when "ADD_TAG" + name = "Add" + tags1 = get_tag1_flags(logger, ce.tags1, true) + tags2 = get_tag2_flags(logger, ce.tags2, true) + desc = "#{tags1[0]},#{tags2[0]}" + + if tags1[1] == Output::RED || tags2[1] == Output::RED + color = Output::RED + elsif tags1[1] == Output::GREEN || tags2[1] == Output::GREEN + color = Output::GREEN + else + color = Output::DEFAULT + end + when "REMOVE_TAG" + name = "Remove" + tags1 = get_tag1_flags(logger, ce.tags1, true) + tags2 = get_tag2_flags(logger, ce.tags2, true) + desc = "#{tags1[0]},#{tags2[0]}" + + if tags1[1] == Output::RED || tags2[1] == Output::RED + color = Output::RED + elsif tags1[1] == Output::GREEN || tags2[1] == Output::GREEN + color = Output::GREEN + else + color = Output::DEFAULT + end + when "DISPLAY_TILE" + if !showdisplayeffects then return "", Output::DEFAULT end + name = "Tile" + desc = "Tile=#{ce.unk_6c}, Colour=#{ce.unk_70}" + color = Output::DEFAULT + when "FLASH_TILE" + if !showdisplayeffects then return "", Output::DEFAULT end + name = "Flash" + color = ce.sym_color >> 8 + tile = ce.sym_color - (color * 256) + desc = "tile = #{tile}, colour=#{color}, time=#{ce.period}, period=#{ce.time}" + color = Output::DEFAULT + when "SPEED_CHANGE" + name = "Physical" + desc = "speed(" + + value = ce.unk_6c + percent = ce.unk_70 + if(value!=0) + desc = desc + "%+d" % value + end + + if (value!=0 and percent!=100) + desc = desc + ", " + end + + if (percent!=100 or value==0) + desc = desc + "%d" % percent + "%" + end + + desc = desc + ")" + + if value < 0 or percent < 100 + color = Output::RED + elsif value >0 or percent >100 + color = Output::GREEN + else + color = Output::DEFAULT + end + + when "CAN_DO_INTERACTION" + name = "Add interaction" + desc = "#{get_interaction(ce)}" + color = Output::GREEN + when "SKILL_ROLL_ADJUST" + name = "Skill check" + desc = "modifier=#{ce.unk_6c}%, chance=#{ce.unk_70}%" + + if ce.unk_6c > 100 + color = Output::GREEN + elsif ce.unk_6c < 100 + color = Output::RED + else + color = Output::DEFAULT + end + + when "BODY_TRANSFORMATION" + name = "Transformation" + + if ce.unk_6c > 0 + chance = ", chance=#{ce.unk_6c} " + else + chance = "" + end + + creature_name = find_creature_name(ce.race_str, ce.caste_str) + + if creature_name[1] == "" + desc = "#{creature_name[0]}#{chance}" + else + desc = "#{creature_name[0]}(#{creature_name[1]})#{chance}" + end + + color = Output::BLUE + when "PHYS_ATT_CHANGE" + name = "Physical" + data = get_att_pairs(ce.phys_att_unk, ce.phys_att_perc, true) + desc = data[0] + color = data[1] + when "MENT_ATT_CHANGE" + name = "Mental" + data = get_att_pairs(ce.ment_att_unk, ce.ment_att_perc, false) + desc = data[0] + color = data[1] + when "MATERIAL_FORCE_MULTIPLIER" + name = "Material force multiplier" + desc = "received damage scaled by #{(ce.unk_c8 * 100 / ce.unk_cc * 100)/100}%" + if ce.unk_cc > ce.unk_c8 + color = Output::GREEN + elsif ce.unk_cc < ce.unk_c8 + color = Output::RED + else + color = Output::DEFAULT + end + + if ce.unk_c4 >=0 + mat = df.decode_mat(ce.unk_c0, ce.unk_c4 ) + elsif ce.unk_c0 >= 0 + mat = df.decode_mat(ce.unk_c0, 0 ) + else + mat = nil + end + + if mat!= nil + token = mat.token + if token.start_with?("INORGANIC:") + token = token.slice("INORGANIC:".length, token.length - "INORGANIC:".length) + end + + desc = "#{desc} vs #{token.capitalize()}" + end + + when "BODY_MAT_INTERACTION" + # interactionId, SundromeTriggerType + name = "Body material interaction" + desc = "a???=#{ce.unk_6c}, b???=#{ce.unk_88}, c???=#{ce.unk_8c}, d???=#{ce.unk_90}, e???=#{ce.unk_94}" + color = Output::DEFAULT + when "BODY_APPEARANCE_MODIFIER" + if !showdisplayeffects then return "", Output::DEFAULT end + # !!! seems to be missing info class !!! + # should be enum and value + name = "Body Appearance" + desc = "" + color = Output::DEFAULT + when "BP_APPEARANCE_MODIFIER" + if !showdisplayeffects then return "", Output::DEFAULT end + name = "Body part appearance" + desc = "value=#{ce.value} change_type_enum?=#{ce.unk_6c}#{get_effect_target(ce.target)}" + color = Output::DEFAULT + when "DISPLAY_NAME" + if !showdisplayeffects then return "", Output::DEFAULT end + name = "Set display name" + desc = "#{ce.name}" + color = Output::DEFAULT + else + name = "Unknown" + color = Output::HIGHLIGHT + end + + text = "#{name}#{duration}#{flags} #{desc}" + if ticks > 0 and ((ce.start > 0 and ticks < ce.start) or (ce.end > 0 and ticks > ce.end)) + text = logger.inactive(text) + end + + return text, color +end + +print_syndrome = lambda { |logger, syndrome, showeffects, showdisplayeffects| + rawsyndrome = df.world.raws.syndromes.all[syndrome.type] + duration = rawsyndrome.ce.minmax_by{ |ce| ce.end } + + if duration[0].end == -1 + durationStr = "permanent" + else + if duration[0].end == duration[1].end + durationStr = "#{syndrome.ticks} of #{duration[0].end}" + else + durationStr = "#{syndrome.ticks} of #{duration[0].end}-#{duration[1].end}" + end + end + + effects = rawsyndrome.ce.collect { |effect| get_effect(logger, effect, syndrome.ticks, showdisplayeffects) } + + if effects.any?{ |text, color| color==Output::RED } + color = Output::RED + elsif effects.any?{|text, color| color==Output::GREEN } + color = Output::GREEN + else + color = Output::DEFAULT + end + + logger.indent() + logger.log "#{get_display_name(rawsyndrome.syn_name, "")} [#{durationStr}]", color + + if showeffects + logger.indent() + effects.each{ |text, color| if text!="" then logger.log text, color end } + logger.unindent() + end + logger.unindent() +} + +print_raw_syndrome = lambda { |logger, rawsyndrome, showeffects, showdisplayeffects| + + effects = rawsyndrome.ce.collect { |effect| get_effect(logger, effect, -1, showdisplayeffects) } + + if effects.any?{ |item| item[1]==Output::RED } + color = Output::RED + elsif effects.any?{|item| item[1]==Output::GREEN } + color = Output::GREEN + else + color = Output::DEFAULT + end + + logger.indent() + logger.log get_display_name(rawsyndrome.syn_name, ""), color + + if showeffects + logger.indent() + effects.each{ |text, color| if text!="" then logger.log text, color end } + logger.unindent() + end + logger.unindent() +} + +print_syndromes = lambda { |logger, unit, showrace, showall, showeffects, showhiddencurse, showdisplayeffects| + + if showhiddencurse + syndromes = unit.syndromes.active + else + syndromes = unit.syndromes.active + # TODO: syndromes = unit.syndromes.active.select{ |s| visible_syndrome?(unit, s) } + end + + if !syndromes.empty? or showall + if showrace + logger.log "#{df.world.raws.creatures.all[unit.race].name[0]}#{unit.name == '' ? "" : ": "}#{unit.name}", Output::HIGHLIGHT + else + logger.log "#{unit.name}", Output::HIGHLIGHT + end + end + + syndromes.each { |syndrome| print_syndrome[logger, syndrome, showeffects, showdisplayeffects] } +} + +def starts_with?(str, prefix) + prefix = prefix.to_s + str[0, prefix.length] == prefix +end + +showall = false +showeffects = false +selected = false +dwarves = false +livestock = false +wildanimals = false +hostile = false +world = false +showhiddencurse = false +showdisplayeffects = false + +if $script_args.any?{ |arg| arg == "help" or arg == "?" or arg == "-?" } + print_help() +elsif $script_args.empty? + dwarves = true + showeffects = true +else + if $script_args.any?{ |arg| arg == "showall" } then showall=true end + if $script_args.any?{ |arg| arg == "showeffects" } then showeffects=true end + if $script_args.any?{ |arg| arg == "ignorehiddencurse" } then showhiddencurse=true end + if $script_args.any?{ |arg| arg == "showdisplayeffects" } then showdisplayeffects=true end + if $script_args.any?{ |arg| arg == "selected" } then selected=true end + if $script_args.any?{ |arg| arg == "dwarves" } then dwarves=true end + if $script_args.any?{ |arg| arg == "livestock" } then livestock=true end + if $script_args.any?{ |arg| arg == "wildanimals" } then wildanimals=true end + if $script_args.any?{ |arg| arg == "hostile" } then hostile=true end + if $script_args.any?{ |arg| arg == "world" } then world=true end + if $script_args.any?{ |arg| starts_with?(arg, "export:") } + exportfile = $script_args.find{ |arg| starts_with?(arg, "export:") }.gsub("export:", "") + export=true + end +end + +if export + logger = Output.new(exportfile) +else + logger = Output.new(nil) +end + +if selected + print_syndromes[logger, df.unit_find(), true, showall, showeffects, showhiddencurse, showdisplayeffects] + logger.break() +end + +if dwarves + logger.log "Dwarves", Output::HIGHLIGHT + df.unit_citizens.each { |unit| + print_syndromes[logger, unit, false, showall, showeffects, showhiddencurse, showdisplayeffects] + } + logger.break() +end + +if livestock + logger.log "Livestock", Output::HIGHLIGHT + df.world.units.active.find_all { |u| df.unit_category(u) == :Livestock }.each { |unit| + print_syndromes[logger, unit, true, showall, showeffects, showhiddencurse, showdisplayeffects] + } + logger.break() +end + +if wildanimals + logger.log "Wild animals", Output::HIGHLIGHT + df.world.units.active.find_all { |u| df.unit_category(u) == :Other and df.unit_other_category(u) == :Wild }.each { |unit| + print_syndromes[logger, unit, true, showall, showeffects, showhiddencurse, showdisplayeffects] + } + logger.break() +end + +if hostile + logger.log "Hostile units", Output::HIGHLIGHT + df.unit_hostiles.each { |unit| + print_syndromes[logger, unit, true, showall, showeffects, showhiddencurse, showdisplayeffects] + } + logger.break() +end + +if world + logger.log "All syndromes", Output::HIGHLIGHT + df.world.raws.syndromes.all.each { |syndrome| print_raw_syndrome[logger, syndrome, showeffects, showdisplayeffects] } +end + +logger.close() diff --git a/scripts/Spawnunit.lua b/scripts/Spawnunit.lua new file mode 100644 index 000000000..76f0e6b25 --- /dev/null +++ b/scripts/Spawnunit.lua @@ -0,0 +1,260 @@ +--Spawnunit.lua +--create unit at pointer. Usage e.g. "spawnunit DWARF 0 Dwarfy" +--author Warmist, Runrusher? +--edited by expwnent + +function findCasteIndex(race_id,casteName) + local cr=df.creature_raw.find(race_id) + for casteIndex,caste in ipairs(cr.caste) do + if caste.caste_id == casteName then + return casteIndex + end + end + return nil +end +function getCaste(race_id,caste_id) + local cr=df.creature_raw.find(race_id) + return cr.caste[caste_id] +end +function genBodyModifier(body_app_mod) + local a=math.random(0,#body_app_mod.ranges-2) + return math.random(body_app_mod.ranges[a],body_app_mod.ranges[a+1]) +end +function getBodySize(caste,time) + --todo real body size... + return caste.body_size_1[#caste.body_size_1-1] --returns last body size +end +function genAttribute(array) + local a=math.random(0,#array-2) + return math.random(array[a],array[a+1]) +end +function norm() + return math.sqrt((-2)*math.log(math.random()))*math.cos(2*math.pi*math.random()) +end +function normalDistributed(mean,sigma) + return mean+sigma*norm() +end +function clampedNormal(min,median,max) + local val=normalDistributed(median,math.sqrt(max-min)) + if valmax then return max end + return val +end +function makeSoul(unit,caste) + local tmp_soul=df.unit_soul:new() + tmp_soul.unit_id=unit.id + tmp_soul.name:assign(unit.name) + tmp_soul.race=unit.race + tmp_soul.sex=unit.sex + tmp_soul.caste=unit.caste + --todo skills,preferences,traits. + local attrs=caste.attributes + for k,v in pairs(attrs.ment_att_range) do + local max_percent=attrs.ment_att_cap_perc[k]/100 + local cvalue=genAttribute(v) + tmp_soul.mental_attrs[k]={value=cvalue,max_value=cvalue*max_percent} + end + for k,v in pairs(tmp_soul.traits) do + local min,mean,max + min=caste.personality.a[k] + mean=caste.personality.b[k] + max=caste.personality.c[k] + tmp_soul.traits[k]=clampedNormal(min,mean,max) + end + unit.status.souls:insert("#",tmp_soul) + unit.status.current_soul=tmp_soul +end +function CreateUnit(race_id,caste_id) + local race=df.creature_raw.find(race_id) + if race==nil then error("Invalid race_id") end + local caste=getCaste(race_id,caste_id) + local unit=df.unit:new() + unit.race=race_id + unit.caste=caste_id + unit.id=df.global.unit_next_id + df.global.unit_next_id=df.global.unit_next_id+1 + if caste.misc.maxage_max==-1 then + unit.relations.old_year=-1 + else + unit.relations.old_year=math.random(caste.misc.maxage_min,caste.misc.maxage_max) + end + unit.sex=caste.gender + local body=unit.body + body.body_plan=caste.body_info + local body_part_count=#body.body_plan.body_parts + local layer_count=#body.body_plan.layer_part + --components + unit.relations.birth_year=df.global.cur_year + --unit.relations.birth_time=?? + + --unit.relations.old_time=?? --TODO add normal age + local cp=body.components + cp.body_part_status:resize(body_part_count) + cp.numbered_masks:resize(#body.body_plan.numbered_masks) + for num,v in ipairs(body.body_plan.numbered_masks) do + cp.numbered_masks[num]=v + end + + cp.layer_status:resize(layer_count) + cp.layer_wound_area:resize(layer_count) + cp.layer_cut_fraction:resize(layer_count) + cp.layer_dent_fraction:resize(layer_count) + cp.layer_effect_fraction:resize(layer_count) + local attrs=caste.attributes + for k,v in pairs(attrs.phys_att_range) do + local max_percent=attrs.phys_att_cap_perc[k]/100 + local cvalue=genAttribute(v) + unit.body.physical_attrs[k]={value=cvalue,max_value=cvalue*max_percent} + --unit.body.physical_attrs:insert(k,{new=true,max_value=genMaxAttribute(v),value=genAttribute(v)}) + end + + body.blood_max=getBodySize(caste,0) --TODO normal values + body.blood_count=body.blood_max + body.infection_level=0 + unit.status2.body_part_temperature:resize(body_part_count) + for k,v in pairs(unit.status2.body_part_temperature) do + unit.status2.body_part_temperature[k]={new=true,whole=10067,fraction=0} + end + -------------------- + local stuff=unit.enemy + stuff.body_part_878:resize(body_part_count) -- all = 3 + stuff.body_part_888:resize(body_part_count) -- all = 3 + stuff.body_part_relsize:resize(body_part_count) -- all =0 + + --TODO add correct sizes. (calculate from age) + local size=caste.body_size_2[#caste.body_size_2-1] + body.size_info.size_cur=size + body.size_info.size_base=size + body.size_info.area_cur=math.pow(size,0.666) + body.size_info.area_base=math.pow(size,0.666) + body.size_info.area_cur=math.pow(size*10000,0.333) + body.size_info.area_base=math.pow(size*10000,0.333) + + stuff.were_race=race_id + stuff.were_caste=caste_id + stuff.normal_race=race_id + stuff.normal_caste=caste_id + stuff.body_part_8a8:resize(body_part_count) -- all = 1 + stuff.body_part_base_ins:resize(body_part_count) + stuff.body_part_clothing_ins:resize(body_part_count) + stuff.body_part_8d8:resize(body_part_count) + unit.recuperation.healing_rate:resize(layer_count) + --appearance + + local app=unit.appearance + app.body_modifiers:resize(#caste.body_appearance_modifiers) --3 + for k,v in pairs(app.body_modifiers) do + app.body_modifiers[k]=genBodyModifier(caste.body_appearance_modifiers[k]) + end + app.bp_modifiers:resize(#caste.bp_appearance.modifier_idx) --0 + for k,v in pairs(app.bp_modifiers) do + app.bp_modifiers[k]=genBodyModifier(caste.bp_appearance.modifiers[caste.bp_appearance.modifier_idx[k]]) + end + --app.unk_4c8:resize(33)--33 + app.tissue_style:resize(#caste.bp_appearance.style_part_idx) + app.tissue_style_civ_id:resize(#caste.bp_appearance.style_part_idx) + app.tissue_style_id:resize(#caste.bp_appearance.style_part_idx) + app.tissue_style_type:resize(#caste.bp_appearance.style_part_idx) + app.tissue_length:resize(#caste.bp_appearance.style_part_idx) + app.genes.appearance:resize(#caste.body_appearance_modifiers+#caste.bp_appearance.modifiers) --3 + app.genes.colors:resize(#caste.color_modifiers*2) --??? + app.colors:resize(#caste.color_modifiers)--3 + + makeSoul(unit,caste) + + df.global.world.units.all:insert("#",unit) + df.global.world.units.active:insert("#",unit) + --todo set weapon bodypart + + local num_inter=#caste.body_info.interactions + --used to be anon_5 and anon_6: I guessed at what those were before the df-structures update. It seems to work at least a bit. ~expwnent + unit.curse.own_interaction:resize(num_inter) + unit.curse.own_interaction_delay:resize(num_inter) + return unit +end +function findRace(name) + for k,v in pairs(df.global.world.raws.creatures.all) do + if v.creature_id==name then + return k + end + end + qerror("Race:"..name.." not found!") +end +function PlaceUnit(raceName,casteName,name,position) + local pos + if position.x==-30000 then + pos = copyall(df.global.cursor) + else + pos = position + end + if pos.x==-30000 then + qerror("Spawnunit: specify location or place the cursor where you want the unit to be created.") + end + local race=findRace(raceName) + local caste=findCasteIndex(race,casteName) + local u=CreateUnit(race,tonumber(caste) or 0) + u.pos:assign(pos) + if name then + u.name.first_name=name + u.name.has_name=true + end + u.civ_id=df.global.ui.civ_id + + local desig,ocupan=dfhack.maps.getTileFlags(pos) + ocupan.unit=true + --createNemesis(u) +end +function createFigure(trgunit) + local hf=df.historical_figure:new() + hf.id=df.global.hist_figure_next_id + hf.race=trgunit.race + hf.caste=trgunit.caste + df.global.hist_figure_next_id=df.global.hist_figure_next_id+1 + hf.name.first_name=trgunit.name.first_name + hf.name.has_name=true + df.global.world.history.figures:insert("#",hf) + return hf +end +function createNemesis(trgunit) + local id=df.global.nemesis_next_id + local nem=df.nemesis_record:new() + nem.id=id + nem.unit_id=trgunit.id + nem.unit=trgunit + nem.flags:resize(1) + nem.flags[4]=true + nem.flags[5]=true + nem.flags[6]=true + nem.flags[7]=true + nem.flags[8]=true + nem.flags[9]=true + --[[for k=4,8 do + nem.flags[k]=true + end]] + df.global.world.nemesis.all:insert("#",nem) + df.global.nemesis_next_id=id+1 + trgunit.general_refs:insert("#",{new=df.general_ref_is_nemesisst,nemesis_id=id}) + trgunit.flags1.important_historical_figure=true + local gen=df.global.world.worldgen + nem.save_file_id=gen.next_unit_chunk_id; + gen.next_unit_chunk_id=gen.next_unit_chunk_id+1 + gen.next_unit_chunk_offset=gen.next_unit_chunk_offset+1 + + --[[ local gen=df.global.world.worldgen + gen.next_unit_chunk_id + gen.next_unit_chunk_offset + ]] + nem.figure=createFigure(trgunit) +end + +args={...} + +pos = df.new(df.coord) +if #args > 3 then + pos.x = tonumber(args[4]) or -30000 + pos.y = tonumber(args[5]) or -30000 + pos.z = tonumber(args[6]) or -30000 +end + +PlaceUnit(args[1],args[2],args[3],pos) + diff --git a/scripts/blooddel.lua b/scripts/blooddel.lua new file mode 100644 index 000000000..aa1ea71a8 --- /dev/null +++ b/scripts/blooddel.lua @@ -0,0 +1,48 @@ +--blooddel.lua +--makes it so that civs won't come with barrels full of blood, ichor, or goo +--author Urist Da Vinci +--edited by expwnent + +local my_entity=df.historical_entity.find(df.global.ui.civ_id) +local sText=" " +local k=0 +local v=1 + +for x,y in pairs(df.global.world.entities.all) do + my_entity=y + k=0 + while k < #my_entity.resources.misc_mat.extracts.mat_index do + v=my_entity.resources.misc_mat.extracts.mat_type[k] + sText=dfhack.matinfo.decode(v,my_entity.resources.misc_mat.extracts.mat_index[k]) + if (sText==nil) then + --LIQUID barrels + my_entity.resources.misc_mat.extracts.mat_type:erase(k) + my_entity.resources.misc_mat.extracts.mat_index:erase(k) + k=k-1 + else + if(sText.material.id=="BLOOD") then + my_entity.resources.misc_mat.extracts.mat_type:erase(k) + my_entity.resources.misc_mat.extracts.mat_index:erase(k) + k=k-1 + end + if(sText.material.id=="ICHOR") then + my_entity.resources.misc_mat.extracts.mat_type:erase(k) + my_entity.resources.misc_mat.extracts.mat_index:erase(k) + k=k-1 + end + if(sText.material.id=="GOO") then + my_entity.resources.misc_mat.extracts.mat_type:erase(k) + my_entity.resources.misc_mat.extracts.mat_index:erase(k) + k=k-1 + end + --VENOM + --POISON + --FLUID + --MILK + --EXTRACT + + end + k=k+1 + end +end + diff --git a/scripts/feeding-timers.lua b/scripts/feeding-timers.lua new file mode 100644 index 000000000..d0aa4cc6e --- /dev/null +++ b/scripts/feeding-timers.lua @@ -0,0 +1,37 @@ +-- feeding-timers.lua +-- original author: tejón +-- rewritten by expwnent +-- see repeat.lua for how to run this every so often automatically + +local args = {...} +if args[1] ~= nil then + print("feeding-timers usage") + print(" feeding-timers") + print(" reset the feeding timers of all units as appropriate") + print(" feeding-timers help") + print(" print this help message") + print(" repeat enable [n] [years/months/ticks/days/etc] feeding-timers") + print(" run this script every n time units") + print(" repeat disable feeding-timers") + print(" stop automatically running this script") + return +end + +local fixcount = 0 +for _,unit in ipairs(df.global.world.units.all) do + if dfhack.units.isCitizen(unit) and not (unit.flags1.dead) then + for _,v in pairs(unit.status.misc_traits) do + local didfix = 0 + if v.id == 0 then -- I think this should have additional conditions... + v.value = 0 -- GiveWater cooldown set to zero + didfix = 1 + end + if v.id == 1 then -- I think this should have additional conditions... + v.value = 0 -- GiveFood cooldown set to zero + didfix = 1 + end + fixcount = fixcount + didfix + end + end +end +print("Fixed feeding timers for " .. fixcount .. " citizens.") diff --git a/scripts/force.lua b/scripts/force.lua new file mode 100644 index 000000000..3e135412f --- /dev/null +++ b/scripts/force.lua @@ -0,0 +1,84 @@ +-- force.lua +-- author Putnam +-- edited by expwnent +-- Forces an event. + +local function findCiv(arg) + local entities = df.global.world.entities.all + if tonumber(arg) then return arg end + if arg then + for eid,entity in ipairs(entities) do + if entity.entity_raw.code == arg then return entity end + end + end + return nil +end + +local args = {...} + +if not args or not args[1] then + qerror('Needs an argument. Valid arguments are caravan, migrants, diplomat, megabeast, curiousbeast, mischievousbeast, flier, siege and nightcreature. Second argument is civ, either raw entity ID or "player" for player\'s civ.') +end + +local eventType = string.lower(args[1]) + +forceEntity = args[2]=="player" and df.historical_entity.find(df.global.ui.civ_id) or findCiv(args[2]) + +if (eventType == "caravan" or eventType == "diplomat" or eventType == "siege") and not forceEntity then + qerror('caravan, diplomat and siege require a civilization ID to be included.') +end + +local function eventTypeIsNotValid() + local eventTypes = { + "caravan", + "migrants", + "diplomat", + "megabeast", + "curiousbeast", + "mischevousbeast", + "mischeviousbeast", + "flier", + "siege", + "nightcreature" + } + for _,v in ipairs(eventTypes) do + if args[1] == v then return false end + end + return true +end + +--code may be kind of bad below :V Putnam ain't experienced in lua... --Putnam's comment, not mine ~expwnent +if eventTypeIsNotValid() then + qerror('Invalid argument. Valid arguments are caravan, migrants, diplomat, megabeast, curiousbeast, mischievousbeast, flier, siege and nightcreature.') +end + +allEventTypes={} +allEventTypes["megabeast"]=function() + df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.Megabeast, season = df.global.cur_season, season_ticks = df.global.cur_season_tick } ) +end +allEventTypes["migrants"]=function() + df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.Migrants, season = df.global.cur_season, season_ticks = df.global.cur_season_tick, entity = df.global.world.entities.all[df.global.ui.civ_id] } ) +end +allEventTypes["caravan"]=function() + df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.Caravan, season = df.global.cur_season, season_ticks = df.global.cur_season_tick, entity = forceEntity } ) +end +allEventTypes["diplomat"]=function() + df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.Diplomat, season = df.global.cur_season, season_ticks = df.global.cur_season_tick, entity = forceEntity } ) +end +allEventTypes["curious"]=function() + df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.WildlifeCurious, season = df.global.cur_season, season_ticks = df.global.cur_season_tick } ) +end +allEventTypes["mischevousbeast"]=function() + df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.WildlifeMichievous, season = df.global.cur_season, season_ticks = df.global.cur_season_tick } ) +end +allEventTypes["flier"]=function() + df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.WildlifeFlier, season = df.global.cur_season, season_ticks = df.global.cur_season_tick } ) +end +allEventTypes["siege"]=function() + df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.CivAttack, season = df.global.cur_season, season_ticks = df.global.cur_season_tick, entity = forceEntity } ) +end +allEventTypes["nightcreature"]=function() + df.global.timed_events:insert('#', { new = df.timed_event, type = df.timed_event_type.NightCreature, season = df.global.cur_season, season_ticks = df.global.cur_season_tick } ) +end + +allEventTypes[eventType]() diff --git a/scripts/forumdwarves.lua b/scripts/forumdwarves.lua new file mode 100644 index 000000000..15ca9bd36 --- /dev/null +++ b/scripts/forumdwarves.lua @@ -0,0 +1,125 @@ +-- Save a copy of a text screen for the DF forums. Use 'forumdwarves help' for more details. +-- original author: Caldfir +-- edited by expwnent + +local args = {...} + +if args[1] == 'help' then + print([[ +description: + This script will attempt to read the current df-screen, and if it is a + text-viewscreen (such as the dwarf 'thoughts' screen or an item + 'description') then append a marked-up version of this text to the + target file. Previous entries in the file are not overwritten, so you + may use the 'forumdwarves' command multiple times to create a single + document containing the text from multiple screens (eg: text screens + from several dwarves, or text screens from multiple artifacts/items, + or some combination). +known screens: + The screens which have been tested and known to function properly with + this script are: + 1: dwarf/unit 'thoughts' screen + 2: item/art 'description' screen + 3: individual 'historical item/figure' screens + There may be other screens to which the script applies. It should be + safe to attempt running the script with any screen active, with an + error message to inform you when the selected screen is not appropriate + for this script. +target file: + The target file's name is 'forumdwarves.txt'. A remider to this effect + will be displayed if the script is successful. +character encoding: + The text will likely be using system-default encoding, and as such + will likely NOT display special characters (eg:é,õ,ç) correctly. To + fix this, you need to modify the character set that you are reading + the document with. 'Notepad++' is a freely available program which + can do this using the following steps: + 1: open the document in Notepad++ + 2: in the menu-bar, select + Encoding->Character Sets->Western European->OEM-US + 3: copy the text normally to wherever you want to use it +]]) + return +end +local utils = require 'utils' +local gui = require 'gui' +local dialog = require 'gui.dialogs' +local colors_css = { + [0] = 'black', + [1] = 'navy', + [2] = 'green', + [3] = 'teal', + [4] = 'maroon', + [5] = 'purple', + [6] = 'olive', + [7] = 'silver', + [8] = 'gray', + [9] = 'blue', + [10] = 'lime', + [11] = 'cyan', + [12] = 'red', + [13] = 'magenta', + [14] = 'yellow', + [15] = 'white' +} + +local scrn = dfhack.gui.getCurViewscreen() +local flerb = dfhack.gui.getFocusString(scrn) + +local function format_for_forum(strin) + local strout = strin + + local newline_idx = string.find(strout, '[P]', 1, true) + while newline_idx ~= nil do + strout = string.sub(strout,1, newline_idx-1)..'\n'..string.sub(strout,newline_idx+3) + newline_idx = string.find(strout, '[P]', 1, true) + end + + newline_idx = string.find(strout, '[B]', 1, true) + while newline_idx ~= nil do + strout = string.sub(strout,1, newline_idx-1)..'\n'..string.sub(strout,newline_idx+3) + newline_idx = string.find(strout, '[B]', 1, true) + end + + newline_idx = string.find(strout, '[R]', 1, true) + while newline_idx ~= nil do + strout = string.sub(strout,1, newline_idx-1)..'\n'..string.sub(strout,newline_idx+3) + newline_idx = string.find(strout, '[R]', 1, true) + end + + local color_idx = string.find(strout, '[C:', 1, true) + while color_idx ~= nil do + local colormatch = (string.byte(strout, color_idx+3)-48)+((string.byte(strout, color_idx+7)-48)*8) + strout = string.sub(strout,1, color_idx-1)..'[/color][color='..colors_css[colormatch]..']'..string.sub(strout,color_idx+9) + color_idx = string.find(strout, '[C:', 1, true) + end + + return strout +end + +if flerb == 'textviewer' then + print(scrn) + printall(scrn) + local lines = scrn.formatted_text + local line = "" + + if lines ~= nil then + local log = io.open('forumdwarves.txt', 'a') + log:write("[color=silver]") + for n,x in ipairs(lines) do + print(x) + printall(x) + print(x.text) + printall(x.text) + if (x ~= nil) and (x.text ~= nil) then + log:write(format_for_forum(x.text), ' ') + --log:write(x[0],'\n') + end + end + log:write("[/color]\n") + log:close() + end + print 'data prepared for forum in \"forumdwarves.txt\"' +else + print 'this is not a textview screen' +end diff --git a/scripts/fullheal.lua b/scripts/fullheal.lua new file mode 100644 index 000000000..01639c7db --- /dev/null +++ b/scripts/fullheal.lua @@ -0,0 +1,127 @@ +--fullheal.lua +--author Kurik Amudnil, Urist DaVinci +--edited by expwnent + +-- attempt to fully heal a selected unit, option -r to attempt to resurrect the unit +local args = {...} +local resurrect = false +local i=0 +for _,arg in ipairs(args) do + if arg == '-r' or arg == '-R' then + resurrect = true + elseif tonumber(arg) then + unit = df.unit.find(tonumber(arg)) + elseif arg == 'help' or arg == '-help' or arg == '-h' then + print('fullheal: heal a unit completely from anything, optionally including death.') + print(' fullheal [unitId]') + print(' heal the unit with the given id') + print(' fullheal -r [unitId]') + print(' heal the unit with the given id and bring them back from death if they are dead') + print(' fullheal') + print(' heal the currently selected unit') + print(' fullheal -r') + print(' heal the currently selected unit and bring them back from death if they are dead') + print(' fullheal help') + print(' print this help message') + return + end +end +unit = unit or dfhack.gui.getSelectedUnit() +if not unit then + qerror('Error: please select a unit or pass its id as an argument.') +end + +if unit then + if resurrect then + if unit.flags1.dead then + --print("Resurrecting...") + unit.flags2.slaughter = false + unit.flags3.scuttle = false + end + unit.flags1.dead = false + unit.flags2.killed = false + unit.flags3.ghostly = false + --unit.unk_100 = 3 + end + + --print("Erasing wounds...") + while #unit.body.wounds > 0 do + unit.body.wounds:erase(#unit.body.wounds-1) + end + unit.body.wound_next_id=1 + + --print("Refilling blood...") + unit.body.blood_count=unit.body.blood_max + + --print("Resetting grasp/stand status...") + unit.status2.limbs_stand_count=unit.status2.limbs_stand_max + unit.status2.limbs_grasp_count=unit.status2.limbs_grasp_max + + --print("Resetting status flags...") + unit.flags2.has_breaks=false + unit.flags2.gutted=false + unit.flags2.circulatory_spray=false + unit.flags2.vision_good=true + unit.flags2.vision_damaged=false + unit.flags2.vision_missing=false + unit.flags2.breathing_good=true + unit.flags2.breathing_problem=false + + unit.flags2.calculated_nerves=false + unit.flags2.calculated_bodyparts=false + unit.flags2.calculated_insulation=false + unit.flags3.compute_health=true + + --print("Resetting counters...") + unit.counters.winded=0 + unit.counters.stunned=0 + unit.counters.unconscious=0 + unit.counters.webbed=0 + unit.counters.pain=0 + unit.counters.nausea=0 + unit.counters.dizziness=0 + + unit.counters2.paralysis=0 + unit.counters2.fever=0 + unit.counters2.exhaustion=0 + unit.counters2.hunger_timer=0 + unit.counters2.thirst_timer=0 + unit.counters2.sleepiness_timer=0 + unit.counters2.vomit_timeout=0 + + --print("Resetting body part status...") + v=unit.body.components + for i=0,#v.nonsolid_remaining - 1,1 do + v.nonsolid_remaining[i] = 100 -- percent remaining of fluid layers (Urist Da Vinci) + end + + v=unit.body.components + for i=0,#v.layer_wound_area - 1,1 do + v.layer_status[i].whole = 0 -- severed, leaking layers (Urist Da Vinci) + v.layer_wound_area[i] = 0 -- wound contact areas (Urist Da Vinci) + v.layer_cut_fraction[i] = 0 -- 100*surface percentage of cuts/fractures on the body part layer (Urist Da Vinci) + v.layer_dent_fraction[i] = 0 -- 100*surface percentage of dents on the body part layer (Urist Da Vinci) + v.layer_effect_fraction[i] = 0 -- 100*surface percentage of "effects" on the body part layer (Urist Da Vinci) + end + + v=unit.body.components.body_part_status + for i=0,#v-1,1 do + v[i].on_fire = false + v[i].missing = false + v[i].organ_loss = false + v[i].organ_damage = false + v[i].muscle_loss = false + v[i].muscle_damage = false + v[i].bone_loss = false + v[i].bone_damage = false + v[i].skin_damage = false + v[i].motor_nerve_severed = false + v[i].sensory_nerve_severed = false + end + + if unit.job.current_job and unit.job.current_job.job_type == df.job_type.Rest then + --print("Wake from rest -> clean self...") + unit.job.current_job = df.job_type.CleanSelf + end +end + diff --git a/scripts/growthbug.lua b/scripts/growthbug.lua new file mode 100644 index 000000000..791dd110e --- /dev/null +++ b/scripts/growthbug.lua @@ -0,0 +1,26 @@ +--growthbug: units only grow when the current tick is 0 mod 10, so only 1/10 units will grow naturally. this script periodically sets the birth time of each unit so that it will grow +--to run periodically, use "repeat enable 2 months growthBug now". see repeat.lua for details +-- author expwnent + +local args = {...} +if args[1] ~= nil then + print("growthbug usage") + print(" growthbug") + print(" fix the growth bug for all units on the map") + print(" growthbug help") + print(" print this help message") + print(" repeat enable [n] [years/months/ticks/days/etc] growthbug") + print(" run this script every n time units") + print(" repeat disable growthbug") + print(" stop automatically running this script") +end + +local count = 0 +for _,unit in ipairs(df.world.units.all) do + local offset = unit.relations.birth_time % 10; + if offset ~= 0 then + count = count+1 + unit.relations.birth_time = unit.relations.birth_time - offset + end +end +print("Fixed growth bug for "..count.." units.") diff --git a/scripts/hackWish.lua b/scripts/hackWish.lua new file mode 100644 index 000000000..26dcc86be --- /dev/null +++ b/scripts/hackWish.lua @@ -0,0 +1,251 @@ +-- hackWish.lua +-- Allows for script-based wishing. +-- author Putnam +-- edited by expwnent + +function getGenderString(gender) + local genderStr + if gender==0 then + genderStr=string.char(12) + elseif gender==1 then + genderStr=string.char(11) + else + return "" + end + return string.char(40)..genderStr..string.char(41) +end + +function getCreatureList() + local crList={} + for k,cr in ipairs(df.global.world.raws.creatures.alphabetic) do + for kk,ca in ipairs(cr.caste) do + local str=ca.caste_name[0] + str=str..' '..getGenderString(ca.gender) + table.insert(crList,{str,nil,ca}) + end + end + return crList +end + +function getMatFilter(itemtype) + local itemTypes={ + SEEDS=function(mat,parent,typ,idx) + return mat.flags.SEED_MAT + end, + PLANT=function(mat,parent,typ,idx) + return mat.flags.STRUCTURAL_PLANT_MAT + end, + LEAVES=function(mat,parent,typ,idx) + return mat.flags.LEAF_MAT + end, + MEAT=function(mat,parent,typ,idx) + return mat.flags.MEAT + end, + CHEESE=function(mat,parent,typ,idx) + return (mat.flags.CHEESE_PLANT or mat.flags.CHEESE_CREATURE) + end, + LIQUID_MISC=function(mat,parent,typ,idx) + return (mat.flags.LIQUID_MISC_PLANT or mat.flags.LIQUID_MISC_CREATURE or mat.flags.LIQUID_MISC_OTHER) + end, + POWDER_MISC=function(mat,parent,typ,idx) + return (mat.flags.POWDER_MISC_PLANT or mat.flags.POWDER_MISC_CREATURE) + end, + DRINK=function(mat,parent,typ,idx) + return (mat.flags.ALCOHOL_PLANT or mat.flags.ALCOHOL_CREATURE) + end, + GLOB=function(mat,parent,typ,idx) + return (mat.flags.STOCKPILE_GLOB) + end, + WOOD=function(mat,parent,typ,idx) + return (mat.flags.WOOD) + end, + THREAD=function(mat,parent,typ,idx) + return (mat.flags.THREAD_PLANT) + end, + LEATHER=function(mat,parent,typ,idx) + return (mat.flags.LEATHER) + end + } + return itemTypes[df.item_type[itemtype]] or getRestrictiveMatFilter(itemtype) +end + +function getRestrictiveMatFilter(itemType) + if not args.veryRestrictive then return nil else + local itemTypes={ + WEAPON=function(mat,parent,typ,idx) + return (mat.flags.ITEMS_WEAPON or mat.flags.ITEMS_WEAPON_RANGED) + end, + AMMO=function(mat,parent,typ,idx) + return (mat.flags.ITEMS_AMMO) + end, + ARMOR=function(mat,parent,typ,idx) + return (mat.flags.ITEMS_ARMOR) + end, + SHOES,SHIELD,HELM,GLOVES=ARMOR,ARMOR,ARMOR,ARMOR, + INSTRUMENT=function(mat,parent,typ,idx) + return (mat.flags.ITEMS_HARD) + end, + GOBLET,FLASK,TOY,RING,CROWN,SCEPTER,FIGURINE,TOOL=INSTRUMENT,INSTRUMENT,INSTRUMENT,INSTRUMENT,INSTRUMENT,INSTRUMENT,INSTRUMENT, + AMULET=function(mat,parent,typ,idx) + return (mat.flags.ITEMS_SOFT or mat.flags.ITEMS_HARD) + end, + EARRING,BRACELET=AMULET,AMULET, + ROCK=function(mat,parent,typ,idx) + return (mat.flags.IS_STONE) + end, + BOULDER=ROCK, + BAR=function(mat,parent,typ,idx) + return (mat.flags.IS_METAL or mat.flags.SOAP or mat.id==COAL) + end + } + return itemTypes[df.item_type[itemType]] + end +end + +function createItem(mat,itemType,quality,pos,description) + local item=df[df.item_type.attrs[itemType[1]].classname]:new() + item.id=df.global.item_next_id + df.global.world.items.all:insert('#',item) + df.global.item_next_id=df.global.item_next_id+1 + item:setSubtype(itemType[2]) + item:setMaterial(mat[1]) + item:setMaterialIndex(mat[2]) + if df.item_type[itemType[1]]=='EGG' then + local creature=df.creature_raw.find(mat[1]) + local eggMat={} + eggMat[1]=dfhack.matinfo.find(creature.creature_id..':EGGSHELL') + if eggMat[1] then + eggMat[2]=dfhack.matinfo.find(creature.creature_id..':EGG_WHITE') + eggMat[3]=dfhack.matinfo.find(creature.creature_id..'EGG_YOLK') + for k,v in ipairs(eggMat) do + item.egg_materials.mat_type:insert('#',v.type) + item.egg_materials.mat_index:insert('#',v.index) + end + else + eggMat=dfhack.matinfo.find(creature.creature_id..':MUSCLE') + item.egg_materials.mat_type:insert('#',eggMat.type) + item.egg_materials.mat_index:insert('#',eggMat.index) + end + end + item:categorize(true) + item.flags.removed=true + item:setSharpness(1,0) + item:setQuality(quality-1) + if df.item_type[itemType[1]]=='SLAB' then + item.description=description + end + dfhack.items.moveToGround(item,{x=pos.x,y=pos.y,z=pos.z}) +end + +--TODO: should this be a function? +function qualityTable() + return {{'None'}, + {'-Well-crafted-'}, + {'+Finely-crafted+'}, + {'*Superior*'}, + {string.char(240)..'Exceptional'..string.char(240)}, + {string.char(15)..'Masterwork'..string.char(15)} + } +end + +local script=require('gui/script') +local guimaterials=require('gui.materials') + +function showItemPrompt(text,item_filter,hide_none) + guimaterials.ItemTypeDialog{ + prompt=text, + item_filter=item_filter, + hide_none=hide_none, + on_select=script.mkresume(true), + on_cancel=script.mkresume(false), + on_close=script.qresume(nil) + }:show() + + return script.wait() +end + +function showMaterialPrompt(title, prompt, filter, inorganic, creature, plant) --the one included with DFHack doesn't have a filter or the inorganic, creature, plant things available + guimaterials.MaterialDialog{ + frame_title = title, + prompt = prompt, + mat_filter = filter, + use_inorganic = inorganic, + use_creature = creature, + use_plant = plant, + on_select = script.mkresume(true), + on_cancel = script.mkresume(false), + on_close = script.qresume(nil) + }:show() + + return script.wait() +end + +function usesCreature(itemtype) + typesThatUseCreatures={REMAINS=true,FISH=true,FISH_RAW=true,VERMIN=true,PET=true,EGG=true,CORPSE=true,CORPSEPIECE=true} + return typesThatUseCreatures[df.item_type[itemtype]] +end + +function getCreatureRaceAndCaste(caste) + return df.global.world.raws.creatures.list_creature[caste.index],df.global.world.raws.creatures.list_caste[caste.index] +end + +function hackWish(posOrUnit) + local pos = df.unit:is_instance(posOrUnit) and posOrUnit.pos or posOrUnit + script.start(function() + --local amountok, amount + local matok,mattype,matindex,matFilter + local itemok,itemtype,itemsubtype=showItemPrompt('What item do you want?',function(itype) return df.item_type[itype]~='CORPSE' and df.item_type[itype]~='FOOD' end ,true) + if not args.notRestrictive then + matFilter=getMatFilter(itemtype) + end + if not usesCreature(itemtype) then + matok,mattype,matindex=showMaterialPrompt('Wish','And what material should it be made of?',matFilter) + else + local creatureok,useless,creatureTable=script.showListPrompt('Wish','What creature should it be?',COLOR_LIGHTGREEN,getCreatureList()) + mattype,matindex=getCreatureRaceAndCaste(creatureTable[3]) + end + local qualityok,quality=script.showListPrompt('Wish','What quality should it be?',COLOR_LIGHTGREEN,qualityTable()) + local description + if df.item_type[itemtype]=='SLAB' then + local descriptionok + descriptionok,description=script.showInputPrompt('Slab','What should the slab say?',COLOR_WHITE) + end + --repeat amountok,amount=script.showInputPrompt('Wish','How many do you want? (numbers only!)',COLOR_LIGHTGREEN) until tonumber(amount) + if mattype and itemtype then + --for i=1,tonumber(amount) do + createItem({mattype,matindex},{itemtype,itemsubtype},quality,pos,description) + --end + end + end) +end + +scriptArgs={...} + +args={} + +for k,v in ipairs(scriptArgs) do + v=v:lower() + if v=='startup' then args.startup=true end + if v=='all' then args.notRestrictive=true end + if v=='restrictive' then args.veryRestrictive=true end + if v=='unit' then args.unitNum=args[k+1] end + if v=='x' then args.x=args[k+1] end + if v=='y' then args.y=args[k+1] end + if v=='z' then args.z=args[k+1] end +end + +eventful=require('plugins.eventful') + +function posIsValid(pos) + return pos.x~=-30000 and pos or false +end + +if not args.startup then + local posOrUnit=args.x and {x=args.x,y=args.y,z=args.z} or args.unitNum and df.unit.find(args.unitNum) or posIsValid(df.global.cursor) or dfhack.gui.getSelectedUnit(true) + hackWish(posOrUnit) +else + eventful.onReactionComplete.hackWishP=function(reaction,unit,input_items,input_reagents,output_items, call_native) + if not reaction.code:find('DFHACK_WISH') then return nil end + hackWish(unit) + end +end diff --git a/scripts/itemsyndrome.lua b/scripts/itemsyndrome.lua new file mode 100644 index 000000000..49f83488f --- /dev/null +++ b/scripts/itemsyndrome.lua @@ -0,0 +1,223 @@ +-- itemsyndrome.lua +-- author: Putnam +-- edited by expwnent +-- Checks for inventory changes and applies or removes syndromes that items or their materials have. Use "disable" (minus quotes) to disable and "help" to get help. + +eventful=eventful or require("plugins.eventful") +syndromeUtil = syndromeUtil or require("plugins.syndromeUtil") + +local function printItemSyndromeHelp() + print("Arguments (non case-sensitive):") + print(' "help": displays this dialogue.') + print(" ") + print(' "disable": disables the script.') + print(" ") + print(' "debugon/debugoff": debug mode.') + print(" ") + print(' "contaminantson/contaminantsoff": toggles searching for contaminants.') + print(' Disabling speeds itemsyndrome up greatly.') + print(' "transformReEquipOn/TransformReEquipOff": toggles transformation auto-reequip.') +end + +itemsyndromedebug=false + +local args = {...} +for k,v in ipairs(args) do + v=v:lower() + if v == "help" then printItemSyndromeHelp() return end + if v == "debugon" then itemsyndromedebug = true end + if v == "debugoff" then itemsyndromedebug = false end + if v == "contaminantson" then itemsyndromecontaminants = true end + if v == "contaminantsoff" then itemsyndromecontaminants = false end + if v == "transformreequipon" then transformationReEquip = true end + if v == "transformreequipoff" then transformationReEquip = false end +end + +local function getMaterial(item) + --What does this line mean? + local material = dfhack.matinfo.decode(item) and dfhack.matinfo.decode(item) or false + if not material then return nil end + if material.mode ~= "inorganic" then + return nil + else + return material.material --the "material" thing up there contains a bit more info which is all pretty important but impertinent, like the creature that the material comes from + end +end + +local function findItemSyndromeInorganic() + local allInorganics = {} + for matID,material in ipairs(df.global.world.raws.inorganics) do + if string.find(material.id,"DFHACK_ITEMSYNDROME_MATERIAL_") then table.insert(allInorganics,matID) end --the last underscore is needed to prevent duped raws; I want good modder courtesy if it kills me, dammit! + end + if itemsyndromedebug then printall(allInorganics) end + if #allInorganics>0 then return allInorganics else return nil end +end + +local function getAllItemSyndromeMats(itemSyndromeMatIDs) + local allActualInorganics = {} + for _,itemSyndromeMatID in ipairs(itemSyndromeMatIDs) do + table.insert(allActualInorganics,df.global.world.raws.inorganics[itemSyndromeMatID].material) + end + if itemsyndromedebug then printall(allActualInorganics) end + return allActualInorganics +end + +local function syndromeIsDfHackSyndrome(syndrome) + for k,v in ipairs(syndrome.syn_class) do + if v.value=="DFHACK_ITEM_SYNDROME" then + if itemsyndromedebug then print('Syndrome is DFHack syndrome, checking if creature is affected...') end + return true + end + end + if itemsyndromedebug then print('Syndrome is not DFHack syndrome. Cancelling.') end + return false +end + +local function itemHasNoSubtype(item) + return item:getSubtype()==-1 +end + +local function itemHasSyndrome(item) + if itemHasNoSubtype(item) or not itemSyndromeMats then return nil end + local allItemSyndromes={} + for _,material in ipairs(itemSyndromeMats) do + for __,syndrome in ipairs(material.syndrome) do + if syndrome.syn_name == item.subtype.name then table.insert(allItemSyndromes,syndrome) end + end + end + return #allItemSyndromes>0 and allItemSyndromes or false +end + +local function getValidPositions(syndrome) + local returnTable={AffectsHauler=false,AffectsStuckins=false,IsArmorOnly=false,IsWieldedOnly=false,OnlyAffectsStuckins=false} + for k,v in ipairs(syndrome.syn_class) do + if v.value:find('DFHACK') then + if v.value=="DFHACK_AFFECTS_HAULER" then returnTable.AffectsHauler=true end + if v.value=="DFHACK_AFFECTS_STUCKIN" then returnTable.AffectsStuckins=true end + if v.value=="DFHACK_STUCKINS_ONLY" then returnTable.OnlyAffectsStuckins=true end + if v.value=="DFHACK_WIELDED_ONLY" then returnTable.IsWieldedOnly=true end + if v.value=="DFHACK_ARMOR_ONLY" then returnTable.IsArmorOnly=true end + end + end + return returnTable +end + +local function itemIsInValidPosition(item_inv, syndrome) + local item = getValidPositions(syndrome) + if not item_inv then print("You shouldn't see this error! At all! Putnam f'd up! Tell him off!") return false end + local isInValidPosition=not ((item_inv.mode == 0 and not item.AffectsHauler) or (item_inv.mode == 7 and not item.AffectsStuckins) or (item_inv.mode ~= 2 and item.IsArmorOnly) or (item_inv.mode ~=1 and item.IsWieldedOnly) or (item_inv.mode ~=7 and item.OnlyAffectsStuckins)) + if itemsyndromedebug then print(isInValidPosition and 'Item is in correct position.' or 'Item is not in correct position.') end + return isInValidPosition +end + +local function syndromeIsTransformation(syndrome) + for _,effect in ipairs(syndrome.ce) do + if df.creature_interaction_effect_body_transformationst:is_instance(effect) then return true end + end + return false +end + +local function rememberInventory(unit) + local invCopy = {} + for inv_id,item_inv in ipairs(unit.inventory) do + invCopy[inv_id+1] = {} + local itemToWorkOn = invCopy[inv_id+1] + itemToWorkOn.item = item_inv.item + itemToWorkOn.mode = item_inv.mode + itemToWorkOn.body_part_id = item_inv.body_part_id + end + return invCopy +end + +local function moveAllToInventory(unit,invTable) + for _,item_inv in ipairs(invTable) do + dfhack.items.moveToInventory(item_inv.item,unit,item_inv.mode,item_inv.body_part_id) + end +end + +local function syndromeIsOnUnequip(syndrome) + for k,v in ipairs(syndrome.syn_class) do + if v.value:upper()=='DFHACK_ON_UNEQUIP' then return true end + end + return false +end + +local function addOrRemoveSyndromeDepending(unit,old_equip,new_equip,syndrome) + local item_inv=new_equip or old_equip + if not syndromeIsDfHackSyndrome(syndrome) then + return + end + local equippedOld = itemIsInValidPosition(old_equip,syndrome) + local equippedNew = itemIsInValidPosition(new_equip,syndrome) + if equippedOld == equippedNew then + return + end + local isOnEquip = not syndromeIsOnUnequip(syndrome) + local apply = (isOnEquip and equippedNew) or (not isOnEquip and not equippedNew) + if apply then + syndromeUtil.infectWithSyndrome(unit,syndrome,syndromeUtil.ResetPolicy.ResetDuration) + else + syndromeUtil.eraseSyndrome(unit,syndrome) + end +end + +eventful.enableEvent(eventful.eventType.INVENTORY_CHANGE,5) + +eventful.onInventoryChange.itemsyndrome=function(unit_id,item_id,old_equip,new_equip) + local item = df.item.find(item_id) + --if not item then return false end + if not item then + return + end + local unit = df.unit.find(unit_id) + if unit.flags1.dead then return false end + if itemsyndromedebug then print("Checking unit #" .. unit_id) end + local transformation = false + if itemsyndromedebug then print("checking item #" .. item_id .." on unit #" .. unit_id) end + local itemMaterial=getMaterial(item) + local function manageSyndromes(syndromes) + for k,syndrome in ipairs(syndromes) do + if itemsyndromedebug then print("item has a syndrome, checking if syndrome is valid for application...") end + if syndromeIsTransformation(syndrome) then + --unitInventory = rememberInventory(unit) + rememberInventory(unit) + transformation = true + end + addOrRemoveSyndromeDepending(unit,old_equip,new_equip,syndrome) + end + end + if itemMaterial then + manageSyndromes(itemMaterial.syndrome) + end + local itemSyndromes = itemHasSyndrome(item) + if itemSyndromes then + if itemsyndromedebug then print("Item itself has a syndrome, checking if item is in correct position and creature is affected") end + manageSyndromes(itemSyndromes) + end + if itemsyndromecontaminants and item.contaminants then + if itemsyndromedebug then print("Item has contaminants. Checking for syndromes...") end + for _,contaminant in ipairs(item.contaminants) do + local contaminantMaterial=getMaterial(contaminant) + if contaminantMaterial then + manageSyndromes(contaminantMaterial.syndrome) + end + end + end + if transformation and transformationReEquip then dfhack.timeout(2,"ticks",function() moveAllToInventory(unit,unitInventory) end) end +end + +dfhack.onStateChange.itemsyndrome=function(code) + if code==SC_WORLD_LOADED then + itemSyndromeMatIDs = findItemSyndromeInorganic() + if itemSyndromeMatIDs then itemSyndromeMats = getAllItemSyndromeMats(itemSyndromeMatIDs) end + end +end + +if disable then + eventful.onInventoryChange.itemsyndrome=nil + print("Disabled itemsyndrome.") + disable = false +else + print("Enabled itemsyndrome.") +end + diff --git a/scripts/moddableGods.lua b/scripts/moddableGods.lua new file mode 100644 index 000000000..e53e44001 --- /dev/null +++ b/scripts/moddableGods.lua @@ -0,0 +1,152 @@ +-- moddableGods.lua +-- Sets player-defined gods to correct civilizations. +-- author: Putnam +-- edited by expwnent + +--[[Here's an example of how to make a god: + +[CREATURE:SHEOGORATH] + [DOES_NOT_EXIST] + [MALE] + [NAME:jovial man:Daedra:madness] "Sheogorath, madness god." "Often depicted as a jovial man" + [CASTE_NAME:Sheogorath:Sheogorath:Sheogorath] + [DESCRIPTION:The Daedric Prince of madness.] + [CREATURE_CLASS:DFHACK_GOD] + [SPHERE:MUSIC] + [SPHERE:ART] + [SPHERE:CHAOS] +]] + +local function getCreatureClasses(creatureRaw) + local creatureClasses = {} + for _,caste in ipairs(creatureRaw.caste) do + for k,class in ipairs(caste.creature_class) do + table.insert(creatureClasses,class.value) + end + end + return creatureClasses +end + +local function deityIsOfSpecialCreature(creatureRaw) + for k,class in ipairs(getCreatureClasses(creatureRaw)) do + if class=="DFHACK_GOD" then return true end + end + return false +end + +local function scriptAlreadyRunOnThisWorld() + local figures = df.global.world.history.figures + for i=#figures-1,0,-1 do --goes through the table backwards because the particular hist figs involved are probably going to be the last + local figure = figures[i] + if not df.isnull(figure.flags) and figure.flags.deity and deityIsOfSpecialCreature(df.global.world.raws.creatures.all[figure.race]) then + return true + end + end + return false +end + +local function findAGod() + for k,fig in ipairs(df.global.world.history.figures) do + if fig.flags.deity then + return fig + end + end + return nil +end + +local function putListOfAllSpecialCreatureGodsTogether() + local specialCreatures = {} + for k,creatureRaw in ipairs(df.global.world.raws.creatures.all) do + if deityIsOfSpecialCreature(creatureRaw) then + table.insert(specialCreatures,{creatureRaw,k}) + end + end + return specialCreatures +end + +local function stringStarts(String,Start) + return string.sub(String,1,string.len(Start))==Start +end + +local function getRacesOfGod(god) + local civList={} + for k,class in ipairs(getCreatureClasses(god)) do + if stringStarts(class,"WORSHIP_ENTITY_") then + table.insert(civList,string.sub(class,15)) + end + end + return civList +end + +local function entityIsInGodsDomain(entity,entityRacesTable) + for k,v in ipairs(entityRacesTable) do + if v==entity.entity_raw.code then + return true + end + end + return false +end + +local function setUpTemplate(godFig,templateGod) + godFig.appeared_year=-1 + godFig.born_year=-1 + godFig.born_seconds=-1 + godFig.curse_year=-1 + godFig.curse_seconds=-1 + godFig.old_year=-1 + godFig.old_seconds=-1 + godFig.died_year=-1 + godFig.died_seconds=-1 + godFig.name.has_name=true + godFig.breed_id=-1 + godFig.flags:assign(templateGod.flags) + godFig.id = df.global.hist_figure_next_id + godFig.info = df.historical_figure_info:new() + godFig.info.spheres={new=true} + godFig.info.secret=df.historical_figure_info.T_secret:new() +end + +local function setUpGod(god,godID,templateGod) + local godFig = df.historical_figure:new() + setUpTemplate(godFig,templateGod) + godFig.sex=god.caste[0].gender + godFig.race=godID + godFig.name.first_name=god.caste[0].caste_name[2] --the adjectival form of the caste_name is used for the god's name, E.G, [CASTE_NAME:god:god:armok] + for k,v in ipairs(god.sphere) do --assigning spheres + godFig.info.spheres:insert('#',v) + end + df.global.world.history.figures:insert('#',godFig) + df.global.hist_figure_next_id=df.global.hist_figure_next_id+1 + return godFig +end + +--[[this function isn't really working right now so it's dummied out +function setGodAsOfficialDeityOfItsParticularEntity(god,godFig) + local entityRaces=getRacesOfGod(god) + for k,entity in ipairs(df.global.world.entities.all) do + if entityIsInGodsDomain(entity,entityRaces) then + entity.unknown1b.worship:insert('#',godFig.id) + end + end +end +]] +local function moddableGods() + if scriptAlreadyRunOnThisWorld() then + print("Already run on world...") + return false + end + local gods = putListOfAllSpecialCreatureGodsTogether() + local templateGod=findAGod() + for k,v in ipairs(gods) do --creature raws first + local god = v[1] + local godID = v[2] + local godFig = setUpGod(god,godID,templateGod) + --setGodAsOfficialDeityOfItsParticularEntity(god,godFig) + end +end + +dfhack.onStateChange.letThereBeModdableGods = function(state) + if state == SC_WORLD_LOADED and df.global.gamemode~=3 then --make sure that the gods show up only after the rest of the histfigs do + moddableGods() + end +end diff --git a/scripts/onReportExample.lua b/scripts/onReportExample.lua new file mode 100644 index 000000000..5e25d4211 --- /dev/null +++ b/scripts/onReportExample.lua @@ -0,0 +1,19 @@ + +local onReport = require 'plugins.onReport' +reload('plugins.onReport') + +onReport.triggers.onReportExample = function(reportId) +-- print('report '..reportId..' happened!') + local report = df.report.find(reportId) + if not report then + return + end +-- printall(report) +-- print('\n') + print(reportId .. ': ' .. df.announcement_type[report["type"]]) + for unitId,_ in pairs(onReport.eventToDwarf[reportId]) do + print('relevant dwarf: ' .. unitId) + end +end + + diff --git a/scripts/onStrikeExample.lua b/scripts/onStrikeExample.lua new file mode 100644 index 000000000..36d194f39 --- /dev/null +++ b/scripts/onStrikeExample.lua @@ -0,0 +1,16 @@ + +local onStrike = require 'plugins.onStrike' +local eventful = require 'plugins.eventful' +--print(onStrike) + +--onStrike.triggers.onStrikeExample = function(information) +-- print(information.attacker .. ' attacks ' .. information.defender .. ': ' .. information.announcement) +onStrike.triggers.onStrikeExample = function(attacker, defender, weapon, wound) + if weapon then + print(attacker.id..' weapon attacks '..defender.id .. ' with ' .. weapon.id) + --df.global.pause_state = true + else + print(attacker.id..' attacks '..defender.id) + end +end + diff --git a/scripts/printArgs.lua b/scripts/printArgs.lua new file mode 100644 index 000000000..095cd98b6 --- /dev/null +++ b/scripts/printArgs.lua @@ -0,0 +1,9 @@ +--printArgs.lua +--author expwnent +--prints all the arguments on their own line with quotes around them. useful for debugging + +local args = {...} +print("printArgs") +for _,arg in ipairs(args) do + print("'"..arg.."'") +end diff --git a/scripts/projectileExpansion.lua b/scripts/projectileExpansion.lua new file mode 100644 index 000000000..df691fc77 --- /dev/null +++ b/scripts/projectileExpansion.lua @@ -0,0 +1,190 @@ +-- projectileExpansion.lua +-- author: Putnam +-- edited by expwnent +-- Adds extra functionality to projectiles. Use the argument "disable" (minus quotes) to disable. + +local syndromeUtil = require("syndromeUtil") +local events=require "plugins.eventful" + +local flowtypes = { + miasma = 0, + mist = 1, + mist2 = 2, + dust = 3, + lavaMist = 4, + smoke = 5, + dragonFire = 6, + fireBreath = 7, + web = 8, + undirectedGas = 9, + undirectedVapor = 10, + oceanWave = 11, + seaFoam = 12 +} + +local function posIsEqual(pos1,pos2) + if pos1.x ~= pos2.x or pos1.y ~= pos2.y or pos1.z ~= pos2.z then return false end + return true +end + +local function getMaterial(item) + if not dfhack.matinfo.decode(item) then return nil end + local matinfo = dfhack.matinfo.decode(item) + return matinfo and matinfo.material +-- return dfhack.matinfo.decode(item).material +end + +local function getSyndrome(material) + if material==nil then return nil end + if #material.syndrome>0 then return material.syndrome[0] + else return nil end +end + +local function removeItem(item) + item.flags.garbage_collect = true +end + +local function findInorganicWithName(matString) + for inorganicID,material in ipairs(df.global.world.raws.inorganics) do + if material.id == matString then return inorganicID end + end + return nil +end + +local function getScriptFromMaterial(material) + local commandStart + local commandEnd + local reactionClasses = material.reaction_class + for classNumber,reactionClass in ipairs(reactionClasses) do + if reactionClass.value == "\\COMMAND" then commandStart = classNumber end + if reactionClass.value == "\\ENDCOMMAND" then commandEnd = classNumber break end + end + local script = {} + if commandStart and commandEnd then + for i = commandStart+1, commandEnd-1, 1 do + table.insert(script,reactionClasses[i].value) + end + end + return script +end + +local function getUnitHitByProjectile(projectile) + for uid,unit in ipairs(df.global.world.units.active) do + if posIsEqual(unit.pos,projectile.cur_pos) then return uid,unit end + end + return nil +end + +local function matCausesSyndrome(material) + for _,reactionClass in ipairs(material.reaction_class) do + if reactionClass.value == "DFHACK_CAUSES_SYNDROME" then return true end --the syndrome is the syndrome local to the projectile material + end + return false +end + +projectileExpansionFlags = projectilExpansionFlags or { + matWantsSpecificInorganic = 0, + matWantsSpecificSize = 50000, + matCausesDragonFire = false, + matCausesMiasma = false, + matCausesMist = false, + matCausesMist2 = false, + matCausesDust = false, + matCausesLavaMist = false, + matCausesSmoke = false, + matCausesFireBreath = false, + matCausesWeb = false, + matCausesUndirectedGas = false, + matCausesUndirectedVapor = false, + matCausesOceanWave = false, + matCausesSeaFoam = false, + matHasScriptAttached = false, + matCausesSyndrome = false, + returnLocation = false, + matDisappearsOnHit = false +} + +local function getProjectileExpansionFlags(material) + local matName = nil + for k,reactionClass in ipairs(material.reaction_class) do + if debugProjExp then print("checking reaction class #" .. k .. "...",reactionClass.value) end + if string.find(reactionClass.value,"DFHACK") then + if debugProjExp then print("DFHack reaction class found!") end + if reactionClass.value == "DFHACK_SPECIFIC_MAT" then matName = material.reaction_class[k+1].value end + if reactionClass.value == "DFHACK_FLOW_SIZE" then projectileExpansionFlags.matWantsSpecificSize = tonumber(material.reaction_class[k+1].value) end + if reactionClass.value == "DFHACK_CAUSES_SYNDROME" then projectileExpansionFlags.matCausesSyndrome = true end + if reactionClass.value == "DFHACK_DRAGONFIRE" then projectileExpansionFlags.matCausesDragonFire = true end + if reactionClass.value == "DFHACK_MIASMA" then projectileExpansionFlags.matCausesMiasma = true end + if reactionClass.value == "DFHACK_MIST" then projectileExpansionFlags.matCausesMist = true end + if reactionClass.value == "DFHACK_MIST2" then projectileExpansionFlags.matCausesMist2 = true end + if reactionClass.value == "DFHACK_DUST" then projectileExpansionFlags.matCausesDust = true end + if reactionClass.value == "DFHACK_LAVAMIST" then projectileExpansionFlags.matCausesLavaMist = true end + if reactionClass.value == "DFHACK_SMOKE" then projectileExpansionFlags.matCausesSmoke = true end + if reactionClass.value == "DFHACK_FIREBREATH" then projectileExpansionFlags.matCausesFireBreath = true end + if reactionClass.value == "DFHACK_WEB" then projectileExpansionFlags.matCausesWeb = true end + if reactionClass.value == "DFHACK_GAS_UNDIRECTED" then projectileExpansionFlags.matCausesUndirectedGas = true end + if reactionClass.value == "DFHACK_VAPOR_UNDIRECTED" then projectileExpansionFlags.matCausesUndirectedVapor = true end + if reactionClass.value == "DFHACK_OCEAN_WAVE" then projectileExpansionFlags.matCausesOceanWave = true end + if reactionClass.value == "DFHACK_SEA_FOAM" then projectileExpansionFlags.matCausesSeaFoam = true end + if reactionClass.value == "DFHACK_DISAPPEARS" then projectileExpansionFlags.matDisappearsOnHit = true end + end + if reactionClass.value == "\\COMMAND" then projectileExpansionFlags.matHasScriptAttached = true end + end + if matName then projectileExpansionFlags.matWantsSpecificInorganic = findInorganicWithName(matName) end + return projectileExpansionFlags +end + +debugProjExp=false + +events.onProjItemCheckImpact.expansion=function(projectile) + if debugProjExp then print("Thwack! Projectile item hit. Running projectileExpansion.") end + if projectile then + if debugProjExp then print("Found the item. Working on it.") end + local material = getMaterial(projectile.item) + if not material then return nil end + local projectileExpansionFlags=getProjectileExpansionFlags(material) + if debugProjExp then print(projectileExpansionFlags) printall(projectileExpansionFlags) end + local syndrome = getSyndrome(material) + local emissionMat = projectileExpansionFlags.matWantsSpecificInorganic --defaults to iron + local flowSize = projectileExpansionFlags.matWantsSpecificSize --defaults to 50000 + if projectileExpansionFlags.matCausesDragonFire then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.dragonFire,0,0,flowSize) end + if projectileExpansionFlags.matCausesMiasma then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.miasma,0,0,flowSize) end + if projectileExpansionFlags.matCausesMist then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.mist,0,0,flowSize) end + if projectileExpansionFlags.matCausesMist2 then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.mist2,0,0,flowSize) end + if projectileExpansionFlags.matCausesDust then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.dust,0,emissionMat,flowSize) end + if projectileExpansionFlags.matCausesLavaMist then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.lavaMist,0,emissionMat,flowSize) end + if projectileExpansionFlags.matCausesSmoke then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.smoke,0,0,flowSize) end + if projectileExpansionFlags.matCausesFireBreath then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.fireBreath,0,0,flowSize) end + if projectileExpansionFlags.matCausesWeb then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.web,0,emissionMat,flowSize) end + if projectileExpansionFlags.matCausesUndirectedGas then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.undirectedGas,0,emissionMat,flowSize) end + if projectileExpansionFlags.matCausesUndirectedVapor then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.undirectedVapor,0,emissionMat,flowSize) end + if projectileExpansionFlags.matCausesOceanWave then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.oceanWave,0,0,flowSize) end + if projectileExpansionFlags.matCausesSeaFoam then dfhack.maps.spawnFlow(projectile.cur_pos,flowtypes.seaFoam,0,0,flowSize) end + if projectileExpansionFlags.matHasScriptAttached or projectileExpansionFlags.matCausesSyndrome then + local uid,unit = getUnitHitByProjectile(projectile) + if projectileExpansionFlags.matHasScriptAttached then + local script = getScriptFromMaterial(material) + for k,v in ipairs(script) do + if script[k] == "\\UNIT_HIT_ID" then script[k] = unit.id end + if script[k] == "\\LOCATION" then + script[k] = projectile.cur_pos.x + table.insert(script,projectile.cur_pos.y,k+1) + table.insert(script,projectile.cur_pos.z,k+2) + end + end + dfhack.run_script(table.unpack(script)) + end + if projectileExpansionFlags.matCausesSyndrome then syndromeUtil.infectWithSyndrome(unit,syndrome.id) end + end + --if projectileExpansionFlags.matDisappearsOnHit then dfhack.items.remove(projectile) end + end + return true +end + +if ... ~= "disable" then + print("Enabled projectileExpansion.") +else + events.onProjItemCheckImpact.expansion = nil + print("Disabled projectileExpansion.") +end + diff --git a/scripts/removewear.lua b/scripts/removewear.lua new file mode 100644 index 000000000..b6bc959dd --- /dev/null +++ b/scripts/removewear.lua @@ -0,0 +1,56 @@ +-- Resets all items in your fort to 0 wear +-- original author: Laggy +-- edited by expwnent + +local args = {...} + +if args[1] == 'help' then + print([[removewear - this script removes wear from all items, or from individual ones + +removewear all + remove wear from all items +removewear n1 n2 n3 ... + remove wear from items with the given ids. order does not matter +repeat enable 2 months removewear all + remove wear from all items every 2 months. see repeat.lua for details +]]) + do return end +elseif args[1] == 'all' then + local count = 0; + for _,item in ipairs(df.global.world.items.all) do + if (item.wear > 0) then + item:setWear(0) + count = count+1 + end + end + print('removewear removed wear from 'count' objects') +else + local argIndex = 1 + local isCompleted = {} + for i,x in ipairs(args) do + args[i] = tonumber(x) + end + table.sort(args) + for _,item in ipairs(df.global.world.items.all) do + local function loop() + if argIndex > #args then + return + elseif item.id > args[argIndex] then + argIndex = argIndex+1 + loop() + return + elseif item.id == args[argIndex] then + --print('removing wear from item with id ' .. args[argIndex]) + item:setWear(0) + isCompleted[args[argIndex]] = true + argIndex = argIndex+1 + end + end + loop() + end + for _,arg in ipairs(args) do + if isCompleted[arg] ~= true then + print('failed to remove wear from item ' .. arg .. ': could not find item with that id') + end + end +end diff --git a/scripts/repeat.lua b/scripts/repeat.lua new file mode 100644 index 000000000..046e27489 --- /dev/null +++ b/scripts/repeat.lua @@ -0,0 +1,73 @@ +-- repeat.lua +-- repeatedly calls a lua script, eg "repeat enable 1 months cleanowned"; to disable "repeat disable cleanowned" +-- author expwnent +-- vaguely based on a script by Putnam +-- repeat -help for details + +local repeatUtil = require 'plugins.repeatUtil' + +local args = {...} +if args[1] == '-cancel' then + repeatUtil.cancel(args[2]) + return +elseif args[1] == '-help' then + print([[repeat.lua + repeat -help + print this help message + repeat -cancel bob + cancels the repetition with the name bob + repeat -name jim -time delay timeUnits -printResult true -command printArgs 3 1 2 + except for -command, arguments can go in any order + -name sets the name for the purposes of cancelling and making sure you don't schedule the same repeating event twice + if not specified, it's set to the first argument after -command + -time delay timeUnits + delay is some positive integer + timeUnits is some valid time unit for dfhack.timeout(delay,timeUnits,function) + -printResult true + print the results of the command + -printResult false + suppress the results of the command + -command ... + specify the command to be run + ]]) +end + +local name=nil +local time +local timeUnits +local i=1 +local command={} +local printResult=true +while i <= #args do + if args[i] == '-name' then + name = args[i+1] + i = i + 2 + elseif args[i] == '-time' then + time = tonumber(args[i+1]) + timeUnits = args[i+2] + i = i+3 + elseif args[i] == '-command' then + name = name or args[i+1] + for j=i+1,#args,1 do + table.insert(command,args[j]) + end + break + elseif args[i] == '-printResult' then + if args[i+1] == "true" then + printOutput = true + elseif args[i+1] == "false" then + printOutput = false + else + qerror("repeat -printResult " .. args[i+1] .. ": expected true or false") + end + i = i+2 + end +end + +repeatUtil.scheduleEvery(name,time,timeUnits,function() + result = dfhack.run_command(table.unpack(command)) + if printResult then + print(result) + end +end) + diff --git a/scripts/shapechange.lua b/scripts/shapechange.lua new file mode 100644 index 000000000..e46129e81 --- /dev/null +++ b/scripts/shapechange.lua @@ -0,0 +1,97 @@ +-- shapechange.lua +-- transforms unit (by number) into another creature, choice given to user. Syntax is: unitID tickamount maxsize namefilter. A size of 0 is ignored. A length of 0 is also ignored. If no filter, all units will be sorted. A filter of ALL will also work with all units. +-- author Putnam +-- edited by expwnent + +--shapechange gui [unitId] [duration] [maxsize] [namefilter] +--shapechange manual [unitId] [creature name] [caste name] [duration] + +local dialog = require('gui.dialogs') +local script = require('gui.script') +function transform(target,race,caste,length) + if target==nil then + qerror("Not a valid target") + end + local defaultRace = target.enemy.normal_race + local defaultCaste = target.enemy.normal_caste + target.enemy.normal_race = race --that's it??? + target.enemy.normal_caste = caste; --that's it! + if length and length>0 then dfhack.timeout(length,'ticks',function() target.enemy.normal_race = defaultRace target.enemy.normal_caste = defaultCaste end) end +end + +function getBodySize(caste) + return caste.body_size_1[#caste.body_size_1-1] +end + +function selectCreature(unitID,length,size,filter) --taken straight from here, but edited so I can understand it better: https://gist.github.com/warmist/4061959/... again. Also edited for syndromeTrigger, but in a completely different way. + size = size or 0 + filter = filter or "all" + length = length or 2400 + local creatures=df.global.world.raws.creatures.all + local tbl={} + local tunit=df.unit.find(unitID) + for cr_k,creature in ipairs(creatures) do + for ca_k,caste in ipairs(creature.caste) do + local name=caste.caste_name[0] + if name=="" then name="?" end + if (not filter or string.find(name,filter) or string.lower(filter)=="all") and (not size or size>getBodySize(caste) or size<1 and not creature.flags.DOES_NOT_EXIST) then table.insert(tbl,{name,nil,cr_k,ca_k}) end + end + end + table.sort(tbl,function(a,b) return a[1]1 and size or "infinity") + .. " (" + .. size/(getBodySize(df.creature_raw.find(tunit.race).caste[tunit.caste]))*100 + .. "% of current size) for " + ..length + .." ticks (" + ..length/1200 + .." days, ~" + ..length/df.global.enabler.fps + .." seconds)?", + COLOR_LIGHTRED + ) + if ok then dialog.showListPrompt("Creature Selection","Choose creature:",COLOR_WHITE,tbl,f) end + end) +end + +local args = {...} +--unit id, length, size, filter +if args[1] == 'gui' then + selectCreature(tonumber(args[2]),tonumber(args[3]),tonumber(args[4]),args[5]) +else + local race-- = df.creature_raw.find(args[3]) + local raceIndex + for index,raceCandidate in ipairs(df.global.world.raws.creatures.all) do + if raceCandidate.creature_id == args[3] then + raceIndex = index + race = raceCandidate + break + end + end + local caste + local casteIndex + if race then + for index,casteCandidate in ipairs(race.caste) do + if casteCandidate.caste_id == args[4] then + caste = casteCandidate + casteIndex = index + break + end + end + end + if not race or not caste then + print("shapechange error: couldn't find " .. args[3] .. " " .. args[4]) + return + end + transform(df.unit.find(tonumber(args[2])), raceIndex, casteIndex, args[5] and tonumber(args[5])) +end + diff --git a/scripts/skillChange.lua b/scripts/skillChange.lua new file mode 100644 index 000000000..cf27fbee9 --- /dev/null +++ b/scripts/skillChange.lua @@ -0,0 +1,47 @@ +-- skillChange.lua +-- Allows skills to be changed with DFHack. +-- author Putnam +-- edited by expwnent + +--[[ +Args are arranged like this: +unitID SKILL level ADD/SUBTRACT/SET + +SKILL being anything such as DETAILSTONE, level being a number that it will be set to (15 is legendary, 0 is dabbling). + +Add will add skill levels, subtract will subtract, set will place it at exactly the level you put in. +]] + +local utils=require('utils') +local args={...} +local skills=df.unit.find(args[1]).status.current_soul.skills +local skill=df.job_skill[tostring(args[2]):upper()] +local level=tonumber(args[3]) or 0 + +local argfunctions={ + __index=function(t,k) + qerror(k..' is not a valid setting! Valid settings are SET, ADD and SUBTRACT.') + end, + set=function(skills,skill,level) + utils.insert_or_update(skills, {new=true, id=skill, rating=level}, 'id') + end, + add=function(skills,skill,level) + local skillExists,oldSkill=utils.linear_index(skills,skill,'id') + if skillExists then + oldSkill.rating=level+oldSkill.rating + else + argfunctions.set(skills,skill,level) + end + end, + subtract=function(skills,skill,level) + local skillExists,oldSkill=utils.linear_index(skills,skill,'id') + if skillExists then + local newRating=oldSkill['rating'] or 0 + oldSkill.rating=oldSkill.rating-level + if oldSkill.rating<0 or (oldSkill.rating==0 and oldSkill.experience<=0) then skills:erase(skillExists) end + end + end +} + +setmetatable(argfunctions,argfunctions) +argfunctions[args[4]:lower()](skills,skill,level) diff --git a/scripts/skillroll.lua b/scripts/skillroll.lua new file mode 100644 index 000000000..6f8aeae9c --- /dev/null +++ b/scripts/skillroll.lua @@ -0,0 +1,60 @@ +-- skillroll.lua +-- Allows skills to activate lua scripts. +-- author Putnam +-- edited by expwnent + +--[[Example usage: +...syndrome stuff... +[SYN_CLASS:\COMMAND][SYN_CLASS:skillroll][SYN_CLASS:\WORKER_ID] For autoSyndrome/syndromeTrigger. +[SYN_CLASS:MELEE_COMBAT] Can use any skill, including NONE (no bonus) +[SYN_CLASS:20] Rolls uniformly from 1 to 20 inclusive. Skill will be weighted to this value. +[SYN_CLASS:DICEROLL_1] If diceroll ends up as one... +[SYN_CLASS:kill][SYN_CLASS:\SKILL_UNIT_ID] Theoretical kill-given-unit-id command; slayrace doesn't do so. +[SYN_CLASS:DICEROLL_10] If diceroll is between 1 and 10 (2-10, inclusive)... +[SYN_CLASS:force][SYN_CLASS:migrants][SYN_CLASS:player] Force migrants. +[SYN_CLASS:DICEROLL_19] If diceroll is between 10 and 19 (11-19, inclusive)... +[SYN_CLASS:fullheal][SYN_CLASS:\SKILL_UNIT_ID] Fully heals unit. +[SYN_CLASS:DICEROLL_20] If diceroll is at least 20... +[SYN_CLASS:shapechange][SYN_CLASS:\SKILL_UNIT_ID] Turns unit into any creature permanently. + +or from the console +skillroll workerId MELEE_COMBAT 20 DICEROLL_1 kill workerId DICEROLL_10 force migrants player DICEROLL_19 fullheal workerId DICEROLL_20 shapechange workerId +]] + +local args={...} +if args[1]=='dryrun' then + unit=df.global.world.units.all[0] +end +local unit = unit or df.unit.find(args[1]) +rando=rando or dfhack.random.new() +local roll=rando:random(tonumber(args[3])) +if args[2] ~= 'NONE' then + local result=roll+(dfhack.units.getEffectiveSkill(unit,df.job_skill[args[2]])*(tonumber(args[3])/20)) + result = result%1<.5 and math.floor(result) or math.ceil(result) + roll = result +end + +local i=4 +local command={} +local scriptIsFinished +repeat + local arg=args[i] + if arg:find('DICEROLL') then + local dicerollnumber=tonumber(arg:match('%d+')) --yes this is truly naive as hell; I imagine if you put DICEROLL3%moa5oam3 it'll return 353. + if dicerollnumber>=roll then + repeat + i=i+1 + if i<=#args and (not args[i]:find('DICEROLL')) then + if args[i]~='\\SKILL_UNIT_ID' then table.insert(command,args[i]) else table.insert(command,args[1]) end + end + until i>#args or args[i]:find('DICEROLL') + local out = dfhack.run_command(table.unpack(command)) + print(out) + scriptIsFinished=true + else + i=i+1 + end + else + i=i+1 + end +until i>#args or scriptIsFinished diff --git a/scripts/teleport.lua b/scripts/teleport.lua new file mode 100644 index 000000000..e01665d3d --- /dev/null +++ b/scripts/teleport.lua @@ -0,0 +1,36 @@ +-- teleport.lua +-- teleports a unit to a location +-- author Putnam +-- edited by expwnent + +local function teleport(unit,pos) + local unitoccupancy = dfhack.maps.getTileBlock(unit.pos).occupancy[unit.pos.x%16][unit.pos.y%16] + unit.pos.x = pos.x + unit.pos.y = pos.y + unit.pos.z = pos.z + if not unit.flags1.on_ground then unitoccupancy.unit = false else unitoccupancy.unit_grounded = false end +end + +local function getArgsTogether(args) + local settings={pos={}} + for k,v in ipairs(args) do + v=string.lower(v) + if v=="unit" then settings.unitID=tonumber(args[k+1]) end + if v=="x" then settings.pos['x']=tonumber(args[k+1]) end + if v=="y" then settings.pos['y']=tonumber(args[k+1]) end + if v=="z" then settings.pos['z']=tonumber(args[k+1]) end + if v=="showunitid" then print(dfhack.gui.getSelectedUnit(true).id) end + if v=="showpos" then printall(df.global.cursor) end + end + if not settings.pos.x or not settings.pos.y or not settings.pos.z then settings.pos=nil end + if not settings.unitID and not settings.pos.x then qerror("Needs a position, a unit ID or both, but not neither!") end + return settings +end + +local args = {...} +local teleportSettings=getArgsTogether(args) +local unit = teleportSettings.unitID and df.unit.find(teleportSettings.unitID) or dfhack.gui.getSelectedUnit(true) +local pos = teleportSettings.pos and teleportSettings.pos or df.global.cursor + +teleport(unit,pos) + diff --git a/scripts/unit-info-viewer.lua b/scripts/unit-info-viewer.lua new file mode 100644 index 000000000..029db28d5 --- /dev/null +++ b/scripts/unit-info-viewer.lua @@ -0,0 +1,793 @@ +-- unit-info-viewer.lua +-- Displays age, birth, maxage, shearing, milking, grazing, egg laying, body size, and death info about a unit. Recommended keybinding Alt-I +-- version 1.04 +-- original author: Kurik Amudnil +-- edited by expwnent + +local gui = require 'gui' +local widgets =require 'gui.widgets' +local utils = require 'utils' + +local DEBUG = false +if DEBUG then print('-----') end + +local pens = { + BLACK = dfhack.pen.parse{fg=COLOR_BLACK,bg=0}, + BLUE = dfhack.pen.parse{fg=COLOR_BLUE,bg=0}, + GREEN = dfhack.pen.parse{fg=COLOR_GREEN,bg=0}, + CYAN = dfhack.pen.parse{fg=COLOR_CYAN,bg=0}, + RED = dfhack.pen.parse{fg=COLOR_RED,bg=0}, + MAGENTA = dfhack.pen.parse{fg=COLOR_MAGENTA,bg=0}, + BROWN = dfhack.pen.parse{fg=COLOR_BROWN,bg=0}, + GREY = dfhack.pen.parse{fg=COLOR_GREY,bg=0}, + DARKGREY = dfhack.pen.parse{fg=COLOR_DARKGREY,bg=0}, + LIGHTBLUE = dfhack.pen.parse{fg=COLOR_LIGHTBLUE,bg=0}, + LIGHTGREEN = dfhack.pen.parse{fg=COLOR_LIGHTGREEN,bg=0}, + LIGHTCYAN = dfhack.pen.parse{fg=COLOR_LIGHTCYAN,bg=0}, + LIGHTRED = dfhack.pen.parse{fg=COLOR_LIGHTRED,bg=0}, + LIGHTMAGENTA = dfhack.pen.parse{fg=COLOR_LIGHTMAGENTA,bg=0}, + YELLOW = dfhack.pen.parse{fg=COLOR_YELLOW,bg=0}, + WHITE = dfhack.pen.parse{fg=COLOR_WHITE,bg=0}, +} + +function getUnit_byID(id) -- get unit by id from units.all via binsearch + if type(id) == 'number' then + -- (vector,key,field,cmpfun,min,max) { item/nil , found true/false , idx/insert at } + return utils.binsearch(df.global.world.units.all,id,'id') + end +end + +function getUnit_byVS(silent) -- by view screen mode + silent = silent or false + -- if not world loaded, return nil ? + local u,tmp -- u: the unit to return ; tmp: temporary for intermediate tests/return values + local v = dfhack.gui.getCurViewscreen() + u = dfhack.gui.getSelectedUnit(true) -- supports gui scripts/plugin that provide a hook for getSelectedUnit() + if u then + return u + -- else: contexts not currently supported by dfhack.gui.getSelectedUnit() + elseif df.viewscreen_dwarfmodest:is_instance(v) then + tmp = df.global.ui.main.mode + if tmp == 17 or tmp == 42 or tmp == 43 then + -- context: @dwarfmode/QueryBuiding/Some/Cage -- (q)uery cage + -- context: @dwarfmode/ZonesPenInfo/AssignUnit -- i (zone) -> pe(N) + -- context: @dwarfmode/ZonesPitInfo -- i (zone) -> (P)it + u = df.global.ui_building_assign_units[df.global.ui_building_item_cursor] + elseif tmp == 49 and df.global.ui.burrows.in_add_units_mode then + -- @dwarfmode/Burrows/AddUnits + u = df.global.ui.burrows.list_units[ df.global.ui.burrows.unit_cursor_pos ] + + elseif df.global.ui.follow_unit ~= -1 then + -- context: follow unit mode + u = getUnit_byID(df.global.ui.follow_unit) + end -- end viewscreen_dwarfmodest + elseif df.viewscreen_petst:is_instance(v) then + -- context: @pet/List/Unit -- z (status) -> animals + if v.mode == 0 then -- List + if not v.is_vermin[v.cursor] then + u = v.animal[v.cursor].unit + end + --elseif v.mode = 1 then -- training knowledge (no unit reference) + elseif v.mode == 2 then -- select trainer + u = v.trainer_unit[v.trainer_cursor] + end + elseif df.viewscreen_layer_workshop_profilest:is_instance(v) then + -- context: @layer_workshop_profile/Unit -- (q)uery workshop -> (P)rofile -- df.global.ui.main.mode == 17 + u = v.workers[v.layer_objects[0].cursor] + elseif df.viewscreen_layer_overall_healthst:is_instance(v) then + -- context @layer_overall_health/Units -- z -> health + u = v.unit[v.layer_objects[0].cursor] + elseif df.viewscreen_layer_militaryst:is_instance(v) then + local PG_ASSIGNMENTS = 0 + local PG_EQUIPMENT = 2 + local TB_POSITIONS = 1 + local TB_CANDIDATES = 2 + -- layer_objects[0: squads list; 1: positions list; 2: candidates list] + -- page 0:positions/assignments 1:alerts 2:equipment 3:uniforms 4:supplies 5:ammunition + if v.page == PG_ASSIGNMENTS and v.layer_objects[TB_CANDIDATES].enabled and v.layer_objects[TB_CANDIDATES].active then + -- context: @layer_military/Positions/Position/Candidates -- m -> Candidates + u = v.positions.candidates[v.layer_objects[TB_CANDIDATES].cursor] + elseif v.page == PG_ASSIGNMENTS and v.layer_objects[TB_POSITIONS].enabled and v.layer_objects[TB_POSITIONS].active then + -- context: @layer_military/Positions/Position -- m -> Positions + u = v.positions.assigned[v.layer_objects[TB_POSITIONS].cursor] + elseif v.page == PG_EQUIPMENT and v.layer_objects[TB_POSITIONS].enabled and v.layer_objects[TB_POSITIONS].active then + -- context: @layer_military/Equip/Customize/View/Position -- m -> (e)quip -> Positions + -- context: @layer_military/Equip/Uniform/Positions -- m -> (e)quip -> assign (U)niforms -> Positions + u = v.equip.units[v.layer_objects[TB_POSITIONS].cursor] + end + elseif df.viewscreen_layer_noblelistst:is_instance(v) then + if v.mode == 0 then + -- context: @layer_noblelist/List -- (n)obles + u = v.info[v.layer_objects[v.mode].cursor].unit + elseif v.mode == 1 then + -- context: @layer_noblelist/Appoint -- (n)obles -> (r)eplace + u = v.candidates[v.layer_objects[v.mode].cursor].unit + end + elseif df.viewscreen_unitst:is_instance(v) then + -- @unit -- (v)unit -> z ; loo(k) -> enter ; (n)obles -> enter ; others + u = v.unit + elseif df.viewscreen_customize_unitst:is_instance(v) then + -- @customize_unit -- @unit -> y + u = v.unit + elseif df.viewscreen_layer_unit_healthst:is_instance(v) then + -- @layer_unit_health -- @unit -> h ; @layer_overall_health/Units -> enter + if df.viewscreen_layer_overall_healthst:is_instance(v.parent) then + -- context @layer_overall_health/Units -- z (status)-> health + u = v.parent.unit[v.parent.layer_objects[0].cursor] + elseif df.viewscreen_unitst:is_instance(v.parent) then + -- @unit -- (v)unit -> z ; loo(k) -> enter ; (n)obles -> enter ; others + u = v.parent.unit + end + elseif df.viewscreen_textviewerst:is_instance(v) then + -- @textviewer -- @unit -> enter (thoughts and preferences) + if df.viewscreen_unitst:is_instance(v.parent) then + -- @unit -- @unit -> enter (thoughts and preferences) + u = v.parent.unit + elseif df.viewscreen_itemst:is_instance(v.parent) then + tmp = v.parent.entry_ref[v.parent.cursor_pos] + if df.general_ref_unit:is_instance(tmp) then -- general_ref_unit and derived ; general_ref_contains_unitst ; others? + u = getUnit_byID(tmp.unit_id) + end + elseif df.viewscreen_dwarfmodest:is_instance(v.parent) then + tmp = df.global.ui.main.mode + if tmp == 24 then -- (v)iew units {g,i,p,w} -> z (thoughts and preferences) + -- context: @dwarfmode/ViewUnits/... + --if df.global.ui_selected_unit > -1 then -- -1 = 'no units nearby' + u = df.global.world.units.active[df.global.ui_selected_unit] + --end + elseif tmp == 25 then -- loo(k) unit -> enter (thoughs and preferences) + -- context: @dwarfmode/LookAround/Unit + tmp = df.global.ui_look_list.items[df.global.ui_look_cursor] + if tmp.type == 2 then -- 0:item 1:terrain >>2: unit<< 3:building 4:colony/vermin 7:spatter + u = tmp.unit + end + end + elseif df.viewscreen_unitlistst:is_instance(v.parent) then -- (u)nit list -> (v)iew unit (not citizen) + -- context: @unitlist/Citizens ; @unitlist/Livestock ; @unitlist/Others ; @unitlist/Dead + u = v.parent.units[v.parent.page][ v.parent.cursor_pos[v.parent.page] ] + end + end -- switch viewscreen + if not u and not silent then + dfhack.printerr('No unit is selected in the UI or context not supported.') + end + return u +end -- getUnit_byVS() + +--http://lua-users.org/wiki/StringRecipes ---------- +function str2FirstUpper(str) + return str:gsub("^%l", string.upper) +end + +-------------------------------------------------- +--http://lua-users.org/wiki/StringRecipes ---------- +local function tchelper(first, rest) + return first:upper()..rest:lower() +end + +-- Add extra characters to the pattern if you need to. _ and ' are +-- found in the middle of identifiers and English words. +-- We must also put %w_' into [%w_'] to make it handle normal stuff +-- and extra stuff the same. +-- This also turns hex numbers into, eg. 0Xa7d4 +function str2TitleCase(str) + return str:gsub("(%a)([%w_']*)", tchelper) +end + +-------------------------------------------------- +--isBlank suggestion by http://stackoverflow.com/a/10330861 +function isBlank(x) + x = tostring(x) or "" + -- returns (not not match_begin), _ = match_end => not not true , _ => true + -- returns not not nil => false (no match) + return not not x:find("^%s*$") +end + +--http://lua-users.org/wiki/StringRecipes (removed indents since I am not using them) +function wrap(str, limit)--, indent, indent1) + --indent = indent or "" + --indent1 = indent1 or indent + local limit = limit or 72 + local here = 1 ---#indent1 + return str:gsub("(%s+)()(%S+)()", --indent1..str:gsub( + function(sp, st, word, fi) + if fi-here > limit then + here = st -- - #indent + return "\n"..word --..indent..word + end + end) +end + + +-------------------------------------------------- +---------------------- Time ---------------------- +-------------------------------------------------- +local TU_PER_DAY = 1200 +--[[ +if advmode then TU_PER_DAY = 86400 ? or only for cur_year_tick? +advmod_TU / 72 = ticks +--]] +local TU_PER_MONTH = TU_PER_DAY * 28 +local TU_PER_YEAR = TU_PER_MONTH * 12 + +local MONTHS = { + 'Granite', + 'Slate', + 'Felsite', + 'Hematite', + 'Malachite', + 'Galena', + 'Limestone', + 'Sandstone', + 'Timber', + 'Moonstone', + 'Opal', + 'Obsidian', +} +Time = defclass(Time) +function Time:init(args) + self.year = args.year or 0 + self.ticks = args.ticks or 0 +end +function Time:getDays() -- >>float<< Days as age (including years) + return self.year * 336 + (self.ticks / TU_PER_DAY) +end +function Time:getMonths() -- >>int<< Months as age (not including years) + return math.floor (self.ticks / TU_PER_MONTH) +end +function Time:getMonthStr() -- Month as date + return MONTHS[self:getMonths()+1] or 'error' +end +function Time:getDayStr() -- Day as date + local d = math.floor ( (self.ticks % TU_PER_MONTH) / TU_PER_DAY ) + 1 + if d == 11 or d == 12 or d == 13 then + d = tostring(d)..'th' + elseif d % 10 == 1 then + d = tostring(d)..'st' + elseif d % 10 == 2 then + d = tostring(d)..'nd' + elseif d % 10 == 3 then + d = tostring(d)..'rd' + else + d = tostring(d)..'th' + end + return d +end +--function Time:__add() +--end +function Time:__sub(other) + if DEBUG then print(self.year,self.ticks) end + if DEBUG then print(other.year,other.ticks) end + if self.ticks < other.ticks then + return Time{ year = (self.year - other.year - 1) , ticks = (TU_PER_YEAR + self.ticks - other.ticks) } + else + return Time{ year = (self.year - other.year) , ticks = (self.ticks - other.ticks) } + end +end +-------------------------------------------------- +-------------------------------------------------- + +-------------------------------------------------- +-------------------- Identity -------------------- +-------------------------------------------------- +local SINGULAR = 0 +local PLURAL = 1 +--local POSSESSIVE = 2 + +local PRONOUNS = { + [0]='She', + [1]='He', + [2]='It', +} +local BABY = 0 +local CHILD = 1 +local ADULT = 2 + +local TRAINING_LEVELS = { + [0] = ' (Semi-Wild)', -- Semi-wild + ' (Trained)', -- Trained + ' (-Trained-)', -- Well-trained + ' (+Trained+)', -- Skillfully trained + ' (*Trained*)', -- Expertly trained + ' ('..string.char(240)..'Trained'..string.char(240)..')', -- Exceptionally trained + ' ('..string.char(15)..'Trained'..string.char(15)..')', -- Masterully Trained + ' (Tame)', -- Domesticated + '', -- undefined + '', -- wild/untameable +} + +local DEATH_TYPES = { + [0] = ' died of old age', -- OLD_AGE + ' starved to death', -- HUNGER + ' died of dehydration', -- THIRST + ' was shot and killed', -- SHOT + ' bled to death', -- BLEED + ' drowned', -- DROWN + ' suffocated', -- SUFFOCATE + ' was struck down', -- STRUCK_DOWN + ' was scuttled', -- SCUTTLE + " didn't survive a collision", -- COLLISION + ' took a magma bath', -- MAGMA + ' took a magma shower', -- MAGMA_MIST + ' was incinerated by dragon fire', -- DRAGONFIRE + ' was killed by fire', -- FIRE + ' experienced death by SCALD', -- SCALD + ' was crushed by cavein', -- CAVEIN + ' was smashed by a drawbridge', -- DRAWBRIDGE + ' was killed by falling rocks', -- FALLING_ROCKS + ' experienced death by CHASM', -- CHASM + ' experienced death by CAGE', -- CAGE + ' was murdered', -- MURDER + ' was killed by a trap', -- TRAP + ' vanished', -- VANISH + ' experienced death by QUIT', -- QUIT + ' experienced death by ABANDON', -- ABANDON + ' suffered heat stroke', -- HEAT + ' died of hypothermia', -- COLD + ' experienced death by SPIKE', -- SPIKE + ' experienced death by ENCASE_LAVA', -- ENCASE_LAVA + ' experienced death by ENCASE_MAGMA', -- ENCASE_MAGMA + ' was preserved in ice', -- ENCASE_ICE + ' became headless', -- BEHEAD + ' was crucified', -- CRUCIFY + ' experienced death by BURY_ALIVE', -- BURY_ALIVE + ' experienced death by DROWN_ALT', -- DROWN_ALT + ' experienced death by BURN_ALIVE', -- BURN_ALIVE + ' experienced death by FEED_TO_BEASTS', -- FEED_TO_BEASTS + ' experienced death by HACK_TO_PIECES', -- HACK_TO_PIECES + ' choked on air', -- LEAVE_OUT_IN_AIR + ' experienced death by BOIL', -- BOIL + ' melted', -- MELT + ' experienced death by CONDENSE', -- CONDENSE + ' experienced death by SOLIDIFY', -- SOLIDIFY + ' succumbed to infection', -- INFECTION + "'s ghost was put to rest with a memorial", -- MEMORIALIZE + ' scared to death', -- SCARE + ' experienced death by DARKNESS', -- DARKNESS + ' experienced death by COLLAPSE', -- COLLAPSE + ' was drained of blood', -- DRAIN_BLOOD + ' was slaughtered', -- SLAUGHTER + ' became roadkill', -- VEHICLE + ' killed by a falling object', -- FALLING_OBJECT +} + +--GHOST_TYPES[unit.relations.ghost_info.type].." This spirit has not been properly memorialized or buried." +local GHOST_TYPES = { + [0]="A murderous ghost.", + "A sadistic ghost.", + "A secretive ghost.", + "An energetic poltergeist.", + "An angry ghost.", + "A violent ghost.", + "A moaning spirit returned from the dead. It will generally trouble one unfortunate at a time.", + "A howling spirit. The ceaseless noise is making sleep difficult.", + "A troublesome poltergeist.", + "A restless haunt, generally troubling past acquaintances and relatives.", + "A forlorn haunt, seeking out known locations or drifting around the place of death.", +} + + +Identity = defclass(Identity) +function Identity:init(args) + local u = args.unit + self.ident = dfhack.units.getIdentity(u) + + self.unit = u + self.name = dfhack.TranslateName( dfhack.units.getVisibleName(u) ) + self.name_en = dfhack.TranslateName( dfhack.units.getVisibleName(u) , true) + self.raw_prof = dfhack.units.getProfessionName(u) + self.pronoun = PRONOUNS[u.sex] or 'It' + + if self.ident then + self.birth_date = Time{year = self.ident.birth_year, ticks = self.ident.birth_second} + self.race_id = self.ident.race + self.caste_id = self.ident.caste + if self.ident.histfig_id > -1 then + self.hf_id = self.ident.histfig_id + end + else + self.birth_date = Time{year = self.unit.relations.birth_year, ticks = self.unit.relations.birth_time} + self.race_id = u.race + self.caste_id = u.caste + if u.hist_figure_id > -1 then + self.hf_id = u.hist_figure_id + end + end + self.race = df.global.world.raws.creatures.all[self.race_id] + self.caste = self.race.caste[self.caste_id] + + self.isCivCitizen = (df.global.ui.civ_id == u.civ_id) + self.isStray = u.flags1.tame --self.isCivCitizen and not u.flags1.merchant + self.cur_date = Time{year = df.global.cur_year, ticks = df.global.cur_year_tick} + + + ------------ death ------------ + self.dead = u.flags1.dead + self.ghostly = u.flags3.ghostly + self.undead = u.enemy.undead + + if self.dead and self.hf_id then -- dead-dead not undead-dead + local events = df.global.world.history.events2 + local e + for idx = #events - 1,0,-1 do + e = events[idx] + if df.history_event_hist_figure_diedst:is_instance(e) and e.victim_hf == self.hf_id then + self.death_event = e + break + end + end + end + if u.counters.death_id > -1 then -- if undead/ghostly dead or dead-dead + self.death_info = df.global.world.deaths.all[u.counters.death_id] + if not self.death_info.flags.discovered then + self.missing = true + end + end + -- slaughtered? + if self.death_event then + self.death_date = Time{year = self.death_event.year, ticks = self.death_event.seconds} + elseif self.death_info then + self.death_date = Time{year = self.death_info.event_year, ticks = self.death_info.event_time} + end + -- age now or age death? + if self.dead and self.death_date then -- if cursed with no age? -- if hacked a ressurection, such that they aren't dead anymore, don't use the death date + self.age_time = self.death_date - self.birth_date + else + self.age_time = self.cur_date - self.birth_date + end + if DEBUG then print( self.age_time.year,self.age_time.ticks,self.age_time:getMonths() ) end + ---------- ---------- ---------- + + + ---------- caste_name ---------- + self.caste_name = {} + if isBlank(self.caste.caste_name[SINGULAR]) then + self.caste_name[SINGULAR] = self.race.name[SINGULAR] + else + self.caste_name[SINGULAR] = self.caste.caste_name[SINGULAR] + end + if isBlank(self.caste.caste_name[PLURAL]) then + self.caste_name[PLURAL] = self.race.name[PLURAL] + else + self.caste_name[PLURAL] = self.caste.caste_name[PLURAL] + end + ---------- ---------- ---------- + + --------- growth_status --------- + -- 'baby_age' is the age the baby becomes a child + -- 'child_age' is the age the child becomes an adult + if self.age_time.year >= self.caste.misc.child_age then -- has child come of age becoming adult? + self.growth_status = ADULT + elseif self.age_time.year >= self.caste.misc.baby_age then -- has baby come of age becoming child? + self.growth_status = CHILD + else + self.growth_status = BABY + end + ---------- ---------- ---------- + + -------- aged_caste_name -------- + local caste_name, race_name + if self.growth_status == ADULT then + caste_name = self.caste.caste_name[SINGULAR] + race_name = self.race.name[SINGULAR] + elseif self.growth_status == CHILD then + caste_name = self.caste.child_name[SINGULAR] + race_name = self.race.general_child_name[SINGULAR] + else --if self.growth_status == BABY then + caste_name = self.caste.baby_name[SINGULAR] + race_name = self.race.general_baby_name[SINGULAR] + end + self.aged_caste_name = {} + if isBlank(caste_name[SINGULAR]) then + self.aged_caste_name[SINGULAR] = race_name[SINGULAR] + else + self.aged_caste_name[SINGULAR] = caste_name[SINGULAR] + end + if isBlank(caste_name[PLURAL]) then + self.aged_caste_name[PLURAL] = race_name[PLURAL] + else + self.aged_caste_name[PLURAL] = caste_name[PLURAL] + end + ---------- ---------- ---------- + + ----- Profession adjustment ----- + local prof = self.raw_prof + if self.undead then + prof = str2TitleCase( self.caste_name[SINGULAR] ) + if isBlank(u.enemy.undead.anon_7) then + prof = prof..' Corpse' + else + prof = u.enemy.undead.anon_7 -- a reanimated body part will use this string instead + end + end + --[[ + if self.ghostly then + prof = 'Ghostly '..prof + end + --]] + if u.curse.name_visible and not isBlank(u.curse.name) then + prof = prof..' '..u.curse.name + end + if isBlank(self.name) then + if self.isStray then + prof = 'Stray '..prof --..TRAINING_LEVELS[u.training_level] + end + end + self.prof = prof + ---------- ---------- ---------- +end +-------------------------------------------------- +-------------------------------------------------- +--[[ + prof_id ? + group_id ? + fort_race_id + fort_civ_id + --fort_group_id? +--]] + + +UnitInfoViewer = defclass(UnitInfoViewer, gui.FramedScreen) +UnitInfoViewer.focus_path = 'unitinfoviewer' -- -> dfhack/lua/unitinfoviewer +UnitInfoViewer.ATTRS={ + frame_style = gui.GREY_LINE_FRAME, + frame_inset = 2, -- used by init + frame_outset = 1,--3, -- new, used by init; 0 = full screen, suggest 0, 1, or 3 or maybe 5 + --frame_title , -- not used + --frame_width,frame_height calculated by frame inset and outset in init +} +function UnitInfoViewer:init(args) -- requires args.unit + --if DEBUG then print('-----') end + local x,y = dfhack.screen.getWindowSize() + -- what if inset or outset are defined as {l,r,t,b}? + x = x - 2*(self.frame_inset + 1 + self.frame_outset) -- 1=frame border thickness + y = y - 2*(self.frame_inset + 1 + self.frame_outset) -- 1=frame border thickness + self.frame_width = args.frame_width or x + self.frame_height = args.frame_height or y + self.text = {} + if df.unit:is_instance(args.unit) then + self.ident = Identity{ unit = args.unit } + if not isBlank(self.ident.name_en) then + self.frame_title = 'Unit: '..self.ident.name_en + elseif not isBlank(self.ident.prof) then + self.frame_title = 'Unit: '..self.ident.prof + if self.ident.isStray then + self.frame_title = self.frame_title..TRAINING_LEVELS[self.ident.unit.training_level] + end + end + self:chunk_Name() + self:chunk_Description() + if not (self.ident.dead or self.ident.undead or self.ident.ghostly) then --not self.dead + if self.ident.isCivCitizen then + self:chunk_Age() + self:chunk_MaxAge() + end + if self.ident.isStray then + if self.ident.growth_status == ADULT then + self:chunk_Milkable() + end + self:chunk_Grazer() + if self.ident.growth_status == ADULT then + self:chunk_Shearable() + end + if self.ident.growth_status == ADULT then + self:chunk_EggLayer() + end + end + self:chunk_BodySize() + elseif self.ident.ghostly then + self:chunk_Dead() + self:chunk_Ghostly() + elseif self.ident.undead then + self:chunk_BodySize() + self:chunk_Dead() + else + self:chunk_Dead() + end + else + self:insert_chunk("No unit is selected in the UI or context not supported.",pens.LIGHTRED) + end + self:addviews{ widgets.Label{ frame={yalign=0}, text=self.text } } +end +function UnitInfoViewer:onInput(keys) + if keys.LEAVESCREEN or keys.SELECT then + self:dismiss() + end +end +function UnitInfoViewer:onGetSelectedUnit() + return self.ident.unit +end +function UnitInfoViewer:insert_chunk(str,pen) + local lines = utils.split_string( wrap(str,self.frame_width) , NEWLINE ) + for i = 1,#lines do + table.insert(self.text,{text=lines[i],pen=pen}) + table.insert(self.text,NEWLINE) + end + table.insert(self.text,NEWLINE) +end +function UnitInfoViewer:chunk_Name() + local i = self.ident + local u = i.unit + local prof = i.prof + local color = dfhack.units.getProfessionColor(u) + local blurb + if i.ghostly then + prof = 'Ghostly '..prof + end + if i.isStray then + prof = prof..TRAINING_LEVELS[u.training_level] + end + if isBlank(i.name) then + if isBlank(prof) then + blurb = 'I am a mystery' + else + blurb = prof + end + else + if isBlank(prof) then + blurb=i.name + else + blurb=i.name..', '..prof + end + end + self:insert_chunk(blurb,dfhack.pen.parse{fg=color,bg=0}) +end +function UnitInfoViewer:chunk_Description() + local dsc = self.ident.caste.description + if not isBlank(dsc) then + self:insert_chunk(dsc,pens.WHITE) + end +end + +function UnitInfoViewer:chunk_Age() + local i = self.ident + local age_str -- = '' + if i.age_time.year > 1 then + age_str = tostring(i.age_time.year)..' years old' + elseif i.age_time.year > 0 then -- == 1 + age_str = '1 year old' + else --if age_time.year == 0 then + local age_m = i.age_time:getMonths() -- math.floor + if age_m > 1 then + age_str = tostring(age_m)..' months old' + elseif age_m > 0 then -- age_m == 1 + age_str = '1 month old' + else -- if age_m == 0 then -- and age_m < 0 which would be an error + age_str = 'a newborn' + end + end + local blurb = i.pronoun..' is '..age_str + if i.race_id == df.global.ui.race_id then + blurb = blurb..', born on the '..i.birth_date:getDayStr()..' of '..i.birth_date:getMonthStr()..' in the year '..tostring(i.birth_date.year)..PERIOD + else + blurb = blurb..PERIOD + end + self:insert_chunk(blurb,pens.YELLOW) +end + +function UnitInfoViewer:chunk_MaxAge() + local i = self.ident + local maxage = math.floor( (i.caste.misc.maxage_max + i.caste.misc.maxage_min)/2 ) + --or i.unit.curse.add_tags1.NO_AGING hidden ident? + if i.caste.misc.maxage_min == -1 then + maxage = ' die of unnatural causes.' + elseif maxage == 0 then + maxage = ' die at a very young age.' + elseif maxage == 1 then + maxage = ' live about '..tostring(maxage)..' year.' + else + maxage = ' live about '..tostring(maxage)..' years.' + end + --' is expected to '.. + local blurb = str2FirstUpper(i.caste_name[PLURAL])..maxage + self:insert_chunk(blurb,pens.DARKGREY) +end +function UnitInfoViewer:chunk_Grazer() + if self.ident.caste.flags.GRAZER then + local blurb = 'Grazing satisfies '..tostring(self.ident.caste.misc.grazer)..' units of hunger.' + self:insert_chunk(blurb,pens.LIGHTGREEN) + end +end +function UnitInfoViewer:chunk_EggLayer() + local caste = self.ident.caste + if caste.flags.LAYS_EGGS then + local clutch = math.floor( (caste.misc.clutch_size_max + caste.misc.clutch_size_min)/2 ) + local blurb = 'Lays clutches of about '..tostring(clutch) + if clutch > 1 then + blurb = blurb..' eggs.' + else + blurb = blurb..' egg.' + end + self:insert_chunk(blurb,pens.GREEN) + end +end +function UnitInfoViewer:chunk_Milkable() + local i = self.ident + if i.caste.flags.MILKABLE then + local milk = dfhack.matinfo.decode( i.caste.extracts.milkable_mat , i.caste.extracts.milkable_matidx ) + if milk then + local days,seconds = math.modf ( i.caste.misc.milkable / TU_PER_DAY ) + days = (seconds > 0) and (tostring(days)..' to '..tostring(days + 1)) or tostring(days) + --local blurb = pronoun..' produces '..milk:toString()..' every '..days..' days.' + local blurb = (i.growth_status == ADULT) and (i.pronoun..' secretes ') or str2FirstUpper(i.caste_name[PLURAL])..' secrete ' + blurb = blurb..milk:toString()..' every '..days..' days.' + self:insert_chunk(blurb,pens.LIGHTCYAN) + end + end +end +function UnitInfoViewer:chunk_Shearable() + local i = self.ident + local mat_types = i.caste.body_info.materials.mat_type + local mat_idxs = i.caste.body_info.materials.mat_index + local mat_info, blurb + for idx,mat_type in ipairs(mat_types) do + mat_info = dfhack.matinfo.decode(mat_type,mat_idxs[idx]) + if mat_info and mat_info.material.flags.YARN then + blurb = (i.growth_status == ADULT) and (i.pronoun..' produces ') or str2FirstUpper(i.caste_name[PLURAL])..' produce ' + blurb = blurb..mat_info:toString()..PERIOD + self:insert_chunk(blurb,pens.BROWN) + end + end +end +function UnitInfoViewer:chunk_BodySize() + local i = self.ident + local pat = i.unit.body.physical_attrs + local blurb = i.pronoun..' appears to be about '..pat.STRENGTH.value..':'..pat.AGILITY.value..' cubic decimeters in size.' + self:insert_chunk(blurb,pens.LIGHTBLUE) +end +function UnitInfoViewer:chunk_Ghostly() + local blurb = GHOST_TYPES[self.ident.unit.relations.ghost_info.type].." This spirit has not been properly memorialized or buried." + self:insert_chunk(blurb,pens.LIGHTMAGENTA) + -- Arose in relations.curse_year curse_time +end +function UnitInfoViewer:chunk_Dead() + local i = self.ident + local blurb, str, pen + if i.missing then --dfhack.units.isDead(unit) + str = ' is missing.' + pen = pens.WHITE + elseif i.death_event then + --str = "The Caste_name Unit_Name died in year #{e.year}" + --str << " (cause: #{e.death_cause.to_s.downcase})," + --str << " killed by the #{e.slayer_race_tg.name[0]} #{e.slayer_hf_tg.name}" if e.slayer_hf != -1 + --str << " using a #{df.world.raws.itemdefs.weapons[e.weapon.item_subtype].name}" if e.weapon.item_type == :WEAPON + --str << ", shot by a #{df.world.raws.itemdefs.weapons[e.weapon.bow_item_subtype].name}" if e.weapon.bow_item_type == :WEAPON + str = DEATH_TYPES[i.death_event.death_cause]..PERIOD + pen = pens.MAGENTA + elseif i.death_info then + --str = "The #{u.race_tg.name[0]}" + --str << " #{u.name}" if u.name.has_name + --str << " died" + --str << " in year #{death_info.event_year}" if death_info + --str << " (cause: #{u.counters.death_cause.to_s.downcase})," if u.counters.death_cause != -1 + --str << " killed by the #{killer.race_tg.name[0]} #{killer.name}" if killer + str = DEATH_TYPES[i.death_info.death_cause]..PERIOD + pen = pens.MAGENTA + elseif i.unit.flags2.slaughter and i.unit.flags2.killed then + str = ' was slaughtered.' + pen = pens.MAGENTA + else + str = ' is dead.' + pen = pens.MAGENTA + end + if i.undead or i.ghostly then + str = ' is undead.' + pen = pens.GREY + end + blurb = 'The '..i.prof -- assume prof is not blank + if not isBlank(i.name) then + blurb = blurb..', '..i.name + end + blurb = blurb..str + self:insert_chunk(blurb,pen) +end + +-- only show if UnitInfoViewer isn't the current focus +if dfhack.gui.getCurFocus() ~= 'dfhack/lua/'..UnitInfoViewer.focus_path then + local gui_no_unit = false -- show if not found? + local unit = getUnit_byVS(gui_no_unit) -- silent? or let the gui display + if unit or gui_no_unit then + local kan_viewscreen = UnitInfoViewer{unit = unit} + kan_viewscreen:show() + end +end +