Something went wrong. Try again.
a Bluesky client for PICO-8 picosky.vinnymac.dev
pico-8 simulation bluesky games
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436-- demoscene (firehose): valley→stream→sky→night, bluesky events rain as particles
parts={} -- firehose particle rainshoot={} -- shooting starsstars={} -- starfield (world space, high up)birds={} -- boids flock (screen space)demo_t=0 -- loop clock (s)df=0 -- frame counter (throttles the costly sun glow)demo_likes=0 -- cumulative firehose tallies, ones place (persist across loops)demo_posts=0demo_lk=0 -- thousands place (16.16 fixed-point caps a single int at ~32767)demo_pk=0cycle=87 -- loop length (s)demo_mb=0 -- prev mouse-button statefire_t=0 -- time() of last real (nonzero) firehose eventdemo_track=1 -- selected tune (persists across demo entries)track_lbl=0 -- show the tune name until this time()tunes={0,4,8,-1} -- music start patterns: gym,cdl,canon,offtunenames=split"gymnopedie,clair de lune,canon in d,music off"
function lerp(a,b,t) return a+(b-a)*t endfunction eio(t) return t*t*(3-2*t) endfunction clmp(t) return mid(0,t,1) end-- altitude 0 (ground) .. 1 (night), eased; drives the upward camera panfunction alt() return eio(clmp((demo_t-16)/34)) endfunction camtop() return lerp(312,0,alt()) endfunction fmtk(k,o) if k>0 then return k.."."..(o\100).."k" end return ""..oend
-- carry ones>=1000 into thousands so totals never hit the fixed-point capfunction roll() if demo_likes>=1000 then demo_lk+=demo_likes\1000 demo_likes%=1000 end if demo_posts>=1000 then demo_pk+=demo_posts\1000 demo_posts%=1000 endend
function play_tune() local m=tunes[demo_track] if m>=0 then music(m) else music(-1) end track_lbl=time()+2end
-- pause-menu music (slot 1) + theme (slot 2); music is demo-only, synced via _update change-detect.function music_label() return chr(141).." "..tunenames[demo_track] end -- 141=♪function theme_label() return "theme: "..tc("day","night") end
function music_menu(b) -- ←=prev, →/🅾/❎=next; "music off" is part of the cycle if b&1>0 then demo_track=(demo_track+2)%4+1 else demo_track=demo_track%4+1 end if scr==scr_demo then play_tune() end -- audible only in the demo; elsewhere just arm it menuitem(nil,music_label()) return trueendfunction theme_menu() theme=1-theme sfx(0) menuitem(nil,theme_label()) return trueend-- feed switcher (slot 3): mutates active_feed; timeline reloads after menu closes.function feed_label() return "feed: "..feedsm[active_feed] endfunction feed_menu(b) if #feeds<2 then return true end if b&1>0 then active_feed=(active_feed-2+#feeds)%#feeds+1 else active_feed=active_feed%#feeds+1 end sfx(0) menuitem(nil,feed_label()) return trueend
function enter_demo() scr=scr_demo demo_t=0 df=0 parts={} shoot={} demo_likes=0 demo_posts=0 demo_lk=0 demo_pk=0 -- cumulative this session stars={} for _=1,40 do add(stars,{x=rnd(128),y=rnd(120),p=rnd(1)}) end init_birds() build_lut() enqueue(op_req_demo) demo_mb=0 fire_t=time() play_tune() track_lbl=time()+5 -- keep readable for ~2s after the 3s fade-inend
function exit_demo() parts={} pal() music(-1) open_timeline(false) sfx(1)end
function quit_demo() enqueue(op_req_demo_stop) exit_demo()end
-- per-kind firehose particle: {y, vy base, vy*rnd range, colour}. kind 4 + kind-1 colour flip stay inline.sd={{-4,.4,.5,8},{132,-.5,-.6,12},{132,-.4,-.5,11}}function spawn(kind,n) -- stop spawning ~3s before credits vanish so particles drift off before the seam. if demo_t>=72 then return end local cap=alt()>0.8 and 14 or 46 for _=1,min(n,4) do if #parts>=cap then return end local p={kind=kind,t=rnd(1),x=rnd(128)} if kind<4 then local d=sd[kind] p.y=d[1] p.vy=d[2]+d[3]*rnd(1) p.c=d[4] if kind==1 and rnd(1)<.5 then p.c=14 end else p.y=20+rnd(90) p.vy=-.2-rnd(.3) p.c=10 end add(parts,p) endend
function on_fire(po,li,re,fo,de) if po+li+re+fo>0 then fire_t=time() end demo_likes+=li demo_posts+=po roll() spawn(1,li) spawn(2,po) spawn(3,re) spawn(4,fo) for _=1,min(de,4) do if #parts>0 then deli(parts,1) end endend
function sim_fire() local li=flr(rnd(3)) local po=rnd(1)<0.35 and 1 or 0 demo_likes+=li demo_posts+=po roll() spawn(1,li) spawn(2,po) spawn(3,rnd(1)<0.12 and 1 or 0) spawn(4,rnd(1)<0.25 and 1 or 0)end
function update_demo() local mx,my,mb=stat(32),stat(33),stat(34)&1 local tap=mb==1 and demo_mb==0 and mx>=116 and mx<=127 and my>=1 and my<=12 demo_mb=mb -- ❎ maps to x/v/m; route desktop keys by char so "m" cycles music, x/v quits. while stat(30) do local c=stat(31) if c=="m" then demo_track=demo_track%4+1 play_tune() elseif c=="x" or c=="v" then quit_demo() return end end if tap or (mobile() and btnp(5)) then quit_demo() return end demo_t+=1/30 df=(df+1)%2 if demo_t>=cycle then demo_t-=cycle end update_birds() if time()-fire_t>1.5 then sim_fire() end if alt()>0.85 and rnd(1)<0.06 and #shoot<3 then add(shoot,{x=rnd(110),y=rnd(50),vx=2.5+rnd(2),vy=1+rnd(1.5)}) end for s in all(shoot) do s.x+=s.vx s.y+=s.vy if s.x>134 or s.y>80 then del(shoot,s) end end for p in all(parts) do p.t+=1/30 p.y+=p.vy p.x+=sin(p.t*0.4)*0.5 if p.kind==1 then if p.y>134 then del(parts,p) end elseif p.y<-6 then del(parts,p) end endend
-- one continuous looped path: off-left -> rise to centre -> swell -> out left.-- start (j=0) and end (j=1) coincide so the loop is seamless.function bfly_pos() local bob=sin(time()*0.8)*3 local a=alt() if demo_t<75 then -- fly in from off-left, skim the valley, then rise return lerp(-20,64,eio(clmp(demo_t/24))),lerp(108,30,a)+bob,lerp(0.8,1,a) elseif demo_t<81 then -- finale: swell at centre with a slow flap local u=(demo_t-75)/6 return 64,lerp(30,56,eio(u)),lerp(1,5,eio(u)) else -- fly out off-screen left (loop seam) local u=(demo_t-81)/6 return lerp(64,-20,eio(u)),lerp(56,108,eio(u)),lerp(5,0.8,eio(u)) endend
function bfly_sx(spd) return bfly_seq[flr(time()*spd)%4+1]*8end
function draw_bfly() local x,y,sc=bfly_pos() local w=16*sc local dx,dy=x-w/2,y-w/2 bflysh(bfly_sx(demo_t>75 and 2.5 or 6),dx,dy,w)end
-- procedural fire-spiral sun: rotating arm in fire colours + bright corefunction fire_sun(cx,cy) for i=0,7 do -- subtle flicker corona, close to the disc local a=i/8+time()*0.1 local r=8+sin(time()*3+i)*1.5 pset(cx+cos(a)*r,cy+sin(a)*r,9) end circfill(cx,cy,7,9) -- orange rim circfill(cx,cy,5,10) -- yellow circfill(cx,cy,3,7) -- bright coreend
gpat={0x0000,0xa0a0,0xa5a5,0xf5f5,0xffff}-- vertical dithered gradient: colour b at the top, a at the bottomfunction grad(y0,y1,a,b) local h=(y1-y0)/5 for i=1,5 do fillp(gpat[i]) rectfill(0,y0+(i-1)*h,127,y0+i*h,a+b*16) end fillp()end
-- tall atmosphere gradient drawn full so upward pan reveals it continuously.function draw_atmos() rectfill(0,0,127,150,0) -- deep space grad(150,212,1,0) -- space -> night blue grad(212,274,13,1) -- night -> dusk indigo grad(274,372,12,13) -- dusk -> light-blue day grad(372,440,7,12) -- day -> bright hazy horizonend
function cloud(x,y) circfill(x,y,6,7) circfill(x+7,y+1,7,7) circfill(x+14,y,5,7) rectfill(x,y,x+14,y+5,7)end
function draw_clouds() local s=demo_t*10 cloud((s*0.6)%190-30,272) -- drifts right cloud((-s*0.42+120)%190-30,300) -- drifts left cloud((s*0.3+50)%190-30,332) -- drifts right (slow)end
function draw_ground() local s=demo_t*14 for x=0,127 do -- per column, back-to-front: far hill, near hill+grass, stream rectfill(x,402+sin((x+s*0.3)/40)*7,x,440,3) rectfill(x,410+sin((x+s*0.7)/30)*8,x,440,11) local ry=424+sin((x+s)/46)*4 rectfill(x,ry,x,ry+5,1) pset(x,ry,sin((x+s)/8)>0.55 and 12 or 1) end for i=0,11 do -- foreground flowers pset((12+i*12-s)%150-8,434+sin(i)*3,8+i%2*6) endend
function draw_night_sky() if alt()<0.3 then return end for st in all(stars) do if sin(time()*0.6+st.p)>-0.2 then pset(st.x,st.y,rnd(1)<0.3 and 6 or 7) end end local mx=18+max(0,demo_t-34)*0.4 circfill(mx,24,8,7) circfill(mx+3,22,7,0) for s in all(shoot) do line(s.x,s.y,s.x-s.vx*3,s.y-s.vy*3,7) pset(s.x,s.y,10) endend
-- credits during the night hold; show the running firehose totalsfunction draw_credits() local a=clmp((demo_t-54)/3) local y=lerp(132,52,eio(a)) print_c("picosky",y,12) print_c("by vinnymac",y+9,7) print_c(fmtk(demo_lk,demo_likes).." likes "..fmtk(demo_pk,demo_posts).." posts",y+22,12) print_c("live stats provided by",y+31,6) print_c("bluesky firehose",y+38,6) print_c("thanks to bluesky, zep,",y+50,6) print_c("and daniel linssen for m3x6",y+58,6)end
function draw_hud() spr(10,4,3) spr(8,42,3) print(fmtk(demo_lk,demo_likes),13,4,7) print(fmtk(demo_pk,demo_posts),51,4,7) if time()-fire_t>1.5 then print("sim",102,4,9) endend
-- round exit button + aligned esc labelfunction draw_exit() local cx,cy=122,6 circfill(cx,cy,3,6) -- pearl button, half size line(cx-1,cy-1,cx+1,cy+1,5) line(cx-1,cy+1,cx+1,cy-1,5) print("esc",cx-18,cy-2,6)end
function heart(x,y,c) circfill(x-1,y,1,c) circfill(x+1,y,1,c) line(x-2,y+1,x+2,y+1,c) line(x-1,y+2,x+1,y+2,c) pset(x,y+3,c)end
function draw_parts() for p in all(parts) do if p.kind==1 then heart(p.x,p.y,p.c) elseif p.kind==4 then pset(p.x,p.y,p.c) pset(p.x,p.y-1,10) else circ(p.x,p.y,2,p.c) pset(p.x-1,p.y-1,7) end endend
-- a small flock of tiny "v" birds: simplified boids (separate/cohere/align)function init_birds() birds={} for _=1,6 do add(birds,{x=rnd(120),y=34+rnd(24),vx=0.3+rnd(0.3),vy=rnd(0.4)-0.2}) endend
function update_birds() if alt()>0.6 then return end for b in all(birds) do local cx,cy,vx,vy,n=0,0,0,0,0 for o in all(birds) do local dx,dy=o.x-b.x,o.y-b.y local d=dx*dx+dy*dy if o~=b and d<400 then cx+=o.x cy+=o.y vx+=o.vx vy+=o.vy n+=1 if d<144 then b.vx-=dx*0.025 b.vy-=dy*0.025 end end end if n>0 then b.vx+=(cx/n-b.x)*0.002+(vx/n-b.vx)*0.06 b.vy+=(cy/n-b.y)*0.002+(vy/n-b.vy)*0.06 end b.vx=mid(0.2,b.vx,1) b.vy=mid(-0.5,b.vy,0.5) b.x+=b.vx b.y+=b.vy b.y=mid(22,b.y,74) if b.x>134 then b.x=-6-rnd(16) b.y=34+rnd(24) end endend
function draw_birds() if alt()>0.55 then return end for b in all(birds) do local w=sin(time()*4+b.x)>0 and 1 or 0 line(b.x-2,b.y-w,b.x,b.y,5) line(b.x,b.y,b.x+2,b.y-w,5) endend
-- screen palette darkening: lv 0=bright..6=black, smooth, no dither.drk=split"0,0,1,1,2,1,5,6,2,4,9,3,13,1,2,5" -- darken LUT, 1-based (drk[c+1] for colour c)function setlight(lv) for c=0,15 do local d=c for _=1,lv do d=drk[d+1] end pal(c,d,1) endend
-- byte->darkened-byte LUTs (one per level) in free RAM, for the per-pixel passfunction build_lut() for lv=0,6 do for b=0,255 do local lo,hi=b%16,b\16 for _=1,lv do lo=drk[lo+1] hi=drk[hi+1] end poke(0x4300+lv*256+b,lo+hi*16) end endend
-- sun light: scene drawn, then darken every pixel by a far from the sun, easing-- to 0 at the centre (concentric rings). rows beyond the glow take a fast path.function light_pass(sx,sy,a) local r=40 local rr=r*r local cb=sx\2 for y=0,127 do local dy=y-sy local dy2=dy*dy local row=0x6000+y*64 if dy2>=rr then local l=0x4300+a*256 for ad=row,row+63 do poke(ad,peek(l+peek(ad))) end else local hw={[0]=0,[a+1]=64} for k=1,a do local q=(r*k/a)*(r*k/a)-dy2 hw[k]=q>0 and sqrt(q)\2 or 0 end for lv=0,a do local l=0x4300+lv*256 local i,o=hw[lv],hw[lv+1] for ad=row+mid(0,cb+i,63),row+mid(0,cb+o-1,63) do poke(ad,peek(l+peek(ad))) end for ad=row+mid(0,cb-o+1,63),row+mid(0,cb-max(i,1),63) do poke(ad,peek(l+peek(ad))) end end end endend
-- loop-seam fade: to black over the last 3s, back from black over the first 3sfunction seam() if demo_t>=cycle-3 then return flr((demo_t-cycle+3)/3*6+.5) end if demo_t<3 then return flr((1-demo_t/3)*6+.5) end return 0end
-- dusk dimming: gradual, held through the day->night pan, released to 0 only-- once the night sky is fully in view (gated on alt, so no bright-blue flash)function amb() local t=demo_t if t<20 then return 0 end if t<28 then return flr((t-20)/8*2+.5) end if alt()<0.55 then return 2 end return flr(max(0,(0.66-alt())/0.11)*2+.5)end
function draw_demo() local la,ls=amb(),seam() local suny=340-camtop() local glow=ls==0 and la>0 and suny<134 if glow and df~=0 then return end -- update the sun glow every other frame cls(0) camera(0,camtop()) draw_atmos() draw_night_sky() fire_sun(96-demo_t*0.4,340) draw_clouds() draw_ground() camera() draw_birds() draw_parts() draw_bfly() if demo_t>54 and demo_t<75 then draw_credits() end if demo_t<53 then draw_hud() end if glow then setlight(0) light_pass(96-demo_t*0.4,suny,la) else setlight(max(la,ls)) end if time()<track_lbl and seam()==0 then print_c(tunenames[demo_track],100,7) print_c(mobile() and "pause to change track" or "m: change music",108,6) end draw_exit()end