384 lines
16 KiB
JavaScript
384 lines
16 KiB
JavaScript
"use strict";
|
|
(function(global){
|
|
const LOOSE_DYNAMIC_TYPES = new Set([
|
|
"ball","balloon","zunchi","pushpin","oshibyo","genkotsu",
|
|
"firecracker","flame_firecracker","sticky_bomb","water"
|
|
]);
|
|
|
|
function nearby(world,x,y,r){ return world?.nearbyItems?.(x,y,r,true) || world?.items || []; }
|
|
function oilAt(world,x,y){
|
|
for(const o of (world?.itemsOfType?.("oil")||[])) {
|
|
if(o&&!o.dead&&distXY(x,y,o.x,o.y)<=Math.max(12,o.r||58)) return o;
|
|
}
|
|
return null;
|
|
}
|
|
function groundFrictionMultiplier(world,x,y){ return oilAt(world,x,y)?0:1; }
|
|
|
|
function spawnOilFire(item,world,initial=false){
|
|
if(!item||!world)return null;
|
|
const r=Math.max(24,item.r||58);
|
|
const angle=initial ? global.deterministicAngle?.(world,"oil-ignite-fire",item,item.oilBurnTimer||0) ?? 0 : Math.random()*Math.PI*2;
|
|
const distance=initial ? 0 : Math.random()*r*.58;
|
|
return global.TarinaiFireRuntimeSystem?.spawnFireAt?.(
|
|
world,
|
|
item.x+Math.cos(angle)*distance,
|
|
item.y+Math.sin(angle)*distance*.58,
|
|
{radius:initial?20:12+Math.random()*5,amount:initial?72:30+Math.random()*24,life:initial?4.4:2.8+Math.random()*2.0,source:item,mergeRadius:initial?48:42}
|
|
) || null;
|
|
}
|
|
|
|
function igniteOil(item,world){
|
|
if(!item||item.dead||item.type!=="oil"||item.oilBurning)return false;
|
|
item.oilBurning=true;
|
|
item.oilBurnTimer=0;
|
|
item.nextOilFireAt=Number(world?.time||0);
|
|
spawnOilFire(item,world,true);
|
|
if(world){ world.drawListDirty=true; world.markSpatialDirty?.("oil-ignited"); }
|
|
return true;
|
|
}
|
|
function igniteOilNear(world,x,y,r=120){
|
|
let n=0;
|
|
for(const o of (world?.itemsOfType?.("oil")||[])){
|
|
if(o&&!o.dead&&distXY(x,y,o.x,o.y)<=r+Math.max(12,o.r||58)) n+=igniteOil(o,world)?1:0;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
function igniteFromBurningOilContact(item,world){
|
|
if(!item||!world||!item.oilBurning)return 0;
|
|
const radius=Math.max(24,Number(item.r||58));
|
|
let ignited=0;
|
|
const tarinai=world.nearbyTarinai?.(item.x,item.y,radius+48,true)||world.tarinai||[];
|
|
for(const t of tarinai){
|
|
if(!t||t.dead||t.insideNestBoxId||world.isTarinaiHiddenInNestBox?.(t))continue;
|
|
if(distXY(item.x,item.y,t.x,t.y)>radius+Math.max(12,Number(t.radius||18))*.72)continue;
|
|
const wasBurning=Boolean(t.burning||(t.burnTimer||0)>.02);
|
|
if(typeof t.igniteFire==="function")t.igniteFire(item,false);
|
|
if(!wasBurning&&Boolean(t.burning||(t.burnTimer||0)>.02))ignited++;
|
|
}
|
|
const ants=world.nearbyAnts?.(item.x,item.y,radius+24,true)||world.ants||[];
|
|
for(const ant of ants){
|
|
if(!ant||ant.dead||distXY(item.x,item.y,ant.x,ant.y)>radius+Math.max(4,Number(ant.r||5)))continue;
|
|
const wasBurning=Boolean(ant.burning||(ant.burnTimer||0)>.02);
|
|
global.TarinaiFireRuntimeSystem?.igniteAntByFire?.(ant,item,world);
|
|
if(!wasBurning&&Boolean(ant.burning||(ant.burnTimer||0)>.02))ignited++;
|
|
}
|
|
return ignited;
|
|
}
|
|
function updateOil(item,dt,world){
|
|
if(!item.oilBurning){
|
|
for(const f of (world?.itemsOfType?.("fire")||[])){
|
|
if(f&&!f.dead&&distXY(item.x,item.y,f.x,f.y)<=Math.max(item.r||58,24)+Math.max(f.r||14,10)){
|
|
igniteOil(item,world); break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
const now=Number(world?.time||0);
|
|
igniteFromBurningOilContact(item,world);
|
|
item.oilBurnTimer=(item.oilBurnTimer||0)+dt;
|
|
item.amount=Math.max(0,(item.amount||100)-dt*5.2);
|
|
if(now>=Number(item.nextOilFireAt||0)){
|
|
item.nextOilFireAt=now+0.36+Math.random()*0.34;
|
|
spawnOilFire(item,world,false);
|
|
}
|
|
if(item.amount<=0){
|
|
item.amount=0;
|
|
world?.markItemBucketsDirty?.("oil-burnout");
|
|
world?.markSpatialDirty?.("oil-burnout");
|
|
world && (world.drawListDirty=true);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isTrappableDynamic(it,world){
|
|
if(!it||it.dead||it.type==="bear_trap"||it.type==="poison_gas"||it.type==="fire") return false;
|
|
if(it.isStructure) return false;
|
|
if(["rope","rod","spring","wire","insulated_wire","rotator","reciprocator","poison_block"].includes(it.type)) return false;
|
|
if(LOOSE_DYNAMIC_TYPES.has(it.type)) return true;
|
|
if((it.dropTimer||0)>0 || Math.hypot(it.vx||0,it.vy||0)>0.2) return true;
|
|
return Boolean(world?.spatial?.isDynamicItem?.(it) && !it.isStructure);
|
|
}
|
|
|
|
function trapTarget(item,world){
|
|
const ref=item?.trapTargetRef;
|
|
if(ref&&!ref.dead&&(!item.trapTargetId||ref.id===item.trapTargetId)) return ref;
|
|
if(!item?.trapTargetId) return null;
|
|
const found=world?.itemById?.(item.trapTargetId)
|
|
|| (world?.items||[]).find(it=>it&&!it.dead&&it.id===item.trapTargetId)
|
|
|| (world?.tarinai||[]).find(t=>t&&!t.dead&&t.id===item.trapTargetId)
|
|
|| null;
|
|
item.trapTargetRef=found;
|
|
return found;
|
|
}
|
|
|
|
function hasTrapDamageStat(target){
|
|
if(!target||target.dead)return false;
|
|
if(Number.isFinite(Number(target.hp))||Number.isFinite(Number(target.health)))return true;
|
|
if(Number.isFinite(Number(target.maxHp))||Number.isFinite(Number(target.maxHealth)))return true;
|
|
if(typeof target.damage==="function"&&Number.isFinite(Number(target.energy)))return true;
|
|
return false;
|
|
}
|
|
|
|
function damageHeld(target,world,trap){
|
|
if(!target||target.dead||!hasTrapDamageStat(target))return false;
|
|
if(typeof target.damage==="function"){
|
|
try {
|
|
target.damage(2,"\u7f60\u306b\u6355\u307e\u3063\u305f",{source:trap});
|
|
} catch (_) {
|
|
target.damage(2,"\u7f60\u306b\u6355\u307e\u3063\u305f",{source:trap});
|
|
}
|
|
if (target.world && Number.isFinite(Number(target.radius))) target.hurtTimer=Math.max(Number(target.hurtTimer||0),.62);
|
|
return true;
|
|
}
|
|
if(Number.isFinite(Number(target.hp))){
|
|
target.hp=Math.max(0,Number(target.hp)-2);
|
|
target.hpBarTimer=Math.max(Number(target.hpBarTimer||0),.8);
|
|
if(target.hp<=0) global.TarinaiStructureLifecycle?.deleteItem?.(world,target,{reason:"bear-trap",breaker:trap});
|
|
return true;
|
|
}
|
|
if(Number.isFinite(Number(target.health))){
|
|
target.health=Math.max(0,Number(target.health)-2);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function holdTrapTarget(trap,target,world){
|
|
if(!trap||!target||target.dead)return false;
|
|
const now=Number(world?.time||0);
|
|
target._bearTrapHoldUntil=Math.max(Number(target._bearTrapHoldUntil||0),Number(trap.trapUntil||now));
|
|
target._bearTrapOwnerId=trap.id;
|
|
target._bearTrapHoldX=trap.x;
|
|
target._bearTrapHoldY=trap.y;
|
|
target.x=trap.x;
|
|
target.y=trap.y;
|
|
target.prevX=trap.x;
|
|
target.prevY=trap.y;
|
|
target.vx=0; target.vy=0; target.impulseVx=0; target.impulseVy=0; target.spinVelocity=0;
|
|
const body=global.TarinaiPhysicsBodySystem?.isBodyType?.(target) ? global.TarinaiPhysicsBodySystem.ensureBody?.(target,world,{syncFromLegacy:true}) : null;
|
|
if(body?.pose){ body.pose.x=trap.x; body.pose.y=trap.y; }
|
|
if(body?.velocity){ body.velocity.x=0; body.velocity.y=0; body.velocity.linear=0; body.velocity.angular=0; }
|
|
target._physicsExternalPoseDirty=true;
|
|
return true;
|
|
}
|
|
|
|
function triggerBearTrap(item,target,world){
|
|
if(!item||!target||target.dead)return false;
|
|
const now=Number(world?.time||0);
|
|
item.trapTargetId=target.id||"";
|
|
item.trapTargetRef=target;
|
|
item.trapTriggeredAt=now;
|
|
item.trapDamageableTarget=hasTrapDamageStat(target);
|
|
item.trapHoldDuration=item.trapDamageableTarget?10:5;
|
|
item.trapUntil=now+item.trapHoldDuration;
|
|
item.nextTrapDamageAt=now+.5;
|
|
item.trapClosed=true;
|
|
holdTrapTarget(item,target,world);
|
|
world?.queueEffect?.("ring",item.x,item.y,{size:Math.max(34,(item.r||34)*1.2),life:.22,color:"rgba(190,204,214,0.58)"});
|
|
if(world){ world.drawListDirty=true; world.markSpatialDirty?.("bear-trap-trigger"); }
|
|
return true;
|
|
}
|
|
|
|
function updateBearTrap(item,dt,world){
|
|
const now=Number(world?.time||0);
|
|
if(item.trapClosed || item.trapUntil>0){
|
|
const target=trapTarget(item,world);
|
|
if(now>=Number(item.trapUntil||0)){
|
|
if(target){
|
|
target._bearTrapHoldUntil=0;
|
|
target._bearTrapOwnerId="";
|
|
target._bearTrapHoldX=undefined;
|
|
target._bearTrapHoldY=undefined;
|
|
}
|
|
item.trapTargetRef=null;
|
|
item.trapTargetId="";
|
|
const removed=global.TarinaiStructureLifecycle?.deleteItem?.(world,item,{reason:"bear-trap-expired"});
|
|
if(!removed)item.amount=0;
|
|
world?.markItemBucketsDirty?.("bear-trap-expired");
|
|
world?.markSpatialDirty?.("bear-trap-expired");
|
|
return true;
|
|
}
|
|
if(target){
|
|
holdTrapTarget(item,target,world);
|
|
if(item.trapDamageableTarget&&now>=Number(item.nextTrapDamageAt||0)){
|
|
item.nextTrapDamageAt=now+.5;
|
|
damageHeld(target,world,item);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
const captureRadius=Math.max(46,(item.r||34)*1.45);
|
|
let best=null,bestD=Infinity;
|
|
for(const it of nearby(world,item.x,item.y,captureRadius)){
|
|
if(it===item||!isTrappableDynamic(it,world))continue;
|
|
if(Number(it._bearTrapHoldUntil||0)>now)continue;
|
|
const d=distXY(item.x,item.y,it.x,it.y);
|
|
const rr=Math.max(12,(item.r||34)*.72)+Math.max(7,(it.r||12)*.62);
|
|
if(d<=rr&&d<bestD){ best=it;bestD=d; }
|
|
}
|
|
for(const t of (world?.nearbyTarinai?.(item.x,item.y,captureRadius+36,true)||world?.tarinai||[])){
|
|
if(!t||t.dead||Number(t._bearTrapHoldUntil||0)>now)continue;
|
|
const d=distXY(item.x,item.y,t.x,t.y);
|
|
const rr=Math.max(12,(item.r||34)*.72)+Math.max(10,(t.radius||18)*.62);
|
|
if(d<=rr&&d<bestD){ best=t;bestD=d; }
|
|
}
|
|
if(best) triggerBearTrap(item,best,world);
|
|
return true;
|
|
}
|
|
|
|
function enforceTrapHolds(world){
|
|
const now=Number(world?.time||0);
|
|
let held=0;
|
|
for(const trap of (world?.itemsOfType?.("bear_trap")||[])){
|
|
if(!trap||trap.dead||!trap.trapClosed||now>=Number(trap.trapUntil||0))continue;
|
|
const target=trapTarget(trap,world);
|
|
if(target&&holdTrapTarget(trap,target,world)) held++;
|
|
}
|
|
if(held) world?.markSpatialDirty?.("bear-trap-hold");
|
|
return held;
|
|
}
|
|
|
|
function leakageRadius(item){ return Math.max(260,(Number(item?.r)||34)*8.2); }
|
|
|
|
function pulseLeakage(item,world){
|
|
if(!item||item.dead||!world)return 0;
|
|
const powered=Boolean(item._signalDriven&&item._signalOn);
|
|
if(!powered)return 0;
|
|
const now=Number(world.time||0);
|
|
if(now<Number(item.nextLeakagePulseAt||0))return 0;
|
|
item.nextLeakagePulseAt=now+.24;
|
|
const radius=leakageRadius(item);
|
|
let hit=0;
|
|
for(const t of world.tarinai||[]){
|
|
if(!t||t.dead||world.isTarinaiHiddenInNestBox?.(t))continue;
|
|
const d=distXY(item.x,item.y,t.x,t.y);
|
|
if(d>radius+(t.radius||18))continue;
|
|
const q=Math.max(.2,1-d/Math.max(1,radius));
|
|
t.damage?.(1.5+.9*q,"\u6f0f\u96fb",{source:item});
|
|
t.hurtTimer=Math.max(t.hurtTimer||0,.22);
|
|
t.electricShockUntil=Math.max(Number(t.electricShockUntil||0),now+.48);
|
|
t.setActionState?.("panic",{target:null,reason:"\u611f\u96fb\u3057\u3066\u3044\u308b\u3002",wake:true,sleeping:false});
|
|
if(t.isAcquiredTarinai&&now>=Number(t.ochibiElectricVoiceAt||0)){t.ochibiElectricVoiceAt=now+1.8;if(t.bubble?.("びりびりちゃ!",1.4,"rgba(70,104,168,0.82)",{force:true})){const a=globalThis.TarinaiAudio;if(a?.playSample)a.playSample("voice_ochibi_electric_006",.9,"voice")||a.play?.("voice_ochibi_electric_006",{category:"voice",minGap:.9});}}
|
|
world.queueEffect?.("electric_shock",t.x,t.y-Math.max(2,(t.radius||18)*.15),{size:Math.max(18,(t.radius||18)*1.35),life:.32,color:"rgba(122,224,255,0.98)",importance:3,forceVisual:true});
|
|
hit++;
|
|
}
|
|
for(const ant of world.ants||[]){
|
|
if(!ant||ant.dead)continue;
|
|
const d=distXY(item.x,item.y,ant.x,ant.y);
|
|
if(d>radius+(ant.r||6))continue;
|
|
ant.hp=Math.max(0,Number(ant.hp??ant.maxHp??32)-1.4);
|
|
ant.hpBarTimer=Math.max(Number(ant.hpBarTimer||0),.7);
|
|
hit++;
|
|
}
|
|
world.queueEffect?.("ring",item.x,item.y,{size:radius*.62,life:.22,color:"rgba(112,206,255,0.38)"});
|
|
world.drawListDirty=true;
|
|
return hit;
|
|
}
|
|
|
|
function updateLeakage(item,dt,world){
|
|
// Electrical damage is applied after the signal graph has been resolved for
|
|
// this simulation step. Keep the per-item update as a lightweight no-op.
|
|
return true;
|
|
}
|
|
|
|
function updatePoweredHazards(world,dt){
|
|
let hit=0;
|
|
for(const item of (world?.itemsOfType?.("leakage_device")||[])) hit+=pulseLeakage(item,world);
|
|
return hit;
|
|
}
|
|
|
|
function spawnPoisonGas(world,can){
|
|
if(!world||!can||can._gasReleased)return 0;
|
|
can._gasReleased=true;
|
|
let n=0;
|
|
const sizeScale=Math.max(.55,Math.min(1.8,Number(can.r||28)/28));
|
|
const count=Math.max(7,Math.round(10*sizeScale));
|
|
for(let i=0;i<count;i++){
|
|
const a=Math.PI*2*i/count+Math.random()*.28;
|
|
const d=(10+Math.random()*56)*sizeScale;
|
|
const g=new Item("poison_gas",can.x+Math.cos(a)*d,can.y+Math.sin(a)*d*.72);
|
|
g.r=(82+Math.random()*48)*sizeScale;
|
|
g.gasConcentration=.82+Math.random()*.28;
|
|
g.gasLife=(18+Math.random()*8)*Math.sqrt(sizeScale);
|
|
g.gasExpansion=(58+Math.random()*42)*sizeScale;
|
|
g.vx=Math.cos(a)*(10+Math.random()*20)*sizeScale;
|
|
g.vy=Math.sin(a)*(10+Math.random()*20)*sizeScale;
|
|
if(world.addItem?.(g,"poison-gas",{terrain:false}))n++;
|
|
}
|
|
world.drawListDirty=true;
|
|
return n;
|
|
}
|
|
|
|
function burstPoisonGasCan(item,world,reason="fuse"){
|
|
if(!item||item.dead||item._gasReleased)return false;
|
|
const x=item.x,y=item.y;
|
|
const sizeScale=Math.max(.55,Math.min(1.8,Number(item.r||28)/28));
|
|
spawnPoisonGas(world,item);
|
|
world?.spawnEffect?.("explosion",x,y,{size:72*sizeScale,life:.58,color:"rgba(136,74,170,0.78)"});
|
|
world?.spawnEffect?.("ring",x,y,{size:112*sizeScale,life:.54,color:"rgba(184,108,216,0.52)"});
|
|
world?.applyExplosionExternalForces?.(x,y,105*sizeScale,item,.42*sizeScale,"poison-gas-can");
|
|
item.amount=0;
|
|
item.hp=0;
|
|
world?.markItemBucketsDirty?.("poison-gas-can-burst");
|
|
world?.markSpatialDirty?.("poison-gas-can-burst");
|
|
world && (world.drawListDirty=true);
|
|
global.TarinaiAudio?.explode?.();
|
|
world?.log?.("\u6bd2\u30ac\u30b9\u7f36\u304c\u7834\u88c2\u3057\u3001\u6bd2\u30ac\u30b9\u304c\u5e83\u304c\u3063\u305f\u3002","accident");
|
|
return true;
|
|
}
|
|
|
|
function updatePoisonGasCan(item,dt,world){
|
|
item.fuseTimer=Math.max(0,Number(item.fuseTimer??5)-Math.max(0,Number(dt||0)));
|
|
if(item.fuseTimer<=0) burstPoisonGasCan(item,world,"fuse");
|
|
return true;
|
|
}
|
|
|
|
function updateGas(item,dt,world){
|
|
item.gasAge=(item.gasAge||0)+dt;
|
|
const life=Math.max(1,item.gasLife||22);
|
|
item.gasConcentration=Math.max(0,1-item.gasAge/life);
|
|
item.r+=(Math.max(36,Number(item.gasExpansion||72))/life)*dt;
|
|
for(const fan of (world?.itemsOfType?.("fan")||[])){
|
|
const wind=global.TarinaiItemDynamicBallSystem?.fanWindAt?.(fan,item.x,item.y,{range:430});
|
|
if(wind){
|
|
const a=itemAngleFor(fan),f=82*wind.falloff*dt;
|
|
item.vx=(item.vx||0)+Math.cos(a)*f;
|
|
item.vy=(item.vy||0)+Math.sin(a)*f;
|
|
}
|
|
}
|
|
item.x+=(item.vx||0)*dt;
|
|
item.y+=(item.vy||0)*dt;
|
|
item.vx*=Math.pow(.62,dt);
|
|
item.vy*=Math.pow(.62,dt);
|
|
for(const t of world?.tarinai||[]){
|
|
if(!t||t.dead)continue;
|
|
if(distXY(item.x,item.y,t.x,t.y)<=item.r+(t.radius||18)) t.damage?.(.16*item.gasConcentration*dt,"\u6bd2\u30ac\u30b9",{source:item});
|
|
}
|
|
if(item.gasConcentration<=.01)item.amount=0;
|
|
return true;
|
|
}
|
|
|
|
function update(item,dt,world){
|
|
if(!item||item.dead)return false;
|
|
if(item.type==="oil")return updateOil(item,dt,world);
|
|
if(item.type==="bear_trap")return updateBearTrap(item,dt,world);
|
|
if(item.type==="leakage_device")return updateLeakage(item,dt,world);
|
|
if(item.type==="poison_gas_can")return updatePoisonGasCan(item,dt,world);
|
|
if(item.type==="poison_gas")return updateGas(item,dt,world);
|
|
if(item.type==="mamekusaredake"||item.type==="mame_spore") return global.TarinaiMamekusaredakeSystem?.updateItem?.(item,dt,world) || false;
|
|
return false;
|
|
}
|
|
|
|
global.TarinaiItemEnvironmentHazardSystem=Object.freeze({
|
|
update,
|
|
updatePoweredHazards,
|
|
enforceTrapHolds,
|
|
groundFrictionMultiplier,
|
|
igniteOil,
|
|
igniteOilNear,
|
|
spawnPoisonGas,
|
|
burstPoisonGasCan,
|
|
leakageRadius,
|
|
});
|
|
})(typeof window!=="undefined"?window:globalThis);
|