-- O32 reusable helpers + a complete small collection game. -- Paste this file into an O32 code tab. Helpers use radians, +Z forward, -- +Y up, and new tables so caller-owned vertices are never mutated. local o3 = {} local function rsin(v) return -sin(v/6.283185307179586) end local function rcos(v) return cos(v/6.283185307179586) end function o3.lerp(a,b,t) return a+(b-a)*t end function o3.overlap(a,b) return a.x<=b.x+b.w and a.x+a.w>=b.x and a.y<=b.y+b.h and a.y+a.h>=b.y and a.z<=b.z+b.d and a.z+a.d>=b.z end function o3.transform(v, pose) local yaw,pitch=pose.yaw or 0,pose.pitch or 0 local cy,sy,cp,sp=rcos(yaw),rsin(yaw),rcos(pitch),rsin(pitch) local scale=pose.scale or 1 local vx,vy,vz=v.x*scale,v.y*scale,v.z*scale local x,z=vx*cy+vz*sy,-vx*sy+vz*cy return {x=x+(pose.x or 0),y=vy*cp-z*sp+(pose.y or 0), z=vy*sp+z*cp+(pose.z or 0),u=v.u or 0,v=v.v or 0,s=v.s or 0} end local function o16at(addr) local value=peek2(addr) if value>=32768 then return value-65536 end return value end -- Append an imported mesh-bank range to a scene. The bank stores Y-down -- model coordinates; the scene helpers use Y-up world coordinates. Pose -- scale converts model units to game units (for example 0.01 for a 192-unit -- import). The material selects the texture page at draw time. function o3.mesh(scene,start,count,pose,material) assert(start==flr(start) and count==flr(count) and start>=0 and count>=0 and start+count<=21845,'mesh range outside bank') pose=pose or {}; material=material or {} local shade=material.shade or 0 for face=start,start+count-1 do local base=0x40000+face*24 local verts={} for i=0,2 do local p=base+i*6 verts[i+1]=o3.transform({x=o16at(p),y=-o16at(p+2),z=o16at(p+4), u=peek(base+18+i*2),v=peek(base+19+i*2),s=shade},pose) end scene[#scene+1]={vertices=verts,color=material.color or 7,tex=material.tex} end return count end function o3.view(v,camera) local x,y,z=v.x-camera.x,v.y-camera.y,v.z-camera.z local cy,sy=rcos(camera.yaw or 0),rsin(camera.yaw or 0) local cp,sp=rcos(camera.pitch or 0),rsin(camera.pitch or 0) local xx,zz=x*cy-z*sy,x*sy+z*cy return {x=xx,y=y*cp+zz*sp,z=-y*sp+zz*cp,u=v.u or 0,v=v.v or 0,s=v.s or 0} end local function intersection(a,b,near) local t=(near-a.z)/(b.z-a.z) return {x=o3.lerp(a.x,b.x,t),y=o3.lerp(a.y,b.y,t),z=near, u=o3.lerp(a.u or 0,b.u or 0,t),v=o3.lerp(a.v or 0,b.v or 0,t), s=o3.lerp(a.s or 0,b.s or 0,t)} end -- Clip a triangle before projection; the result has zero, three or four -- polygon vertices. UVs/shades interpolate at the new near-plane edges. function o3.clip(vertices,near) assert(near>0,'near plane must be positive') local out={} if #vertices==0 then return out end local previous=vertices[#vertices] for _,current in ipairs(vertices) do local a,b=previous.z>=near,current.z>=near if a~=b then out[#out+1]=intersection(previous,current,near) end if b then out[#out+1]=current end previous=current end return out end function o3.project(v,camera) assert(v.z>0,'clip before projection') local f=camera.focal or 160 return (camera.cx or 160)+v.x*f/v.z,(camera.cy or 120)-v.y*f/v.z end -- faces: {vertices={a,b,c}, color=7, tex=nil}. Texture coordinates are -- texels; tex may be 0..3. Painter sorting is approximate for intersecting -- geometry. Split such geometry or design scenes without cyclic overlap. function o3.draw(faces,camera) local queue={} for _,face in ipairs(faces) do local vertices={} for _,v in ipairs(face.vertices) do vertices[#vertices+1]=o3.view(v,camera) end local clipped=o3.clip(vertices,camera.near or 1) for i=2,#clipped-1 do local a,b,c=clipped[1],clipped[i],clipped[i+1] queue[#queue+1]={a=a,b=b,c=c,depth=(a.z+b.z+c.z)/3, color=face.color or 7,tex=face.tex,order=#queue+1} end end table.sort(queue,function(a,b) if a.depth==b.depth then return a.orderb.depth end) for _,face in ipairs(queue) do local a,b,c=face.a,face.b,face.c local ax,ay=o3.project(a,camera) local bx,by=o3.project(b,camera) local cx,cy=o3.project(c,camera) color(face.color) ttri(ax,ay,a.u or 0,a.v or 0,bx,by,b.u or 0,b.v or 0, cx,cy,c.u or 0,c.v or 0,face.tex,a.s or 0,b.s or 0,c.s or 0) end end -- DEMO: collect all six crystals. Arrow keys move; Z restarts after winning. -- Game progress stores only the best completion time; each run starts fresh. -- saveid: omibit.o32.crystal-starter.v1 -- omibit-scene:begin -- omibit-scene:data [] local o3_scene_objects={ } -- omibit-scene:end cartdata() local player,camera,crystals,ticks,won,best function _init() player={x=0,y=0,z=0,w=0.8,h=1.8,d=0.8} camera={x=0,y=5,z=-9,pitch=0.32,near=0.5,focal=180} crystals={} for i=1,6 do crystals[i]={x=(i%3-1)*3,y=0,z=2+flr((i-1)/3)*4,w=1,h=2,d=1} end ticks=0;won=false;best=dget(0) end function _update() if won then if btnp(4) then _init() end; return end ticks=ticks+1 if btn(0) then player.x=player.x-0.1 end if btn(1) then player.x=player.x+0.1 end if btn(2) then player.z=player.z+0.1 end if btn(3) then player.z=player.z-0.1 end player.x=mid(-6,player.x,6);player.z=mid(-2,player.z,10) local remaining=0 for _,c in ipairs(crystals) do if not c.collected and o3.overlap(player,c) then c.collected=true end if not c.collected then remaining=remaining+1 end end if remaining==0 then won=true if best==0 or ticks