public/index.html

分割 2 / 6 · 元ファイル 176〜409行付近

原文の連続部分です。長い圧縮行は行の途中で分割する場合があります。全分割を順に連結するとファイル全文になります。

   ★長さはメートル。画面の左端が0m、右端が7.8m。px へ直すのはここだけの仕事 */
var stage=document.getElementById('stage'), cv=document.getElementById('cv'), g2=cv.getContext('2d');
var WC=ZWORLD.C;
var W=390,H=700,L={},stars=[],bldg=[],cloud=[];
function calcL(){
  var floorY=H-Math.max(64,H*0.17);
  var ppm=W/WC.VIEW_M;
  return { ppm:ppm, floorY:floorY,
    hr:0.294*ppm,                 /* シュールキャラの頭の半径(身長1.72m=5.85r) */
    stoneR:Math.max(3.2,0.11*ppm),
    maxPull:Math.min(205,H*0.34) };
}
function MX(m){ return m*L.ppm; }
function MY(m){ return L.floorY-m*L.ppm; }
function layout(){
  W=stage.clientWidth||390; H=stage.clientHeight||700;
  var dpr=Math.min(3,window.devicePixelRatio||1);
  cv.width=Math.round(W*dpr); cv.height=Math.round(H*dpr);
  g2.setTransform(dpr,0,0,dpr,0,0);
  L=calcL(); build(); makeBG();
}
function makeBG(){
  stars=[];
  for(var i=0;i<64;i++) stars.push({ x:Math.random()*W, y:Math.random()*L.floorY*0.74,
    r:0.3+Math.random()*1.1, a:0.18+Math.random()*0.55 });
  bldg=[];
  var x=-10;
  while(x<W+20){ var w=40+Math.random()*70, h=(0.9+Math.random()*1.9)*L.ppm;
    bldg.push({x:x,w:w,h:h}); x+=w+2+Math.random()*8; }
  cloud=[];
  for(var j=0;j<5;j++) cloud.push({ x:Math.random()*W, y:L.floorY*(0.16+Math.random()*0.42),
    w:70+Math.random()*130, h:10+Math.random()*16, a:0.035+Math.random()*0.045 });
}

/* ===== 石の物理(フリースローの流用。ここは1文字も作り変えていない) ===== */
var Mt=window.Matter;
var STONE={ restitution:0.46, friction:0.30, frictionAir:0.0022, density:0.0016,
            collisionFilter:{group:-1} };
var eng=null, acc=0, STEP=1000/60;
var S=[];
function build(){
  if(eng){ try{ Mt.Events.off(eng); Mt.Composite.clear(eng.world,false); Mt.Engine.clear(eng); }catch(e){} }
  eng=Mt.Engine.create();
  eng.gravity.y=(9.8*L.ppm)/1000;
  var floor=Mt.Bodies.rectangle(W/2,L.floorY+200,W*6,400,
    { isStatic:true, label:'floor', restitution:0.46, friction:0.30 });
  Mt.Composite.add(eng.world,floor);
  Mt.Events.on(eng,'collisionStart',function(ev){
    for(var i=0;i<ev.pairs.length;i++){
      var p=ev.pairs[i];
      var st=(p.bodyA.label==='stone')?p.bodyA:((p.bodyB.label==='stone')?p.bodyB:null);
      if(!st) continue;
      var v=Math.abs(st.velocity.y)*60;
      st.zgB=(st.zgB||0)+1;
      if(v>55){ seGround(v); dust(st.position.x,L.floorY,st.zgRock?7:3); }
    }
  });
  S.length=0;
}

/* ===== 世界 ===== */
var world=new ZWORLD.World({sim:'full'});
var meId='solo';
var NET={ on:false, ws:null, room:'', tries:0, mode:'off', ping:0 };

/* ===== 当たり(メートル→px) ===== */
function zGeo(z){
  var r=L.hr, feet=L.floorY, x=MX(z.x);
  var hy=feet-4.85*r, shY=hy+1.30*r, bot=shY+2.40*r;
  return { r:r, x:x, feet:feet, hy:hy, shY:shY, bot:bot, hr:r*1.04,
    bx0:x-1.16*r, bx1:x+1.16*r, by0:shY-0.10*r, by1:bot,
    lx0:x-0.66*r, lx1:x+0.80*r, ly0:bot, ly1:feet+r*0.10 };
}
/* カラスの当たりと大きさ。0.95だと夜空で小石と見分けが付かなかったので1.28にした(2026-09-13 実測) */
function cGeo(c){ return { x:MX(c.x), y:MY(c.y), r:L.hr*1.28 }; }

function segCircle(x0,y0,x1,y1,cx,cy,r){
  var dx=x1-x0, dy=y1-y0, fx=x0-cx, fy=y0-cy;
  var a=dx*dx+dy*dy, b=2*(fx*dx+fy*dy), c=fx*fx+fy*fy-r*r;
  if(a<1e-9) return c<=0?0:-1;
  var d=b*b-4*a*c; if(d<0) return -1;
  d=Math.sqrt(d);
  var t1=(-b-d)/(2*a), t2=(-b+d)/(2*a);
  if(t1>=0&&t1<=1) return t1;
  if(t1<0&&t2>=0) return 0;
  return -1;
}
function segRect(x0,y0,x1,y1,ax,ay,bx,by){
  var dx=x1-x0, dy=y1-y0, t0=0, t1=1;
  var p=[-dx,dx,-dy,dy], q=[x0-ax,bx-x0,y0-ay,by-y0];
  for(var i=0;i<4;i++){
    if(p[i]===0){ if(q[i]<0) return -1; }
    else{ var t=q[i]/p[i];
      if(p[i]<0){ if(t>t1) return -1; if(t>t0) t0=t; }
      else{ if(t<t0) return -1; if(t<t1) t1=t; } }
  }
  return t0;
}
/* ★この関数がゲームの芯★
   倒せるのは「頭の丸へ、上から、急な角度で入った石」だけ。
   それ以外(体・足・頭の横・寝た角度)は全部ノックバックで終わる。
   ただし**大岩だけは例外**で、どこに当たっても倒せる(K1指示)。
   カラスは空の敵なので、どこに当たっても落ちる(地上と別の仕事にして2人の分担を作る)。 */
var TOP_STEEP=0.60, TOP_BAND=0.25;
function hitTest(s){
  if(s.cool>0) return;
  var x0=s.px, y0=s.py, b=s.b, x1=b.position.x, y1=b.position.y;
  var vx=b.velocity.x*60, vy=b.velocity.y*60;
  var bt=2, bz=null, bk='', bc=null, i;
  for(i=0;i<world.cs.length;i++){
    var c=world.cs[i]; if(c.dead) continue;
    var G2=cGeo(c);
    var tcc=segCircle(x0,y0,x1,y1,G2.x,G2.y,G2.r+s.r);
    if(tcc>=0&&tcc<bt){ bt=tcc; bc=c; bz=null; bk='crow'; }
  }
  for(i=0;i<world.zs.length;i++){
    var z=world.zs[i]; if(z.dead) continue;
    var G=zGeo(z);
    var tc=segCircle(x0,y0,x1,y1,G.x,G.hy,G.hr+s.r);
    if(tc>=0 && tc<bt){
      var ey=y0+(y1-y0)*tc;
      var top=(vy>0 && ey<=G.hy-G.hr*TOP_BAND && vy>=Math.abs(vx)*TOP_STEEP);
      bt=tc; bz=z; bc=null; bk=(s.rock||top)?'kill':'knock';
    }
    var tb=segRect(x0,y0,x1,y1,G.bx0-s.r,G.by0-s.r,G.bx1+s.r,G.by1+s.r);
    if(tb>=0 && tb<bt){ bt=tb; bz=z; bc=null; bk=s.rock?'kill':'knock'; }
    var tl=segRect(x0,y0,x1,y1,G.lx0-s.r,G.ly0-s.r,G.lx1+s.r,G.ly1+s.r);
    if(tl>=0 && tl<bt){ bt=tl; bz=z; bc=null; bk=s.rock?'kill':'knock'; }
  }
  if(!bz&&!bc) return;
  s.cool=s.rock?0.05:0.10; LASTB=s.b.zgB||0;
  var kind=(bk==='crow')?'crow':(bk==='kill'?(s.rock?'rock':'kill'):'knock');
  claim(kind,bc?bc.id:bz.id,bc?cGeo(bc):zGeo(bz));
  if(s.rock){ Mt.Body.setVelocity(b,{ x:b.velocity.x*0.86, y:b.velocity.y*0.86 }); }
  else if(bk==='kill'||bk==='crow'){
    Mt.Body.setVelocity(b,{ x:b.velocity.x*0.35, y:Math.abs(b.velocity.y)*0.30 });
  }else{
    Mt.Body.setVelocity(b,{ x:-Math.abs(b.velocity.x)*0.30, y:b.velocity.y*0.25-0.5 });
  }
}
/* 当たったと**自分の端末が決めた**結果を、その場で見せつつサーバーへ渡す。
   K1「自分では明らかに当たったのに何も起きない、は極力避ける」——だから先に見せる。
   通信している時はサーバーが正なので、次に届いた中身で静かに揃う */
function claim(kind,tid,G){
  LASTEV=(kind==='knock')?'knock':'kill';
  if(NET.on){ netSend({y:'hit',k:kind,i:tid}); fxHit(kind,G); }
  else world.hit(meId,kind,tid);
}
function fxHit(kind,G){
  if(kind==='knock'){ dust(G.x-L.hr*0.4,(G.shY+G.bot)/2,5); seKnock(); }
  else if(kind==='crow'){ burst(G.x,G.y,12,'#3a3f46'); burst(G.x,G.y,6,'#8f98a2'); seCrow(); }
  else{ burst(G.x,G.hy,16,'#9fbf83'); burst(G.x,G.hy,8,'#d8cf9e'); seKill(); }
}

/* ===== 粒と浮き文字 ===== */
var P=[], FL=[];
function burst(x,y,n,col){
  for(var i=0;i<n;i++) P.push({ x:x, y:y, vx:(Math.random()*2-1)*200, vy:-Math.random()*250-40,
    t:0, life:0.75+Math.random()*0.35, col:col, r:1+Math.random()*2.3 });
}
function dust(x,y,n){
  for(var i=0;i<n;i++) P.push({ x:x, y:y, vx:(Math.random()*2-1)*70, vy:-Math.random()*70,
    t:0, life:0.35+Math.random()*0.25, col:'rgba(190,180,150,0.7)', r:1+Math.random()*1.6 });
}
function floatUp(x,y,s,col){ FL.push({ x:x, y:y, s:s, t:0, col:col||'#ffce6a' }); }

/* ===== 遊びの流れ ===== */
var scr='title';
var throws=0, overT=0, pop=0, result=null;
var FREEZE=false, RENDER=true, LASTEV='', LASTB=0;
var joinMsg=0, netMsg='';

function myP(){ return world.find(meId); }
function myX(){ var p=myP(); return p?world.xOf(p.id):WC.SLOT_X[0]; }
function handX(){ return MX(myX()+WC.HAND_DX); }
function handY(){ return MY(WC.HAND_Y); }

function startGame(){
  scr='play'; throws=0; overT=0; pop=0; result=null;
  P.length=0; FL.length=0; S.length=0; build();
  world=new ZWORLD.World({sim:'full'});
  meId='solo';
  world.addPlayer(meId,SAVE.av,SAVE.eq);
  world.ev.length=0;
  rockArm=false; arm=62; armT=0; cool=0; shake=0;
  seStart();
  netStart();
}
function toTitle(){
  netStop();
  scr='title'; result=null;
  world=new ZWORLD.World({sim:'full'});
}
function endGame(){
  if(scr!=='play') return;
  var p=myP();
  var pay=p?world.payout(p):{base:0,mult:world.mult(),total:0};
  result={ base:pay.base, mult:pay.mult, total:pay.total, kills:world.kills, t:world.t };
  SAVE.scrap+=pay.total;
  if(world.kills>SAVE.best) SAVE.best=world.kills;
  putSave();
  scr='result'; overT=0; seLose();
  netStop();
}

/* ===== 通信 =====
   「はじめる」で**即**遊び始め、裏で相手を探す。繋がったらその場で世界を預ける。
   繋がらなければそのままひとりで遊ぶ(遊びは絶対に止めない) */
function netSend(o){ if(NET.ws&&NET.ws.readyState===1){ try{ NET.ws.send(JSON.stringify(o)); }catch(e){} } }
function netStart(){
  NET.mode='finding'; NET.tries=0; netMsg='';
  if(location.protocol==='file:'){ NET.mode='off'; return; }
  fetch('/api/join',{method:'POST'}).then(function(r){ return r.json(); }).then(function(j){
    if(!j||j.busy||!j.room){ NET.mode='off'; if(j&&j.busy){ netMsg='ただいま混み合っています'; joinMsg=2.5; } return; }
    NET.room=j.room; netOpen();
  }).catch(function(){ NET.mode='off'; });
}
function netOpen(){
  if(scr!=='play') return;
  var pr=(location.protocol==='https:')?'wss:':'ws:';
  var ws;
  try{ ws=new WebSocket(pr+'//'+location.host+'/ws?r='+NET.room+'&id='+encodeURIComponent(devId())); }
  catch(e){ NET.mode='off'; return; }
  NET.ws=ws;
  ws.onopen=function(){ NET.tries=0; sendLook();
    if(NET.ping) clearInterval(NET.ping);
    NET.ping=setInterval(function(){ netSend({y:'ping',t:Date.now()}); },8000); };
  ws.onmessage=function(ev){
    var d; try{ d=JSON.parse(ev.data); }catch(e){ return; }
    if(d.y==='full'){ NET.on=false; NET.mode='finding'; setTimeout(netStart,700); return; }
    if(d.y==='w'){
      meId=d.me; NET.on=true; NET.mode='online';
      world.sim='motion';
      applySnap(d.snap);