commit 588466c2739d21bfdb8a70f2e2c5727900de4bc6
parent f61e77ab6fdaca800766af169431e801d0de94a7
Author: Skylar Hill <stellarskylark@posteo.net>
Date: Thu, 2 Jun 2022 16:06:55 -0500
Various refactors and refinements
Diffstat:
4 files changed, 77 insertions(+), 157 deletions(-)
diff --git a/rpgsheet.nimble b/rpgsheet.nimble
@@ -1,6 +1,6 @@
# Package
-version = "0.0.1"
+version = "0.1.1"
author = "Skylar Hill"
description = "CLI/TUI application for TTRPG character sheets"
license = "GPL-3.0-only"
diff --git a/src/tui.nim b/src/tui.nim
@@ -1,10 +1,11 @@
import
+ algorithm,
illwill,
os,
- strutils,
+ std/wordwrap,
strformat,
+ strutils,
tables,
- std/wordwrap,
yaml
import utils, verb
@@ -29,18 +30,34 @@ proc exitProc() {.noconv.} =
proc runTui*(sheet: YamlDocument): void =
let tabs = sheet.root["tabs"]
- var currentTabIndex = 0
+ var tabIndex = 0
illwillInit(fullscreen=true)
setControlCHook(exitProc)
hideCursor()
- var selectedWindowIndex = 0
+ var selWinIndex = 0
var selectedByWindow = initTable[string, int]()
var itemsByWindow = initTable[string, seq[string]]()
var statusText = ""
var descScroll = 0
+ # Initialize tabs, windows, and expressions
+ for tab in tabs:
+ for name, windows in tab.fields.pairs:
+ for win in windows.elems:
+ itemsByWindow[win.content] = newSeqOfCap[string](100)
+ selectedByWindow[win.content] = 0
+
+ for name, content in sheet.root["expressions"].fields.pairs:
+ let win = content.getContent("window")
+ var working = itemsByWindow[win]
+ working.add(name.content)
+ itemsByWindow[win] = working
+
+ for win in itemsByWindow.keys:
+ itemsbyWindow[win] = itemsByWindow[win].sorted
+
while true:
var tb = newTerminalBuffer(terminalWidth(), terminalHeight())
var bb = newBoxBuffer(tb.width, tb.height)
@@ -48,64 +65,40 @@ proc runTui*(sheet: YamlDocument): void =
let descY = tb.height - 10
- var currentTabName: string
- var currentTabWindows: seq[YamlNode]
+ var tabName: string
+ var tabWindows: seq[YamlNode]
# There should only be one item, but because
# fields is a Table, it's necessary to use an iterator
# to get the name and content
- for n, w in tabs[currentTabIndex].fields.pairs:
- currentTabName = n.content
- currentTabWindows = w.elems
+ for n, w in tabs[tabIndex].fields.pairs:
+ tabName = n.content
+ tabWindows = w.elems
- let windowPositions = placeWindows(tb, currentTabWindows, descY)
-
- for win in currentTabWindows:
- if win.content notin itemsByWindow:
- itemsByWindow[win.content] = newSeqOfCap[string](100)
+ let windowPositions = placeWindows(tb, tabWindows, descY)
+ let win = tabWindows[selWinIndex].content
var key = getKey()
case key
of Key.Q, Key.Escape: exitProc()
of Key.ShiftJ:
- if currentTabIndex < tabs.elems.high:
- currentTabIndex += 1
- else:
- currentTabIndex = tabs.elems.low
- selectedWindowIndex = 0
+ tabIndex = tabIndex.loopInc(tabs.elems)
+ selWinIndex = 0
of Key.ShiftK:
- if currentTabIndex > tabs.elems.low:
- currentTabIndex -= 1
- else:
- currentTabIndex = tabs.elems.high
- selectedWindowIndex = 0
+ tabIndex = tabIndex.loopDec(tabs.elems)
+ selWinIndex = 0
of Key.L:
- if selectedWindowIndex < currentTabWindows.high:
- selectedWindowIndex += 1
- else:
- selectedWindowIndex = currentTabWindows.low
+ selWinIndex = selwinIndex.loopInc(tabWindows)
descScroll = 0
of Key.H:
- if selectedWindowIndex > currentTabWindows.low:
- selectedWindowIndex -= 1
- else:
- selectedWindowIndex = currentTabWindows.high
+ selWinIndex = selwinIndex.loopDec(tabWindows)
descScroll = 0
of Key.J:
- let win = currentTabWindows[selectedWindowIndex].content
- if selectedByWindow[win] < itemsByWindow[win].high:
- selectedByWindow[win] = selectedByWindow[win] + 1
- else:
- selectedByWindow[win] = itemsByWindow[win].low
+ selectedByWindow[win] = selectedByWindow[win].loopInc(itemsByWindow[win])
descScroll = 0
of Key.K:
- let win = currentTabWindows[selectedWindowIndex].content
- if selectedByWindow[win] > itemsByWindow[win].low:
- selectedByWindow[win] = selectedByWindow[win] - 1
- else:
- selectedByWindow[win] = itemsByWindow[win].high
+ selectedByWindow[win] = selectedByWindow[win].loopDec(itemsByWindow[win])
descScroll = 0
of Key.Enter:
- let win = currentTabWindows[selectedWindowIndex].content
let itemList = itemsByWindow[win]
let action = itemList[selectedByWindow[win]]
statusText = fmt"{fancyDisplay(action, 99)}: {doVerb(action, Roll, sheet)}"
@@ -129,13 +122,13 @@ proc runTui*(sheet: YamlDocument): void =
# Write current tab
let tabX = title.len + 5
- let tabWidth = currentTabName.len + 3
+ let tabWidth = tabName.len + 3
bb.drawRect(
tabX, 0,
tabX + tabWidth, 2,
doubleStyle = true
)
- tb.write(tabX + 2, 1, currentTabName)
+ tb.write(tabX + 2, 1, tabName.fancyDisplay(99))
# Write status line
let statusX = tabX + tabWidth + 1
@@ -146,9 +139,9 @@ proc runTui*(sheet: YamlDocument): void =
tb.write(statusX + 2, 1, statusText)
# Write windows
- for win in currentTabWindows:
- let (x1, y1, x2, y2) = windowPositions[win.content]
- if win == currentTabWindows[selectedWindowIndex]:
+ for window in tabWindows:
+ let (x1, y1, x2, y2) = windowPositions[window.content]
+ if window == tabWindows[selWinIndex]:
sbb.drawRect(x1, y1, x2, y2)
sbb.drawHorizLine(x1, x2, y1+2, doubleStyle=true)
else:
@@ -156,44 +149,29 @@ proc runTui*(sheet: YamlDocument): void =
bb.drawHorizLine(x1, x2, y1+2, doubleStyle=true)
let textWidth = x2 - x1 - 4
- tb.write(x1 + 2, y1+1, fancyDisplay(win.content, textWidth))
+ tb.write(x1 + 2, y1+1, fancyDisplay(window.content, textWidth))
# Write expressions
- for name, content in sheet.root["expressions"].fields.pairs:
- if content.getContent("tab") != currentTabName:
- continue
-
- let win = content.getContent("window")
- if win == "":
- continue
- if win notin windowPositions:
- continue
-
- if win notin selectedByWindow:
- selectedByWindow[win] = 0
-
- let (x1, y1, x2, _) = windowPositions[win]
- let textWidth = x2 - x1 - 4
- let sel = selectedByWindow[win]
- var currentItems = itemsByWindow[win]
- if name.content notin currentItems:
- currentItems.add(name.content)
- if sel <= currentItems.high:
- if name.content == currentItems[sel]:
+ for window in tabWindows:
+ for item in itemsByWindow[window.content]:
+ let itemWin = sheet.getExpression(item).getContent("window")
+ let (x1, y1, x2, _) = windowPositions[itemWin]
+ let textWidth = x2 - x1 - 4
+ let sel = selectedByWindow[itemWin]
+ if item == itemsByWindow[itemWin][sel]:
tb.setForegroundColor(fgBlue)
- tb.write(
- x1 + 2,
- y1 + 3 + currentItems.find(name.content),
- fancyDisplay(name.content, textWidth)
- )
- tb.setForegroundColor(fgNone)
- itemsbyWindow[win] = currentItems
+ tb.write(
+ x1 + 2,
+ y1 + 3 + itemsByWindow[itemWin].find(item),
+ fancyDisplay(item, textWidth)
+ )
+ tb.setForegroundColor(fgNone)
# Write description
bb.drawRect(1, descY, tb.width - 1, tb.height - 1, doubleStyle = true)
- let selWin = currentTabWindows[selectedWindowIndex].content
+ let selWin = tabWindows[selWinIndex].content
let sel = itemsByWindow[selWin][selectedByWindow[selWin]]
- let item = sheet.root["expressions"][sel]
+ let item = sheet.getExpression(sel)
let desc = item.getContent("desc")
let val = (item.getContent("dice") & " " & item.getContent("modifier")).strip
let roll =
diff --git a/src/utils.nim b/src/utils.nim
@@ -1,5 +1,22 @@
import tables, strutils, yaml
+let noCap = ["and", "of", "the", "as", "at", "but", "by", "for", "from", "if", "in", "on", "once", "onto", "or", "over", "so", "than", "that", "to", "up", "with", "when"]
+
+proc loopInc*(index: int, list: seq): int =
+ result =
+ if index < list.high:
+ index + 1
+ else: list.low
+
+proc loopDec*(index: int, list: seq): int =
+ result =
+ if index > list.low:
+ index - 1
+ else: list.high
+
+proc getExpression*(sheet: YamlDocument, exp: string): YamlNode =
+ sheet.root["expressions"][exp]
+
proc find*[T](list: seq[T], item: T): int =
for i in 0..list.high:
if list[i] == item: return i
@@ -13,7 +30,8 @@ proc fancyDisplay*(exp: string, maxLen: int): string =
exp[0..min(maxLen-1, exp.len-1)]
result = result.replace("-", " ")
for word in result.split(" "):
- result = result.replace(word, word.capitalizeAscii)
+ if word notin noCap:
+ result = result.replace(word, word.capitalizeAscii)
proc contains*(node: YamlNode, search: string): bool =
for key in keys(node.fields):
diff --git a/templates/dnd5e.yaml b/templates/dnd5e.yaml
@@ -20,344 +20,268 @@ expressions:
# Change "race" to your character's race
race:
desc: Description of your character's race here
- tab: main
window: features
# Add as many more of these as you want. Make the key
# like this; if you have Unarmored Defense, add an
# unarmored-defense field.
your-feature:
desc: Your character's awesome feature
- tab: main
window: features
# Set these equal to your stats
max-health:
modifier: 10
- tab: main
window: stats
current-health:
modifier: 10
- tab: main
window: stats
temporary-health:
modifier: 0
- tab: main
window: stats
speed:
modifier: 30
- tab: main
window: stats
armor-class:
modifier: 10
- tab: main
window: stats
proficiency:
modifier: 2
- tab: main
window: stats
strength:
modifier: 10
- tab: main
window: stats
dexterity:
modifier: 10
- tab: main
window: stats
constitution:
modifier: 10
- tab: main
window: stats
intelligence:
modifier: 10
- tab: main
window: stats
wisdom:
modifier: 10
- tab: main
window: stats
charisma:
modifier: 10
- tab: main
window: stats
# Add your tool and language proficiencies here
other-proficiencies:
- tab: main
window: stats
desc: Common, Simple weapons
# You probably don't want to change these
STR:
modifier: "(strength-10)/2"
- tab: main
window: ability-modifiers
DEX:
modifier: "(dexterity-10)/2"
- tab: main
window: ability-modifiers
CON:
modifier: "(constitution-10)/2"
- tab: main
window: ability-modifiers
INT:
modifier: "(intelligence-10)/2"
- tab: main
window: ability-modifiers
WIS:
modifier: "(wisdom-10)/2"
- tab: main
window: ability-modifiers
CHA:
modifier: "(charisma-10)/2"
- tab: main
window: ability-modifiers
# Add "+proficiency" to the skills you are proficient in,
# and "+(proficiency*2)" if you hvae expertise
passive-perception:
modifier: 10+WIS
- tab: main
window: skills
strength-saving-throw:
dice: d20
modifier: STR
- tab: main
window: skills
dexterity-saving-throw:
dice: d20
modifier: DEX
- tab: main
window: skills
constitution-saving-throw:
dice: d20
modifier: CON
- tab: main
window: skills
intelligence-saving-throw:
dice: d20
modifier: INT
- tab: main
window: skills
wisdom-saving-throw:
dice: d20
modifier: WIS
- tab: main
window: skills
charisma-saving-throw:
dice: d20
modifier: CHA
- tab: main
window: skills
acrobatics:
dice: d20
modifier: DEX
- tab: main
window: skills
animal-handling:
dice: d20
modifier: WIS
- tab: main
window: skills
arcana:
dice: d20
modifier: INT
- tab: main
window: skills
athletics:
dice: d20
modifier: STR
- tab: main
window: skills
deception:
dice: d20
modifier: CHA
- tab: main
window: skills
history:
dice: d20
modifier: INT
- tab: main
window: skills
insight:
dice: d20
modifier: WIS
- tab: main
window: skills
intimidation:
dice: d20
modifier: CHA
- tab: main
window: skills
investigation:
dice: d20
modifier: INT
- tab: main
window: skills
medicine:
dice: d20
modifier: WIS
- tab: main
window: skills
nature:
dice: d20
modifier: INT
- tab: main
window: skills
perception:
dice: d20
modifier: WIS
- tab: main
window: skills
performance:
dice: d20
modifier: CHA
- tab: main
window: skills
persuasion:
dice: d20
modifier: CHA
- tab: main
window: skills
religion:
dice: d20
modifier: INT
- tab: main
window: skills
sleight-of-hand:
dice: d20
modifier: DEX
- tab: main
window: skills
stealth:
dice: d20
modifier: DEX
- tab: main
window: skills
survival:
dice: d20
modifier: WIS
- tab: main
window: skills
# Attacks; add two items for every way you have to attack,
# one for the attack roll, and the second for damage
unarmed-strike:
dice: d20
modifier: STR+proficiency
- tab: inventory-and-attacks
window: attacks
desc: A simple attack with the body
unarmed-strike-damage:
modifier: 1+STR
- tab: inventory-and-attacks
window: attacks
# Inventory; add an item for whatever you happen to have
common-clothes:
desc: Simple, common clothes for a simple, common person. Nothing wrong with that.
- tab: inventory-and-attacks
window: equipment
copper-pieces:
modifier: 0
- tab: inventory-and-attacks
window: wallet
silver-pieces:
modifier: 0
- tab: inventory-and-attacks
window: wallet
gold-pieces:
modifier: 0
- tab: inventory-and-attacks
window: wallet
electrum-pieces:
modifier: 0
- tab: inventory-and-attacks
window: wallet
platinum-pieces:
modifier: 0
- tab: inventory-and-attacks
window: wallet
# You can remove these if you're not a caster
first-level-spell-slots:
modifier: 1
- tab: spells
window: spell-slots
second-level-spell-slots:
modifier: 0
- tab: spells
window: spell-slots
third-level-spell-slots:
modifier: 0
- tab: spells
window: spell-slots
fourth-level-spell-slots:
modifier: 0
- tab: spells
window: spell-slots
fifth-level-spell-slots:
modifier: 0
- tab: spells
window: spell-slots
sixth-level-spell-slots:
modifier: 0
- tab: spells
window: spell-slots
seventh-level-spell-slots:
modifier: 0
- tab: spells
window: spell-slots
eighth-level-spell-slots:
modifier: 0
- tab: spells
window: spell-slots
ninth-level-spell-slots:
modifier: 0
- tab: spells
window: spell-slots
first-level-slots-expended:
modifier: 1
- tab: spells
window: spell-slots
second-level-slots-expended:
modifier: 0
- tab: spells
window: spell-slots
third-level-slots-expended:
modifier: 0
- tab: spells
window: spell-slots
fourth-level-slots-expended:
modifier: 0
- tab: spells
window: spell-slots
fifth-level-slots-expended:
modifier: 0
- tab: spells
window: spell-slots
sixth-level-slots-expended:
modifier: 0
- tab: spells
window: spell-slots
seventh-level-slots-expended:
modifier: 0
- tab: spells
window: spell-slots
eighth-level-slots-expended:
modifier: 0
- tab: spells
window: spell-slots
ninth-level-slots-expended:
modifier: 0
- tab: spells
window: spell-slots
spellcasting-ability:
modifier: CHA # set this appropriately
- tab: spells
window: spellcasting-stats
spell-save-dc:
modifier: 8+spellcasting-ability+proficiency
- tab: spells
window: spellcasting-stats
spell-attack-bonus:
dice: 1d20
modifier: spellcasting-ability+proficiency
- tab: spells
window: spellcasting-stats
prestidigitation:
desc: "Cantrip. This spell is a minor magical trick that novice spellcasters use for practice. You create one of the following magical effects within range: 1) You create an instantaneous, harmless sensory effect, such as a shower of sparks, a puff of wind, faint musical notes, or an odd odor. 2) You instantaneously light or snuff out a candle, a torch, or a small campfire. 3) You instantaneously clean or soil an object no larger than 1 cubic foot. 4) You chill, warm, or flavor up to 1 cubic foot of nonliving material for 1 hour. 5) You make a color, a small mark, or a symbol appear on an object or a surface for 1 hour. 6) You create a nonmagical trinket or an illusory image that can fit in your hand and that lasts until the end of your next turn. If you cast this spell multiple times, you can have up to three of its non-instantaneous effects active at a time, and you can dismiss such an effect as an action."
- tab: spells
window: spells