App

Play this beta version of Daphnia Dash by copying and pasting into your html browser (e.g. Chrome)

  <div class="wrap">
    <h1 style="margin:0">Daphnia Dash — playable prototype</h1>
    <div class="meta">Use Space / ↑ to swim up, ↓ to dive. Tap on mobile.</div>
    <canvas id="game" width="900" height="300"></canvas>
    <div class="controls">
      <button id="startBtn">Start / Restart</button>
      <div class="hint">Score: <span id="score">0</span> ⋅ Best: <span id="best">0</span></div>
    </div>
    <div class="hint">Obstacles: hydra (green tentacles) and small fish. Collect algae for +points.</div>
  </div>

  <script>
  /* Daphnia Dash — single-file HTML/JS prototype
     - Canvas-based endless runner (swimmer)
     - No external assets
     - Touch + keyboard support
  */
  (()=>{
    const canvas = document.getElementById('game');
    const ctx = canvas.getContext('2d');
    const W = canvas.width, H = canvas.height;

    // Game state
    let running = false;
    let score = 0, best = localStorage.getItem('daphnia_best')||0;
    let speed = 2.2; // world speed
    const gravity = 0.22;

    // Daphnia player
    const player = {
      x: 120, y: H/2, vy:0, radius:14, alive:true
    };

    // Entities
    const obstacles = [];
    const pickups = [];
    let spawnTimer = 0;
    let dayNightTimer = 0;

    // Controls
    let pressing = false;

    // UI refs
    const scoreEl = document.getElementById('score');
    const bestEl = document.getElementById('best');
    bestEl.textContent = best;

    // Sound setup
    const jumpSound = new Audio('https://freesound.org/data/previews/66/66717_931655-lq.mp3');
    const gameOverSound = new Audio('https://freesound.org/data/previews/331/331912_3248244-lq.mp3');
    let jumpSoundPlaying = false;

    function reset(){
      running = true; score=0; speed=2.2; player.y = H/2; player.vy=0; player.alive=true;
      obstacles.length=0; pickups.length=0; spawnTimer=0; dayNightTimer=0;
      scoreEl.textContent = 0;
      loop();
    }

    document.getElementById('startBtn').addEventListener('click', ()=>{ reset(); });

    // Input
    window.addEventListener('keydown', e=>{
      if(e.code==='Space' || e.key==='ArrowUp') { press(); e.preventDefault(); }
      if(e.key==='ArrowDown') { dive(); }
      if(pressing && !jumpSoundPlaying){
          jumpSound.currentTime = 0; 
          jumpSound.play(); 
          jumpSoundPlaying = true;
      } else if(!pressing){
          jumpSoundPlaying = false;
      }
    });
    window.addEventListener('keyup', e=>{ if(e.code==='Space' || e.key==='ArrowUp') release(); });
    canvas.addEventListener('touchstart', e=>{ press(); e.preventDefault(); });
    canvas.addEventListener('touchend', e=>{ release(); e.preventDefault(); });
    canvas.addEventListener('mousedown', e=>{ press(); });
    canvas.addEventListener('mouseup', e=>{ release(); });

    function press(){ pressing = true; player.vy = -3.6; }
    function release(){ pressing = false; }
    function dive(){ player.vy += 1.6; }

    // Helpers
    function rand(min,max){return Math.random()*(max-min)+min}

    // Spawn obstacles & pickups
    function spawn(dt){
      spawnTimer -= dt;
      if(spawnTimer<=0){
        spawnTimer = rand(900,1600) - Math.min(score,200); // faster spawns as score grows
        // choose obstacle type
        if(Math.random()<0.65){
          // hydra (tangled tentacles) - tall thin
          obstacles.push({type:'hydra', x:W+40, w:18, h:rand(40,140), gap:rand(60,110)});
        } else {
          // fish - small fast moving
          obstacles.push({type:'fish', x:W+40, w:36, h:18, y:rand(70,H-70)});
        }
        // occasionally spawn algae pickup
        if(Math.random()<0.28){ pickups.push({x:W+80, y:rand(60,H-80), r:9, claimed:false}); }
      }
    }

    // Collision detection simple circle vs rect
    function circleRect(cx,cy,r,rx,ry,rw,rh){
      const nearestX = Math.max(rx, Math.min(cx, rx+rw));
      const nearestY = Math.max(ry, Math.min(cy, ry+rh));
      const dx = cx - nearestX; const dy = cy - nearestY;
      return (dx*dx + dy*dy) < (r*r);
    }

    // Draw water plants background
    function drawBackground(t){
      // subtle waves via gradient already set by CSS; draw floating particles/bubbles
      for(let i=0;i<6;i++){
        const bx = (t*0.03 + i*120) % (W+60) - 30;
        ctx.beginPath(); ctx.globalAlpha = 0.06; ctx.arc(bx, 40 + (i%3)*18, 18,0,Math.PI*2); ctx.fillStyle='#ffffff'; ctx.fill(); ctx.globalAlpha =1;
      }
      // sandy bottom
      ctx.fillStyle='rgba(18,10,6,0.06)'; ctx.fillRect(0,H-20,W,20);
    }

    // Draw Daphnia (stylized)
    function drawDaphnia(x,y,angle=0){
      ctx.save(); ctx.translate(x,y); ctx.rotate(angle);
      // carapace (oval)
      ctx.beginPath(); ctx.ellipse(0,0,16,11,0,0,Math.PI*2); ctx.fillStyle='#fff9e6'; ctx.fill();
      ctx.strokeStyle='#0b4b4b'; ctx.lineWidth=1; ctx.stroke();
      // eye
      ctx.beginPath(); ctx.arc(3,-2,2.6,0,Math.PI*2); ctx.fillStyle='#08303a'; ctx.fill();
      // tail-spine
      ctx.beginPath(); ctx.moveTo(-14,2); ctx.quadraticCurveTo(-22,6,-28,3); ctx.stroke();
      // antennae
      ctx.beginPath(); ctx.moveTo(7,-8); ctx.quadraticCurveTo(14,-18,20,-16); ctx.moveTo(6,-6); ctx.quadraticCurveTo(12,-12,18,-10); ctx.stroke();
      ctx.restore();
    }

    // Draw obstacle hydra
    function drawHydra(o){
      // base stem
      ctx.fillStyle='#255b2b'; ctx.fillRect(o.x, H - 20 - o.h, o.w, o.h);
      // tentacles (simple bezier lines)
      ctx.strokeStyle='#1a6a2b'; ctx.lineWidth=3;
      for(let i=0;i<5;i++){
        const sx = o.x + i*(o.w/4);
        ctx.beginPath(); ctx.moveTo(sx, H - 20 - o.h);
        ctx.quadraticCurveTo(sx-12 + i*6, H - 10 - o.h/2, sx - 20 + i*8, H-20);
        ctx.stroke();
      }
    }

    // Draw fish obstacle
    function drawFish(o){
      ctx.save(); ctx.translate(o.x, o.y);
      ctx.beginPath(); ctx.ellipse(0,0, o.w/2, o.h/2, 0,0,Math.PI*2); ctx.fillStyle='#ffb86b'; ctx.fill();
      ctx.beginPath(); ctx.moveTo(-o.w/2,0); ctx.lineTo(-o.w/2-8,-6); ctx.lineTo(-o.w/2-8,6); ctx.closePath(); ctx.fill();
      ctx.restore();
    }

    // Draw algae pickup
    function drawAlgae(p){
      ctx.save(); ctx.translate(p.x,p.y);
      ctx.beginPath(); ctx.moveTo(0,0); ctx.bezierCurveTo(-6,-6,-10,-16,-2,-20); ctx.bezierCurveTo(4,-10,10,-6,6,0); ctx.fillStyle='#6ab04c'; ctx.fill();
      ctx.restore();
    }

    // Update world
    let last = performance.now();
    function loop(now=performance.now()){
      if(!running) return;
      const dt = now - last; last = now;
      // update timers
      dayNightTimer += dt;
      // speed up slowly
      speed += dt * 0.00005;

      spawn(dt);

      // physics: player
      player.vy += gravity;
      player.y += player.vy;
      // clamp
      if(player.y < 18) { player.y = 18; player.vy = 0; }
      if(player.y > H-28) { player.y = H-28; player.vy = 0; }

      // move obstacles
      for(let i=obstacles.length-1;i>=0;i--){
        const o = obstacles[i];
        o.x -= speed * (o.type==='fish'?1.6:1);
        if(o.type==='hydra'){
          // gap is the safe space above bottom
          const rx = o.x; const ry = H - 20 - o.h; const rw = o.w; const rh = o.h;
          // check collision against carapace circle
          if(circleRect(player.x, player.y, player.radius, rx, ry, rw, rh)) {
            player.alive = false; running = false; endGame(); return; }
        } else if(o.type==='fish'){
          if(circleRect(player.x, player.y, player.radius, o.x - o.w/2, o.y - o.h/2, o.w, o.h)) { player.alive = false; running = false; endGame(); return; }
        }
        if(o.x < -80) obstacles.splice(i,1);
      }

      // move pickups
      for(let i=pickups.length-1;i>=0;i--){
        const p = pickups[i]; p.x -= speed*1.1;
        const d2 = (player.x - p.x)*(player.x - p.x) + (player.y - p.y)*(player.y - p.y);
        if(d2 < (player.radius + p.r)*(player.radius + p.r)){
          score += 12; pickups.splice(i,1); continue;
        }
        if(p.x < -40) pickups.splice(i,1);
      }

      // scoring by distance
      score += Math.floor(dt * 0.02 * (speed/2));
      scoreEl.textContent = score;

      // render
      ctx.clearRect(0,0,W,H);
      // subtle night overlay
      const nightAlpha = Math.max(0, Math.sin(dayNightTimer*0.0006))*0.15;
      drawBackground(dayNightTimer);

      // draw pickups
      for(const p of pickups) drawAlgae(p);

      // draw obstacles
      for(const o of obstacles){ if(o.type==='hydra') drawHydra(o); else drawFish(o); }

      // draw player with small bobbing angle
      const angle = Math.sin(performance.now()*0.006) * 0.08;
      drawDaphnia(player.x, player.y, angle);

      // HUD
      ctx.fillStyle='#023544'; ctx.font='14px sans-serif'; ctx.fillText('Score: '+score, 12,22);

      // loop
      if(running) requestAnimationFrame(loop);
    }

    function endGame(){
      // show game over overlay
      ctx.fillStyle='rgba(0,0,0,0.35)'; ctx.fillRect(0,0,W,H);
      ctx.fillStyle='white'; ctx.font='28px sans-serif'; ctx.fillText('Game Over', W/2 -74, H/2 -6);
      ctx.font='14px sans-serif'; ctx.fillText('Click Start/Restart to play again', W/2 -116, H/2 +18);
      if(score > best){ best = score; localStorage.setItem('daphnia_best', best); bestEl.textContent = best; }
      gameOverSound.currentTime = 0; 
      gameOverSound.play();    
    }

    // One-time draw (before start)
    (function intro(){
      ctx.clearRect(0,0,W,H);
      drawBackground(0);
      drawDaphnia(player.x, player.y);
      ctx.fillStyle='#023544'; ctx.font='16px sans-serif'; ctx.fillText('Press Start to play', 16, 36);
    })();

  })();
  </script>
</body>
</html>