tarinai/js/world_placement_log.js

475 lines
23 KiB
JavaScript
Raw Normal View History

2026-06-21 14:09:35 +09:00
"use strict";
(function (global) {
const World = global.World;
if (!World) throw new Error("World is not available for mixin: world_placement_log.js");
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
fencePlacementBlocked(item) {
if (!item || !this.isFenceType(item.type)) return false;
const rect = this.fenceRect(item);
if (!rect) return false;
const margin = Math.max(6, (item.r || 42) * 0.24);
if (rect.left < CONFIG.worldPadding || rect.top < CONFIG.worldPadding || rect.right > this.w - CONFIG.worldPadding || rect.bottom > this.h - CONFIG.worldPadding) return true;
const searchRadius = Math.max(rect.right - rect.left, rect.bottom - rect.top) * 0.5 + 72;
for (const it of this.nearbyItems(item.x, item.y, searchRadius)) {
if (!it || it.dead) continue;
for (const r of this.solidObstacleRects(it)) {
const separated = rect.right + margin < r.left || rect.left - margin > r.right || rect.bottom + margin < r.top || rect.top - margin > r.bottom;
if (!separated) return true;
}
}
return false;
},
importantPlacementOverlapBlocked(item) {
if (!item || item.dead) return true;
const important = new Set(["grass", "fence_v", "fence_h", "nest_box", "water_bowl"]);
if (!important.has(item.type)) return false;
const placingGrass = item.type === "grass";
const itemRects = this.solidObstacleRects(item);
const radiusFor = typeof itemRadiusFor === "function" ? itemRadiusFor : ((type, fallback = 12) => fallback);
const itemRadius = Math.max(14, item.r || radiusFor(item.type, 12) || 12);
const rectCircleOverlap = (rect, cx, cy, radius, margin = 0) => {
if (!rect) return false;
const px = clamp(cx, rect.left - margin, rect.right + margin);
const py = clamp(cy, rect.top - margin, rect.bottom + margin);
return distXY(cx, cy, px, py) < radius + margin;
};
const rectsOverlap = (a, b, margin = 0) => Boolean(a && b && !(a.right + margin < b.left || a.left - margin > b.right || a.bottom + margin < b.top || a.top - margin > b.bottom));
for (const other of this.nearbyItems(item.x, item.y, Math.max(132, itemRadius * 4))) {
if (!other || other === item || other.dead || !important.has(other.type)) continue;
// Grass is a ground layer: objects may be placed on top of existing grass.
// Grass placement itself still avoids objects and other grass via grassSpotOpen().
if (!placingGrass && other.type === "grass") continue;
const otherRects = this.solidObstacleRects(other);
const otherRadius = Math.max(14, other.r || radiusFor(other.type, 12) || 12);
const margin = Math.max(4, Math.min(itemRadius, otherRadius) * 0.18);
if (itemRects.length && otherRects.length) {
if (itemRects.some(a => otherRects.some(b => rectsOverlap(a, b, margin)))) return true;
continue;
}
if (itemRects.length) {
if (itemRects.some(r => rectCircleOverlap(r, other.x, other.y, otherRadius, margin))) return true;
continue;
}
if (otherRects.length) {
if (otherRects.some(r => rectCircleOverlap(r, item.x, item.y, itemRadius, margin))) return true;
continue;
}
if (distXY(item.x, item.y, other.x, other.y) < itemRadius + otherRadius + margin) return true;
}
return false;
},
placementBlocked(item) {
if (!item || item.dead) return true;
if (item.type === "genkotsu") return false;
const pad = CONFIG.worldPadding || 30;
const rects = this.solidObstacleRects(item);
if (rects.length) {
for (const rect of rects) {
if (rect.left < pad || rect.top < pad || rect.right > this.w - pad || rect.bottom > this.h - pad) return true;
}
} else {
const radius = Math.max(14, (item.r || 12) * (item.type === "bed" ? 1.55 : 1.25));
if (item.x - radius < pad || item.y - radius < pad || item.x + radius > this.w - pad || item.y + radius > this.h - pad) return true;
}
if (item.type === "grass" && !this.grassSpotOpen(item.x, item.y, { avoidTarinai: true })) return true;
if (this.isFenceType(item.type) && this.fencePlacementBlocked(item)) return true;
if (this.importantPlacementOverlapBlocked(item)) return true;
if (item.type === "nest_box") {
const margin = Math.max(8, (item.r || 42) * 0.18);
for (const it of this.nearbyItems(item.x, item.y, Math.max(120, (item.r || 42) * 3))) {
if (!it || it === item || it.dead) continue;
for (const rect of this.solidObstacleRects(it)) {
for (const own of rects) {
const separated = own.right + margin < rect.left || own.left - margin > rect.right || own.bottom + margin < rect.top || own.top - margin > rect.bottom;
if (!separated) return true;
}
}
}
}
return false;
},
placementClampPointFor(item, x = item?.x || 0, y = item?.y || 0) {
const pad = CONFIG.worldPadding || 30;
const rects = this.solidObstacleRects(item);
if (rects.length) {
let left = 0, right = 0, top = 0, bottom = 0;
for (const rect of rects) {
left = Math.max(left, (item.x || 0) - rect.left);
right = Math.max(right, rect.right - (item.x || 0));
top = Math.max(top, (item.y || 0) - rect.top);
bottom = Math.max(bottom, rect.bottom - (item.y || 0));
}
return {
x: clamp(x, pad + left, this.w - pad - right),
y: clamp(y, pad + top, this.h - pad - bottom),
};
}
const radius = Math.max(14, (item.r || 12) * (item.type === "bed" ? 1.55 : 1.25));
return {
x: clamp(x, pad + radius, this.w - pad - radius),
y: clamp(y, pad + radius, this.h - pad - radius),
};
},
placementProbeItem(item, x, y) {
return { ...item, x, y, dead: false };
},
findPlacementSpot(item, { allowOriginal = true, maxRadius = null, attempts = null } = {}) {
if (!item || item.dead) return null;
if (item.type === "grass") {
const spot = this.findGrassPlantingSpot
? this.findGrassPlantingSpot(item.x, item.y, { allowOriginal, attempts: attempts || 42, maxRadius: maxRadius || 170, avoidTarinai: true })
: null;
if (!spot) return null;
const probe = this.placementProbeItem(item, spot.x, spot.y);
return this.placementBlocked(probe) ? null : spot;
}
const start = this.placementClampPointFor(item, item.x, item.y);
const original = this.placementProbeItem(item, start.x, start.y);
if (allowOriginal && !this.placementBlocked(original)) return start;
const solid = this.isFenceType(item.type) || item.type === "nest_box";
const searchRadius = maxRadius || (solid ? Math.max(150, (item.r || 42) * 4.2) : Math.max(90, (item.r || 16) * 3.2));
const count = attempts || (solid ? 72 : 36);
const golden = Math.PI * (3 - Math.sqrt(5));
let best = null;
let bestD = Infinity;
for (let i = 0; i < count; i++) {
const t = count <= 1 ? 1 : i / (count - 1);
const radius = Math.max(8, searchRadius * Math.sqrt(t));
const angle = i * golden + stableUnit(`${item.type}:${item.x.toFixed(1)},${item.y.toFixed(1)}`, "place") * Math.PI * 2;
const rawX = item.x + Math.cos(angle) * radius;
const rawY = item.y + Math.sin(angle) * radius * 0.74;
const p = this.placementClampPointFor(item, rawX, rawY);
const probe = this.placementProbeItem(item, p.x, p.y);
if (this.placementBlocked(probe)) continue;
const d = distXY(item.x, item.y, p.x, p.y);
if (d < bestD) { best = p; bestD = d; }
if (d < Math.max(18, (item.r || 12) * 0.75)) break;
}
return best;
},
findGrabTargetAt(x, y) {
let foundT = null;
for (let i = this.tarinai.length - 1; i >= 0; i--) {
const t = this.tarinai[i];
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) continue;
if (t.contains ? t.contains(x, y) : distXY(x, y, t.x, t.y) <= (t.radius || 22) * 1.4) { foundT = t; break; }
}
let foundItem = null, foundItemD = Infinity;
for (let i = this.items.length - 1; i >= 0; i--) {
const it = this.items[i];
if (!it || it.dead || it.type === "trace" || it.type === "splat") continue;
const hit = Math.max(24, (it.r || 12) * (it.type === "ball" ? 4.0 : 2.2));
const d = distXY(x, y, it.x, it.y);
if (d <= hit && d < foundItemD) { foundItem = it; foundItemD = d; }
}
if (foundT && foundItem) {
const td = distXY(x, y, foundT.x, foundT.y);
return td <= foundItemD + 8 ? foundT : foundItem;
}
return foundT || foundItem;
},
placeItem(item, dropped = true) {
if (!item) return null;
if (item.type === "grass" && (this.itemCounts?.grass || 0) >= (this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 96))) {
showToast("\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u306e\u8349\u306e\u4e0a\u9650\u306b\u9054\u3057\u3066\u3044\u307e\u3059\u3002");
return null;
}
if (this.isFenceType(item.type)) {
this.compactItems();
this.updateItemCounts();
if (this.liveFenceCount() >= this.fenceLimit()) {
showToast("\u67f5\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u5148\u306b\u4e00\u90e8\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044\u3002");
return null;
}
}
if (this.placementBlocked(item)) {
showToast(this.isFenceType(item.type) ? "\u305d\u3053\u306b\u306f\u67f5\u3092\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3002" : "\u305d\u3053\u306b\u306f\u914d\u7f6e\u3067\u304d\u307e\u305b\u3093\u3002");
return null;
}
if (dropped) {
2026-06-22 02:03:19 +09:00
item.dropMax = item.type === "genkotsu" ? 0.22 : (item.type === "stone" ? 0.72 : (isPinType(item.type) ? 0.48 : 0.58));
2026-06-21 14:09:35 +09:00
item.dropTimer = item.dropMax;
item.dropImpactDone = false;
}
this.items.push(item);
this.itemCounts[item.type] = (this.itemCounts[item.type] || 0) + 1;
this.markTerrainDirty?.(`place:${item.type}`);
this.emit("tool:place", { item, type: item.type, dropped });
2026-06-21 22:29:00 +09:00
this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type });
2026-06-21 14:09:35 +09:00
if (dropped) audio.place?.(item.type);
this.rebuildSpatial(true);
return item;
},
2026-06-23 11:27:55 +09:00
signboardAt(x, y) {
let sign = null;
let bestD = Infinity;
for (let i = (this.items || []).length - 1; i >= 0; i--) {
const it = this.items[i];
if (!it || it.dead || it.type !== "signboard") continue;
const d = distXY(x, y, it.x, it.y);
if (d <= Math.max(72, (it.r || 42) * 2.2) && d < bestD) { sign = it; bestD = d; }
}
return sign;
},
machineAt(x, y) {
let best = null;
let bestD = Infinity;
for (let i = (this.items || []).length - 1; i >= 0; i--) {
const it = this.items[i];
if (!it || it.dead || !(typeof isMachineType === "function" ? isMachineType(it.type) : (it.type === "duplicator" || it.type === "cannon"))) continue;
const hit = Math.max(46, (it.r || 42) * (it.type === "cannon" ? 1.35 : 1.55));
const d = distXY(x, y, it.x, it.y);
if (d <= hit && d < bestD) { best = it; bestD = d; }
}
return best;
},
setMachineSlot(machine, itemType) {
if (!machine || machine.dead || !itemType || !(typeof isMachineConsumableType === "function" ? isMachineConsumableType(itemType) : false)) return false;
if (machine.machineSlotType && machine.machineSlotType !== itemType) {
for (const it of this.items || []) {
if (it && !it.dead && it.machineProducerId === machine.id) it.amount = 0;
}
}
machine.machineSlotType = itemType;
machine.machineSlotSize = this.toolSizeFor ? this.toolSizeFor(itemType) : "medium";
machine.machineSpawnTimer = 0;
machine.cannonFireTimer = machine.type === "cannon" ? 0.35 : machine.cannonFireTimer;
const label = toolLabel(itemType);
this.log?.(`${toolLabel(machine.type)}${label}をセットした。`, "observe");
showToast(`${toolLabel(machine.type)}: ${label}をセット`);
this.rebuildSpatial?.(true);
return true;
},
rotateCannon(cannon, x, y) {
if (!cannon || cannon.dead || cannon.type !== "cannon") return false;
const dx = x - cannon.x;
const dy = y - cannon.y;
if (Math.hypot(dx, dy) > 3) cannon.cannonAngle = Math.atan2(dy, dx);
else cannon.cannonAngle = ((Number.isFinite(cannon.cannonAngle) ? cannon.cannonAngle : 0) + Math.PI / 4) % (Math.PI * 2);
const deg = Math.round((((cannon.cannonAngle * 180 / Math.PI) % 360) + 360) % 360);
showToast(`大砲の向き: ${deg}°`);
this.log?.(`大砲の向きを${deg}度に変えた。`, "observe");
return true;
},
editSignboard(sign) {
if (!sign || sign.dead || sign.type !== "signboard") return false;
const current = String(sign.text || "");
const next = typeof prompt === "function" ? prompt("看板のテキスト", current) : current;
if (next === null) return true;
sign.text = String(next || "").replace(/[\r\n]+/g, " ").slice(0, 36).trim();
sign.textEditedAt = this.time || 0;
this.markTerrainDirty?.("signboard:text");
this.log(sign.text ? `看板を書き換えた。` : `看板の文字を消した。`, "observe");
return true;
},
2026-06-21 14:09:35 +09:00
handleClick(x, y) {
const tool = this.tool;
if (tool === "observe") {
let found = null;
for (let i = this.tarinai.length - 1; i >= 0; i--) {
if (!this.isTarinaiHiddenInNestBox(this.tarinai[i]) && this.tarinai[i].contains(x, y)) { found = this.tarinai[i]; break; }
}
2026-06-22 02:03:19 +09:00
if (!found) {
2026-06-23 11:27:55 +09:00
const machine = this.machineAt?.(x, y) || null;
if (machine) {
if (machine.type === "cannon") this.rotateCannon?.(machine, x, y);
else showToast(machine.machineSlotType ? `複製機: ${toolLabel(machine.machineSlotType)}を複製中` : "複製機: 消費アイテムを選んでクリックするとセット");
this.selected = null;
return;
2026-06-22 02:03:19 +09:00
}
2026-06-23 11:27:55 +09:00
const sign = this.signboardAt?.(x, y) || null;
2026-06-22 02:03:19 +09:00
if (sign) {
2026-06-23 11:27:55 +09:00
this.editSignboard?.(sign);
2026-06-22 02:03:19 +09:00
this.selected = null;
return;
}
}
2026-06-21 14:09:35 +09:00
this.selected = found;
return;
}
if (tool === "poke") {
let ballTarget = null;
let ballTargetD = Infinity;
for (let i = this.items.length - 1; i >= 0; i--) {
const it = this.items[i];
if (!it || it.dead || it.type !== "ball") continue;
const hit = Math.max(72, (it.r || 18) * 4.0);
const d = distXY(x, y, it.x, it.y);
if (d <= hit && d < ballTargetD) { ballTarget = it; ballTargetD = d; }
}
if (ballTarget && this.pokeBall(ballTarget, x, y)) return;
const target = [...this.tarinai].reverse().find(t => t.contains(x, y));
if (target) {
target.poke();
if (!target.dead) this.log(`${target.name}\u306f\u3064\u3064\u304b\u308c\u3001\u305f\u308a\u306a\u3044\u3053\u3068\u3092\u899a\u3048\u305f\u3002`, "observe", { participants: [target] });
}
return;
}
if (tool === "new") {
const spot = this.edgeSpawnPoint(x, y, 42, true);
const t = this.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, type: baseSpriteId(), generation: 1, entryTimer: 2.0 });
t.wanderAngle = spot.angle;
audio.birth();
this.log(`${t.name}\u304c\u753b\u9762\u5916\u304b\u3089\u8ff7\u3044\u8fbc\u3093\u3067\u304d\u305f\u3002`);
return;
}
if (tool === "delete") {
let best = null, bestD = Infinity;
for (let i = this.items.length - 1; i >= 0; i--) {
const it = this.items[i];
if (it.dead) continue;
const visualHit = Math.max(22, (it.r || 12) * 2.0);
const hit = this.screenSizeToWorld ? this.screenSizeToWorld(visualHit) : visualHit;
const d = distXY(x, y, it.x, it.y);
if (d < hit && d < bestD) { best = it; bestD = d; }
}
if (!best) { showToast("\u524a\u9664\u3067\u304d\u308b\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u304c\u3042\u308a\u307e\u305b\u3093\u3002"); return; }
2026-06-22 02:03:19 +09:00
if (isPinType(best.type) && best.pinState === "lodged") { showToast("\u523a\u3055\u3063\u305f\u92f2\u306f\u3064\u307e\u3093\u3067\u53d6\u308a\u9664\u3044\u3066\u304f\u3060\u3055\u3044\u3002"); return; }
2026-06-23 11:27:55 +09:00
if (best.isStructure && typeof best.damage === "function") best.damage(9999, null, this);
else best.amount = 0;
2026-06-21 14:09:35 +09:00
audio.delete?.();
this.compactItems();
this.updateItemCounts();
this.rebuildSpatial();
const label = toolLabel(best.type);
this.log(`${label}\u3092\u524a\u9664\u3057\u305f\u3002`, "observe");
return;
}
const itemType = toolItemType(tool);
2026-06-23 11:27:55 +09:00
if (itemType === "signboard") {
const sign = this.signboardAt?.(x, y) || null;
if (sign) { this.editSignboard?.(sign); return; }
}
2026-06-21 14:09:35 +09:00
if (itemType) {
2026-06-23 11:27:55 +09:00
const machine = this.machineAt?.(x, y) || null;
if (machine && (typeof isMachineConsumableType === "function" ? isMachineConsumableType(itemType) : false)) {
this.setMachineSlot?.(machine, itemType);
return;
}
if (machine && machine.type === "cannon" && itemType !== "cannon" && itemType !== "duplicator") {
this.rotateCannon?.(machine, x, y);
return;
}
2026-06-21 14:09:35 +09:00
let px = x, py = y;
const rawItem = this.applyToolSize(new Item(itemType, px, py));
// Placement uses the same footprint that the preview validates.
const placed = this.placeItem(rawItem, true);
if (!placed) return;
const label = toolLabel(itemType);
2026-06-22 02:03:19 +09:00
this.log(itemType === "genkotsu" || isPinType(itemType) ? `${label}\u3092\u843d\u3068\u3057\u305f\u3002` : `${label}\u3092\u914d\u7f6e\u3057\u305f\u3002`);
2026-06-23 11:27:55 +09:00
if (itemType === "signboard") this.editSignboard?.(placed);
2026-06-21 14:09:35 +09:00
}
},
inferLogKind(text) {
const s = String(text || "").toLowerCase();
2026-06-21 22:29:00 +09:00
if (s.includes("\u914d\u7f6e") || s.includes("\u8a2d\u7f6e") || s.includes("placed") || s.includes("selected")) return "observe";
2026-06-21 14:09:35 +09:00
if (s.includes("firecracker") || s.includes("accident") || s.includes("drop") || s.includes("\u7206\u7af9") || s.includes("\u843d\u3061") || s.includes("\u4e8b\u6545") || s.includes("\u5439\u304d\u98db")) return "accident";
if (s.includes("fight") || s.includes("lost to") || s.includes("\u55a7\u5629") || s.includes("\u8ca0\u3051")) return "fight";
if (s.includes("appeared") || s.includes("wandered in") || s.includes("birth") || s.includes("\u73fe\u308c") || s.includes("\u8ff7\u3044\u8fbc")) return "birth";
if (s.includes("dead") || s.includes("trace") || s.includes("reason:") || s.includes("\u6b7b\u4ea1") || s.includes("\u75d5\u8de1") || s.includes("\u7406\u7531:") || s.includes("\u6b7b\u56e0:")) return "death";
if (s.includes("weather") || s.includes("day") || s.includes("night") || s.includes("morning") || s.includes("evening") || s.includes("\u5929\u6c17") || s.includes("\u65e5\u76ee") || s.includes("\u591c") || s.includes("\u671d") || s.includes("\u5915")) return "weather";
if (s.includes("zunda") || s.includes("food") || s.includes("eat") || s.includes("appreciated") || s.includes("\u305a\u3093\u3060") || s.includes("\u98df\u3079\u7269") || s.includes("\u5473\u308f")) return "food";
if (s.includes("grass") || s.includes("soil") || s.includes("\u8349") || s.includes("\u571f")) return "grass";
if (s.includes("selected") || s.includes("placed") || s.includes("\u9078\u629e") || s.includes("\u914d\u7f6e")) return "observe";
if (s.includes("near") || s.includes("\u8fd1\u304f") || s.includes("\u95a2\u4fc2")) return "relation";
return "note";
},
shouldHideFromObservationRecords(entry = {}) {
const type = typeof entry === "string" ? "" : (entry.eventType || "");
return type === "ball_poke" || type === "avoid_relationship";
},
playLogSoundForEntry(entry = {}) {
if (!entry || entry.hiddenFromObservation || typeof audio === "undefined") return;
const kind = entry.kind || "note";
const text = String(entry.text || "");
if (kind === "death") return audio.death?.();
if (kind === "birth") return audio.birth?.();
2026-06-21 22:29:00 +09:00
if (kind === "fight") return /\u6c7a\u7740|\u52dd|\u8ca0|\u899a\u3048/.test(text) ? audio.fightFinish?.() : audio.fight?.();
2026-06-21 14:09:35 +09:00
if (kind === "food") return audio.eat?.();
if (kind === "grass") return audio.play?.("sfx_grass", { category: "ops", minGap: 0.25 });
if (kind === "weather") return audio.phase?.();
if (kind === "relation") return audio.notify?.();
if (kind === "accident") {
2026-06-21 22:29:00 +09:00
if (/\u914d\u7f6e|\u7f6e\u3044\u305f|\u8a2d\u7f6e/.test(text)) return audio.place?.(/\u7206\u7af9/.test(text) ? "firecracker" : "");
if (/\u3052\u3093\u3053\u3064/.test(text)) return audio.genkotsuImpact?.();
if (/\u7206\u7af9|\u7206\u767a/.test(text)) return audio.explode?.();
if (/\u77f3/.test(text)) return audio.stoneImpact?.();
if (/\u30dc\u30fc\u30eb/.test(text)) return audio.ballHit?.();
2026-06-21 14:09:35 +09:00
return audio.damage?.(18);
}
if (kind === "event") {
2026-06-21 22:29:00 +09:00
if (/\u5973\u738b\u30a2\u30ea/.test(text)) return audio.queenAnt?.();
if (/\u30a2\u30ea|\u5de3/.test(text)) return audio.antNest?.();
if (/\u75c5|\u767a\u75c5/.test(text)) return audio.disease?.();
if (/\u56de\u5fa9|\u6cbb/.test(text)) return audio.heal?.();
2026-06-21 14:09:35 +09:00
}
},
log(text, kind = null, options = {}) {
const entry = {
time: this.time,
text,
kind: kind || this.inferLogKind(text),
eventType: options.eventType || "",
hiddenFromObservation: Boolean(options.hiddenFromObservation),
participants: (options.participants || []).filter(Boolean).map(t => ({
id: t.id || t.releasedLiveId || "",
liveId: t.id || t.releasedLiveId || "",
liveToken: t.liveToken || t.releasedLiveToken || 0,
familyKey: this.tarinaiFamilyKey(t),
name: t.name || "",
type: t.type || "",
generation: t.generation || 1,
})),
participantLiveIds: (options.participants || []).filter(t => t?.id).map(t => t.id),
participantSnapshots: (options.participants || []).filter(Boolean).map(t => ({
liveId: t.id || t.releasedLiveId || "",
liveToken: t.liveToken || t.releasedLiveToken || 0,
familyKey: this.tarinaiFamilyKey(t),
name: t.name || "",
type: t.type || "",
generation: t.generation || 1,
})),
};
if (this.shouldHideFromObservationRecords(entry)) entry.hiddenFromObservation = true;
const countInStats = options.countInStats ?? !entry.hiddenFromObservation;
entry.countInStats = Boolean(countInStats);
if (!this.eventCounters) this.eventCounters = {};
if (countInStats) this.eventCounters[entry.kind] = (this.eventCounters[entry.kind] || 0) + 1;
for (const t of options.participants || []) {
if (!t) continue;
if (!entry.hiddenFromObservation && t.addRecord) t.addRecord(text, entry.kind);
}
this.logs.unshift(entry);
this.emit("log:entry", { entry });
this.playLogSoundForEntry?.(entry);
this.logs = this.logs.slice(0, 100);
renderLog(this.logs);
if (typeof pushLogNotification === "function") pushLogNotification(entry);
}
}));
})(typeof window !== "undefined" ? window : globalThis);