Bay 12 Games Forum

Please login or register.

Login with username, password and session length
Advanced search  

Author Topic: [DFhack 43.05r1] Atkana's Scripts (Adventure as anything, Change appearance)  (Read 699 times)

Atkana

  • Bay Watcher
  • [CURIOUSBEAST]
    • View Profile


While playing adventure mode I've often made scripts to improve my adventuring experience, and since I've now reached the skill level that I'm capable of making somewhat presentable scripts, I figured I'd share the ones I've made that might be of use to some people.

Table of contents: What do you mean, "there are no anchors"?
  • Adventurer Freedom + AdvTweaks - A DFhack command-prompt + light raw mod that allows you to adventure as any creature, from any civilization. Now with added automaticness!
  • View Allegiance - A DFhack script that gives a readable report about a unit's entity links, past and present
  • Change Appearance - A DFhack command-prompt to change a unit's appearance.

Adventurer Freedom - Download
Brief Feature Overview:
  • Play as any creature, including world-unique generated creatures (contains spoilers)
  • Start as a member of any Civilization
  • Start as an Outsider of any race
  • Begin with any natural skills you should have (bugfix of vanilla bug)
  • Edit the size of your attribute and skill pool
At the moment it runs through the DFhack command-prompt but I hope one day (when I actually figure GUI stuff out) it'll have a swanky GUI interface, or at the very least use the Dwarf Fortress window
I was delayed long enough before uploading the mod that there's now a whole new addition. First, there's the original command-prompt version, and now there's an additional new script that performs some of its features automatically, as well as some new features. The new automatic version automatically adds every creature to the Intelligent Wilderness Animal section, automatically makes every civ available to start from, automatically patches in any natural skills and automatically unlocks the skill list to make every skill available to purchase. Automatically automatically automatically. It also has a bunch of settings that can be altered to toggle certain features on/off, among other things. With all the extra stuff added to the menus, it's worth remembering that Page Up and Page Down can be used to help navigate :P

The download also contains a small tweak script to allow for playing as non-standard race to be a more enjoyable experience, like giving your adventurer the ability to open doors, learn skills, and speak. The features of the tweak script are fully customizable and can be toggled on/off.

I'd include the readme in this post but it sets me over the character limit ^_^;

View Allegiance
One of the things that always nags me is that there's generally no way to ask characters what groups they belong to - you usually only end up finding out when the citizens of the nearby hamlet start calling you a murderer for killing the person you thought was a bandit, or when your new companion suddenly decides to start a massacre in the town market because it turns out they're a sworn enemy of the people who live there. So I made this script. It prints all the entities a person has links to and the nature of those links, so used in conjunction with information gathered ingame and perhaps a bit of legends viewer it allows you to make informed decisions on how to act.
Spoiler (click to show/hide)

Change Appearance
A simple command-prompt script I made to edit a unit's appearance. Kind of self-explanatory... It does enough to change the descriptions in the unit view, though there might be something related to resizing a creature based off the size of its body parts that I'm not aware of which doesn't get updated unless something specific is done (and knowing Dwarf Fortress, that probably exists). If you're aware of anything like this please point me in the right direction.
Spoiler (click to show/hide)
Note: There's a GUI version of this in the works, which also features changing personalities and beliefs. The current iteration is here, with a proper release coming who knows when.


Shoutouts to Max for teaching me some of the stuff I needed to know for making Adventurer Freedom, as well as letting me steal his code :P

Feel free to post if you have any questions/know how I could be doing things better/run into problems - I do my best to make sure my scripts work properly before I share them, but they could all definitely benefit from some decent field testing.
« Last Edit: July 07, 2017, 01:40:37 pm by Atkana »
Logged

Max™

  • Bay Watcher
  • [CULL:SQUARE]
    • View Profile

Hey that is cool shit and I'm totally going to incorporate what you did there with the appearance stuff since it's exactly what I was trying to unlazy my ass and finish working on.
Logged
Engraved here is a rendition of an image of the Dwarf Fortress learning curve. All craftsdwarfship is of the highest quality. It depicts an obsidian overhang which menaces with spikes of obsidian and tears. Carved on the overhang is an image of Toady One and the players. The players are curled up in a fetal position. Toady One is laughing. The players are burning.
The VectorCurses+1 tileset strikes the square set and the severed part sails off in an arc!

Max™

  • Bay Watcher
  • [CULL:SQUARE]
    • View Profile

Ok, so I noticed we were both being silly here and had a great idea, I tried to use the work you did to get it done but the only thing you really lack with scripting is familiarity with the dfhack api and such cause there is some shit in there that I can immediately see better ways to achieve (like replacing "if df.global.world.raws.creature.all[k] matches foo then bar" stuff with df.creature_raw.find(k) and so on) but I ran out of oomph while trying to figure this out so I'll let you see if you can get anything going here.

This is the existing script gui/gm-unit.lua with an attempt at getting your appearance modification script folded in as an editor, it's near the very end of the script, just after the wound editor bits:
Code: (gm-unit.lua) [Select]
-- Interface powered, user friendly, unit editor

--[====[

gui/gm-unit
===========
An editor for various unit attributes.

]====]
local gui = require 'gui'
local dialog = require 'gui.dialogs'
local widgets =require 'gui.widgets'
local guiScript = require 'gui.script'
local utils = require 'utils'
local args={...}


local target
--TODO: add more ways to guess what unit you want to edit
if args[1]~= nil then
    target=df.units.find(args[1])
else
    target=dfhack.gui.getSelectedUnit(true)
end

if target==nil then
    qerror("No unit to edit") --TODO: better error message
end
local editors={}
function add_editor(editor_class)
    table.insert(editors,{text=editor_class.ATTRS.frame_title,on_submit=function ( unit )
        editor_class{target_unit=unit}:show()
    end})
end
-------------------------------various subeditors---------
--TODO set local sould or better yet skills vector to reduce long skill list access typing
editor_skills=defclass(editor_skills,gui.FramedScreen)
editor_skills.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Skill editor",
    target_unit = DEFAULT_NIL,
    learned_only= false,
}
function list_skills(unit,learned_only)
    local s_=df.job_skill
    local u_skills=unit.status.current_soul.skills
    local ret={}
    for i,v in ipairs(s_) do
        if i>=0 then
            local u_skill=utils.binsearch(u_skills,i,"id")
            if u_skill or not learned_only then
                if not u_skill then
                    u_skill={rating=-1,experience=0}
                end

                local rating
                if u_skill.rating >=0 then
                    rating=df.skill_rating.attrs[u_skill.rating]
                else
                    rating={caption="<unlearned>",xp_threshold=0}
                end

                local text=string.format("%s: %s %d %d/%d",df.job_skill.attrs[i].caption,rating.caption,u_skill.rating,u_skill.experience,rating.xp_threshold)
                table.insert(ret,{text=text,id=i})
            end
        end
    end
    return ret
end
function editor_skills:update_list(no_save_place)
    local skill_list=list_skills(self.target_unit,self.learned_only)
    if no_save_place then
        self.subviews.skills:setChoices(skill_list)
    else
        self.subviews.skills:setChoices(skill_list,self.subviews.skills:getSelected())
    end
end
function editor_skills:init( args )
    if self.target_unit.status.current_soul==nil then
        qerror("Unit does not have soul, can't edit skills")
    end

    local skill_list=list_skills(self.target_unit,self.learned_only)

    self:addviews{
    widgets.FilteredList{
        choices=skill_list,
        frame = {t=0, b=1,l=1},
        view_id="skills",
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    {text=": remove level ",
                    key = "SECONDSCROLL_UP",
                    on_activate=self:callback("level_skill",-1)},
                    {text=": add level ",
                    key = "SECONDSCROLL_DOWN",
                    on_activate=self:callback("level_skill",1)}
                    ,
                    {text=": show learned only ",
                    key = "CHANGETAB",
                    on_activate=function ()
                        self.learned_only=not self.learned_only
                        self:update_list(true)
                    end}
                    }
            },
        }
end
function editor_skills:get_cur_skill()
    local list_wid=self.subviews.skills
    local _,choice=list_wid:getSelected()
    if choice==nil then
        qerror("Nothing selected")
    end
    local u_skill=utils.binsearch(self.target_unit.status.current_soul.skills,choice.id,"id")
    return choice,u_skill
end
function editor_skills:level_skill(lvl)
    local sk_en,sk=self:get_cur_skill()
    if lvl >0 then
        local rating

        if sk then
            rating=sk.rating+lvl
        else
            rating=lvl-1
        end

        utils.insert_or_update(self.target_unit.status.current_soul.skills, {new=true, id=sk_en.id, rating=rating}, 'id') --TODO set exp?
    elseif sk and sk.rating==0 and lvl<0 then
        utils.erase_sorted_key(self.target_unit.status.current_soul.skills,sk_en.id,"id")
    elseif sk and lvl<0 then
        utils.insert_or_update(self.target_unit.status.current_soul.skills, {new=true, id=sk_en.id, rating=sk.rating+lvl}, 'id') --TODO set exp?
    end
    self:update_list()
end
function editor_skills:remove_rust(skill)
    --TODO
end
add_editor(editor_skills)
------- civ editor
RaceBox = defclass(RaceBox, dialog.ListBox)
RaceBox.focus_path = 'RaceBox'

RaceBox.ATTRS{
    format_name="$NAME ($TOKEN)",
    with_filter=true,
    allow_none=false,
}
function RaceBox:format_creature(creature_raw)
    local t = {NAME=creature_raw.name[0],TOKEN=creature_raw.creature_id}
    return string.gsub(self.format_name, "%$(%w+)", t)
end
function RaceBox:preinit(info)
    self.format_name=RaceBox.ATTRS.format_name or info.format_name -- preinit does not have ATTRS set yet
    local choices={}
    if RaceBox.ATTRS.allow_none or info.allow_none then
        table.insert(choices,{text="<none>",num=-1})
    end
    for i,v in ipairs(df.global.world.raws.creatures.all) do
        local text=self:format_creature(v)
        table.insert(choices,{text=text,raw=v,num=i,search_key=text:lower()})
    end
    info.choices=choices
end
function showRacePrompt(title, text, tcolor, on_select, on_cancel, min_width,allow_none)
    RaceBox{
        frame_title = title,
        text = text,
        text_pen = tcolor,
        on_select = on_select,
        on_cancel = on_cancel,
        frame_width = min_width,
        allow_none = allow_none,
    }:show()
end
CivBox = defclass(CivBox,dialog.ListBox)
CivBox.focus_path = "CivBox"

CivBox.ATTRS={
    format_name="$NAME ($ENGLISH):$ID",
    format_no_name="<unnamed>:$ID",
    name_other="<other(-1)>",
    with_filter=true,
    allow_other=false,
}

function civ_name(id,format_name,format_no_name,name_other,name_invalid)
    if id==-1 then
        return name_other or "<other (-1)>"
    end
    local civ
    if type(id)=='userdata' then
        civ=id
    else
        civ=df.historical_entity.find(id)
        if civ==nil then
            return name_invalid or "<invalid>"
        end
    end
    local t={NAME=dfhack.TranslateName(civ.name),ENGLISH=dfhack.TranslateName(civ.name,true),ID=civ.id} --TODO race?, maybe something from raws?
    if t.NAME=="" then
        return string.gsub(format_no_name or "<unnamed>:$ID", "%$(%w+)", t)
    end
    return string.gsub(format_name or "$NAME ($ENGLISH):$ID", "%$(%w+)", t)
end
function CivBox:update_choices()
    local choices={}
    if self.allow_other then
        table.insert(choices,{text=self.name_other,num=-1})
    end

    for i,v in ipairs(df.global.world.entities.all) do
        if not self.race_filter or (v.race==self.race_filter) then --TODO filter type
            local text=civ_name(v,self.format_name,self.format_no_name,self.name_other,self.name_invalid)
            table.insert(choices,{text=text,raw=v,num=i})
        end
    end
    self.choices=choices
    if self.subviews.list then
        self.subviews.list:setChoices(self.choices)
    end
end
function CivBox:update_race_filter(id)
    local raw=df.creature_raw.find(id)
    if raw then
        self.subviews.race_label:setText(": "..raw.name[0])
        self.race_filter=id
    else
        self.subviews.race_label:setText(": <none>")
        self.race_filter=nil
    end

    self:update_choices()
end
function CivBox:choose_race()
    showRacePrompt("Choose race","Select new race:",nil,function (id,choice)
        self:update_race_filter(choice.num)
    end,nil,nil,true)
end
function CivBox:init(info)
    self.subviews.list.frame={t=3,r=0,l=0}
    self:addviews{
        widgets.Label{frame={t=1,l=0},text={
        {text="Filter race ",key="CUSTOM_CTRL_A",key_sep="()",on_activate=self:callback("choose_race")},
        }},
        widgets.Label{frame={t=1,l=21},view_id="race_label",
        text=": <none>",
        }
    }
    self:update_choices()
end
function showCivPrompt(title, text, tcolor, on_select, on_cancel, min_width,allow_other)
    CivBox{
        frame_title = title,
        text = text,
        text_pen = tcolor,
        on_select = on_select,
        on_cancel = on_cancel,
        frame_width = min_width,
        allow_other = allow_other,
    }:show()
end

editor_civ=defclass(editor_civ,gui.FramedScreen)
editor_civ.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Civilization editor",
    target_unit = DEFAULT_NIL,
    }

function editor_civ:update_curren_civ()
    self.subviews.civ_name:setText("Currently: "..civ_name(self.target_unit.civ_id))
end
function editor_civ:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end

    self:addviews{
    widgets.Label{view_id="civ_name",frame = { t=1,l=1}, text="Currently: "..civ_name(self.target_unit.civ_id)},
    widgets.Label{frame = { t=2,l=1}, text={{text=": set to other (-1, usually enemy)",key="CUSTOM_N",
        on_activate= function() self.target_unit.civ_id=-1;self:update_curren_civ() end}}},
    widgets.Label{frame = { t=3,l=1}, text={{text=": set to current civ("..df.global.ui.civ_id..")",key="CUSTOM_C",
        on_activate= function() self.target_unit.civ_id=df.global.ui.civ_id;self:update_curren_civ() end}}},
    widgets.Label{frame = { t=4,l=1}, text={{text=": manually enter",key="CUSTOM_E",
        on_activate=function ()
         dialog.showInputPrompt("Civ id","Enter new civ id:",COLOR_WHITE,
            tostring(self.target_unit.civ_id),function(new_value)
                self.target_unit.civ_id=new_value
                self:update_curren_civ()
            end)
        end}}
        },
    widgets.Label{frame= {t=5,l=1}, text={{text=": select from list",key="CUSTOM_L",
        on_activate=function (  )
            showCivPrompt("Choose civilization", "Select units civilization",nil,function ( id,choice )
                self.target_unit.civ_id=choice.num
                self:update_curren_civ()
            end,nil,nil,true)
        end
        }}},
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    }
            },
        }
end
add_editor(editor_civ)
------- counters editor
editor_counters=defclass(editor_counters,gui.FramedScreen)
editor_counters.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Counters editor",
    target_unit = DEFAULT_NIL,
    counters1={
    "think_counter",
    "job_counter",
    "swap_counter",
    "winded",
    "stunned",
    "unconscious",
    "suffocation",
    "webbed",
    "soldier_mood_countdown",
    "soldier_mood", --todo enum,
    "pain",
    "nausea",
    "dizziness",
    },
    counters2={
    "paralysis",
    "numbness",
    "fever",
    "exhaustion",
    "hunger_timer",
    "thirst_timer",
    "sleepiness_timer",
    "stomach_content",
    "stomach_food",
    "vomit_timeout",
    "stored_fat" --TODO what to reset to?
    }
}
function editor_counters:fill_counters()
    local ret={}
    local u=self.target_unit
    for i,v in ipairs(self.counters1) do
        table.insert(ret,{f=u.counters:_field(v),name=v})
    end
    for i,v in ipairs(self.counters2) do
        table.insert(ret,{f=u.counters2:_field(v),name=v})
    end
    return ret
end
function editor_counters:update_counters()
    for i,v in ipairs(self.counter_list) do
        v.text=string.format("%s:%d",v.name,v.f.value)
    end
    self.subviews.counters:setChoices(self.counter_list)
end
function editor_counters:set_cur_counter(value,index,choice)
    choice.f.value=value
    self:update_counters()
end
function editor_counters:choose_cur_counter(index,choice)
    dialog.showInputPrompt(choice.name,"Enter new value:",COLOR_WHITE,
            tostring(choice.f.value),function(new_value)
                self:set_cur_counter(new_value,index,choice)
            end)
end
function editor_counters:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end

    self.counter_list=self:fill_counters()


    self:addviews{
    widgets.FilteredList{
        choices=self.counter_list,
        frame = {t=0, b=1,l=1},
        view_id="counters",
        on_submit=self:callback("choose_cur_counter"),
        on_submit2=self:callback("set_cur_counter",0),--TODO some things need to be set to different defaults
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    {text=": reset counter ",
                    key = "SEC_SELECT",
                    },
                    {text=": set counter ",
                    key = "SELECT",
                    }
                   
                    }
            },
        }
    self:update_counters()
end
add_editor(editor_counters)

wound_creator=defclass(wound_creator,gui.FramedScreen)
wound_creator.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Wound creator",
    target_wound = DEFAULT_NIL,
    --filter
}
function wound_creator:init( args )
    if self.target_wound==nil then
        qerror("invalid wound")
    end
   

    self:addviews{
    widgets.List{
       
        frame = {t=0, b=1,l=1},
        view_id="fields",
        on_submit=self:callback("edit_cur_wound"),
        on_submit2=self:callback("delete_current_wound")
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")},

                    {text=": edit wound ",
                    key = "SELECT"},

                    {text=": delete wound ",
                    key = "SEC_SELECT"},
                    {text=": create wound ",
                    key = "CUSTOM_CTRL_I",
                    on_activate= self:callback("create_new_wound")},

                    }
            },
        }
    self:update_wounds()
end
-------------------
editor_wounds=defclass(editor_wounds,gui.FramedScreen)
editor_wounds.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Wound editor",
    target_unit = DEFAULT_NIL,
    --filter
}
function is_scar( wound_part )
    return wound_part.flags1.scar_cut or wound_part.flags1.scar_smashed or
        wound_part.flags1.scar_edged_shake1 or wound_part.flags1.scar_blunt_shake1
end
function format_flag_name( fname )
    return fname:sub(1,1):upper()..fname:sub(2):gsub("_"," ")
end
function name_from_flags( wp )
    for i,v in ipairs(wp.flags1) do
        if v then
            return format_flag_name(df.wound_damage_flags1[i])
        end
    end
    for i,v in ipairs(wp.flags2) do
        if v then
            return format_flag_name(df.wound_damage_flags2[i])
        end
    end
    return "<unnamed wound>"
end
function format_wound( list_id,wound, unit)

    local name="<unnamed wound>"
    if #wound.parts>0 and #wound.parts[0].effect_type>0 then --try to make wound name by effect...
        name=tostring(df.wound_effect_type[wound.parts[0].effect_type[0]])
        if #wound.parts>1 then --cheap and probably incorrect...
            name=name.."s"
        end
    elseif #wound.parts>0 and is_scar(wound.parts[0]) then
        name="Scar"
    elseif #wound.parts>0 then
        local wp=wound.parts[0]
        name=name_from_flags(wp)
    end

    return string.format("%d. %s id=%d",list_id,name,wound.id)
end
function editor_wounds:update_wounds()
    local ret={}
    for i,v in ipairs(self.trg_wounds) do
        table.insert(ret,{text=format_wound(i,v,self.target_unit),wound=v})
    end
    self.subviews.wounds:setChoices(ret)
    self.wound_list=ret
end
function editor_wounds:dirty_unit()
    print("todo: implement unit status recalculation")
end
function editor_wounds:get_cur_wound()
    local list_wid=self.subviews.wounds
    local _,choice=list_wid:getSelected()
    if choice==nil then
        qerror("Nothing selected")
    end
    local ret_wound=utils.binsearch(self.trg_wounds,choice.id,"id")
    return choice,ret_wound
end
function editor_wounds:delete_current_wound(index,choice)
   
    utils.erase_sorted(self.trg_wounds,choice.wound,"id")
    choice.wound:delete()
    self:dirty_unit()
    self:update_wounds()
end
function editor_wounds:create_new_wound()
    print("Creating")
end
function editor_wounds:edit_cur_wound(index,choice)
   
end
function editor_wounds:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end
    self.trg_wounds=self.target_unit.body.wounds

    self:addviews{
    widgets.List{
       
        frame = {t=0, b=1,l=1},
        view_id="wounds",
        on_submit=self:callback("edit_cur_wound"),
        on_submit2=self:callback("delete_current_wound")
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")},

                    {text=": edit wound ",
                    key = "SELECT"},

                    {text=": delete wound ",
                    key = "SEC_SELECT"},
                    {text=": create wound ",
                    key = "CUSTOM_CTRL_I",
                    on_activate= self:callback("create_new_wound")},

                    }
            },
        }
    self:update_wounds()
end
add_editor(editor_wounds)
--------
editor_appearance=defclass(editor_appearance,gui.FramedScreen)
editor_appearance.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Appearance editor",
    target_unit = DEFAULT_NIL,
    appearance={
     body_modifiers = {},
     bp_modifiers = {},
     tissue_style = {},
     tissue_length = {},
     colors = {}
    }
}
local urace = df.global.world.units.active[0].race
local ucritter = df.creature_raw.find(urace)
local ucaste = ucritter.caste
function list_body(height,broad)
    local modifiers = ucaste.body_appearance_modifiers
    local height, broad
    for i = 0, #modifiers-1 do
if modifier[i].type==0 then
height = bodmod[i]
elseif modifier[i].type==1 then
broad = bodmod[i]
end
    end
    return height, broad
end
function editor_appearance:format_body()
    local bodmod = {}
    for i,v in ipairs(self.body_modifiers) do
table.insert(bodmod,{["height"] = height, ["broadness"] = broad, self.target_unit})
    end
    return bodmod
end
function list_bps(bps, bplist)
    local bps = ucaste.bp_appearance
    local ret = {}
    for i = 0, #bps.part_idx-1 do
ret[i] = {["modifierId"] = bps.modifier_idx[i], ["partId"] = bps.part_idx[i]}
    end
    bplist = ret
end
function list_bpmods(bpmods)
    local bpmods = ucaste.bp_appearance.modifiers
    for i = 0, #bpmods-1 do
     local bpm = bpmods[i]
     local bpmname
if #bpm.name > 0 then
bpmname = bpm.name
else
bpmname = unit.body_info.body_parts[bpm.body_parts[0]].name_singular[0].value
end
    end
    local changes = {}
    list_bps(bps, bplist)
    for i = 0, bpm.body_parts-1 do
  local partId = bpm.body_parts[i]
  for j = 0, #bplist-1 do
  if bplist[j].modifierId == k and modMatch[j].partId == partId then
table.insert(changes, j)
        local out = {["modifier"] = bpm, ["changes"] = changes}
  end
   end
    end
    return bpmods, out   
end
function editor_appearance:format_bpmods()
    local bpmodlist = {}
    for i,v in ipairs(self.bp_modifiers) do
table.insert(bpmodlist,{text=list_bpmods(bpmods[i], out, self.target_unit)})
    end
    return bpmodlist
end
--[[
function editor_appearance:list_colors()
    local ret={}
    local u=self.target_unit.appearance
local function getColor(id)
return df.descriptor_color.find(id)
end
local function getPattern(id)
return df.descriptor_pattern.find(id)
end
local function patternString(id)
local pattern = getPattern(id)
local prefix
if pattern.pattern == 0 then --Monochrome
return getColor(pattern.colors[0]).name
elseif pattern.pattern == 1 then --Stripes
prefix = "striped"
elseif pattern.pattern == 2 then --Iris_eye
return getColor(pattern.colors[2]).name .. " eye"
elseif pattern.pattern == 3 then --Spots
prefix = "spotted" --that's a guess
elseif pattern.pattern == 4 then --Pupil_eye
return getColor(pattern.colors[2]).name .. " eye"
elseif pattern.pattern == 5 then --mottled
prefix = "mottled"
end
local out = prefix .. " "
for i=0, #pattern.colors-1 do
if i == #pattern.colors-1 then
out = out .. "and " .. getColor(pattern.colors[i]).name
elseif i == #pattern.colors-2 then
out = out .. getColor(pattern.colors[i]).name .. " "
else
out = out .. getColor(pattern.colors[i]).name .. ", "
end
end
return out
end
    for j,k in ipairs(self.colors) do
table.insert(ret,{text=out,name=j})
    end
    return ret
end
function editor_appearance:update_colors()
    for i,v in ipairs(self.color_list) do
v.text=string.format
self.color_list=self:list_colors()
local function modifierString(mod)
local out = df.appearance_modifier_type[mod.type]
out = out:lower() --Make lowercase
out = out:gsub("_", " ") --Replace underscores with spaces
out = firstUpper(out) --capitalises first letter
return out
end]]
function editor_appearance:reduce_value()
end
function editor_appearance:raise_value()
end
function editor_appearance:init( args )
    self:addviews{
    widgets.FilteredList{
        choices=bodmod, bpmodlist,
        frame = {t=0, b=1,l=1},
        view_id="appearance",
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    {text=": reduce value ",
                    key = "SECONDSCROLL_UP",
                    on_activate=self:callback("reduce_value",-1)},
                    {text=": raise value ",
                    key = "SECONDSCROLL_DOWN",
                    on_activate=self:callback("raise_value",1)}
                    }
            },
        }
end
add_editor(editor_appearance)

-------------------------------main window----------------
unit_editor = defclass(unit_editor, gui.FramedScreen)
unit_editor.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "GameMaster's unit editor",
    target_unit = DEFAULT_NIL,
    }


function unit_editor:init(args)

    self:addviews{
    widgets.FilteredList{
        choices=editors,
        on_submit=function (idx,choice)
            if choice.on_submit then
                choice.on_submit(self.target_unit)
            end
        end
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    }
            },
        }
end


unit_editor{target_unit=target}:show()
The pieces should be there, I had to comment out the color section while I was testing the rest but I left it in place, there's just a few more bits missing regarding getting it to display the values properly but you're clever so I will be completely unsurprised if you pop up and point out two or three lines that needed to be added to get it all working right.

Still, I got it to stop spitting errors and do this for me:
« Last Edit: June 30, 2017, 11:37:27 pm by Max™ »
Logged
Engraved here is a rendition of an image of the Dwarf Fortress learning curve. All craftsdwarfship is of the highest quality. It depicts an obsidian overhang which menaces with spikes of obsidian and tears. Carved on the overhang is an image of Toady One and the players. The players are curled up in a fetal position. Toady One is laughing. The players are burning.
The VectorCurses+1 tileset strikes the square set and the severed part sails off in an arc!

Atkana

  • Bay Watcher
  • [CURIOUSBEAST]
    • View Profile

-snip-
It took a while but I managed to get something that works, the features that reported the natural ranges for body parts and the ability to change an adventurer during creation were lost in the process but they both weren't really important. Feel free to github magic it if you want to submit the edited version, since I have no idea how to github - though it'll probably need vetting by people who a) know programming and b) have a knowledge of how the GUI API works beyond whatever they could work out from seeing how it's used in gm-unit :P Body modifications still have the problem of not actually updating size (which I'm still assuming is an actual mechanic) - a dwarf changed to be 100,000% the height of a dwarf is still technically the same volume as they were before (I think).

Code: (gm-newnit.lua) [Select]
-- Interface powered, user friendly, unit editor

--[====[

gui/gm-unit
===========
An editor for various unit attributes.

]====]
local gui = require 'gui'
local dialog = require 'gui.dialogs'
local widgets =require 'gui.widgets'
local guiScript = require 'gui.script'
local utils = require 'utils'
local args={...}


local target
--TODO: add more ways to guess what unit you want to edit
if args[1]~= nil then
    target=df.units.find(args[1])
else
    target=dfhack.gui.getSelectedUnit(true)
end

if target==nil then
    qerror("No unit to edit") --TODO: better error message
end
local editors={}
function add_editor(editor_class)
    table.insert(editors,{text=editor_class.ATTRS.frame_title,on_submit=function ( unit )
        editor_class{target_unit=unit}:show()
    end})
end
-------------------------------various subeditors---------
--TODO set local sould or better yet skills vector to reduce long skill list access typing
editor_skills=defclass(editor_skills,gui.FramedScreen)
editor_skills.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Skill editor",
    target_unit = DEFAULT_NIL,
    learned_only= false,
}
function list_skills(unit,learned_only)
    local s_=df.job_skill
    local u_skills=unit.status.current_soul.skills
    local ret={}
    for i,v in ipairs(s_) do
        if i>=0 then
            local u_skill=utils.binsearch(u_skills,i,"id")
            if u_skill or not learned_only then
                if not u_skill then
                    u_skill={rating=-1,experience=0}
                end

                local rating
                if u_skill.rating >=0 then
                    rating=df.skill_rating.attrs[u_skill.rating]
                else
                    rating={caption="<unlearned>",xp_threshold=0}
                end

                local text=string.format("%s: %s %d %d/%d",df.job_skill.attrs[i].caption,rating.caption,u_skill.rating,u_skill.experience,rating.xp_threshold)
                table.insert(ret,{text=text,id=i})
            end
        end
    end
    return ret
end
function editor_skills:update_list(no_save_place)
    local skill_list=list_skills(self.target_unit,self.learned_only)
    if no_save_place then
        self.subviews.skills:setChoices(skill_list)
    else
        self.subviews.skills:setChoices(skill_list,self.subviews.skills:getSelected())
    end
end
function editor_skills:init( args )
    if self.target_unit.status.current_soul==nil then
        qerror("Unit does not have soul, can't edit skills")
    end

    local skill_list=list_skills(self.target_unit,self.learned_only)

    self:addviews{
    widgets.FilteredList{
        choices=skill_list,
        frame = {t=0, b=1,l=1},
        view_id="skills",
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    {text=": remove level ",
                    key = "SECONDSCROLL_UP",
                    on_activate=self:callback("level_skill",-1)},
                    {text=": add level ",
                    key = "SECONDSCROLL_DOWN",
                    on_activate=self:callback("level_skill",1)}
                    ,
                    {text=": show learned only ",
                    key = "CHANGETAB",
                    on_activate=function ()
                        self.learned_only=not self.learned_only
                        self:update_list(true)
                    end}
                    }
            },
        }
end
function editor_skills:get_cur_skill()
    local list_wid=self.subviews.skills
    local _,choice=list_wid:getSelected()
    if choice==nil then
        qerror("Nothing selected")
    end
    local u_skill=utils.binsearch(self.target_unit.status.current_soul.skills,choice.id,"id")
    return choice,u_skill
end
function editor_skills:level_skill(lvl)
    local sk_en,sk=self:get_cur_skill()
    if lvl >0 then
        local rating

        if sk then
            rating=sk.rating+lvl
        else
            rating=lvl-1
        end

        utils.insert_or_update(self.target_unit.status.current_soul.skills, {new=true, id=sk_en.id, rating=rating}, 'id') --TODO set exp?
    elseif sk and sk.rating==0 and lvl<0 then
        utils.erase_sorted_key(self.target_unit.status.current_soul.skills,sk_en.id,"id")
    elseif sk and lvl<0 then
        utils.insert_or_update(self.target_unit.status.current_soul.skills, {new=true, id=sk_en.id, rating=sk.rating+lvl}, 'id') --TODO set exp?
    end
    self:update_list()
end
function editor_skills:remove_rust(skill)
    --TODO
end
add_editor(editor_skills)
------- civ editor
RaceBox = defclass(RaceBox, dialog.ListBox)
RaceBox.focus_path = 'RaceBox'

RaceBox.ATTRS{
    format_name="$NAME ($TOKEN)",
    with_filter=true,
    allow_none=false,
}
function RaceBox:format_creature(creature_raw)
    local t = {NAME=creature_raw.name[0],TOKEN=creature_raw.creature_id}
    return string.gsub(self.format_name, "%$(%w+)", t)
end
function RaceBox:preinit(info)
    self.format_name=RaceBox.ATTRS.format_name or info.format_name -- preinit does not have ATTRS set yet
    local choices={}
    if RaceBox.ATTRS.allow_none or info.allow_none then
        table.insert(choices,{text="<none>",num=-1})
    end
    for i,v in ipairs(df.global.world.raws.creatures.all) do
        local text=self:format_creature(v)
        table.insert(choices,{text=text,raw=v,num=i,search_key=text:lower()})
    end
    info.choices=choices
end
function showRacePrompt(title, text, tcolor, on_select, on_cancel, min_width,allow_none)
    RaceBox{
        frame_title = title,
        text = text,
        text_pen = tcolor,
        on_select = on_select,
        on_cancel = on_cancel,
        frame_width = min_width,
        allow_none = allow_none,
    }:show()
end
CivBox = defclass(CivBox,dialog.ListBox)
CivBox.focus_path = "CivBox"

CivBox.ATTRS={
    format_name="$NAME ($ENGLISH):$ID",
    format_no_name="<unnamed>:$ID",
    name_other="<other(-1)>",
    with_filter=true,
    allow_other=false,
}

function civ_name(id,format_name,format_no_name,name_other,name_invalid)
    if id==-1 then
        return name_other or "<other (-1)>"
    end
    local civ
    if type(id)=='userdata' then
        civ=id
    else
        civ=df.historical_entity.find(id)
        if civ==nil then
            return name_invalid or "<invalid>"
        end
    end
    local t={NAME=dfhack.TranslateName(civ.name),ENGLISH=dfhack.TranslateName(civ.name,true),ID=civ.id} --TODO race?, maybe something from raws?
    if t.NAME=="" then
        return string.gsub(format_no_name or "<unnamed>:$ID", "%$(%w+)", t)
    end
    return string.gsub(format_name or "$NAME ($ENGLISH):$ID", "%$(%w+)", t)
end
function CivBox:update_choices()
    local choices={}
    if self.allow_other then
        table.insert(choices,{text=self.name_other,num=-1})
    end

    for i,v in ipairs(df.global.world.entities.all) do
        if not self.race_filter or (v.race==self.race_filter) then --TODO filter type
            local text=civ_name(v,self.format_name,self.format_no_name,self.name_other,self.name_invalid)
            table.insert(choices,{text=text,raw=v,num=i})
        end
    end
    self.choices=choices
    if self.subviews.list then
        self.subviews.list:setChoices(self.choices)
    end
end
function CivBox:update_race_filter(id)
    local raw=df.creature_raw.find(id)
    if raw then
        self.subviews.race_label:setText(": "..raw.name[0])
        self.race_filter=id
    else
        self.subviews.race_label:setText(": <none>")
        self.race_filter=nil
    end

    self:update_choices()
end
function CivBox:choose_race()
    showRacePrompt("Choose race","Select new race:",nil,function (id,choice)
        self:update_race_filter(choice.num)
    end,nil,nil,true)
end
function CivBox:init(info)
    self.subviews.list.frame={t=3,r=0,l=0}
    self:addviews{
        widgets.Label{frame={t=1,l=0},text={
        {text="Filter race ",key="CUSTOM_CTRL_A",key_sep="()",on_activate=self:callback("choose_race")},
        }},
        widgets.Label{frame={t=1,l=21},view_id="race_label",
        text=": <none>",
        }
    }
    self:update_choices()
end
function showCivPrompt(title, text, tcolor, on_select, on_cancel, min_width,allow_other)
    CivBox{
        frame_title = title,
        text = text,
        text_pen = tcolor,
        on_select = on_select,
        on_cancel = on_cancel,
        frame_width = min_width,
        allow_other = allow_other,
    }:show()
end

editor_civ=defclass(editor_civ,gui.FramedScreen)
editor_civ.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Civilization editor",
    target_unit = DEFAULT_NIL,
    }

function editor_civ:update_curren_civ()
    self.subviews.civ_name:setText("Currently: "..civ_name(self.target_unit.civ_id))
end
function editor_civ:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end

    self:addviews{
    widgets.Label{view_id="civ_name",frame = { t=1,l=1}, text="Currently: "..civ_name(self.target_unit.civ_id)},
    widgets.Label{frame = { t=2,l=1}, text={{text=": set to other (-1, usually enemy)",key="CUSTOM_N",
        on_activate= function() self.target_unit.civ_id=-1;self:update_curren_civ() end}}},
    widgets.Label{frame = { t=3,l=1}, text={{text=": set to current civ("..df.global.ui.civ_id..")",key="CUSTOM_C",
        on_activate= function() self.target_unit.civ_id=df.global.ui.civ_id;self:update_curren_civ() end}}},
    widgets.Label{frame = { t=4,l=1}, text={{text=": manually enter",key="CUSTOM_E",
        on_activate=function ()
         dialog.showInputPrompt("Civ id","Enter new civ id:",COLOR_WHITE,
            tostring(self.target_unit.civ_id),function(new_value)
                self.target_unit.civ_id=new_value
                self:update_curren_civ()
            end)
        end}}
        },
    widgets.Label{frame= {t=5,l=1}, text={{text=": select from list",key="CUSTOM_L",
        on_activate=function (  )
            showCivPrompt("Choose civilization", "Select units civilization",nil,function ( id,choice )
                self.target_unit.civ_id=choice.num
                self:update_curren_civ()
            end,nil,nil,true)
        end
        }}},
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    }
            },
        }
end
add_editor(editor_civ)
------- counters editor
editor_counters=defclass(editor_counters,gui.FramedScreen)
editor_counters.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Counters editor",
    target_unit = DEFAULT_NIL,
    counters1={
    "think_counter",
    "job_counter",
    "swap_counter",
    "winded",
    "stunned",
    "unconscious",
    "suffocation",
    "webbed",
    "soldier_mood_countdown",
    "soldier_mood", --todo enum,
    "pain",
    "nausea",
    "dizziness",
    },
    counters2={
    "paralysis",
    "numbness",
    "fever",
    "exhaustion",
    "hunger_timer",
    "thirst_timer",
    "sleepiness_timer",
    "stomach_content",
    "stomach_food",
    "vomit_timeout",
    "stored_fat" --TODO what to reset to?
    }
}
function editor_counters:fill_counters()
    local ret={}
    local u=self.target_unit
    for i,v in ipairs(self.counters1) do
        table.insert(ret,{f=u.counters:_field(v),name=v})
    end
    for i,v in ipairs(self.counters2) do
        table.insert(ret,{f=u.counters2:_field(v),name=v})
    end
    return ret
end
function editor_counters:update_counters()
    for i,v in ipairs(self.counter_list) do
        v.text=string.format("%s:%d",v.name,v.f.value)
    end
    self.subviews.counters:setChoices(self.counter_list)
end
function editor_counters:set_cur_counter(value,index,choice)
    choice.f.value=value
    self:update_counters()
end
function editor_counters:choose_cur_counter(index,choice)
    dialog.showInputPrompt(choice.name,"Enter new value:",COLOR_WHITE,
            tostring(choice.f.value),function(new_value)
                self:set_cur_counter(new_value,index,choice)
            end)
end
function editor_counters:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end

    self.counter_list=self:fill_counters()


    self:addviews{
    widgets.FilteredList{
        choices=self.counter_list,
        frame = {t=0, b=1,l=1},
        view_id="counters",
        on_submit=self:callback("choose_cur_counter"),
        on_submit2=self:callback("set_cur_counter",0),--TODO some things need to be set to different defaults
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    {text=": reset counter ",
                    key = "SEC_SELECT",
                    },
                    {text=": set counter ",
                    key = "SELECT",
                    }
                   
                    }
            },
        }
    self:update_counters()
end
add_editor(editor_counters)

wound_creator=defclass(wound_creator,gui.FramedScreen)
wound_creator.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Wound creator",
    target_wound = DEFAULT_NIL,
    --filter
}
function wound_creator:init( args )
    if self.target_wound==nil then
        qerror("invalid wound")
    end
   

    self:addviews{
    widgets.List{
       
        frame = {t=0, b=1,l=1},
        view_id="fields",
        on_submit=self:callback("edit_cur_wound"),
        on_submit2=self:callback("delete_current_wound")
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")},

                    {text=": edit wound ",
                    key = "SELECT"},

                    {text=": delete wound ",
                    key = "SEC_SELECT"},
                    {text=": create wound ",
                    key = "CUSTOM_CTRL_I",
                    on_activate= self:callback("create_new_wound")},

                    }
            },
        }
    self:update_wounds()
end
-------------------
editor_wounds=defclass(editor_wounds,gui.FramedScreen)
editor_wounds.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Wound editor",
    target_unit = DEFAULT_NIL,
    --filter
}
function is_scar( wound_part )
    return wound_part.flags1.scar_cut or wound_part.flags1.scar_smashed or
        wound_part.flags1.scar_edged_shake1 or wound_part.flags1.scar_blunt_shake1
end
function format_flag_name( fname )
    return fname:sub(1,1):upper()..fname:sub(2):gsub("_"," ")
end
function name_from_flags( wp )
    for i,v in ipairs(wp.flags1) do
        if v then
            return format_flag_name(df.wound_damage_flags1[i])
        end
    end
    for i,v in ipairs(wp.flags2) do
        if v then
            return format_flag_name(df.wound_damage_flags2[i])
        end
    end
    return "<unnamed wound>"
end
function format_wound( list_id,wound, unit)

    local name="<unnamed wound>"
    if #wound.parts>0 and #wound.parts[0].effect_type>0 then --try to make wound name by effect...
        name=tostring(df.wound_effect_type[wound.parts[0].effect_type[0]])
        if #wound.parts>1 then --cheap and probably incorrect...
            name=name.."s"
        end
    elseif #wound.parts>0 and is_scar(wound.parts[0]) then
        name="Scar"
    elseif #wound.parts>0 then
        local wp=wound.parts[0]
        name=name_from_flags(wp)
    end

    return string.format("%d. %s id=%d",list_id,name,wound.id)
end
function editor_wounds:update_wounds()
    local ret={}
    for i,v in ipairs(self.trg_wounds) do
        table.insert(ret,{text=format_wound(i,v,self.target_unit),wound=v})
    end
    self.subviews.wounds:setChoices(ret)
    self.wound_list=ret
end
function editor_wounds:dirty_unit()
    print("todo: implement unit status recalculation")
end
function editor_wounds:get_cur_wound()
    local list_wid=self.subviews.wounds
    local _,choice=list_wid:getSelected()
    if choice==nil then
        qerror("Nothing selected")
    end
    local ret_wound=utils.binsearch(self.trg_wounds,choice.id,"id")
    return choice,ret_wound
end
function editor_wounds:delete_current_wound(index,choice)
   
    utils.erase_sorted(self.trg_wounds,choice.wound,"id")
    choice.wound:delete()
    self:dirty_unit()
    self:update_wounds()
end
function editor_wounds:create_new_wound()
    print("Creating")
end
function editor_wounds:edit_cur_wound(index,choice)
   
end
function editor_wounds:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end
    self.trg_wounds=self.target_unit.body.wounds

    self:addviews{
    widgets.List{
       
        frame = {t=0, b=1,l=1},
        view_id="wounds",
        on_submit=self:callback("edit_cur_wound"),
        on_submit2=self:callback("delete_current_wound")
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")},

                    {text=": edit wound ",
                    key = "SELECT"},

                    {text=": delete wound ",
                    key = "SEC_SELECT"},
                    {text=": create wound ",
                    key = "CUSTOM_CTRL_I",
                    on_activate= self:callback("create_new_wound")},

                    }
            },
        }
    self:update_wounds()
end
add_editor(editor_wounds)

------ Body editor

modifier_selector = defclass(modifier_selector, gui.FramedScreen)

function modifierString(mod)
local out = df.appearance_modifier_type[mod.type]
out = out:lower() --Make lowercase
out = out:gsub("_", " ") --Replace underscores with spaces
out = out:gsub("^%l", string.upper) --capitalises first letter

return out
end

function showModifierScreen(data)
modifier_selector{
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Select a modifier",
    target_unit = DEFAULT_NIL,
data = data
    }:show()
end

function modifier_selector:set_value(value,index,choice)
for i,v in ipairs(choice.changes) do
self.changeType[v] = value
end
self:update_features()
end

function modifier_selector:on_select(index, choice)
dialog.showInputPrompt(modifierString(choice.mod),"Enter new value:",COLOR_WHITE,
            tostring(self.changeType[choice.changes[1]]),function(new_value)
                self:set_value(new_value,index,choice)
            end)
end

function modifier_selector:update_features()
local out = {}
for i, v in ipairs(self.partPicked.modList) do
table.insert(out, {text = (modifierString(v.modifier) .. ": " .. self.changeType[v.changes[1]]), mod = v.modifier, changes = v.changes})
end
self.subviews.modifiers:setChoices(out)
end

function modifier_selector:init( info )
self.partPicked = info.data.choice --The part that was picked in editor_body

if info.data.choice.isPart then
self.changeType = target.appearance.bp_modifiers
else
self.changeType = target.appearance.body_modifiers
end

    self:addviews{
    widgets.FilteredList{
        frame = {t=0, b=1,l=1},
        view_id="modifiers",
on_submit=self:callback("on_select")
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": back to part selector ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    {text=": select modifier ",
                    key = "SELECT",
                    }
                   
                    }
            },
        }

    self:update_features()
end


editor_body=defclass(editor_body,gui.FramedScreen)
editor_body.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Body modifier editor",
    target_unit = DEFAULT_NIL,
}

function editor_body:bp_links()
local out = {}
local uc = self.ucaste
for i,v in ipairs(uc.bp_appearance.part_idx) do
out[i] = {["modId"] = uc.bp_appearance.modifier_idx[i], ["partId"] = uc.bp_appearance.part_idx[i]}
end

return out
end

--Following is a relic from my original change-appearance script. There's probably a more efficient way of doing this, but I'm not in the mood to be redesigning ;P
function editor_body:make_bplist()
local ret = {}
local bpm = self.ucaste.bp_appearance.modifiers
local links = self:bp_links()
local point = {}

for i, v in ipairs(bpm) do
local mod = v

local bpmname
if #mod.noun > 0 then
bpmname = mod.noun
else
bpmname = self.ucaste.body_info.body_parts[mod.body_parts[0]].name_singular[0].value
end

local changes = {}
for i2, v2 in ipairs(mod.body_parts) do
local partId = v2
for i3, v3 in ipairs(links) do
if v3.modId == i and v3.partId == partId then
table.insert(changes, i3) --?
end
end
end

if point[bpmname] then
table.insert(ret[point[bpmname]].modList, {["modifier"] = mod, ["changes"] = changes})
else
table.insert(ret, {["name"] = bpmname, ["modList"] = {[1] = {["modifier"] = mod, ["changes"] = changes}}})
point[bpmname] = #ret --Stores the index of the name for future additions
end
end
return ret
end

function editor_body:make_bodmodlist() --Version of make_bplist() to make a spoof version for body so it can be treated the same. Only makes modList
local ret = {}
local bm = self.ucaste.body_appearance_modifiers

for i, v in ipairs(bm) do
table.insert(ret, {["modifier"] = v, ["changes"] = {[1] = i}})
end

return ret
end

function editor_body:update_features()
self.bplist = self:make_bplist()
local out = {}
local uc = self.ucaste
self.bodmodlist = self:make_bodmodlist()
--Special case of body
--First check to discover if there are any body mods
if #uc.body_appearance_modifiers > 0 then
table.insert(out, {text = "Body", modList = self.bodmodlist})
end

for i,v in ipairs(self.bplist) do
table.insert(out, {text = v.name:gsub("^%l", string.upper), modList = v.modList, isPart = true})
end

self.subviews.body:setChoices(out)
end

function editor_body:choose_cur_bp(index, choice)
local data = {["choice"] = choice}

showModifierScreen(data)
end


function editor_body:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end

self.urace = self.target_unit.race
self.ucritter = df.creature_raw.find(self.urace)
self.ucaste = self.ucritter.caste[self.target_unit.caste]

    self:addviews{
    widgets.FilteredList{
        choices=self.features_list,
        frame = {t=0, b=1,l=1},
        view_id="body",
on_submit=self:callback("choose_cur_bp")
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    {text=": select feature ",
                    key = "SELECT",
                    }
                   
                    }
            },
        }

    self:update_features()
end
add_editor(editor_body)

------ Colors editor
ColorBox = defclass(ColorBox, dialog.ListBox)
ColorBox.focus_path = 'ColorBox'

ColorBox.ATTRS{
with_filter = true,
allow_none = false,
}

function showColorPrompt(title, text, tcolor, on_select, on_cancel, min_width,allow_other, data)
    ColorBox{
        frame_title = title,
        text = text,
        text_pen = tcolor,
        on_select = on_select,
        on_cancel = on_cancel,
        frame_width = min_width,
        allow_other = allow_other,
data = data
    }:show()
end

function ColorBox:update_choices()
local choices = {}
for i,v in ipairs(self.mod.pattern_index) do
table.insert(choices, {text=patternString(self.mod.pattern_index[i]), index = i})
end

self.choices = choices
if self.subviews.list then
        self.subviews.list:setChoices(self.choices)
    end
end

function ColorBox:init(info)
self.mod = info.data.mod

self.target_unit = target
self:update_choices()
end

editor_colors=defclass(editor_colors,gui.FramedScreen)
editor_colors.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Colors editor",
    target_unit = DEFAULT_NIL,
}

function getColor(id)
return df.descriptor_color.find(id)
end

function getPattern(id)
return df.descriptor_pattern.find(id)
end

function patternString(id)
local pattern = getPattern(id)
local prefix
if pattern.pattern == 0 then --Monochrome
return getColor(pattern.colors[0]).name
elseif pattern.pattern == 1 then --Stripes
prefix = "striped"
elseif pattern.pattern == 2 then --Iris_eye
return getColor(pattern.colors[2]).name .. " eyes"
elseif pattern.pattern == 3 then --Spots
prefix = "spotted" --that's a guess
elseif pattern.pattern == 4 then --Pupil_eye
return getColor(pattern.colors[2]).name .. " eyes"
elseif pattern.pattern == 5 then --mottled
prefix = "mottled"
end
local out = prefix .. " "
for i=0, #pattern.colors-1 do
if i == #pattern.colors-1 then
out = out .. "and " .. getColor(pattern.colors[i]).name
elseif i == #pattern.colors-2 then
out = out .. getColor(pattern.colors[i]).name .. " "
else
out = out .. getColor(pattern.colors[i]).name .. ", "
end
end
return out
end

function editor_colors:change_color(index,patternId)
self.target_unit.appearance.colors[index] = patternId
end

function editor_colors:update_features()
local uc = self.ucaste
local out = {}
for i,v in ipairs(uc.color_modifiers) do
table.insert(out, {text=uc.color_modifiers[i].part:gsub("^%l", string.upper), mod = uc.color_modifiers[i], index = i})
end
self.subviews.colors:setChoices(out)
end

function editor_colors:choose_cur_feature(index,choice)
self.chosenFeature = choice
local data = {ucaste = self.ucaste, mod = choice.mod} --data to pass to color prompt
showColorPrompt("Choose color", "Select features color",nil,function ( id,choice )
self:change_color(self.chosenFeature.index, choice.index)
            end,nil,nil,true, data)
end


function editor_colors:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end

self.urace = self.target_unit.race
self.ucritter = df.creature_raw.find(self.urace)
self.ucaste = self.ucritter.caste[self.target_unit.caste]

    self:addviews{
    widgets.FilteredList{
        choices=self.features_list,
        frame = {t=0, b=1,l=1},
        view_id="colors",
on_submit=self:callback("choose_cur_feature")
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    {text=": select feature ",
                    key = "SELECT",
                    }
                   
                    }
            },
        }

    self:update_features()
end
add_editor(editor_colors)

-------------------------------main window----------------
unit_editor = defclass(unit_editor, gui.FramedScreen)
unit_editor.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "GameMaster's unit editor",
    target_unit = DEFAULT_NIL,
    }


function unit_editor:init(args)

    self:addviews{
    widgets.FilteredList{
        choices=editors,
        on_submit=function (idx,choice)
            if choice.on_submit then
                choice.on_submit(self.target_unit)
            end
        end
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    }
            },
        }
end


unit_editor{target_unit=target}:show()

Now I've remembered the whole reason I went on this modding tangent was because I wanted to make a personality + values editor, I guess now I understand GUIs a bit that'll be my next project, probably as another addition to gm-unit. In retrospect it would've been neat to have the option to press +/- minus to jump forwards/back along description thresholds so I guess I'll see about adding that in when I add the personality + values stuff. Not sure when I'll get around to it though - I'm gonna take a short break from modding to actually play Dwarf Fortress :P

Max™

  • Bay Watcher
  • [CULL:SQUARE]
    • View Profile

Oh, I can try to figure out the +/- thing for you if you got it working, fucking awesome, and the brainwash script works on personality/values as I recall?

Oh my god you are awesome, that is way better than I was trying to get it to work, I'll poke at it in a little bit and see about the keybinds since I happen to know how to do that and then if you don't have a github yourself I'll put in my scripts entry with a pull request and note that you did it.

Edit: you are frustratingly better at slapping these parts together than I am. Been trying to get the +/- input function to work but it's not grabbing the right values so I took a break from that and was trying to get the styles included but I've only had a couple of weeks of doing ANY sort of fucking around with strings.

These are the bits I was trying to add in:
Code: [Select]
[snipped lines]
    {text=": raise ",
                    key = "SECONDSCROLL_UP",
                    on_activate=self:callback("change_value",rav)},
    {text=": reduce ",
                    key = "SECONDSCROLL_DOWN",
                    on_activate=self:callback("change_value",rev)},
[snipped lines]

function modifier_selector:change_value(index,rav,rev)
    local list_wid=self.subviews.modifiers
    local _,choice=list_wid:getSelected()
    local value=self.changeType
    if rav then
choice[index] = choice[index]+1
value[index] = value[index]+1
    elseif rev then
choice[index] = choice[index]-1
value[index] = value[index]-1
    end
    self:set_value(value,index,choice)
end
I tried screwing around a few ways but I have a headache and lost track of what I was trying to do at various points so I just left it where it wasn't erroring for the moment.

The styles seem like a natural fit for the color menu trick you did:
Code: [Select]
function editor_body:make_stylelist()
local ret = {}
local sl = self.ucaste.tissue_styles
for i,v in ipairs(sl) do
table.insert(ret, {text = i.styles:gsub("_"," ","^%l", string.upper)})--bleh what am I even doing
end
return ret
end

function editor_body:update_features()
[snipped stuff up to bodmodlist...]
self.styleList = self:make_stylelist()
for i,v in ipairs(uc.tissue_styles) do
if uc.tissue_styles[i].part_idx[v]==uc.bp_appearance.part_idx[v] then
table.insert(out, {text = i.token:gsub("^%l", string.upper), modList = self.styleList})
end
end
I go through periods of playing, modding, and sometimes everything clicks and I hack out scripts until I get stumped, I don't question it because it's fun whether I'm hacking at lua or goblin guts, yanno?
Spoiler (click to show/hide)
« Last Edit: July 04, 2017, 11:55:37 pm by Max™ »
Logged
Engraved here is a rendition of an image of the Dwarf Fortress learning curve. All craftsdwarfship is of the highest quality. It depicts an obsidian overhang which menaces with spikes of obsidian and tears. Carved on the overhang is an image of Toady One and the players. The players are curled up in a fetal position. Toady One is laughing. The players are burning.
The VectorCurses+1 tileset strikes the square set and the severed part sails off in an arc!

Atkana

  • Bay Watcher
  • [CURIOUSBEAST]
    • View Profile

-snip-
Code: (gm-newnit.lua) [Select]
-- Interface powered, user friendly, unit editor

--[====[

gui/gm-unit
===========
An editor for various unit attributes.

]====]
local gui = require 'gui'
local dialog = require 'gui.dialogs'
local widgets =require 'gui.widgets'
local guiScript = require 'gui.script'
local utils = require 'utils'
local args={...}


local target
--TODO: add more ways to guess what unit you want to edit
if args[1]~= nil then
    target=df.units.find(args[1])
else
    target=dfhack.gui.getSelectedUnit(true)
end

if target==nil then
    qerror("No unit to edit") --TODO: better error message
end
local editors={}
function add_editor(editor_class)
    table.insert(editors,{text=editor_class.ATTRS.frame_title,on_submit=function ( unit )
        editor_class{target_unit=unit}:show()
    end})
end
-------------------------------various subeditors---------
--TODO set local sould or better yet skills vector to reduce long skill list access typing
editor_skills=defclass(editor_skills,gui.FramedScreen)
editor_skills.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Skill editor",
    target_unit = DEFAULT_NIL,
    learned_only= false,
}
function list_skills(unit,learned_only)
    local s_=df.job_skill
    local u_skills=unit.status.current_soul.skills
    local ret={}
    for i,v in ipairs(s_) do
        if i>=0 then
            local u_skill=utils.binsearch(u_skills,i,"id")
            if u_skill or not learned_only then
                if not u_skill then
                    u_skill={rating=-1,experience=0}
                end

                local rating
                if u_skill.rating >=0 then
                    rating=df.skill_rating.attrs[u_skill.rating]
                else
                    rating={caption="<unlearned>",xp_threshold=0}
                end

                local text=string.format("%s: %s %d %d/%d",df.job_skill.attrs[i].caption,rating.caption,u_skill.rating,u_skill.experience,rating.xp_threshold)
                table.insert(ret,{text=text,id=i})
            end
        end
    end
    return ret
end
function editor_skills:update_list(no_save_place)
    local skill_list=list_skills(self.target_unit,self.learned_only)
    if no_save_place then
        self.subviews.skills:setChoices(skill_list)
    else
        self.subviews.skills:setChoices(skill_list,self.subviews.skills:getSelected())
    end
end
function editor_skills:init( args )
    if self.target_unit.status.current_soul==nil then
        qerror("Unit does not have soul, can't edit skills")
    end

    local skill_list=list_skills(self.target_unit,self.learned_only)

    self:addviews{
    widgets.FilteredList{
        choices=skill_list,
        frame = {t=0, b=1,l=1},
        view_id="skills",
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    {text=": remove level ",
                    key = "SECONDSCROLL_UP",
                    on_activate=self:callback("level_skill",-1)},
                    {text=": add level ",
                    key = "SECONDSCROLL_DOWN",
                    on_activate=self:callback("level_skill",1)}
                    ,
                    {text=": show learned only ",
                    key = "CHANGETAB",
                    on_activate=function ()
                        self.learned_only=not self.learned_only
                        self:update_list(true)
                    end}
                    }
            },
        }
end
function editor_skills:get_cur_skill()
    local list_wid=self.subviews.skills
    local _,choice=list_wid:getSelected()
    if choice==nil then
        qerror("Nothing selected")
    end
    local u_skill=utils.binsearch(self.target_unit.status.current_soul.skills,choice.id,"id")
    return choice,u_skill
end
function editor_skills:level_skill(lvl)
    local sk_en,sk=self:get_cur_skill()
    if lvl >0 then
        local rating

        if sk then
            rating=sk.rating+lvl
        else
            rating=lvl-1
        end

        utils.insert_or_update(self.target_unit.status.current_soul.skills, {new=true, id=sk_en.id, rating=rating}, 'id') --TODO set exp?
    elseif sk and sk.rating==0 and lvl<0 then
        utils.erase_sorted_key(self.target_unit.status.current_soul.skills,sk_en.id,"id")
    elseif sk and lvl<0 then
        utils.insert_or_update(self.target_unit.status.current_soul.skills, {new=true, id=sk_en.id, rating=sk.rating+lvl}, 'id') --TODO set exp?
    end
    self:update_list()
end
function editor_skills:remove_rust(skill)
    --TODO
end
add_editor(editor_skills)
------- civ editor
RaceBox = defclass(RaceBox, dialog.ListBox)
RaceBox.focus_path = 'RaceBox'

RaceBox.ATTRS{
    format_name="$NAME ($TOKEN)",
    with_filter=true,
    allow_none=false,
}
function RaceBox:format_creature(creature_raw)
    local t = {NAME=creature_raw.name[0],TOKEN=creature_raw.creature_id}
    return string.gsub(self.format_name, "%$(%w+)", t)
end
function RaceBox:preinit(info)
    self.format_name=RaceBox.ATTRS.format_name or info.format_name -- preinit does not have ATTRS set yet
    local choices={}
    if RaceBox.ATTRS.allow_none or info.allow_none then
        table.insert(choices,{text="<none>",num=-1})
    end
    for i,v in ipairs(df.global.world.raws.creatures.all) do
        local text=self:format_creature(v)
        table.insert(choices,{text=text,raw=v,num=i,search_key=text:lower()})
    end
    info.choices=choices
end
function showRacePrompt(title, text, tcolor, on_select, on_cancel, min_width,allow_none)
    RaceBox{
        frame_title = title,
        text = text,
        text_pen = tcolor,
        on_select = on_select,
        on_cancel = on_cancel,
        frame_width = min_width,
        allow_none = allow_none,
    }:show()
end
CivBox = defclass(CivBox,dialog.ListBox)
CivBox.focus_path = "CivBox"

CivBox.ATTRS={
    format_name="$NAME ($ENGLISH):$ID",
    format_no_name="<unnamed>:$ID",
    name_other="<other(-1)>",
    with_filter=true,
    allow_other=false,
}

function civ_name(id,format_name,format_no_name,name_other,name_invalid)
    if id==-1 then
        return name_other or "<other (-1)>"
    end
    local civ
    if type(id)=='userdata' then
        civ=id
    else
        civ=df.historical_entity.find(id)
        if civ==nil then
            return name_invalid or "<invalid>"
        end
    end
    local t={NAME=dfhack.TranslateName(civ.name),ENGLISH=dfhack.TranslateName(civ.name,true),ID=civ.id} --TODO race?, maybe something from raws?
    if t.NAME=="" then
        return string.gsub(format_no_name or "<unnamed>:$ID", "%$(%w+)", t)
    end
    return string.gsub(format_name or "$NAME ($ENGLISH):$ID", "%$(%w+)", t)
end
function CivBox:update_choices()
    local choices={}
    if self.allow_other then
        table.insert(choices,{text=self.name_other,num=-1})
    end

    for i,v in ipairs(df.global.world.entities.all) do
        if not self.race_filter or (v.race==self.race_filter) then --TODO filter type
            local text=civ_name(v,self.format_name,self.format_no_name,self.name_other,self.name_invalid)
            table.insert(choices,{text=text,raw=v,num=i})
        end
    end
    self.choices=choices
    if self.subviews.list then
        self.subviews.list:setChoices(self.choices)
    end
end
function CivBox:update_race_filter(id)
    local raw=df.creature_raw.find(id)
    if raw then
        self.subviews.race_label:setText(": "..raw.name[0])
        self.race_filter=id
    else
        self.subviews.race_label:setText(": <none>")
        self.race_filter=nil
    end

    self:update_choices()
end
function CivBox:choose_race()
    showRacePrompt("Choose race","Select new race:",nil,function (id,choice)
        self:update_race_filter(choice.num)
    end,nil,nil,true)
end
function CivBox:init(info)
    self.subviews.list.frame={t=3,r=0,l=0}
    self:addviews{
        widgets.Label{frame={t=1,l=0},text={
        {text="Filter race ",key="CUSTOM_CTRL_A",key_sep="()",on_activate=self:callback("choose_race")},
        }},
        widgets.Label{frame={t=1,l=21},view_id="race_label",
        text=": <none>",
        }
    }
    self:update_choices()
end
function showCivPrompt(title, text, tcolor, on_select, on_cancel, min_width,allow_other)
    CivBox{
        frame_title = title,
        text = text,
        text_pen = tcolor,
        on_select = on_select,
        on_cancel = on_cancel,
        frame_width = min_width,
        allow_other = allow_other,
    }:show()
end

editor_civ=defclass(editor_civ,gui.FramedScreen)
editor_civ.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Civilization editor",
    target_unit = DEFAULT_NIL,
    }

function editor_civ:update_curren_civ()
    self.subviews.civ_name:setText("Currently: "..civ_name(self.target_unit.civ_id))
end
function editor_civ:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end

    self:addviews{
    widgets.Label{view_id="civ_name",frame = { t=1,l=1}, text="Currently: "..civ_name(self.target_unit.civ_id)},
    widgets.Label{frame = { t=2,l=1}, text={{text=": set to other (-1, usually enemy)",key="CUSTOM_N",
        on_activate= function() self.target_unit.civ_id=-1;self:update_curren_civ() end}}},
    widgets.Label{frame = { t=3,l=1}, text={{text=": set to current civ("..df.global.ui.civ_id..")",key="CUSTOM_C",
        on_activate= function() self.target_unit.civ_id=df.global.ui.civ_id;self:update_curren_civ() end}}},
    widgets.Label{frame = { t=4,l=1}, text={{text=": manually enter",key="CUSTOM_E",
        on_activate=function ()
         dialog.showInputPrompt("Civ id","Enter new civ id:",COLOR_WHITE,
            tostring(self.target_unit.civ_id),function(new_value)
                self.target_unit.civ_id=new_value
                self:update_curren_civ()
            end)
        end}}
        },
    widgets.Label{frame= {t=5,l=1}, text={{text=": select from list",key="CUSTOM_L",
        on_activate=function (  )
            showCivPrompt("Choose civilization", "Select units civilization",nil,function ( id,choice )
                self.target_unit.civ_id=choice.num
                self:update_curren_civ()
            end,nil,nil,true)
        end
        }}},
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    }
            },
        }
end
add_editor(editor_civ)
------- counters editor
editor_counters=defclass(editor_counters,gui.FramedScreen)
editor_counters.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Counters editor",
    target_unit = DEFAULT_NIL,
    counters1={
    "think_counter",
    "job_counter",
    "swap_counter",
    "winded",
    "stunned",
    "unconscious",
    "suffocation",
    "webbed",
    "soldier_mood_countdown",
    "soldier_mood", --todo enum,
    "pain",
    "nausea",
    "dizziness",
    },
    counters2={
    "paralysis",
    "numbness",
    "fever",
    "exhaustion",
    "hunger_timer",
    "thirst_timer",
    "sleepiness_timer",
    "stomach_content",
    "stomach_food",
    "vomit_timeout",
    "stored_fat" --TODO what to reset to?
    }
}
function editor_counters:fill_counters()
    local ret={}
    local u=self.target_unit
    for i,v in ipairs(self.counters1) do
        table.insert(ret,{f=u.counters:_field(v),name=v})
    end
    for i,v in ipairs(self.counters2) do
        table.insert(ret,{f=u.counters2:_field(v),name=v})
    end
    return ret
end
function editor_counters:update_counters()
    for i,v in ipairs(self.counter_list) do
        v.text=string.format("%s:%d",v.name,v.f.value)
    end
    self.subviews.counters:setChoices(self.counter_list)
end
function editor_counters:set_cur_counter(value,index,choice)
    choice.f.value=value
    self:update_counters()
end
function editor_counters:choose_cur_counter(index,choice)
    dialog.showInputPrompt(choice.name,"Enter new value:",COLOR_WHITE,
            tostring(choice.f.value),function(new_value)
                self:set_cur_counter(new_value,index,choice)
            end)
end
function editor_counters:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end

    self.counter_list=self:fill_counters()


    self:addviews{
    widgets.FilteredList{
        choices=self.counter_list,
        frame = {t=0, b=1,l=1},
        view_id="counters",
        on_submit=self:callback("choose_cur_counter"),
        on_submit2=self:callback("set_cur_counter",0),--TODO some things need to be set to different defaults
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    {text=": reset counter ",
                    key = "SEC_SELECT",
                    },
                    {text=": set counter ",
                    key = "SELECT",
                    }
                   
                    }
            },
        }
    self:update_counters()
end
add_editor(editor_counters)

wound_creator=defclass(wound_creator,gui.FramedScreen)
wound_creator.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Wound creator",
    target_wound = DEFAULT_NIL,
    --filter
}
function wound_creator:init( args )
    if self.target_wound==nil then
        qerror("invalid wound")
    end
   

    self:addviews{
    widgets.List{
       
        frame = {t=0, b=1,l=1},
        view_id="fields",
        on_submit=self:callback("edit_cur_wound"),
        on_submit2=self:callback("delete_current_wound")
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")},

                    {text=": edit wound ",
                    key = "SELECT"},

                    {text=": delete wound ",
                    key = "SEC_SELECT"},
                    {text=": create wound ",
                    key = "CUSTOM_CTRL_I",
                    on_activate= self:callback("create_new_wound")},

                    }
            },
        }
    self:update_wounds()
end
-------------------
editor_wounds=defclass(editor_wounds,gui.FramedScreen)
editor_wounds.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Wound editor",
    target_unit = DEFAULT_NIL,
    --filter
}
function is_scar( wound_part )
    return wound_part.flags1.scar_cut or wound_part.flags1.scar_smashed or
        wound_part.flags1.scar_edged_shake1 or wound_part.flags1.scar_blunt_shake1
end
function format_flag_name( fname )
    return fname:sub(1,1):upper()..fname:sub(2):gsub("_"," ")
end
function name_from_flags( wp )
    for i,v in ipairs(wp.flags1) do
        if v then
            return format_flag_name(df.wound_damage_flags1[i])
        end
    end
    for i,v in ipairs(wp.flags2) do
        if v then
            return format_flag_name(df.wound_damage_flags2[i])
        end
    end
    return "<unnamed wound>"
end
function format_wound( list_id,wound, unit)

    local name="<unnamed wound>"
    if #wound.parts>0 and #wound.parts[0].effect_type>0 then --try to make wound name by effect...
        name=tostring(df.wound_effect_type[wound.parts[0].effect_type[0]])
        if #wound.parts>1 then --cheap and probably incorrect...
            name=name.."s"
        end
    elseif #wound.parts>0 and is_scar(wound.parts[0]) then
        name="Scar"
    elseif #wound.parts>0 then
        local wp=wound.parts[0]
        name=name_from_flags(wp)
    end

    return string.format("%d. %s id=%d",list_id,name,wound.id)
end
function editor_wounds:update_wounds()
    local ret={}
    for i,v in ipairs(self.trg_wounds) do
        table.insert(ret,{text=format_wound(i,v,self.target_unit),wound=v})
    end
    self.subviews.wounds:setChoices(ret)
    self.wound_list=ret
end
function editor_wounds:dirty_unit()
    print("todo: implement unit status recalculation")
end
function editor_wounds:get_cur_wound()
    local list_wid=self.subviews.wounds
    local _,choice=list_wid:getSelected()
    if choice==nil then
        qerror("Nothing selected")
    end
    local ret_wound=utils.binsearch(self.trg_wounds,choice.id,"id")
    return choice,ret_wound
end
function editor_wounds:delete_current_wound(index,choice)
   
    utils.erase_sorted(self.trg_wounds,choice.wound,"id")
    choice.wound:delete()
    self:dirty_unit()
    self:update_wounds()
end
function editor_wounds:create_new_wound()
    print("Creating")
end
function editor_wounds:edit_cur_wound(index,choice)
   
end
function editor_wounds:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end
    self.trg_wounds=self.target_unit.body.wounds

    self:addviews{
    widgets.List{
       
        frame = {t=0, b=1,l=1},
        view_id="wounds",
        on_submit=self:callback("edit_cur_wound"),
        on_submit2=self:callback("delete_current_wound")
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")},

                    {text=": edit wound ",
                    key = "SELECT"},

                    {text=": delete wound ",
                    key = "SEC_SELECT"},
                    {text=": create wound ",
                    key = "CUSTOM_CTRL_I",
                    on_activate= self:callback("create_new_wound")},

                    }
            },
        }
    self:update_wounds()
end
add_editor(editor_wounds)

------ Body editor
modifier_selector = defclass(modifier_selector, gui.FramedScreen)

function modifierString(mod)
local out = df.appearance_modifier_type[mod.type]
out = out:lower() --Make lowercase
out = out:gsub("_", " ") --Replace underscores with spaces
out = out:gsub("^%l", string.upper) --capitalises first letter

return out
end

function showModifierScreen(data)
modifier_selector{
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Select a modifier",
    target_unit = DEFAULT_NIL,
data = data
    }:show()
end

function modifier_selector:set_value(value,index,choice)
for i,v in ipairs(choice.changes) do
self.changeType[v] = value
end
self:update_features()
end

function modifier_selector:on_select(index, choice)
dialog.showInputPrompt(modifierString(choice.mod),"Enter new value:",COLOR_WHITE,
            tostring(self.changeType[choice.changes[1]]),function(new_value)
                self:set_value(new_value,index,choice)
            end)
end

function modifier_selector:update_features()
local out = {}
for i, v in ipairs(self.partPicked.modList) do
table.insert(out, {text = (modifierString(v.modifier) .. ": " .. self.changeType[v.changes[1]]), mod = v.modifier, changes = v.changes})
end
self.subviews.modifiers:setChoices(out)
end

--The following function was written on a day I couldn't brain. There's probably a simpler way to implement this but this way made sense to me at the time - Atkana
function modifier_selector:step_value(dir)
local index, choice = self.subviews.modifiers:getSelected()
if not choice then --It's possible this gets called when there isn't actually anything selected because of how filtered lists work
return
end
local ranges = {} --Records the value at every step of the description range
for i, v in ipairs(choice.mod.desc_range) do
if #ranges == 0 or v > ranges[#ranges] then --Don't bother adding any entries if the same as the previous
table.insert(ranges, v)
end
end

local cur --The index for ranges that the current modifier lies on
local curValue = self.changeType[choice.changes[1]]
for i, v in ipairs(ranges) do
if ranges[i+1] then --If there's a next entry
if curValue < ranges[i+1] then --If the current value is less than the next entry
cur = i
break
end
else --This is the last entry
cur = i
end
end

local newVal --New value the chosen modifier will be set to
if dir > 0 then --positive direction
newVal = ranges[cur+dir] or ranges[#ranges]
else
newVal = ranges[cur+dir] or ranges[1]
end

self:set_value(newVal, index, choice)
end

function modifier_selector:init( info )
self.partPicked = info.data.choice --The part that was picked in editor_body

if info.data.choice.isPart then
self.changeType = target.appearance.bp_modifiers
else
self.changeType = target.appearance.body_modifiers
end

    self:addviews{
widgets.FilteredList{
        frame = {t=0, b=1,l=1},
        view_id="modifiers",
on_submit=self:callback("on_select")
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": back to part selector ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    {text=": edit modifier ",
                    key = "SELECT",
                    },
{text=": raise ",
                    --key = "SECONDSCROLL_DOWN",
key = "STANDARDSCROLL_RIGHT",
                    on_activate=self:callback("step_value",1)},
{text=": reduce ",
                    --key = "SECONDSCROLL_UP",
key = "STANDARDSCROLL_LEFT",
                    on_activate=self:callback("step_value",-1)},
                    }
            },
        }

    self:update_features()
end


editor_body=defclass(editor_body,gui.FramedScreen)
editor_body.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Body modifier editor",
    target_unit = DEFAULT_NIL,
}

function editor_body:bp_links()
local out = {}
local uc = self.ucaste
for i,v in ipairs(uc.bp_appearance.part_idx) do
out[i] = {["modId"] = uc.bp_appearance.modifier_idx[i], ["partId"] = uc.bp_appearance.part_idx[i]}
end

return out
end

--Following is a relic from my original change-appearance script. There's probably a more efficient way of doing this, but I'm not in the mood to be redesigning ;P
function editor_body:make_bplist()
local ret = {}
local bpm = self.ucaste.bp_appearance.modifiers
local links = self:bp_links()
local point = {}

for i, v in ipairs(bpm) do
local mod = v

local bpmname
if #mod.noun > 0 then
bpmname = mod.noun
else
bpmname = self.ucaste.body_info.body_parts[mod.body_parts[0]].name_singular[0].value
end

local changes = {}
for i2, v2 in ipairs(mod.body_parts) do
local partId = v2
for i3, v3 in ipairs(links) do
if v3.modId == i and v3.partId == partId then
table.insert(changes, i3) --?
end
end
end

if point[bpmname] then
table.insert(ret[point[bpmname]].modList, {["modifier"] = mod, ["changes"] = changes})
else
table.insert(ret, {["name"] = bpmname, ["modList"] = {[1] = {["modifier"] = mod, ["changes"] = changes}}})
point[bpmname] = #ret --Stores the index of the name for future additions
end
end
return ret
end

function editor_body:make_bodmodlist() --Version of make_bplist() to make a spoof version for body so it can be treated the same. Only makes modList
local ret = {}
local bm = self.ucaste.body_appearance_modifiers

for i, v in ipairs(bm) do
table.insert(ret, {["modifier"] = v, ["changes"] = {[1] = i}})
end

return ret
end

function editor_body:update_features()
self.bplist = self:make_bplist()
local out = {}
local uc = self.ucaste
self.bodmodlist = self:make_bodmodlist()

--Special case of body
--First check to discover if there are any body mods
if #uc.body_appearance_modifiers > 0 then
table.insert(out, {text = "Body", modList = self.bodmodlist})
end

for i,v in ipairs(self.bplist) do
table.insert(out, {text = v.name:gsub("^%l", string.upper), modList = v.modList, isPart = true})
end

self.subviews.body:setChoices(out)
end

function editor_body:choose_cur_bp(index, choice)
local data = {["choice"] = choice}

showModifierScreen(data)
end


function editor_body:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end

self.urace = self.target_unit.race
self.ucritter = df.creature_raw.find(self.urace)
self.ucaste = self.ucritter.caste[self.target_unit.caste]

    self:addviews{
    widgets.FilteredList{
        choices=self.features_list,
        frame = {t=0, b=1,l=1},
        view_id="body",
on_submit=self:callback("choose_cur_bp")
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    {text=": select feature ",
                    key = "SELECT",
                    }
                   
                    }
            },
        }

    self:update_features()
end
add_editor(editor_body)

------ Colors editor
ColorBox = defclass(ColorBox, dialog.ListBox)
ColorBox.focus_path = 'ColorBox'

ColorBox.ATTRS{
with_filter = true,
allow_none = false,
}

function showColorPrompt(title, text, tcolor, on_select, on_cancel, min_width,allow_other, data)
    ColorBox{
        frame_title = title,
        text = text,
        text_pen = tcolor,
        on_select = on_select,
        on_cancel = on_cancel,
        frame_width = min_width,
        allow_other = allow_other,
data = data
    }:show()
end

function ColorBox:update_choices()
local choices = {}
for i,v in ipairs(self.mod.pattern_index) do
table.insert(choices, {text=patternString(self.mod.pattern_index[i]), index = i})
end

self.choices = choices
if self.subviews.list then
        self.subviews.list:setChoices(self.choices)
    end
end

function ColorBox:init(info)
self.mod = info.data.mod

self.target_unit = target
self:update_choices()
end

editor_colors=defclass(editor_colors,gui.FramedScreen)
editor_colors.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Colors editor",
    target_unit = DEFAULT_NIL,
}

function getColor(id)
return df.descriptor_color.find(id)
end

function getPattern(id)
return df.descriptor_pattern.find(id)
end

function patternString(id)
local pattern = getPattern(id)
local prefix
if pattern.pattern == 0 then --Monochrome
return getColor(pattern.colors[0]).name
elseif pattern.pattern == 1 then --Stripes
prefix = "striped"
elseif pattern.pattern == 2 then --Iris_eye
return getColor(pattern.colors[2]).name .. " eyes"
elseif pattern.pattern == 3 then --Spots
prefix = "spotted" --that's a guess
elseif pattern.pattern == 4 then --Pupil_eye
return getColor(pattern.colors[2]).name .. " eyes"
elseif pattern.pattern == 5 then --mottled
prefix = "mottled"
end
local out = prefix .. " "
for i=0, #pattern.colors-1 do
if i == #pattern.colors-1 then
out = out .. "and " .. getColor(pattern.colors[i]).name
elseif i == #pattern.colors-2 then
out = out .. getColor(pattern.colors[i]).name .. " "
else
out = out .. getColor(pattern.colors[i]).name .. ", "
end
end
return out
end

function editor_colors:change_color(index,patternId)
self.target_unit.appearance.colors[index] = patternId
end

function editor_colors:update_features()
local uc = self.ucaste
local out = {}
for i,v in ipairs(uc.color_modifiers) do
table.insert(out, {text=uc.color_modifiers[i].part:gsub("^%l", string.upper), mod = uc.color_modifiers[i], index = i})
end
self.subviews.colors:setChoices(out)
end

function editor_colors:choose_cur_feature(index,choice)
self.chosenFeature = choice
local data = {ucaste = self.ucaste, mod = choice.mod} --data to pass to color prompt
showColorPrompt("Choose color", "Select features color",nil,function ( id,choice )
self:change_color(self.chosenFeature.index, choice.index)
            end,nil,nil,true, data)
end


function editor_colors:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end

self.urace = self.target_unit.race
self.ucritter = df.creature_raw.find(self.urace)
self.ucaste = self.ucritter.caste[self.target_unit.caste]

    self:addviews{
    widgets.FilteredList{
        choices=self.features_list,
        frame = {t=0, b=1,l=1},
        view_id="colors",
on_submit=self:callback("choose_cur_feature")
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor ",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    {text=": select feature ",
                    key = "SELECT",
                    }
                   
                    }
            },
        }

    self:update_features()
end
add_editor(editor_colors)

------ Values (/beliefs)
editor_beliefs=defclass(editor_beliefs,gui.FramedScreen)
editor_beliefs.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Beliefs editor",
    target_unit = DEFAULT_NIL,
}

function editor_beliefs:buildPointers()
local out = {}
for i, v in ipairs(self.target_unit.status.current_soul.personality.values) do
out[v.type] = i
end
self.pointers = out
end

function editor_beliefs:getCurBeliefValue(unit, beliefId)
local upers = unit.status.current_soul.personality
if self.pointers[beliefId] then
return upers.values[self.pointers[beliefId]].strength, false
elseif upers.cultural_identity ~= -1 then
return df.cultural_identity.find(upers.cultural_identity).values[beliefId], true
else
return 0, true --outsiders have no culture
end
end

function editor_beliefs:update_choices()
self:buildPointers()
local out = {}
for i, v in ipairs(df.value_type) do
local niceText = v
niceText = niceText:lower()
niceText = niceText:gsub("_", " ")
niceText = niceText:gsub("^%l", string.upper)

local strength, isCulture = self:getCurBeliefValue(self.target_unit, i)
local numAddition = strength
if isCulture then
numAddition = numAddition .. "*"
end
table.insert(out, {["text"] = niceText .. ": " .. numAddition, ["beliefId"] = i, ["strength"] = strength, ["name"] = niceText})
end
self.subviews.beliefs:setChoices(out)
end

function editor_beliefs:set_belief(new_value, index, choice)
dfhack.run_script("modtools/set-belief", table.unpack({"-value", '\\' .. new_value,"-target",tostring(self.target_unit.id), "-belief", tostring(choice.beliefId)}))
self:update_choices()
end

function editor_beliefs:step_belief(dir)
local index, choice = self.subviews.beliefs:getSelected()
if not choice then
return
end
dfhack.run_script("modtools/set-belief", table.unpack({"-target",tostring(self.target_unit.id), "-belief", tostring(choice.beliefId), "-step", "\\" .. dir}))
self:update_choices()
end

function editor_beliefs:edit_belief(index, choice)
dialog.showInputPrompt(choice.name,"Enter new value:",COLOR_WHITE,
            tostring(choice.strength),function(new_value)
                self:set_belief(new_value,index,choice)
            end)
--This one causes choices to flicker for some reason
end

function editor_beliefs:default_belief(index, choice)
dfhack.run_script("modtools/set-belief", table.unpack({"-target",tostring(self.target_unit.id), "-belief", tostring(choice.beliefId), "-default"}))
self:update_choices()
end

function editor_beliefs:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end

    self:addviews{
widgets.FilteredList{
frame = {t=0, b=2,l=1},
view_id="beliefs",
on_submit=self:callback("edit_belief"),
on_submit2=self:callback("default_belief")
},
widgets.Label{
frame = {b=1, l=1},
text ={{text= ": exit editor ",
key  = "LEAVESCREEN",
on_activate= self:callback("dismiss")
},
{text=": edit value ",
key = "SELECT",
},
{text=": raise ",
key = "STANDARDSCROLL_RIGHT",
on_activate=self:callback("step_belief",1)},
{text=": reduce ",
key = "STANDARDSCROLL_LEFT",
on_activate=self:callback("step_belief",-1)},
}
},
widgets.Label{
frame = {b=0, l=1},
text = {
{
text = "* denotes cultural default  "
},
{
text=": set to cultural default ",
key = "SEC_SELECT",
},
}



},
    }

    self:update_choices()
end
add_editor(editor_beliefs)

------ Personality
editor_pers=defclass(editor_pers,gui.FramedScreen)
editor_pers.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "Personality editor",
    target_unit = DEFAULT_NIL,
}

function editor_pers:getCurTraitValue(unit, traitId)
return unit.status.current_soul.personality.traits[traitId]
end

function editor_pers:update_choices()
local out = {}
for i, v in ipairs(df.personality_facet_type) do
local niceText = v
niceText = niceText:lower()
niceText = niceText:gsub("_", " ")
niceText = niceText:gsub("^%l", string.upper)

local strength = self:getCurTraitValue(self.target_unit, i)

table.insert(out, {["text"] = niceText .. ": " .. strength, ["traitId"] = i, ["strength"] = strength, ["name"] = niceText})
end
self.subviews.traits:setChoices(out)
end

function editor_pers:set_trait(new_value, index, choice)
dfhack.run_script("modtools/set-personality", table.unpack({"-value", '\\' .. new_value,"-target",tostring(self.target_unit.id), "-trait", tostring(choice.traitId)}))
self:update_choices()
end

function editor_pers:step_trait(dir)
local index, choice = self.subviews.traits:getSelected()
if not choice then
return
end
dfhack.run_script("modtools/set-personality", table.unpack({"-target",tostring(self.target_unit.id), "-trait", tostring(choice.traitId), "-step", "\\" .. dir}))
self:update_choices()
end

function editor_pers:edit_trait(index, choice)
dialog.showInputPrompt(choice.name,"Enter new value:",COLOR_WHITE,
            tostring(choice.strength),function(new_value)
                self:set_trait(new_value,index,choice)
            end)
end

function editor_pers:average_trait(index, choice)
dfhack.run_script("modtools/set-personality", table.unpack({"-target",tostring(self.target_unit.id), "-trait", tostring(choice.traitId), "-average"}))
self:update_choices()
end

function editor_pers:init( args )
    if self.target_unit==nil then
        qerror("invalid unit")
    end

    self:addviews{
widgets.FilteredList{
frame = {t=0, b=2,l=1},
view_id="traits",
on_submit=self:callback("edit_trait"),
on_submit2=self:callback("average_trait")
},
widgets.Label{
frame = {b=1, l=1},
text ={{text= ": exit editor ",
key  = "LEAVESCREEN",
on_activate= self:callback("dismiss")
},
{text=": edit value ",
key = "SELECT",
},
{text=": raise ",
key = "STANDARDSCROLL_RIGHT",
on_activate=self:callback("step_trait",1)},
{text=": reduce ",
key = "STANDARDSCROLL_LEFT",
on_activate=self:callback("step_trait",-1)},
}
},
widgets.Label{
frame = {b=0, l=1},
text = {
{
text=": set to caste average",
key = "SEC_SELECT",
},
}



},
    }

    self:update_choices()
end
add_editor(editor_pers)


-------------------------------main window----------------
unit_editor = defclass(unit_editor, gui.FramedScreen)
unit_editor.ATTRS={
    frame_style = gui.GREY_LINE_FRAME,
    frame_title = "GameMaster's unit editor",
    target_unit = DEFAULT_NIL,
    }


function unit_editor:init(args)

    self:addviews{
    widgets.FilteredList{
        choices=editors,
        on_submit=function (idx,choice)
            if choice.on_submit then
                choice.on_submit(self.target_unit)
            end
        end
    },
    widgets.Label{
                frame = { b=0,l=1},
                text ={{text= ": exit editor",
                    key  = "LEAVESCREEN",
                    on_activate= self:callback("dismiss")
                    },
                    }
            },
        }
end


unit_editor{target_unit=target}:show()
I got +/- to jump description ranges added, though I chose left/right instead (which're annoyingly prompted as 4/6 in the ui and I'm not sure if I can change it). While I was at it, I added personality and beliefs editing too, and also gave them the +/- feature. I forgot about styles (and don't really know much about them), I guess I'll have to look into those at some point #.# Note: this version relies on the current iteration of my modtools which, in the interest of screwing with anyone attempting to follow all this, is in an entirely different thread (here) :P

Max™

  • Bay Watcher
  • [CULL:SQUARE]
    • View Profile

Awesome, I'm curious what your coding background is btw.
Logged
Engraved here is a rendition of an image of the Dwarf Fortress learning curve. All craftsdwarfship is of the highest quality. It depicts an obsidian overhang which menaces with spikes of obsidian and tears. Carved on the overhang is an image of Toady One and the players. The players are curled up in a fetal position. Toady One is laughing. The players are burning.
The VectorCurses+1 tileset strikes the square set and the severed part sails off in an arc!

Atkana

  • Bay Watcher
  • [CURIOUSBEAST]
    • View Profile

Awesome, I'm curious what your coding background is btw.
A while ago I read a book on/the manual for lua so I could get the computers in a Minecraft mod to tell jokes (as well as do some progressively more useful things) and also an introduction book to Unity which covered a bit about C# scripting. The rest of what I know is basically just a mix of learning from experience, picking apart what other people do to see how it works, and a healthy amount of Google-fu :P

Max™

  • Bay Watcher
  • [CULL:SQUARE]
    • View Profile

Well hell, still say you should set up a github and push stuff to the main dfhack repo.
Logged
Engraved here is a rendition of an image of the Dwarf Fortress learning curve. All craftsdwarfship is of the highest quality. It depicts an obsidian overhang which menaces with spikes of obsidian and tears. Carved on the overhang is an image of Toady One and the players. The players are curled up in a fetal position. Toady One is laughing. The players are burning.
The VectorCurses+1 tileset strikes the square set and the severed part sails off in an arc!