/* ===== 石とゾンビ・世界の中身(2026-09-13) ===== ★この1本を**サーバー(Durable Object)と端末の両方が回す。**二重に書かない。 ・サーバー: `sim:'full'` で全部回す。**ここが正**(HP・死亡・撃破・スクラップ・大岩・前後交代・参加退出) ・端末 : `sim:'motion'` で**動きだけ**回す。ゾンビは受け取った位置の間をなめらかに進み、 中身はサーバーから届いた物で上書きする ・ひとりで遊ぶ時(通信が繋がらない時): 端末が `sim:'full'` で回す。中身は同じ ★長さは全部**メートル**。画面の幅が端末で違っても同じ世界になる(マルチの前提)。 画面の左端が0m、右端が VIEW_M(7.8m)。px への直しは絵を描く側の仕事。 ★石はここに入っていない。石は**投げた本人の端末**で飛ばす(通信を待たない)。 当たったかどうかも本人の端末が決めて、結果だけをここへ渡す(`hit`)。 K1「投石は操作レスポンス最優先」「自分では明らかに当たったのに何も起きない、は極力避ける」 */ (function(root){ 'use strict'; var C={ VIEW_M:7.8, /* 画面に入る横幅(m) */ SLOT_X:[1.48,0.60], /* 0=前衛 / 1=後衛 の立ち位置(m)。 1.30/0.58だと2人の体が触れて頭のHP帯が重なった(2026-09-13 実測) */ HAND_DX:0.30, /* 手を離す所(立ち位置からの前) */ HAND_Y:1.58, /* 手を離す高さ(足もとから) */ BODY_H:1.72, /* 身長 */ GRAB_GAP:0.44, /* 前衛のこれだけ前まで来たら取り付く */ MAXHP:100, DRAIN:11, /* 取り付かれている間、1体につき毎秒これだけ減る */ KB:1.7, KBMAX:2.6, STAG:0.40, /* ノックバックの強さ・上限・よろける秒数 */ SPAWN0:4.6, SPAWNMIN:1.25, SPAWNK:0.11, TWOP:0.68, /* 2人いる時は湧く間隔をこの割合にする(私が決めた) */ SPD0:0.34, SPDK:0.013, SPDMAX:0.95, SPAWNX:[8.15,9.05], BOULDER_CD:30, /* 大岩のクールダウン(秒) */ CROW_FIRST:28, /* 最初のカラスが来るまで(秒) */ CROW_INT0:15, CROW_INTMIN:5.5, CROW_INTK:0.10, CROW_Y:[2.5,3.6], /* 飛ぶ高さ(m)。山なりの邪魔をしない所 */ CROW_SPD:0.95, CROW_DIVE_DMG:7, /* 突いた時のダメージ */ CROW_DIVE_CD:3.2, /* 生存時間倍率。K1の仮例(60秒1.2 / 90秒1.3 / 120秒1.5 / 以降さらに上昇)をそのまま段にした */ MULT:[[0,1.0],[60,1.2],[90,1.3],[120,1.5],[150,1.7],[180,1.9],[210,2.1],[240,2.3],[300,2.6],[360,3.0]], SCRAP_Z:1, /* ゾンビ1体 */ SCRAP_C:3 /* カラス1羽 */ }; function rndFn(seed){ var s=(seed|0)||12345; return function(){ s^=s<<13; s^=s>>>17; s^=s<<5; s|=0; return ((s>>>0)%100000)/100000; }; } function World(o){ o=o||{}; this.sim=o.sim||'full'; this.seed=(o.seed|0)||((Math.random()*1e9)|0); this.rnd=rndFn(this.seed); this.t=0; this.nid=1; this.seq=0; this.zs=[]; this.cs=[]; this.ps=[]; this.kills=0; this.spawnT=1.1; this.crowT=C.CROW_FIRST; this.over=false; this.ev=[]; } World.C=C; /* ---------- 人 ---------- */ World.prototype.addPlayer=function(id,av,look){ if(this.find(id)) return this.find(id); var p={ id:id, hp:C.MAXHP, alive:true, base:0, joinT:this.t, av:av||{h:0,hc:0,sk:0,w:0}, look:look||{c:0,ht:0,st:0}, bT:0, grabs:0, ping:0 }; this.ps.push(p); this.ev.push({e:'join',p:id}); return p; }; World.prototype.rmPlayer=function(id){ var i=this.idx(id); if(i<0) return null; var p=this.ps.splice(i,1)[0]; for(var j=0;j=C.MULT[i][0]) m=C.MULT[i][1]; return m; }; World.prototype.payout=function(p){ var m=this.mult(); return { base:p.base, mult:m, total:Math.floor(p.base*m) }; }; /* 大岩。30秒に1回・スクラップは要らない */ World.prototype.rockReady=function(p){ return this.t>=p.bT; }; World.prototype.useRock=function(pid){ var p=this.find(pid); if(!p||!p.alive) return false; if(this.sim==='full'){ if(this.t0){ this.spawnZ(); var iv=Math.max(C.SPAWNMIN, C.SPAWN0-this.kills*C.SPAWNK); if(np>=2) iv*=C.TWOP; this.spawnT=iv; } this.crowT-=dt; if(this.crowT<=0 && np>0 && this.t>=C.CROW_FIRST){ this.spawnC(); this.crowT=Math.max(C.CROW_INTMIN, C.CROW_INT0-this.kills*C.CROW_INTK); } } var fr=this.front(); var frX=fr?this.xOf(fr.id):C.SLOT_X[0]; /* ゾンビ */ for(i=this.zs.length-1;i>=0;i--){ z=this.zs[i]; if(z.dead){ z.dt+=dt; if(z.dt>1.5) this.zs.splice(i,1); continue; } z.ph+=dt*(1.5+z.spd*2.2); if(z.kb>0.05){ z.x+=z.kb*dt; z.kb*=Math.pow(0.015,dt); } if(z.gr){ /* 取り付いている。HPを削り続ける(サーバー側だけ) */ if(full){ p=this.find(z.gr); if(!p||!p.alive){ z.gr=null; } else{ p.hp-=C.DRAIN*dt; if(p.hp<=0){ p.hp=0; p.alive=false; this.ev.push({e:'down',p:p.id}); z.gr=null; } } } }else if(z.stag>0){ z.stag-=dt; } else{ z.x-=z.spd*dt; if(full && fr && z.x<=frX+C.GRAB_GAP){ z.x=frX+C.GRAB_GAP; z.gr=fr.id; this.ev.push({e:'grab',p:fr.id,id:z.id}); } if(!full && z.x<=frX+C.GRAB_GAP) z.x=frX+C.GRAB_GAP; } if(z.x>C.VIEW_M+2.2) z.x=C.VIEW_M+2.2; } /* カラス。左へ流れながら、下の人めがけて突きに降りる */ for(i=this.cs.length-1;i>=0;i--){ c=this.cs[i]; if(c.dead){ c.dt+=dt; c.y-=0; c.y+=4.2*dt; if(c.dt>1.4) this.cs.splice(i,1); continue; } c.ph+=dt*9; if(c.dive>0){ c.dive-=dt; var k=1-Math.abs(c.dive/C.CROW_DIVE_CD*2-1); /* 0→1→0 で降りて戻る */ c.y=c.y0-(c.y0-1.42)*Math.max(0,k); if(full && c.dive>C.CROW_DIVE_CD*0.45 && c.dive0) c.cd-=dt; p=this.front(); if(full && c.cd<=0 && p && p.alive && Math.abs(c.x-this.xOf(p.id))<0.35){ c.dive=C.CROW_DIVE_CD; c.bit=0; } if(c.x<-0.8) this.cs.splice(i,1); } } /* 終わり。生きている人が1人もいなくなったら世界は終わる */ if(full && !this.over && this.ps.length>0){ var any=false; for(i=0;i0?1:0]); } for(i=0;i0.75) z.x=a[1]; else z.x+=(a[1]-z.x)*0.34; if(a[3]&&!z.dead){ z.dead=1; z.dt=0; } z.gr=a[4]?'x':null; } for(i=this.zs.length-1;i>=0;i--) if(!seen[this.zs[i].id] && !this.zs[i].dead) this.zs.splice(i,1); seen={}; for(i=0;i0.75) c.x=b[1]; else c.x+=(b[1]-c.x)*0.34; c.y+=(b[2]-c.y)*0.34; c.dive=b[4]?1:0; if(b[3]&&!c.dead){ c.dead=1; c.dt=0; } } for(i=this.cs.length-1;i>=0;i--) if(!seen[this.cs[i].id] && !this.cs[i].dead) this.cs.splice(i,1); /* 人は並び順がそのまま前衛/後衛。届いた順に作り直す */ var np=[]; for(i=0;i