🎮ArcadeLab

Cosmo Runner 3D

by BlazePanther14
495 lines20.9 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Cosmo Runner 3D</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
  html, body { height:100%; overflow:hidden; background:#87ceeb; font-family:-apple-system,system-ui,sans-serif; touch-action:none; user-select:none; }
  #canvas3d { position:absolute; inset:0; display:block; }
  #overlay { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; color:#fff; background:rgba(3,4,15,0.86); padding:24px; z-index:10; }
  .screen { display:flex; flex-direction:column; align-items:center; }
  .screen h1 { font-size:30px; margin-bottom:6px; letter-spacing:1px; color:#ffd23f; text-shadow:0 0 12px rgba(255,210,63,0.6); }
  .screen p { font-size:14px; opacity:0.85; margin-bottom:14px; max-width:300px; line-height:1.5; }
  .score { font-size:18px; margin-bottom:16px; color:#5ce1ff; }
  button { font-size:17px; font-weight:700; padding:13px 32px; border:none; border-radius:30px; background:#ffd23f; color:#3a2a00; box-shadow:0 6px 0 #c99f1e; cursor:pointer; margin:6px 0; }
  button:active { transform:translateY(4px); box-shadow:0 2px 0 #c99f1e; }
  button.secondary { background:transparent; color:#5ce1ff; box-shadow:none; border:2px solid #5ce1ff; padding:10px 26px; font-size:14px; }
  #leaderboardList { list-style:none; width:280px; margin-bottom:16px; max-height:260px; overflow-y:auto; }
  #leaderboardList li { display:flex; justify-content:space-between; padding:7px 10px; background:rgba(255,255,255,0.06); border-radius:8px; margin-bottom:5px; font-size:14px; }
  #leaderboardList li .rank { color:#ffd23f; font-weight:700; width:24px; }
  #leaderboardList li .name { flex:1; text-align:left; padding-left:6px; }
  #leaderboardList .empty { opacity:0.6; font-size:13px; padding:14px; }
  #nameEntry { display:flex; gap:8px; margin-bottom:8px; }
  #nameInput { font-size:16px; padding:10px 12px; border-radius:20px; border:2px solid #5ce1ff; background:rgba(255,255,255,0.08); color:#fff; width:150px; text-align:center; }
  #hud { position:absolute; top:14px; left:0; right:0; text-align:center; color:#fff; font-weight:700; font-size:26px; text-shadow:0 2px 6px rgba(0,0,0,0.5); z-index:5; pointer-events:none; }
  #muteBtn { position:absolute; top:12px; right:12px; z-index:6; width:40px; height:40px; border-radius:50%; padding:0; margin:0; background:rgba(255,255,255,0.18); color:#fff; box-shadow:none; font-size:18px; display:flex; align-items:center; justify-content:center; }
  #hint { position:absolute; bottom:6px; left:0; right:0; text-align:center; color:rgba(255,255,255,0.75); font-size:11px; z-index:5; text-shadow:0 1px 3px rgba(0,0,0,0.5); }
</style>
</head>
<body>
<canvas id="canvas3d"></canvas>
<div id="hud">0</div>
<button id="muteBtn">🔊</button>
<div id="hint">Свайп влево/вправо — смена полосы • вверх — прыжок • вниз — подкат</div>

<div id="overlay">
  <div id="startScreen" class="screen">
    <h1>COSMO RUNNER</h1>
    <p>Беги по трём полосам, перепрыгивай ящики, подкатывайся под балки и уворачивайся от стен. Свайпы на телефоне, стрелки/WASD на компьютере.</p>
    <div class="score" id="hi">Рекорд: 0</div>
    <button id="startBtn">Играть</button>
    <button id="showLeaderboardBtn" class="secondary">Таблица лидеров</button>
  </div>
  <div id="gameOverScreen" class="screen" style="display:none">
    <h1>СТОЛКНОВЕНИЕ!</h1>
    <p id="goStats">Счёт: 0</p>
    <div id="nameEntry" style="display:none">
      <input id="nameInput" maxlength="12" placeholder="Твоё имя">
    </div>
    <button id="saveScoreBtn" style="display:none">Сохранить результат</button>
    <button id="restartBtn">Заново</button>
    <button id="goLeaderboardBtn" class="secondary">Таблица лидеров</button>
  </div>
  <div id="leaderboardScreen" class="screen" style="display:none">
    <h1>ТОП-10</h1>
    <ol id="leaderboardList"></ol>
    <button id="backBtn">Назад</button>
  </div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function(){
  // ---------- UI refs ----------
  const overlay = document.getElementById('overlay');
  const startScreen = document.getElementById('startScreen');
  const gameOverScreen = document.getElementById('gameOverScreen');
  const leaderboardScreen = document.getElementById('leaderboardScreen');
  const startBtn = document.getElementById('startBtn');
  const restartBtn = document.getElementById('restartBtn');
  const showLeaderboardBtn = document.getElementById('showLeaderboardBtn');
  const goLeaderboardBtn = document.getElementById('goLeaderboardBtn');
  const backBtn = document.getElementById('backBtn');
  const saveScoreBtn = document.getElementById('saveScoreBtn');
  const nameEntry = document.getElementById('nameEntry');
  const nameInput = document.getElementById('nameInput');
  const hiEl = document.getElementById('hi');
  const goStats = document.getElementById('goStats');
  const leaderboardList = document.getElementById('leaderboardList');
  const muteBtn = document.getElementById('muteBtn');
  const hud = document.getElementById('hud');

  // ---------- sound ----------
  let audioCtx = null, muted = false;
  try { muted = localStorage.getItem('runner_muted') === '1'; } catch(e){}
  function updateMuteBtn(){ muteBtn.textContent = muted ? '🔇' : '🔊'; }
  updateMuteBtn();
  function ensureAudio(){
    if (!audioCtx){ try { audioCtx = new (window.AudioContext||window.webkitAudioContext)(); } catch(e){ return; } }
    if (audioCtx.state === 'suspended') audioCtx.resume();
  }
  muteBtn.addEventListener('click', ()=>{
    muted = !muted;
    try { localStorage.setItem('runner_muted', muted?'1':'0'); } catch(e){}
    updateMuteBtn();
  });
  function tone(freq,dur,type,vol,glideTo){
    if (muted || !audioCtx) return;
    const t0=audioCtx.currentTime;
    const osc=audioCtx.createOscillator(), gain=audioCtx.createGain();
    osc.type=type||'square'; osc.frequency.setValueAtTime(freq,t0);
    if (glideTo) osc.frequency.exponentialRampToValueAtTime(glideTo,t0+dur);
    gain.gain.setValueAtTime(vol||0.12,t0);
    gain.gain.exponentialRampToValueAtTime(0.001,t0+dur);
    osc.connect(gain).connect(audioCtx.destination);
    osc.start(t0); osc.stop(t0+dur);
  }
  function noiseBurst(dur,vol){
    if (muted || !audioCtx) return;
    const size=Math.floor(audioCtx.sampleRate*dur);
    const buffer=audioCtx.createBuffer(1,size,audioCtx.sampleRate);
    const data=buffer.getChannelData(0);
    for (let i=0;i<size;i++){ data[i]=(Math.random()*2-1)*(1-i/size); }
    const src=audioCtx.createBufferSource(); src.buffer=buffer;
    const gain=audioCtx.createGain();
    gain.gain.setValueAtTime(vol||0.2,audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+dur);
    src.connect(gain).connect(audioCtx.destination); src.start();
  }
  const sfx = {
    jump: ()=> tone(440,0.1,'square',0.08,660),
    slide: ()=> tone(220,0.1,'square',0.07,150),
    coin: ()=> { tone(880,0.06,'square',0.09,1200); setTimeout(()=>tone(1200,0.06,'square',0.08),50); },
    crash: ()=> { noiseBurst(0.4,0.3); tone(90,0.5,'sawtooth',0.2,40); },
    laneswitch: ()=> tone(330,0.05,'square',0.05)
  };

  // ---------- leaderboard ----------
  function loadLB(){ try { return JSON.parse(localStorage.getItem('runner_leaderboard')||'[]'); } catch(e){ return []; } }
  function saveLB(list){ try { localStorage.setItem('runner_leaderboard', JSON.stringify(list)); } catch(e){} }
  function qualifies(score){ const l=loadLB(); return l.length<10 || score>l[l.length-1].score; }
  function addScore(name,score){
    const list=loadLB();
    list.push({ name:(name||'Игрок').slice(0,12), score });
    list.sort((a,b)=>b.score-a.score);
    const trimmed=list.slice(0,10);
    saveLB(trimmed);
    return trimmed;
  }
  function escapeHtml(s){ return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
  function renderLB(){
    const list=loadLB();
    leaderboardList.innerHTML='';
    if (!list.length){ leaderboardList.innerHTML='<div class="empty">Пока нет результатов — беги первым!</div>'; return; }
    list.forEach((e,i)=>{
      const li=document.createElement('li');
      li.innerHTML='<span class="rank">'+(i+1)+'</span><span class="name">'+escapeHtml(e.name)+'</span><span>'+e.score+'</span>';
      leaderboardList.appendChild(li);
    });
  }
  function loadBest(){ try { return parseInt(localStorage.getItem('runner_best')||'0',10); } catch(e){ return 0; } }
  function saveBest(v){ try { localStorage.setItem('runner_best', String(v)); } catch(e){} }
  let best = loadBest();
  hiEl.textContent = 'Рекорд: ' + best;

  function showScreen(name){
    startScreen.style.display = name==='start' ? 'flex' : 'none';
    gameOverScreen.style.display = name==='gameover' ? 'flex' : 'none';
    leaderboardScreen.style.display = name==='leaderboard' ? 'flex' : 'none';
    overlay.style.display = 'flex';
  }
  let returnToGameOver = false;
  showLeaderboardBtn.addEventListener('click', ()=>{ returnToGameOver=false; renderLB(); showScreen('leaderboard'); });
  goLeaderboardBtn.addEventListener('click', ()=>{ returnToGameOver=true; renderLB(); showScreen('leaderboard'); });
  backBtn.addEventListener('click', ()=> showScreen(returnToGameOver?'gameover':'start'));

  // ---------- three.js setup ----------
  const canvas = document.getElementById('canvas3d');
  const renderer = new THREE.WebGLRenderer({ canvas, antialias:true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio||1, 2));
  renderer.setSize(window.innerWidth, window.innerHeight);

  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x8fd3ff);
  scene.fog = new THREE.Fog(0x8fd3ff, 22, 78);

  const camera = new THREE.PerspectiveCamera(62, window.innerWidth/window.innerHeight, 0.1, 200);
  camera.position.set(0, 4.6, 7);

  const hemi = new THREE.HemisphereLight(0xffffff, 0x445566, 0.9);
  scene.add(hemi);
  const sun = new THREE.DirectionalLight(0xffffff, 0.8);
  sun.position.set(5, 10, 5);
  scene.add(sun);

  window.addEventListener('resize', ()=>{
    camera.aspect = window.innerWidth/window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  });

  // ground with scrolling stripe texture
  const stripeCanvas = document.createElement('canvas');
  stripeCanvas.width = 64; stripeCanvas.height = 256;
  const sctx = stripeCanvas.getContext('2d');
  sctx.fillStyle = '#5a6270'; sctx.fillRect(0,0,64,256);
  sctx.fillStyle = '#767f8f';
  for (let y=0;y<256;y+=32){ sctx.fillRect(0,y,64,16); }
  sctx.fillStyle = '#e8d24a';
  sctx.fillRect(0,0,4,256); sctx.fillRect(60,0,4,256);
  const groundTex = new THREE.CanvasTexture(stripeCanvas);
  groundTex.wrapS = THREE.RepeatWrapping; groundTex.wrapT = THREE.RepeatWrapping;
  groundTex.repeat.set(1, 60);
  const groundMat = new THREE.MeshLambertMaterial({ map: groundTex });
  const groundGeo = new THREE.PlaneGeometry(7, 320);
  const ground = new THREE.Mesh(groundGeo, groundMat);
  ground.rotation.x = -Math.PI/2;
  ground.position.set(0, 0, -100);
  scene.add(ground);

  // side rails
  const railMat = new THREE.MeshLambertMaterial({ color: 0x334155 });
  [-3.7, 3.7].forEach(x=>{
    const rail = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.6, 320), railMat);
    rail.position.set(x, 0.3, -100);
    scene.add(rail);
  });

  // ---------- player ----------
  const LANE_X = [-1.8, 0, 1.8];
  const player = {
    lane: 1, x: 0, y: 0, vy: 0,
    isJumping: false, isSliding: false, slideTime: 0,
    alive: true
  };
  const GRAVITY = -32, JUMP_VELOCITY = 11.5, SLIDE_DURATION = 0.55;

  const playerGroup = new THREE.Group();
  const bodyMat = new THREE.MeshLambertMaterial({ color: 0xff6a3d });
  const limbMat = new THREE.MeshLambertMaterial({ color: 0x3d5aff });
  const headMat = new THREE.MeshLambertMaterial({ color: 0xffd7a8 });

  const torso = new THREE.Mesh(new THREE.BoxGeometry(0.62, 0.8, 0.36), bodyMat);
  torso.position.y = 0.95;
  playerGroup.add(torso);
  const head = new THREE.Mesh(new THREE.SphereGeometry(0.28, 12, 12), headMat);
  head.position.y = 1.62;
  playerGroup.add(head);
  const legL = new THREE.Mesh(new THREE.BoxGeometry(0.24, 0.62, 0.24), limbMat);
  legL.position.set(-0.16, 0.31, 0);
  playerGroup.add(legL);
  const legR = new THREE.Mesh(new THREE.BoxGeometry(0.24, 0.62, 0.24), limbMat);
  legR.position.set(0.16, 0.31, 0);
  playerGroup.add(legR);
  const armL = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.6, 0.18), bodyMat);
  armL.position.set(-0.44, 0.95, 0);
  playerGroup.add(armL);
  const armR = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.6, 0.18), bodyMat);
  armR.position.set(0.44, 0.95, 0);
  playerGroup.add(armR);

  scene.add(playerGroup);

  // ---------- obstacles & coins ----------
  const obstacles = [];
  const coins = [];
  const SPAWN_Z = -75;
  const REMOVE_Z = 6;

  const crateGeo = new THREE.BoxGeometry(1.3, 1.05, 1.3);
  const crateMat = new THREE.MeshLambertMaterial({ color: 0x8b5a2b });
  const beamGeo = new THREE.BoxGeometry(1.55, 0.4, 0.6);
  const beamMat = new THREE.MeshLambertMaterial({ color: 0x2e3a59 });
  const wallGeo = new THREE.BoxGeometry(1.6, 2.3, 0.5);
  const wallMat = new THREE.MeshLambertMaterial({ color: 0xd23a3a });
  const coinGeo = new THREE.TorusGeometry(0.26, 0.09, 8, 16);
  const coinMat = new THREE.MeshLambertMaterial({ color: 0xffd700 });

  function makeObstacleMesh(type){
    if (type === 'jump') { const m = new THREE.Mesh(crateGeo, crateMat); m.position.y = 0.52; return m; }
    if (type === 'duck') { const m = new THREE.Mesh(beamGeo, beamMat); m.position.y = 1.75; return m; }
    const m = new THREE.Mesh(wallGeo, wallMat); m.position.y = 1.15; return m;
  }

  function spawnPattern(){
    const laneIdx = [0,1,2];
    for (let i=laneIdx.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [laneIdx[i],laneIdx[j]]=[laneIdx[j],laneIdx[i]]; }
    const numBlocked = Math.random() < 0.6 ? 1 : 2;
    const blocked = laneIdx.slice(0, numBlocked);
    const open = laneIdx.slice(numBlocked);
    const type = ['jump','duck','wall'][Math.floor(Math.random()*3)];

    for (const lane of blocked){
      const mesh = makeObstacleMesh(type);
      mesh.position.x = LANE_X[lane];
      mesh.position.z = SPAWN_Z;
      scene.add(mesh);
      obstacles.push({ mesh, lane, type, resolved:false });
    }
    if (Math.random() < 0.75){
      for (const lane of open){
        for (let k=0;k<4;k++){
          const c = new THREE.Mesh(coinGeo, coinMat);
          c.rotation.x = Math.PI/2;
          c.position.set(LANE_X[lane], 1.05, SPAWN_Z + k*1.7);
          scene.add(c);
          coins.push({ mesh:c, lane, collected:false });
        }
      }
    }
  }

  // ---------- game state ----------
  let running = false;
  let distance = 0, score = 0, speed = 15;
  const MAX_SPEED = 32;
  let spawnAcc = 0, nextSpawnAt = 18;

  function resetGame(){
    for (const o of obstacles) scene.remove(o.mesh);
    for (const c of coins) scene.remove(c.mesh);
    obstacles.length = 0; coins.length = 0;
    player.lane = 1; player.x = 0; player.y = 0; player.vy = 0;
    player.isJumping = false; player.isSliding = false; player.slideTime = 0;
    playerGroup.position.set(0,0,0);
    playerGroup.scale.set(1,1,1);
    distance = 0; score = 0; speed = 15;
    spawnAcc = 0; nextSpawnAt = 18;
    running = true;
    hud.style.display = 'block';
    hud.textContent = '0';
  }

  function tryJump(){
    if (!running) return;
    if (!player.isJumping && !player.isSliding){
      player.isJumping = true; player.vy = JUMP_VELOCITY; sfx.jump();
    }
  }
  function trySlide(){
    if (!running) return;
    if (!player.isJumping && !player.isSliding){
      player.isSliding = true; player.slideTime = SLIDE_DURATION; sfx.slide();
    }
  }
  function moveLane(dir){
    if (!running) return;
    const newLane = player.lane + dir;
    if (newLane >= 0 && newLane <= 2){ player.lane = newLane; sfx.laneswitch(); }
  }

  // ---------- input ----------
  let touchStartX=0, touchStartY=0, touchActive=false;
  canvas.addEventListener('touchstart', (e)=>{
    ensureAudio();
    const t = e.changedTouches[0];
    touchStartX = t.clientX; touchStartY = t.clientY; touchActive = true;
  }, {passive:true});
  canvas.addEventListener('touchend', (e)=>{
    if (!touchActive) return;
    touchActive = false;
    const t = e.changedTouches[0];
    const dx = t.clientX - touchStartX, dy = t.clientY - touchStartY;
    if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 32){
      moveLane(dx > 0 ? 1 : -1);
    } else if (dy < -32){
      tryJump();
    } else if (dy > 32){
      trySlide();
    }
  }, {passive:true});

  window.addEventListener('keydown', (e)=>{
    ensureAudio();
    if (e.code==='ArrowLeft'||e.code==='KeyA') moveLane(-1);
    else if (e.code==='ArrowRight'||e.code==='KeyD') moveLane(1);
    else if (e.code==='ArrowUp'||e.code==='KeyW'||e.code==='Space'){ e.preventDefault(); tryJump(); }
    else if (e.code==='ArrowDown'||e.code==='KeyS') trySlide();
  });

  // ---------- update ----------
  const clock = new THREE.Clock();
  let runTime = 0;

  function update(dt){
    if (!running) return;
    runTime += dt;

    // difficulty
    speed = Math.min(MAX_SPEED, 15 + distance*0.012);
    distance += speed * dt;
    score = Math.floor(distance);
    hud.textContent = score;

    // lane movement (smooth)
    const targetX = LANE_X[player.lane];
    player.x += (targetX - player.x) * Math.min(1, dt*10);

    // jump physics
    if (player.isJumping){
      player.vy += GRAVITY * dt;
      player.y += player.vy * dt;
      if (player.y <= 0){ player.y = 0; player.isJumping = false; player.vy = 0; }
    }
    // slide timer
    if (player.isSliding){
      player.slideTime -= dt;
      if (player.slideTime <= 0){ player.isSliding = false; }
    }

    playerGroup.position.x = player.x;
    playerGroup.position.y = player.y;
    playerGroup.scale.y = player.isSliding ? 0.55 : 1;

    // running limb animation
    const swing = Math.sin(runTime * 14) * 0.5;
    legL.rotation.x = swing; legR.rotation.x = -swing;
    armL.rotation.x = -swing; armR.rotation.x = swing;

    // ground scroll
    groundTex.offset.y -= speed * dt * 0.05;

    // spawn
    spawnAcc += speed * dt;
    if (spawnAcc >= nextSpawnAt){
      spawnAcc = 0; nextSpawnAt = 15 + Math.random()*8;
      spawnPattern();
    }

    // move obstacles
    for (const o of obstacles){
      o.mesh.position.z += speed * dt;
      if (!o.resolved && o.mesh.position.z > -1.0 && o.mesh.position.z < 1.0 && o.lane === player.lane){
        let hit = false;
        if (o.type === 'jump') hit = !(player.isJumping && player.y > 0.55);
        else if (o.type === 'duck') hit = !player.isSliding;
        else hit = true;
        if (hit){ o.resolved = true; crash(); }
        else o.resolved = true;
      }
    }
    for (let i=obstacles.length-1;i>=0;i--){
      if (obstacles[i].mesh.position.z > REMOVE_Z){ scene.remove(obstacles[i].mesh); obstacles.splice(i,1); }
    }

    // coins
    for (const c of coins){
      c.mesh.position.z += speed * dt;
      c.mesh.rotation.z += dt * 5;
      if (!c.collected && c.lane === player.lane && Math.abs(c.mesh.position.z) < 1.0){
        c.collected = true;
        score += 5;
        scene.remove(c.mesh);
        sfx.coin();
      }
    }
    for (let i=coins.length-1;i>=0;i--){
      if (coins[i].collected || coins[i].mesh.position.z > REMOVE_Z){
        if (coins[i].mesh.parent) scene.remove(coins[i].mesh);
        coins.splice(i,1);
      }
    }

    // camera follow
    camera.position.x += (player.x*0.5 - camera.position.x) * Math.min(1, dt*4);
    camera.lookAt(player.x*0.3, 1.3, -8);
  }

  function crash(){
    running = false;
    sfx.crash();
    if (score > best){ best = score; saveBest(best); }
    hiEl.textContent = 'Рекорд: ' + best;
    goStats.textContent = 'Счёт: ' + score;
    hud.style.display = 'none';
    if (qualifies(score)){
      nameEntry.style.display = 'flex'; saveScoreBtn.style.display = 'inline-block'; nameInput.value='';
    } else {
      nameEntry.style.display = 'none'; saveScoreBtn.style.display = 'none';
    }
    showScreen('gameover');
  }

  saveScoreBtn.addEventListener('click', ()=>{
    addScore(nameInput.value.trim(), score);
    nameEntry.style.display='none'; saveScoreBtn.style.display='none';
    renderLB(); returnToGameOver = true; showScreen('leaderboard');
  });

  startBtn.addEventListener('click', ()=>{ ensureAudio(); overlay.style.display='none'; resetGame(); });
  restartBtn.addEventListener('click', ()=>{ ensureAudio(); overlay.style.display='none'; resetGame(); });

  function animate(){
    const dt = Math.min(0.05, clock.getDelta());
    update(dt);
    renderer.render(scene, camera);
    requestAnimationFrame(animate);
  }
  hud.style.display = 'none';
  animate();
})();
</script>
</body>
</html>

Game Source: Cosmo Runner 3D

Creator: BlazePanther14

Libraries: three

Complexity: complex (495 lines, 20.9 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: cosmo-runner-3d-blazepanther14" to link back to the original. Then publish at arcadelab.ai/publish.