95 lines
4.8 KiB
JavaScript
95 lines
4.8 KiB
JavaScript
"use strict";
|
|
|
|
// Layer: simulation/system-items-ants
|
|
// Owns scheduled item updates, random grass growth, ball/ant spatial refresh,
|
|
// and item/ant spatial post-processing.
|
|
(function (global) {
|
|
const helpers = global.TarinaiSimulationRuntime.helpers;
|
|
|
|
function updateRandomGrassGrowth(worldRef, dt) {
|
|
if (!worldRef || !global.TarinaiGrass) return false;
|
|
if ((worldRef.groundGrassMultiplier?.() ?? 1) <= 0) return false;
|
|
const groundGrowthMul = Math.max(0, Number(global.TarinaiGround.grassGrowthMultiplier(worldRef.groundType || "soil") ?? (worldRef.groundGrassMultiplier?.() ?? 1)) || 0);
|
|
if (groundGrowthMul <= 0) return false;
|
|
worldRef.grassGrowthTimer = (worldRef.grassGrowthTimer || 0) + dt;
|
|
const field = worldRef.fieldType || "garden";
|
|
const growthSpeedMul = 6.0 * groundGrowthMul;
|
|
const interval = (field === "park" ? 1.8 : (field === "cage" ? 3.2 : 2.4)) / Math.max(0.1, growthSpeedMul);
|
|
if (worldRef.grassGrowthTimer < interval) return false;
|
|
worldRef.grassGrowthTimer = 0;
|
|
const grasses = worldRef.itemsOfType ? worldRef.itemsOfType("grass") : (worldRef.items || []).filter(it => it && it.type === "grass");
|
|
if (!grasses || !grasses.length) return false;
|
|
const tries = Math.min(12, grasses.length);
|
|
for (let i = 0; i < tries; i++) {
|
|
const it = grasses[Math.floor(Math.random() * grasses.length)];
|
|
if (!it || it.dead || (it.amount || 0) <= 0) continue;
|
|
if (worldRef.grassBlockedAt?.(it.x, it.y, it)) {
|
|
if (global.TarinaiGrass.regress(it, 1) > 0) {
|
|
worldRef.markTerrainDirtyAt?.(it.x, it.y, Math.max(it.r || 24, 36), "random-grass-blocked") || worldRef.markTerrainDirty?.("random-grass-blocked");
|
|
worldRef.markItemBucketsDirty?.("random-grass-blocked");
|
|
return true;
|
|
}
|
|
continue;
|
|
}
|
|
if (global.TarinaiGrass.advance(it, 1)) {
|
|
worldRef.markTerrainDirtyAt?.(it.x, it.y, Math.max(it.r || 24, 36), "random-grass-grow") || worldRef.markTerrainDirty?.("random-grass-grow");
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function activeBallLikeCount(worldRef) {
|
|
if (!worldRef?.itemsOfType) return (worldRef?.items || []).filter(it => it && !it.dead && (it.type === "ball" || it.type === "balloon") && (Math.hypot(it.vx || 0, it.vy || 0) > 0.08 || (it.dropTimer || 0) > 0 || it.playerHeld || it._heldByPlayer)).length;
|
|
let count = 0;
|
|
for (const type of ["ball", "balloon"]) {
|
|
for (const it of worldRef.itemsOfType(type) || []) {
|
|
if (!it || it.dead) continue;
|
|
const active = Math.hypot(it.vx || 0, it.vy || 0) > 0.08 || (it.dropTimer || 0) > 0 || it.playerHeld || it._heldByPlayer || (worldRef.time || 0) - Number(it.lastKickedAt || -999) < 0.35;
|
|
if (active) count += 1;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function updateItemsAndAnts(worldRef, dt) {
|
|
const h = helpers;
|
|
const itemCountBefore = worldRef.items.length;
|
|
const prevMode = worldRef.deferSpatialRebuildMode || "";
|
|
worldRef.deferSpatialRebuildDepth = (worldRef.deferSpatialRebuildDepth || 0) + 1;
|
|
worldRef.deferSpatialRebuildMode = "mobile";
|
|
let itemsMoved = false;
|
|
let antsMoved = false;
|
|
try {
|
|
h.updateItemsScheduled(worldRef, dt);
|
|
updateRandomGrassGrowth(worldRef, dt);
|
|
itemsMoved = h.markScheduledItemsMoved(worldRef, "items-moved", 0.25);
|
|
const activeBalls = activeBallLikeCount(worldRef);
|
|
if (activeBalls >= 2) {
|
|
worldRef.ensureSpatial?.("ball-ball-collisions");
|
|
const solved = worldRef.resolveBallBallCollisions(dt) || 0;
|
|
if (solved > 0) worldRef.markSpatialDirty?.("ball-ball-collisions");
|
|
} else {
|
|
worldRef.workStats && (worldRef.workStats.ballCollisionSkips = (worldRef.workStats.ballCollisionSkips || 0) + 1);
|
|
}
|
|
worldRef.updateAnts?.(dt);
|
|
antsMoved = h.markSpatialDirtyIfMoved(worldRef, worldRef.ants || [], "ants-moved", 0.25);
|
|
} finally {
|
|
worldRef.deferSpatialRebuildDepth = Math.max(0, (worldRef.deferSpatialRebuildDepth || 1) - 1);
|
|
worldRef.deferSpatialRebuildMode = prevMode;
|
|
}
|
|
if (itemsMoved || antsMoved || worldRef.spatialDirty) {
|
|
// Mobile items/ants are obstacle and contact sources for the next phase.
|
|
// Force one fresh grid after the mobile phase; reads inside the phase are
|
|
// allowed to use the frame-start grid to avoid rebuild ping-pong.
|
|
if (worldRef.rebuildSpatial) worldRef.rebuildSpatial(true, worldRef.deferredSpatialDirtyReason || "post-mobile-item-update");
|
|
else worldRef.ensureSpatial?.("post-mobile-item-update");
|
|
worldRef.deferredSpatialDirtyReason = "";
|
|
}
|
|
return { itemCountBefore, itemsMoved, antsMoved };
|
|
}
|
|
|
|
const phases = Object.freeze({ updateItemsAndAnts });
|
|
|
|
global.TarinaiItemAntSimulationSystem = Object.freeze({ phases });
|
|
})(typeof window !== "undefined" ? window : globalThis);
|