BASE · 2D MOBILE
The 2D mobile game base, remade beautiful: a dawn sky and every asset as a real 3D object on alpha-perfect pixels. Fork it and carve.
THE VISION
THE 2D MOBILE BASE — a gorgeous, complete 2D game foundation built through the process. Portrait 576x1024. A dawn sky (shared atmos-kit), field-owned platforms (pixels=hitbox), and every game asset rendered as a REAL 3D object on its own alpha-perfect pixels — player, coins, platforms all dimensional over the flat sky (2D+3D, one field engine). One node per subsystem. Beautiful by default; carve toward your vision. The manifest names every part; load-bearing parts refuse removal.
HOW TO PLAY
A / D or tap left/right — walk W / SPACE or tap the sky — jump R — restart Collect all the coins. This is a BASE: fork it and carve away what your vision doesn't need — worldData.baseManifest names every part and what's safe to remove. Assets are real 3D objects on 2D pixels; the sky pulls the shared atmos-kit.
— VISUAL SHADERS (WGSL) —
visual · base_stone
fn visual_base_stone(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let aspect = max(params.x, 1.0) / max(params.y, 1.0);
// rounded 3D slab: a height field raised in the middle, beveled at edges.
let ex = (1.0 - abs(uv.x)) * aspect; // aspect-corrected edge distance x
let ey = 1.0 - abs(uv.y);
let e = min(ex, ey);
let h = smoothstep(0.0, 0.55, e); // height: 0 at rim, 1 in body
// real surface normal from the height gradient (a genuine 3D relief)
let nx = -sign(uv.x) * (1.0 - smoothstep(0.0, 0.55, ex)) / aspect;
let ny = -sign(uv.y) * (1.0 - smoothstep(0.0, 0.55, ey));
let n = normalize(vec3f(nx, ny, 1.4));
let key = normalize(vec3f(0.35, -0.7, 0.6)); // dawn from upper-right (y down)
let dif = max(dot(n, key), 0.0);
let spec = pow(max(dot(reflect(-key, n), vec3f(0.0,0.0,1.0)), 0.0), 16.0);
var stone = mix(vec3f(0.30,0.26,0.30), vec3f(0.52,0.46,0.48), dif); // dawn-lit stone
if (uv.y < -0.55 && ey > 0.12) { stone = mix(stone, vec3f(0.55,0.70,0.40), 0.65); } // grassy top lip
stone += vec3f(1.0,0.9,0.8) * spec * 0.25;
stone *= (0.55 + 0.45 * h); // ambient occlusion into the bevel
return vec4f(stone, smoothstep(0.0, 0.06, e)); // alpha-perfect: cut at the rim
}
visual · base_sky
fn visual_base_sky(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let t = uv.y * 0.5 + 0.5; // 0 top .. 1 bottom
let wp = (uv * 0.5 + 0.5) * vec2f(576.0, 1024.0); // grid-ish px for features
// dawn: violet-blue top -> peach bottom, rose horizon seam
var col = mix(vec3f(0.15, 0.19, 0.42), vec3f(0.99, 0.83, 0.66), t);
col += vec3f(1.0, 0.60, 0.42) * pow(clamp(1.0 - abs(t - 0.66) * 3.4, 0.0, 1.0), 2.2) * 0.5;
// stars up high (small t)
col += vec3f(0.9, 0.93, 1.0) * step(0.987, ak_hash(floor(wp * 0.09))) * (0.4 + 0.4 * sin(time * 2.0)) * (1.0 - t) * 0.8;
// sun with bloom, upper-right
let sun = length(wp - vec2f(430.0, 250.0));
col += vec3f(1.0, 0.95, 0.82) * smoothstep(48.0, 26.0, sun) + vec3f(1.0, 0.80, 0.55) * exp(-sun * sun / 8600.0);
// one warm cloud shelf drifting
let c1 = smoothstep(0.52, 0.86, ak_fbm(wp * vec2f(0.0018, 0.0040) + vec2f(time * 0.006, 0.0))) * exp(-abs(t - 0.30) * 3.2);
col = mix(col, vec3f(1.0, 0.95, 0.90), c1 * 0.5);
col = max(col, vec3f(0.06));
return vec4f(col, 1.0);
}
visual · base_actors
fn visual_base_actors(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let vb = viewbox(); let wp = vb.xy + uv * vb.zw;
var col = behind; // start from what's behind (sky/platforms)
let key = normalize(vec3f(0.35, -0.7, 0.65)); // dawn key (y down)
// ── COINS: real 3D spinning gold discs (population kind 0), alpha-perfect ──
for (var i = 0; i < 24; i++) {
if (i >= popCount()) { break; }
let e = pop(i);
if (e.z > 0.5) { continue; }
let spin = abs(cos(time * 3.0 + e.w)); // 0..1 face-on..edge
let d = wp - e.xy;
let rx = 13.0 * max(spin, 0.12); let ry = 13.0;
let q = vec2f(d.x / rx, d.y / ry);
let dd = dot(q, q);
if (dd > 1.0) { continue; }
let nz = sqrt(1.0 - dd);
let n = normalize(vec3f(q.x * spin, q.y, nz + 0.6));
let dif = max(dot(n, key), 0.0);
let spec = pow(max(dot(reflect(-key, n), vec3f(0.,0.,1.)), 0.0), 24.0);
var g = mix(vec3f(0.75, 0.55, 0.12), vec3f(1.0, 0.90, 0.45), dif) + vec3f(1.0,0.95,0.7)*spec*0.8;
col = vec4f(g, 1.0); // opaque coin pixel
}
// ── THE PLAYER: a real 3D roundling sphere with a face, alpha-perfect ──
let pp = vec2f(uni(0), uni(1));
let pd = wp - pp;
let R = 16.0;
let rr = dot(pd, pd) / (R * R);
if (rr <= 1.0) {
let nz = sqrt(1.0 - rr);
let n = normalize(vec3f(pd.x / R, (pd.y / R) * (1.0 + uni(3) * 0.3), nz));
let dif = max(dot(n, key), 0.0);
let spec = pow(max(dot(reflect(-key, n), vec3f(0.,0.,1.)), 0.0), 28.0);
var c = mix(vec3f(0.85, 0.45, 0.22), vec3f(1.0, 0.78, 0.45), dif) + spec * 0.6;
// eyes
let eye = pd - vec2f(uni(2) * 5.0, -4.0);
if (length(eye - vec2f(4.0,0.0)) < 2.4 || length(eye + vec2f(4.0,0.0)) < 2.4) { c = vec3f(0.08); }
col = vec4f(c, 1.0);
}
// hit flash overlay
if (uni(5) > 0.01) { col = vec4f(mix(col.rgb, vec3f(1.0,0.4,0.3), uni(5)*0.5), 1.0); }
return col;
}
visual · base_bg
fn visual_base_bg(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
let g = uv.y * 0.5 + 0.5;
var col = mix(vec3f(0.075, 0.085, 0.115), vec3f(0.028, 0.033, 0.05), g);
col += vec3f(0.05, 0.06, 0.09) * (0.5 + 0.5 * sin(uv.x * 2.0 + uv.y * 3.0)) * 0.06;
col += 0.010 * (vnoise(uv * 34.0) - 0.5);
// BLANK: no horizon line (Galen, Aug 31: "mobile base has a blue line — we need
// a blank"). The old soft floor line at uv.y≈0.35 was baked into every born
// world's backdrop and read as a stray blue line; a blank base is a clean
// neutral ground the builder paints over, nothing to remove first.
return vec4f(col, 1.0);
}— SHADER MODULES —
module · atmos_kit
fn ak_hash(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn ak_noise(p: vec2f) -> f32 { let i=floor(p); let f=fract(p); let u=f*f*(3.0-2.0*f);
return mix(mix(ak_hash(i),ak_hash(i+vec2f(1.,0.)),u.x),mix(ak_hash(i+vec2f(0.,1.)),ak_hash(i+vec2f(1.,1.)),u.x),u.y); }
fn ak_fbm(p: vec2f) -> f32 { var v=0.0; var a=0.5; var q=p; for(var i=0;i<5;i++){v+=a*ak_noise(q);q=q*2.03+vec2f(1.7,9.2);a*=0.5;} return v; }
— STEP HOOKS (JAVASCRIPT) —
hook · player
const wd = sim.worldData;
wd.__fixedStep = 1/60; if (wd.__seed === undefined) wd.__seed = 11;
if (!wd.__p) wd.__p = { x: 288, y: 900, vx: 0, vy: 0, face: 1, ground: false, hop: 0, intent:{ax:0,jump:false} };
const P = wd.__p; const inp = wd.input || {}; const ptr = inp.pointer || {};
let ax = inp.moveX || 0;
let jump = !!(wd.key_w || wd.key_space || (inp.moveY||0) < -0.5);
if (ptr.down) { if ((ptr.y||0) < 340) jump = true; else ax = (ptr.x||288) < P.x-20 ? -1 : (ptr.x||288) > P.x+20 ? 1 : 0; }
P.intent.ax = Math.max(-1, Math.min(1, ax)); P.intent.jump = jump;
if (ax !== 0) P.face = ax > 0 ? 1 : -1;
hook · world
const wd = sim.worldData; const P = wd.__p; if (!P) return;
if (!wd.__phys) wd.__phys = { g: 1500 };
const dt2 = Math.min(dt, 1/30); const I = P.intent || {ax:0,jump:false};
P.vx += (I.ax * 900 - P.vx * 6) * dt2;
P.vy += wd.__phys.g * dt2;
if (I.jump && P.ground) { P.vy = -640; P.ground = false; (wd.__ev=wd.__ev||[]).push({t:'jump'}); }
P.x += P.vx * dt2; P.y += P.vy * dt2;
if (P.x < 20) { P.x = 20; P.vx = 0; } if (P.x > 556) { P.x = 556; P.vx = 0; }
P.ground = false;
for (const f of sim.fields.values()) {
const pr = f.properties || {};
const st = typeof pr.get === 'function' ? pr.get('standable') : pr.standable;
if (!st || f.shapeType !== 'rect') continue;
const sc = (f.transform && f.transform.scale) || 1;
const w = (f.w||0)*sc, top = (f.transform.y||0) - (f.h||0)*sc*0.5;
if (P.vy >= 0 && Math.abs(P.x - f.transform.x) < w*0.5 + 18 && P.y >= top-2 && P.y <= top + Math.max(14, P.vy*dt2+6)) { P.y = top; P.vy = 0; P.ground = true; }
}
P.hop = P.ground ? Math.max(0, (P.hop||0) - dt2*6) : Math.min(1, (P.hop||0) + dt2*4);
if (P.y > 1100) { (wd.__ev=wd.__ev||[]).push({t:'fell'}); P.x=288; P.y=900; P.vx=0; P.vy=0; }
hook · entities
const wd = sim.worldData; const P = wd.__p; if (!P) return;
if (!wd.__coins) wd.__coins = [{x:150,y:770},{x:430,y:660},{x:200,y:530},{x:440,y:400},{x:170,y:280},{x:288,y:910}].map(c=>({...c,got:false}));
for (const c of wd.__coins) { if (c.got) continue; if (Math.hypot(P.x-c.x, P.y-20-c.y) < 26) { c.got = true; (wd.__ev=wd.__ev||[]).push({t:'coin'}); } }
hook · rules
const wd = sim.worldData;
if (!wd.__rules) wd.__rules = { coins: 0, lives: 3, state: 0 };
const R = wd.__rules;
for (const ev of (wd.__ev||[])) { if (R.state!==0) break;
if (ev.t==='coin') { R.coins += 1; if (wd.__coins && wd.__coins.every(c=>c.got)) R.state = 1; }
if (ev.t==='fell') { R.lives -= 1; if (R.lives <= 0) R.state = 2; } }
const restart = wd.key_r || (wd.__uiClickT && wd.__uiClickT !== R.lc && wd.__uiClick === 'restart');
if (restart) { R.lc = wd.__uiClickT || 0; if (R.state!==0 || wd.key_r) { R.coins=0; R.lives=3; R.state=0;
if (wd.__coins) for (const c of wd.__coins) c.got=false; if (wd.__p) { wd.__p.x=288; wd.__p.y=900; wd.__p.vx=0; wd.__p.vy=0; } } }
hook · hud
const wd = sim.worldData; const R = wd.__rules;
const total = (wd.__coins||[]).length;
const rev = R ? (R.coins*100 + R.lives + R.state*10000) : -1;
if (rev === wd.__hudRev) return; wd.__hudRev = rev;
if (!R) { wd.ui = { rev:0, root:[] }; return; }
wd.ui = { rev, root: [
{ id:'lives', kind:'panel', anchor:{wx:0.03,wy:0.015}, align:'tl', glass:true, pad:7, children:[{kind:'text', text:'♥'.repeat(Math.max(0,R.lives))+'·'.repeat(Math.max(0,3-R.lives)), fontSize:14, color:'#ff8f7a'}] },
{ id:'coins', kind:'panel', anchor:{wx:0.97,wy:0.015}, align:'tr', glass:true, pad:7, children:[{kind:'text', text:'COINS '+R.coins+(total?'/'+total:''), fontSize:12, color:'#ffd27f'}] },
...(R.state===1 ? [{ id:'won', kind:'panel', anchor:{wx:0.5,wy:0.32}, align:'tc', glass:true, pad:10, children:[{kind:'text', text:'ALL COINS! YOU WIN', fontSize:15, color:'#ffe28a'}] },
{ id:'again', kind:'panel', anchor:{wx:0.5,wy:0.88}, align:'bc', glass:true, pad:8, children:[{id:'restart', kind:'button', text:'PLAY AGAIN', click:'restart'}] }] : []),
...(R.state===2 ? [{ id:'over', kind:'panel', anchor:{wx:0.5,wy:0.32}, align:'tc', glass:true, pad:10, children:[{kind:'text', text:'OUT OF HEARTS', fontSize:15, color:'#ff8866'}] },
{ id:'again', kind:'panel', anchor:{wx:0.5,wy:0.88}, align:'bc', glass:true, pad:8, children:[{id:'restart', kind:'button', text:'TRY AGAIN', click:'restart'}] }] : []),
] };
hook · net
// ── NET — this node owns SHARED STATE: when this world goes
// multiplayer (arena), what state is authoritative-server truth vs
// local cosmetic? Declare the world's mpManifest here as it grows.
// Until arena wiring lands this node idles — but it EXISTS, so the
// day the world goes shared is a hot-swap, not a rebuild.
hook · camera
// BASE camera — INTENTIONALLY inert. The engine frames a declared-rect
// world at exact cover (grid = viewport). A game built on the base can write
// wd.__camera to follow; the base must not, or it breaks the resting frame.
return;hook · fx
const wd = sim.worldData; if (!wd.__fx) wd.__fx = { flash:0 };
const dt2 = Math.min(dt,1/30); wd.__fx.flash = Math.max(0, wd.__fx.flash - dt2*2.5);
for (const ev of (wd.__ev||[])) if (ev.t==='fell') wd.__fx.flash = 1;
hook · audio
const wd = sim.worldData;
for (const ev of (wd.__ev||[])) { if (ev.t==='coin') wd.__play_sound={id:'coin'}; if (ev.t==='jump') wd.__play_sound={id:'jump'}; }
hook · save
const wd = sim.worldData; const R = wd.__rules;
if (R && (wd.__best === undefined || R.coins > wd.__best)) wd.__best = R.coins;
hook · uplink
const wd = sim.worldData;
const U = new Array(64).fill(0);
const P = wd.__p || { x:288, y:900, face:1, hop:0 };
U[0]=P.x; U[1]=P.y; U[2]=P.face||1; U[3]=P.hop||0;
U[5]=wd.__fx?wd.__fx.flash:0; U[7]=wd.__rules?wd.__rules.state:0;
wd.gpuUniforms = U;
const POP=[]; for (const c of (wd.__coins||[])) if (!c.got) POP.push(c.x,c.y,0,(c.x+c.y)*0.01);
wd.gpuPopulation = POP; wd.__ev = [];