Skip to content

Instantly share code, notes, and snippets.

@TheTomster
Created December 9, 2019 00:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TheTomster/d0bda50de04547c21541715f8b5ac3df to your computer and use it in GitHub Desktop.
Save TheTomster/d0bda50de04547c21541715f8b5ac3df to your computer and use it in GitHub Desktop.
-- ufo santa candy blaster
-- cc-by-sa (c) 2019 tom wright
-- minbytes.com
--[[
notes and ideas
music
spike balls that make you drop coins
dropped coins bounce
spike-b-gone upgrade
sync gift throwing to music
sfx is annoying
random % things for spawning a gift
giant present
throw elf instead of present
reindeer from side as obstacle
if you let too many presents through
big mint-like thing that chases after you and bumps you and bullies you
]]
gravity = 0.05
-- 1 in n chance of returning true
function chance(n)
return flr(rnd(n)) == 0
end
-- call flip for n frames
function stall(n)
for i=1,n do
flip()
end
end
-- prints a string (s) using the big font, at position x,y in color c
function bigprint(s, x, y, c)
scale = scale or 8
s = tostr(s)
pal()
pal(7, c)
for idx = 1,#s do
local char = sub(s, idx, idx)
local num = tonum(char)
sspr(num * 8, 16, 8, 8, x, y)
x += scale
end
end
-- outlined print functions
function printol_helper(pstring,px,py,pcol,fn)
for printx = 0, 2 do
for printy = 0, 2 do
fn(pstring, px + printx, py + printy, 0)
end
end
fn(pstring, px + 1, py + 1, pcol)
end
function printol(pstring,px,py,pcol)
printol_helper(pstring, px, py, pcol, print)
end
function bigprintol(s, x, y, c)
printol_helper(s, x, y, c, bigprint)
end
-- chooses a random element from the list. it's really handy to call this with
-- the special table call syntax: pick{1,2,3}
function pick(l)
local i = flr(rnd(#l)) + 1
return l[i]
end
-- broad-phase step for mint collision split the screen into a grid of "areas".
-- each mint will update the area list whenever it moves. it will add itself to
-- any grid squares that overlap with it. then, gifts / coins can quickly look
-- up which mints overlap with the grid square they are in.
area_split = 4
area_size = 128 / area_split
-- math to get the index of a particular x/y grid location in the areas array.
function area_idx(x, y)
local i = flr(x / area_size)
i += flr(y / area_size) * area_split
return i
end
-- initialize empty arrays for each grid square
function init_areas()
areas = {}
for i = 0, area_split * area_split do
areas[i] = {}
end
end
-- removes the mint from the grid
function area_rm(mint)
for i = 0, #areas do
del(areas[i], mint)
end
end
-- adds the mint to each area that it overlaps with.
function area_update(mint)
-- if a mint is smaller than area_size, then we end up with an off-by-one
-- problem. we should always step at least one area beyond the top-left
-- corner, even if that means smaller mints don't actually overlap the second
-- grid square.
local w = max(mint.r, area_size)
-- start at top left corner of mint, and step one area-width at a time. add
-- ourselves to any area we overlap with.
local y = max(0, mint.y)
while y <= mint.y + w and y < 128 do
local x = max(0, mint.x)
while x <= mint.x + w and x < 128 do
add(areas[area_idx(x, y)], mint)
x = x + area_size
end
y = y + area_size
end
end
-- get mints that overlap the grid square at the given screen point
function in_area(x, y)
return areas[area_idx(x, y)]
end
-- draws the area grid and a count of how many mints are in each area.
function debug_areas()
for x = 0, 127, area_size do
for y = 0, 127, area_size do
local c = 7
local n = #areas[area_idx(x, y)]
if n > 0 then
c = 12
print(n, x+3, y+2, 12)
end
rect(x, y, x + area_size, y + area_size, c)
end
end
end
-- peppermint collision logic
function collide_mints(e, effect)
local ex, ey = e.x + 3, e.y + 3
ex = mid(0, ex, 127)
ey = mid(0, ey, 127)
for p in all(in_area(ex, ey)) do
-- ex, ey represents center point of the gift
-- and px, py are the center point of the current peppermint
local px, py = p.x + p.r / 2, p.y + p.r / 2
-- measure the x and y deltas, and the distance
local dx, dy = ex - px, ey - py
local dist2 = dx * dx + dy * dy
-- cache divided size value to avoid repeated calculations
local r = p.r / 2
-- the distance value by itself frequently overflows and causes problems
-- with a simple comparison... so first we check if the distance is close
-- enough in the x and y directions. if x and y are close then we know
-- overflow won't be a problem, and we can do the normal distance check.
if abs(dx) < r and abs(dy) < r and dist2 < r * r then
-- spawn a small spark effect
mksparks(flr(rnd(3)+3), ex, ey, p.pal)
-- calculate bounce angle.
local a = atan2(dx, dy)
local spd = sqrt(e.dx * e.dx + e.dy * e.dy)
-- a minimum bounce speed helps keep gifts from getting stuck vibrating
-- next to a peppermint. if they somehow overlap a peppermint it helps
-- make sure they get ejected.
spd = max(spd, 1.5)
-- converting the bounce angle back to dx/dy speed
e.dx = cos(a) * spd
e.dy = sin(a) * spd
sfx(pick{10, 15, 16})
-- tell the peppermint to flash, and subtract life from it
if effect == 'damage' then
-- a little variety in sfx helps a lot
p.flash = 2
p.life -= 1
elseif effect == 'coin_damage' then
p.flash = 2
p.life -= 2
stat_coin_damage += 2
elseif effect == 'power' then
p.flash = 2
p.life -= 5
mkexp(ex, ey, 4)
elseif effect == 'hyper' then
p.flash = 2
p.life -= 20
end
end
end
end
-- constructs a gift. starts out at the given location with 0 speed, and a
-- random color palette.
function mkgift(x, y)
stat_gifts += 1
local effect = 'damage'
local pal = pick{
{3,8},
{12,7},
{7,12},
{8,3},
}
if have_upgrade(ug_hyper_gifts) and chance(6) then
effect = 'hyper'
pal = {14, 7}
elseif have_upgrade(ug_power_gifts) and chance(4) then
effect = 'power'
pal = {10, 9}
end
return {
x = x, y = y,
dx = 0, dy = 0,
-- gifts rotate over time, this is the angle of rotation
a=rnd(),
-- choose a random two color palette
pal=pal,
-- gifts draw a little trail behind them... every frame the gift's position
-- will be added to the tail table. we can use the last few points to draw
-- the tail
tail = {},
effect = effect
}
end
-- update function for gifts
function upgift(g)
-- constant rotation, turns out the simplest solution looks just fine
g.a+=1/60
-- apply movement speed
g.x+=g.dx
g.y+=g.dy
-- accelerate the gift downward based on gravity, but limit the maximum fall
-- speed
g.dy+=gravity
g.dy=min(g.dy,3)
-- gifts bounce off the sides of the screen
if g.x < 0 or g.x > 128 then
g.dx = -g.dx
end
collide_mints(g, g.effect)
-- tail handling. add current position to tail, and clip it to contain the
-- last 4 positions. we can adjust that constant to increase the length of the
-- tail.
add(g.tail, {x = g.x, y = g.y})
local max_tail = 4
if g.effect == 'power' then
max_tail = 8
elseif g.effect == 'hyper' then
max_tail = 16
end
if #g.tail > max_tail then
del(g.tail, g.tail[1])
end
end
-- delete gifts that have fallen off the bottom of the screen
function killgifts()
local alive = {}
for g in all(gifts) do
if g.y < 128 then
add(alive, g)
end
end
gifts = alive
end
-- draws gifts
function drawgift(g)
-- set up palette for drawing the tail. the sprite uses a black outline and
-- orange background, so we need to adjust the transparency.
pal()
palt(0,false)
palt(14,true)
-- draw tail with flickering trasnparency
if t % 2 == 0 then
-- setting a fill pattern (Sean Leblanc's fillp tool is great for these)
fillp(bor(0b101111101011111.1))
local c = 0
if g.effect == 'hyper' then
c = 7
end
-- draw all the tail points
for t in all(g.tail) do
circfill(t.x+3, t.y+3, 3, c)
end
fillp()
end
if g.effect == 'power' then
circfill(g.x + rnd(8) - 1, g.y + rnd(8) - 1, 4, 0)
circfill(g.x + rnd(8) - 1, g.y + rnd(8) - 1, 4, 7)
elseif g.effect == 'hyper' then
circfill(g.x + rnd(8) - 1, g.y + rnd(8) - 1, 8, pick{0, 2})
circfill(g.x + rnd(8) - 1, g.y + rnd(8) - 1, 8, pick{14, 7})
end
-- setup palette for drawing the actual sprite
pal(3, g.pal[1])
pal(8, g.pal[2])
-- we have 8 possible rotations we can draw. so we start by converting the
-- angle to an integer from 0 to 7
local i = flr((g.a%1)*8) + 1
-- we use look up tables to identify the correct sprite number and flip flags
-- for the angle we're drawing.
local sprs = {1,2,3,2,1,2,3,2}
local fx = {false,false,false,false,true,true,true,true}
local fy = {false,false,false,true,true,true,false,false}
-- sprite and flags pulled from lookup tables
spr(sprs[i],g.x,g.y,1,1,fx[i],fy[i])
end
-- explosions! draws a flashing circle for a few frames.
function mkexp(x, y, r)
add(explosions, {
x = x, y = y,
r = r, t = 5
})
end
-- update is just a timer to control the drawing.
function upexp()
for e in all(explosions) do
e.t -= 1
end
end
-- the first frew frames draw a black circle, then it switches to white. gives a
-- nice visual pop.
function drexp()
pal()
for e in all(explosions) do
if e.t > 2 then
circfill(e.x, e.y, e.r, 7)
elseif e.t > 0 then
circfill(e.x, e.y, e.r, 0)
end
end
end
-- kill off the explosion objects after their timer runs out.
function killexp()
local alive = {}
for e in all(explosions) do
if e.t >= 0 then
add(alive, e)
end
end
explosions = alive
end
-- peppermint initializer
function mkmint()
-- setting up a table for randomizing the size of peppermints. pick selects a
-- value at random uniformly... if we add a bunch of copies of a value, it is
-- more likely to be chosen.
--
-- most mints will be size 16-48... occasionally larger mints will spawn.
-- originally 'r' was going to mean 'radius', but it turns out it's more like
-- diameter based on how i implemented the drawing. whoops.
local rs = {}
for i = 1, 40 do add(rs, 16) end
for i = 1, 60 do add(rs, 32) end
for i = 1, 20 do add(rs, 48) end
for i = 1, 5 do add(rs, 64) end
for i = 1, 1 do add(rs, 128) end
local r = pick(rs)
-- spawn most mints in the middle, but occasionally allow one to spawn closer
-- to the edge. i found that truly random spawns made it awkward to set up
-- shots so that a gift bounced back and forth between the wall and a mint.
-- but we do occasionally want to spawn them on the edge, or else it looks
-- weird. spawning them at exactly 0 or 128 when they're on the edge works
-- really well.
--
-- so, by default cluster them in the middle. (subtracting r / 2 makes sure
-- the mint is centered on the chosen point)
local x = rnd(96) + 16 - r / 2
-- but 10% of the time put them on the edges
if rnd() < 0.1 then
x = pick{0, 128} - r / 2
end
-- give each mint a random life total
local life = flr(rnd(5) + 3)
-- bigger mints have more life
if r > 32 then
life *= 2
end
-- and the biggest boys are just guaranteed to have a ton of life
if r == 128 then
life = 40
end
-- y position. if there aren't a lot of mints, we want to spawn them close to
-- the edge of the screen so that the player sees them right away. but when
-- there are more mints we need to spread them out a bit more.
local y = rnd(256) + 128
if #mints < 6 then
y = rnd(64) + 128
end
-- different colored mints with more health
local pal = {7, 8, 2}
local power = have_upgrade(ug_power_gifts) or have_upgrade(ug_fast_gifts)
local hyper = have_upgrade(ug_hyper_gifts) or have_upgrade(ug_super_fast)
if hyper and chance(10) then
pal = {2, 13, 0}
life *= 16
elseif chance(30) then
pal = {7, 12, 1}
life *= 8
elseif hyper and chance(4) then
pal = {7, 12, 1}
life *= 8
elseif chance(30) then
pal = {7, 11, 3}
life *= 4
elseif power and chance(5) then
pal = {7, 11, 3}
life *= 4
elseif hyper and chance(2) then
pal = {7, 11, 3}
life *= 4
end
return {
-- random initial rotation angle used for drawing
a = rnd(), da = pick{1 / 45, 1 / 60, 1 / 90},
r = r,
x = x,
-- the first batch of mints starts some ways off screen
y = y,
-- and they glide upwards quite slowly
dy = rnd() * 0.1 + 0.1,
-- number of hits to destroy the mint
life = life,
-- palette to draw the mint
pal = pal,
-- used to make the mint flash when it is hit. 0 means no flash, a positive
-- value flashes for that many frames.
flash = 0
}
end
function reward_more_coins(x, y)
if chance(500) then
-- jackpot!
sfx(21)
mkcoins(x, y, 40)
for mint in all(mints) do
mint.life = 0
end
for i = 1, 16 do
mkmint()
end
elseif chance(8) then
mkcoins(x, y, 8)
elseif chance(4) then
mkcoins(x, y, 4)
elseif chance(2) then
mkcoins(x, y, 2)
else
mkcoins(x, y, 1)
end
end
function reward_coins(x, y)
if chance(500) then
-- once in a great while, spawn a ton
mkcoins(x, y, 20)
elseif chance(8) then
-- occasionally spawn 4
mkcoins(x, y, 4)
elseif timer <= 0 then
-- if the timer is out, guarantee we make one
mkcoin(x, y)
elseif chance(4) then
-- one in 4 mints drops a coin
mkcoin(x, y)
end
end
function upmint(p)
local alive = {}
for p in all(mints) do
area_rm(p)
-- movement
p.y -= p.dy
-- speed them up once the timer has run out
if timer <= 0 then
p.dy += 0.02
p.dy = min(p.dy, 0.4)
if p.life > 0 then p.life = 1 end
end
-- rotation
p.a += p.da
-- check if we're dead.
if p.life <= 0 then
-- double up the sfx for impact. a bass hit plus a noise.
sfx(13)
sfx(14)
-- spawn a whole bunch of sparks.
local colors = p.pal
if timer <= 0 then
colors = {8,12,7,13,14,11,10,9,7,11,12,14}
end
mksparks(5, p.x - 8, p.y - 8, colors)
mksparks(5, p.x - 8, p.y + 16, colors)
mksparks(5, p.x + 16, p.y - 8, colors)
mksparks(5, p.x + 16, p.y + 16, colors)
-- screen shake!
shake += 3
freeze = 3
-- spawn additional peppermints... unless there are already too many. so
-- the more the player blows up the more show up. we also stop spawning
-- mints if the timer has run down. once the timer is at 0 and all
-- mints are dead the game moves on to the next stage.
local max_mints = 20
if have_upgrade(ug_fast_gifts) then
max_mints = 30
elseif have_upgrade(ug_super_fast) then
max_mints = 40
end
if timer > 0 and #mints < 20 then
add(mints, mkmint())
add(mints, mkmint())
add(mints, mkmint())
if have_upgrade(ug_fast_gifts) then
add(mints, mkmint())
add(mints, mkmint())
end
end
-- explode!
mkexp(p.x + p.r / 2, p.y + p.r / 2, p.r / 1.2)
-- update the timer
timer -= 1
-- random chance of spawning a coin!
if have_upgrade(ug_more_coins) then
reward_more_coins(p.x, p.y)
else
reward_coins(p.x, p.y)
end
stat_sweets += 1
end
-- if the mint has floated off the top of the screen, remove it
if p.y < -(p.r) then
p.life = -1
stat_missed_mints += 1
if timer > 0 then
add(mints, mkmint())
end
end
if p.life > 0 then
add(alive, p)
area_update(p)
end
end
mints = alive
end
-- sort the peppermints. we want smaller mints to draw on top of larger mints,
-- so that the huge mints don't hide them. makes a big difference when you can
-- see where the small mints are, instead of being surprised when a big one
-- blows up and reveals a bunch more hidden mints.
function sortmints()
if #mints < 2 then
-- nothing to sort
return
end
-- do a single pass of bubble sort each frame. thanks pico8 discord for this
-- gem!
for i = 1, #mints - 1 do
-- we want smaller mints to be last in the list so that they draw last and
-- end up on top. so, if we are smaller than our neighbor we should swap and
-- move further back in the list.
if mints[i].r < mints[i + 1].r then
mints[i], mints[i + 1] = mints[i + 1], mints[i]
end
end
end
-- save rotated copies of peppermint in sprite sheet. because of symmetry,
-- saving 4 rotations lets us smoothly draw 16 different angles.
function cacherot()
-- saving 4 rotations.
for i = 0, 4 do
-- each rotation is (360 / 16 = 22.5 degrees)
local a = i / 16
-- loop over each pixel of the 16x16 sprite
for x = 0, 15 do
for y = 0, 15 do
-- calculate rotation. (thanks trasevol_dog:
-- https://trasevol.dog/2017/04/27/di14/)
local dx, dy = x - 8, y - 8
local aa = atan2(dx, dy) + a
local l = sqrt(dx * dx + dy * dy)
local rx = 8 + l * cos(aa)
local ry = 8 + l * sin(aa)
-- use the rotated coordinates to fetch the color from the base sprite.
local c
if rx <= 0 or rx > 15 or ry < 0 or ry > 15 then
-- if we're outside the bounds of the base sprite, force it to the
-- transparent color (orange this time)
c = 14
else
-- we're inside the sprite, so grab teh color
c = sget(32 + flr(rx + 0.5), flr(ry + 0.5))
-- fudge a bit to try and change red and white pixels to black for
-- cleaner outlines. basically we're feeling around to some adjacent
-- pixels, replacing the current color with the outline color. this
-- helps ensure the outline is continuous, but makes it a bit chunky.
-- i like how it ended up looking though.
if (c ~= 14) and (
sget(32 + flr(rx), flr(ry)) == 2
or sget(32 + flr(rx), ceil(ry)) == 2
or sget(32 + ceil(rx), flr(ry)) == 2
or sget(32 + ceil(rx), ceil(ry)) == 2
) then
c = 2
end
end
-- write to some free space on the sprite sheet. now drawpepp can use
-- sspr to copy this rotated version of the sprite to the screen.
sset(x+i*16, y+64, c)
end
end
end
end
-- drawing mints. each sprite is taken from the cached copies created by
-- `cacherot`. i also experimented with drawing the peppermint in code, so that
-- i could have smoother scaling and rotation... but it was very inefficient
-- compared to sspr.
function drmint(p)
-- figure out which rotation sprite matches up with the current angle.
local a = flr(p.a%1 * 4)
-- set up palette
pal()
palt(0, false)
palt(14, true)
-- if we're flashing, force the whole palette to be the flash color. we'll do
-- an orange flash if we're almost dead, otherwise white.
if p.flash > 0 then
for i = 0, 16 do
if p.life < 3 then
pal(i, 9)
else
pal(i, 7)
end
end
p.flash -= 1
else
pal(7, p.pal[1])
pal(8, p.pal[2])
pal(2, p.pal[3])
end
-- now that the palette is finally set, sspr copies the sprite to the screen.
sspr(
a * 16, 64, 16, 16,
p.x, p.y, p.r, p.r
)
end
-- make santa! the player! he's in a ufo for some reason!
function mksanta()
return {
-- position
x = 58, y = 24,
-- velocity
dx = 0, dy = 0,
-- hand position, relative to x/y
hx = 8, hy = 5,
-- gift throwing timer
gt = 120,
-- show tutorial popups for the buttons
tutgift = true,
tutmove = true,
}
end
function upsanta(s)
-- movement input. we'll adjust the hand position subtly based on the player's
-- input, so it looks like santa's pushing a joystick to move. we also
-- accelerate in the x direction. if there's no x directional input, apply
-- some drag to slow santa down.
if btn(0) then
s.hx = 7
s.dx -= 0.1
s.tutmove = false
elseif btn(1) then
s.hx = 9
s.dx += 0.1
s.tutmove = false
else
s.dx *= 0.95
s.hx = 8
end
-- now y movement, no hand movement here as it looked pretty weird.
if btn(2) then
s.dy -= 0.1
s.tutmove = false
elseif btn(3) then
s.dy += 0.1
s.tutmove = false
else
s.dy *= 0.95
end
-- clamp the speed. vertical movement being a little slower than horizontal
-- felt right.
s.dx = mid(-2, s.dx, 2)
s.dy = mid(-1, s.dy, 1)
-- and if we're shooting, clamp the speed so it's real slow
if btn(4) then
s.dx = mid(-0.2, s.dx, 0.2)
s.dy = mid(-0.2, s.dy, 0.2)
end
-- bounce off mints. very similar collision logic to the gifts.
for p in all(mints) do
local sx, sy = s.x + 3 + s.dx, s.y + 3 + s.dy
local px, py = p.x + p.r / 2, p.y + p.r / 2
local dx, dy = sx - px, sy - py
local dist = sqrt(dx * dx + dy * dy)
if abs(dx) < p.r / 2 and abs(dy) < p.r / 2 and dist < p.r / 2 then
-- play a small bump sound.
sfx(10)
-- make a few sparks. looks hilarious if santa gets wedged between a mint
-- and the edge of the screen.
mksparks(flr(rnd(3)+3), sx, sy, {7, 8})
-- small screenshake
shake = 1
-- calculate the bounce angle. speed is hardcoded to get you away from
-- mints.
local a = atan2(dx, dy)
s.dx = cos(a) * 2
s.dy = sin(a) * 2
end
end
-- keep on screen. if our movement is about to push us off screen flip it to
-- push us back into the screen instead, and do a small screenshake.
if s.x + s.dx < -6 or s.x + s.dx > 124 then
s.dx = -s.dx
shake = 2
end
if s.y + s.dy < -10 or s.y + s.dy > 124 then
s.dy = -s.dy
shake = 2
end
-- apply movement
s.x += s.dx
s.y += s.dy
-- add a small y movement to make you do a little bob when you're sitting
-- still.
s.y += sin(t / 120) * 0.08
-- gift throwing. if the timer is > 0 then santa can't drop a gift yet... and
-- the timer ticks down.
if s.gt > 0 then
s.gt -= 1
end
-- waits until the player presses the gift drop button to start the game.
if btn(4) and s.gt <= 0 and s.tutgift then
s.tutgift = false
mktimer()
round_state = 'playing'
local num_mints = round_number > 1 and 16 or 4
for i = 1, num_mints do
add(mints, mkmint())
end
end
-- if the timer is 0 or lower, and the player is holding the button, drop a
-- gift
if btn(4) and s.gt <= 0 then
-- reset gift drop timer. timing is based on which upgrades we have
if have_upgrade(ug_super_fast) then
s.gt = 10
elseif have_upgrade(ug_fast_gifts) then
s.gt = 30
else
s.gt = 60
end
-- spawn the gift
local g = mkgift(s.x + 3, s.y + 6)
add(gifts, g)
if g.effect == 'power' then
sfx(19)
elseif g.effect == 'hyper' then
sfx(20)
else
sfx(12)
end
end
-- really really keep on screen though. sometimes when you're being pushed
-- around by a mint the check we had above breaks down. so last thing we do is
-- make extra sure you're on screen.
s.x = mid(-6, s.x, 124)
s.y = mid(-10, s.y, 124)
end
function drawsanta(s)
pal()
-- highlight for when the game gets crazy towards the end
if have_upgrade(ug_golden) and #gifts + #mints + #coins > 10 then
circ(s.x + 6, s.y + 6, 12, 10)
end
-- adjust palette
pal()
palt(0, false)
palt(11, true)
-- draw the tiny hand sprite
sspr(
64, 0, 4, 4,
s.x + s.hx, s.y + s.hy
)
if have_upgrade(ug_golden) then
pal(6, 10)
pal(5, 9)
end
-- and draw santa ufo sprite on top
sspr(
48, 0, 13, 13,
s.x, s.y
)
-- and sunglasses if we have the upgrade
if have_upgrade(ug_golden) then
sspr(72, 0, 7, 2, s.x + 3, s.y + 4)
end
-- draw the tutorial if it hasn't been dismissed prompt isn't shown until you
-- can actually drop a gift.
if s.gt <= 0 and s.tutgift then
printol("\x8E gift", s.x - 9, s.y + 21, 7)
end
if s.tutmove then
printol("+ move", s.x - 9, s.y + 14, 7)
end
end
-- spark spawner. spawns n sparks with random velocity, at the given location,
-- choosing a random color from the list `c`.
function mksparks(n, x, y, cs)
for i = 1, n do
add(sparks, mkspark(x, y, pick(cs)))
end
end
-- individual spark spawn
function mkspark(x, y, c)
return {
x = x,
y = y,
c = c,
dx = rnd(4) - 2,
dy = rnd(4) - 2,
life = 60, -- sparks last this many frames
tail = {} -- sparks are drawn using a tail
}
end
-- spark updater
function upsparks()
-- this one includes the kill functionality as part of the update function
local alive = {}
for s in all(sparks) do
-- subtract a frame from the life counter and determine if it should be kept
-- alive.
s.life -= 1
if s.life > 0 then
add(alive, s)
end
-- movement
s.x += s.dx
s.y += s.dy
-- y acceleration
s.dy += gravity
-- add a point to the tail
add(s.tail, {x = s.x, y = s.y})
-- keep the last 4 tail points. can change this to change the length of the
-- spark.
if #s.tail > 4 then
del(s.tail, s.tail[1])
end
end
sparks = alive
end
-- spark drawing
function drspark(s)
pal()
-- draw line along the tail points.
for i = 1, #s.tail - 1 do
local st = s.tail[i]
local ed = s.tail[i + 1]
line(st.x, st.y, ed.x, ed.y, s.c)
end
end
-- let it snow
function mksnow()
snow = {}
for i = 1, 200 do
add(snow, {
x = rnd(128), y = rnd(128),
dx = rnd(2) - 1, dy = rnd(0.5) + 0.5,
})
end
end
-- snow updater
function upsnow()
for s in all(snow) do
-- movement
s.x += s.dx
s.y += s.dy
-- screen wrapping
if s.x < 0 then s.x += 128 end
if s.x > 128 then s.x -= 128 end
if s.y > 128 then s.y = -rnd(16) end
-- random drift
s.dx += (rnd(1) - 0.5) * 0.25
s.dx = mid(-1, s.dx, 1)
s.dy += 0.1
s.dy = min(s.dy, 1)
end
end
-- snow is just drawn as pixels
function drsnow()
for s in all(snow) do
pset(s.x, s.y, 7)
end
end
-- create some clouds to decorate the background. we spawn several cloud
-- objects... each one consists of a bunch of blobs clustered around the cloud's
-- origin.
function mkclouds()
clouds = {}
for i = 1,8 do
c = {
-- spawn on one of the edges of the screen
x = pick{-8, 120},
-- random y location... can be on screen or below the screen
y = rnd(256),
bits = {}
}
-- spawn the cloud bits
for i = 1,16 do
add(c.bits, {
-- spread them out a little taller than the cloud is wide
x = rnd(32), y = rnd(48),
-- random sizes... tending towards a middle size instead of being purely
-- uniform.
r = rnd(6) + rnd(6) + 4
})
end
add(clouds, c)
end
end
function upclouds()
for c in all(clouds) do
-- clouds float upwards
c.y -= 0.4
-- once they are off the top of the screen, respawn them below
if c.y < -64 then
c.y = rnd(128) + 128
c.bits = {}
for i = 1,16 do
add(c.bits, {
x = rnd(32),
y = rnd(48),
r = pick{4, 4, 4, 6, 6, 6, 8, 8, 8, 16}
})
end
end
end
end
function drclouds()
pal()
for c in all(clouds) do
for b in all(c.bits) do
-- draw a base circle with a shaded pattern
fillp(0b101111101011111)
circfill(c.x + b.x, c.y + b.y, b.r, 118)
fillp()
-- now, offset and draw a lighter highlight. the offset is different
-- depending which side of the screen the cloud is on.
local x
if c.x == -8 then
x = c.x + b.x - 3
else
x = c.x + b.x + 3
end
circfill(x, c.y + b.y - 3, b.r - 1, 7)
end
end
end
-- easing fn, by sean
function ease(t)
t = mid(0, t, 1)
if t >= 0.5 then
return (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
else
return 4 * t * t * t
end
end
function lerp(a, b, t)
return a + (b - a) * t
end
function mkbonus()
bonus = {
y = -64,
t = 0
}
end
function upbonus()
if bonus == nil then
return
end
if bonus.t < 30 then
bonus.y = lerp(-64, 32, bonus.t / 30)
elseif bonus.t == 30 then
shake = 8
for x = 32, 96 do
mksparks(1, x, 64, {8,12,7,13,14,11,10,9,7,11,12,14})
end
sfx(22)
elseif bonus.t < 60 then
elseif bonus.t < 120 then
local t = bonus.t - 120
bonus.y = lerp(32, 160, ease(t / 30))
end
bonus.t += 1
if bonus.t > 80 then
bonus = nil
end
end
function drbonus()
if bonus == nil then
return
end
pal()
palt(0, false)
palt(14, true)
sspr(0, 24, 63, 29, 33, bonus.y)
end
function mktimer()
timer = 90
timerobj = {
t = 0
}
end
function uptimer()
if timerobj then
timerobj.t += 1
end
if timer > 0 and t % 60 == 0 then
timer -= 1
end
if timer <= 0 and round_state == 'playing' then
round_state = 'bonus'
mkbonus()
end
end
function drtimer()
if timer > 0 then
local y = lerp(-16, 4, ease(timerobj.t / 40))
bigprintol(timer, 56, y, 7)
end
end
function mkcoin(x, y)
add(coins, {
x = x, y = y,
dx = rnd(2) - 1, dy = -rnd(3),
tail = {},
t = 60 * 7
})
end
function mkcoins(x, y, n)
for i=1,n do
mkcoin(x, y)
end
end
function upcoin()
local alive = {}
for c in all(coins) do
add(alive, c)
-- tick down timer. kill the coin if the timer reaches 0.
c.t -= 1
if c.t <= 0 then
stat_missed_coins += 1
mkexp(c.x, c.y, 4)
del(alive, c)
end
local magnet_dist = 20
if have_upgrade(ug_magnet) then
magnet_dist = 80
end
-- if a coin is close to santa, collect it and add it to the cash counter.
local dist = abs(c.x - santa.x) + abs(c.y - santa.y)
if dist < 4 then
cash += 1
stat_coins += 1
sfx(17)
del(alive, c)
elseif timer <= 0 and #mints == 0 then
-- at the end of the round, slurp all coins towards santa
c.dx = mid(-2, santa.x - c.x, 2)
c.dy = mid(-2, santa.y - c.y, 2)
elseif dist < magnet_dist then
-- for coins that are slightly further away, change their movement so they
-- get slurped into santa.
c.dx = mid(-2, santa.x - c.x, 2)
c.dy = mid(-2, santa.y - c.y, 2)
end
-- apply movement speed
c.x+=c.dx
c.y+=c.dy
-- accelerate the coin downward based on gravity
c.dy += gravity / 2
-- coins have a pretty slow movement speed clamped on them. goal is to make
-- it easier to predict where they're going so you can catch them.
c.dx=mid(-1, c.dx, 1)
c.dy=mid(-3, c.dy, 1)
-- coins bounce off the sides of the screen, and the bottom
if c.x < 0 or c.x > 128 then
c.dx = -c.dx
end
-- the bounce off the bottom is exaggerated
if c.y > 128 then
c.dy = -c.dy * 2
end
local effect = 'none'
if have_upgrade(ug_crusher_coins) then
effect = 'coin_damage'
end
collide_mints(c, effect)
-- guarantee coins stay on screen
c.x = mid(0, c.x, 128)
c.y = min(c.y, 128)
-- glittery tail
if t % 3 == 0 then
add(c.tail, {
x = c.x + rnd(8), y = c.y + 4,
c = pick{7, 9, 9, 10, 10},
-- set a random int between 0 and 15... used to change some pixels into
-- $ signs
t = flr(rnd(16))
})
end
if #c.tail > 4 then
del(c.tail, c.tail[1])
end
for t in all(c.tail) do
t.y -= 0.3
end
end
coins = alive
end
function drcoin()
for c in all(coins) do
if chance(60) then
circfill(c.x + 4, c.y + 4, 8, 7)
end
for t in all(c.tail) do
if t.t == 0 then
pal()
pal(7, t.c)
spr(19, t.x, t.y)
else
circ(t.x, t.y, 1, t.c)
end
end
if c.t < 60 and c.t % 2 == 0 then
goto next
end
pal()
palt(0, false)
palt(14, true)
local sprs = {16, 17, 18, 17}
local i = flr(t / 6) % #sprs
local s = sprs[i + 1]
spr(s, c.x, c.y)
::next::
end
end
function mkcash()
cash_particles = {}
end
function upcash()
if t % 9 == 0 then
add(cash_particles, {
x = rnd(8), y = rnd(8),
c = pick{7, 7, 9, 9},
-- set a random int between 0 and 15... used to change some pixels into
-- $ signs
t = flr(rnd(16))
})
end
if #cash_particles > 4 then
del(cash_particles, cash_particles[1])
end
end
function drcash()
if cash > 0 then
pal()
palt(0, false)
palt(14, true)
spr(16, 1, 1)
pal()
for t in all(cash_particles) do
if t.t == 0 then
pal()
pal(7, t.c)
spr(19, t.x, t.y)
else
circ(t.x, t.y, 1, t.c)
end
end
printol(cash, 10, 2, 10)
end
end
function fade()
local t = 0
local fade = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}
local darks = {
0, 0, 0, 1,
2, 0, 5, 6,
4, 8, 9, 3,
6, 6, 13, 14
}
while t < 60 do
t += 1
if t % 6 == 0 then
all_black = true
for i = 1,#fade do
fade[i] = darks[fade[i]+1]
end
end
for i = 0,15 do
pal(i, fade[i+1], 1)
end
flip()
end
end
function init_game()
state = 'game'
-- screen shake timer!
shake = 0
-- freeze timer
freeze = 0
-- initialize objects
gifts = {}
mints = {}
santa = mksanta()
sparks = {}
explosions = {}
coins = {}
-- game state
timer = 0
round_state = 'start'
mkcash()
music(0, 0, 3)
end
function update_game()
if freeze > 0 then
freeze -= 1
return
end
t += 1 -- update the global frame counter
-- update all the objects
upsanta(santa)
foreach(gifts, upgift)
killgifts()
upmint()
sortmints()
upsparks()
-- upsnow()
upclouds()
upexp()
uptimer()
upcoin()
upcash()
upbonus()
if round_state == 'bonus' and #mints == 0 and #coins == 0 then
fade()
init_shop()
end
-- decrease screen shake
shake -= 0.4
shake = mid(0, shake, 4)
end
function drround()
local s = 'round ' .. tostr(round_number)
pal()
printol(s, 126 - 4 * #s, 3, 7)
end
-- draw upgrades
function drbought()
local y = 120
for i = 1,#bought do
local u = upgrades[bought[i]]
printol(u[5], 1, y, 7)
y -= 8
end
end
function draw_game()
cls(5)
-- shake screen at a random angle
local a = rnd()
local cx = cos(a) * shake
local cy = sin(a) * shake
camera(cx, cy)
-- draw background
-- drsnow()
drclouds()
-- draw objects
foreach(mints, drmint)
drtimer()
drcash()
drround()
drbought()
drexp()
foreach(gifts, drawgift)
drcoin()
foreach(sparks, drspark)
drawsanta(santa)
drbonus()
end
function init_title()
cls(5)
sfx(22)
printol('minbytes.com', 40, 56, 7)
printol('copyright 2019 tom wright', 14, 96, 7)
printol('creative commons cc-by-sa', 14, 104, 7)
stall(20)
fade()
t = 0
-- mksnow()
mkclouds()
title_x = -75
title_timer = nil
music(17)
end
function update_title()
t += 1
-- upsnow()
upclouds()
title_x -= 0.5
if title_x <= -300 then
title_x = 0
end
if title_timer ~= nil then
title_timer -= 1
end
if title_timer == nil and (btnp(4) or btnp(5)) then
music(-1)
title_timer = 60
sfx(21)
end
if title_timer ~= nil and title_timer <= 0 then
init_game()
end
end
function draw_title()
cls(5)
-- drsnow()
drclouds()
printol('ufo santa candy blaster', 16, 8, 7)
palt(0, false)
palt(11, true)
spr(6, 58, 24, 1.75, 1.75)
pal()
printol('drop gifts', 42, 44, 7)
printol('destroy sweets', 34, 54, 8)
printol('collect coins', 36, 64, 10)
if t % 60 < 30 then
printol('press \x97 or \x8E to play', 22, 94, 7)
end
printol('a game by tom wright for advent calendar 2019 - twitter:@thetomster3', title_x, 120, 7)
printol('a game by tom wright for advent calendar 2019 - twitter:@thetomster3', title_x + 300, 120, 7)
end
-- list of upgrades available
-- each item has a sprite, price, name, description, letter for hud
upgrades = {
{14, 20, "power gifts", " random chance to throw a super gift ", "p"},
{10, 50, "speed up", " santa drops gifts more quickly ", "s"},
{46, 50, "coin up", " coins are more likely to appear ", "c+"},
{44, 30, "coin magnet", " coins are attracted from farther away ", "m"},
{74, 60, "crusher coins", " coins can deal damage to mints ", "c"},
{42, 90, "hyper gifts", " random chance to throw a hyper super gift ", "p+"},
{12, 120, "super speed", "santa drops gifts even more quickly ", "s+"},
{78, 500, "golden ufo", " ", "$"}
-- {76, 60, "spike-b-gone", " protects santa from spike balls ", "x"},
}
-- setting up global variables for upgrade indices, so that have_upgrade calls
-- can look nice. have_upgrade(ug_magnet) instead of have_upgrade(4)
ug_power_gifts = 1
ug_fast_gifts = 2
ug_more_coins = 3
ug_magnet = 4
ug_crusher_coins = 5
ug_hyper_gifts = 6
ug_super_fast = 7
ug_golden = 8
ug_nospike = 9
-- a cheat code :)
function unlock_all()
for i=1,#upgrades do
add(bought, i)
end
end
function init_shop()
t = 0
round_state = 'done'
state = 'shop'
-- these upgrades are available in the shop currently. each list item is an
-- index into the upgrade table.
shop = {}
for i = 1, #upgrades do
-- check if bought
if not have_upgrade(i) then
add(shop, i)
end
-- stop when we have 3 items in shop
if #shop >= 3 then
break
end
end
if #shop == 0 then
-- game over
init_game_over()
return
end
-- which shop item is currently selected
selected = 1
santa = {x = 32}
music(12)
end
function update_shop()
t += 1
local target_x = 26 + (selected - 1) * 32
santa.x = lerp(santa.x, target_x, 0.4)
if btnp(0) then
sfx(18)
selected -= 1
elseif btnp(1) then
sfx(18)
selected += 1
end
selected = mid(1, selected, #shop)
local upgrade_idx = shop[selected]
local price = upgrades[upgrade_idx][2]
if btnp(4) and cash >= price then
-- if we're buying the more powerful version of a powerup, also include the
-- lower powered version.
if upgrade_idx == ug_hyper_gifts and not have_upgrade(ug_power_gifts) then
add(bought, ug_power_gifts)
elseif upgrade_idx == ug_super_fast and not have_upgrade(ug_fast_gifts) then
add(bought, ug_fast_gifts)
end
add(bought, upgrade_idx)
cash -= price
sfx(22)
fade()
round_number += 1
init_game()
elseif btnp(5) then
sfx(23)
fade()
round_number += 1
init_game()
end
end
function have_upgrade(i)
for j = 1, #bought do
if bought[j] == i then
return true
end
end
return false
end
function draw_shop()
cls(5)
local s = 'upgrade time'
local x = 40
for i = 1, #s do
local c = sub(s, i, i)
local y = sin((t + x) / 30) * 1.5 + 5
printol(c, x, y, 7)
x += 4
end
printol('you have ', 36, 16, 10)
pal()
palt(0, false)
palt(14, true)
spr(16, 72, 15)
printol(cash, 81, 16, 10)
pal()
local y = 32 + cos(t / 120) * 1.5
palt(0, false)
palt(11, true)
sspr(
64, 0, 4, 4,
santa.x + 8, y + 5
)
spr(6, santa.x, y, 2, 2)
pal()
palt(0, false)
x, y = 24, 50
for i = 1, #shop do
local u = upgrades[shop[i]]
spr(u[1], x, y, 2, 2)
local price = tostr(u[2])
printol(price, x + 7 - #price * 2, 68, 10)
x += 32
end
local sel = upgrades[shop[selected]]
local name, desc = sel[3], sel[4]
printol(name, 64 - #name * 2, 85, 7)
local d1, d2 = sub(desc, 0, 28), sub(desc, 28, 999)
printol(d1, 64 - #d1 * 2, 95, 6)
printol(d2, 64 - #d2 * 2, 103, 6)
if cash >= sel[2] and t % 60 < 30 then
printol('press \x8E to purchase', 24, 112, 7)
elseif cash < sel[2] then
printol('you need more coins!', 24, 112, 6)
end
printol('press \x97 to skip', 30, 120, 6)
end
function init_game_over()
state = 'game_over'
game_over_t = 0
mksnow()
music(16)
end
function update_game_over()
t += 1
game_over_t += 1
upsnow()
end
function draw_game_over()
cls(5)
pal()
drsnow()
printol('game over', 46, min(game_over_t / 2, 16), 7)
if (game_over_t > 40) printol('gifts dropped: '.. stat_gifts, 16, 32, 7)
if (game_over_t > 70) printol('sweets destroyed: '.. stat_sweets, 16, 40, 8)
if (game_over_t > 100) printol('sweets missed: ' .. stat_missed_mints, 16, 48, 6)
if (game_over_t > 130) printol('coins collected: '.. stat_coins, 16, 56, 10)
if (game_over_t > 160) printol('coins missed: ' .. stat_missed_coins, 16, 64, 6)
if (game_over_t > 190) printol('coin damage: ' .. stat_coin_damage, 16, 72, 7)
if (game_over_t > 220) printol('ended on round ' .. round_number, 16, 80, 7)
if (game_over_t > 250) printol('thanks for playing!', 26, 112, 7)
if (game_over_t > 280) printol('tom wright - @thetomster3', 14, 120, 7)
end
function _init()
-- generate cached rotations
cacherot()
-- global frame timer, useful for simple animations
t=0
round_number = 1
cash = 0
stat_gifts = 0
stat_sweets = 0
stat_coins = 0
stat_missed_coins = 0
stat_coin_damage = 0
stat_missed_mints = 0
-- upgrade system: upgrades are defined in init_shop. when an upgrade is
-- purchased, its index in the upgrades list is added to the `bought` array.
-- logic throughout the game checks the array with have_upgrade and acts
-- accordingly.
bought = {}
-- collision helper
init_areas()
state = 'title'
init_title()
end
function _update60()
if state == 'title' then
update_title()
elseif state == 'game' then
update_game()
elseif state == 'shop' then
update_shop()
elseif state == 'game_over' then
update_game_over()
end
update_cpu = stat(1)
end
function _draw()
if state == 'title' then
draw_title()
elseif state == 'game' then
draw_game()
elseif state == 'shop' then
draw_shop()
elseif state == 'game_over' then
draw_game_over()
end
draw_cpu = stat(1) - update_cpu
max_cpu = max(max_cpu, stat(1))
-- printol('\x96'..flr(stat(1) * 100) .. '%', 26, 1, 12)
-- printol('\x85'..flr(update_cpu * 100) .. '%', 26, 9, 9)
-- printol('\x82'..flr(draw_cpu * 100) .. '%', 26, 17, 11)
-- printol('\x96'..flr(max_cpu * 100) .. '%', 76, 3, 8)
-- print(stat_gifts, 0, 108, 12)
-- print(stat_sweets, 0, 114, 12)
-- print(stat_coins, 0, 120, 12)
-- debug_areas()
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment