Struggling with a transclusion from a state tiddler with a variable field name

The default value of a procedure parameter is expected to be a constant string (see Procedure Definition Syntax)

To assign a variable (i.e., <<currentTiddler>>) as a default value, you can use a $let widget, like this:

\procedure select-mods(advantage, book)
<$let book={{{ [<book>!match[]] ~[<currentTiddler>] }}}>

When a non-blank book value is passed into the procedure it is used as-is; however, if the passed in value is blank, then the <currentTiddler> value is assigned to book.

enjoy,
-e

1 Like

Good job!

A couple of suggestions to make your code more concise:

  1. Rather than adding a new <% if [<book>!has<advantage>] %> case to your conditional, replace $value={{{ [<current>] [<new>] +[join[, ]] }}} under the <% else %> case with $value={{{ [<current>!match[]] [<new>] +[join[, ]] }}}. join only applies when it receives at least 2 input values, so it won’t add the comma if [<new>] is the only input.

  2. Rather than repeating <$action-setfield $tiddler="$:/temp/generated-list-mod-state" $field=text $value="" /> for each case, move it outside the conditional. You want it to apply no matter what, after all!
    As a slightly shorter option, you could also use <$action-deletefield $tiddler="$:/temp/generated-list-mod-state" text /> or even <$action-deletetiddler $tiddler="$:/temp/generated-list-mod-state" />, since this is a temporary tiddler whose only function is to hold your selection.

1 Like

Thanks!

I’m moving on to editing the levels of modifiers… and it’s another tall order for me :slight_smile:

My plan:

  1. Retrieve the contents of the <<advantage>> field of the <<book>> tiddler as parsed by the list step we are currently using the procedure in and that works the same as the <list-modifiers> procedure we worked on earlier, and copy its contents in a temporary tiddler for edition, where every string of the form modifier level in between commas is copied to the temporary tiddler as fields named after modifier and containing the level.
  2. Use a combination of “edit” and “set” buttons to toggle the edition process (using <$reveal>) which will happen through the use of <$edittext> widgets editing the values of the relevant modifier field in the temp tiddler, then when the button is clicked, close the $reveal and replace the values in the <<advantage>> field with the values from the temp tiddler.

Does this make sense?

I’m currently writing this procedure as part of this plan (untested and unfinished), trying to replicate your approach with backtick substitutions:

\procedure edit-mod-level()
<$action-setfield $tiddler="$:/temp/generated-mod-level-values" $field=<<modname>> $value=<<modlevel>>/>
<$let current={{{ [<book>get<advantage>] }}} selected={{$:/temp/generated-mod-level-values!!<modname>}}>
<$reveal type="nomatch" state="$:/temp/modlevels" text="show" animate=yes><$button set="$:/temp/modlevels" setTo="show">Edit mod level</$button></$reveal><$reveal type="match" state="$:/temp/modlevels" text="show" animate=yes><$edittext tiddler="$:/temp/generated-mod-level-values" field=<<modname>> type="number"/><$let newvalue={{$:/temp/generated-mod-level-values!!<modname>}} new=`$(selected)$ <newvalue>` existing=`$(selected)$ \d+(,\s?)?`>
<$button set="$:/temp/modlevels" setTo="hide" actions="<action-setfield $tiddler=<book> $field=<<advantage>> $value={{{ [<current>] [<new>] +[join[, ]] }}}">set</$button></$let></$reveal>
</$let>
\end

In it, the thinking is:

  • The first <$action-setfield> creates the temporary tiddler, naming the field with the modifier being edited, and giving it the current value of its level,
  • Bunch of variables declarations with the first <$let>,
  • The first reveal draws the button to edit if we’re not already editing,
  • The second reveal is where the meat is: the edittext widget grabbing from the temporary tiddler, as well as the second <$let> to declare the rest of the needed variables including the backtick stuff, as well as
  • The second button to both close the editing process and save the new values

I’m afraid that I might be trying to do too much here, and quite possibly confused by the fact that <new> has to refer to the new level value instead of “1”.

Yes, I think your approach makes sense!

It’s often easier for me to write code than to think things through in the abstract, so here’s how I might do it (written without looking at your code):

Code under the fold for brevity; feel free to use or not use, as you like
\procedure update-mod-level()
\function updated() [<segment>split[ ]butlast[]] [<newLevel>] +[join[ ]]
<$action-setfield $tiddler=<<book>>
	$field=<<advantage>>
	$value={{{ [<current>search-replace<segment>,<updated>] }}} />
<$action-deletetiddler $tiddler=<<tempLevel>> />
\end

\procedure select-modsB(advantage, book)
<$let
	book={{{ [<book>!match[]] ~[<currentTiddler>] }}}
	current={{{ [<book>get<advantage>] }}}
>

Add or remove
<$select tiddler="$:/temp/generated-list-mod-state"
	default=""
	actions=<<mod-select-actions>>
>
	<option value="" disabled>Select modifier</option>
	<optgroup label="Special">
		<$list filter="[tag[GURPS Modifier]search:to-advantages:<advantage>]">
			<<mod-select-option>>
		</$list>
	</optgroup>
	<optgroup label="Global modifiers">
		<$list filter="[tag[GURPS Modifier]!has[to-advantages]] -[[Modifier template]]">
			<<mod-select-option>>
		</$list>
	</optgroup>
</$select>

<% if [<current>!match[]] %>
Modify levels:
<table>
<$list filter="[<current>split[, ]]" variable="segment">
	<tr>
	<$let
		mod={{{ [<segment>split[ ]butlast[]join[ ]] }}}
		currentLevel={{{ [<segment>split[ ]last[]else[1]] }}}
		tempLevel=`$:/temp/$(book)$/$(mod)$/level`
	>
		<td><<mod>></td>
		<td>
			<$select tiddler=<<tempLevel>> default=<<currentLevel>>>
				<$list filter="[range[20]]">
				<!--                   ^ use the maximum level possible -->
					<option>{{!!title}}</option>
				</$list>
			</$select>
		</td>
		<td>
			<$list filter="[<tempLevel>get[text]]" variable="newLevel">
				<$button actions=<<update-mod-level>>>Update</$button>
			</$list>
		</td>
	</$let>
	</tr>
</$list>
</table>
<% endif %>
</$let>
\end

Incidentally, I noticed a few stray </$let> tags in your mod-select-actions macro. These shouldn’t actually break anything (as they’re scoped within conditional cases) and won’t show up since they’re part of an action macro (and thus don’t get rendered as HTML). But they’re technically errors, and potentially confusing, so best to avoid.

Here’s a corrected version:

\procedure mod-select-actions()
<$let
	currentTiddler=<<book>>
	current={{{ [<book>get<advantage>] }}}
	selected={{$:/temp/generated-list-mod-state}}
	new=`$(selected)$ 1`
	existing=`$(selected)$ \d+(,\s?)?`
>
<% if [<current>search<selected>] %>
<$action-setfield
	$field=<<advantage>>
	$value={{{ [<current>search-replace::regexp<existing>,[]] }}} />
<% else %>
<$action-setfield
	$field=<<advantage>>
	$value={{{ [<current>!match[]] [<new>] +[join[, ]] }}} />
<% endif %>
<$action-deletetiddler $tiddler="$:/temp/generated-list-mod-state" />
\end

If you do end up using any of my code above, note that I included current={{{ [<book>get<advantage>] }}} in the first $let widget used in select-modsB, along with Eric’s <<book>> definition, as it’s convenient to have access to the <<current>> variable for the level-modifying code as well. This means you can remove that line from the mod-select-actions code above, as it’s already accounted for in the parent procedure.

1 Like

I ended up using most of your code in the procedure that I named edit-modifiers instead of select-modsB; just refactored it to be displayed on a line rather than vertically and a few minor tweaks. I also included a conditional to check if a modifier can be leveled or not (not all of them can be, and if they can’t be, they should remain at level 1 and not allow users to access their level), as well as a call to the lvl-mods-sum procedure for visibility, that calculates the total effect of the mods:

\procedure update-mod-level()
\function updated() [<segment>split[ ]butlast[]] [<newLevel>] +[join[ ]]
<$action-setfield $tiddler=<<book>>
	$field=<<advantage>>
	$value={{{ [<current>search-replace<segment>,<updated>] }}} />
<$action-deletetiddler $tiddler=<<tempLevel>> />
\end

\procedure edit-modifiers(advantage, book)
<$let
	book={{{ [<book>!match[]] ~[<currentTiddler>] }}}
	current={{{ [<book>get<advantage>] }}}
>
<% if [<current>!match[]] %>
<table>
	<tr><td><$macrocall $name="lvl-mods-sum" advbook=<<book>> adv=<<advantage>>/></td><$list filter="[<current>split[, ]!is[blank]]" variable="segment">
	<$let
		mod={{{ [<segment>split[ ]butlast[]join[ ]] }}}
		currentLevel={{{ [<segment>split[ ]last[]else[1]] }}}
		tempLevel=`$:/temp/$(book)$/$(mod)$/level`
	>
		<td><$link to=<<mod>>/></td>
		<td>
			<% if [<mod>get[levelable]match[yes]] %><$select tiddler=<<tempLevel>> default=<<currentLevel>>>
				<$list filter="[range[20]]">
				<!--                   ^ use the maximum level possible -->
					<option>{{!!title}}</option>
				</$list>
			</$select>
			<$list filter="[<tempLevel>get[text]]" variable="newLevel">
				<$button actions=<<update-mod-level>>>{{$:/core/images/save-button}}</$button>
			</$list><% else %>Not levelable<% endif %>
		</td>
	</$let>
</$list>
</tr></table>
<% endif %>
<$macrocall $name=select-mods advantage=<<advantage>> book=<<book>>/>
</$let>
\end

However, I have a bug to squash, as when one adds a modifier through the select, then removes it, the <advantage> field in the <book> is left with an extra empty mod that throws things off:

image

As you’ll see in the code above, I have a “cosmetic” workaround in place for now through the addition of !is[blank] after the split expression of the <$list> widget in the edit-modifiers procedure, but the root cause is most likely in the mod-select-actions procedure below (copied the whole thing as it stands currently for easier reference):

\procedure mod-select-actions()
<$let
	currentTiddler=<<book>>
	current={{{ [<book>get<advantage>] }}}
	selected={{$:/temp/generated-list-mod-state}}
	new=`$(selected)$ 1`
	existing=`$(selected)$ \d+(,\s?)?`
>
<% if [<current>search<selected>] %>
<$action-setfield
	$field=<<advantage>>
	$value={{{ [<current>search-replace::regexp<existing>,[]] }}} />
<% else %>
<$action-setfield
	$field=<<advantage>>
	$value={{{ [<current>!match[]] [<new>] +[join[, ]] }}} />
<% endif %>
<$action-deletetiddler $tiddler="$:/temp/generated-list-mod-state" />
\end

\procedure mod-select-option()
<option value={{!!title}}>
{{!!title}} ({{{ [{!!title}get[modifier-value]] }}}%)
<$list filter=`[<book>search:$(advantage)${!!title}]`>&#x2714;</$list>
</option>
\end

\procedure select-mods(advantage, book)
<$let book={{{ [<book>!match[]] ~[<currentTiddler>] }}}>
<$select tiddler="$:/temp/generated-list-mod-state"
	default=""
	actions=<<mod-select-actions>>
>
	<option value="" disabled>Select modifier</option>
	<optgroup label="Special">
		<$list filter="[tag[GURPS Modifier]search:to-advantages:<advantage>]">
			<<mod-select-option>>
		</$list>
	</optgroup>
	<optgroup label="Global modifiers">
		<$list filter="[tag[GURPS Modifier]!has[to-advantages]] -[[Modifier template]]">
			<<mod-select-option>>
		</$list>
	</optgroup>
</$select></$let>
\end

My guess is there may be something off with the backtick substitution in $value={{{ [<current>search-replace::regexp<existing>,[]] }}} />, or the behaviour of the $value={{{ [<current>!match[]] [<new>] +[join[, ]] }}} /> line?

I’m not always able to reproduce it, unfortunately, that’s why I’m not entirely sure how/when it happens. It may be around acting on the first or last mod of the list. I’m trying to figure it out right now.

It occurs when you add a first mod, then a second, then remove the second and re-add it. image - in fact, when you add a first mod, then a second, then remove the second, the ", " remains after the first:

  1. Adding a first mod: image
  2. Adding a second mod: image
  3. remove the second mod: image ← here’s the culprit, I think
  4. Re-adding the second mod: image

It seems to be that adding then removing the last mod of the list, the list then ends with ", " rather than nothing, and ", " remains there afterwards because there’s no blank mod and one can’t access it through the Select.

Adding and removing the last mod of the list several times: image
:joy:

You’re probably right about the regexp being the culprit, but I’m not sure why, exactly. Nevermind, this was my oversight. As you observed, the issue is that the regexp is only looking for commas after the segment… and we don’t want to add an optional preceding comma+space to the regexp or it will eliminate ones you want to keep.

In an earlier iteration, I’d used search-replace:g[ ,],[] after the first search-replace to handle this specific problem. I’d assumed the regexp would render it unnecessary, but as a quick fix I’d try adding it back in.

I just tried it like this (that’s what you meant, right?):

$value={{{ [<current>search-replace::regexp<existing>,[]search-replace:g[ ,],[]] }}} />

… But it doesn’t work either; because in the use case I detailed, there seems to be only an extra comma left behind on the penultimate element of the list when you remove the last one (thus making the penultimate one the last one after the operation has gone through). Well - it does clean up the field when there’s several empties one after the other, but the error gets reintroduced because of the remaining comma after the last mod in the list.

Removing only the comma using this addition would remove it everywhere, however, maybe we can search for , , and replace it with , to at least remove any blank? Or would that just introduce double spaces…

It would, but you could try , , to , (note the trailing spaces) instead.

Edit: Actually - try adding trim[, ] as a final step in that same line, and that should clean up unnecessary commas at the end of the field.

Doesn’t seem to fare any better than your original global search-replace. I’m losing a patch of hair by scratching my head on this XD

edit for your edit: it seems you nailed it again with the trim… still testing, but so far, so good!

Oh, good. I was going to start tearing my hair, too.

For future reference, it’s helpful if you can provide a demo with at least the minimum set of tiddlers needed to test your desired features. I did test my level-modifying code, but couldn’t test the $select widget as I didn’t have any [tag[GURPS Modifier]] tiddlers to populate the options. Otherwise (I’d like to think :wink:) I would have caught some of those issues sooner!

I’m sure you’d have :wink:

Not against the idea of course, but I wonder how to do it easily, since I’m working entirely offline on a local single-file copy?

edit: I actually just uploaded the whole file on TiddlyHost :stuck_out_tongue_winking_eye:

1 Like

So I’m playing with the backtick substitution to avoid having to write a conditional (first line inside the <$let> widget below, which I pasted with extra debug stuff):

\procedure calc-attribute(attribute, character)
<$let character={{{ [<character>!match[]] ~[<currentTiddler>] }}}>
//calc-attribute-from-cp macro call with substitution:// <$macrocall $name="calc-attribute-from-cp" name=<<attribute>> value=`{{!!$(attribute)$-cp}}`/>
<br>//calc-attribute-from-cp macro call with direct field ref:// <$macrocall $name="calc-attribute-from-cp" name=<<attribute>> value={{!!ST-cp}}/>
<br> //attribute param passed:// <<attribute>> //value param substitution passed:// <$text text=`{{!!$(attribute)$-cp}}`/>
</$let>
\end

The correct value the above procedure (it’s in the tiddler hosted here) should return where I’m testing it is 12. It returns 10, which is expected from the calculation last step (we are dealing with ST in the testing example, and a value coming from the ST-cp field which is 20) when the <value> is 0:

\procedure calc-attribute-from-cp(name, value)
<% if [<name>match[ST]] %>
  <$text text={{{ [<value>divide[10]trunc[]add[10]] }}} />
<% elseif [<name>match[DX]] %>
  <$text text={{{ [<value>divide[20]trunc[]add[10]] }}} />
<% elseif [<name>match[IQ]] %>
  <$text text={{{ [<value>divide[20]trunc[]add[10]] }}} />
<% elseif [<name>match[HT]] %>
  <$text text={{{ [<value>divide[10]trunc[]add[10]] }}} />
<% else %>
  error: <<name>> isn't an attribute
<% endif %>
\end

With the debug lines, here is what I see on the test Character:
image

I’m confused how I can end up with a 10 when everything seems to be passed correctly (i.e. the correct name, “ST”, and the correct value, “{{!!ST-cp}}”). Do you have any idea of what I’m not seeing?

Your substitution is working correctly:

value=`{{!!$(attribute)$-cp}}`

produces value="{{!!ST-cp}}". However, macros/procedures don’t fully evaluate their parameters before substituting them; instead, they’re substituted “as-is” and evaluated, inline, at the same time as the rest of the procedure. So while <<value>> = {{!!ST-cp}} and {{!!ST-cp}} = 20, that transclusion isn’t being performed in time, and {{!!ST-cp}} gets treated like a literal value:

<$text text={{{ [<value>divide[10]trunc[]add[10]] }}} />

will produce this:

<$text text={{{ [[{{!!ST-cp}}]divide[10]trunc[]add[10]] }}} />

not

<$text text={{{ [[20]divide[10]trunc[]add[10]] }}} />

Since “{{!!ST-cp}}” isn’t a number, it gets treated as 0, and [[0]divide[10]trunc[]add[10]] = 10.

To ensure everything gets evaluated before it gets substituted, I recommend using functions instead. Here’s an example of how I might do it. As you can see, this also lets us avoid $wikify and eliminate a bunch of <% if %> syntax by moving all the then/else logic into the function itself.

\function cp-field() [<name>] cp +[join[-]]
\function cp-value() [<currentTiddler>get<cp-field>]

\function calc.attribute.from.cp(name, divide:"1")
ST DX IQ HT :intersection[<name>] :then[<cp-value>divide<divide>trunc[]add[10]]
:else[[error: $(name)$ isn't an attribute]substitute[]]
\end

\function ST() [calc.attribute.from.cp[ST],[10]]
\function DX() [calc.attribute.from.cp[DX],[20]]
\function IQ() [calc.attribute.from.cp[IQ],[20]]
\function HT() [calc.attribute.from.cp[HT],[10]]

\function calc.secondary.from.cp(name, divide:"1", primary)
HP Will Per FP :intersection[<name>] :then[<cp-value>divide<divide>trunc[]add<primary>]
:else[[error: $(name)$ isn't a secondary]substitute[]]
\end

\function HP() [calc.secondary.from.cp[HP],[2],<ST>]
\function Will() [calc.secondary.from.cp[Will],[5],<IQ>]
\function Per() [calc.secondary.from.cp[Per],[5],<IQ>]
\function FP() [calc.secondary.from.cp[FP],[3],<HT>]

Notes:

  • Like macros and procedures, functions can take named parameters. Unlike macros/procedures, these parameters are substituted (along with any other variables/transclusions) before the filter is evaluated. You can think of them as a more flexible alternative to $let variable={{{ ... }}}.
  • You’ll notice I used . in several of the function names. Any function whose name contains at least one period can be used as a custom filter operator as well as a <<variable>>.
    • cp-field can only be called with <<cp-field>> or <cp-field> in a filter
    • calc.attribute.from.cp can be called with <<calc.attribute.from.cp ST 10>> or <calc.attribute.from.cp ST 10>, or as an operator with comma-separated parameters: calc.attribute.from.cp[ST],[10].
  • When a function is used as a filter operator, its parameters can be literal values, transclusions, or variables — including variables defined with other functions! This lets us do some function nesting, as you can see above: calc.attribute.from.cp[ST],[10] uses <name> = ST
    • in <cp-field> to calculate the appropriate field name, ST-cp
    • then <cp-value> retrieves the field value ST-cp: 20
    • which gets plugged into [<cp-value>divide<divide>trunc[]add[10]] along with <<divide>> = 10, to get [[20]divide[10]trunc[]add[10]] = 12.

Let me know if you’d like further explanation for anything I did there. I’m not familiar with GURPS rules, so while the numbers this produced looked reasonable enough to me, you may also want to make sure I didn’t inadvertently introduce any copy-paste errors.

1 Like

This is so elegantly designed!

So it would not be valid for me to call it like this for example?

<$macrocall $name="calc.attribute.from.cp" name="ST" divide="10" />

That’s also perfectly valid, and you could of course replace those named attributes with variables or transclusions, e.g.

<$macrocall $name="calc.attribute.from.cp" name=<<attribute>> divide={{!!number}} />

You can also use the new $transclude syntax to call a function:

<$transclude $variable="calc.attribute.from.cp" name=<<attribute>> divide={{!!number}} />

Note that the “legacy” usage of $transclude doesn’t support transcluding functions, and in order to use the “modern” form, you’ll need the $ forms of any predefined attributes. In modern mode, all attributes that don’t begin with $ are assumed to be named parameters of the macro/procedure/function being transcluded — so you can’t mix $variable="name" mode="block", for instance.

Assuming that each cp attribute has its own hard-coded divisor, though, I think it’s probably more efficient to use the short form <<calc.attribute.from.cp ST 10>>… unless you’re storing the divisors for each cp type elsewhere.

1 Like