Skip to main content

On Sale: GamesAssetsToolsTabletopComics
Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

Lil Programming Questions Sticky

A topic by Internet Janitor created Oct 28, 2022 Views: 8,191 Replies: 231
Viewing posts 1 to 72
Developer(+1)

This thread is for small questions about Lil, the Decker scripting language. If you have a more involved question, feel free to start your own thread. If we accumulate enough information for an FAQ I'll edit this post to include a link.

The Lil manual can be found here.

(+1)

Lil performance question: I have used LiveCode for years, and its built-in language is not fast. So when I try out a new language, the first thing I do after Hello World is a for loop to get a sense of the speed of the language. I tried that with Lil on an M1 MacBook, and found that 

on click do
each x in range 1000000
end
 alert["Done!"]
end

takes about 2 seconds to run. This is about 30x slower than even LiveCode. This is not a criticism, just a question: any thoughts on ways to speed up Lil?

Developer (4 edits) (+2)

There's a lot of room for improvement in Lil performance. The each loop in your example is particularly expensive because "each" is a map operation yielding a result list, range eagerly creates an entire list, and each loop body is executed in its own scope. Contrast with "while", which is considerably faster due to involving less bookkeeping and keeping much less in memory at once:

while x<1000000
  x:1+x
end

Or, when it's possible, just using the natural "conforming" over lists:

1+range 1000000

Performance for each over a huge list is particularly bad in c-lil; I'll do some investigating.

Interesting -- I'm not terribly familiar with list generation: I've played with Python, so I know it's a thing, but LiveCode has nothing like it built in, and I would never do something like

repeat with i = 1 to 1000000
    put i,"" after aList
end repeat
repeat for each item i in aList
    -- do something
end repeat

I just checked and found that the while loop was even slower :-)

I'm not sure this accomplishes the same task, albeit that the task is synthetic in the first place, but in any case, this is about 20x faster:

on click do
range 10000000
 alert["Done!"]
end
Developer (1 edit) (+1)

Oh, there's one other thing I should note that's probably making us talk past one another somewhat: Lil in Decker is deliberately capped at a specific number of "ops" (VM bytecode steps) per "frame" (display updates at 60fps) . This is an arbitrary creative constraint intended to help decks behave more consistently across different machines with wildly different levels of performance. The "FRAME_QUOTA" constant in the JavaScript implementation controls this cutoff.

Oh, HA! Okay, that makes sense. "constant in the JavaScript implementation" -- meaning updating it requires rebuilding Decker from source? Or is it configurable?

Developer

You can rebuild from source, or, in a web build, just open the file in your favorite text editor, search for "FRAME_QUOTA", and tweak as desired. I may raise the limit and/or make it configurable at runtime in the future.

(1 edit)

LiveCode script (and the GPLv3 community fork OpenXTalk) has arrays which can be considerably faster then using comma-seperated-values for  text container 'lists' (but when I do I use tab as a delimiter).
Also LC / OXT has a second language, the Extension Builder lang for making 'Widgets' and wrapping external code libraries, which does have an actual List type where the ordered elements can be of any type, Text, Numbers, JSON, Java, or C and ObjC types, Pointers, etc.

What's a good way to get an image string? I was trying `read["image"]` in c-lil and I was only getting the contents of the file as a byte string.

so I tried to get it from js-lil, and I couldn’t manage to copy the string out of the listener. After copying it and pasting it in the script editor, the string appears as an image within the script..

for reference, the image string is

%%IMG2ACIAGCAuAQwgEgEEIAYBAiAEAQIgEAEEIAYBAiAEAQIgDgECIAgBAiAIAQIgDAECIAgBAiAIAQIgCgECIAoBAiAGAQIgAgECIAgBAiAKAQIgBgECIAIBAiAGAQIgDAECIAYBAiACAQIgBgECIAwBAiAGAQIgAgECIAYBCiAGAQIgCAECIAYBCiAGAQIgCAECIAQBAiAKAQIgBgEMIAIBAiAKAQIgBgEMIAIBAiAKAQIgBAECIAwBBCAKAQIgBAECIAwBAiACAQogBAECIAIBDCAEAQogBAECIAIBDCAGAQIgDAECIAoBAiAGAQIgDAECIAoBAiAIAQQgCgEKIAoBBCAKAQogDgEKIBgBCiAO

and this is a screenshot of what happens:

Developer(+1)

Pasting images in non-rich fields was a bug; this should be fixed now. Image strings are how images are represented in the clipboard, so you should now be able to make image-strings using normal drawing tools.

Lilt's read[] has a slightly different signature than Decker, since Decker prompts the user instead of taking a path argument; that might be what was tripping you up there.

(+2)

What is a good way to order a query by two values? I’m making a flash-card deck and want to sort the cards based on some “score” I calculate, and if they’re equal fall back on the time they were last accessed. Tupling the values is not an option, since anything but an integer will be converted to string for comparison. I could just do that with nested queries, but that doesn’t feel very good.

Developer(+1)

Well, you could've formatted columns together to form keys with a natural lexicographic comparison, but that's horrible.

I made a tweak to the behavior of "orderby" which borrows from how the "grade" operators work in K: lists are now given a lexicographic comparison, so tupling will now work. The "join" operator will zip together columns for this purpose. As a contrived example:

    e:insert c:""split"ABBBABABABABAAABA" n:(7,7,2,4,4,0,3,4,2,0,9,7,4,6,8,2,3) into 0 
    select c n orderby (c join n) asc from e
+-----+---+
| c   | n |
+-----+---+
| "A" | 2 |
| "A" | 3 |
| "A" | 3 |
| "A" | 4 |
| "A" | 4 |
| "A" | 6 |
| "A" | 7 |
| "A" | 8 |
| "A" | 9 |
| "B" | 0 |
| "B" | 0 |
| "B" | 2 |
| "B" | 2 |
| "B" | 4 |
| "B" | 4 |
| "B" | 7 |
| "B" | 7 |
+-----+---+
(+4)

Would be cool if Decker had a discord server, or a channel on the Fantasy Consoles server

(+2)

You could ask in the Fantasy Consoles server to add a channel?

(+1)

Is there a good way of returning multiple values from a function? For now the best I have is to pack them in a list and manually unpack to variables. Something like Lua’s unpacking would be really handy, but it conflicts with the current syntax: a, b: 1, 2 is a list of a, 1, 2 and assignment of 1 to b. In a world of pure functions it’s really hard to avoid multiple output.

(4 edits) (+2)

Not really an answer to my question, but a solution to my problem: I found a way to store mutable state. Using closures it’s possible to write “constructors” that return “objects” which have persistent mutable state, and calling “methods” of the “object” will change its state for all references to the object. Here’s an example of a stack constructor I’m using in my current project:

on new_stack do
 state: ()
 ("stack","push","peek","pop") dict
 (on _ do state end
 ,on _ x do state[count state]: x end
 ,on _ do (-1 take state)[0] end
 ,on _ do ans:-1 take state state:-1 drop state ans[0] end
 )
end

This method is used in the example module in the documentation, although outside the function so it only creates a single global mutable state.

Edit: A universal minimal mutable variable:

on new_var state do
 ("get","set") dict
 (on _ do state end
 ,on _ new do state:new end
 )
end
(+1)

my question is kinda rudimentary in nature, so how do I make a sound play continuously in a card? I want it to be played after the button click sound ends(we arrive at card) and play until another button is interacted with, thanks in advance! 

Developer

Waiting for all sounds to finish is straightforward: sleep["play"]

So, playing a sound during a transition, letting the sound finish, and then playing another might have a script like:

play["firstSound"]
go[anotherCard "BoxIn"]
sleep["play"]
play["secondSound"]

Decker isn't really designed with looped audio in mind. Keep in mind that individual sounds in Decker are capped at 10 seconds, so they wouldn't work especially well for background music. You might be able to get continuous background noise by monitoring sys.ms and periodically issuing another play[], but it isn't straightforward. Perhaps a feature for the future...

(+1)

as someone who procrastinates a lot, i'm building a timer in decker using a field and a button to both act as a focus tool and also to apply some newfound programming knowledge. i have the basic countdown, but i'm wondering how i can make the program delay a second before going through the while loop again. any help is appreciated!

on click do  
    while display.text>0   
        display.text: display.text - 1  
        #delay goes here   
    end 
end
Developer(+1)

You could use the sleep[] function to wait a given number of frames. "sleep[60]" would delay for approximately one second.

A different approach for timing (which could be more accurate over longer time periods) would be to use `sys.now` or `sys.ms` to record a starting time and then consult them periodically to determine how much time has passed, possibly using "sleep[]" to delay between checks. (See: the System interface)

(+1)

Thank you to the both of you! This helps a lot.

(3 edits) (+2)

sleep[] looks a bit unsatisfying, as the card is not interactive as it waits, so the label isn’t that readable.

I have tried this:

Make a textfield to store a timer (here I used “timer”) initially set to 0. You can put it on another card if you want to hide it out the way.

Card:

on view do
 timer.text:timer.text+1
 if (timer.text<0) & ((10%timer.text)=0)
   display.text:display.text - 1 
 end
 go[card]
end

Button:

on click do
 timer.text:(display.text+1)*-10
end

Edit: I was late to the party. Using sys.now as suggested above sounds like a more suitable approach :)

Developer(+1)

Using the same "view[] refreshing" strategy is definitely the best way to do something like this if you want to be able to interact with the rest of the card while the timer is running.

You can also make the "secret timer" field invisible if you want to hide it on the current card.

https://cptnqusr.itch.io/super-cool-timer

here's the finished timer! not exactly the greatest thing ever but as a first project i'm quite happy with it

i know that lil has fuse, but how would i do something like `{x,"\n",y}/("hello";"world";"etc")` in k? i have a list of strings of arbitrary length and i want to combine them all into a single string by a delimiter. is there a way to do that?

(+1)

nevermind, i figured it out! somehow i missed that part of the lil docs.

Developer (1 edit)

To use K terminology, fuse is a dyad which takes a delimiter as its left argument:

 "\n" fuse ("hello","world","etc")
"hello\nworld\netc"
  ":ANYTHING:" fuse ("hello","world","etc")
"hello:ANYTHING:world:ANYTHING:etc"

And you can handle even fancier cases with a recursive "format":

  (list "ITEM<%s>") format ("hello","world","etc")
("ITEM<hello>","ITEM<world>","ITEM<etc>")
  ("\n","ITEM<%s>") format ("hello","world","etc")
"ITEM<hello>\nITEM<world>\nITEM<etc>"
(+2)

I may be missing something obvious, but how do you actually make a hyperlink in a field? The guided tour doesn't seem to have a script loaded into the field and the documentation doesn't specifically mention inline hyperlinks unless I missed it.

Developer(+2)

To make a hyperlink in a field, ensure that it is a "Rich Text" field (the default for new fields), switch to the Interact tool, and select a region of text within the field. You can then use the "Text -> Link..." menu item to create a hyperlink, which will be shown with a dotted underline. When you're finished editing, to make the hyperlinks "clickable" you must lock the field, by switching to the Widget tool, selecting the field, and choosing the "Widgets -> Locked" menu item.

Fields emit a "link" event when a hyperlink is clicked, which you can intercept with a script to do as you please. If you don't write a script, the default behavior is to go[] to the text you provided when you created the hyperlink. If that's the name of a card, it will navigate to that card. If it's a url like "http://google.com" it will ask the web browser to open a new tab.

Hope that helps clear things up!

(+2)

Ah, gotcha! Thank you so much!

Why does distinct not always return a list?

In the example for distinct elements given:

extract first value by value from "ABBAAC"

when all items are equal, this provides a single element instead of a single element list. I don’t know why, but in my head it seems more flexible to return a single element list.

This is what i ended up using instead, since select always returns a list. There is another workaround here since selecting the first element of each group in () will give (0), the default value, but at least that makes sense.

on uniq x do
  if x~() () else
    t:select list first value by value from x
    t.c0
  end
end
Developer (2 edits) (+1)

I think the simplest way to get the edge cases you want would be using "()," to coerce lists or scalars to lists, and using "() unless" to coerce an empty result with "first" to an empty list.

  (),extract () unless first value by value from "ABBBCD"
("A","B","C","D")
  (),extract () unless first value by value from "AAAA"
("A")
  (),extract () unless first value by value from ""
()

The former coercion is always valid, but the latter does require some care depending on the data; this is the nasty side of unifying nullity and a numeric value:

  (),extract () unless first value by value from 1,1,0,5,1,2,0,1
(1,5,2)

Edit: and another approach entirely would be to use "dict":

  range (11,22,33,0,11,22) dict ()
(11,22,33,0)
  range (11,11) dict ()
(11)
  range (0) dict ()
(0)
  range () dict ()
()

Depending upon the context, you might not even need the "range".

seems like dict is the most foolproof method. unless has the problem of ignoring zeroes.

  (),extract () unless first value by value from 0,0,0
()
Developer

Among other things, Decker 1.32 revises the behavior of extract to remove its problematic "automatic de-listing".

extract first value by value from ()

Now returns (), like it ought to have from the beginning.

Is there a better way to get the characters of a string other than this?

each x in "str" x end
Developer(+2)

I would probably use "split" with an empty string as the left argument:

  "" split "str"
("s","t","r")
(+1)

How to display an image after a delay such as sys.ms? i understand display.text but dont know how to approach image displaying, thanks for the help in advance guys!

(+2)

If you have a canvas containing the image, you can change visibility using the show attribute:

canvas_name.show: "none"
canvas_name.show: "solid"

To show or hide it on a delay, there is a simple way and a more complex way.

The simple way uses the sleep function. It (mostly) pauses the whole program until it’s finished sleeping. For example, you could use it in a button’s click action:

# Simple, e.g. button script
on click do
  card.widgets.canvas_name.show: "none"
  sleep[60] # number of frames to sleep for
  card.widgets.canvas_name.show: "solid"
end

Complex uses sys.ms and recursive go[] functions to allow other things to occur in the meanwhile. You’d need a hidden field to store some extra data. Here the image shows after 1s.

# Complex, with a field called hidden_time
# Button script
on click do
  card.widgets.canvas_name.show: "none"
  card.widgets.hidden_time.text: sys.ms
  go[card]
end
# Card script
on view do
  elapsed: sys.ms - card.widgets.hidden_time.text
  if elapsed > 1000
    card.widgets.canvas_name.show: "solid"
  else
    go[card]
  end
end
(+2)

thank you so much sunil! this was very informative, i tried the sleep method.. but i goofed it lol, this explained it very well, thank you

(2 edits) (+1)

Edit: solved!

Is there a way to include multiple where conditions?

Example:

data: select num:("1","2","3","4","5") parity:("odd","even","odd","even","odd") prime:(0,1,1,0,1) from 0

What I want:

extract num where parity="even" and prime from data

Edit: Of course I find it as soon as I post. The answer is brackets!

extract num where (parity="even") & prime from data

how does one find the upper and lower bounds of numbers in Lil?

(+1)

are there any plans to add the ability to plot a pixel in code? maybe it exists, and i haven't found it. but i think it'd be cool.

Developer

Plotting (or reading out) individual pixels on a Canvas (or an Image interface) is possible; just index or assign through them with an (x,y) pair as if they were a list or dictionary.

Plotting a large number of pixels will be fairly slow, since doing so will force the Lil interpreter to do a large number of serial operations. Both canvases and images provide a variety of higher-level methods for scaling, transforming, and drawing which operate upon pixels in bulk, and should generally be preferred, especially if the goal is any sort of realtime animation.

(+1)

thanks for the advice! really loving your work here.

(+1)

I couldn't find it in the manual, is there a way to hide the menubar?


PS I discovered this a few days ago, so awesome! Loved hypercard, and I'll join the decker jam :)

Developer (3 edits) (+1)

You can hide Decker's menu bar by "locking" a deck. In the main menu, choose "File -> Properties..". and then click "Protect..." to save a locked copy of the current deck.

In the deck file itself, this adds a line like:

locked:1

You can also manipulate whether a deck is locked on the fly by setting "deck.locked" in a script:

on click do
 deck.locked:!deck.locked
end

(Careful, though; if you use scripts to lock a deck you haven't saved yet you might get yourself stuck!)

Edit: oh, and if you meant hide the menu bar while editing, pressing "m" on your keyboard while using drawing tools will temporarily toggle the visibility of the main menu, allowing you to draw "underneath" it.

(+1)

Thanks a lot! Ow yes that second one is convenient too :)

Lilt’s write[x y] says it will write a value to a file, can it deal with image and audio interfaces? Can lil inside decker export data to a well known image/audio format?

Developer

The "write[]" functions in both Decker and Lilt can save image interfaces as .GIF images (including transparency and animation, if desired), and it can save sound interfaces as 8khz monophonic .WAV files.

The GIF files emitted by Lilt/Decker tend to be quite large, as they make no effort to compress their image data, so it may be desirable to use ImageMagick, Gifsicle, or a similar GIF optimizer to process their output.

It's also possible to write out arbitrary binary files by using an Array interface, but this is more involved.

(+2)

So excited to be working with Decker! I couldn’t find it listed elsewhere, is there a way to control the default volume a sound, or multiple sounds play at in a deck?

Developer (1 edit) (+1)

There isn't currently any kind of volume control; volumes are effectively "baked into" the amplitude of a sound's samples.

A slightly clumsy workaround would be to make a copy of an existing sound on the fly and use sound.map[] to rescale its samples. Assuming the deck contains a sound clip named "sosumi":

on play_scaled name vol do
  r:-128+range 256
  play[sound[deck.sounds[name].encoded].map[r dict floor vol*r]]
end
on click do
  play_scaled["sosumi"   1] # normal  volume
  sleep["play"]
  play_scaled["sosumi"  .5] # half    volume
  sleep["play"]
  play_scaled["sosumi" .25] # quarter volume
  sleep["play"]
  play_scaled["sosumi" .10] # 1/10th  volume
end
(+1)

got it, thanks so much for the quick response!

for an interactive adventure, how would you save a text input as a string variable for the player character's name ? i understand that it would start with a field.. say the variable for the string is playerName, and the field itself is called inputPlayerName.. would it just be the following in the script for the field?

playerName: ""

playerName: inputPlayerName.text

?? i can't test it because i also don't know how to print a string variable in a field.. simple things but still getting to grips!

Developer

In Decker, persistent state lives in widgets. If you want to remember anything beyond the scope of an event handler, it should be stored in a widget. For example:

Type of Data to StoreAppropriate Representations
StringField text
Ranged NumberSlider value
Arbitrary NumberField text
Boolean (true or false)Button value, any Widget's visibility
ImageCanvas, Rich Text Field (encoded as inline image)
TableGrid value, Field text (encoded as CSV)
Dictionary or ListField text (encoded as JSON)
Position or Sizeany Widget's bounding box

If you want to remember something without showing it to a user, you could use invisible widgets, or widgets on a hidden card.

The walkthrough and examples in this thread might be helpful to you.

I recommend checking out The Listener as a way of interactively trying short snippets of code. You can use the Listener to poke and prod at the contents of a deck or card and verify behaviors before you write a script.

The print[] and show[] Built-In Functions can be used to log information (formatted text or arbitrary Lil values, respectively) to the Listener for debugging. The alert[] function can sometimes be handy for debugging because it pauses the current script and displays text to the user. The panic[] function stops scripts completely, but can likewise be a tool for peering into the workings of complex scripts.

Does any of that help?

(+1)

yes that is really helpful! thank you for the functions and guidance about the listener- i'll keep tinkering with it! and thank you for making decker- super cool project, and it's really nice how you always answer questions

(+1)

hello- i'm so sorry to do this, but i've just spent the last 4 hours trying to figure out how to put a string variable in a field (i.e. player types in their character's name -> that name appears in text when the game or NPCs address the player's character) and i just can't? figure it out?? thank you so much for your response, i did manage to grok that you can just use the field itself as the variable (using the listener to figure that out!!) but actually taking that text and putting it in a sentence in a field is just beyond me.. i've read the reference manuals, looked at examples, gone thru the community threads and have now decided to swallow my pride and ask how you would 'embed' a string variable in a different field? like "hi [playerName], it's nice to meet you" or something like that?

sorry, i am very aware that i've been asking a lot of questions.. this is the most ambitious thing i want to do, so i don't imagine i'll be asking too many more!

Developer

No worries. Asking "obvious" questions in a public forum like this helps future users and lets me know about potential documentation/usability problems so I can continue to improve Decker.

There are a few different ways we could approach using the value of one field to update another. For starters, let's take a look at string formatting.

The Lil "format" operator takes a formatting string on the left and one or more parameters on the right. Let's see a few examples in the listener:

"Hello, %s. How's it hanging?" format "Alice"
"Hello, Alice. How's it hanging?"
"%s missed your call; they were busy %s." format ("Phil","gardening")
"Phil missed your call; they were busy gardening."

Each "%s" in the formatting string is replaced with a string on the right, in order of appearance. There are lots of other formatting codes and features available for dealing with numbers, zero-padding, case conversion, etc, but for the moment we can ignore them.

Now, let's say we have two fields on the same card: "name" and "reply":

There are a few ways we could approach updating "reply" when "name" is changed. One way would be to add a script to "name" and use the "on change" event:

on change do
 reply.text:"Hi, %s. I hope this example makes sense!" format name.text
end

It would also be possible to do the same thing when a button is clicked, etc. If "reply" was on another card, we might have to specify the "path" to it:

on change do
 otherCard.widgets.reply.text:"Hi, %s- how's it going?" format name.text
end

Both of these approaches are "push"-based: something explicitly happens to the name field and our script reaches out to other fields in response. A different way to think about it would be "pull"-based: logic on individual cards which reach out to other widgets and update themselves. For example, we could have an "on view" script on our "other card" which updates reply whenever a user travels to that card:

on view do
 reply.text:"Hi, %s- how's it going?" format firstCard.widgets.name.text
end

Either way works.

Does that get you "unstuck"?

(+1)

this is incredibly helpful! it did get me unstuck- thank you so much, i really appreciate it !!

(1 edit)

I am running decker on bash in linux with ./c/build/decker, How do I print to stdout from that process?

I have tried

shell["/usr/bin/env bash -c \"echo asdas\""]
shell["echo asdas"]
shell["echo asdas > /dev/fd/1"]

which simply returns 0 and do nothing. I am not sure how it works.

print["adasda"] just prints inside the decker interface.

i’d also like to know how to receive from stdin, if that will be possible.

Developer (1 edit) (+1)

Decker does not have the ability to execute shell commands or otherwise interact with the host system without explicit user permission; "shell[]" and similar functions are part of Lilt, which has a similar but distinct set of APIs from Decker itself.

As it happens, there is a way to print to stdout from Decker in the latest source revisions- the "app.print[]" and "app.show[]" functions. Note that this feature is not part of Decker v1.31, the current release at time of writing.

There is  presently no mechanism for polling from stdin. The closest analogy might be to use the alert[] function:

alert["please input a string:" "string"]

hmm, guess i’ll have to mod C decker to do what I want then. Thanks for the info.

Is there a function for creating a font interface from a font string, similar to image[]?

Developer (2 edits) (+1)

Not directly, no.

The Array, Image and Sound interfaces can be encoded as strings or reconstituted from strings via their "constructor" functions- array[], image[], sound[]. All of these are "free-floating" value-like objects that have no connection to or awareness of a deck.

Fonts are always part of a deck. The deck.add[] function can be used to create new fonts or make a copy of existing fonts, and deck.remove[] can likewise remove an existing font from a deck, which will as a side effect modify any widgets previously referencing said font. In Lilt, you can have access to multiple deck interfaces at the same time, so it's possible to copy fonts between decks.

It is technically possible to obtain encoded font strings- indirectly- via deck.copy[] and card.copy[], since those functions produce the same JSON-encoded string blobs you get when you copy cards or widgets manually, and correspondingly it is possible to use deck.paste[] and card.paste[] to indirectly add fonts or prototypes to a deck. In either case, manually parsing the copied representation of cards or widgets is a hack; the format used is subject to change in the future.

The ideal way to distribute Decker fonts, like modules, is to package them as decks. The other alternative is to copy a widget and share the "%%WGT0 " representation of that widget, along with any fonts and/or prototypes it may depend upon.

(1 edit)

In Interact mode, is it intentional that text copied between rich text enabled fields does not retain font formatting?

Developer

Yes. If a text selection consists solely of whitespace and one image, Decker will copy the image to the clipboard. Otherwise, Decker will copy the plain-text interpretation of the selection.

(+1)

Having a blast working within Decker, but coming up short in trying to figure out how to turn a substring within a rich text field into a hyperlink from an onclick event. I know how to make text a hyperlink from the Text menu option, but I want to call a function that will sometimes change the text property of a field to a string containing a hyperlink. Hopefully that makes sense!

Developer(+1)

I think I follow you.

Copying rich text from one field to another requires accessing their "value" property. Let's say we have rich text fields name "target", "a", and "b". The "a" field contains a link, and the "b" field does not:


In the button above I have a script like this which randomly picks between the value of "a" and "b":

on click do
  target.value:random[(list a.value),(list b.value)]
end

In this example, the values each need to be wrapped in a sublist with the "list" operator before joining them together with "," to prevent the rich text tables from being fused together into a single table.

The "a" and "b" fields could be hidden from view (Widgets -> Show None), and this approach generalizes to any number of alternative texts.

If you want to programmatically modify existing rich text to insert links it's a bit more complicated. Rich text is represented in Decker as a table, and the rtext interface contains some functions that can make it easier to manipulate such a table. If you wanted to change the styling of a specific range of characters within a field, you could use rtext.span[] to grab the text before and after your region of interest, retaining their existing styling, rtext.string[] to extract a region of interest without its styling, rtext.make[] to create a new styled chunk (including a hyperlink or inline image), and rtext.cat[] to glue all the pieces back together. If this is a road you need to go down I can try to furnish a more detailed example if you clarify your requirements. The dialogizer demo deck uses rtext functions to build its index on the fly.

Does that help at all?

(+1)

That absolutely does, thank you! I realize though that I could have done a better job providing context. I'm migrating a project that generates content for a tabletop RPG from something I built in Twine to Decker. A lot of random[] functions.

For a lot of the cards I'm working on, there's a contextual link between them that's based around a single word. In the below case, the word "Alien" in the Anecdote field would ideally be hyperlinked to a card titled Aliens, which would have a similar list of fields with randomly selected values in fields. I initially tried including link[aliens] in the array, but that just caused the aliens card to load upon clicking the button that contained the array.

These results are never a composite of multiple results, but just a fixed list of potential results, just in some cases there's a "contextual link" to another card. So as far as the solutions you provided, I could have "link fields" that contain the few instances of when a link makes sense within the card and just use *.text as the array value in those cases, right?  I'm willing to do this programmatically if it'll be a cleaner solution, though. Thank you again!

Developer(+2)

Just to make sure you're clear on the distinction, a field's .value attribute is a table, and a field's .text attribute is a string:

An rtext table can contain hyperlinks, inline images, and multiple fonts, but a plain string cannot. In many situations that ask for an rtext table you can supply a string and it will be "widened" into rtext, but it will all be in the default font. If you copy the .value of one rich text field to another it will preserve its formatting, but if you copy the .text you will flatten it out into a plain text representation.

(For anyone with web development experience, field.value versus field.text is loosely similar to element.innerHTML versus element.innerText.)

Decker 1.34 introduced a new "rtext.replace[]" utility function that might be handy. If you're working with a lot of text, links, and cards, it might be useful to write a deck-level utility function that finds certain keywords in a string or rtext table and replaces them with appropriate links, which you could then call whenever you populate a field. For example, perhaps something like this:

on contextualize text do
  db:insert keyword replacement with
   "Alien"  rtext.make["Alien"  "" "About Aliens"     ]
   "Weapon" rtext.make["Weapon" "" "Weaponry"         ]
   "Snacks" rtext.make["Snacks" "" "Delicious Treats" ]
   "Zombo"  rtext.make["Zombo"  "" "https://zombo.com"]
  end
  rtext.replace[text db.keyword db.replacement]
end

There's basically no limit to the possible complexity here (for example, you could automatically populate the keyword list by inspecting the titles of cards in the current deck), so it's up to you to choose what makes sense to you and is reasonably convenient for your purposes.

(+1)

First off I really appreciate your time in providing practical solutions. I gave this my best shot before returning here but every attempt I made at implementing the utility function you provided, at either the deck level down to the control itself, did not seem to work. I could get it to work within the Listener, but any table expressions seemed to do nothing outside of Listener. I'm sure I'm missing something, but I couldn't find a solution or workaround. 

Developer(+1)

Are you writing the result into a widget?

If you had a rich text field named "foo" which contained some of the words defined in the table for contextualize[], you'd apply it to the field something like this:

foo.value:contextualize[foo.value]
  • reading "foo.value" produces a rich text table.
  • "contextualize[foo.value] " calls the contextualize function with that rich text table and returns a modified rich text table.
  • the colon (:) is the assignment operator in Lil.
(+1)

As I anticipated, my issue was a syntax one and I wasn't properly calling "contextualize". I also unnecessarily added a comma delimiter to each of the rtext.make[] functions (I use Power Apps and JSON a lot with work). Everything is working as intended now, thank you!

(+2)

Hi I'm working on a small game very loosely inspired by Her Story. The player types a keyword into a search bar "display.text" and if valid it takes them to a new part of the game. Very simply I am doing this by creating cards and if the player types in the name of the card it will take them there. I used the following script on the button.

on click do

go[ display.text "SlideLeft"]

end

Is there a way for me to check for invalid entries so I can display an alert? Currently, it doesn't do anything. I'm not sure if there's a way to create a list of valid keywords and check from that, or else check from the list of cards that are there and go from there.


Thanks!

Developer(+1)

Sure- there are several ways to approach something like this!

The "in" operator can be used to check a string against a hardcoded list of valid options:

on click do
  if display.text in ("Keyword1","Keyword2","Keyword3")
   go[display.text "SlideLeft"]
  else
   alert["404: Page not found."]
  end
end

And it is also possible to obtain a list of valid card names from the deck; deck.cards is a dictionary from card names to cards:

on click do
  if display.text in deck.cards
   go[display.text "SlideLeft"]
  else
   alert["No such card, I'm afraid."]
  end
end

It might be a good idea to make the comparison case-insensitive if a user is typing free input. The easiest way to handle this would be to make sure the card names are all lowercase and then to convert the user input to lowercase with the "format" operator before doing any checks:

on click do
  t:"%l" format display.text
  if t in keys deck.cards
   go[t "SlideLeft"]
  else
   alert["I don't know anything about that."]
  ends
end

Yet another option is to make a dictionary to associate one or more keywords with destination cards; this provides more options for "forgiveness" in input handling:

on click do
  words["reindeerflotilla" ]:"puzzle1"
  words["reindeer flotilla"]:"puzzle1"
  words["reindeer"         ]:"puzzle1"
  words["smashthestate"    ]:"puzzle2"
  words["smash the state"  ]:"puzzle2"
  
  t:"%l" format display.text
  if t in words
   go[words[t] "BoxIn"]
  else
   alert["that's bogus!"]
  end
end

Does that make sense?

(+1)

Yes that does, thanks so much!

(1 edit)

I running into a somewhat unexpected behavior for a list of lists. Here is what happens in the listener:

eqs:((list 4, 2, 1, 3), (list 16, 4, 1, 12), (list 8, 1, 0, 0))
# listener prints back the list ((4, 2, 1, 3), (16, 4, 1, 12), (8, 1, 0, 0)). So far so good
e:eqs
# listener nicely print the list again ((4, 2, 1, 3), (16, 4, 1, 12), (8, 1, 0, 0))
e
# listener prints the actual value of e: 2.718282. Why???
# I have also tried, with the same amount of success :(
e:each eq in eqs end

How can I copy a list of lists? And why do I get this decimal value assigned to e even though the listener prints back the list that I'm trying to copy into e?


Edit:

I found the issue, e is a constant so even though Lil doesn't complain about me assigning a value to it the assignment doesn't actually happen and fails silently.

Deleted 1 year ago
Developer

Could you describe what you're trying to accomplish in a bit more detail?

Is the idea that you'd just be stacking copies of images on top of one another repeatedly, or is it more like "gradually reveal a series of layered images"? Do you want this effect on a single card, or is it something you want to repeat in a variety of places with different images?

(+1)

Oh sorry, I deleted my post because I found a solution (which is, I believe, really messy but it works approximatively...) 

To summarize : I launch a song (divided into 6 part (witch 3-witch4 -witch5-witch6-witch7-TapeOut)
I want to display images and texts one after the other, they appear one on top of the other in a somewhat chaotic fashion. 

I tried this on my play button :

on click do
 play["witch3"]
 card.widgets.canvas1.show: "none"
 card.widgets.canvas2.show: "none"
 card.widgets.canvas3.show: "none"
 card.widgets.canvas4.show: "none"
 card.widgets.canvas5.show: "none"
 card.widgets.canvas6.show: "none"
 card.widgets.field1.show: "none"
 card.widgets.field2.show: "none"
 card.widgets.field3.show: "none"
 card.widgets.field4.show: "none"
 card.widgets.field5.show: "none"
 card.widgets.field6.show: "none"
 sleep[120]
 card.widgets.canvas1.show: "solid"
 sleep[120]
 card.widgets.field1.show: "invert"
 sleep["play"]
 play["witch4"]
 sleep[120]
 card.widgets.canvas2.show: "solid"
 sleep[120]
 card.widgets.field2.show: "invert"
 sleep[100]
 card.widgets.canvas3.show: "solid"
 sleep[100]
 card.widgets.field3.show: "invert"
 sleep["play"]
 play["witch5"]
 sleep[120]
 card.widgets.canvas4.show: "solid"
 sleep[120]
 card.widgets.field4.show: "invert"
 sleep[120]
 card.widgets.canvas5.show: "solid"
 sleep[120]
 card.widgets.field5.show: "invert"
 sleep["play"]
 play["witch6"]
 sleep[120]
 card.widgets.canvas6.show: "solid"
 sleep[120]
 card.widgets.field6.show: "invert"
 sleep[120]
 card.widgets.canvas7.show: "solid"
 sleep[120]
 card.widgets.field7.show: "invert"
 sleep["play"]
 play["witch7"]
 sleep["play"]
 play["TapeOut"]
 card.widgets.canvas1.show: "none"
 card.widgets.canvas2.show: "none"
 card.widgets.canvas3.show: "none"
 card.widgets.canvas4.show: "none"
 card.widgets.canvas5.show: "none"
 card.widgets.canvas6.show: "none"
 card.widgets.canvas7.show: "none"
 card.widgets.field1.show: "none"
 card.widgets.field2.show: "none"
 card.widgets.field3.show: "none"
 card.widgets.field4.show: "none"
 card.widgets.field5.show: "none"
 card.widgets.field6.show: "none"
 card.widgets.field7.show: "none"
end
Developer

This approach absolutely works, and is very straightforward. A "synchronous" animation script like this is often the easiest.

I can offer a few tips that could make such a script shorter and easier to maintain:

  • If your script is on the same card as the widgets it references, it isn't necessary to use a fully-qualified name like card.widgets.canvas1; canvas1 will do just as well.
  • If you want to modify a large group of widgets at the same time, as in the beginning and ending of your script where you hide all your animation frames, you could make a list of the widgets and then take advantage of the ".." syntax. For example:
on click do
 parts:(canvas1,canvas2,canvas3,canvas4,canvas5,canvas6,field1,field2,field3,field4,field5,field6)
 parts..show:"none"
 # ...the rest of the animation goes here...
 parts..show:"none"
end
  • It's possible to introduce "helper" functions to factor out repeated patterns. We could, for example, make a function that sleeps for a few frames and then displays a widget- inverted if it's a field, and otherwise solid:
on reveal delay target do
  sleep[delay]
  target.show:if target.type~"field" "invert" else "solid" end
end

Which would then turn the main script into:

on click do
 parts:(canvas1,canvas2,canvas3,canvas4,canvas5,canvas6,field1,field2,field3,field4,field5,field6)
 parts..show:"none"
 play["witch3"]
 reveal[120 canvas1]
 reveal[120 field1 ]
 sleep["play"]
 play["witch4"]
 reveal[120 canvas2]
 reveal[120 field2 ]
 reveal[100 canvas3]
 reveal[100 field3 ]
 sleep["play"]
 play["witch5"]
 reveal[120 canvas4]
 reveal[120 field4 ]
 reveal[120 canvas5]
 reveal[120 field5 ]
 sleep["play"]
 play["witch6"]
 reveal[120 canvas6]
 reveal[120 field6 ]
 reveal[120 canvas7]
 reveal[120 field7 ]
 sleep["play"]
 play["witch7"]
 sleep["play"]
 play["TapeOut"]
 parts..show:"none"
end

Perhaps you could go even further, making a function that played a music segment, revealed several items in sequence, and then waited for the segment to complete:

on phrase audio parts do
  play[audio]
  each row in parts
   sleep[row.delay]
   row.part.show:if row.part.type~"field" "invert" else "solid" end
  end
  sleep["play"]
end
on click do
 parts:(canvas1,canvas2,canvas3,canvas4,canvas5,canvas6,field1,field2,field3,field4,field5,field6)
 parts..show:"none"
 phrase["witch3" insert delay part with
  120 canvas1
  120 field1
 end]
 phrase["witch4" insert delay part with
  120 canvas2
  120 field2 
  100 canvas3
  100 field3 
 end]
 phrase["witch5" insert delay part with
  120 canvas4
  120 field4 
  120 canvas5
  120 field5 
 end]
 phrase["witch6" insert delay part with
  120 canvas6
  120 field6 
  120 canvas7
  120 field7 
 end]
 phrase["witch7" insert delay part with
 end]
 play["TapeOut"]
 parts..show:"none"
end

Of course, abstraction adds some complexity, and might make it harder to introduce new exceptions to the rule if you continue to modify the script. Always choose the approach that feels the simplest to you!

(1 edit) (+1)

A question a day ! (I'm trying hard to have something completed for the deck-month !!)

So, I'm using two things : 

1. The Interior Contraption (I can drag a canvas to show what's behind a card)

2. The Decker Dialogizer (I can display text on the bottom of the screen as in a narrative adventure)

What I'm trying to do is to display a text only once, when I enter a card. For this I use a checkbox called tooted, and I use this script on my card : 

on go card trans delay do
 c:deck.card
 send go[card trans (30 unless delay)]
 if tooted.value.1
  dd.open[deck o]
  dd.say["Premier texte."]
  dd.close[]
  tooted.value:!tooted.value
 end
end

The text is only displayed once BUT my interior contraption can't be drag anymore ! And if I delete this lines of script, I can move the interior contraption. 

Have you got an idea ? 

Developer

Hmm.

I suspect "tooted.value.1" should probably be "tooted.value~1" or even just "tooted.value", and you don't appear to be doing anything with the variable "c" or defining anything for the variable "o".

Dialogizer runs "synchronously", blocking most forms of input while its dialogs are open. You shouldn't be able to drag widgets around, click buttons, etc, while a dialog is open, but when you close the dialog and the initiating script finishes everything should be back to normal.

Is the problem that you want to drag the Interior while the dialog is open, or does it somehow cease working even after the dialog has been closed?

(2 edits)

A button "Start" on another card leads me to the "bedroom_n" card and set all tooted checkbox to 1.
Once inside "bedroom_n", a text is displayed, I click and it disappear, but after this, I can't drag the Interior. 

Developer(+1)

I'm not completely certain what's going on here without really digging into the whole deck, but it looks like there's a conflict between your use of "go[card]" to drive non-blocking animation and bob the navigation arrows and your wrapper for "on go ... end" that conditionally triggers a dialog.

It might be better to make those buttons contraptions so they can handle their animation in a self-contained way and reduce the amount of scripting you need on each card; perhaps you could modify the bob contraption to expose a click event?

Looks like a really ambitious project so far!

(+1)

Indeed, the problem was a conflict between the bob contraption and the dialogizer ! Now I can do what I wanted to ! Thanks !!

(2 edits) (+1)

I got a little problem ! 
I want to randomly play 4 sounds, but only when I'm on a specific card

I'm using this code on the script of my card

on loop do  
random["dove_1","dove_2","dove_3","dove_4"] 
end

But even when I'm moving to another card, the script keeps on playing randomly the 4 files.

How should I do to stop the sound playing when I'm not on my card ? 


EDIT : I found it ! I needed to add - play[0 "loop"] - when moving to another page ! sorry for this useless post 

Developer(+1)

The default handler for the "loop" event provided by Decker looks like this:

on loop prev do
 prev
end

This handler is why the background loop plays forever by default; every time the loop ends, this function asks for the same sound to be played again.

If you write a deck-level script (File -> Properties -> Script...) which defines a replacement:

on loop do
 0
end

Then the loop will stop on all cards that do not have their own "on loop ... end" handler which overrides the above.

Does that make sense?

(+1)

absolutely !

(2 edits) (+1)

And how do I lock and unlock a button with a command ?
I tried 

on click do
card1.widgets.button1.locked.1
end

but I can see it's wrong

(sorry for being that level of noob)

EDIT : damn, again I found it : card1.widgets.button1.locked:1

There's a weird issue with the tangent function, where tan(pi/4) = 0, but using approximate values, like tan(0.7854), you get an answer close to 1. sin(pi/4) and cos(pi/4) are unaffected. I guess this might be some kind of floating point issue with the pi constant, and for that reason I'm not sure if it's a bug or a kind of user error. 


Developer

Looks like a problem with prettyprinting floats, rather than arithmetic itself. Applied a patch.

Developer

The number formatting issue has been patched in v1.37

(+1)

That was quick! Thank you! 

Hi! I need to authorize access to a card only on condition that all the other cards have been visited. Do you have any tips on how I can do this easily? 

Developer (1 edit)

Hm. Well, the first step (if you haven't done it already) would probably be stubbing out keyboard navigation, with a card- or deck-level script that overrides navigate[] to do nothing, to make sure the player can't accidentally go to your "protected" card without using editing tools:

on navigate do
end

Then you need some way of keeping track of whether cards have been "visited". Several ways to handle this. If you had an invisible checkbox on every card that needed to be visited named "visited", you could record visits something like so in each card's script:

on view do
  visited.value:1
end

Or even handle it automatically with a deck-level script fragment (not inside a function handler, just bare):

deck.card.widgets.visited.value:1

Left bare, it will execute whenever *any* event is processed. On cards without a "visited" checkbox it will have  no effect.

Check if every card with such a checkbox was checked using a query:

if min extract value..value where value from deck.cards..widgets.visited
  # ...
end

The ".." can be read as "at every index". The minimum of a list of 1/0 values is equivalent to logically ANDing them all together.

And you might want a button somewhere for resetting all the visited checks:

on click do
  deck.cards..widgets.visited.value:0
end

The nice thing about this approach is that as you add new cards in the future, you can decide on an individual basis whether they need to be counted toward "visiting everywhere".

Make sense?

I don’t get where i’m supposed to enter the minimal number of cards visited to allow a new thing ? 

If i have a button, and if its clicked whereas all cards have not been visisted i want to display a text (with dialogizer) / and if the value is high enough another text is displayed, then go to the new unlocked card 

Developer(+1)

You originally stated your goal as "all other cards have been visited".

In the model I describe above, cards whose visitation can count toward the total are each given an invisible checkbox (with a consistent name) that keeps track of whether that particular card has been visited yet. Cards that do not have such a checkbox don't count toward the total, so for example you may not need one on a "title screen" card.

If you only want to know whether some threshold has been exceeded, the query I showed for checking whether all such cards have been visited can be modified to instead count how many have been visited, and compared to some threshold (say, 10) like so:

if 10<sum extract value..value where value from deck.cards..widgets.visited
  # let the player go to another card...
else
  # tell the player they can't go yet...
end

If you only kept a single counter somewhere to track card visits, there would be no way to identify repeat visits to the same card, which is probably not what you have in mind.

If you used a different naming convention (call some checkboxes 'visited1', some 'visited2', etc?) and modified the queries I describe accordingly, you could track several disjoint or partially-overlapping groups of "visited" cards.

(+1)

It's good for me !!!! 

Is there any way built in way to do networking over HTTP in Lil? I am trying to build a simple client for a simple messaging protocol called "Nostr" which is mainly websocket based. 

Developer

Not currently.

If you're familiar with JavaScript it is relatively straightforward to install new primitives in the Lil interpreter by modifying an HTML build of Decker. This thread includes an example of exposing browser localStorage to Lil.

Async communication is somewhat awkward; if you were to expose an API based on callbacks, like the underlying JS APIs, you could easily end up with Lil functions attached via closure to parts of a deck that have been modified or destroyed since the original call was performed. A polling-based system exposed on an Interface might be less error-prone.

Here's a sketch (untested) of a polling-based wrapper for XMLHttpRequests:

const pending_messages=[]
interface_messagebox=lmi((self,i,x)=>{
   if(ikey(i,'send'))return lmnat(([url,verb,text,id])=>{
     const x=new XMLHttpRequest()
     x.onreadystatechange=_=>{
       if(x.readyState!=XMLHttpRequest.DONE||x.status!=200)return
       pending_messages.push(lmd(['text','id'].map(lms),[lms(x.responseText),id?id:NONE]))
     }
     x.open(verb?ls(verb):'POST',url?ls(url):'')
     x.send(text?ls(text):'')
     return NONE
   })
   if(ikey(i,'poll'))return lmnat(_=>pending_messages.length?pending_messages.shift():NONE)
   return x?x:NONE
})

WebSockets add some additional complexity because the connections need to be persistent, but a similar approach might work.

Very helpful! Thank you and all the best for 2024.

Beginner question - is there a property on widgets to return the card that widget is contained in?  So, for example, so you could create a button that changes its text property to that of the name of whatever card it is within?

Developer (1 edit)

Widgets do not expose an attribute with a reference to the card that contains them. There are, however, several ways to accomplish what you describe.

Whenever a widget's event handler is fired, a number of variables will be in scope, including:

  • "me": the recipient of the event
  • "deck": the current deck
  • "card": the current card (there's some additional subtlety to this in Contraptions, which I will omit for clarity)
  • all of the widgets on the same card, according to their names
  • all of the cards in the deck, according to their names

So, for example, your button script might look something like:

on click do
 me.text:card.name
end

If the button happened to be named "myButton" and the card it was on happened to be named "myCard" you could equivalently say:

on click do
 myButton.text:myCard.name
end

Another way to access cards is via the deck. The "deck.card" attribute is the card that the user is presently viewing. Generally this will be the same as the "card" variable, but "deck.card" might be more up-to-date if you happen to go[] to another card midway through a script. Thus, we could also write the example as:

on click do
 me.text:deck.card.name
end

The mechanics of events are described in more detail in the Decker Reference Manual.

Does that answer your question?

(+1)

Yes! Thanks so much for the help. Decker is sick btw!

(1 edit)

EDIT: Also, sorry if I'm not so great at explaining haha ^^; I tried my best to explain it since it is somewhat complex...

How do I program a simple text adventure into Decker? I want to let the player input their response into the parser, press "GO!" and add an output (Like adding a ">" before copying their response to the game text area and adding another response that correlates with the response they entered as a normal text adventure would.) Here is what I made so far:

Developer

That's a pretty open-ended question. Text adventures can be as complicated as you want to make them. It's only a little bit more specific than "how do I program a video game?"

As a really simple starting point, if you have widgets like so:


You could add a script to the button (or to the card, since there's only one thing that can be "clicked") something like

on click do
  response:"i don't know how to %s, buddy." format input.text
  
  log.text: log.text, ("\n>%s\n" format input.text), response, "\n"
  log.scroll:999999999
  input.text:""
end

Obviously this doesn't interpret any commands, but it handles appending text to the log, ensuring the log is scrolled to the bottom, and resetting the input field each time. If you run it, you can get something like this:


For the parsing and formatting bits themselves, you might get a few useful ideas from the mini-twine proof-of-concept I posted earlier; it demonstrates parsing a markup format with Lil and building chunks of "rich text" on the fly.

The "Lildoc" script which is used to build Decker's HTML documentation from Markdown files might also be worth looking at: https://github.com/JohnEarnest/Decker/blob/main/scripts/lildoc.lil

Does any of that point you in the right direction?

(1 edit)

I don't know if this really has anything to do with lil, it's about the dialog contraption, so it's still a programming question. I am making a game in Swedish, so i need to use the letters Åå, Ää and Öö. Since Decker doesn't support them, I have made a custom font, replacing other less used symbols with them, though this, of course, requires rich text. When using r:dd.ask, is it possible for the alternatives to be rich text? I made this attempt, where field6, 8 and 9 all contain rich text:

r:dd.ask[   
field6.value   
(field8.value, field9.value)  
] 
if r~0     
 dd.say["placeholder"]     
 dd.close[]  
else       
 dd.say["placeholder"]       
 dd.close[] 
end

Decker would, however, display 8 and 9 in plain text and like this:


Menu is the font name. Do you have any ideas what could cause this?

Developer(+1)

The ideal place to ask questions or submit bug reports about Dialogizer is the Dialogizer thread.

Rich text is represented as a table. While the "," operator applied to a pair of strings will form a list of strings, it will concatenate the rows of a pair of tables into a single table. We need to be a little more careful when we want to create a list of tables.

The "list" operator can solve this problem; it wraps any value in a length-1 list. The "," operator will join those length-1 lists together instead of the values they contain. Try constructing your list of choices for dd.ask[] like so:

(list field8.value),(list field9.value)

If the only special thing about the rtexts is the use of a single particular font, you could alternatively give the choices as plain strings and set the "bfont" option when you set up dialogizer; this will apply the specified font as the default for all the dd.ask[] choices:

o.bfont:"swedishFontName"
dd.open[deck o]
...

For general dialogs there's also "tfont" for the default font of the body text in dd.say[] and dd.ask[] boxes.

Ah! Thank you. I guess it makes sense.

Hi ! 

On a card I have a slider, which can display values between 0 and 5. Next to it is a "validate" button.

I'd like the validate button to take the player to different cards depending on the value he chooses (so 6 different cards, since there are 6 possible values). I imagine this isn't very complicated, but I can't find the trick!

Developer(+1)

The go[] function is used to navigate to a different card.

There are three ways to specify the card you want:

  • by index: if you provide a number n, go[n] will navigate to the nth card in the deck, counting from 0.
  • by name: if you provide a string s, go[s] will navigate to the card with that name.
  • by value: if you provide a card c, go[c] will navigate to the card you provided.

Navigating by index is usually not a great idea, because adding or removing cards can easily break such a script. We'll use navigation by value for these examples.

The numeric value of a slider widget can be viewed through its .value attribute.

To turn a numeric index into a card value, we'll make a list of cards and index into it using the slider's .value.

To slightly simplify your example, let's say you have a slider named "mySlider" with a range from 0 to 2, and cards named cardA, cardB, and cardC.

Forming a list:

(cardA,cardB,cardC)

Forming a list and then indexing it:

(cardA,cardB,cardC)[mySlider.value]

So the full button's script could be:

on click do
 go[(cardA,cardB,cardC)[mySlider.value]]
end

And of course, if you want a smoother transition you can specify go[]'s optional arguments for a transition style and delay:

on click do
 go[(cardA,cardB,cardC)[mySlider.value] "BoxIn" 15]
end 

This kind of approach also works if you don't need/want all the destinations to be distinct. Consider:

on click do
 go[(cardA,cardA,cardB,cardC,cardB)[mySlider.value]]
end

Is that clear?

(+1)

Totally clear and perfectly works (as usual you're the boss)

I'd like to make a "search" game system, so players type something into the bar, then click on a card that contains the term (so far I've had no problems!). 

But to put an end to the game, I'd like their number of moves to be limited, so they can view say 10 documents (so 10 different cards) before automatically arriving at a new card that forces them to turn in an investigation report. What would be the easiest way to do this? 

The other idea I have would be to set up a countdown visible from all the maps, at the end of which you arrive at a final map, but I imagine that's a lot more complicated...

Developer

I'd make a locked and/or invisible field (or maybe a slider) somewhere to track the number of moves the player has remaining.

If you're displaying search results as a bunch of links in a field, you could make clicking a link count down the remaining moves by overriding the "link" event for the field. The default handler for link events is

on link x do
 go[x]
end

So you could instead give the field something like

on link x do
 moves.text:moves.text-1
 if moves.text=0
  go[theGameOverCard]
 else
  go[x]
 end
end

Side note: you can also write that conditional as:

go[if moves.text=0 theGameOverCard else x end]

This approach might be a little weird, though, since it ignores the user's final selection and surprises them with a game over.

If the cards you can get to via search have a "back" button, maybe it would make sense to test for game overs there, and navigate to the final card instead of the search page?

I'm using SearchEngine contraption, I'm not sure if I can "override" the links displayed in this prototype?

Developer

You'd need to modify the contraption prototype, but it's a very simple change. In the SearchEngine the results are displayed in a rich-text field named "o". It doesn't currently contain any scripts and simply relies upon the default "on link" behavior.

The main thing to be aware of is that within a Contraption you won't auto-magically have cards and widgets of the deck in scope as variables; You do have access to "deck", though, so you can use fully-qualified paths to reach out to a specific widget. e.g. "deck.cards.theSearchCard.widgets.theSearchCounter", and if you use go[] you can refer to cards by name (as strings) instead of value (as cards).

I've tried to make all the contraptions in the bazaar as generic and reusable as possible, but don't be afraid of hacking them up or making changes to suit a specific application. In the worst case you can always delete the prototype and re-import the reference copy.

I think I'm close to the solution, but I don't understand why when I write "moves.text:moves.text-1" -> the field displays "-1" instead of doing a calculation... 

Hi there!!! I've been loving playing with decker so far and have a  noobish(?) question. Is there a way we can edit and/or import are own animated brushes like the last 4 in the default palette?  I've looked around the Contraption thread as well as searching thru the forum. Thank you!! <3 

Developer (1 edit)

It sounds like you mean animated patterns. There isn't a built-in tool for modifying patterns, but Decker includes scripting APIs that could be used to build one. For example, the PatEdit contraption can edit patterns themselves, and the AnimEdit contraption can be used to configure animated sequences (patterns 28-31) of the static patterns (2-27). You could also set up animated patterns manually with The Listener.

If you actually mean brushes, the decker reference manual section "Brushes" describes how you can define your own. Decker comes with an example deck that contains several custom brushes.

(+1)

This is a question regarding scripting. I would like to have a button "toggle" between two states; revealing a widget A, then hiding a widget A, then revealing a widget A, etc. but don't know what the script for that would look like. And speculatively, would it be possible to add other elements so that the button might cycle between revealing widget A, to revealing widget B,  to revealing widget C, then back to hiding them all? Thanks in advance!

Developer (1 edit)

The first example is very straightforward: widgets have a .toggle[] method. If you call it with a single argument it will alternate the value of their .show attribute between "none" and the string specified ("solid", "transparent", or "invert"):

on click do
  a.toggle["solid"]
end

You can see more examples of .toggle[] in the release notes.

Cycling through displaying several different widgets is a bit more complicated.


One approach would be to test the .show attributes of widgets, find which one is currently visible, and then set the .show attributes of all the widgets in a group in order to advance to the "next" selection. The main issue here is it's very ugly and inconvenient to add more widgets to the cycle:

on click do
  if     "solid"~a.show a.show:c.show:"none" b.show:"solid"
  elseif "solid"~b.show b.show:a.show:"none" c.show:"solid"
  else                  c.show:b.show:"none" a.show:"solid"
  end
end

We can reduce repetition and make maintenance a bit easier by making a list of widgets of interest, scanning for them in a loop, and using an index modulo the length of the list to pick the next item:

on click do
  cycle:a,b,c
  current:0
  each wid index in cycle
    if "solid"~wid.show current:index end
  end
  cycle..show:"none"
  cycle[(count cycle)%1+current].show:"solid"
end

If we want to get really clever, there's also a "vector-oriented" way to handle that search instead of an each-loop. In general, multiplying a list of integers [0,n) by a length-n list containing zeroes and a single one will "mask out" the index of the one:

(0,1,2,3)*(1,0,0,0)   # (0,0,0,0)
(0,1,2,3)*(0,1,0,0)   # (0,1,0,0)
(0,1,2,3)*(0,0,1,0)   # (0,0,2,0)
(0,1,2,3)*(0,0,0,1)   # (0,0,0,3)

So we could compute "current" like so:


Giving a complete script as follows:

on click do
  cycle:a,b,c
  current:sum(range count cycle)*"solid"=cycle..show
  cycle..show:"none"
  cycle[(count cycle)%1+current].show:"solid"
end

If you want to include "don't show anything" in the cycle, you can add a "0" dummy element to the list of widgets; 0.show will always be 0, which is not equal to the string "show", and attempting to set the .show property of a number is likewise harmless:

on click do
  cycle:0,a,b,c
  current:sum(range count cycle)*"solid"=cycle..show
  cycle..show:"none"
  cycle[(count cycle)%1+current].show:"solid"
end

Does that answer the question?

Edit: one more note: depending on what you're trying to do it might be worth considering using a slider widget instead of a button; Sliders naturally represent choosing between a range of integers, which could represent an index into a list:

on change val do
  cycle:0,a,b,c
  cycle..show:"none"
  cycle[val].show:"solid"
end

See also, the enum contraption.

(+1)

This is great. Exactly what I was looking for (and more!) It's enlightening to see how many different ways their are to tackle a problem. Thanks so much for this John.

(2 edits) (+1)

Hi! I’m a bit stuck trying to figure out how to search for a substring inside a table. I’m thinking of a journal application where I have Date and Entry columns, and I was hoping to add a search function that returns rows that match a certain substring. Reading the docs, it looks like I can match the first part of the string by doing this:

select Date Entry where ("hi%m" parse Entry) from journal_list.value

But I can’t quite figure out how to match a string that doesn’t start at the beginning. 😅

Developer (1 edit)

The "in" operator can test whether a substring is present in a string:

 "upon" in "stars upon thars"
1

To use this on a column, we could define a helper function. While we're at it, this can also lowercase the column we're searching for a case-insensitive match:

on like needle haystack do
     each v in (list "%l")format haystack
         needle in v
     end
end

Allowing us to write a query like so:

ex:insert Date Entry with
     20210412 "Once Upon A Time"
     20220709 "There once were"
     20230101 "stars upon thars"
end
select where like["upon" Entry] from ex
# +----------+--------------------+
# | Date     | Entry              |
# +----------+--------------------+
# | 20210412 | "Once Upon A Time" |
# | 20230101 | "stars upon thars" |
# +----------+--------------------+

How's that?

(+1)

Amazing thank you so much for the help! I’m still getting used to Lil you can do so much with so little. I’m just now seeing this: “The each loop collects together the results of each iteration of the loop and returns them” 🤯

Developer

The absence of a direct equivalent to SQL's "like" operator has plagued me for some time, so in Lil v1.40 I decided to introduce a first-class equivalent. Using this operator, we can now perform the same type of case-insensitive search (as well as quite a few handy variations) without needing a helper function:

ex:insert Date Entry with
     20210412 "Once Upon A Time"
     20220709 "There once were"
     20230101 "stars upon thars"
end
select where ((list "%l")format Entry) like "*upon*" from ex
# +----------+--------------------+
# | Date     | Entry              |
# +----------+--------------------+
# | 20210412 | "Once Upon A Time" |
# | 20230101 | "stars upon thars" |
# +----------+--------------------+

If you prefer the previous way, it will be necessary to rename the "like[]" helper function, since "like" is now a reserved word. Sorry for any inconvenience!

Hello,

There's probably a trick for that, but I didn't find a way to create a single element dict:


("foo") dict ("bar") => {"f":"b", "o": "r"}



Whereas:

("foo", "baz") dict ("bar", "qux")


does the expected.


Developer

The straightforward way is to use the "list" operator, which wraps its argument in a length-1 list:

 (list "foo") dict (list "bar") 
{"foo":"bar"}

A fancier, more concise way (provided your keys are strings) is to perform an amending assignment to an empty list, like so:

 ().foo:"bar"
{"foo":"bar"}
(+1)

Thanks! very helpful!

Some cards in my deck have a field called "group" with a number in the text field.

In my deck, how would I go about selecting a random card that matches a specific group number and then send the player to that card?

Developer (1 edit)

To obtain a list of the cards with a particular group number you could use a query something like

c:extract value where value..widgets.group.text=2 from deck.cards

(Note that if a 'group' field doesn't exist it will behave the same as a card in group "0"; you probably want to count from 1 for your groups)

Given that list, you can pick a random item and navigate to some card like so:

go[random[c]]

Or, all at once,

go[random[extract value where value..widgets.group.text=2 from deck.cards]]

Does that make sense?

(+1)

Yes! Thank you.

(+1)

Is it possible to hide a column in a grid? I find myself having to use a lot of hidden grids and then selecting a subset of the columns into a visible grid. Which is fine enough, but I wonder if I'm misunderstanding the scope of variables or features of grids and maybe there's a way to not do this.

Developer

You can adjust the widths of the columns of a grid to hide trailing columns. In "Interact" mode, a small draggable handle appears between column headers:


You can reset the columns to their default uniform spacing with the "Reset Widths" button in grid properties.

Manually resizing column widths enforces a minimum size. You can set column widths to 0 programmatically via "grid.widths", but a 0-width column looks a bit odd; I'll make a note to fix that in the next release.

(+1)

Somehow I missed the grid.widths value in the grid interface. Thanks!

Developer

FYI i've patched the problem with 0-width columns in grids; that should now offer a fairly flexible option for visually suppressing columns without physically removing them from the underlying table. You can try it out now at the bleeding-edge source revision, and the fix will be incorporated into the v1.41 release; probably next week.

I'm having a hard time wrapping my head around arrays, maybe it's not what I actually need to be using. Basically I have two fields that I can enter text and I want to take each field and append them to an array that splits it all up by word. So "This is my text"  in field1 and "More text" in field2 would be ["This", "is", "my", "text", "More", "text"] that I could then manipulate.

Developer

Starting from the two fields you describe:

You can obtain the text of either field through its ".text" attribute. The "split" operator breaks a string on the right at instances of the string on the left and produces a list. Splitting one string on spaces gets us part of the way to what you're asking for. We can then use the comma operator to join a pair of lists.

" " split field1.text

The best way to experiment with this sort of thing is to use The Listener. Ask a "question", get an answer:

Another approach for gathering space-separated words from several fields would be to use an "each" loop:

Does that help point you in the right direction?

(+1)

This does! When looking at the docs I guess this made me think "split" and such were tied specifically to tables. But now I'm starting to wrap my head around this.

Developer (1 edit)

In general, for string manipulation the main tools Lil provides are:

  • drop, take, split, and parse for cutting strings apart into smaller pieces
  • fuse and format for gluing pieces together to make strings
  • like, in, <, >, and = for comparing and searching strings

...and a few of the more complicated utility functions functions in rtext do also apply to plain strings.

I may not understand fully how random works.

Say I have a table with 5 rows stored in 'temp'

random[temp,1] will usually get me a row with all the data, But sometimes it just results in the integer 1.


Why is that?
Developer

The Lil "comma" operator forms a list by combining the elements of its left and right arguments.

If you use this operator to combine a table and a number, the table will be coerced to its list interpretation (a list of the rows of the table, each a dictionary), and then combined with the number 1 (whose only element is itself), forming a list of several dictionaries and the number 1. Applied to such a list, random[] will occasionally choose the 1.

When you call a Lil function with multiple arguments, commas should not be placed between arguments. You probably meant

random[temp 1]

 Instead of

random[temp,1]

Furthermore, note that if you specify 1 as a second argument to random[] you will get a length-1 list as a result, whereas if you call random[] with only a single argument you will get a single value.

In preparing this post I have also observed that there is some inconsistency between native-decker and web-decker with respect to applying random[] to table values, which may have compounded the confusion; I'll have this fixed in the v1.41 release tomorrow. In the meantime the alternative is to explicitly crack the table into rows before making a random selection, like so:

random[(rows temp)]
(+1)

Thank you!

(+1)

Does random use a fixed seed? When I repeatedly run a lil script in lilt, random[range 15] returns 4 every time.

Developer

Lilt uses a fixed seed by default; this is a design decision inherited from K.

One simple way to randomize it would be something like

sys.seed:sys.ms

Note that Decker automatically randomizes the RNG seed at startup.

(+1)

Cool. Thanks!

What’s the best way to filter a list based on some predicate?

I was able to use “extract value where … from somelist” for some basic arithmetic, but when I throw a function into the predicate I get unexpected results (“value < f[value]” always seems to be true even though it definitely isn’t).

I’m wondering if I’ve misunderstood the “where” clause and it is only supposed to be used in table columns or something.

Developer(+1)

In the context of query clauses, column names refer to the entire column as a list. The "where" clause expects an expression which yields a list of boolean values. Arithmetic operators like =, <, and + conform over list-list and list-scalar arguments.

Depending on how a predicate is written, it might naturally generalize to operating on lists for the same reason. For example: 

on iseven x do 0=2%x end
iseven[5]
# 0
iseven[11,22,33]
# (0,1,0)
extract value where iseven[value] from 11,24,3,8
# (24,8)

If a predicate is only designed to operate on a single scalar value at a time, you can use the "@" operator to apply it to each element of a column, like so:

on seconde x do x[1]="e" end
extract value where seconde@value from "Lemon","Lime","Soda","Demon"
# ("Lemon","Demon")

This shorthand is semantically equivalent to

extract value where each v in value seconde[v] end from "Lemon","Lime","Soda","Demon"

(And of course in this particular case the "like" operator would be simpler:)

extract value where value like ".e*" from "Lemon","Lime","Soda","Demon"

Does that clear things up?

(+1)

Thanks! That does explain why it seemed to work sometimes and not others. I will give it another try. For the record, It looks like my skimming skills failed me, because I now see where this is explicitly noted in the docs:

When computing columns, you're working with lists of elements, and taking advantage of the fact that primitives like < and + automatically "spread" to lists. When performing comparisons, be sure to use = rather than ~! If you want to call your own functions- say, to average within a grouped column- write them to accept a list

It even calls out “where” in the next paragraph.

(+1)

Hi! I've been having a fun time messing around in Decker, and am now attempting something more game-y.

Specifically, I'd like to know if these are possible to do:

  • When Button X is clicked, have Button Y on a different card disappear/become hidden
  • When Button Z is clicked, unlock new dialogue on a different card (with the Dialogizer module)
I'm assuming it would be something like, "on click do: Button X = true"—but I'm not sure exactly what terms to use, so I would appreciate any help. Thanks!
Developer(+1)

Widgets have an attribute called "show" which controls their visibility; this can be "solid" (the default), "invert" (an alternate color-scheme), "transparent", or "none".

Suppose the button X is on card C1 and the button Y is on card C2.

You could give X a script for toggling Y's visibility:

on click do
 C2.widgets.Y.toggle["solid"]
end

Or a simpler version that just makes Y visible:

on click do
 C2.widgets.Y.show:"solid"
end

There are several other examples of doing this sort of thing in this thread.

To make clicking a button "unlock" dialogue or behavior elsewhere in the deck, you'll need to remember that the button has been clicked, and consult that record at a later time.

Checkboxes are just a special visual appearance for a button, so every button widget has a "value" attribute that can store a single boolean (0 or 1) value. Thus, we can use the button itself to keep track of whether it's been clicked. Suppose you've given the button Z on card C1 a script like so:

on click do
 me.value:1
end

A script on another card could reset Z's value,

C1.widgets.Z.value:0

Or test its value in a conditional:

if C1.widgets.Z.value
  # do something special
end

If you have lots of "flags" like this, it may be a good idea to centralize them in some kind of "backstage" card so you can keep track of them easily, and perhaps to give yourself a script for resetting the state of the deck.

Does that make sense?

(+1)

Makes sense! Thank you for the detailed walkthrough, that was all very helpful :)

is it possible to have something special unlocked only when two buttons are checked at once?🤔...

Developer(+1)

That would be a logical "and", produced with the & operator:

if C1.widgets.Z.value & C1.widgets.W.value
 # ...
end

hey, I'm completely new to lil and decker and I can't crack the random command. I want to make a widget that sends me to a random card from a selection of say ten cards. is it even possible? I feel like it should be, but I just end up sending myself off to the home card. 

Developer

The "random[]" function can be called in several different ways to produce different kinds of random values. In your case, you want to select a random value from a list of possibilities and then supply that value to the "go[]" function.

The "go[]" function can navigate to cards by index (a number), by name (a string), or by value (a Card interface). We'll go with the latter. Within a script, all the cards of the deck are available as variables of the same name. Commas (,) between values are used to form a list. If we have cards named "cardA", "cardB", and "cardC", and we wish to navigate to one of those cards randomly when a button is clicked, we could give it a script like:

on click do
 go[random[cardA,cardB,cardC]]
end

Does that help?

You might also find some of the examples in this tutorial illustrative: https://itch.io/t/3593043/make-your-own-chicken-generator

(+1)

I'm trying to figure out format strings, specifically to get a slider to show its max value. Is %v what I want, and if so how do I use it? Or is this even possible without additional scripting?

Developer (2 edits) (+1)

You can find documentation for format strings in Lil, The Formatting Language.

The format string of a slider widget is only given its value, which would usually be displayed as an integer (%i) or a floating-point number (%f); the rest of the string could, for example, provide units, like "%f degrees". In this way you *could* hardcode a maximum into the format string, like "%i/20" for an integer between 1 and 20 to be displayed like "7/20".

Extending the current behavior of sliders to make min/max and possibly other attributes of the widget available for formatting could be an interesting feature; I'll mull it over. For now, including that sort of information would require scripting. If you need this functionality in more than one place, perhaps you could use The Enum Contraption as a starting point?

(+1)

Okay, thanks. The description of the "v" pattern type in the Lil docs made me think I could maybe insert any variable using just format strings. It's not too bad to just hardcode it and update it with a script whenever I change the max, though.

Hi there, I noticed an odd behaviour, not sure if it has been flagged before. It relates to having a button widget with a script. If the button already has a script, and you go to the Action area to add a sound or pick a card etc. it overwrites the old script with the new action instead of just adding it into any existing script. Possibly there are good reasons for that, but thought I'd point it out just in case!

Developer

When you open the "Action..." dialog, Decker will attempt to "unpack" any script already attached to the button and reflect it in the settings you see in that dialog, and when you confirm it writes an entirely new script. This dialog exists purely as a convenience for composing simple scripts without writing code, and is not intended to manipulate or append to arbitrary user-generated scripts.

(+1)

Makes sense, thank you!

(1 edit)

I think I've seen somewhere that it is possible to build a personal website using Decker as the "frontend".  So for example, I could have a GitHub Pages repo with the Web Decker HTML file as index.html and a data file (e.g. a CSV at first) to have a certain degree of decoupling between the data and the app. Is it possible and if so, how can I programmatically (in Lil, e.g. when the main deck loads or when a particular button is pressed) import an external CSV file in the Web-based Decker so I can display it as a table (for a start)?

I hope that my problem is clear and thank you.

Developer

It is not possible for an unmodified copy of web-decker to programmatically fetch external files hosted e.g. somewhere on github pages, make http requests, etc. Doing this sort of thing would require adding custom javascript extensions to web-decker. An example of how such an extension might be written can be found in this subthread.

In general, decks should be self-contained objects pre-packed with any data they require to operate. A deck is a database, with grid widgets for storing tables and Lil for querying and manipulating them.

Lilt can be used to import or export data to or from a .deck or .html file in an automated fashion. If you insist on storing some dataset independently from a deck that uses it, it might be possible to use Lilt as part of a continuous integration pipeline or "build script" to bake out an updated web-decker build when an associated csv file is changed.

(+1)

Is it possible to render text and widgets in a color other than black?

Developer(+2)

Many widgets will render in white-on-black instead of black-on-white if you set them to "Show Inverted".

If you set widgets to "Show Transparent" you can draw underneath them and have it show through, and thus provide background colors or patterns. The same trick is also handy for making buttons that appear to have an icon instead of text: draw the icon underneath!

Canvases can be drawn on in any color or pattern; you can use these directly or within Contraptions to make colorful variations on normal widgets. The fancy button contraptions I made recently can be configured with color images.

Finally, you can customize Decker's palette, substituting some different color globally for white and/or black.

(3 edits)

hey ij!

is there a way to write a single line of command to copy and paste more than one widget from one card to another?

i have widgets X, Y and Z stored in card A, and so far i can import each of them separately to card B by writing

card.add[deck.cards.A.widgets.X]
card.add[deck.cards.A.widgets.Y] 
card.add[deck.cards.A.widgets.Z]

but i can't really nail a way to import all three of them with a single line (unless i call all three lines with an .eval[]).

i saw in the decker manual that you can use the .add[] command to import a list or dictionary of widgets from another card, but i don't see how.

thank you!

(+4)

Hi, I'm not IJ but I did some experimentation and I think I've figured it out - it seems the card.copy and card.paste commands let you copy multiple widgets at once.

So you'd do like this:

card.paste[A.copy[A.widgets.X,A.widgets.Y,A.widgets.Z]]

(As a side note, if you're referring to a card in a script you can just use the card's name, you don't need the deck.cards in front of it)

Let me know if this works for you!

(+1)

oooooooooo!!! thank you so much, millie! i'll try as soon as i can!

(+1)

worked like a charm <3 thank you once again!

Developer(+4)

Just to build on this, if the only widgets on card A are X, Y, and Z, you can copy them all with:

A.copy[A.widgets]

And here's one other slightly more concise way to select a bunch of widgets by name:

A.copy[A.widgets @ "X","Y","Z"]
(+1)

i learned so much today!

i think i used "on view" scripts on too many widgets, and now my deck is kinda sluggish. are there lighter ways to make widgets receive information from other widgets without having them wait for signals on every frame?

for example, since i'm building a clicker game, i use on view scripts to keep information flowing between cards. things such as updating numbers and detecting clicks. 

is it lighter to make one widget be the animated one running all the code, or to make many widgets animated running smaller bits of code?

Developer(+1)

There would be somewhat less overhead to having a single animated "pump" which is responsible for updating everything else on a card.

Have you tried enabling the script profiler (Decker -> Script Profiler) to confirm that it's scripts which are slowing things down? Having a large number of widgets shown at any given time (or particularly large canvases) can also start to stack up.

If you're updating large numbers of widgets individually- like lots of separate fields displaying stats- you might find that it's more efficient to replace them with multi-line rich-text fields or grid widgets, both of which can be useful for displaying formatted bulk data. There's no hard-and-fast rule about the best way to approach things; you may need to experiment. If you find any particular operation which seems unusually slow, and you can provide me with a minimized example, I can investigate and possibly improve Decker's performance.

Hey there! I've been redoing The Steppe code, following your suggestions on cohost, but I made a minor modification that seemed to have broken something.

So, there was an invisible field "counter" that would have its numeric text altered according to what card you came from, starting from 1. Then, there was a screen-sized invisible button that would take you to another card based on a giant "if... elseif" code. Your suggestion to simplify it would be to use lists. This was the example given:

cards:(card2,card3,card4,card5,card6,card7,card8)
trans:("n/a","WipeUp","Dissolve","Dissolve","Wink","Dissolve","CircleIn")
go[cards[counter.text-1] trans[counter.text-1]]

It was working well, but then I thought "maybe I could make the counter start from 0, instead of 1, so it would align with the list index on the 'go' and I can remove the -1". So I rewrote all the code for counter to go from 0 to 7 instead 1 to 8, and changed the "[counter.text-1]" to just "[counter.text]".  Except now the button doesn't work properly, it just goes back to the home screen no matter the text in the field counter.

However, if put a +0, it works again. So, right now, it's like this:

cards:(card2,card3,card4,card5,card6,card7,card8) 
trans:("n/a","WipeUp","Dissolve","Dissolve","Wink","Dissolve","CircleIn") 
go[cards[counter.text+0] trans[counter.text+0]]

What I want to know is... am I doing something wrong? Is there another way to use counter.text as an index that I'm missing? I've been messing around with it for quite a while, but can't seem to solve this. 

Developer

The .text attribute of a Field is a string. Performing any sort of arithmetic operation on a string coerces it to a number (as a convenience), and in many places a string like "23" is fully interchangeable with the number 23:

 100+"23"    # 123

Unfortunately, indexing into lists isn't one of those places:

 foo:"Alpha","Beta","Gamma"
 foo[1]      # "Beta"
 foo["1"]    # 0
 foo[0+"1"]  # "Beta"

Part of the reason Lil draws a hard line here is that indexing lists by strings is one way to force them to "promote" to dictionaries when you assign through them; trying to parse strings into numbers could lead to some very nasty ambiguity:

 foo["z"]:"Delta"    # {0:"Alpha",1:"Beta",2:"Gamma","z":"Delta"}

I apologize for the confusion stemming from my suggestions.

One possible alternative would be to make "counter" a Slider widget instead of a Field; The value of a Slider is always a number.

(+1)

Ah, don't worry, you have nothing to apologize for! I thought this could be the case, but that might have had another workaround I wasn't figuring it out.

The code works perfectly now, so I'll keep it as-is with the +0 and write a small note that field.text is always a string, so I don't forget. Thanks for the help!

(1 edit)

hey, ij and fellow deckheads! 

i've been using the rect module to detect the cursor hovering over buttons and having a field widget display information on that button. so far i can only do this by having every button being animated and having a rect.overlaps[pointer me] command. 

what i really wish i could do was having the rect module detect the widget type. like rect.overlaps[pointer <button>] (this doesn't work) and then specify the button by its text. 

is it feasible?

thank you!

Developer (2 edits)

The rect module (which, for those unfamiliar, is included in the "All About Draggable" example deck) includes functions like rect.overlaps[] and rect.inside[] for manipulating "rectangles", which can either be a dictionary containing pairs with the keys "pos" and "size" or any Decker interface value that happens to have those fields- widgets, for example.

The pointer interface has a .pos field, but no .size field. Using an invalid index into an interface type returns 0, so the pointer interface kinda fulfills the contract and sort of works as an argument to these utility functions (as if it were a rectangle of size 0), if only by coincidence/accident.

I whipped up a little test, and sure enough this seems to work fine given an animated button:

on view do
 field1.toggle["solid" rect.inside[pointer button1]]
end


(Note that "tooltips" like this won't work properly on multitouch devices, since such input devices do not update the pointer position while a user's finger happens to be hovering above the display; this is why Decker doesn't have any sort of tooltip functionality built in.)

The same script body ought to work just fine from a centralized event pump rather than having every object with a tooltip be individually animated. I don't really follow what you mean in terms of the rect module "detecting the widget type".

thank you for the answer! this already opens up a lot of possibilities, actually.

what i meant was if there was a way of having, for example, an animated field widget that displayed text whenever the pointer went over a button. and i figured the way to do this was such field having something like:

on view do
 if rect.overlaps[pointer (((any button)))]
  me.text:"pointer over some button"
 else
  me.text:"pointer over any other thing"
 end

(it just occurred to me that i can name a group of widgets that are buttons and do this, but i'm thinking of a less manual approach)

(1 edit)

So, I'm trying to add import options to the image importer/exporter contraption I'm building. So far, this is the interface and everything seems to be working fine. "Extra Options" just makes everything disappear, keeping only itself and the Import/Export buttons.


Only one of the checkbox buttons can be selected at a time (I'm using a basic "if" to check the value of each one and unmark the other 2 if is true).

Here's how the images look when imported in the order of the checkboxes.




So, erm, I think the issue is clear. The Gray Import isn't importing in grays, but rather in a psychedelic way.  Funny enough, the Dither Import relies on the Gray Import to use transformation, otherwise it just completely breaks too. But even so, the Dither result is noisier than just dragging and dropping an image into Decker.

It's worth noting the image is a jpeg, so I'm not sure why it's getting animated with the Gray Import.

Here's the code in the Import button:

on click do
    if colorbutton.value=1
        i:read["image"]
        card.image.paste[i 0,0,card.size]
    end
    if graybutton.value=1
        i:read["image" "gray"]
        card.image.paste[i 0,0,card.size]
    end
    
    if ditherbutton.value=1
        i:read["image" "gray"]
        i.transform["dither"]
        card.image.paste[i 0,0,card.size]
    end
end

I tested it with a canvas, and also did some testing in the Listener, but I can't figure out what's going on, if there should be some other sort of conversion happening before, or something like that.

Developer

This is working as designed. 256-gray images are not directly displayable within Decker, since Decker's color palette does not contain 256 colors and patterns; only 48:

 

A grayscale image must be converted into a proper paletted image before it can be displayed; otherwise the grayscale values are effectively randomly mapped to entries in the above table, and any higher indices appear as white. The image.transform["dither"] function is one way to produce a 1-bit dithered image from a 256-gray image, using Bill Atkinson's algorithm. Rescaling a dithered image will considerably reduce its quality. The correct order of operations to prepare a dithered image therefore must be:

  1. obtain or otherwise create a 256-gray image.
  2. perform any desired palette adjustments to the 256-gray image, like adjusting white/black points or contrast.
  3. scale and crop the 256-gray image to the desired final size.
  4. dither the 256-gray image, resulting in an image consisting exclusively of patterns 0/1.
(4 edits)

I was talking about this on cohost and millie also told me about Decker not being able to display the 256 grays which... makes complete sense tbh, as you yourself explained the palette limitation (which I actually know but didn't connect the dots, so I'm a bit embarrassed). Funny enough people liked the result, so I think I'll rename the option and leave it there for them to use and give life to fever dreams. 

And the order for the dithered image makes sense now, I'll rewrite its import code and see if I can get it right then. Thanks a lot!

Edit: the dithering improved a lot now!

Still on the same contraption as before, I'm having trouble finding a solution for a thing that might be beyond what I understand at the moment: paste the background of a contraption into the card's background without having to use the Listener or create a widget button in the card.

What I mean is, I can copy and paste into the card the image that was imported into the contraption with the following code, for example:

importer.image.paste[importer.widgets.Importer1.image.copy[]]

Where "importer" is the card name and "Importer1" is the name of the contraption. I also use some variations, depending if I'm in the same card as the contraption or not. However, having to rewrite this by changing the card's name every time isn't that practical, so I wanted to make the contraption itself paste its image on the card it's over (kinda like a "stamp"). But, from inside the contraption, I can't seem to get the image to be pasted anywhere else.

I tried messing around with the attributes, but that doesn't seem useful in this situation since no widget is trying to access or modify the contraption's content. Making a script with the generic "card." followed by the rest doesn't work, I guess because contraptions work like cards themselves from what I understood, so the command is ambiguous? I tried using a canvas too, instead of the contraption's background, thinking it would be easier to access, but it also didn't work.

So, am I overlooking something obvious? Or to do what I want, it requires Lil's more complex stuff to deal with prototypes/contraptions also being "cards"?

Developer

From inside a contraption instance, "card" refers to the contraption instance itself; this is handy if you want to send events to the "external" scripts on the contraption or inspect default properties like .locked, .show, or .font.

If you want to refer to the card within which a contraption resides, you can generally use "deck.card". Strictly speaking, "deck.card" is the active card on the deck; the card the user is looking at. This will only be distinct from the contraption's parent card if the contraption is being sent a synthetic event from some external script.

A different angle to consider would be prompting the user for a destination card when importing an image. You could, for example, use alert[] in "choose" mode to select cards from the deck by name, using the current card as a default:

alert["pick a destination card:" "choose" deck.cards deck.card.name]


The downside to the above is that if a user hasn't given cards logical names they're "picking blind", which can be error prone. Every design problem is fractal in nature...

Using "deck.card" worked, hooray! The contraption is now working as intended with all the features I wanted, I'll just do some testing in the next few days and upload it to the jam.

I decided to avoid the "alert" route because, at least for my usage, I need it to just behave like a "stamp", copying the contraption background (which uses Decker full screen) and pasting it exactly like it's shown in the card below. Then one can delete the contraption and work straight on top of the "stamped" image with other widgets, including invisible ones. And I figured being pasted into the background allows image manipulation/editing/painting too.

Now, this whole thing led me to 2 other questions:

 - I'm using this code in a button to paste the contraption background into the card:

deck.card.image.paste[deck.card.widgets.deckstamp1.image.copy[]]

Is there a way to make the "deckstamp1" be a string pulled straight out of the contraption's own name field? This way, the script would always "autocomplete" and the button wouldn't stop working if someone changed the contraption name, for example. I did some testing with various syntaxes, but as always, not sure if I'm doing something wrong or if it's not possible.

- Is there a way to access a contrast adjustment, like the one with "j" and "k" that works only after dragging an image into Decker? I looked around and didn't find anything. I figured that if there is, it would be cool to have a contrast slider and a button to update the image, even if it works just with the dithered import.

(+1)

I'm just starting with decker, and I feel like my question is absolutely basic and stupid, but I swear I've been digging at the documentation and examples and can't find the issue.
I have a button on a card that has the following script:

on click do
 key: 1
 go[bednokey]
end

The idea being that you pick up the key and it leads you to an identical card of the same location with no key.

On a previous card, I have the following script on a button, to avoid going to the card with the key once it's been picked up already:

on click do
 if key=0
  go["fieldbed" "SlideUp"]
 else
  go["fieldbednokey" "SlideUp"]
 end
end

And it won't work at all. It always goes to the card with the key. I tried reversing the conditions, using other symbols, no dice. What's the proper way to do this check?

Developer(+1)

Anything that you want to "remember" across event handlers needs to be stored in a widget. A true/false flag, for example, could be stored in a hidden checkbox. If we had a card named "flags" with a checkbox widget named "hasKey" your scripts might look something like

on click do
 flags.widgets.hasKey.value:1
 go[bednokey]
end
...
on click do
 if flags.widgets.hasKey.value
  go[bednokey "SlideUp"]
 else
  go[bed "SlideUp"]
 end
end

If the widget happens to be on the same card as the script which references it you can shorten paths like "flags.widgets.hasKey.value" to just "hasKey.value".

If you want to show or hide objects on a card, you may find it more flexible to use a transparent Canvas widget containing an image of the key. You can show, hide, or reposition canvases at will, sort of like a sprite object:

myCanvas.show:"solid"
myCanvas.show:"transparent" 
myCanvas.show:"none"
myCanvas.toggle["solid" booleanExpression]
myCanvas.pos:otherWidget.pos

This may also remove the need for a separate "flag" widget; the canvas itself will remember its visibility setting:

if bed.widgets.keyCanvas.show~"none"
 # ...
end

Does that help answer your question? There are lots of other examples of doing this sort of thing in this thread and elsewhere on the forum.

(+1)

This absolutely goes above and beyond answering my question, thanks! I'll try all these out right away.

I might be a bit over my head here, but I'm trying to add the gif export to Deckstamp. However, I'm having trouble adapting the new WigglyKit example with the export code to Deckstamp. In short, I'm... not sure how it works. I understand it grabs the frames and their order, but I don't know how it does that.

The normal canvas and the image interfaces don't have ".value", so it seems it's not that simple to just copy frame order from them, even if the image has "movement" (like the moving patterns). So I tried to use wigglyCanvas, but now I'm having trouble pasting an image in it: I tried using the image interface and it didn't work, so I tried using parts of the code of the import example on WigglyKit, and still nothing.

In any case, I'm guessing the best way would be to grab the card background image and save its different frames together instead of fiddling with wigglyCanvas, but is that possible? What would a code like that look like?

Developer

Let's start with some context. All of the contraptions in WigglyKit share the convention of an "animation value"  consisting of a dictionary with the keys "frames" (a list of images) and "order" (a list of integers; indices into the list "frames"). There's nothing intrinsically special about this dictionary; it's just a convention to bundle together all the relevant information for a wiggly animation such that it can be easily moved around between those contraptions and manipulated with scripts.

WigglyKit offers an example of pulling the animation value out of a WigglyCanvas and then saving it as a GIF:

v:wc.value
# (optionally) make the background opaque:
v.frames:v.frames..copy[].map[0 dict 32]
gif.frames:v.frames @ v.order
gif.delays:10
write[gif]

As noted in the Decker reference manual for the built-in function write[] (see note 12), to save an animated GIF we need to call write[] with a dictionary with the keys "frames" (a list of images) and "delays" (a list of integers; how many 100ths of a second should each frame be displayed?)

So really, what we need to do to save a GIF outside the specific context of WigglyKit is make an appropriately-shaped dictionary and hand it to write[]:

gif.frames: ... # obtain or assemble a list of images, somehow
gif.delays: 10  # one number applies the delay to every frame
write[gif]

Obtaining/assembling that list of images will depend entirely upon your use case. We might grab them from a sequence of canvases:

gif.frames: (c1,c2,c3)..copy[]

We could do the above in a more verbose way, if we wanted:

gif.frames: c1.copy[],c2.copy[],c3.copy[]

Or perhaps we could use the recently-added ".images" attribute of rich-text fields to grab all the inline images stored in a field:

gif.frames: myRichTextField.images

Etc.

Does that help point you in the right direction?

I think I understand better now how it works! After some tests, I kinda did it, but the results were a bit different from what I was seeing on the screen. It's worth mentioning all the images are pasted into the card's background.

Here's the source image, which was exported using Decker's own "export image" in the menu. It's worth mentioning this image was generated by importing a photo with the "gray" hint and not doing anything else:


I then reimported this image into Decker using the same options, and this is what I'm seeing on the screen now. This is an OS screenshot because this time, Decker's export image isn't exporting an animated gif anymore, just a still image (shown below this one). Every diagonal line was supposed to be "moving" (which causes a really cool effect).

OS screenshot (in Decker, all the lines have movement):


Decker's exported image:

Fiddling around with trying to grab frames and delays, I arrived at this result, which doesn't seem to have any movement in this post, but it does if you open the image in another tab:

I used this code to arrive at this result:

i:read["image" "gray_frames"]
card.image.paste[i]
gif.frames:i.frames
gif.delays:i.delays
write[gif]

It's not the same thing I'm seeing on the screen, but it's progress!

Funny enough, if I try to replicate the first image using "gray_frames" in a normal photo instead of just "gray", it just imports the image as if it was using "color" instead.

Now I'm trying to figure out how to grab the animated patterns in a card's background as different frames. Maybe that will export the same thing I see on the screen? I was able to grab frames from imported gifs, from different canvases and put them together, and was successful with images in rich text fields too, but I'm not sure how to go about a background image.

(+1)

Hi, I'm thinking of making a small text adventure game with a parser, a little bit like Zork and games like that. So far, I found this post to make a parser with a button next to it. But I was wondering if it was possible to make a parser input without the button, like if you could press the return key in the field widget to check the words in it. I'm guessing that since the field widget already use the return key to go to the next line in the widget, I'd need to replace that action with the parser input, but I'm a bit clueless how to do that, if it's possible.

Developer(+1)

Hmm. I think this would be rather tricky.

Decker is designed to try to accommodate text input on devices that don't have a physical keyboard, so it avoids exposing low-level keyboard events and doesn't (presently) allow scripts to modify the contents of a text field while it has user focus. Consider how the soft keyboard overlay introduced in Decker 1.19 interacts with unlocked field widgets.  While it's easy enough to detect that a user has pressed return in a text field (and typed a newline),

on change do
 if "\n" in me.text
  # ...
 end
end

That event handler wouldn't be able to clear out the user's line of input, which would make a scripting REPL or an IF parser input awkward.

I'll give this some serious thought and see if I can come up with a good compromise to allow the sort of thing you're interested in making

(+1)

Yeah, I thought it might be tricky, especially for touchscreen users. I might stick with the version I found with the button in the meantime. Thanks for the response and for giving it some thoughts!

Developer(+2)

Decker 1.49 includes some subtle changes that I think will address this request.

It is now possible to clear a field via scripts while it is selected. In touch mode, this will additionally dismiss the on-screen keyboard and remove focus from the field.


In the above example, the script looks like this:

on change do
 if "\n" in me.text
  log.text:log.text,"\n %s%J" format me.text,list eval[me.text].value
  log.scroll:99999
  me.text:""
 end
end

There are a few details to be aware of when using this technique:

  • It takes 1/4 of a second without user input for the contents of a field to fire a change[] event, and it is also possible to fire a change[] event by pasting an arbitrary block of text into a field. Therefore it's possible for a user to type several characters following a newline, or even multiple newlines; input handling code will need to be robust to this.
  • An alternative to looking for "\n" in a change[] event would be to use the run[] event, which is fired immediately when the user presses shift+return. This may be less intuitive for users, and therefore unsuitable for many applications, but it's much less error-prone and more flexible than newline-delimited input; the run[] event makes it possible to build UIs which behave like Decker's Listener.
  • Since the user actually does type a newline, you'll need to carefully adjust the vertical sizing of the input field to match the font in order to maintain the illusion that the field is "cleared" instantly when the user presses return.

I hope this helps!

(+2)

This seems to work exactly how I wanted to, thank you! I see in the exemple that it evaluates what is written in the input and I've tried to modify the code so that if a specific word or phrase is written in the input, that it would say a specific answer in the log, but I don't quite understant the code enough to make something that works. And I was wondering if this technique would also works with triggering events, like to toggle other widgets in the card of for alerts?

Developer(+2)

OK, let's make a simple Colossal-Cave-Adventure-style two-word parser.

Lil's parsing patterns can get a little bit cryptic, but essentially what we want to break up user input is:

  • Skip any leading spaces.
  • Grab any characters up to a space or newline and call them "verb".
  • Skip any spaces.
  • Grab any characters up to a newline and call them "obj".

Which can be expressed as the pattern:

pat:"%*r %[verb]-.2r \n%*r %[obj]s\n"

I did a few tests in the Listener to make sure it was working as intended:

 pat parse "foo"
{"verb":"foo","obj":""}
 pat parse "  foo"
{"verb":"foo","obj":""}
 pat parse "foo bar"
{"verb":"foo","obj":"bar"}
 pat parse "foo bar\nbaz"
{"verb":"foo","obj":"bar"}
 pat parse "foo  bar\nbaz"
{"verb":"foo","obj":"bar"}

Now we can rework the input field's change[] handler to bundle this up and send the verb/object to the card script:

on change do
 if "\n" in me.text
  c:"%*r %[verb]-.2r \n%*r %[obj]s\n" parse me.text
  me.text:""
  typed[c.verb c.obj]
 end
end

In the card script, we'll have our actual game logic, a twisty maze of if statements:

on println x do
 log.text:log.text,x,"\n"
 log.scroll:999999
end
on typed verb obj do
 println["> %s %s" format verb,obj]
 if verb~"look"
  if obj~"flask"
   println["it is a flask."]
  elseif obj~"self"
   println["that's difficult unless your eyes are prehensile."]
  else
   println["ye see ye flask."]
  end
 elseif verb~"take"
  if obj~"flask"
   println["ye cannot take ye flask."]  
  else
   println["i don't see a %s anywhere here." format obj]
  end
 else
  println["ye is speakin' nonsense."]
 end
end

And our scintillating gameplay experience begins:

(+1)

I'm starting to understand a lot more how it works now. Thanks a lot for answering my questions!

(+1)

hi, can you somehow unlock a locked deck? 

(+1)

If you have the file on your computer you can open it with a text editor like Notepad and change:

locked:1

to

locked:0

It should be in the first few lines of text.

(+1)

Regarding conditionals, do "if" statements support "and" and "or"? I've been doing some testing in the Listener and it seems to work, but I'm unsure if I got the syntax right. The same goes for using ">" or "<" with "=". I used, for example:

if a = X and b = Y
  <code1>
elseif a < or = W
 <code2>
else
 <code3>
end

That seems to work but I've never used syntax like this, so I'm unsure if I'll be messing something up down the line depending on these conditional results.

And on an unrelated note, is there any editor that highlights Lil's syntax? I'm getting into territory where I have to click around a lot to modify things that bugged out due to some unintended modification and I always forget to change some bits of code in a widget tucked somewhere. I've been using TextEdit to quickly find and modify these, but a proper editor seems a better solution for long-term projects.

Developer(+1)

In Lil, the "&" and "|" operators provide logical AND and logical OR. Both "conform" to the elements of lists:

(1,1,0,0) & (1,0,1,0)  # -> (1,0,0,0)
(1,1,0,0) | (1,0,1,0)  # -> (1,1,1,0)

To be more precise, these operators are actually binary "minimum" and "maximum" operators for numbers:

(1,2,3,4) & 2          # -> (1,2,2,2)
(1,2,3,4) | 2          # -> (2,2,3,4)

As always, be careful to remember that Lil expressions are carried out right-to-left unless you include parentheses.

For comparisons, Lil has < (less), > (more), and = (equal). It does not have "compound" symbols like "<=", ">=", or "!=". If you want the complementary operation, you can use ! (negation):

  a > b      # a is more than b
! a > b      # a is not more than b (a <= b)
  a < b      # a is less than b
! a < b      # a is not less than b (a >= b)
  a = b      # a is equal to b
! a = b      # a is not equal to be (a != b)

Note that all of these operations work to lexicographically compare strings, too:

"Apple" < "Aardvark","Blueberry"    # -> (0,1)
"Apple" & "Aardvark","Blueberry"    # -> ("Aardvark","Apple")
"Apple" | "Aardvark","Blueberry"    # -> ("Apple","Blueberry")

Lil syntax highlighting profiles for vim, emacs, and Sublime Text are available at the Decker Github Repo.

Syntax highlighting is also available in the Lil Playground.

(+1)

Oh, I forgot about the right-to-left reading, now it makes sense why some conditionals were returning 1 even if not all conditions were true. Now, if I understood it correctly, the following code would be truthy?

a.value:4 #slider a
b.value:5 #slider b
if (a=4) & (b=5)
go[card2]
end

It seems to work in the Listener, I just want to make sure I got it.

The "!" negation also worked perfectly on some tests I did, which solved some issues for me. Thanks!

(+1)

hi!! i'm making my first game in decker and i'm wondering if there is a way of accessing a variable defined in script (ideally on card or deck level)? so far i bypassed this by using an additional card with checkboxes but maybe there's an easier way to do it?

(+1)

aand one more question.

i want to add dialogue options after the player interacts with objects on other cards

so i thought of having a widget with a table that can be updated with new options and importing it into the dd.chat function like this:

r:dd.chat [ "question" raze #insert table end]

is it possible to do?

Developer

Variables that live beyond an event handler must be stored in a widget. If you find it inconvenient to reference widgets on another card,

thatCard.widgets.has_knife.value
thatCard.widgets.has_soap.value
thatCard.widgets.has_corncob.value

You could write utility functions on the card script that aggregate information from widgets:

on get_inventory do
 ("knife","soap","corncob") dict (has_knife,has_soap,has_corncob)..value
end

And then send events at that card from elsewhere:

thatCard.event["get_inventory"]

(Or as shorthand if the event handler doesn't take any arguments,)

thatCard.event.get_inventory

In this way you can give cards an "API" of sorts. You could also define helper functions in the deck-level script to reach into specific cards:

on items do
 thatCard.event.get_inventory
end

And then call that helper function from anywhere in the deck:

items[]

(I think this is probably overkill unless you're accessing the same info in lots of places within a deck.)

You can absolutely use tables to keep track of dialog options. A grid widget stores a table in its "value" attribute. Aside from manually editing table contents as CSV or JSON, you can initialize one with a script:

myGrid.value:insert k v with
 "Tell me about Chickens"  "They cool."
 "Tell me about Daffodils" "They flower."
end

You could append a new row (or several rows) to the existing table in a grid like so:

myGrid.value:insert k v with
 "Tell me about Easter Eggs" "They rare."
into myGrid.value

And you could then later retrieve that table and convert it into the dictionary form dd.chat[] wants by razing it (which turns the first two columns into dictionary keys and values, respectively):

dd.open[deck]
dd.chat["Ask away." (raze myGrid.value)]
dd.close[]

Note that if you plan on revisiting the same dialog tree over the course of a game many times but you don't want to repeat old selections you'll need to do additional bookkeeping; dd.chat[] only remembers/winnows selections within the course of one conversation.

For more information about tables and querying, see Lil, The Query Language.

Does that help point you in the right direction?

(+1)

that's all i needed, thank you so much <33

Is there a way to replace a color on a card with another color from the palette via the Listener? 

Developer

The "image.map[]" function can be used to re-palette any Image interface, including card backgrounds. The simplest way to call it is to feed it a dictionary where the keys are the original pattern indices and the values are the replacement pattern indices:

card.image.map[colors.red dict colors.green]

Note that this operation modifies the image in-place.

(+1)

thank you!

Hi! In this post, you mentioned that:

If we wanted clicking on chester to toggle frobnicate, we'd write a script for him like so:
on click do
 home.widgets.frobnicate.value:!home.widgets.frobnicate.value
end
Or, to be a bit less repetitive,
on click do
 f:home.widgets.frobnicate 
 f.value:!f.value
end

It seems that in this case, assigning to `f.value` actually modifies `home.widgets.frobnicate`.


However, when running this in the Listener:

x.a:1 x.b:2 x.c:3
show[x] # {"a":1,"b":2,"c":3}
y:x y.a:42
show[y] # {"a":42,"b":2,"c":3}
show[x] # {"a":1,"b":2,"c":3} still the same!

The behavior is different, `y.a:42` doesn't modify `x`.

Could you explain why?

Developer(+2)

The difference is that deck parts are "Interface" values, while your later example is a dictionary.

Lil's basic datatypes (numbers, strings, lists, dictionaries, tables, functions) are all immutable values: attempting to modify them will return a new value and leave the original value unchanged.

Interfaces (like most deck parts) are mutable, and accessing or modifying their attributes may have side-effects. For example, modifying the ".index" attribute of a widget to change its order on a card will also visibly modify the ".widgets" property of the container card, since these attributes are related to one another. The "sys" interface exposes "sys.now", an attribute which contains the current Unix timestamp, and may (generally) be different every time it's accessed. Lil also offers utility Interfaces like Array for those times when mutable data structures are necessary for performance reasons.

Does that make sense?

Thanks for the quick response! Initially, I thought this might be that `home.widgets.frobnicate` is copied by reference, now I see that Interface values are more to this. They look more like modifiable "values" instead of "variable names", and after assigning them to a name, making changes to the attributes under that name actually interacts with the "value" instead of replacing it with a new one.

Hello I was trying to use the decker diagolizer.

I wanted to open dialogue box when a card opens. I used inside the on view do. It works but it seems to make the same dialogue box twice instead of once :( Do you guys know how to do it ?

(+1)

Things that are inside an `on view do` script will usually continue to happen as long as you're viewing the thing it's attached to.

I sometimes set up a hidden (visibility: none) checkbox on the card and an ' if...' statement inside of the `on view do` script. 

And then I make sure that something unchecks the checkbox when the autoplaying event is finished, so it doesn't play again.

on view do
if autoplay.value #if the checkbox named 'autoplay' is checked...#
#(your dialogizer stuff goes here)#
autoplay.value:0 #uncheck the box#
end
end

You can check your checkbox manually or you can set up a script to do it before you leave the previous card. 

Something like this could be inside the previous card's exit button. (With the correct names for your project)

on click do
cardname.widgets.autoplay.value:1
go[cardname]
end
(+1)

Hi, I was wondering if there was a way to re-order widgets on a card via script (the equivalent of the Widgets > Order feature). For example if I click on a draggable canvas partially covering/overlapping with another can it be promoted to the top of the pile? 

Developer (1 edit) (+2)

You can inspect and modify the order of widgets on a card via their .index attribute, which counts from back to front, starting at 0. This is automatically clamped within the permissible range on a card, so assigning it to 0 or -1 can be a convenient shorthand for "all the way to the back" and any very large number (say, 999999) can likewise be used for "all the way to the front".

For example, you could give your draggable canvas a script like so:

on click do
 me.index:999999
end
(+1)

Thank you so much!

(5 edits) (+1)

Is it possible for a contraption to change its own size? I'm creating an animated sprite contraption that gets the sprite width and height from its attributes, and then shows an animation taken from a sprite sheet card.

I tried to do

me.size:(sprite_width.text,sprite_height.text)

on the view event, but it doesn't do anything when the contraption is running on a card (it does change its size when running inside the prototype view). I'm saving the state in hidden fields inside the prototype. Here's the whole script:

on get_spritesheet do sprite_sheet_card.text end
on set_spritesheet x do sprite_sheet_card.text: x end
on get_frames do frames.text+0 end
on set_frames x do frames.text:x end
on get_sprite_width do sprite_width.text+0 end
on set_sprite_width x do sprite_width.text:x end
on get_sprite_height do sprite_height.text+0 end
on set_sprite_height x do sprite_height.text:x end
on get_fps do fps.text+0 end
on set_fps x do fps.text:x end
on get_loop do loop.text+0 end
on set_loop x do loop.text:x end
on get_current_frame do current_frame.text+0 end
on set_current_frame x do current_frame.text:x end
on view do
 me.size:(sprite_width.text,sprite_height.text)
 c:deck.cards[sprite_sheet_card.text]
 w:c.image.size[0]
 h:c.image.size[1]
 f:current_frame.text
 i:c.image
 me.image.paste[i.copy[(w%sprite_width.text*floor f,floor sprite_width.text*floor f / w) (sprite_width.text,sprite_height.text)]]
 current_frame.text:frames.text%f+fps.text*1/60
end

The only thing I'm missing is for the contraption to resize itself to fit the configured sprite size.

EDIT: I made the prototype resizable, and it's working now. The problem is that the state is shared between different instances of the same prototype, so I'm obviously doing something very wrong here.

Developer(+1)

Don't worry- it's quite normal to stash contraption state in auxiliary hidden widgets. The contents of such widgets is distinct between contraption instances, but the background image of contraptions is shared among all instances; pasting directly onto the contraption background will cause issues if you have more than one sprite. The conventional solution would be to add a canvas widget to the contraption and draw on that instead. You'll also need to configure non-zero margins for the contraption to ensure that the canvas automatically stretches and resizes properly along with it. Both the contraption instance and the canvas will need to be set to "show transparent" if you want objects behind the sprite to show through.

Since you intend to redraw the contents of the canvas frequently, you may be able to mark it as volatile, which will ensure that having many sprite instances won't bloat deck size with unnecessary copies of sprite frames. If you aren't already doing something similar, you may find it useful to mark the canvas as "animated" so that it will automatically bubble view[] events to the contraption instance at 60fps and "locked" so that it doesn't inherit the default click-and-drag-to-scribble behavior of canvases. (Be aware that "me" will be bound to the original target of the view[] event, not necessarily the contraption itself!)

The Sokoban example deck contains a contraption called "mover" which works similarly in some ways to your "sprite" design, and the Path example deck has another variant called "follower". You might find these useful to reference.

Does any of this point you in the right direction?

(+1)

Yes, this is all very useful. Thank you. I will probably end up using zazz for animated sprites (unless I end up needing non-looping sprites, or fancy events that report when animations end, and things like that). But it's very useful to be able to understand the subtleties of working with contraptions. Decker is awesome, but a bit overwhelming at the beginning.

(+2)

Hey, IJ and Decker community

I've been messing with hyperlinks. I'm trying without much success to come up with a way to make a functional hyperlink (string + link event) show up inside a field through coding alone. Like, say, opening up the listener and making the word "Apple" show up in a field, functioning as a link to the Apple card.

Is there a way to do it?

Thanks :)

Developer(+2)

When a field is configured to display "Rich Text", it can contain text spans that are links, use different fonts, or include inline images. Rich Text is described in the reference manual here.

Rich Text is represented as a Lil table, and can be constructed like any other table so long as it has the appropriate columns. For example,

myField.value:insert text font arg with
 "Apple" "" "theAppleCard"
end

Remember, of course, that fields need to be locked for their hyperlinks to be clickable. Unless you define your own link[] handler for a field, clicking a link will call the go[] built-in function with that "arg" value, which conveniently serves either to navigate to cards by name or to prompt the user to open URLs in a new browser tab.

The "RText" utility interface (linked above) offers a number of convenience functions for creating and manipulating Rich Text tables. The "rtext.make[]" function offers a more concise alternative to the above:

myField.value:rtext.make["Apple" "" "theAppleCard"]

In the interactive docs for a variety of modules I use this sort of approach to automatically generate the index in the title card's view[] event handler:

bullet:image["%%IMG0AAYADQAAAAB49Pz8/HgAAAA="]
i:select c:key t:value..widgets.title.text where value..widgets.title from deck.cards
index.value:raze each row in rows i
 rtext.make["" "" bullet],
 rtext.make["  "],
 rtext.make[("%s\n" format row.t) "mono" row.c]
end 

Does that help?

(+1)

Yes! That does help a lot! Thank you sou much :)

I'll just ask you my next doubt already, if you don't mind.

I'm fiddling with the Mini Twine deck using a twee file of an old Twine game I made. What I wanted to do is keep a log field with the narrative text as the player makes their choices, but so far I'm having trouble leaving the link lines out of it. 

Example:

How do I break the game field string and leave the verb choices out of the log field? Like so (tampered with example):


Thank you again!

Developer (1 edit) (+3)

I think this would be difficult in the general case without making some assumptions about the structure of passages. Hyperlinks can appear anywhere within the text of a Twine passage, so it's not simply a matter of hiding or trimming off a suffix of the output to remove the listed "verbs".

If there's demand for it (and it looks like there might be) I could look into developing a more complete and robust .twee manipulation module for Decker, and maybe even a story mode specifically designed for interoperation with Lil. I don't think I'll have time for it this month, though.

It's not quite what you're asking for, but you might be able to find some useful ideas in this thread where I discuss parser-based IF systems; my example deck features an output log which distinguishes user input from responses with bold/plain fonts.

(+1)

that's fine! we're in no hurry at all :) it's great to see decker growing with each question brought here by its users. have a great december!

(2 edits) (+1)

Hey, just wanted to make sure I'm on the right track here with a speech bubble component I'm working on:


The roundrect is manually drawn on the contraption's background, with margins set to allow shrinking and growing. The bubble's "tail" is a volatile canvas because the tail needs to change both image (mirror left/right) and position. Right now I'm making those changes from the view handler:

on view do
 left: state.text in "se","w"
 mirror: state.text in "sw","w"
 if left
  me.margin: 7,25,7,7
  tail.pos: 12,tail.pos[1]
 else
  me.margin: 7,7,25,7
  tail.pos: (me.size[0]-24),tail.pos[1]
 end
 img: image["%%IMG2..."] # tail image initially points to the right
 if mirror
  img.transform["horiz"]
  end
 tail.paste[img]
end

I call view[] from each of the set_ handlers, so if the text changes or the direction changes, everything gets repositioned and redrawn. This mostly works, but it has some weird side effects during editing. For example, if I resize the widget, the tail jumps back to its default position (its position in the prototype). And sometimes the volatile tail canvas clears itself during editing, and won't be redrawn until I switch to Interact mode.

Am I on the right track here? Is there something else that should be calling view[]? Or is there a better approach you would recommend?