Code examples and snippets - nesbox/TIC-80 GitHub Wiki
Code Snippets
Utilities
- Copy Sprite Data to a Table
- Load string palette
- Inc and Dec
- Trace all global variables
- Write and read function from memory
- Interpolation functions
- Normally distributed random number generator
- Perlin Noise
- Rescale RangeA to RangeB
- Lapse of time spent at function
- Using the require function to load external code
Drawing
- Draw ellipse function on Moonscript
- Draw Bézier curves on Moonscript
- Set/Get spritesheet pixel
- Sky gradient
- Clear map
- Palette swapping
- pal function
- palette functions (JavaScript)
- Example Edit the palette using poke
- Rainbow color
- Texrect
- Draw hanging cable between 2 points
- ttri x,y,z Rotation
Animation / Effects
Sound / Music
- Mute one channel of a music track
- Set stereo volume for a channel
- Set sound register volume for a channel
Input
Other
Uncategorized
For older versions of TIC-80
The latest version of TIC-80 supports all of these things with built-in API functions.
- Mouse Scroll
- Peek and Poke Nibbles
peek4
andpoke4
- Sprite Flags
fset
andfget
- Draw circles
circ
andcircb
Copy Sprite Data to a Table
Sometimes you might want to copy sprite data to a table for manipulation, rather than display it on the screen directly. This example shows how to get pixel data for a given sprite by supplying its ID, width and height:
-- Parameters sprite number, width and height of the sprite(s).
-- returns a table containing pixel data
function gspr(num, w, h)
local addr=0x4000
local offset = w * num % 128
local hoffset= h * math.floor((w*num)/128)
local g = {}
for x=0,w-1 do
g[x] = {}
for y=0, h-1 do
g[x][y] = peek4((addr + (x//8+y//8*16)*32) *2+x%8+y%8*8)
end
end
return g
end
Load String Palette
Useable with palette string defined in palette page or copied from the Sprite Editor.
-- script: lua
--Load palette string
function loadPalette(palette)
for i=0,15 do
r=tonumber(string.sub(palette,i*6+1,i*6+2),16)
g=tonumber(string.sub(palette,i*6+3,i*6+4),16)
b=tonumber(string.sub(palette,i*6+5,i*6+6),16)
poke(0x3FC0+(i*3)+0,r)
poke(0x3FC0+(i*3)+1,g)
poke(0x3FC0+(i*3)+2,b)
end
end
// script: js
function loadPalette(string){
for(var i=0;i<16;i++){
poke(0x03FC0+(i*3),parseInt(string.substr(i*6,2),16));
poke(0x03FC0+(i*3)+1,parseInt(string.substr((i*6)+2,2),16));
poke(0x03FC0+(i*3)+2,parseInt(string.substr((i*6)+4,2),16));
}
}
Clear map
Use sync(4,0,true)
to save the editing of the map to cart.
function clearEntireMap(){
for(var y=0;y<136;y++){
for(var x=0;x<240;x++){
mset(x,y,0);
}
}
}
function clearMap(mx,my){ //Since there are 8x8 maps.
mx=Math.floor(mx)*30;my=Math.floor(my)*17;
for(var y=0;y<17;y++){
for(var x=0;x<30;x++){
mset(mx+x,my+y,0);
}
}
}
function clearEntireMap()
for y=0,135 do
for x=0,239 do
mset(x,y,0)
end
end
end
Inc and Dec
These functions increment and decrement variables by 1. Works only for globals. The use is for minifying the code when you have many somevar = somevar+1 calls which can waste many characters when names are very descriptive such as enemy_health, while still keeping the code readable.
function inc(v)
_G[v] = _G[v] + 1
end
function dec(v)
_G[v] = _G[v] - 1
end
With MoonScript you can use the +=
and -=
operators.
Glitch
-- 'Change screen offset in every scanline' demo
-- author: Vadim
shake=0
d=4
function TIC()
if btnp()~=0 then shake=30 end
if shake>0 then
poke(0x3FF9+1,math.random(-d,d))
shake=shake-1
if shake==0 then memset(0x3FF9,0,2) end
end
cls(12)
print("PRESS ANY KEY TO GLITCH!",54,64)
end
function BDR(row)
if shake>0 then
poke(0x3FF9,math.random(-d,d))
end
end
Palette swapping
-- swap c0 and c1 colors, call pal() to reset
function pal(c0,c1)
if(c0==nil and c1==nil)then for i=0,15 do poke4(0x3FF0*2+i,i)end
else poke4(0x3FF0*2+c0,c1)end
end
// script: js
function pal(c0,c1) {
if (arguments.length==2)
poke4(0x3ff0*2+c0,c1)
else
for (var i=0;i<16;i++)
poke4(0x3ff0*2+i,i)
}
Slightly different approach in Janet which uses a macro and can handle any number of swaps.
(defmacro with-pallete-swap [pallete & body]
"""
Swap the color pallete during the given body.
Pallete is a dict of {from to} color ids.
"""
(with-syms [$from $to]
~(do
# apply the theme
(loop [[,$from ,$to] :pairs ,pallete]
(tic80/poke4 (+ (* 0x3FF0 2) ,$from) ,$to))
,;body
# remove theme
(loop [[,$from ,$to] :pairs ,pallete]
(tic80/poke4 (+ (* 0x3FF0 2) ,$from) ,$from)))))
(with-pallete-swap {0 1
2 3}
(tic80/map 0 0 8 8))
Screen wave and sky gradient
-- title: screen wave and sky gradient snippet
-- author: Darkhog
-- TIC function cut because it doesn't matter here.
-- This code can be used for e.g. water or some hazy/hot environments.
wavelimit = 136/2
function scanline(row)
-- skygradient
poke(0x3fc0,190-row)
poke(0x3fc1,140-row)
poke(0x3fc2,0)
-- screen wave
if row>wavelimit then
poke(0x3ff9,math.sin((time()/200+row/5))*10)
else
poke(0x3ff9,0)
end
end
pal function
--sets the palette indice i to specified rgb
--or return the colors if no rgb values are declared.
function pal(i,r,g,b)
--sanity checks
if i<0 then i=0 end
if i>15 then i=15 end
--returning color r,g,b of the color
if r==nil and g==nil and b==nil then
return peek(0x3fc0+(i*3)),peek(0x3fc0+(i*3)+1),peek(0x3fc0+(i*3)+2)
else
if r==nil or r<0 then r=0 end
if g==nil or g<0 then g=0 end
if b==nil or b<0 then b=0 end
if r>255 then r=255 end
if g>255 then g=255 end
if b>255 then b=255 end
poke(0x3fc0+(i*3)+2,b)
poke(0x3fc0+(i*3)+1,g)
poke(0x3fc0+(i*3),r)
end
end
pal = (i=0, r, g, b) ->
mx = math.max
mn = math.min
fl = math.floor
i = fl(mx(0, mn(15, i)))
if r==nil and g==nil and b==nil
return peek(0x3fc0+(i*3)), peek(0x3fc0+(i*3)+1), peek(0x3fc0+(i*3)+2)
r = fl(mx(0, mn(255, r or 0)))
g = fl(mx(0, mn(255, g or 0)))
b = fl(mx(0, mn(255, b or 0)))
poke(0x3fc0+(i*3), r)
poke(0x3fc0+(i*3)+1, g)
poke(0x3fc0+(i*3)+2, b)
function pal(i,r,g,b){
i=(i|0)*3;
if(r===undefined&&g===undefined&&b===undefined){
return[peek(16320+i),peek(16321+i),peek(16322+i)];
}else{
poke(16320+i,r);poke(16321+i,g);poke(16322+i,b);
}
}
Shake screen
-- 'shake screen demo' demo
-- author: Vadim
shake=0
d=4
function TIC()
if btnp()~=0 then shake=30 end
if shake>0 then
poke(0x3FF9,math.random(-d,d))
poke(0x3FF9+1,math.random(-d,d))
shake=shake-1
if shake==0 then memset(0x3FF9,0,2) end
end
cls(12)
print("PRESS ANY KEY TO SHAKE THE SCREEN!",24,64)
end
Trace all global variables by Al Rado
-- title: Trace all global variables
-- author: Al Rado 28.02.2017
-- desc: Standard Lua demo
-- script: lua
-- input: gamepad
-- pal: DB16
local seen={}
function dump(t,i)
seen[t]=true
local s={}
local n=0
for k in pairs(t) do
n=n+1 s[n]=k
end
table.sort(s)
for k,v in ipairs(s) do
trace(i..v)
v=t[v]
if type(v)=="table" and not seen[v] then
dump(v,i.."\t")
end
end
end
cls()
trace("---------------------")
trace("All global variables:")
dump(_G,"")
trace("---------------------")
print("See all global variables in console!")
function TIC()
exit()
end
Simple Sprite Flags
Update: fset and fget are now built-in API functions and the Sprite Editor also supports setting flags during editing.
FLAGS={}
function fset(i,f,v)
local val=FLAGS[i] or 0
local mask=1<<f
FLAGS[i]=v
and val|mask
or val&~mask
end
function fget(i,f)
local val=FLAGS[i] or 0
return val&(1<<f)~=0
end
fset(5,5,true)
trace(fget(5,5))
fset(5,5,false)
trace(fget(5,5))
cls()
function TIC()
end
Set/Get spritesheet pixel
The spritesheet isn't a 128x256 image in the memory, it's 512 consecutive 8x8 4 bit sprites. These functions map it to a 128x256 image.
-- set spritesheet pixel
function sset(x,y,c)
local addr=0x4000+(x//8+y//8*16)*32 -- get sprite address
poke4(addr*2+x%8+y%8*8,c) -- set sprite pixel
end
-- get spritesheet pixel
function sget(x,y)
local addr=0x4000+(x//8+y//8*16)*32 -- get sprite address
return peek4(addr*2+x%8+y%8*8) -- get sprite pixel
end
function sget(x,y){
return peek4(32768+56*(x/8|0)+960*(y/8|0)+x+8*(y|0));
}
function sset(x,y,c){
poke4(32768+56*(x/8|0)+960*(y/8|0)+x+8*(y|0),c);
}
;parlortricks
;script:fennel
(fn sget [x y]
"get sprite sheet pixel"
(local addr (+ 0x4000 (* (+ (// x 8) (* (// y 8) 16)) 32)))
(peek4 (+ (* addr 2) (% x 8) (* (% y 8) 8))))
;parlortricks
;script:fennel
(fn sset [x y c]
"set sprite sheet pixel"
(local addr (+ 0x4000 (* (+ (// x 8) (* (// y 8) 16)) 32)))
(poke4 (+ (* addr 2) (% x 8) (* (% y 8) 8)) c))
Draw circle function
made by Raidez
function _360(x,y,r,color,fill)
if not fill then
for a=1,360,1 do --draw outline only
dx=math.cos(a)*r+x
dy=math.sin(a)*r+y
pix(dx,dy,color)
end
else
for r=r,1,-1 do --draw circle for each radius (for fill circle)
for a=1,360,1 do
dx=math.cos(a)*r+x
dy=math.sin(a)*r+y
pix(dx,dy,color)
end
end
end
end
function _bresenham(x,y,r,color)
px=0
py=r
m=5-4*r
while(px<=py) do
pix((px+x),(py+y),color)
pix((py+x),(px+y),color)
pix((-px+x),(py+y),color)
pix((-py+x),(px+y),color)
pix((px+x),(-py+y),color)
pix((py+x),(-px+y),color)
pix((-px+x),(-py+y),color)
pix((-py+x),(-px+y),color)
if m>0 then
py=py-1
m=m-8*py
end
px=px+1
m=m+8*px+4
end
end
function _andres(x,y,r,color,fill)
px=x
py=y
d=r-1
a=r
--a=r-1
b=0
while(a>=b) do
if not fill then
pix((px+b),(py+a),color)
pix((px+a),(py+b),color)
pix((px-b),(py+a),color)
pix((px-a),(py+b),color)
pix((px+b),(py-a),color)
pix((px+a),(py-b),color)
pix((px-b),(py-a),color)
pix((px-a),(py-b),color)
else
line((px+b),(py-a),(px+b),(py+a),color)
line((px+a),(py-b),(px+a),(py+b),color)
line((px-b),(py-a),(px-b),(py+a),color)
line((px-a),(py-b),(px-a),(py+b),color)
end
if d>=2*b then
d=d-2*b-1
b=b+1
elseif d<2*(r-a) then
d=d+2*a-1
a=a-1
else
d=d+2*(a-b-1)
a=a-1
b=b+1
end
end
end
function _axis(cx,cy,r,color,fill)
if not fill then
for y=-r,r,1 do --outline
for x=-r,r,1 do
if (x*x+y*y)<=(r*r) then
pix(cx+x,cy+y,color)
end
end
end
r=r-1
for y=-r,r,1 do --unfill
for x=-r,r,1 do
if (x*x+y*y)<=(r*r) then
pix(cx+x,cy+y,-1)
end
end
end
else
for y=-r,r,1 do
for x=-r,r,1 do
if (x*x+y*y)<=(r*r) then
pix(cx+x,cy+y,color)
end
end
end
end
end
function circ(x,y,r,color,algo)
if algo=="360" then
--don't use it, it's false/incomplete
--_360(x,y,r,color,true)
elseif algo=="bresenham" then
_bresenham(x,y,r,color)
_andres(x,y,(r),color,true)
elseif algo=="andres" then
_andres(x,y,r,color,true)
elseif algo=="axis" then
_axis(x,y,r,color,true)
end
end
function circb(x,y,r,color,algo)
if algo=="360" then
--don't use it, it's false/incomplete
--_360(x,y,r,color,false)
elseif algo=="bresenham" then
_bresenham(x,y,r,color)
elseif algo=="andres" then
_andres(x,y,r,color,false)
elseif algo=="axis" then
_axis(x,y,r,color,false)
end
end
function TIC()
cls()
circb(100,60,40,14,"bresenham")
end
Draw ellipse function on Moonscript
made by harraps
-- script: moon
--function to draw an ellipse
--x,y : position of the ellipse
--A,B : major and minor radius
--ang : angle of the ellipse in radian
--col : color of the filled ellipse
ell=(x,y,A,B,ang,col)->
--angles used
cos=math.cos ang
sin=math.sin ang
cos2=math.cos ang+math.pi*0.5
sin2=math.sin ang+math.pi*0.5
--vectors in euclidian space
ux=A*cos
uy=A*sin
vx=B*cos2
vy=B*sin2
--bounds of ellipse
w=math.sqrt ux*ux + vx*vx
h=math.sqrt uy*uy + vy*vy
--rect x-w+1,y-h+1,w*2,h*2,8
--for each pixel within the bounds
for x0=-w,w
for y0=-h,h
x1=( cos*x0 + sin*y0)/A
y1=(-sin*x0 + cos*y0)/B
if x1*x1 + y1*y1 < 1
pix x+x0,y+y0,col
Draw Bézier curves on Moonscript
made by harraps
-- title: bezier
-- desc: function to draw Bezier curves
-- script: moon
-- input: gamepad
class Point
new:(x,y)=>
@x=x
@y=y
Bezier =
-- simple line
linear:(t,p0,p1)->
return{
x:p0.x+t*(p1.x-p0.x)
y:p0.y+t*(p1.y-p0.y)
--z:p0.z+t*(p1.z-p0.z)
}
-- simple curve
quadratic:(t,p0,p1,p2)->
n =1-t
t0=n*n
t1=n*t*2
t2=t*t
return{
x:t0*p0.x+t1*p1.x+t2*p2.x
y:t0*p0.y+t1*p1.y+t2*p2.y
--z:t0*p0.z+t1*p1.z+t2*p2.z
}
--better curve
cubic:(t,p0,p1,p2,p3)->
n =1-t
t0=n*n*n
t1=3*n*n*t
t2=3*n*t*t
t3=t*t*t
return{
x:t0*p0.x+t1*p1.x+t2*p2.x+t3*p3.x
y:t0*p0.y+t1*p1.y+t2*p2.y+t3*p3.y
--z:t0*p0.z+t1*p1.z+t2*p2.z+t3*p3.z
}
class Linear
new:(col,seg,x0,y0,x1,y1)=>
@col=col
@seg=seg
@p0 =Point x0,y0
@p1 =Point x1,y1
draw:=>
t=0
pad=1/@seg
pp=@p0
for i=1,@seg
t+=pad
np=Bezier.linear t,@p0,@p1
line pp.x,pp.y,np.x,np.y,@col
pp=np
line pp.x,pp.y,@p1.x,@p1.y,@col
class Quadratic
new:(col,seg,x0,y0,x1,y1,x2,y2)=>
@col=col
@seg=seg
@p0 =Point x0,y0
@p1 =Point x1,y1
@p2 =Point x2,y2
draw:=>
t=0
pad=1/@seg
pp=@p0
for i=1,@seg
t+=pad
np=Bezier.quadratic t,@p0,@p1,@p2
line pp.x,pp.y,np.x,np.y,@col
pp=np
line pp.x,pp.y,@p2.x,@p2.y,@col
class Cubic
new:(col,seg,x0,y0,x1,y1,x2,y2,x3,y3)=>
@col=col
@seg=seg
@p0 =Point x0,y0
@p1 =Point x1,y1
@p2 =Point x2,y2
@p3 =Point x3,y3
draw:=>
t=0
pad=1/@seg
pp=@p0
for i=1,@seg
t+=pad
np=Bezier.cubic t,@p0,@p1,@p2,@p3
line pp.x,pp.y,np.x,np.y,@col
pp=np
line pp.x,pp.y,@p3.x,@p3.y,@col
c=Cubic 6,60, 0,0, 246,0, 0,136, 246,136
cls 1
c\draw!
export TIC=->
peek4
and poke4
Update: peek4 and poke4 are now built-in API functions.
// script: js
function peek4(address){
var value=peek(address>>1);
return address&1?value>>4:value&0x0f;
}
function poke4(address,value){
var tmp=peek(address>>1);
value&=0x0f;
poke(address>>1,address&1?(tmp&0x0f)|(value<<4):(tmp&0xf0)|value);
}
-- script: lua
function peek4(address)
local value=peek(address//2)
if(address&1)then
return value>>4
else
return value&0x0f
end
end
function poke4(address,value)
local tmp=peek(address//2)
value=value&0x0f
if(address&1)then
tmp=(tmp&0x0f)|(value<<4)
else
tmp=(tmp&0xf0)|value
end
poke(address//2,tmp)
end
BrainF**k on Moonscript
made by harraps
-- script: moon
-- title: brainfuck interpreter
-- author: by oSchyns
-- version 0.3.0
BF="++++ ++++"..
"[>+++++ +++++<-]"..
">++++.----- ----- -.--- ---."--..
--uncomment to test error output
--"<< force a tape outbound"
IN=""
--tape to perform computation
class Tape
new:(l,m)=>
@l=l --cell limit
@m=m --loop the finite tape
@t={} --tape
@h=0 --read head
--correct values
@l-=1 if @l~=nil
@m-=1 if @m~=nil
--rhead out of the bounds of the tape
unbound:=>
if @m~=nil then return @h>@m
@h<0
--init nil cell to zero
init:=> @t[@h]=0 if @t[@h]==nil
--move read head to the left
mvL:=>
@h-=1
if @m~=nil then if @h<0
@h=@m
--move read head to the right
mvR:=>
@h+=1
if @m~=nil then if @h>@m
@h=0
--decreament cell
decr:=>
@init!
@t[@h]-=1
if @l~=nil then if @t[@h]<0
@t[@h]=@l
--increament cell
incr:=>
@init!
@t[@h]+=1
if @l~=nil then if @t[@h]>@l
@t[@h]=0
--ins: insert value in cell
--out: get value from cell
ins:(i)=> @t[@h]=i if i~=nil
out: => @t[@h] or 0
--stack to hold indexes of brackets
class Stack
new: => @s={}
top: => @s[#@s]
pop: => @s[#@s]=nil
push:(i)=> @s[#@s+1]=i
--automaton to execute the program
class Automaton
new:(p,i,l,m)=>
@t=Tape l,m --tape
@s=Stack! --stack
@r=1 --read head
@p="" --program
@i=i --input
@o="" --output
@e=nil --error
--strip useless chars of program
for c in p\gmatch "[<>%+%-%[%]%.%,]+"
@p..=c
b=@check!
if b~=nil
@e="brackets mismatch at "..b
--check for brackets mismatch
check:=>
s=Stack!
for i=1,#@p
if @program(i)=='[' then s\push i
elseif @program(i)==']'
return i if #s.s<=0
s\pop!
return s\top! if #s.s> 0
return nil
--get char from input
input:=>
if @i==nil or @i=="" then return nil
c1=@i\byte! --1st char of i
@i=@i\sub 2 --remove 1st char
return c1
--output: add char to output
--program: get instruction at 'n'
--continue: continue execution
output: (n)=> @o..=string.char n
program:(n)=> @p\sub n,n
continue: => @r<=#@p and @e==nil
--find matching bracket
match:(b)=>
m=1
while m>0 and b<=#@p
b+=1
if @program(b)=='[' then m+=1
elseif @program(b)==']' then m-=1
return b
--opening bracket
open:=>
--jump to matching bracket
if (@t\out!)==0 then @r=@match @r
else @s\push @r
--closing bracket
clos:=>
if (@t\out!)==0 then @s\pop!
elseif @s\top! ~=nil then @r=@s\top!
--automaton's' execution step
step:=>
switch @program @r
when '<' then @t\mvL!
when '>' then @t\mvR!
when '-' then @t\decr!
when '+' then @t\incr!
when ',' then @t\ins @input!
when '.' then @output @t\out!
when '[' then @open!
when ']' then @clos!
@r+=1
if @t\unbound!
@e="tape outbound !"
--display to see the execution
class Display
new:(a,s=1,f=nil)=>
@a=a --automaton
@s=s/60 --number of update per second
@c=0 --counter
@f=3 --display format of cells
if f=="hexadecimal" then @f=2
elseif f=="character" then @f=1
--execute automaton all at once
execute:=>
while @a\continue!
@a\step!
@output!
--update automaton at given frame rate
update:=>
if @a\continue!
if @c<1 then @c+=@s
else
@c=0
@a\step!
@draw!
--draw automaton state and output
draw:=>
cls!
@tape 8, 2
@program 8,12
@output 4
--draw the tape
tape:(w=6,h=0,c=1,a=3)=>
rect 0,h,240,w,c
l=math.ceil (40/@f)*0.5
p=(w-6)*0.5
q=6*@f
[email protected]
d=15
if @f==2 then d=6
elseif @f==1 then d=3
rect l*q-d,h,6*@f,w,a
for [email protected],@a.t.h+l
unless i<0
c=15
c=14 if [email protected]
[email protected][i] or 0
if @f==3 then v="%3d"\format v
elseif @f==2 then v="%2x"\format v
elseif @f==1 then v=string.char v
print v,(i+b)*q-d,h+p,c
--draw the program
program:(w=6,h=6,c=1,a=3)=>
rect 0,h,240,w,c
rect 117,h, 6,w,a
p=(w-6)*0.5
[email protected]
for [email protected],@a.r+20
unless i<1
c=15
c=14 if [email protected]
v=@a\program i
print v,(i+b)*6-2,h+p,c
--print result of execution
output:(s=0)=>
i=s
[email protected].."\n"
--print each line
for l in o\gmatch "[%S \t]*\n"
print l,0,i*6
i+=1
break if i==21 --no more room
--print error
unless @a.e==nil
print @a.e,0,130,6
--create automaton 'TM' using
--program : BF
--input : IN
TM=Automaton BF,IN,128,nil
--create display 'DP' using
--automaton : TM
--frame rate : 30
--cell display : decimal
DP=Display TM,30,"decimal"
--execute the automaton and display it
export TIC=->
DP\update!
--http://moonscript.org/reference/
--http://moonscript.org/compiler/
--https://esolangs.org/wiki/Brainfuck
--http://zacstewart.com/2013/09/15/learning-cpp-a-brainfuck-interpreter.html
Write and read function from memory
-- title: Write function to memory
-- author: Al Rado
-- desc: Shows how write code to memory
-- script: lua
-- addres to write code block / sprites addres
ADDR = 0x4000
-- target function, for example
function prints(msg,x,y,c)
for i=-1,1 do
for j=-1,1 do
print(msg,x+i,y+j,0);
end
end
print(msg,x,y,c);
end
-- write dump of target function to memory
local funcBin = string.dump(prints, true)
for i=1,#funcBin do
local code = funcBin:byte(i)
poke(ADDR+i-1,code)
end
sync(0,0,true)
-- trace lenght of memory block
trace("len: "..#funcBin)
exit()
function TIC() end
After the function is written to the cartridge memory, it can be read and run. It is important to specify the length of the code block (LEN) shown in the previous example
-- title: Read function from memory
-- author: Al Rado
-- desc: Shows how read code from memory
-- script: lua
-- addres of code block / sprites addres
ADDR = 0x4000
-- lenght of memory block in bytes
LEN = 211
-- get dump of function from memory
local funcStr=""
for i=1,LEN do
local code = peek(ADDR+i-1)
funcStr=funcStr..string.char(code)
end
-- load function from string
local prints = assert(load(funcStr));
-- call target function
cls(3)
prints("HELLO WORLD!",84,84,15)
function TIC() end
Mute one channel of a music track
The 6-bit structure of the music tracks makes muting a single channel tricky. A channel consists of 16 patterns, and one 6-bit pattern is always span across two nibbles (a half-byte) that are from separate bytes. The other nibble also contains data from some other pattern, which should be left intact. To mute all patterns of the channel in a song, some bitmasking is needed.
-- title: Mute one channel of a music track
-- author: @oggborbis
-- script: lua
function mutechannel(track, channel) --tracks 0-8, channels 1-4
local offset=3*(channel+channel%2)/2-2-channel%2
local mask
if channel%2==0 then
mask=0x3 --the high nibble is bitmasked empty for channels 2&4
else
mask=0xc --the low nibble is bitmasked empty for channels 1&3
end
for i=0,15 do --there are 16 sections in a track, each one taking 3 bytes for four 6-bit patterns
local adr=(0x13e64+51*track+3*i)*2 --a track is 51 bytes long
local q=adr+offset+channel%2
poke4(q,peek4(q)&mask) --one half of this nibble (2bit) is bitmasked empty
poke4(adr+offset+1-channel%2,0) --the other nibble (4bit) can be emptied
end
end
Set stereo volume for a channel
You can set a channels global volume for both the left and right side. Check out the RAM mapping here
(defn set-stereo-volume [channel left-volume &opt right-volume]
"""
Set stereo volume (0 - 15) for a channel (0 - 3).
If one volume provided, will use for both left and right.
"""
(default right-volume left-volume)
# NOTE: need to multiply the hex by 2 to convert from byte to nibble space.
(tic80/poke4 (* 2 (+ 0x14000 channel)) left-volume)
(tic80/poke4 (+ 1 (* 2 (+ 0x14000 channel))) right-volume))
Set sound register volume for a channel
You can also update the volume of channel in the sound register, which happens independent of the sfx() volume parameter and Mxy commands in the tracker. Here's a function that lets you set the volume as a percentage of its current value.
(defn set-channel-register-volume [channel volume-percent]
"""
Set the volume percentage (0 to 1) for a channel (from 0 to 3) in the
sound register. This effects the SFX volume envelope, and is independent
of the sfx() volume parameter and Mxy commands in the tracker.
for more --> https://github.com/nesbox/TIC-80/wiki/RAM#sound-registers
"""
(let [mem-location (+ 3 (* 2 (+ 0x0FF9C (* 18 channel))))
volume (tic80/peek4 mem-location)
new-volume (math/round (* volume volume-percent))]
(tic80/poke4 mem-location new-volume)))
Sample RGB Color RAM Address
Go to page here RGB Address Page
This RGB color (Red Green Blue) is mixed colors. Can make fade in with color. Be simple code example. Easy to use animate colors.
-- title: RGB color animation test
-- author: masternama
-- script: lua
sw=0 -- screen wait
function TIC()
if sw>0 then
cls() -- black color
poke(0x3FC0,255) -- Red
end
if sw>10 then
cls() -- black color
poke(0x3FC1,255) -- Green
end
if sw>20 then
cls() -- black color
poke(0x3FC2,255) -- Blue
end
if sw>30 then
reset() -- Loop reset
end
print("HELLO WORLD!",88,70)
--sync(0,0,true)
sw=sw+1
end
Javascript palette functions
Some useful JavaScript 16-color palette functions for the console in your browser:
// author: Anastasia Dunbar
function reverse(x){return typeof x==="string"?x.split("").reverse().join(""):x.reverse();}
function chunkString(string,n){
var chunks=[];
for(var i=0;i<string.length;i+=n){
chunks.push(string.substring(i,i+n));
}
return chunks;
}
function hexChars(string){return string.replace(/[^0-9A-F]/gi,"");}
function hexTo24RGB(string){
return chunkString(hexChars(string).substring(0,6),2).map(x=>parseInt(x,16));
}
function perceivedBrightness(R,G,B){ //Doesn't matter what range R, G and B is within.
return Math.sqrt((.299*(R**2))+(.587*(G**2))+(.114*(B**2)));
}
function sortPaletteByBrightness(array){ //From dark to bright.
array=array.map(x=>{
if(typeof x==="string"){return hexTo24RGB(x);}
return x;
});
return array.sort((x,y)=>perceivedBrightness(...x)-perceivedBrightness(...y));
}
function rgbArrayToHex(array){
return array.flat().map(x=>x.toString(16).padStart(2,"0")).join("");
}
function hexToRgbArray(string){
return chunkString(hexChars(string),6).map(x=>chunkString(x,2).map(y=>parseInt(y,16)));
}
function makeSortedPalette(string){
return rgbArrayToHex(sortPaletteByBrightness(string.split(/\s+/).filter(x=>x)));
}
function visualizePalette(string){ //Normal TIC-80 palette string as a parameter.
chunkString(string,6).forEach((x,i)=>{
console.log(`${String(i).padStart(2)}: %c${x}`,`background-color:#${x};color:#${perceivedBrightness(...hexTo24RGB(x))<100?"fff":"000"}`);
});
}
Mouse scroll
Update: Mouse scroll can now be retrieved easily via the mouse API function.
Returns mouse Y-scroll as an integer from -32 to +31
function mscroll()
local scroll=(peek(0x0FF87)&0x7E)>>1
return -(scroll&32)+(scroll&31)
end
Interpolation functions
Linear interpolation
function lerp(a,b,mu)
return a*(1-mu)+b*mu
end
Cosine interpolation
function coserp(a,b,mu)
local T=(1-math.cos(mu*math.pi))/2
return a*(1-T)+b*T
end
Cubic interpolation
function cuberp(a,b,c,d,t)
local A=d-c-a+b
local B=a-b-A
local C=c-a
local D=b
local T=t*t
return A*t*T+B*T+C*t+D
end
Normal random number
Return a random number in a pseudo-normal distribution with a mean of m and d as standard deviation (with a "quality" of q)
function randomnorm(m,d,q)
if m==nil then m=0 end
if d==nil then d=1 end
if q==nil then q=10 end
local sum=0
for i=1,q*2 do
sum=sum+(math.random())
end
val=(sum-q)*d+m
if val%1>0.5 then val=math.ceil(val) end
if val%1<0.5 then val=math.floor(val) end
return val
end
Sky gradient
Sky gradient it works by using SCN()
. Page: Sky gradient Page
-- title: Sky Gradient
-- author: masternama
-- script: lua
--[[
Color Gradient looks like e.g
(sky, lava, water & shadow)
Result will be smooth gradient...
]]
function SCN(scn)
--poke(0x3fc0,scn) -- Unused Sky Color
poke(0x3fc1,scn)
poke(0x3fc2,scn+120)
end
function TIC()
cls()
end
Rainbow color function
red=255
grn=0
blu=0
cyRG=true
cyGB=false
cyBR=false
function rainbowColor(slot) {
if (cyRG == true) {
grn++
if (grn > 255) {
grn = 255
red--
}
if (red == 0) {
cyRG=false
cyGB=true
}
}
if (cyGB == true) {
blu++
if (blu > 255) {
blu = 255
grn--
if (grn == 0) {
cyGB=false
cyBR=true
}
}
}
if (cyBR == true) {
red++
if (red > 255) {
red = 255
blu--
if (blu == 0) {
cyBR=false
cyRG=true
}
}
}
if (red==255) {
}
poke(0x3fc0+(slot*3)+2,blu)
poke(0x3fc0+(slot*3)+1,grn)
poke(0x3fc0+(slot*3),red)
}
Rescale RangeA to RangeB
Sometimes you come across wanting to map/scale a number from one range to another. For example you have a numer from the range [0-10] and wants to scale it to [100-1000]. Using the rescale-linear function you could do the following:
(fn _G.TIC []
(cls)
(print (rescale-linear 9 0 10 100 1000)))
The value 9 would end up being 910.0
Rescales number linearly
;parlortricks
;script:fennel
(fn rescale-linear [value start1 stop1 start2 stop2]
"remap rangeA to rangeB for a given value"
(+ start2 (* (- stop2 start2) (/ (- value start1) (- stop1 start1)))))
Lapse of time
-- script: moon
-- author: darltrash
lapse = (fn, ...) ->
t = time()
fn(...)
time() - t
export TIC=->
cls 6
spent = lapse ->
for x=0, 240/8
for y=0, 136/8
cx = math.sin(x*4+time()/300)*5
cy = math.cos(y*4+time()/300)*5
circ cx+x*8, cy+y*8, 3, 7
print "Took: #{spent}ms", 10, 10, 12
-- script: lua
-- author: darltrash
local function lapse(fn, ...)
local t = time()
fn(...)
return time() - t
end
function TIC()
cls(6)
local spent = lapse(function()
for x=0, 240/8 do
for y=0, 136/8 do
local cx, cy
cx = math.sin(x*4+time()/300)*5
cy = math.cos(y*4+time()/300)*5
circ(cx+x*8, cy+y*8, 3, 7)
end
end
end)
print("Took: " ..spent.. "ms", 10, 10, 12)
end
texrect
Draws a textured rectangle.
function texrect(x1,y1,x2,y2,u1,v1,u2,v2,usemap,colorkey)
usemap= usemap or false
colorkey = colorkey or -1
textri(x1,y1,x1,y2,x2,y2,u1,v1,u1,v2,u2,v2,usemap,colorkey)
textri(x1,y1,x2,y1,x2,y2,u1,v1,u2,v1,u2,v2,usemap,colorkey)
end
Similar function in Janet, but uses width and height values.
(defn trect [x y w h ux uy uw uh &opt use-map trans]
"Draw a textured rectangle using 2 textured tiangles"
(default use-map false)
(default trans -1)
(let [[x1 y1] [x y]
[x2 y2] [(+ x w) y]
[x3 y3] [x (+ y h)]
[x4 y4] [(+ x w) (+ y h)]
[u1 v1] [ux uy]
[u2 v2] [(+ ux uw) uy]
[u3 v3] [ux (+ uy uh)]
[u4 v4] [(+ ux uw) (+ uy uh)]]
(tic80/ttri x1 y1 x2 y2 x3 y3
u1 v1 u2 v2 u3 v3
use-map trans)
(tic80/ttri x2 y2 x3 y3 x4 y4
u2 v2 u3 v3 u4 v4
use-map trans)))
Draw Hanging Cable
-- script: lua
-- author: ArchaicVirus
-- description: Draws a 'hanging' cable between two points
function draw_cable(p1, p2, color)
-- calculate the length and midpoint of the cable
local dx = p2.x - p1.x
local dy = p2.y - p1.y
local length = math.sqrt(dx * dx + dy * dy)
local midpoint = { x = (p1.x + p2.x) / 2, y = (p1.y + p2.y) / 2 }
-- calculate the height of the sag
local sag_height = math.max(length / 8, 8)
-- draw the cable
for x = 0, length do
local y = math.sin(x / length * math.pi) * sag_height
local px = p1.x + dx * x / length + 0.5
local py = p1.y + dy * x / length + y + 0.5
if x > 0 then
local px_prev = p1.x + dx * (x - 1) / length + 0.5
local py_prev = p1.y + dy * (x - 1) / length + math.sin((x - 1) / length * math.pi) * sag_height + 0.5
line(px_prev, py_prev, px, py, color)
else
pix(px, py, color)
end
end
end
ttri x,y,z Rotation
-- title: Sprite Rotation
-- author: Skeptim with ChatGPT
-- desc: Sprite rotation along x,y,z axes.
-- site: https://tic80.com/dev?id=6947
-- license: GNU General Public License v3.0 (GPL)
-- version: 2.0
-- script: lua
-- input: gamepad
local sin = math.sin
local cos = math.cos
-- Initialize variables
local x, y, z = 120, 68, 0 -- Position
local angx, angy, angz = 0, 0, 0 -- Rotation angles
-- UV coordinates for each corner
local u1, v1 = 8, 0 -- Corner 1, top left
local u2, v2 = 24, 0 -- Corner 2, top right
local u3, v3 = 24, 16 -- Corner 3, bottom right
local u4, v4 = 8, 16 -- Corner 4, bottom left
-- Rotation center: can be the center of the sprite or not
local u0=(u1+u3)/2 -- U coordinate
local v0=(v1+v3)/2 -- V coordinate
-- Scale factor
local scale = 4
local function sprite_rotation(x, y, z, angx, angy, angz, u1, v1, u2, v2, u3, v3, u4, v4, scale, chromakey, u0, v0)
local z_offset=300 -- to avoid negative z values
-- Calculate rotation matrices
local sinX, cosX = sin(angx), cos(angx)
local sinY, cosY = sin(angy), cos(angy)
local sinZ, cosZ = sin(angz), cos(angz)
-- Center of the sprite
if not u0 or not v0 then
u0=(u1+u3)/2
v0=(v1+v3)/2
end
-- Define the vertices of the first triangle
local vertices1 = {
{ x = (u1-u0) * scale, y = (v1-v0) * scale, z = 0 },
{ x = (u2-u0) * scale, y = (v2-v0) * scale, z = 0 },
{ x = (u4-u0) * scale, y = (v4-v0) * scale, z = 0 }
}
-- Apply rotation matrices to the vertices of the first triangle
for _, vertex in ipairs(vertices1) do
local x, y, z = vertex.x, vertex.y, vertex.z
-- Apply rotation around x-axis
local newY = y * cosX - z * sinX
local newZ = y * sinX + z * cosX
y, z = newY, newZ
-- Apply rotation around y-axis
local newX = x * cosY + z * sinY
newZ = -x * sinY + z * cosY
x, z = newX, newZ
-- Apply rotation around z-axis
newX = x * cosZ - y * sinZ
newY = x * sinZ + y * cosZ
x, y = newX, newY
vertex.x, vertex.y, vertex.z = x, y, z+z_offset
end
-- Define the vertices of the first triangle
local vertices2 = {
{ x = (u2-u0) * scale, y = (v2-v0) * scale, z = 0 },
{ x = (u4-u0) * scale, y = (v4-v0) * scale, z = 0 },
{ x = (u3-u0) * scale, y = (v3-v0) * scale, z = 0 }
}
-- Apply rotation matrices to the vertices of the second triangle
for _, vertex in ipairs(vertices2) do
local x, y, z = vertex.x, vertex.y, vertex.z
-- Apply rotation around x-axis
local newY = y * cosX - z * sinX
local newZ = y * sinX + z * cosX
y, z = newY, newZ
-- Apply rotation around y-axis
local newX = x * cosY + z * sinY
newZ = -x * sinY + z * cosY
x, z = newX, newZ
-- Apply rotation around z-axis
newX = x * cosZ - y * sinZ
newY = x * sinZ + y * cosZ
x, y = newX, newY
vertex.x, vertex.y, vertex.z = x, y, z+z_offset
end
-- Draw the rotated triangles to form a sprite using ttri
local v1_1, v2_1, v3_1 = vertices1[1], vertices1[2], vertices1[3]
local v1_2, v2_2, v3_2 = vertices2[1], vertices2[2], vertices2[3]
-- Draw first triangle
ttri(
v1_1.x + x, v1_1.y + y,
v2_1.x + x, v2_1.y + y,
v3_1.x + x, v3_1.y + y,
u1, v1,
u2, v2,
u4, v4,
0, chromakey,
v1_1.z, v2_1.z, v3_1.z
)
-- Draw second triangle
ttri(
v1_2.x + x, v1_2.y + y,
v2_2.x + x, v2_2.y + y,
v3_2.x + x, v3_2.y + y,
u2, v2,
u4, v4,
u3, v3,
0, chromakey,
v1_2.z, v2_2.z, v3_2.z
)
end
function TIC()
cls(1)
sprite_rotation(x, y, z, angx, angy, angz, u1, v1, u2, v2, u3, v3, u4, v4, scale, 14, u0, v0)
-- Update rotation angles based on input
if btn(0) then angx = angx + 0.1 end
if btn(1) then angy = angy + 0.1 end
if btn(2) then angz = angz + 0.1 end
if btn(3) then u0 = u0 + 0.2 end
end
Console Program Example
Using co-routines we can make a console simulation, accepting user input. By @dcaillia
-- example imperative console program
function program()
console.print("Hoeveel spelers?")
local count=console.read()
for i = 1,count do
local name
while true do
console.newline()
console.print("Speler " .. i .. "?")
name=console.read()
if (string.len(name)<2) then
console.print("(minstens 2 letters)")
else
break
end
end
console.print("Dag " .. name .. "!")
console.newline()
end
end
-- library functions below: just copy this part
local main=coroutine.create(function()
cls()
program()
-- when program is done, stick here
coroutine.yield(wait,"wait")
end)
local wait=coroutine.create(function()
-- never actually resumed
end)
local input=coroutine.create(function()
while true do
console.newline()
console.input=true
coroutine.yield(wait,"wait")
end
end)
local r,n=main,"main"
function TIC()
while (not (n == "wait")) do
s,r,n=coroutine.resume(r)
trace(n)
end
r,n=console.run()
end
console = {
x=0,
y=0,
h=6, -- row height in pixels
r=120//6, -- number of rows
line="",
input=false,
log={}
}
function console.run()
if (console.input) then
local c=getc()
if (c)
then
rect(console.x,console.y,6,6,0)
if (c=="enter")
then
console.input=false
return main,"main"
elseif c=="backspace" then
local del=console.line:sub(#console.line,#console.line)
local width=print(del,0,200)
console.x=console.x-width
rect(console.x,console.y,width,6,0)
console.line=console.line:sub(1,#console.line-1)
else
console.print(c)
end
--console.status(console.x .. "," .. console.y .. " " .. console.line)
end
local color=6
rect(console.x,console.y,6,6,color)
end
return wait,"wait"
end
function console.newline()
table.insert(console.log,console.line)
console.line=""
console.x=0
console.y=console.y+console.h
if (console.y >= console.h*console.r) then
rect(0,0,240,console.h*console.r,0)
local offset=#console.log-console.r+2
for i=1,console.r-2 do
print(console.log[i+offset],0,i*console.h)
end
console.y=console.h*(console.r-1)
end
end
function console.print(c)
console.line = console.line .. c
local width=print(c,console.x,console.y)
console.x=console.x+width
end
function console.read()
coroutine.yield(input,"input")
local line=console.line
console.newline()
return line
end
function console.status(text)
rect(0,120,240,128,1)
print(text,0,120)
print(#console.log,0,128)
end
keys = {
enter = 50,
backspace = 51,
shift = 64,
capslock = 62,
lbracket = 39,
rbracket = 40,
comma = 45,
period = 46,
space = 48,
tab = 49,
equals = 38,
ctrl = 63
}
function getc()
local letter = nil
-- loop to handle the easy cases: A to Z, 0 to 9
for i = 1,36 do
if(keyp(i)) then
if i <= 26 then -- letter
letter = string.char(i+97-1) -- 65 is `a` in ascii
else -- number
letter = string.char(i-26+48-1) -- 48 is `0` in ascii
end
return letter
end
end
if(keyp(keys.space)) then
letter = " "
elseif(keyp(keys.tab)) then
letter = " "
elseif(keyp(keys.comma)) then
letter = ","
elseif(keyp(keys.period)) then
letter = "."
elseif(keyp(keys.lbracket)) then
letter = "["
elseif(keyp(keys.rbracket)) then
letter = "]"
elseif(keyp(keys.equals)) then
letter = "="
elseif(keyp(keys.enter)) then
letter = "enter"
elseif(keyp(keys.backspace)) then
letter = "backspace"
end
return letter
end