This commit is contained in:
33333-33333 2026-07-03 20:57:02 +09:00
commit 88f618cadf
55 changed files with 1299 additions and 447 deletions

View file

@ -227,13 +227,27 @@
return ((base % twoPi) + twoPi) % twoPi;
}
function fanWindAngleFor(item) {
return normalizeAngleLocal(item?.angle, item?.fanSwingCenter ?? 0);
}
function ensureFanBodyAngle(item) {
if (!item || item.type !== "fan") return 0;
const body = Number(item.fanBodyAngle);
if (Number.isFinite(body)) return normalizeAngleLocal(body, item.angle || 0);
item.fanBodyAngle = normalizeAngleLocal(item.angle || item.fanSwingCenter || 0, 0);
return item.fanBodyAngle;
}
function updateFanSwing(item, dt) {
if (!item || item.dead || item.type !== "fan") return false;
ensureFanBodyAngle(item);
if (!item.fanSwingOn) {
item.fanSwingCenter = Number.isFinite(Number(item.fanSwingCenter)) ? Number(item.fanSwingCenter) : itemAngleFor(item);
item.fanSwingCenter = Number.isFinite(Number(item.fanSwingCenter)) ? Number(item.fanSwingCenter) : fanWindAngleFor(item);
item.angle = normalizeAngleLocal(item.fanSwingCenter, item.angle || 0);
return false;
}
const center = normalizeAngleLocal(item.fanSwingCenter, itemAngleFor(item));
const center = normalizeAngleLocal(item.fanSwingCenter, fanWindAngleFor(item));
const range = clamp(Number(item.fanSwingRange ?? (35 * Math.PI / 180)) || 0, 0, 150 * Math.PI / 180);
const speed = clamp(Number(item.fanSwingSpeed ?? (48 * Math.PI / 180)) || 0, 0, 360 * Math.PI / 180);
item.fanSwingCenter = center;
@ -250,7 +264,7 @@
const spread = Math.max(0.18, Number(opts.spread || 0.62) || 0.62);
const dx = x - fan.x;
const dy = y - fan.y;
const angle = itemAngleFor(fan);
const angle = fanWindAngleFor(fan);
const ax = Math.cos(angle);
const ay = Math.sin(angle);
const along = dx * ax + dy * ay;
@ -271,11 +285,12 @@
const now = worldRef?.time || 0;
item.prevX = item.x;
item.prevY = item.y;
ensureFanBodyAngle(item);
item.fanSpin = (item.fanSpin || 0) + 11.5 * frameDt;
item.fanPulse = (item.fanPulse || 0) + 2.2 * frameDt;
updateFanSwing(item, frameDt);
const angle = itemAngleFor(item);
const angle = fanWindAngleFor(item);
const ax = Math.cos(angle);
const ay = Math.sin(angle);
const range = 320;
@ -306,6 +321,46 @@
return true;
}
function spreadFireByFan(fan, fire, wind, dt, worldRef) {
if (!fan || !fire || !worldRef || fire.dead || fire.type !== "fire") return false;
const now = worldRef.time || 0;
const falloff = Math.max(0, Number(wind?.falloff || 0) || 0);
if (falloff < 0.16 || (fire.amount || 0) < 18) return false;
const interval = Math.max(0.10, 0.34 - falloff * 0.18);
if ((fire.lastFanSpreadAt || -999) + interval > now) return false;
fire.lastFanSpreadAt = now;
const ax = Math.cos(wind.angle);
const ay = Math.sin(wind.angle);
const side = global.deterministicRange(worldRef, "fan-fire-side", -22, 22, fan, fire, Math.round(now * 10));
const dist = global.deterministicRange(worldRef, "fan-fire-distance", 58, 118, fan, fire, Math.round(now * 10)) * (0.82 + falloff * 0.58);
const x = clamp((fire.x || 0) + ax * dist - ay * side, CONFIG.worldPadding || 30, Math.max(CONFIG.worldPadding || 30, (worldRef.w || 0) - (CONFIG.worldPadding || 30)));
const y = clamp((fire.y || 0) + ay * dist + ax * side, CONFIG.worldPadding || 30, Math.max(CONFIG.worldPadding || 30, (worldRef.h || 0) - (CONFIG.worldPadding || 30)));
const spawned = global.TarinaiItemDynamicToolSystem?.spawnFireAt?.(worldRef, x, y, {
radius: Math.max(8, (fire.r || 14) * (0.55 + falloff * 0.22)),
amount: Math.max(18, Math.min(86, (fire.amount || 60) * (0.20 + falloff * 0.22))),
life: 2.2 + falloff * 3.4,
source: fan,
mergeRadius: 24,
});
if (spawned) {
// A fan should carry embers forward instead of merely dimming the source flame.
// The source loses a little fuel, while the downwind flame handles normal fire contacts.
fire.amount = Math.max(4, (fire.amount || 0) - (2.2 + falloff * 3.8));
worldRef.effects?.push(new Effect("flame", fire.x + ax * Math.max(12, fire.r || 14), fire.y + ay * Math.max(12, fire.r || 14), {
size: Math.max(7, (fire.r || 14) * 0.72),
life: 0.22,
color: "rgba(255,126,54,0.58)",
vx: ax * (80 + falloff * 120),
vy: ay * (80 + falloff * 120),
}));
worldRef.markItemBucketsDirty?.("fan-fire-spread");
worldRef.markSpatialDirty?.("fan-fire-spread");
worldRef.drawListDirty = true;
return true;
}
return false;
}
function applyFanToItem(fan, target, wind, dt, worldRef) {
const type = target.type || "";
let massFactor = 0;
@ -314,7 +369,10 @@
else if (type === "pushpin" || type === "oshibyo") massFactor = 0.90;
else if (type === "water" || type === "zunda_juice" || type === "mercury") massFactor = 0.42;
else if (type === "zunchi") massFactor = 0.30;
else if (type === "fire") massFactor = 0.18;
else if (type === "fire") {
spreadFireByFan(fan, target, wind, dt, worldRef);
return;
}
else return;
const force = 236 * wind.falloff * massFactor;
@ -326,7 +384,6 @@
target.vy = clamp((target.vy || 0) + ay * force * dt, -820, 820);
if (type === "balloon") target.spinVelocity = clamp((target.spinVelocity || 0) + 3.2 * wind.falloff * dt, -12, 12);
else if (type === "ball") target.spinVelocity = clamp((target.spinVelocity || 0) + 5.4 * wind.falloff * dt, -38, 38);
if (type === "fire") target.amount = Math.max(0, (target.amount ?? 1) - wind.falloff * dt * 1.0);
worldRef?.markSpatialDirty?.("fan-wind-item");
}
@ -353,8 +410,9 @@
const radius = Math.max(26, (balloon.r || 20) + 22);
for (const it of worldRef.nearbyItems(balloon.x, balloon.y, radius) || []) {
if (!it || it.dead || it === balloon) continue;
if ((typeof isPinType === "function" && isPinType(it.type) && it.pinState !== "lodged") || it.type === "fire" || it.type === "flame_firecracker") {
const hit = distXY(balloon.x, balloon.y, it.x, it.y) <= (balloon.r || 20) + (it.r || 10) + (it.type === "fire" ? 16 : 4);
if ((typeof isPinType === "function" && isPinType(it.type) && it.pinState !== "lodged") || it.type === "fire" || it.type === "flame_firecracker" || it.type === "firecracker") {
const extra = it.type === "fire" ? 16 : (it.type === "firecracker" || it.type === "flame_firecracker" ? 8 : 4);
const hit = distXY(balloon.x, balloon.y, it.x, it.y) <= (balloon.r || 20) + (it.r || 10) + extra;
if (hit) {
popBalloon(balloon, worldRef, it);
return;
@ -410,22 +468,26 @@
function spawnWaterFromBalloon(balloon, worldRef) {
if (!balloon || !worldRef) return 0;
const baseR = Math.max(14, Number(balloon.r || 20) || 20);
const count = Math.max(7, Math.min(13, Math.round(baseR * 0.42)));
const count = Math.max(12, Math.min(22, Math.round(baseR * 0.68)));
let spawned = 0;
for (let i = 0; i < count; i++) {
if (typeof global.Item !== "function") break;
const a = deterministicAngle(worldRef || balloon, "water-balloon-splash-angle", balloon, i);
const d = deterministicRange(worldRef || balloon, "water-balloon-splash-dist", baseR * 0.10, baseR * 0.58, balloon, i);
const speed = deterministicRange(worldRef || balloon, "water-balloon-drop-speed", 150, 420, balloon, i);
const drop = new global.Item("water", balloon.x + Math.cos(a) * d, balloon.y + Math.sin(a) * d);
drop.r = deterministicRange(worldRef || balloon, "water-balloon-drop-radius", 5.5, 10.5, balloon, i);
drop.amount = deterministicRange(worldRef || balloon, "water-balloon-drop-amount", 10, 28, balloon, i);
drop.prevX = drop.x;
drop.prevY = drop.y;
drop.vx = Math.cos(a) * speed + (Number(balloon.vx || 0) || 0) * 0.18;
drop.vy = Math.sin(a) * speed + (Number(balloon.vy || 0) || 0) * 0.18;
drop.splashFlightTimer = deterministicRange(worldRef || balloon, "water-balloon-drop-flight", 0.30, 0.62, balloon, i);
drop.splashSourceId = balloon.id || "";
const d = deterministicRange(worldRef || balloon, "water-balloon-splash-dist", baseR * 0.12, baseR * 0.95, balloon, i);
const speed = deterministicRange(worldRef || balloon, "water-balloon-drop-speed", 280, 760, balloon, i);
const x = balloon.x + Math.cos(a) * d;
const y = balloon.y + Math.sin(a) * d;
const vx = Math.cos(a) * speed + (Number(balloon.vx || 0) || 0) * 0.28;
const vy = Math.sin(a) * speed + (Number(balloon.vy || 0) || 0) * 0.28;
const flight = deterministicRange(worldRef || balloon, "water-balloon-drop-flight", 0.42, 0.88, balloon, i);
const amount = deterministicRange(worldRef || balloon, "water-balloon-drop-amount", 24, 58, balloon, i);
const drop = global.TarinaiWeatherSystem?.createRainWaterItem
? global.TarinaiWeatherSystem.createRainWaterItem(worldRef, { x, y, amount, vx, vy, splashFlightTimer: flight })
: new global.Item("water", x, y);
if (!Number.isFinite(Number(drop.amount))) drop.amount = amount;
if (!Number.isFinite(Number(drop.vx))) drop.vx = vx;
if (!Number.isFinite(Number(drop.vy))) drop.vy = vy;
if (!Number.isFinite(Number(drop.splashFlightTimer))) drop.splashFlightTimer = flight;
if (worldRef.addItem?.(drop, "water-balloon-splash", { terrain: false })) spawned += 1;
else { worldRef.items?.push?.(drop); spawned += 1; }
if (i < 8) {
@ -538,7 +600,11 @@
worldRef?.effects?.push(new Effect("fight", balloon.x, balloon.y, { size: deterministicRange(worldRef || balloon, "water-balloon-pop-size", 4, 10, balloon, i), life: 0.28, color: "rgba(95,204,246,0.72)", vx: Math.cos(a) * s, vy: Math.sin(a) * s }));
}
spawnWaterFromBalloon(balloon, worldRef);
const sourceLabel = cause?.type === "fire" || cause?.type === "flame_firecracker" ? "火" : (cause?.type === "genkotsu" ? "げんこつ" : (cause && typeof isPinType === "function" && isPinType(cause.type) ? "鋲" : "何か"));
const sourceLabel = cause?.type === "fire" || cause?.type === "flame_firecracker" ? "火"
: (cause?.type === "firecracker" ? "爆竹"
: (cause?.type === "shoot" ? "射撃"
: (cause?.type === "genkotsu" ? "げんこつ"
: (cause && typeof isPinType === "function" && isPinType(cause.type) ? "鋲" : "強い衝撃"))));
if (worldRef?.relationNotice?.(balloon.id || "balloon", cause?.id || sourceLabel, "water-balloon-pop", 1.4)) {
worldRef.log?.(`水風船が${sourceLabel}に触れて割れた。`, "accident", { participants: [] });
}