This commit is contained in:
33333-33333 2026-07-11 21:13:39 +09:00
commit d14550dad6
78 changed files with 2827 additions and 563 deletions

View file

@ -11,6 +11,7 @@ const RESET_PRESETS = Object.freeze({
family: { label: "\u5BB6\u65CF" },
war: { label: "\u6226\u4E89" },
happy: { label: "\u5E78\u798F" },
hair_trigger: { label: "\u4E00\u89E6\u5373\u767A" },
});
function normalizeResetPresetId(presetId = "default") {
@ -152,6 +153,10 @@ const RESET_PRESETS = Object.freeze({
const otherRects = worldRef.solidObstacleRects?.(other) || globalThis.TarinaiCollisionFootprints?.placementRectsFor?.(worldRef, other) || [];
const otherRadius = presetItemFootprintRadius(worldRef, other);
const margin = (itemIsFence || otherIsFence) ? 4 : Math.max(8, Math.min(itemRadius, otherRadius) * 0.28);
if (opts.rotationEnvelope && item.type === "rotator" && otherIsFence && otherRects.length) {
const envelope = Math.max(itemRadius, globalThis.TarinaiMechanicalSystem?.reach?.(item) || 0) + Math.max(8, Number(opts.rotationMargin || 12) || 12);
if (otherRects.some(rect => globalThis.TarinaiCollisionFootprints?.rectCircleOverlap?.(rect, item.x, item.y, envelope, 0))) return true;
}
if (itemRects.length && otherRects.length) {
if (itemRects.some(a => otherRects.some(b => globalThis.TarinaiCollisionFootprints?.rectsOverlap?.(a, b, margin)))) return true;
continue;
@ -200,8 +205,19 @@ const RESET_PRESETS = Object.freeze({
function addPresetItem(worldRef, type, x, y, opts = {}) {
const item = new Item(type, x, y);
if (Number.isFinite(Number(opts.r))) item.r = Number(opts.r);
if (Number.isFinite(Number(opts.angle))) item.angle = Number(opts.angle);
if (opts.randomAngle) item.angle = randomPresetAngle(Number(opts.angleStep) || Math.PI / 4);
else if (Number.isFinite(Number(opts.angle))) item.angle = Number(opts.angle);
if (opts.toolSize) item.toolSize = opts.toolSize;
if (type === "fan") {
const center = Number.isFinite(Number(opts.fanSwingCenter)) ? Number(opts.fanSwingCenter) : (Number(item.angle) || 0);
item.angle = center;
item.fanBodyAngle = Number.isFinite(Number(opts.fanBodyAngle)) ? Number(opts.fanBodyAngle) : center;
item.fanSwingCenter = center;
item.fanSwingRange = Number.isFinite(Number(opts.fanSwingRange)) ? Number(opts.fanSwingRange) : (35 * Math.PI / 180);
item.fanSwingSpeed = Number.isFinite(Number(opts.fanSwingSpeed)) ? Number(opts.fanSwingSpeed) : (48 * Math.PI / 180);
item.fanSwingPhase = Number.isFinite(Number(opts.fanSwingPhase)) ? Number(opts.fanSwingPhase) : rand(0, Math.PI * 2);
item.fanSwingOn = opts.fanSwingOn === true && item.fanSwingRange > 0 && item.fanSwingSpeed > 0;
}
if (type === "duplicator") {
item.storedFoodType = opts.storedFoodType || "sweet";
item.storedFoodLabel = typeof toolLabel === "function" ? toolLabel(item.storedFoodType) : item.storedFoodType;
@ -235,13 +251,15 @@ const RESET_PRESETS = Object.freeze({
function addPresetItemsScattered(worldRef, type, count, zone = null, opts = {}) {
let placed = 0;
const placedItems = [];
const n = Math.max(0, Number(count) | 0);
const attempts = opts.avoidOverlap ? Math.max(n * 4, n + 8) : n;
for (let i = 0; i < attempts && placed < n; i++) {
const p = randPoint(worldRef, opts.pad || 82, zone);
const item = addPresetItem(worldRef, type, p.x, p.y, opts);
if (item) placed++;
if (item) { placed++; placedItems.push(item); }
}
return placedItems;
}
function addPresetFenceLine(worldRef, count, yRatio, angle = 0, type = "fence_h", opts = {}) {
@ -264,6 +282,48 @@ const RESET_PRESETS = Object.freeze({
}
}
function addPresetBounceFenceBox(worldRef, cx, cy, halfW, halfH) {
const fenceLength = 142;
const horizontalCount = Math.max(2, Math.ceil((halfW * 2) / fenceLength));
const verticalCount = Math.max(2, Math.ceil((halfH * 2) / fenceLength));
const horizontalStep = (halfW * 2) / horizontalCount;
const verticalStep = (halfH * 2) / verticalCount;
for (let i = 0; i < horizontalCount; i++) {
const x = cx - halfW + horizontalStep * (i + 0.5);
addPresetItem(worldRef, "bounce_fence", x, cy - halfH, { angle: 0, reason: "preset-hair-trigger-boundary" });
addPresetItem(worldRef, "bounce_fence", x, cy + halfH, { angle: 0, reason: "preset-hair-trigger-boundary" });
}
for (let i = 0; i < verticalCount; i++) {
const y = cy - halfH + verticalStep * (i + 0.5);
addPresetItem(worldRef, "bounce_fence", cx - halfW, y, { angle: Math.PI / 2, reason: "preset-hair-trigger-boundary" });
addPresetItem(worldRef, "bounce_fence", cx + halfW, y, { angle: Math.PI / 2, reason: "preset-hair-trigger-boundary" });
}
return { cx, cy, halfW, halfH };
}
function pointOutsidePresetBox(point, box, margin = 0) {
return Math.abs((point?.x || 0) - box.cx) > box.halfW + margin || Math.abs((point?.y || 0) - box.cy) > box.halfH + margin;
}
function addPresetTarinaiOutsideBox(worldRef, count, box) {
const n = Math.max(0, Number(count) | 0);
const zones = [
{ x0: 82, x1: Math.max(82, box.cx - box.halfW - 70), y0: 82, y1: worldRef.h - 82 },
{ x0: Math.min(worldRef.w - 82, box.cx + box.halfW + 70), x1: worldRef.w - 82, y0: 82, y1: worldRef.h - 82 },
{ x0: box.cx - box.halfW, x1: box.cx + box.halfW, y0: 82, y1: Math.max(82, box.cy - box.halfH - 70) },
{ x0: box.cx - box.halfW, x1: box.cx + box.halfW, y0: Math.min(worldRef.h - 82, box.cy + box.halfH + 70), y1: worldRef.h - 82 },
].filter(zone => zone.x1 >= zone.x0 && zone.y1 >= zone.y0);
for (let i = 0; i < n; i++) {
let p = randPoint(worldRef, 82, zones[i % Math.max(1, zones.length)] || null);
if (!pointOutsidePresetBox(p, box, 28)) {
const side = i % 2 === 0 ? -1 : 1;
p = { x: clamp(box.cx + side * (box.halfW + 96), 82, worldRef.w - 82), y: clamp(box.cy + rand(-box.halfH, box.halfH), 82, worldRef.h - 82) };
}
worldRef.addTarinai({ x: p.x, y: p.y, generation: 1 });
}
}
function generateDefaultPreset(worldRef, field, seedPopulation) {
const population = seedPopulation ?? field.population ?? CONFIG.initialPopulation;
const grassCount = field.grass ?? 18;
@ -282,10 +342,11 @@ const RESET_PRESETS = Object.freeze({
addPresetItemsScattered(worldRef, "stone", presetCount(field, compact ? 4 : 6, 3, 9), center, { pad: compact ? 118 : 132, reason: "preset-athletic-physical-block", avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 260 : 220 });
addPresetItemsScattered(worldRef, "stove", presetCount(field, 2, 1, 3), center, { pad: 118, reason: "preset-athletic-stove", avoidOverlap: true, avoidAllObstacles: true, attempts: 100, maxRadius: 210 });
addPresetItemsScattered(worldRef, "ball", presetCount(field, compact ? 8 : 10, 4, 18), null, { pad: 96, reason: "preset-ball", avoidOverlap: true, avoidAllObstacles: true, attempts: 90, maxRadius: compact ? 230 : 190 });
addPresetItemsScattered(worldRef, "balloon", presetCount(field, compact ? 5 : 7, 3, 12), center, { pad: 92, reason: "preset-athletic-water-balloon", avoidOverlap: true, avoidAllObstacles: true, attempts: 100, maxRadius: compact ? 240 : 200 });
addPresetFenceLine(worldRef, Math.max(2, Math.round((compact ? 3 : 4) * factor)), 0.36, 0, "fence_h", { avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210, reason: "preset-athletic-fence-line" });
addPresetFenceLine(worldRef, Math.max(1, Math.round((compact ? 2 : 3) * factor)), 0.62, Math.PI / 2, "fence_v", { avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210, reason: "preset-athletic-fence-line" });
addPresetFencesScattered(worldRef, Math.max(1, Math.round((compact ? 2 : 4) * factor)), null, { pad: 108, reason: "preset-athletic-fence", avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210 });
addPresetItemsScattered(worldRef, "rotator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-rotator-slow-custom", motorSpeed: Math.PI * 0.18, thickness: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 280 : 240 });
addPresetItemsScattered(worldRef, "rotator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-rotator-simple", segments: [[-66, 0, 66, 0]], motorSpeed: Math.PI * 0.18, thickness: 13, rotationEnvelope: true, rotationMargin: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 180, maxRadius: compact ? 320 : 280 });
addPresetItemsScattered(worldRef, "reciprocator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-reciprocator-slow-custom", railMotorSpeed: 36, railTravel: 120, thickness: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 280 : 240 });
}
@ -317,6 +378,8 @@ const RESET_PRESETS = Object.freeze({
addPresetItemsScattered(worldRef, "ant_nest", presetCount(field, 1.25, 1, 2), null, { pad: 116, reason: "preset-war-ant-nest", avoidOverlap: true, attempts: 80, maxRadius: 190 });
addPresetItemsScattered(worldRef, "pushpin", presetCount(field, 5, 2, 9), null, { pad: 88, reason: "preset-war-pushpin-reduced" });
addPresetItemsScattered(worldRef, "oshibyo", presetCount(field, 8, 4, 16), null, { pad: 88, reason: "preset-war-oshibyo" });
addPresetItemsScattered(worldRef, "fan", presetCount(field, 2, 1, 4), null, { pad: 118, reason: "preset-war-rotating-fan", randomAngle: true, fanSwingOn: true, fanSwingRange: 110 * Math.PI / 180, fanSwingSpeed: 72 * Math.PI / 180, avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: 230 });
addPresetItemsScattered(worldRef, "bounce_fence", presetCount(field, 3, 2, 5), null, { pad: 104, reason: "preset-war-bounce-fence", randomAngle: true, avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: 230 });
addPresetFencesScattered(worldRef, presetCount(field, 8, 4, 14), null, { pad: 94, reason: "preset-war-fence" });
}
@ -325,10 +388,38 @@ const RESET_PRESETS = Object.freeze({
addPresetTarinai(worldRef, presetCount(field, 16, 8, 28), center);
addPresetItemsScattered(worldRef, "nest_box", presetCount(field, 8, 4, 14), center, { pad: 96, reason: "preset-happy-nest-box" });
addPresetItemsScattered(worldRef, "robot_cleaner", presetCount(field, 2, 1, 3), center, { pad: 110, reason: "preset-happy-robot-cleaner" });
addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 2, 1, 4), center, { pad: 110, storedFoodType: "sweet", reason: "preset-happy-duplicator-zunda" });
const happyDuplicators = addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 2, 1, 4), center, { pad: 110, storedFoodType: "sweet", reason: "preset-happy-duplicator-zunda" });
for (const duplicator of happyDuplicators) addPresetItem(worldRef, "toilet", duplicator.x, duplicator.y, { reason: "preset-happy-toilet-sand" });
addPresetItemsScattered(worldRef, "rain_shelter", presetCount(field, 3, 2, 5), center, { pad: 132, reason: "preset-happy-parasol", avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: 240 });
addPresetItemsScattered(worldRef, "balloon", presetCount(field, 6, 3, 10), center, { pad: 88, reason: "preset-happy-water-balloon", avoidOverlap: true, attempts: 90, maxRadius: 190 });
addPresetItemsScattered(worldRef, "ball", presetCount(field, 6, 3, 10), center, { pad: 88, reason: "preset-happy-ball" });
}
function generateHairTriggerPreset(worldRef, field) {
const cx = worldRef.w * 0.5;
const cy = worldRef.h * 0.5;
const halfW = clamp(worldRef.w * 0.18, 132, 238);
const halfH = clamp(worldRef.h * 0.18, 104, 182);
const box = addPresetBounceFenceBox(worldRef, cx, cy, halfW, halfH);
const inside = {
x0: cx - halfW + 52,
x1: cx + halfW - 52,
y0: cy - halfH + 52,
y1: cy + halfH - 52,
};
addPresetItemsScattered(worldRef, "ball", presetCount(field, 7, 4, 12), inside, { pad: 0, reason: "preset-hair-trigger-ball" });
addPresetTarinaiOutsideBox(worldRef, presetCount(field, 14, 7, 24), box);
const duplicatorCount = presetCount(field, 2, 1, 4);
for (let i = 0; i < duplicatorCount; i++) {
const side = i % 2 === 0 ? -1 : 1;
const x = clamp(cx + side * (halfW + 112), 84, worldRef.w - 84);
const spread = duplicatorCount <= 2 ? 0 : ((i / Math.max(1, duplicatorCount - 1)) - 0.5) * Math.min(worldRef.h * 0.42, 260);
const y = clamp(cy + spread, 84, worldRef.h - 84);
addPresetItem(worldRef, "duplicator", x, y, { storedFoodType: "food", reason: "preset-hair-trigger-pet-food-duplicator" });
}
}
function addDefaultPresetRobotCleaner(worldRef, presetId = "default") {
if (!worldRef || normalizeResetPresetId(presetId) === "empty") return;
if ((worldRef.items || []).some(item => item && !item.dead && item.type === "robot_cleaner")) return;
@ -355,6 +446,7 @@ const RESET_PRESETS = Object.freeze({
case "family": generateFamilyPreset(worldRef, field); break;
case "war": generateWarPreset(worldRef, field); break;
case "happy": generateHappyPreset(worldRef, field); break;
case "hair_trigger": generateHairTriggerPreset(worldRef, field); break;
case "default":
default:
generateDefaultPreset(worldRef, field, seedPopulation);