src/worker.js
分割 2 / 2 · 元ファイル 231〜349行付近
/* 見た目(アバターと買った物)。数字しか受け取らない */
p.av = { h:(d.h|0)&7, hc:(d.hc|0)&7, sk:(d.sk|0)%4, w:(d.w|0)%8 };
p.look = { c:Math.max(0,Math.min(80,d.c|0)), ht:Math.max(0,Math.min(40,d.ht|0)),
st:Math.max(0,Math.min(20,d.st|0)) };
break;
case 'hit': {
const k = (d.k==='kill'||d.k==='knock'||d.k==='crow'||d.k==='rock') ? d.k : null;
if (k) w.hit(a.pid, k, d.i|0);
break;
}
case 'swap': w.swap(); break;
case 'rock': w.useRock(a.pid); break;
}
}
gone(ws){
if (!this.socks.has(ws)) return;
const a = this.info(ws);
this.socks.delete(ws);
if (this.w && a.pid) this.w.rmPlayer(a.pid);
if (this.socks.size === 0){ this.emptyAt = Date.now(); this.stop(); }
this.report();
}
start(){
if (this.timer) return;
this.last = Date.now();
this.timer = setInterval(() => this.tick(), TICK_MS);
/* ★目覚ましは「畳むため」だけに使う。遊びの進行には使わない */
this.s.storage.setAlarm(Date.now() + IDLE_MS + 5000).catch(()=>{});
}
stop(){ if (this.timer){ clearInterval(this.timer); this.timer = null; } }
tick(){
/* ★人が0人なら1コマも進めない */
if (this.socks.size === 0){ this.stop(); return; }
const now = Date.now();
const dt = Math.min(0.25, (now - this.last)/1000);
this.last = now;
/* 合図の来なくなった繋がりを切る */
for (const w of Array.from(this.socks)){
if (now - (this.info(w).t||now) > STALE_MS){ try{ w.close(1001,'stale'); }catch(e){} this.gone(w); }
}
if (this.socks.size === 0){ this.stop(); return; }
const w = this.world();
w.step(dt);
this.all(w.snap());
if (w.over){
/* 終わったら部屋は使い回さない。相手探しからも外す */
this.report(true);
}
}
async report(force){
if (!this.name) return;
const n = this.players(), open = this.isOpen();
const key = n + (open?'o':'x');
if (!force && key === this.reported) return;
this.reported = key;
try{
await this.env.LOBBY.get(this.env.LOBBY.idFromName('hall'))
.fetch('https://do/report?r='+this.name+'&n='+n+'&o='+(open?1:0));
}catch(e){}
}
/* ★誰も繋がっていない部屋を畳む門。ここが無いと空の部屋が回り続ける */
async alarm(){
if (this.socks.size > 0){
await this.s.storage.setAlarm(Date.now() + IDLE_MS + 5000).catch(()=>{});
return;
}
if (this.emptyAt && Date.now() - this.emptyAt >= IDLE_MS){
this.stop(); this.w = null;
await this.s.storage.deleteAll().catch(()=>{});
await this.s.storage.deleteAlarm().catch(()=>{});
return;
}
await this.s.storage.setAlarm(Date.now() + 30000).catch(()=>{});
}
}
/* ===== バグ報告の箱(平成ゾンビ学園から相続した🐛の受け先) ===== */
const BUG_MAX = 900*1024, BUG_KEEP = 200;
export class Bug {
constructor(state){ this.s = state; }
async fetch(req){
const u = new URL(req.url);
if (req.method === 'POST'){
const body = await req.text();
if (body.length > BUG_MAX) return J({ ok:0, why:'big' }, 413);
const id = Date.now().toString(36) + Math.random().toString(36).slice(2,6);
await this.s.storage.put('b:'+id, { at:Date.now(), body });
const list = (await this.s.storage.get('list'))||[];
list.unshift(id);
while (list.length > BUG_KEEP){ const old = list.pop(); await this.s.storage.delete('b:'+old); }
await this.s.storage.put('list', list);
return J({ ok:1, id });
}
const id = u.searchParams.get('id');
if (id){
if (u.searchParams.get('del') === '1'){
await this.s.storage.delete('b:'+id);
const l = ((await this.s.storage.get('list'))||[]).filter(x=>x!==id);
await this.s.storage.put('list', l);
return J({ ok:1 });
}
const r = await this.s.storage.get('b:'+id);
if (!r) return J({ ok:0 }, 404);
return new Response(r.body, { headers:{'content-type':'application/json; charset=utf-8'} });
}
const list = (await this.s.storage.get('list'))||[];
const out = [];
for (const x of list.slice(0,60)){
const r = await this.s.storage.get('b:'+x);
if (r) out.push({ id:x, at:r.at, bytes:r.body.length });
}
return J(out);
}
}