187 lines
11 KiB
JavaScript
187 lines
11 KiB
JavaScript
"use strict";
|
|
(function(global){
|
|
function mushroomImage(){ return global.getRenderableImage?.("mamekusaredake", "") || global.images?.get?.("mamekusaredake") || null; }
|
|
function growthOf(t){ return Math.max(0,Math.min(1,Number(t?.mamekusaredakeGrowth||0)||0)); }
|
|
const hostAlphaCache = typeof WeakMap !== "undefined" ? new WeakMap() : null;
|
|
function unit(seed="",salt=""){
|
|
if(typeof global.stableUnit==="function") return global.stableUnit(String(seed||"mame"),String(salt||""));
|
|
const s=`${seed}|${salt}`; let h=2166136261;
|
|
for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619);}
|
|
return ((h>>>0)%1000003)/1000003;
|
|
}
|
|
function alphaPointsForImage(img){
|
|
if(!img)return null;
|
|
if(hostAlphaCache?.has(img)) return hostAlphaCache.get(img);
|
|
let out=null;
|
|
try{
|
|
const c=document.createElement("canvas"); c.width=64; c.height=64;
|
|
const cx=c.getContext("2d",{willReadFrequently:true}); cx.clearRect(0,0,64,64); cx.drawImage(img,0,0,64,64);
|
|
const data=cx.getImageData(0,0,64,64).data, pts=[];
|
|
for(let y=3;y<61;y++) for(let x=3;x<61;x++) if(data[(y*64+x)*4+3]>=96) pts.push([x,y]);
|
|
out=pts.length?pts:null;
|
|
}catch(_){out=null;}
|
|
hostAlphaCache?.set(img,out); return out;
|
|
}
|
|
function hostPoint(t,img,dw,dh,index=0){
|
|
const seed=t?.birthSeed||t?.familyKey||t?.id||"mame-host";
|
|
const pts=alphaPointsForImage(img);
|
|
if(pts?.length){
|
|
const k=Math.min(pts.length-1,Math.floor(unit(seed,`mame-pos-${index}`)*pts.length));
|
|
const p=pts[k]||pts[0];
|
|
return {x:(p[0]/63-.5)*dw,y:(p[1]/63-.5)*dh};
|
|
}
|
|
// Fallback stays inside the usual non-transparent body ellipse.
|
|
const a=unit(seed,`mame-a-${index}`)*Math.PI*2, rr=Math.sqrt(unit(seed,`mame-r-${index}`))*.70;
|
|
return {x:Math.cos(a)*dw*.34*rr,y:Math.sin(a)*dh*.28*rr+dh*.03};
|
|
}
|
|
function hostMushroomCount(g){ return g<=.001?0:Math.max(1,Math.min(5,Math.ceil(g*5))); }
|
|
function infect(t, source=null){
|
|
if(!t||t.dead||t.mamekusaredakeDisease) return false;
|
|
t.mamekusaredakeDisease=true; t.mamekusaredakeGrowth=0; t.mamekusaredakeDeathPlanted=false;
|
|
t.mamekusaredakeSpeechAt=(t.world?.time||0)+3+Math.random()*5;
|
|
t.world?.log?.(`${t.name}がマメクサレダケ病に罹患した。`,"accident",{participants:[t]});
|
|
return true;
|
|
}
|
|
function visualSizeForHost(t,g=growthOf(t)){
|
|
const rendered=Number(t?._mameRenderedSize||0)||0;
|
|
if(rendered>0) return Math.max(3,rendered);
|
|
const radius=Math.max(16,Number(t?.radius||20)||20);
|
|
return Math.max(3,radius*1.16*Math.max(.05,g));
|
|
}
|
|
function spawnDetached(world,t,growth=growthOf(t)){
|
|
if(!world||!t||typeof Item==="undefined") return null;
|
|
const side=t.facingDir?.()||1;
|
|
const it=new Item("mamekusaredake", t.x+side*(t.radius||20)*0.46, t.y+(t.radius||20)*0.30);
|
|
it.mameDetached=true; it.mamePlanted=false; it.mameLife=1.25; it.vx=side*(35+Math.random()*25); it.vy=-35-Math.random()*20;
|
|
it.mameDetachedSize=visualSizeForHost(t,growth); it.r=Math.max(2,it.mameDetachedSize/2.15);
|
|
world.addItem?.(it,"mame-detached",{terrain:false}); return it;
|
|
}
|
|
function recover(t, reason="キノコが取れた"){
|
|
if(!t?.mamekusaredakeDisease) return false;
|
|
const growth=growthOf(t);
|
|
spawnDetached(t.world,t,growth);
|
|
t.mamekusaredakeDisease=false; t.mamekusaredakeGrowth=0; t.mamekusaredakeSpeechAt=0; t.mamekusaredakeDeathPlanted=false;
|
|
t.world?.spawnEffect?.("ring",t.x,t.y,{size:(t.radius||20)*(.55+.75*growth),life:.55,color:"rgba(190,153,80,.66)"});
|
|
t.world?.log?.(`${t.name}のマメクサレダケが外れて治った。`,"event",{participants:[t]}); return true;
|
|
}
|
|
function plantAt(world,x,y,r=34){
|
|
if(!world||typeof Item==="undefined")return null;
|
|
const it=new Item("mamekusaredake",x,y); it.r=r; it.mamePlanted=true; it.mameDetached=false; it.nextSporeAt=(world.time||0)+1.2+Math.random();
|
|
world.addItem?.(it,"mame-plant",{terrain:false}); return it;
|
|
}
|
|
function plantFromHost(t){
|
|
if(!t?.world||t.mamekusaredakeDeathPlanted) return null;
|
|
t.mamekusaredakeDeathPlanted=true;
|
|
const g=growthOf(t);
|
|
// 死亡サイズの50%以下なら地面には残らず消滅する。
|
|
if(g<=.5) return null;
|
|
// 表示上の本数とは無関係に、死亡時に植わるのは必ず1本だけ。
|
|
return plantAt(t.world,t.x,t.y,Math.max(22,(t.radius||20)*(.62+.32*g)));
|
|
}
|
|
function spawnSpore(world,m){
|
|
if(!world||typeof Item==="undefined")return null;
|
|
const a=Math.random()*Math.PI*2,d=Math.random()*(m.r||34)*.35;
|
|
const s=new Item("mame_spore",m.x+Math.cos(a)*d,m.y+Math.sin(a)*d*.55); s.r=22+Math.random()*18; s.sporeAge=0; s.sporeLife=7+Math.random()*5; s.sporeConcentration=.45+Math.random()*.25; s.vx=Math.cos(a)*(6+Math.random()*10); s.vy=-4+Math.sin(a)*5;
|
|
world.addItem?.(s,"mame-spore",{terrain:false}); return s;
|
|
}
|
|
function updateTarinai(t,dt){
|
|
if(!t||t.dead)return false;
|
|
const now=t.world?.time||0;
|
|
const sick=Boolean(t.zunchiDisease||t.sleepDisease||t.explosionDisease||t.fightDisease||t.mamekusaredakeDisease);
|
|
if(t.mamekusaredakeDisease){
|
|
if(t.state==="sunbath"||(t.sunbathTimer||0)>.04){
|
|
t.sunbathTimer=0; t.sunbathLeisure=false; t.sunbathCooldown=Math.max(t.sunbathCooldown||0,20);
|
|
t.visibleSpriteId=""; t.setActionState?.("idle",{target:null,reason:"マメクサレダケ病のため日光浴できない",sleeping:false,clearTarget:true});
|
|
}
|
|
t.mamekusaredakeGrowth=Math.min(1.08,(Number(t.mamekusaredakeGrowth||0)||0)+dt/90);
|
|
const g=growthOf(t);
|
|
if(g>=.5){
|
|
const maxEnergy=Math.max(1,typeof global.tarinaiMaxEnergy==="function"?global.tarinaiMaxEnergy(t):(Number(t.maxEnergy)||100));
|
|
const severity=Math.max(0,Math.min(1,(g-.5)/.5));
|
|
const loss=dt*maxEnergy*(.0018+.0037*severity);
|
|
t.energy=Math.max(0,(Number(t.energy)||0)-loss);
|
|
if(loss>.01) t.showHpBar?.();
|
|
if(t.energy<=.5){ t.die?.("マメクサレダケ病",{fromDamage:true}); return true; }
|
|
}
|
|
}
|
|
if(t.isAcquiredTarinai && sick && now>=Number(t.mamekusaredakeSpeechAt||0) && (t.nextBubbleAt||0)<=now){
|
|
const choices=[{text:"くるち、くるち",id:"voice_ochibi_disease_001"},{text:"おみじゅ...",id:"voice_ochibi_hot_007"},{text:"ぶるぶる",id:"voice_temp_cold"}];
|
|
const c=choices[Math.floor(Math.random()*choices.length)]; if(c&&t.bubble?.(c.text,4.5,"rgba(96,72,48,.82)")){ const a=global.TarinaiAudio; a?.playSample?.(c.id,1.1,"voice")||a?.play?.(c.id,{category:"voice",minGap:1.1}); }
|
|
t.mamekusaredakeSpeechAt=now+7+Math.random()*10;
|
|
}
|
|
if(t.mamekusaredakeDisease && t.mamekusaredakeGrowth>=1){ t.die?.("マメクサレダケ病"); }
|
|
return true;
|
|
}
|
|
function updateItem(item,dt,world){
|
|
if(item.type==="mamekusaredake"){
|
|
if(item.mameDetached){ item.mameLife=(item.mameLife||1)-dt; item.x+=(item.vx||0)*dt; item.y+=(item.vy||0)*dt; item.vy=(item.vy||0)+55*dt; item.vx*=Math.pow(.6,dt); if(item.mameLife<=0)item.amount=0; return true; }
|
|
item.mamePlanted=true; const now=world?.time||0; if(now>=Number(item.nextSporeAt||0)){ item.nextSporeAt=now+1.4+Math.random()*1.8; if(Math.random()<.75)spawnSpore(world,item); } return true;
|
|
}
|
|
if(item.type==="mame_spore"){
|
|
item.sporeAge=(item.sporeAge||0)+dt; const life=Math.max(1,item.sporeLife||9); item.sporeConcentration=Math.max(0,1-item.sporeAge/life); item.x+=(item.vx||0)*dt; item.y+=(item.vy||0)*dt; item.vx*=Math.pow(.72,dt); item.vy*=Math.pow(.72,dt);
|
|
for(const t of world?.tarinai||[]){ if(!t||t.dead||t.mamekusaredakeDisease)continue; if(distXY(item.x,item.y,t.x,t.y)<=item.r+(t.radius||18)*.45 && Math.random()<dt*.036*item.sporeConcentration) infect(t,item); }
|
|
if(item.sporeConcentration<=.01)item.amount=0; return true;
|
|
} return false;
|
|
}
|
|
function drawMushroom(ctx,x,y,size,alpha=1,rot=0){ const img=mushroomImage(); if(!img)return false; ctx.save();ctx.globalAlpha*=alpha;ctx.translate(x,y);ctx.rotate(rot);ctx.drawImage(img,-size*.44,-size*.55,size*.88,size);ctx.restore();return true; }
|
|
function drawOnTarinai(ctx,t,dw,dh,faceRight=false,hostImage=null,renderInfo=null){
|
|
if(!t?.mamekusaredakeDisease)return false;
|
|
const g=growthOf(t); if(g<=.001)return false;
|
|
const count=hostMushroomCount(g), seed=t.birthSeed||t.familyKey||t.id||"mame-host";
|
|
const baseSize=Math.max(4,dh*.34*g);
|
|
t._mameRenderedSize=baseSize;
|
|
t._mameHostLocalTargets=[];
|
|
t._mameHostWorldTargets=[];
|
|
const originX=Number(renderInfo?.originX);
|
|
const originY=Number(renderInfo?.originY);
|
|
const renderAngle=Number(renderInfo?.angle||0)||0;
|
|
const hasWorldTransform=Number.isFinite(originX)&&Number.isFinite(originY);
|
|
const cosA=Math.cos(renderAngle), sinA=Math.sin(renderAngle);
|
|
let drew=false;
|
|
for(let i=0;i<count;i++){
|
|
const p=hostPoint(t,hostImage,dw,dh,i);
|
|
const size=baseSize*(.72+unit(seed,`mame-size-${i}`)*.34);
|
|
const rot=(-40+unit(seed,`mame-rot-${i}`)*80)*Math.PI/180;
|
|
t._mameHostLocalTargets.push({x:p.x,y:p.y,size});
|
|
if(hasWorldTransform){
|
|
// draw() mirrors local X when faceRight, then rotates the whole sprite.
|
|
// Use the identical transform for hit testing so the first click lands
|
|
// on the mushroom that is actually visible on screen.
|
|
const lx=faceRight?-p.x:p.x, ly=p.y;
|
|
t._mameHostWorldTargets.push({
|
|
x:originX+cosA*lx-sinA*ly,
|
|
y:originY+sinA*lx+cosA*ly,
|
|
size
|
|
});
|
|
}
|
|
drew=drawMushroom(ctx,p.x,p.y,size,Math.min(1,.32+g*.82),rot)||drew;
|
|
}
|
|
return drew;
|
|
}
|
|
function drawItem(ctx,item,time=0){
|
|
if(item.type==="mamekusaredake"){
|
|
const alpha=item.mameDetached?Math.max(0,Math.min(1,(item.mameLife||0)/.45)):1;
|
|
const size=item.mameDetached&&Number(item.mameDetachedSize)>0?Number(item.mameDetachedSize):(item.r||34)*2.15;
|
|
if(!item.mameDetached){
|
|
ctx.save(); ctx.globalAlpha*=.34; ctx.fillStyle="rgba(38,28,18,.72)"; ctx.beginPath(); ctx.ellipse(size*.055,size*.41,size*.30,size*.085,0,0,Math.PI*2); ctx.fill(); ctx.restore();
|
|
}
|
|
return drawMushroom(ctx,0,0,size,alpha,item.mameDetached?(time*2.2):0);
|
|
}
|
|
if(item.type==="mame_spore"){ const r=item.r||28,c=Math.max(0,Math.min(1,item.sporeConcentration??1));ctx.save();ctx.globalAlpha*=.10+.22*c;ctx.fillStyle="rgba(181,139,65,.58)";for(let i=0;i<3;i++){const a=(item.seed||0)+i*2.2,rr=r*(.30+i*.06);ctx.beginPath();ctx.arc(Math.cos(a)*r*.18,Math.sin(a)*r*.12,rr,0,Math.PI*2);ctx.fill();}ctx.restore();return true;}
|
|
return false;
|
|
}
|
|
function mushroomHit(t,x,y){
|
|
if(!t?.mamekusaredakeDisease)return false;
|
|
const worldTargets=Array.isArray(t._mameHostWorldTargets)?t._mameHostWorldTargets:[];
|
|
for(const p of worldTargets){
|
|
if(distXY(x,y,p.x,p.y)<=Math.max(8,(p.size||10)*.52)) return true;
|
|
}
|
|
// Fallback for the short interval before the first render after infection.
|
|
// It intentionally stays conservative so pinching the body itself does not
|
|
// accidentally cure the disease.
|
|
const g=Math.max(.05,growthOf(t));
|
|
const face=typeof t.facingDir==="function"?(t.facingDir()||1):(t.facing||1);
|
|
return distXY(x,y,t.x+face*(t.radius||20)*.30,t.y+(t.radius||20)*.18)<=Math.max(7,(t.radius||20)*.34*g);
|
|
}
|
|
global.TarinaiMamekusaredakeSystem=Object.freeze({infect,recover,plantAt,plantFromHost,spawnSpore,updateTarinai,updateItem,drawOnTarinai,drawItem,mushroomHit});
|
|
})(typeof window!=="undefined"?window:globalThis);
|