--!name 5E Character Sheet
--!icon sword
--!category Games
local at=6 local aK=8000 local i,E={},{}local D=0 local function A(W)local f=i[W]if f==nil and E[W]then f=E[W].d end return f end local function r(W)return tonumber(A(W))or 0 end local function C(o)return o>=0 and"+"..o or tostring(o)end local function H(a)return tonumber(i[a[1].."_"])or a[4]or 0 end local function Q(a)local al=H(a)local f=tonumber(i[a[1]])or al return f>al and al or f end local function X(a)local e=i[a[1]]if type(e)~="table"then e={};i[a[1]]=e end return e end local aJ,au="5E Characters","nm"local ac={"Strength","Dexterity","Constitution","Intelligence","Wisdom","Charisma"}local as={"STR","DEX","CON","INT","WIS","CHA"}local ad={"st","dx","co","it","wi","ch"}local aQ={300,900,2700,6500,14000,23000,34000,48000,64000,85000,100000,120000,140000,165000,195000,225000,265000,305000,355000}local function _()return math.max(1,r("lv"))end local function G()return 2+(_()-1)//4 end local function P(b)return(r(ad[b])-10)//2 end local ax,aG,af={},{},{}for b=1,6 do local a={ad[b],"n",ac[b],1,30,10}a.s=function(f)return ac[b].."  "..f.."   ("..C((f-10)//2)..")"end ax[b]=a a={"s"..ad[b],"k",ac[b],{"[ ]","[x]"}}a.s=function(f)return(f>0 and"[x]  "or"[  ]  ")..ac[b].." save   "..C(P(b)+f*G())end aG[b]=a end local aw={"[  ]","[x]","[E]"}local av local aP="Acrobatics2Animal Handling5Arcana4Athletics1Deception6History4Insight5Intimidation6".."Investigation4Medicine5Nature4Perception5Performance6Persuasion6Religion4Sleight of Hand2Stealth2Survival5"for an,s in aP:gmatch("([%a ]+)(%d)")do s=tonumber(s)local a={"k"..(#af+1),"k",an,aw}if an=="Perception"then av=a[1]end a.s=function(f)return aw[f%3+1].."  "..an.." ("..as[s]..")   "..C(P(s)+f%3*G())end af[#af+1]=a end local function aE()return 10+P(5)+r(av)%3*G()end local J={{"sa","c","Spellcasting ability",{"none","Intelligence","Wisdom","Charisma"}},}for b=1,9 do J[#J+1]={"l"..b,"p","Level "..b.." slots",0}end J[#J+1]={"ca","l","Cantrips",20,"cantrip",50}J[#J+1]={"sl","l","Spells",40,"spell",60}local function a7()i.hp,i.th,i.ds,i.df=nil,0,0,0 i.hl=_()for b=1,9 do i["l"..b]=nil end end local function aT()local aB=aQ[_()]return"Proficiency bonus "..C(G())..(aB and"   Next level at "..aB.." XP"or"")end local function aU()return(A("cl")~=""and A("cl").." "or"Level ").._()end local function aR()local e={}for b=1,6 do e[b]=as[b]..r(ad[b])end return table.concat(e," ")end local function ba()return"Passive Perception "..aE().."   [E] = expertise"end local function aV()return"Initiative "..C(P(2)).."   Proficiency "..C(G()).."   Passive "..aE()end local function aW()local a=E.hp return"HP "..Q(a).."/"..H(a)..(r("th")>0 and" +"..r("th")or"").."  AC "..r("ac")end local function bc()local s=r("sa")if s==0 then return"Choose a spellcasting ability for your save DC and attack bonus."end return"Spell save DC "..(8+G()+P(s+3)).."   Spell attack "..C(G()+P(s+3))end local function a_()return r("gp").." gp"end local z={{"Basics",{{"nm","t","Name",30},{"cl","t","Class",40},{"lv","n","Level",1,20,1},{"sc","t","Subclass",40},{"sp","t","Species",30},{"bg","t","Background",30},{"al","t","Alignment",20},{"xp","n","Experience points",0,999999,0},{"la","t","Languages",80},{"pr","t","Armor, weapon and tool training",80},},h=aT,s=aU},{"Abilities",ax,s=aR},{"Saving throws",aG},{"Skills",af,h=ba},{"Combat",{{"hp","p","Hit points",10},{"th","n","Temporary hit points",0,999,0},{"ac","n","Armor class",0,40,10},{"sd","n","Speed (ft)",0,300,30},{"hd","t","Hit dice",20},{"hl","n","Hit dice left",0,40,1},{"ds","n","Death save successes",0,3,0},{"df","n","Death save failures",0,3,0},{"in","k","Heroic inspiration",{"[  ]","[x]"}},{"cn","t","Conditions",60},{"lr","a","Long rest",a7,"Rested: HP, hit dice and slots are back."},},h=aV,s=aW},{"Attacks",{{"at","l","Attacks",20,"attack",70}}},{"Spells",J,h=bc},{"Features and traits",{{"ft","l","Features and traits",40,"feature",80}}},{"Inventory",{{"iv","l","Items",40,"item",60},{"gp","n","Gold (gp)",0,9999999,0},{"sv","n","Silver (sp)",0,9999999,0},{"cp","n","Copper (cp)",0,9999999,0},{"ep","n","Electrum (ep)",0,9999999,0},{"pp","n","Platinum (pp)",0,9999999,0},},s=a_},{"Notes",{{"no","l","Notes",40,"note",90}}},}local function aj()local k,h=A("nm"),A("cl")return(k~=""and k or"Unnamed")..(h~=""and" - "..h.." ".._()or"")end for L,d in ipairs(z)do for L,a in ipairs(d[2])do assert(not E[a[1]],"two fields are called "..a[1])E[a[1]]=a local c=a[2]a.d=(c=="t"and"")or(c=="n"and(a[6]or a[4]))or((c=="k"or c=="c")and(a[5]or 0))or nil end end local m={}local l,R,t,M,u="list",1,nil,nil,nil local g={list=1,sheet=1,items=1}local function a9()local p={}for c,f in pairs(i)do if type(f)=="table"then if#f>0 then p[#p+1]=c.."="for b,o in ipairs(f)do if b>1 then p[#p+1]="|"end p[#p+1]=o end p[#p+1]="\n"end elseif f~=""then p[#p+1]=c.."="..f.."\n"end end return table.concat(p)end local function aY(d)i={}for c,f in(d or""):gmatch("([%w_]+)=([^\n]*)")do local a=E[c]if a and a[2]=="l"then local e={}for a0 in f:gmatch("[^|]+")do e[#e+1]=a0 end i[c]=e elseif a and a[2]=="t"then i[c]=f else i[c]=tonumber(f)end end end local function ap()local p={"S1"}for L,q in ipairs(m)do p[#p+1]=q.slot.."|"..q.sum end snail.save(table.concat(p,"\n"))end local function S()if D==0 then return end snail.cache("c"..D,a9())local ag=aj():sub(1,60)for L,q in ipairs(m)do if q.slot==D and q.sum~=ag then q.sum=ag;ap()end end end local function aa()local k=A(au);return k~=""and k or"Unnamed"end local aL="!$%&()+,-0123456789:;<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnpqrstuvwxyz|~"local function bd(b)return aL:sub(b+1,b+1)end local aN={"1234567890<","QWERTYUIOP'","ASDFGHJKL-+","^ZXCVBNM,./","()[_____]?>"}local aO="'-+,./()?"local j local x,v,y=1,2,1 local function F(o,K)return aN[K]:sub(o,o)end local function ah()local k=0 for L,f in pairs(i)do if type(f)=="table"then for L,o in ipairs(f)do k=k+#o+1 end elseif type(f)=="string"then k=k+#f end end return k end local function ak(a,b,aS)local ab=a[2]=="n"or a[2]=="p"local d=(a[2]=="t"and A(a[1]))or(b and X(a)[b])or""local am=ab and 9 or(a[2]=="t"and(a[4]or 40))or a[6]or 80 local aF=ab and am or math.max(#d,aK-ah()+#d)j={f=a,i=b,s=d,at=#d,back=aS,num=ab,max=math.min(am,aF),full=aF<am}y=(d==""and not ab)and 1 or 0 if ab then x,v=1,1 elseif v==1 then x,v=1,2 end l="kb"end local function aA()local a=j.f local d=(j.s:gsub("^%s+",""):gsub("%s+$",""))local c=a[2]if c=="t"then i[a[1]]=d elseif c=="l"then local e=X(a)if j.i then if d==""then table.remove(e,j.i)else e[j.i]=d end elseif d~=""then e[#e+1]=d g.items=math.min(#e+1,a[4])end elseif d~=""then local s,ay=d:match("^(%d*)/(%d+)$")local aq,k=d:match("^([+-]?)(%d+)$")if c=="p"and ay then i[a[1].."_"]=tonumber(ay)if s~=""then i[a[1]]=tonumber(s)end i[a[1]]=Q(a)elseif k then k=tonumber(k)local T=c=="p"and Q(a)or r(a[1])if c=="n"and a[4]<0 then if aq=="-"then k=-k end elseif aq=="+"then k=T+k elseif aq=="-"then k=T-k end if c=="p"then i[a[1]]=math.max(0,math.min(H(a),k))else i[a[1]]=math.max(a[4],math.min(a[5],k))end else u="That isn't a number: "..d end end l,j=j.back,nil S()end local function a6()local h=F(x,v)local d,w=j.s,j.at if h=="<"then if w>0 then j.s,j.at=d:sub(1,w-1)..d:sub(w+1),w-1 end elseif h=="^"then y=(y+1)%3 elseif h=="["then j.at=math.max(0,w-1)elseif h=="]"then j.at=math.min(#d,w+1)elseif h==">"then aA()elseif#d>=j.max then u=j.full and"Sheet full: shorten a list or note"or"That's as long as it can be"else if h=="_"then h=" "elseif y==0 then h=h:lower()end j.s,j.at=d:sub(1,w)..h..d:sub(w+1),w+1 if y==1 and h:match("%a")then y=0 end end end local function a1(c)if c=="top"then aA()elseif c=="ok"then a6()elseif c=="left"or c=="right"then local n,h=c=="left"and-1 or 1,F(x,v)repeat x=(x-1+n)%11+1 until h~="_"or F(x,v)~="_"elseif c=="up"or c=="down"then v=(v-1+(c=="up"and-1 or 1))%5+1 end end local function a2()local I,a4={},F(x,v)=="_"for K=1,5 do local e={}for o=1,11 do local h=F(o,K)local ae=K==v and(o==x or(a4 and h=="_"))local N,aD,b=ae and 1 or 0,aO:find(h,1,true)if h:match("%u")then b=ae and h:byte()-65 elseif h:match("%d")then b=ae and 26+h:byte()-48 elseif aD then b=(ae and 35+aD)or(h=="/"and 45)elseif h=="<"then b=46+N elseif h=="^"then b=48+y*2+N elseif h=="["then b=54+N elseif h=="]"then b=56+N elseif h=="_"then b=58+N*3+(F(o-1,K)~="_"and 0 or F(o+1,K)~="_"and 2 or 1)else b=64+N end e[o]=b and bd(b)or("{"..h.."}")end I[K]=table.concat(e)end return table.concat(I,"/")end local function a8(a)local c,f=a[2],A(a[1])if M==a then return"Press OK again: "..a[3]end if a.s then return a.s(f)end if c=="t"then return a[3]..": "..(f~=""and f or"-")end if c=="n"then return a[3]..": "..f end if c=="p"then return a[3]..": "..(H(a)>0 and Q(a).." / "..H(a)or"none")end if c=="k"then return a[4][f%#a[4]+1].."  "..a[3]end if c=="c"then return a[3]..": "..a[4][f%#a[4]+1]end if c=="l"then return a[3].." ("..#X(a)..")"end return a[3]end local function az(a,n)local c=a[2]if c=="n"then i[a[1]]=math.max(a[4],math.min(a[5],r(a[1])+n))elseif c=="p"then i[a[1]]=math.max(0,math.min(H(a),Q(a)+n))elseif c=="k"or c=="c"then i[a[1]]=(r(a[1])+n)%#a[4]else return end S()end local function I(B,T,ao)local aI=math.max(1,math.min(T-ao//2,#B-ao+1))for b=aI,math.min(#B,aI+ao-1)do snail.row(B[b]:sub(1,90),b==T)end end local function ai(b,n,k)return(b-1+n)%k+1 end local function aH(b)local U=z[b][2]return#U==1 and U[1][2]=="l"end local function aC(b)R=b if aH(b)then t,g.items,l=z[b][2][1],1,"items"else g["s"..b]=g["s"..b]or 1 l="sect"end end local function a5(d)D=d aY((snail.cache("c"..d)))g.sheet,l=1,"sheet"end local function a3()local ah={}for L,q in ipairs(m)do ah[q.slot]=true end local d=1 while ah[d]do d=d+1 end D,i=d,{}m[#m+1]={slot=d,sum=aj()}g.list,g.sheet=#m,1 ap()S()aC(1)g.s1=1 ak(E[au],nil,"sect")end local function aX()snail.cache("c"..D,nil)for b,q in ipairs(m)do if q.slot==D then table.remove(m,b);break end end ap()u=aa().." was deleted."D,i,l=0,{},"list"g.list=math.max(1,math.min(g.list,#m))end function key(c)if l=="kb"then u=nil;a1(c);return end if l=="list"and c=="top"then return end u=nil local s=M M=nil local n=(c=="up"and-1)or(c=="down"and 1)local Z=(c=="left"and-1)or(c=="right"and 1)if l=="list"then local k=#m+(#m<at and 1 or 0)if n then g.list=ai(g.list,n,k)elseif c=="ok"then local q=m[g.list]if q then a5(q.slot)else a3()end end elseif l=="sheet"then if n then g.sheet=ai(g.sheet,n,#z+1)elseif c=="top"then S();l="list"elseif c=="ok"then if g.sheet<=#z then aC(g.sheet)elseif s=="del"then aX()else M="del"end end elseif l=="sect"then local U,ar=z[R][2],"s"..R local a=U[g[ar]]if n then g[ar]=ai(g[ar],n,#U)elseif c=="top"then l="sheet"elseif Z then az(a,Z)elseif c=="ok"then local O=a[2]if O=="t"or O=="n"or O=="p"then ak(a,nil,"sect")elseif O=="k"or O=="c"then az(a,1)elseif O=="l"then t,g.items,l=a,1,"items"elseif s==a then u=a[4]()or a[5]S()else M=a end end elseif l=="items"then local e=X(t)if n then g.items=ai(g.items,n,#e+(#e<t[4]and 1 or 0))elseif c=="top"then l=aH(R)and"sheet"or"sect"elseif c=="ok"then ak(t,e[g.items]and g.items,"items")elseif Z and e[g.items]and e[g.items+Z]then local Y=g.items+Z e[Y],e[g.items]=e[g.items],e[Y]g.items=Y S()end end end local aM={t="BACK back  OK edit  UP/DN move",n="OK type  L/R -1/+1  UP/DN move",p="OK type  L/R -1/+1  UP/DN move",k="OK or L/R change  UP/DN move",c="OK or L/R change  UP/DN move",l="BACK back  OK open  UP/DN move",a="BACK back  OK twice to do it",}local function aZ()local a=j.f snail.title(j.i==nil and a[2]=="l"and"New "..a[5]or a[2]=="l"and"Edit "..a[5]or a[3])local V if a[2]=="p"then V="Now "..Q(a).." / "..H(a)..". Type -8 for damage, +5 to heal, 12 to set, or 12/30 to set the most too."elseif j.num and a[4]<0 then V="Now "..C(r(a[1]))..". Type the new value, like 3 or -1."elseif j.num then V="Now "..r(a[1])..". Type 12 to set it, or +2 / -2 to change it."else V=#j.s.." of "..j.max.." letters"end snail.status(u or(y==2 and"CAPS"or y==1 and"Shift"or""))snail.text(j.s:sub(1,j.at).."|"..j.s:sub(j.at+1))snail.small(V)snail.board(a2())snail.hint("BACK done  OK press  arrows move")end function draw()if l=="kb"then return aZ()end if l=="list"then snail.title(aJ)if u then snail.status(u)end local e={}for b,q in ipairs(m)do e[b]=q.sum end if#m<at then e[#e+1]="+ New character"end I(e,g.list,13)if#m==0 then snail.small("Make a character to start. Sheets are kept on this X3.")end snail.hint("BACK leave  OK open  UP/DN move")return end if l=="sheet"then snail.title(aa())snail.status(u or aj())local e={}for b,d in ipairs(z)do e[b]=d[1]..(d.s and("  "..d.s())or"")end e[#e+1]=M=="del"and"Press OK again to delete "..aa()or"Delete this character"I(e,g.sheet,13)snail.hint("BACK characters  OK open  UP/DN move")return end if l=="sect"then local d=z[R]local b=g["s"..R]snail.title(d[1])snail.status(u or aa())if d.h then snail.small(d.h())end local e={}for Y,a in ipairs(d[2])do e[Y]=a8(a)end I(e,b,12)snail.hint(aM[d[2][b][2]])return end local e,B=X(t),{}snail.title(t[3])snail.status(u or(aa().." - "..#e.." of "..t[4]))if t.h then snail.small(t.h())end for b,f in ipairs(e)do B[b]=f end if#e<t[4]then B[#B+1]="+ Add "..t[5]end I(B,g.items,12)snail.hint(e[g.items]and"OK edit  L/R reorder  UP/DN move"or"BACK back  OK add  UP/DN move")end function start()local d=snail.load()if type(d)=="string"and d:sub(1,2)=="S1"then for bb,ag in d:gmatch("\n(%d)|([^\n]*)")do m[#m+1]={slot=tonumber(bb),sum=ag}end end end
