[object Object]

← back to Atmos Game

Upgrade procedural obstacle art (striped balloon, flapping seagull, finned blimp) + fix distance-score always-zero bug

d8e013a5348de88e855b8c628060ccee1119d795 · 2026-07-24 07:39:41 -0700 · Steve Abrams

Files touched

Diff

commit d8e013a5348de88e855b8c628060ccee1119d795
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 24 07:39:41 2026 -0700

    Upgrade procedural obstacle art (striped balloon, flapping seagull, finned blimp) + fix distance-score always-zero bug
---
 assets/gen/capture_burst.mjs |  29 +++++++++++++++++++
 assets/review5/01-start.png  | Bin 0 -> 1255340 bytes
 assets/review5/02-play.png   | Bin 0 -> 624082 bytes
 assets/review5/03-play2.png  | Bin 0 -> 1251476 bytes
 assets/review6/burst-00.png  | Bin 0 -> 1063529 bytes
 assets/review6/burst-01.png  | Bin 0 -> 1145577 bytes
 assets/review6/burst-02.png  | Bin 0 -> 1053852 bytes
 assets/review6/burst-03.png  | Bin 0 -> 1142851 bytes
 assets/review6/burst-04.png  | Bin 0 -> 1069215 bytes
 assets/review6/burst-05.png  | Bin 0 -> 1053246 bytes
 assets/review6/burst-06.png  | Bin 0 -> 985815 bytes
 assets/review6/burst-07.png  | Bin 0 -> 1067821 bytes
 index.html                   |  67 +++++++++++++++++++++++++++++++------------
 13 files changed, 78 insertions(+), 18 deletions(-)

diff --git a/assets/gen/capture_burst.mjs b/assets/gen/capture_burst.mjs
new file mode 100644
index 0000000..0fa81f7
--- /dev/null
+++ b/assets/gen/capture_burst.mjs
@@ -0,0 +1,29 @@
+import { createRequire } from 'module';
+const require = createRequire(import.meta.url);
+const { chromium } = require('/Users/macstudio3/.npm-global/lib/node_modules/playwright');
+import { mkdirSync } from 'fs';
+const OUT = process.env.OUT || '/Users/macstudio3/Projects/atmos-game/assets/review6';
+mkdirSync(OUT, { recursive: true });
+
+const browser = await chromium.launch({
+  channel: 'chrome', headless: true,
+  args: ['--use-gl=angle','--use-angle=swiftshader','--enable-unsafe-swiftshader','--ignore-gpu-blocklist','--enable-webgl']
+});
+const page = await browser.newPage({ viewport:{width:1280,height:800}, deviceScaleFactor:2 });
+const errs=[];
+page.on('console', m=>{ if(m.type()==='error') errs.push(m.text()); });
+page.on('pageerror', e=>errs.push('PAGEERR '+e.message));
+
+await page.goto('http://localhost:8087/?v=3', { waitUntil:'networkidle', timeout:60000 });
+await page.waitForTimeout(2500);
+await page.mouse.click(640,400);           // begin
+await page.keyboard.down('Space');          // hold fire
+// gentle vertical drift to sweep the field; grab a burst of frames
+for(let i=0;i<8;i++){
+  await page.mouse.move(640, 340 + (i%2?40:-40));
+  await page.waitForTimeout(850);
+  await page.screenshot({ path:`${OUT}/burst-${String(i).padStart(2,'0')}.png` });
+}
+await page.keyboard.up('Space');
+console.log('CONSOLE_ERRORS', JSON.stringify(errs.slice(0,12)));
+await browser.close();
diff --git a/assets/review5/01-start.png b/assets/review5/01-start.png
new file mode 100644
index 0000000..fcc06b0
Binary files /dev/null and b/assets/review5/01-start.png differ
diff --git a/assets/review5/02-play.png b/assets/review5/02-play.png
new file mode 100644
index 0000000..16e3360
Binary files /dev/null and b/assets/review5/02-play.png differ
diff --git a/assets/review5/03-play2.png b/assets/review5/03-play2.png
new file mode 100644
index 0000000..cba48e7
Binary files /dev/null and b/assets/review5/03-play2.png differ
diff --git a/assets/review6/burst-00.png b/assets/review6/burst-00.png
new file mode 100644
index 0000000..438c141
Binary files /dev/null and b/assets/review6/burst-00.png differ
diff --git a/assets/review6/burst-01.png b/assets/review6/burst-01.png
new file mode 100644
index 0000000..76f428b
Binary files /dev/null and b/assets/review6/burst-01.png differ
diff --git a/assets/review6/burst-02.png b/assets/review6/burst-02.png
new file mode 100644
index 0000000..2cd7b0c
Binary files /dev/null and b/assets/review6/burst-02.png differ
diff --git a/assets/review6/burst-03.png b/assets/review6/burst-03.png
new file mode 100644
index 0000000..c83aca0
Binary files /dev/null and b/assets/review6/burst-03.png differ
diff --git a/assets/review6/burst-04.png b/assets/review6/burst-04.png
new file mode 100644
index 0000000..4c37264
Binary files /dev/null and b/assets/review6/burst-04.png differ
diff --git a/assets/review6/burst-05.png b/assets/review6/burst-05.png
new file mode 100644
index 0000000..bc2ecb6
Binary files /dev/null and b/assets/review6/burst-05.png differ
diff --git a/assets/review6/burst-06.png b/assets/review6/burst-06.png
new file mode 100644
index 0000000..1c09f7c
Binary files /dev/null and b/assets/review6/burst-06.png differ
diff --git a/assets/review6/burst-07.png b/assets/review6/burst-07.png
new file mode 100644
index 0000000..ad4eca3
Binary files /dev/null and b/assets/review6/burst-07.png differ
diff --git a/index.html b/index.html
index 98497f5..6b33990 100644
--- a/index.html
+++ b/index.html
@@ -239,29 +239,60 @@ const TYPES = {
 const obstacles=[];   // {group,type,alive,...}
 
 function birdPrimitive(){
-  const g=new THREE.Group(), mat=new THREE.MeshStandardMaterial({color:0x243242,roughness:.9});
-  const wl=new THREE.Mesh(new THREE.BoxGeometry(2.2,.12,.9),mat), wr=wl.clone();
-  wl.position.x=-1.15; wr.position.x=1.15;
-  g.add(wl,wr,new THREE.Mesh(new THREE.SphereGeometry(.42,10,8),mat));
+  // low-poly seagull: tapered body + head + beak + tail, wings on shoulder pivots
+  const g=new THREE.Group();
+  const mat=new THREE.MeshStandardMaterial({color:0x2b3340,roughness:.85,metalness:.05});
+  const body=new THREE.Mesh(new THREE.SphereGeometry(.5,14,12),mat); body.scale.set(1,.62,1.7); g.add(body);
+  const head=new THREE.Mesh(new THREE.SphereGeometry(.3,12,10),mat); head.position.set(0,.14,1.0); g.add(head);
+  const beak=new THREE.Mesh(new THREE.ConeGeometry(.1,.36,6), new THREE.MeshStandardMaterial({color:0xf0a63c,roughness:.55}));
+  beak.rotation.x=Math.PI/2; beak.position.set(0,.11,1.42); g.add(beak);
+  const tail=new THREE.Mesh(new THREE.ConeGeometry(.36,.95,4),mat); tail.rotation.x=-Math.PI/2; tail.position.set(0,0,-1.15); g.add(tail);
+  // each wing is a mesh offset OUTWARD inside a pivot group placed at the shoulder,
+  // so rotating the pivot on z flaps the wing about the body — not about its own middle
+  function wing(side){
+    const pivot=new THREE.Group();
+    const w=new THREE.Mesh(new THREE.BoxGeometry(1.9,.08,.8),mat);
+    w.position.x=side*1.05; w.rotation.y=side*-.12;   // slight backward sweep
+    pivot.add(w); pivot.position.set(side*.34,.06,0); return pivot;
+  }
+  const wl=wing(-1), wr=wing(1); g.add(wl,wr);
   g.userData.wl=wl; g.userData.wr=wr; return g;
 }
 function balloonPrimitive(){
+  // real vertical gores (alternating colors) via sphere phi-slices + woven basket + suspension lines
   const g=new THREE.Group();
-  const hue=Math.random(); const col=new THREE.Color().setHSL(hue,.7,.55);
-  const env=new THREE.Mesh(new THREE.SphereGeometry(3,20,16), new THREE.MeshStandardMaterial({color:col,roughness:.5}));
-  env.scale.y=1.25; env.position.y=1.5; g.add(env);
-  const basket=new THREE.Mesh(new THREE.BoxGeometry(1,1,1), new THREE.MeshStandardMaterial({color:0x7a4a22,roughness:.9}));
-  basket.position.y=-2.4; g.add(basket);
+  const h=Math.random();
+  const cA=new THREE.Color().setHSL(h,.72,.55), cB=new THREE.Color().setHSL((h+.5)%1,.7,.62);
+  const N=8, R=3;
+  const env=new THREE.Group(); env.position.y=1.6; env.scale.y=1.18; g.add(env);
+  for(let i=0;i<N;i++){
+    const mat=new THREE.MeshStandardMaterial({color:i%2?cA:cB,roughness:.42,metalness:.04});
+    const gore=new THREE.Mesh(new THREE.SphereGeometry(R,6,20,(i/N)*Math.PI*2,(1/N)*Math.PI*2,0,Math.PI*0.9),mat);
+    env.add(gore);
+  }
+  const mouth=new THREE.Mesh(new THREE.CylinderGeometry(.55,.9,.5,12), new THREE.MeshStandardMaterial({color:cA,roughness:.5}));
+  mouth.position.y=-2.7; g.add(mouth);
+  const rope=new THREE.MeshStandardMaterial({color:0x5a4632,roughness:.9});
+  for(let k=0;k<4;k++){
+    const a=k/4*Math.PI*2, line=new THREE.Mesh(new THREE.CylinderGeometry(.03,.03,1.5,5),rope);
+    line.position.set(Math.cos(a)*.55,-3.5,Math.sin(a)*.55); g.add(line);
+  }
+  const basket=new THREE.Mesh(new THREE.CylinderGeometry(.55,.45,.75,8), new THREE.MeshStandardMaterial({color:0x8a5a2a,roughness:.95}));
+  basket.position.y=-4.35; g.add(basket);
   return g;
 }
 function blimpPrimitive(){
+  // elongated ellipsoid hull + nose cap + cruciform tail fins + gondola + hull band
   const g=new THREE.Group();
-  const body=new THREE.Mesh(new THREE.SphereGeometry(3,24,16), new THREE.MeshStandardMaterial({color:0xdad6cf,metalness:.2,roughness:.5}));
-  body.scale.set(3,1,1); g.add(body);
-  const fin=new THREE.Mesh(new THREE.BoxGeometry(.2,2,1.4), new THREE.MeshStandardMaterial({color:0xff7a59}));
-  fin.position.x=-8; g.add(fin);
-  const gon=new THREE.Mesh(new THREE.BoxGeometry(2,.8,.8), new THREE.MeshStandardMaterial({color:0x333}));
-  gon.position.y=-2.6; g.add(gon);
+  const skin=new THREE.MeshStandardMaterial({color:0xdcd8d0,metalness:.25,roughness:.42});
+  const body=new THREE.Mesh(new THREE.SphereGeometry(3,30,20),skin); body.scale.set(3,1,1); g.add(body);
+  const nose=new THREE.Mesh(new THREE.SphereGeometry(1.15,16,12),skin); nose.scale.set(1.5,1,1); nose.position.x=8.3; g.add(nose);
+  const accent=new THREE.MeshStandardMaterial({color:0xff7a59,roughness:.5});
+  const finV=new THREE.Mesh(new THREE.BoxGeometry(2.0,2.6,.18),accent); finV.position.set(-7.6,1.1,0); finV.rotation.z=-.32; g.add(finV);
+  const finH=new THREE.Mesh(new THREE.BoxGeometry(2.0,.18,2.6),accent); finH.position.set(-7.6,0,0); g.add(finH);
+  const band=new THREE.Mesh(new THREE.TorusGeometry(3,.14,8,44),accent); band.rotation.y=Math.PI/2; g.add(band);
+  const gon=new THREE.Mesh(new THREE.BoxGeometry(2.6,.75,.95), new THREE.MeshStandardMaterial({color:0x2c2c30,roughness:.6}));
+  gon.position.y=-2.5; g.add(gon);
   return g;
 }
 const PRIM = { bird:birdPrimitive, balloon:balloonPrimitive, blimp:blimpPrimitive };
@@ -319,7 +350,7 @@ const hpFill=document.getElementById('hpfill'), scoreEl=document.querySelector('
 function begin(){ if(state==='play')return; score=kills=dist=0; hp=100; firing=false; cooldown=0; target.x=0; target.y=1; plane.position.set(0,0,0);
   obstacles.forEach(o=>{o.alive=false;o.group.visible=false;}); missiles.forEach(m=>{m.userData.alive=false;m.visible=false;});
   state='play'; document.body.classList.add('playing'); startEl.classList.add('hidden'); overEl.classList.remove('show'); }
-function endGame(){ state='over'; document.body.classList.remove('playing'); document.getElementById('finalScore').textContent=score; document.getElementById('finalDist').textContent=Math.floor(dist/100); document.getElementById('finalKills').textContent=kills; overEl.classList.add('show'); }
+function endGame(){ state='over'; document.body.classList.remove('playing'); document.getElementById('finalScore').textContent=Math.round(score).toLocaleString(); document.getElementById('finalDist').textContent=Math.floor(dist/100); document.getElementById('finalKills').textContent=kills; overEl.classList.add('show'); }
 
 addEventListener('resize',()=>{ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); });
 
@@ -346,7 +377,7 @@ function tick(){
     plane.position.y+=Math.sin(t*2.2)*.004;
 
     cooldown-=dt; if(firing&&cooldown<=0){ fire(); cooldown=CFG.fireCooldown; }
-    dist+=worldSpeed*dt; score+=Math.floor(worldSpeed*dt*.1);
+    dist+=worldSpeed*dt; score+=worldSpeed*dt*.5;   // float accumulator; rounded at HUD (Math.floor per-frame was always 0)
 
     spawnTimer-=dt;
     if(spawnTimer<=0 && obstacles.filter(o=>o.alive).length < 16){ spawnObstacle(); spawnTimer=.5+Math.random()*.7; }
@@ -386,7 +417,7 @@ function tick(){
 
   if(flash>0){ flash=Math.max(0,flash-dt*2.2); scene.background.copy(SKY_BOT).lerp(new THREE.Color(0xff4433),flash*.5); } else scene.background.copy(SKY_BOT);
 
-  hpFill.style.width=hp+'%'; scoreEl.textContent=score; distEl.textContent=Math.floor(dist/100); altEl.textContent=(32000+Math.round(plane.position.y*140)).toLocaleString();
+  hpFill.style.width=hp+'%'; scoreEl.textContent=Math.round(score).toLocaleString(); distEl.textContent=Math.floor(dist/100); altEl.textContent=(32000+Math.round(plane.position.y*140)).toLocaleString();
   renderer.render(scene,camera);
 }
 function ITEMscale(o){ return o.type==='bird'?1:.4; }

← 89093ba Design-review pass (graphic-designer agent): IBL+ACES tonema  ·  back to Atmos Game  ·  (newest)