From e5daa18bc2aef0a15c6e706d5705f98f9c1fb8ea Mon Sep 17 00:00:00 2001 From: sanine Date: Sat, 16 Sep 2023 14:42:22 -0500 Subject: add traits and actions --- demo.lua | 17 +++++++ draw.lua | 167 +++++++++++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 153 insertions(+), 31 deletions(-) diff --git a/demo.lua b/demo.lua index 2463290..a855cbc 100755 --- a/demo.lua +++ b/demo.lua @@ -18,6 +18,23 @@ local stats = { int = 12, wis = 13, cha = 10, + + saving_throws = 'Con +4', + immunities = 'fire, poison', + condition_immunities = 'poisoned', + senses = 'passive Perception 11', + languages = 'Ignan', + cr = '2 (450 XP)', + + traits = { + { name='Heated Body', value="A creature that touches the azer or hits it with a melee attack while within 5 feet of it takes 5 (1d10) fire damage." }, + { name="Heated Weapons", value="When the azer hits with a metal melee weapon, it deals and extra 3 (1d6) fire damage (included in the attack)." }, + { name="Illumination", value="The azer sheds bright light in a 10=foot radius and dim light for an additional 10 feet." }, + }, + + actions = { + { name='Warhammer', value="*Melee Weapon Attack:* +5 to hit, reach 5 ft., one target. *Hit:* 7 (1d8 + 3) bludgeoning damage or 8 (1d10 + 3) bludgeoning damage if used with two hands to make a melee attack, plus 3 (1d6) fire damage." }, + }, } diff --git a/draw.lua b/draw.lua index f80d565..b0aa3b1 100644 --- a/draw.lua +++ b/draw.lua @@ -47,33 +47,85 @@ end local divider = function() return pad(0, 10, _divider()) end ---===== basic stats =====-- +local function empty() + return function(y) return m.g{} end, 0 +end + + +local function wrap_text(str, max_len, tbl) + tbl = tbl or {} + local len = 0 + local count = 0 + local in_tag = false + for i = 1,#str do + count = count+1 + local c = string.sub(str, i, i) + if c == '>' then in_tag = false + elseif c == '<' then in_tag = true + end -local function name(stats) - local size = 40 - return function(y) - return m.text(stats.name, { - x=10, y=y+size, - style="font-size:"..size.."px;font-variant-caps:small-caps;stroke:black;fill:darkred;font-family:serif", - }) - end, size + if not in_tag then len = len+1 end + if len >= max_len then break end + end + local line = string.sub(str, 1,count) + if len < max_len then + table.insert(tbl, line) + return tbl + end + + -- remove word-end from line + local start = string.find(line, '[^ ]+$') + local word = string.sub(line, start) + local line = string.sub(line, 1, start-1) + local rest = word .. string.sub(str, count+1) + table.insert(tbl, line) + return wrap_text(rest, max_len, tbl) end -local function subheading(stats) - local size = 14 - return function(y) - return m.text( - string.format("%s %s, %s", capitalize(stats.size), stats.type, stats.alignment), - { - x=10, y=y+size, style="font-size:"..size.."px;font-style:italic;" - } - ) - end, size +local function wrapped_text(str, max_len, x, size, line_spacing, style) + local lines = wrap_text(str, max_len) + local transform = function(index, line) + return function(y) return m.text(line, { + x=x, y=y+size, + style="font-size:"..size.."px;"..(style or ''), + textLength=(index ~= #lines and 480) or nil, + }) end, size+line_spacing + end + local tbl = {} + for index, line in ipairs(lines) do + table.insert(tbl, {transform(index, line)}) + end + return stack(unpack(tbl)) +end + + +local function format(str) + local str = string.gsub(str, "%*%*(.-)%*%*", '%1') + str = string.gsub(str, "%*(.-)%*", '%1') + return str end + +local function text(str, size, style) + return function(y) return m.text( + str, + { style="font-size:"..size.."px;" .. (style or ''), x=10, y=y+size } + ) end, size +end + + +--===== basic stats =====-- + local function header(stats) local f, h = stack( - {pad(10, 10, name(stats))}, - {pad(0, 10, subheading(stats))} + --{pad(10, 10, name(stats))}, + {pad(10, 10, text( + stats.name, 40, + 'font-variant-caps:small-caps;stroke:black;fill:darkred;font-family:serif' + ))}, + {pad(0, 10, text( + string.format("%s %s, %s", capitalize(stats.size), stats.type, stats.alignment), + 14, 'font-style:italic' + ))} ) return f, h end @@ -85,7 +137,13 @@ local function _statline(name, value) { x=10, y=y+size, style="font-size:"..size.."px;fill:darkred;" } ) end, 14 end -local statline = function(name, value) return pad(0, 2, _statline(name, value)) end +--local statline = function(name, value) return pad(0, 2, _statline(name, value)) end +local function statline(name, value) + return pad(0, 2, wrapped_text( + string.format('%s %s', name, value), + 80, 10, 14, 2, 'fill:darkred' + )) +end local function armor_hp(stats) local f, h = pad(0, 10, stack( @@ -118,6 +176,35 @@ local function ability_scores(stats) } end, 40 end +local function optional_attribute(stats, name, key) + local value = stats[key] + if value then + local f, h = statline(name, value) + return f, h + else + local f, h = empty() + return f, h + end +end +local function misc_attributes(stats) + local f, h = stack( + {optional_attribute(stats, 'Saving Throws', 'saving_throws')}, + {optional_attribute(stats, 'Skills', 'skills')}, + {optional_attribute(stats, 'Damage Vulnerabilities', 'vulnerabilities')}, + {optional_attribute(stats, 'Damage Resistances', 'resistances')}, + {optional_attribute(stats, 'Damage Immunities', 'immunities')}, + {optional_attribute(stats, 'Condition Immunities', 'condition_immunities')}, + {optional_attribute(stats, 'Senses', 'senses')}, + {optional_attribute(stats, 'Languages', 'languages')}, + {optional_attribute(stats, 'Challenge', 'cr')} + ) + if h > 0 then return stack({pad(0, 10, f, h)}, {divider()}) + else return empty(), 0 end +end + + + + local function base(stats) local f, h = stack( {header(stats)}, @@ -125,33 +212,51 @@ local function base(stats) {armor_hp(stats)}, {divider()}, {ability_scores(stats)}, - {divider()} + {divider()}, + {misc_attributes(stats)} ) return f, h end ---===== attributes =====-- +--===== traits =====-- -local function attribs(stats) - return function(y) return m.rect{ - x=0, y=y, width=500, height=20, - style="fill:blue", - } end, 20 +local function trait(t) + return pad(0, 10, wrapped_text( + string.format('%s. %s', t.name, format(t.value)), + 80, 10, 14, 5 + )) +end +local function traits(stats) + if not stats.traits then return empty() end + + local tbl = {} + for _, t in ipairs(stats.traits) do + table.insert(tbl, {trait(t)}) + end + return stack(unpack(tbl)) end --===== actions =====-- local function actions(stats) - return function(y) return m.g{} end, 0 + if not stats.actions then return empty() end + + local tbl = {} + for _, action in ipairs(stats.actions) do + table.insert(tbl, {trait(action)}) + end + return stack( + unpack(tbl) + ) end --===== draw =====-- function draw(stats) - local f, h = stack({base(stats)}, {attribs(stats)}, {actions(stats)}) + local f, h = stack({base(stats)}, {traits(stats)}, {actions(stats)}) return m.render(m.svg{ viewBox = string.format("0 0 500 %d", h), width = 500, -- cgit v1.2.1