a
This commit is contained in:
parent
d163ceb291
commit
f6d7412744
94 changed files with 3836 additions and 1226 deletions
|
|
@ -4,7 +4,7 @@
|
|||
(function (global) {
|
||||
const World = global.World;
|
||||
if (!World) throw new Error("World is not available for mixin: world_combat_effects.js");
|
||||
const EFFECT_POOL_TYPES = new Set(["ring", "fight", "flame", "fall", "bleed", "bubble", "eat", "explosion", "heart", "shoot_impact", "zunchi_miasma", "electric_shock", "wind"]);
|
||||
const EFFECT_POOL_TYPES = new Set(["ring", "fight", "flame", "fall", "bleed", "bubble", "eat", "explosion", "heart", "shoot_impact", "zunchi_miasma", "electric_shock", "wind", "ascend_soul"]);
|
||||
const EFFECT_POOL_LIMIT_PER_TYPE = 96;
|
||||
const effectPool = new Map();
|
||||
|
||||
|
|
@ -28,13 +28,15 @@
|
|||
|
||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
queueEffect(type, x, y, options = {}) {
|
||||
// The visual-effects toggle is a true simulation gate: when disabled,
|
||||
// do not allocate, enqueue, update, or draw transient Effect objects.
|
||||
if (global.TarinaiPerf?.visualLevel?.("effects", "high") === "off") return null;
|
||||
const opts = options || {};
|
||||
const forceVisual = opts.forceVisual === true || type === "ascend_soul";
|
||||
// Semantic one-shot effects such as grave ascension remain visible even when
|
||||
// optional ambient effects are disabled.
|
||||
if (global.TarinaiPerf?.visualLevel?.("effects", "high") === "off" && !forceVisual) return null;
|
||||
const importance = Math.max(0, Math.min(3, Number(opts.importance ?? (type === "explosion" || type === "electric_shock" || type === "bubble" || type === "heart" ? 3 : type === "ring" || type === "fall" ? 2 : 1))));
|
||||
const effect = acquireEffect(type, x, y, opts);
|
||||
effect._effectImportance = importance;
|
||||
effect._forceVisual = forceVisual;
|
||||
this.effects.push(effect);
|
||||
if (global.TarinaiPerf?.diagnosticsEnabled?.() === true) {
|
||||
this._effectSpawnStats = this._effectSpawnStats || {};
|
||||
|
|
@ -45,8 +47,9 @@
|
|||
},
|
||||
|
||||
spawnEffect(type, x, y, options = {}) {
|
||||
if (global.TarinaiPerf?.visualLevel?.("effects", "high") === "off") return null;
|
||||
const opts = options || {};
|
||||
const forceVisual = opts.forceVisual === true || type === "ascend_soul";
|
||||
if (global.TarinaiPerf?.visualLevel?.("effects", "high") === "off" && !forceVisual) return null;
|
||||
const importance = Math.max(0, Math.min(3, Number(opts.importance ?? (type === "explosion" || type === "electric_shock" || type === "bubble" || type === "heart" ? 3 : type === "ring" || type === "fall" ? 2 : 1))));
|
||||
if (importance <= 1 && opts.allowOffscreen !== true) {
|
||||
const margin = Math.max(80, Number(opts.offscreenMargin || 140));
|
||||
|
|
@ -112,6 +115,53 @@
|
|||
},
|
||||
|
||||
|
||||
applyExplosionExternalForces(x, y, blastRadius, source = null, scale = 1, salt = "explosion-force") {
|
||||
globalThis.TarinaiItemEnvironmentHazardSystem?.igniteOilNear?.(this, x, y, blastRadius * 1.05);
|
||||
for (const can of this.itemsOfType?.("poison_gas_can") || []) {
|
||||
if (!can || can.dead || distXY(x, y, can.x, can.y) > blastRadius) continue;
|
||||
global.TarinaiStructureLifecycle?.deleteItem?.(this, can, { reason: "explosion", breaker: source });
|
||||
}
|
||||
const radius = Math.max(1, Number(blastRadius || 0) || 1);
|
||||
// Mechanical bodies can have long custom shapes, so do not rely on a spatial
|
||||
// query that only sees their center/radius. Scan the bounded item list and use
|
||||
// the mechanical extent for blast reach; this keeps rotators and reciprocators
|
||||
// consistent even immediately after placement or shape edits.
|
||||
const affected = this.items || [];
|
||||
let moved = 0;
|
||||
for (const item of affected) {
|
||||
if (!item || item.dead || item === source) continue;
|
||||
const mechanical = global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type);
|
||||
if (!mechanical && item.type !== "grave") continue;
|
||||
let dx = (Number(item.x || 0) || 0) - x;
|
||||
let dy = (Number(item.y || 0) || 0) - y;
|
||||
let d = Math.hypot(dx, dy);
|
||||
const bodyExtent = mechanical ? Math.max(18, Number(global.TarinaiMechanicalSystem?.extent?.(item) || item.r || 0) || 0) : Math.max(16, Number(item.r || 0) || 0);
|
||||
if (d > radius + bodyExtent) continue;
|
||||
if (d < 0.001) {
|
||||
const a = deterministicAngle(this, `${salt}-overlap`, source || item, item);
|
||||
dx = Math.cos(a); dy = Math.sin(a); d = 1;
|
||||
}
|
||||
const q = clamp(1 - d / Math.max(1, radius), 0, 1);
|
||||
const force = (430 + q * 960) * Math.max(0.1, Number(scale || 1) || 1);
|
||||
const nx = dx / d, ny = dy / d;
|
||||
if (item.type === "grave") {
|
||||
if (global.TarinaiMemorialBellSystem?.applyExternalForce?.(this, item, force, source, "explosion")) moved += 1;
|
||||
continue;
|
||||
}
|
||||
const contactScale = Math.max(18, Math.min(160, Number(global.TarinaiMechanicalSystem?.extent?.(item) || item.r || 40) || 40)) * 0.55;
|
||||
// Give rotators a small off-centre pressure component as well as translation.
|
||||
// A purely radial force through the pivot produces zero torque and previously
|
||||
// made blasts look as though they did nothing to a rotator.
|
||||
const tangentOffset = item.type === "rotator" ? Math.min(56, contactScale * 0.42) : 0;
|
||||
const contactX = item.x - nx * contactScale - ny * tangentOffset;
|
||||
const contactY = item.y - ny * contactScale + nx * tangentOffset;
|
||||
const hit = global.TarinaiMechanicalSystem?.applyExternalImpulse?.(item, contactX, contactY, nx * force, ny * force, this, item.type === "rotator" ? 1.18 : 1);
|
||||
if (hit) moved += 1;
|
||||
}
|
||||
if (moved) { this.markSpatialDirty?.("explosion-external-force"); this.drawListDirty = true; }
|
||||
return moved;
|
||||
},
|
||||
|
||||
blastLoosePinsFrom(x, y, blastRadius, source = null, scale = 1, salt = "blast") {
|
||||
let blastedPins = 0;
|
||||
const pinSystem = globalThis.TarinaiItemDynamicPinSystem;
|
||||
|
|
@ -221,6 +271,7 @@
|
|||
if (!t.dead && deterministicChance(this, "firecracker-panic-bubble", 0.45, it, t)) this.spawnBubble(t.x, t.y - t.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
||||
}
|
||||
if (typeof damageNearbyStructures === "function") damageNearbyStructures(this, x, y, blastRadius, 34 * (it.blastScale || 1), it);
|
||||
this.applyExplosionExternalForces?.(x, y, blastRadius, it, it.blastScale || 1, "firecracker");
|
||||
this.damageAntsInRadius(x, y, blastRadius, p => 5 + p * 18, "\u7206\u7af9", {
|
||||
push: p => (260 + (p * p * 0.55 + p * 0.45) * 920) * 0.42 * (it.blastScale || 1),
|
||||
source: it,
|
||||
|
|
@ -269,6 +320,7 @@
|
|||
this.spawnEffect("ring", x, y, { size: (30 + r * 24) * blastScale, life: 0.48 + r * 0.11, color: r % 2 ? "rgba(255,116,62,0.78)" : "rgba(255,240,124,0.88)" });
|
||||
}
|
||||
this.blastZunchiFrom(x, y, blastRadius * 0.92, blastScale);
|
||||
this.applyExplosionExternalForces?.(x, y, blastRadius, t, blastScale, "disease-explosion");
|
||||
for (let i = 0; i < 34; i++) {
|
||||
const a = Math.PI * 2 * i / 34 + deterministicRange(this, "disease-explosion-spark-angle", -0.10, 0.10, t, i);
|
||||
const speed = deterministicRange(this, "disease-explosion-spark-speed", 90, 260, t, i) * blastScale;
|
||||
|
|
@ -360,15 +412,18 @@
|
|||
const nx = speed > 1 ? dx / speed : Math.cos(this.time * 9.1);
|
||||
const ny = speed > 1 ? dy / speed : Math.sin(this.time * 7.7);
|
||||
let cleaned = 0;
|
||||
let itemTouched = false;
|
||||
for (const it of this.nearbyItems(x, y, radius + 80)) {
|
||||
if (!it || it.dead) continue;
|
||||
const d = distXY(it.x, it.y, x, y);
|
||||
if (d > radius + (it.r || 12)) continue;
|
||||
const p = clamp(1 - d / (radius + (it.r || 12)), 0, 1);
|
||||
if (it.type === "zunchi" || it.type === "splat" || it.type === "trace") {
|
||||
it.amount -= dt * (it.type === "zunchi" ? 230 : 180) * (0.35 + p);
|
||||
if (it.type === "zunchi") {
|
||||
itemTouched = true;
|
||||
it.amount -= dt * 230 * (0.35 + p);
|
||||
cleaned += p;
|
||||
} else if (it.type === "grass") {
|
||||
itemTouched = true;
|
||||
it.amount = Math.max(0, (Number(it.amount) || 0) - dt * 135 * (0.30 + p));
|
||||
it.growth = Math.max(0, (Number(it.growth) || 0) - dt * 0.50 * (0.20 + p));
|
||||
if (it.amount <= 0.5) {
|
||||
|
|
@ -379,6 +434,7 @@
|
|||
}
|
||||
cleaned += p * 0.72;
|
||||
} else if (it.type === "fire") {
|
||||
itemTouched = true;
|
||||
it.amount = Math.max(0, (Number(it.amount) || 0) - dt * 540 * (0.35 + p));
|
||||
it.flameLife = Math.max(0, (Number(it.flameLife) || 0) - dt * 5.2 * (0.35 + p));
|
||||
it.fireShrinkPulse = Math.max(Number(it.fireShrinkPulse || 0), 0.18);
|
||||
|
|
@ -388,12 +444,31 @@
|
|||
}
|
||||
cleaned += p * 1.15;
|
||||
} else if (it.type === "grass_bed") {
|
||||
itemTouched = true;
|
||||
it.amount = 0;
|
||||
it.hp = 0;
|
||||
global.TarinaiStructureLifecycle.deleteItem(this, it, { reason: "water-hose-grass-bed", userReason: "\u6d17\u6d44\u3067\u6d41\u3055\u308c\u305f", wake: true });
|
||||
global.TarinaiStructureLifecycle.deleteItem(this, it, { reason: "water-hose-grass-bed", userReason: "洗浄で流された", wake: true });
|
||||
cleaned += p * 0.95;
|
||||
} else if (it.type === "grave") {
|
||||
itemTouched = true;
|
||||
const removed = global.TarinaiStructureLifecycle?.deleteItem?.(this, it, {
|
||||
reason: "water-hose-grave",
|
||||
userReason: "洗浄で流された",
|
||||
wake: true,
|
||||
});
|
||||
if (removed) cleaned += p * 1.05;
|
||||
}
|
||||
}
|
||||
for (const residue of this.nearbyResidues?.(x, y, radius + 80) || []) {
|
||||
if (!residue || residue.dead) continue;
|
||||
const d = distXY(residue.x, residue.y, x, y);
|
||||
if (d > radius + (residue.r || 12)) continue;
|
||||
const p = clamp(1 - d / (radius + (residue.r || 12)), 0, 1);
|
||||
residue.amount = Math.max(0, (Number(residue.amount) || 0) - dt * 180 * (0.35 + p));
|
||||
cleaned += p;
|
||||
if (residue.amount <= 0.5) this.removeResidue?.(residue, "water-hose-residue");
|
||||
else this.markTerrainDirtyAt?.(residue.x, residue.y, Math.max(residue.r || 24, 36), "water-hose-residue");
|
||||
}
|
||||
for (const ef of this.effects || []) {
|
||||
if (!ef || ef.dead) continue;
|
||||
if (ef.type !== "bleed" && ef.type !== "splat") continue;
|
||||
|
|
@ -432,11 +507,13 @@
|
|||
}
|
||||
if (cleaned > 0.25) {
|
||||
this.drawListDirty = true;
|
||||
this.markItemBucketsDirty?.("water-hose-clean");
|
||||
this.markTerrainDirtyAt?.(x, y, radius + 84, "water-hose-clean");
|
||||
this.markSpatialDirty?.("water-hose-clean");
|
||||
this.compactItems?.();
|
||||
this.updateItemCounts();
|
||||
if (itemTouched) {
|
||||
this.markItemBucketsDirty?.("water-hose-clean");
|
||||
this.markSpatialDirty?.("water-hose-clean");
|
||||
this.compactItems?.();
|
||||
this.updateItemCounts();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -512,18 +589,29 @@
|
|||
},
|
||||
|
||||
spawnBubble(x, y, text, color = "rgba(42,36,29,0.78)") {
|
||||
if ((this.effectCounts.bubble || 0) >= CONFIG.bubbleLimit) return;
|
||||
const bubbleLimit = Math.max(1, Number(CONFIG.bubbleLimit || 32) || 32);
|
||||
let bubbleCount = Math.max(0, Number(this.effectCounts.bubble || 0) || 0);
|
||||
if (bubbleCount >= bubbleLimit) {
|
||||
bubbleCount = 0;
|
||||
for (const effect of this.effects || []) if (effect && !effect.dead && effect.type === "bubble") bubbleCount += 1;
|
||||
this.effectCounts.bubble = bubbleCount;
|
||||
if (bubbleCount >= bubbleLimit) return;
|
||||
}
|
||||
if (text === "z") text = "Zzz...";
|
||||
audio.bubble(text);
|
||||
this.spawnEffect("bubble", x, y, {
|
||||
const effect = this.spawnEffect("bubble", x, y, {
|
||||
vx: deterministicRange(this, "bubble-vx", -2, 2, x, y, text),
|
||||
vy: deterministicRange(this, "bubble-vy", -5, -2, x, y, text),
|
||||
life: deterministicRange(this, "bubble-life", 2.2, 3.2, x, y, text),
|
||||
size: 8,
|
||||
text,
|
||||
color,
|
||||
forceVisual: true,
|
||||
importance: 3,
|
||||
});
|
||||
if (!effect) return null;
|
||||
this.effectCounts.bubble = (this.effectCounts.bubble || 0) + 1;
|
||||
return effect;
|
||||
},
|
||||
|
||||
spawnHeadbuttEffect(x, y) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue