Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

My first card

A topic by Kartik Agaram created 3 days ago Views: 396 Replies: 11
Viewing posts 1 to 3
(+3)

Nothing great, I made dancing text that randomly changes case while learning Decker. I don't know the best way to share it, but here's my code (just at the card level), I'd appreciate comments and critique:

local s: "abcdef"
on view do
  if ! sys.frame < next_frame.text
   canvas.clear[]
   local t: flip (list "" split s),(list random[2 count s])
   local y: "" fuse each u in t if u[1] "%u" else "%s" end format u[0] end
   canvas.text[y (50,50, 200,100)]
   next_frame.text: sys.frame + 5
  end
  go[card]
end
(+3)

Probably the best way to share it is to save out the deck as html and upload it somewhere - neocities is a good option for simple web hosting if you don't otherwise have something available.

In the meantime, if anyone wants to see this in action, just paste the code into the card script and make a canvas on screen named "canvas".

It's a cool effect that I can see people getting some use of - it could be a fun challenge to turn it into a module so it can be more easily reused, as described in the modules section of the doco. Or maybe as a contraption?

Developer (2 edits) (+3)

If you use "Card -> Copy Card" from the menu, Decker will store a text-based serialization of the card, its widgets, and any fonts or contraption prototypes those widgets reference in the clipboard. This also works with "Edit -> Copy Widgets" for a group of widgets. As in the examples in The Contraption Bazaar, this can be a handy way of sharing a chunk of a project that Decker users can immediately paste into their own deck and try out. Here's how that might look for your example:

%%CRD0{"c":{"name":"card1","script":"local s: \"abcdef\"\non view do\n  if ! sys.frame < next_frame.text\n   canvas.clear[]\n   local t: flip (list \"\" split s),(list random[2 count s])\n   local y: \"\" fuse each u in t if u[1] \"%u\" else \"%s\" end format u[0] end\n   canvas.text[y (50,50, 200,100)]\n   next_frame.text: sys.frame + 5\n  end\n  go[card]\nend","widgets":{"canvas":{"type":"canvas","size":[100,100],"pos":[206,121],"volatile":1,"scale":1},"next_frame":{"type":"field","size":[100,20],"pos":[63,128],"volatile":1}}},"d":{}}

(Note that sometimes the itch.io WYSIWYG editor will scramble code that it confuses for embedded HTML; this can be quite annoying, but is not a bug in Decker.)

I have marked the canvas as "volatile" to keep the snippet smaller, since the script clears and repaints it constantly.

There are a few ways we can simplify and improve the script:

  • Instead of using a "next_frame" field to keep track of the most recent refresh, we could repaint when sys.frame modulo 5 is 0; then there's no extra state to worry about. This also avoids the latent problem that saving a deck (and thus serializing the field) may mean that the number in that field is much larger than sys.frame on the next work session, causing the animation to appear to "freeze" for an arbitrary amount of time before playing. This can be avoided (as in the snippet above) by also making the next_frame field volatile.
  • By marking the canvas as "animated", Decker will automatically send "view" events to the canvas, allowing us to move the script within the canvas and make it self-contained, referring to itself as "me". Using a card-level event pump may be easier in some cases; use whatever approach you prefer.
  • If you want to "zipper" a pair of lists into a list of pairs, you can use the Lil "join" operator instead of transposing a pair of lists:

 (1,2,3) join "ABC"
((1,"A"),(2,"B"),(3,"C"))
 local s: "abcdef" s join random[2 count s]
(("a",0),("b",1),("c",0),("d",0),("e",0),("f",0))
  • In this situation, however, we don't actually need to build that list of pairs; we could instead randomly choose a case-formatting string while iterating over the letters. The random[] function can be called with any list, producing a random choice from that list:
 random["%u","%l"]
"%u"
 random["%u","%l"]
"%l"
  • canvas.text[], like many functions in Lil which expect a string argument, will implicitly fuse lists of strings together, so you don't need to do so explicitly.
  • Assuming the goal was to center the string within the canvas, you can do this generically by using the third "anchor" parameter for canvas.text[].

With these tweaks, the script would be:

local s: "abcdef"
on view do
 if !5%sys.frame
  local y:each c in s random["%u","%l"] format c end
  me.clear[]
  me.text[y me.size/2 "center"]
 end
end

If we wanted to "code golf" there are a few more ways we could shorten this script, but whether you'd consider them improvements is subjective:

  • The "local" keyword for variable declarations is optional; we only need it explicitly as documentation of intent to human readers and to force Lil to shadow, rather than inherit, variable declarations of the same name in a higher scope. In this situation it doesn't change the semantics of the program.
  • The variable "s" is now used only once, so we can inline its value.
  • Likewise the variable "y" is now used only once, so we can inline the entire loop into its use site.
on view do
 if !5%sys.frame
  me.clear[]
  me.text[
   each c in "abcdef" random["%u","%l"] format c end
   me.size/2 "center"
  ]
 end
end

And as a widget snippet attached to a volatile canvas:

%%WGT0{"w":[{"name":"canvas","type":"canvas","size":[100,100],"pos":[206,121],"animated":1,"volatile":1,"script":"on view do\n if !5%sys.frame\n  me.clear[]\n  me.text[\n   each c in \"abcdef\" random[\"%u\",\"%l\"] format c end\n   me.size/2 \"center\"\n  ]\n end\nend","scale":1}],"d":{}}

Does that help?

(+1)

Thanks a lot! Even your "golfed" version is much more readable than my

(+1)

Is there a way in the UI to mark a card as animated, or do I need to do that by editing the .deck file? I don't see it in Card properties..

Developer (2 edits) (+2)

A card itself cannot be marked animated; only widgets.

With the "Widgets" tool active, you can set this property from the "Widgets -> Animated" menu item.

If you want an "animated card" you can either use a self-refreshing approach with go[card] like in your original script, or add an animated widget to the card that acts as an "event pump" by allowing the view[] event to bubble up to the card. The Decker Sokoban example uses something similar to the latter approach.

(+1)

Oh I misread your comment and then also the code. Got it now.

(3 edits) (+2)

I gravitated toward editing the script for the whole card because that's the model I'm used to. I started out trying to draw directly on the card, and later realized I needed to add widgets to get the script to work. Procedural drawing must go on a canvas, is that right?

It's easy to bounce between editing and running the whole card by hitting ctrl+e and escape. Do you use any hotkeys for switching between editing a specific widget and running the whole card/deck?

One thing I didn't share was that the string I make to dance is much longer and wraps. And I was using the 4-element position when drawing text to make it wrap. This works beautifully now:

local margin:15
me.text[y (margin,margin,me.size-margin*2)]

I don't quite see how to make center alignment work with 4-element positions and text wrap. This doesn't do any centering or wrapping at all:

me.text[y (me.size/2, me.size) "center"]

Does text wrapping work with center alignment?

Does the choice of anchor affect how the `pos` argument of canvas.text is interpreted? The only way your code makes sense to me is if I assume that the pos is specifying the center of where to draw the text and not the start..

Developer(+3)

It is possible to directly manipulate the card background (card.image) instead of using a canvas widget, but it's limited in some ways; see the Image Interface documentation. If you have a hand-drawn card background this can also make it easy to accidentally destroy your doodles, so use caution! For a quite elaborate example of such an animation, try clicking the first "e" in "Decker" on the title screen of the guided tour deck.

When I'm frequently making changes to a script I do often defer logic to the card-level script for editing convenience, possibly by giving the widget a stub script like:

on click do
 reset_game[]
end

With "reset_game" defined on the card or deck script. It is possible to quickly jump to a widget's script by enabling "File -> X-Ray Specs" from within the script editor and then ctrl+clicking in the widget's bounding box to view its script; ctrl+clicking outside any widgets switches to the card script. This is a very useful tool for getting a "bird's eye view" of what's happening on a card. Alternatively you could use the F-keys on your keyboard to switch between Interact and Widgets mode (F1 and F2, respectively), click a widget, and press cmd/ctrl+R to edit its script.

I think in your example you may be tripping over Lil's order of operations (or lack thereof). Without parentheses, expressions are carried out strictly right-to-left. Parens may be useful for visually grouping arguments, but do not inherently denote lists. The following expressions are equivalent:

(me.size/2, me.size)
me.size/(2, me.size)
me.size/2,me.size

I think you intended:

(me.size/2),me.size

When the position for canvas.text[] is specified as an (x,y) pair, anchors control the positioning of the text relative to that point: "center" means the string will be centered upon that point.

When the position for canvas.text[] is instead specified as a (x,y,w,h) rectangle, anchors control the alignment and justification of text within that rectangle. Thus, to wrap and center text within the bounds of the canvas you'd want something like 

canvas.text[somestring 0,0,canvas.size "center"]

Or, incorporating a margin,

local margin:15
canvas.text[somestring margin,margin,me.size-margin*2 "center"]


How's that?

(1 edit) (+3)

Thanks! That's perfect. Just the F1/F2 hotkeys are a huge improvement.

The thing that's been tripping me up the most: "whitespace" (the boundary between function args)  has lower precedence than everything else, including comma. The right-to-left precedence only happens within each function argument, right? So in this line:

canvas.text[somestring margin,margin,me.size-margin*2 "center"]

..the 3 arguments are first split up, and then operations within them are run independently. Is that an accurate mental model?

Developer(+2)

In C, expressions are explicitly terminated with a semicolon (;). In Lil, expressions are implicitly terminated when they are no longer "incomplete"; that is, their rightmost token is not a primitive or other syntactic form which "expects" an additional subexpression to the right. Outside comments and string literals, all whitespace in Lil is equivalent, so we can sequence "statements" with newlines between them:

ax:2+3
bx:ax*5

...or we could run them together on a single line:

ax:2+3 bx:ax*5

In this particular example we could even remove the space separating the two statements, because Lil identifiers may not begin with a number; splitting the "3" from the "bx" is unambiguous. I do not recommend using such a style in practice:

ax:2+3bx:ax*5

Lil interprets all three variations as the sequence of tokens (shown separated by whitespace):

ax : 2 + 3 bx : ax * 5

Whitespace is only required when Lil would otherwise "see" a single token but multiple tokens are intended:

onfoo   # reference to a variable named "onfoo"
on foo  # the keyword "on" followed by the identifier "foo"

The bracketed argument list for a function call is not a special case; just like the "do...end" of a function body, it accepts any sequence of expressions, each implicitly terminated. If the argument-expressions are complex, it may be stylistically clearer to parenthesize them and/or use whitespace to produce a visually apparent grouping, but this does not change how Lil interprets the expression. Consider:

x:11
y:first range 10
z:3*5
foo[x y z]

Versus the following equivalent alternatives:

foo[11 first range 10 3 * 5]
foo[(11) (first range 10) (3 * 5)]
foo[
 11
 first range 10
 3 * 5
]

Or, my preference for this particular situation,

foo[11 (first range 10) 3*5]

The comma operator (,) should always be understood as a primitive operator, rather than special syntax; it's of the same nature as the operators plus (+) or times (*).

Does that help clarify?

(1 edit) (+2)

馃挴 Thank you.