Anotomy of a Card in TIC-80

Another Ludum Dare has come and gone! This time I teamed up with Whaies and we created a game about summoning lawyers and other ... things ... to court. You can play it here.

video11.gif


Upcard write is written in the Janet Programming Langauge using tic80. I had a lot of fun figuring out the code for Cards. I wanted them to lift, rotate, and shuffle like the real thing and I think the solution I came up with in TIC-80 was pretty neat! Here's a technically rundown

Anybody familiar with TIC-80 will know the basic way to draw a sprite is with the spr function. You give it an index in the sprite sheet and [x,y] coordinates and your done. But what if you want to do something crazy like rotate the sprite?! For that we must somehow implement affine transformation.

The way everyone in the TIC-80 community has done this is using the ttri, textured triangle, function. I actually wrote about this a couple years ago! The basic idea is to use 2 textured triangles to draw a rectangle, and by manipulating the 4 coordinates using math you can do all sorts of fun transformations. Here's what that look likes like in Janet.

```

adapted from https://cxong.github.io/tic-80-examples/affine-sprites

(defn deg2rad [theta] (* theta PIOVER180)) (defn rad2deg [theta] (* theta ONEEIGHTYOVER_PI))

(defn aspr/rotate [x y ca sa] [(- (* x ca) (* y sa)) (+ (* x sa) (* y ca))])

(defn aspr [x y &named u1 v1 texsrc chromakey sx sy flip rotate w h ox oy shx1 shy1 shx2 shy2] (default u1 0) (default v1 0) (default texsrc TEXSRC_SPR) (default chromakey -1) (default sx 1) (default sy 1) (default flip 0) (default rotate 0) (default w 1) (default h 1) (default ox (math/floor (/ (* w 8) 2))) (default oy (math/floor (/ (* h 8) 2))) (default shx1 0) (default shy1 0) (default shx2 0) (default shy2 0)

(let [sx (if (= 1 (% flip 2)) (* -1 sx) sx) sy (if (> flip 2) (* -1 sy) sy) ox (* -1 ox sx) oy (* -1 oy sy)

# Shear & rotate
shx1 (* -1 shx1 sx)
shy1 (* -1 shy1 sy)
shx2 (* -1 shx2 sx)
shy2 (* -1 shy2 sy)
rr rotate
ca (math/cos rr)
sa (math/sin rr)
[rx1 ry1] (aspr/rotate (+ ox shx1) (+ oy shy1) ca sa)
[rx2 ry2] (aspr/rotate (+ ox shx1 (* w 8 sx)) (+ oy shy2) ca sa)
[rx3 ry3] (aspr/rotate (+ ox shx2) (+ oy shy1 (* h 8 sy)) ca sa)
[rx4 ry4] (aspr/rotate (+ ox shx2 (* w 8 sx)) (+ oy shy2 (* h 8 sy)) ca sa)
[x1 y1] [(+ x rx1) (+ y ry1)]
[x2 y2] [(+ x rx2) (+ y ry2)]
[x3 y3] [(+ x rx3) (+ y ry3)]
[x4 y4] [(+ x rx4) (+ y ry4)]

# UV coords
u2 (+ u1 (* w 8))
v2 (+ v1 (* h 8))]
(tic80/ttri x1 y1 x2 y2 x3 y3 u1 v1 u2 v1 u1 v2 texsrc chromakey)
(tic80/ttri x3 y3 x4 y4 x2 y2 u1 v2 u2 v2 u2 v1 texsrc chromakey)))

```

Sidenote: that this implementation does not use matrices, which maybe means "affine" is the wrong technical term?

With this function we can draw a sprite and rotate/scale it however we need.

upcard-writ-1.gif

Awesome!

HOWEVER, there's one problem. This rotates sprites, but how do we transform text? I didn't want to waste space in the spritesheet embedding letters, and TIC-80's print function definitely does not support rotating.

The Solution is to ~~abuse~~ use VRAM. The video ram in TIC-80 is "double banked", which means you basically have 2 screens which are drawn over each other to work with. Conveniently, ttri supports pulling the texture from various different places. Those are the spritesheet, tilemap, and the VRAM you're currently not drawing to. With this we can

  1. switch to
  2. draw out the entire card using boring functions. Don't worry about any sort of rotation or scaling at this point.
  3. switch to VRAM 1
  4. clear the screen to hide everything on VRAM 0
  5. use our custom ssprfunction with VRAM 0 as our texture source to draw our card with scale and rotation!

Here's the annotated card drawing source code in Upcard Writ

```

begin drawing in vbank 0

(tic80/vbank 0) (tic80/cls 0)

draw empty base of card, which is in the map

I palette swap based on its type... maybe more on that in a future blog post

(with-pallete-swap (match (character :integrity) :virtuous {5 14} :pragmatic {5 9} :sleazy {5 4}) (tic80/map 0 0 6 10 0 0 0))

only face up cards need to have their details drawn

(when flipped? # print out title (print-centered (character :name) 24 4 1 false 1 true) (print-centered (character :name) 24 3 7 false 1 true)

  # draw the cards picture if it has one
  (when (character :sprite)
(tic80/rect 0 11 48 32 (character :sprite-bg))
(tic80/spr (character :sprite) ;(character :sprite-args))
(tic80/rectb 0 11 48 32 7))

  # draw the cards "resources"
  # ... theres some annoying logic to split it into 2 rows
  (loop  [[i [mod ev]] :pairs (array/slice (character :evidence) 0
                 (min 2 (length (character :evidence))))]
(evidence-spr ev (+ 8 (* i 22)) 43)
(tic80/spr (match mod :+ 1 :- 2) (+ 3 (* i 22)) 43 0))
  (when (> (length (character :evidence)) 2)
(loop  [[i [mod ev]] :pairs (array/slice (character :evidence) 2)]
  (evidence-spr ev (+ 8 (* i 22)) 60)
  (tic80/spr (match mod :+ 1 :- 2) (+ 3 (* i 22)) 57 0))))

some other stuff happens...

eventually we switch to vbank 1

(tic80/vbank 0) (tic80/cls 0)

and Finally draw the card with rotation and scale!

(aspr ;(round (- (self :pos) [0 (self :height)])) :u1 0 :v1 0 :w 6 :h 10 :texsrc TEXSRC_VRAM :sx (self :scale) :sy (self :scale) :rotate (self :rotation)) ```

Phew! Here's the results (I've removed a screen clear so you can see both vrams).

upcard-writ-2.gif

If nothing else, maybe all this has inspired you to take a look at tic80, its pretty neat.

Regardless, thanks for reading!