hmm
This commit is contained in:
parent
d5c661bc22
commit
9ef1b8dbb1
23 changed files with 329 additions and 96 deletions
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.tmp-edge-profile/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Local browser smoke/probe artifacts.
|
||||
*probe*.html
|
||||
*probe*.png
|
||||
*smoke*.html
|
||||
*smoke*.png
|
||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
level=none expiry=0
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"user_experience_metrics.stability.exited_cleanly":false,"variations_crash_streak":0}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 810 B |
13
css/base.css
13
css/base.css
|
|
@ -442,7 +442,7 @@ h1 {
|
|||
}
|
||||
.signboard-editor.hidden { display: none; }
|
||||
.signboard-editor-panel {
|
||||
width: min(430px, calc(100vw - 28px));
|
||||
width: min(220px, calc(100vw - 28px));
|
||||
border-radius: 18px;
|
||||
padding: 14px;
|
||||
background: linear-gradient(180deg, rgba(255,253,247,0.98), rgba(245,237,221,0.96));
|
||||
|
|
@ -476,8 +476,11 @@ h1 {
|
|||
}
|
||||
.signboard-editor-close:hover { background: rgba(255,248,232,0.96); }
|
||||
#signboardEditorText {
|
||||
width: 100%;
|
||||
width: 7.4em;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
resize: none;
|
||||
min-height: 88px;
|
||||
max-height: 88px;
|
||||
|
|
@ -498,6 +501,8 @@ h1 {
|
|||
}
|
||||
.signboard-editor-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 8px;
|
||||
justify-content: space-between;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
|
|
@ -505,8 +510,8 @@ h1 {
|
|||
}
|
||||
.signboard-editor-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ const DECOR_ASSETS = [
|
|||
{ id: "pushpin_stuck", path: "assets/objects/pushpin_stuck.webp" },
|
||||
{ id: "oshibyo", path: "assets/objects/oshibyo.webp" },
|
||||
{ id: "oshibyo_stuck", path: "assets/objects/oshibyo_stuck.webp" },
|
||||
{ id: "plushie", path: "assets/objects/plushie.webp" },
|
||||
];
|
||||
|
||||
const TOOL_DEFINITIONS = Object.freeze({
|
||||
|
|
|
|||
|
|
@ -24,15 +24,18 @@
|
|||
const diag = w?.lastRuntimeDiagnostics || w?.runtimeDiagnostics?.() || {};
|
||||
const behaviorCounts = diag.behavior?.counts || {};
|
||||
const topBehaviors = Object.entries(behaviorCounts).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([id, n]) => `${id}:${n}`).join(", ");
|
||||
const quality = diag.performance?.quality || perf.quality || {};
|
||||
panel.textContent = [
|
||||
`tarinai v${window.TARINAI_VERSION || "?"}`,
|
||||
`fps ${perf.fps ?? window.__tarinaiFps ?? "?"} perf ${perf.level ?? "?"}`,
|
||||
`live ${diag.performance?.liveTarinai ?? quality.tarinaiCount ?? (w?.tarinai || []).length} qlevel ${quality.level ?? diag.performance?.level ?? "?"} pressure ${Number(diag.performance?.pressure ?? quality.pressure ?? 0).toFixed(2)} dpr ${diag.performance?.dpr || uiCache?.canvasDpr || "?"} maxDpr ${Number(quality.maxDpr ?? 0).toFixed ? Number(quality.maxDpr ?? 0).toFixed(2) : "?"}`,
|
||||
`items ${(w?.items || []).length} tarinai ${(w?.tarinai || []).length} ants ${(w?.ants || []).length}`,
|
||||
`effects ${(w?.effects || []).length}`,
|
||||
`terrain v${w?.terrainVersion ?? 0} spatial v${w?.spatialVersion ?? 0}`,
|
||||
`spatial rebuild ${diag.spatial?.rebuildsThisFrame ?? 0}/frame total:${diag.spatial?.rebuildsTotal ?? 0} dirty:${diag.spatial?.dirtyMarksThisFrame ?? 0} reason:${diag.spatial?.dirtyReason || diag.spatial?.lastRebuildReason || "-"}`,
|
||||
`drawList ${(w?.drawList || []).length} dirty:${Boolean(diag.render?.drawListDirty)} logs ${(w?.logs || []).length}`,
|
||||
`item buckets:${diag.items?.bucketTypes ?? 0} dirty:${Boolean(diag.items?.bucketsDirty)} id-map:${diag.items?.idMapSize ?? 0} rebuilds:${diag.items?.bucketRebuildsTotal ?? 0}`,
|
||||
`work ai ${diag.work?.stats?.aiRuns ?? 0}/${diag.work?.stats?.aiSkips ?? 0} env ${diag.work?.stats?.envRuns ?? 0}/${diag.work?.stats?.envSkips ?? 0} coll ${diag.work?.stats?.collisionRuns ?? 0}/${diag.work?.stats?.collisionSkips ?? 0}`,
|
||||
`behavior forced:${diag.behavior?.forced ?? 0} locked:${diag.behavior?.locked ?? 0} top ${topBehaviors || "-"}`,
|
||||
`dpr ${uiCache?.canvasDpr || "?"} cache ${window.TARINAI_APP?.cacheName || "?"}`,
|
||||
`sw ${navigator.serviceWorker?.controller ? "controlled" : "uncontrolled"} soundPack ${window.TarinaiAudio?.soundPackVersion || "?"}`,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ let fpsTimer = 0;
|
|||
let fpsFrames = 0;
|
||||
let perfResizeTimer = 0;
|
||||
let lastPerfLevel = 0;
|
||||
let lastPerfMaxDpr = 0;
|
||||
window.__tarinaiFps = 0;
|
||||
|
||||
const LOADING_SPRITES = [
|
||||
|
|
@ -91,11 +92,14 @@ function loop() {
|
|||
world.update(dt);
|
||||
render();
|
||||
statsTimer += dt;
|
||||
const perf = world.performanceLevel ? world.performanceLevel() : 0;
|
||||
const quality = world.performanceQuality?.() || {};
|
||||
const perf = Number(quality.level ?? (world.performanceLevel ? world.performanceLevel() : 0)) || 0;
|
||||
perfResizeTimer += rawDt;
|
||||
if (perfResizeTimer > 1.5) {
|
||||
if (perf !== lastPerfLevel) {
|
||||
const maxDpr = Number(quality.maxDpr || 0) || 0;
|
||||
if (perf !== lastPerfLevel || Math.abs(maxDpr - lastPerfMaxDpr) > 0.08) {
|
||||
lastPerfLevel = perf;
|
||||
lastPerfMaxDpr = maxDpr;
|
||||
resizeCanvas();
|
||||
}
|
||||
perfResizeTimer = 0;
|
||||
|
|
@ -139,4 +143,3 @@ if ("serviceWorker" in navigator) {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ class TarinaiPerformanceManager {
|
|||
this.lastLevel = 0;
|
||||
this.samples = [];
|
||||
this.sampleLimit = 20;
|
||||
this.pressure = 0;
|
||||
}
|
||||
|
||||
updateFps(fps, worldRef = null) {
|
||||
|
|
@ -16,7 +17,8 @@ class TarinaiPerformanceManager {
|
|||
if (this.samples.length > this.sampleLimit) this.samples.shift();
|
||||
const avg = this.samples.reduce((a, b) => a + b, 0) / this.samples.length;
|
||||
this.fps = Math.round(avg);
|
||||
const next = this.computeLevel(avg);
|
||||
this.pressure = this.computePressure(avg, worldRef);
|
||||
const next = this.computeLevel(avg, worldRef);
|
||||
if (next !== this.level) {
|
||||
this.lastLevel = this.level;
|
||||
this.level = next;
|
||||
|
|
@ -25,33 +27,68 @@ class TarinaiPerformanceManager {
|
|||
return this.level;
|
||||
}
|
||||
|
||||
computeLevel(fps = this.fps) {
|
||||
if (!fps || !Number.isFinite(fps)) return 0;
|
||||
if (fps < 22) return 3;
|
||||
if (fps < 34) return 2;
|
||||
if (fps < 48) return 1;
|
||||
computeLevel(fps = this.fps, worldRef = null) {
|
||||
const pressure = this.computePressure(fps, worldRef);
|
||||
if (pressure >= 0.78) return 3;
|
||||
if (pressure >= 0.54) return 2;
|
||||
if (pressure >= 0.30) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
quality(worldRef = null) {
|
||||
const level = this.level;
|
||||
computePressure(fps = this.fps, worldRef = null) {
|
||||
const q = this.countWorkload(worldRef);
|
||||
const n = Number(fps);
|
||||
const fpsPressure = Number.isFinite(n) && n > 0 ? clamp((58 - n) / 38, 0, 1) : 0;
|
||||
const workPressure = clamp(Math.log1p(q.workUnits / 42) / Math.log1p(260 / 42), 0, 1);
|
||||
const sampleBoost = this.samples.length < 4 ? 0.84 : 1.0;
|
||||
return clamp(Math.max(fpsPressure, workPressure * 0.78) * sampleBoost, 0, 1);
|
||||
}
|
||||
|
||||
countWorkload(worldRef = null) {
|
||||
let tarinaiCount = 0;
|
||||
for (const t of worldRef?.tarinai || []) if (t && !t.dead) tarinaiCount += 1;
|
||||
let antCount = 0;
|
||||
for (const ant of worldRef?.ants || []) if (ant && !ant.dead) antCount += 1;
|
||||
const itemCount = (worldRef?.items || []).length || 0;
|
||||
const effectCount = (worldRef?.effects || []).length || 0;
|
||||
const antStride = antCount >= 95 || level >= 3 ? 3 : (antCount >= 35 || level >= 2 ? 2 : 1);
|
||||
const workUnits = tarinaiCount + antCount * 0.35 + itemCount * 0.08 + effectCount * 0.12;
|
||||
return { tarinaiCount, antCount, itemCount, effectCount, workUnits };
|
||||
}
|
||||
|
||||
quality(worldRef = null) {
|
||||
const counts = this.countWorkload(worldRef);
|
||||
const pressure = this.computePressure(this.fps, worldRef);
|
||||
const level = Math.max(this.level, this.computeLevel(this.fps, worldRef));
|
||||
const density = clamp(1 - pressure * 0.82, 0.18, 1);
|
||||
const antStride = Math.max(1, Math.round(1 + pressure * 2.4));
|
||||
const aiIntervalScale = 1 + pressure * 5.2;
|
||||
const envIntervalScale = 1 + pressure * 4.8;
|
||||
const socialScanLimit = Math.max(3, Math.round(14 - pressure * 10));
|
||||
const itemScanLimit = Math.max(5, Math.round(28 - pressure * 18));
|
||||
const collisionInterval = 0.04 + pressure * 0.24;
|
||||
return {
|
||||
fps: this.fps,
|
||||
pressure,
|
||||
level,
|
||||
effectDensity: density,
|
||||
tarinaiCount: counts.tarinaiCount,
|
||||
antStride,
|
||||
itemCount,
|
||||
antCount,
|
||||
effectCount,
|
||||
effectLimit: level >= 3 ? 34 : level >= 2 ? 52 : level >= 1 ? 68 : 88,
|
||||
statsInterval: level >= 3 ? 1.20 : level === 2 ? 0.92 : level === 1 ? 0.68 : 0.48,
|
||||
terrainRedrawInterval: level >= 3 ? 1.30 : level === 2 ? 1.05 : level === 1 ? 0.82 : 0.62,
|
||||
backgroundLightStep: level >= 3 ? 64 : level === 2 ? 96 : 180,
|
||||
uiUpdateInterval: level >= 3 ? 1.2 : level === 2 ? 0.9 : level === 1 ? 0.65 : 0.45,
|
||||
itemCount: counts.itemCount,
|
||||
antCount: counts.antCount,
|
||||
effectCount: counts.effectCount,
|
||||
workUnits: counts.workUnits,
|
||||
maxDpr: clamp(2.0 - pressure * 1.35, 0.65, 2.0),
|
||||
effectLimit: Math.round(30 + density * 58),
|
||||
aiIntervalScale,
|
||||
envIntervalScale,
|
||||
socialScanLimit,
|
||||
itemScanLimit,
|
||||
collisionInterval,
|
||||
bedConflictInterval: 0.12 + pressure * 0.72,
|
||||
statsInterval: 0.42 + pressure * 0.86,
|
||||
terrainRedrawInterval: 0.62 + pressure * 0.72,
|
||||
backgroundLightStep: Math.round(180 - pressure * 116),
|
||||
uiUpdateInterval: 0.45 + pressure * 0.78,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
42
js/render.js
42
js/render.js
|
|
@ -704,8 +704,9 @@ function drawAtmosphericLighting(ctx, w, h, lighting) {
|
|||
function drawRain(ctx, w, h, t) {
|
||||
if (world.weather !== "light_rain") return;
|
||||
ctx.save();
|
||||
const perf = world.performanceLevel ? world.performanceLevel() : 0;
|
||||
const count = perf >= 2 ? 64 : 138;
|
||||
const quality = world.performanceQuality?.() || {};
|
||||
const density = clamp(Number(quality.effectDensity || 1) || 1, 0.12, 1);
|
||||
const count = Math.max(20, Math.round(138 * density));
|
||||
const angle = -0.32; // Rain visual effect: diagonal streaks, separate from fallen water item sprites.
|
||||
const slant = Math.tan(angle);
|
||||
for (let i = 0; i < count; i++) {
|
||||
|
|
@ -729,7 +730,7 @@ function drawRain(ctx, w, h, t) {
|
|||
ctx.moveTo(x - dx * 0.5, y - dy * 0.5);
|
||||
ctx.lineTo(x + dx * 0.5, y + dy * 0.5);
|
||||
ctx.stroke();
|
||||
if (perf < 3 && i % 4 === 0) {
|
||||
if (density > 0.25 && i % 4 === 0) {
|
||||
ctx.globalAlpha = 0.13 * alpha;
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.70)";
|
||||
ctx.lineWidth = 0.7;
|
||||
|
|
@ -795,8 +796,9 @@ function render() {
|
|||
}
|
||||
ctx.restore();
|
||||
|
||||
const perf = world.performanceLevel ? world.performanceLevel() : 0;
|
||||
if (perf < 2) drawLightRays(ctx, screenW, screenH, lighting);
|
||||
const quality = world.performanceQuality?.() || {};
|
||||
const density = clamp(Number(quality.effectDensity || 1) || 1, 0.12, 1);
|
||||
if (density > 0.45) drawLightRays(ctx, screenW, screenH, lighting);
|
||||
|
||||
const visibleRect = visibleWorldRect(world, 180);
|
||||
const terrainLayer = ensureTerrainCache(world, lighting);
|
||||
|
|
@ -812,21 +814,23 @@ function render() {
|
|||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.lineWidth = 1.15;
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.strokeStyle = light > 0.42 ? "rgba(115, 151, 86, 0.32)" : "rgba(202, 220, 255, 0.22)";
|
||||
for (const t of world.tarinai) {
|
||||
if (!isEntityVisibleInRect(t, visibleRect, 80)) continue;
|
||||
const p = t.parentToFollow?.();
|
||||
if (!p || !t.juvenile || t.dead || p.dead || dist(t, p) > 96 || !isEntityVisibleInRect(p, visibleRect, 80)) continue;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(t.x, t.y);
|
||||
ctx.quadraticCurveTo((t.x + p.x) * 0.5, Math.min(t.y, p.y) - 10, p.x, p.y);
|
||||
ctx.stroke();
|
||||
if (density > 0.45) {
|
||||
ctx.save();
|
||||
ctx.lineWidth = 1.15;
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.strokeStyle = light > 0.42 ? "rgba(115, 151, 86, 0.32)" : "rgba(202, 220, 255, 0.22)";
|
||||
for (const t of world.tarinai) {
|
||||
if (!isEntityVisibleInRect(t, visibleRect, 80)) continue;
|
||||
const p = t.parentToFollow?.();
|
||||
if (!p || !t.juvenile || t.dead || p.dead || dist(t, p) > 96 || !isEntityVisibleInRect(p, visibleRect, 80)) continue;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(t.x, t.y);
|
||||
ctx.quadraticCurveTo((t.x + p.x) * 0.5, Math.min(t.y, p.y) - 10, p.x, p.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.setLineDash([]);
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.setLineDash([]);
|
||||
ctx.restore();
|
||||
|
||||
for (const entry of renderStack.layered) {
|
||||
if (isEntityVisibleInRect(entry.entity, visibleRect)) entry.entity.draw(ctx, world.time, lighting);
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@
|
|||
|
||||
function itemExtra(item, tarinaiIndex = new Map()) {
|
||||
const type = item?.type || "";
|
||||
if (item?.isStructure) return ["S", q(item.hp, 10), q(item.maxHp, 10), q(item.attachment, 10), q(item.usedCount, 1), tarinaiIndex.get(item.ownerId) ?? -1, item.carriedById ? (tarinaiIndex.get(item.carriedById) ?? -1) : -1, item.onHead ? 1 : 0];
|
||||
if (item?.isStructure) return ["S", q(item.hp, 10), q(item.maxHp, 10), q(item.attachment, 10), q(item.usedCount, 1), tarinaiIndex.get(item.ownerId) ?? -1, item.carriedById ? (tarinaiIndex.get(item.carriedById) ?? -1) : -1, item.onHead ? 1 : 0, item.plushieSpriteId || ""];
|
||||
if (type === "grass") return [q(item.growth, 1000), q(item.health, 1000), q(item.seedTimer, 10), q(item.eatenAmount, 10), q(item.fertilityBoost, 1000), q(item.lifeSpan, 10), q(item.wither, 1000)];
|
||||
if (type === "bed") return [q(item.comfort, 1000), q(item.wear, 1000)];
|
||||
if (type === "signboard") return [item.text || ""];
|
||||
|
|
@ -296,6 +296,7 @@
|
|||
item.ownerId = tarinaiList[extra[5]]?.id || "";
|
||||
item.carriedById = tarinaiList[extra[6]]?.id || "";
|
||||
item.onHead = !!extra[7];
|
||||
item.plushieSpriteId = extra[8] || item.plushieSpriteId || "";
|
||||
const def = global.StructureRegistry?.get?.(type);
|
||||
if (def) {
|
||||
item.label = def.label;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@
|
|||
this.needEffects = clonePlain(definition.needEffects);
|
||||
this.carriedById = definition.type === "plushie" ? this.ownerId : "";
|
||||
this.onHead = definition.type === "plushie";
|
||||
if (definition.type === "plushie") {
|
||||
const sprites = typeof spawnableSprites === "function" ? spawnableSprites().map(s => s.id).filter(Boolean) : [];
|
||||
this.plushieSpriteId = sprites.length ? sprites[Math.floor(Math.random() * sprites.length)] : (owner?.type || "smile");
|
||||
}
|
||||
this.dead = false;
|
||||
this.world = world || null;
|
||||
}
|
||||
|
|
@ -106,10 +110,11 @@
|
|||
ctx.ellipse(0, this.r * 0.22, this.r * 1.28, this.r * 0.28, -0.06, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
} else if (this.type === "plushie") {
|
||||
const img = typeof getRenderableImage === "function" ? getRenderableImage("plushie", "plushie") : global.images?.get?.("plushie");
|
||||
const spriteId = this.plushieSpriteId || "smile";
|
||||
const img = typeof getRenderableImage === "function" ? getRenderableImage(spriteId, "smile") : (global.images?.get?.(spriteId) || global.images?.get?.("smile"));
|
||||
ctx.rotate(Math.sin(time * 2 + this.seed) * 0.05);
|
||||
if (img) {
|
||||
const metrics = typeof getImageMetrics === "function" ? getImageMetrics("plushie") : null;
|
||||
const metrics = typeof getImageMetrics === "function" ? getImageMetrics(spriteId) || getImageMetrics("smile") : null;
|
||||
const w = this.r * 3.0;
|
||||
const h = w * (metrics?.ratio || 0.74);
|
||||
ctx.drawImage(img, -w * 0.58, -h * 0.60, w, h);
|
||||
|
|
|
|||
|
|
@ -166,14 +166,14 @@
|
|||
rel.fightsWon = rel.fightsWon || 0;
|
||||
rel.fightsLost = rel.fightsLost || 0;
|
||||
if (event) { rel.lastEvent = event; rel.lastTime = this.world.time; }
|
||||
this.relationCache = null;
|
||||
},
|
||||
|
||||
strongestRelation(kind = "friend", liveOnly = true) {
|
||||
let bestId = null;
|
||||
let bestScore = kind === "fear" ? 5 : FRIEND_AFFINITY_THRESHOLD;
|
||||
const liveIds = liveOnly ? new Set((this.world?.tarinai || []).filter(o => o && !o.dead && o !== this).map(o => o.id).filter(Boolean)) : null;
|
||||
for (const [id, rel] of Object.entries(this.relationships || {})) {
|
||||
if (liveIds && !liveIds.has(id)) continue;
|
||||
if (liveOnly && (!this.world?.liveTarinaiById?.(id) || id === this.id)) continue;
|
||||
const score = kind === "fear" ? (rel.fear || 0) : (rel.affinity || 0);
|
||||
if (score > bestScore) { bestId = id; bestScore = score; }
|
||||
}
|
||||
|
|
@ -187,13 +187,16 @@
|
|||
},
|
||||
|
||||
currentFriendCount(minScore = FRIEND_AFFINITY_THRESHOLD, liveOnly = true) {
|
||||
const now = this.world?.time || 0;
|
||||
const cacheKey = `${minScore}:${liveOnly ? 1 : 0}`;
|
||||
if (this.relationCache?.friendCountKey === cacheKey && now < (this.relationCache.friendCountUntil || 0)) return this.relationCache.friendCount;
|
||||
let count = 0;
|
||||
const liveIds = liveOnly ? new Set((this.world?.tarinai || []).filter(o => o && !o.dead && o !== this).map(o => o.id)) : null;
|
||||
for (const [id, rel] of Object.entries(this.relationships || {})) {
|
||||
if ((rel.affinity || 0) <= minScore) continue;
|
||||
if (liveIds && !liveIds.has(id)) continue;
|
||||
if (liveOnly && (!this.world?.liveTarinaiById?.(id) || id === this.id)) continue;
|
||||
count += 1;
|
||||
}
|
||||
this.relationCache = { ...(this.relationCache || {}), friendCountKey: cacheKey, friendCount: count, friendCountUntil: now + 1.4 };
|
||||
return count;
|
||||
},
|
||||
|
||||
|
|
@ -203,16 +206,23 @@
|
|||
},
|
||||
|
||||
bestFriendLive(minScore = FRIEND_AFFINITY_THRESHOLD, maxDist = Infinity) {
|
||||
const now = this.world?.time || 0;
|
||||
const cacheKey = `${minScore}:${Number.isFinite(maxDist) ? Math.round(maxDist) : "inf"}`;
|
||||
if (this.relationCache?.bestFriendKey === cacheKey && now < (this.relationCache.bestFriendUntil || 0)) {
|
||||
const cached = this.world?.liveTarinaiById?.(this.relationCache.bestFriendId);
|
||||
if (cached && (!Number.isFinite(maxDist) || dist(this, cached) <= maxDist)) return cached;
|
||||
}
|
||||
let best = null, bestScore = minScore;
|
||||
for (const [id, rel] of Object.entries(this.relationships || {})) {
|
||||
const score = rel.affinity || 0;
|
||||
if (score <= bestScore) continue;
|
||||
const t = this.world.tarinai.find(o => o.id === id && !o.dead);
|
||||
const t = this.world.liveTarinaiById?.(id) || null;
|
||||
if (!t) continue;
|
||||
if (Number.isFinite(maxDist) && dist(this, t) > maxDist) continue;
|
||||
best = t;
|
||||
bestScore = score;
|
||||
}
|
||||
this.relationCache = { ...(this.relationCache || {}), bestFriendKey: cacheKey, bestFriendId: best?.id || "", bestFriendUntil: now + 1.0 };
|
||||
return best;
|
||||
},
|
||||
|
||||
|
|
@ -223,7 +233,7 @@
|
|||
const when = rel.lastTime || -Infinity;
|
||||
if (this.world.time - when > maxAge) continue;
|
||||
if (!(event.includes("fight") || event.includes("\u55a7\u5629") || (rel.fightsWon || 0) || (rel.fightsLost || 0))) continue;
|
||||
const t = this.world.tarinai.find(o => o.id === id && !o.dead);
|
||||
const t = this.world.liveTarinaiById?.(id) || null;
|
||||
if (!t) continue;
|
||||
if (Number.isFinite(maxDist) && dist(this, t) > maxDist) continue;
|
||||
if (when > bestT) { best = t; bestT = when; }
|
||||
|
|
|
|||
|
|
@ -122,13 +122,26 @@ function needPressure(value) {
|
|||
}
|
||||
|
||||
function calculateStressFromNeeds(needs) {
|
||||
return clamp(
|
||||
const weightedLinear =
|
||||
(Number(needs.food) || 0) * 0.09 +
|
||||
(Number(needs.sleep) || 0) * 0.11 +
|
||||
(Number(needs.health) || 0) * 0.22 +
|
||||
(Number(needs.safety) || 0) * 0.26 +
|
||||
(Number(needs.social) || 0) * 0.07 +
|
||||
(Number(needs.fulfill) || 0) * 0.13;
|
||||
const peakNeed = Math.max(0, ...TARINAI_NEED_KEYS.map(key => Number(needs?.[key] || 0) || 0));
|
||||
const peakPressure = Math.max(0, peakNeed - 62) * 0.10;
|
||||
const quadraticPressure =
|
||||
needPressure(needs.food) * 0.10 +
|
||||
needPressure(needs.sleep) * 0.12 +
|
||||
needPressure(needs.health) * 0.25 +
|
||||
needPressure(needs.safety) * 0.30 +
|
||||
needPressure(needs.social) * 0.08 +
|
||||
needPressure(needs.fulfill) * 0.15,
|
||||
needPressure(needs.fulfill) * 0.15;
|
||||
return clamp(
|
||||
weightedLinear +
|
||||
peakPressure +
|
||||
quadraticPressure * 0.35,
|
||||
0,
|
||||
130
|
||||
);
|
||||
|
|
@ -1392,7 +1405,8 @@ function createSleepBuildGrassBedActionSpec() {
|
|||
timerBacked: true,
|
||||
selectTarget: selectRoleTarget("grassMaterial", 520),
|
||||
canStart(t, world, ctx = {}) {
|
||||
return !findNearestSleepPlace(world, t, 760)
|
||||
return canBuildOwnedStructureByAge(t)
|
||||
&& !findNearestSleepPlace(world, t, 760)
|
||||
&& !findOwnedStructure(world, t, "grass_bed")
|
||||
&& !!(ctx.target ?? this.selectTarget(t, world, ctx))
|
||||
&& !["fight", "panic", "intimidate", "birth_ritual"].includes(t.state);
|
||||
|
|
@ -1780,7 +1794,7 @@ const TARINAI_ACTION_DEFINITIONS = [
|
|||
phrase: "かんたんベッドを作った",
|
||||
weight: 34,
|
||||
selectTarget: selectRoleTarget("grassMaterial", 520),
|
||||
canStart(t, world, ctx = {}) { return !findOwnedStructure(world, t, "grass_bed") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); },
|
||||
canStart(t, world, ctx = {}) { return canBuildOwnedStructureByAge(t) && !findOwnedStructure(world, t, "grass_bed") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); },
|
||||
start(t, world) { return buildOwnedStructureFromGrass(t, world, "grass_bed", this.label); },
|
||||
},
|
||||
{
|
||||
|
|
@ -1791,7 +1805,7 @@ const TARINAI_ACTION_DEFINITIONS = [
|
|||
phrase: "ぬいぐるみを作った",
|
||||
weight: 28,
|
||||
selectTarget: selectRoleTarget("grassMaterial", 520),
|
||||
canStart(t, world, ctx = {}) { return !findOwnedStructure(world, t, "plushie") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); },
|
||||
canStart(t, world, ctx = {}) { return canBuildOwnedStructureByAge(t) && !findOwnedStructure(world, t, "plushie") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); },
|
||||
start(t, world) { return buildOwnedStructureFromGrass(t, world, "plushie", this.label); },
|
||||
},
|
||||
{
|
||||
|
|
@ -2065,6 +2079,32 @@ function structureBuildProbe(type, x, y) {
|
|||
return { type, x, y, r, dead: false, isStructure: true, roles: def.roles || {}, ownerId: "" };
|
||||
}
|
||||
|
||||
function canBuildOwnedStructureByAge(tarinai) {
|
||||
if (!tarinai || tarinai.dead) return false;
|
||||
if (tarinai.generation <= 1) return true;
|
||||
const growth = Number.isFinite(tarinai.growth)
|
||||
? tarinai.growth
|
||||
: clamp(((tarinai.world?.time || 0) - (tarinai.birthTime || 0)) / Math.max(1, CONFIG.childGrowTime || 58), 0, 1);
|
||||
return growth >= 0.55;
|
||||
}
|
||||
|
||||
function stopFailedBuildPlan(t, world, type = "", reason = "") {
|
||||
if (!t) return false;
|
||||
t.buildPlan = null;
|
||||
t.actionCooldowns = t.actionCooldowns || {};
|
||||
t.actionIdCooldowns = t.actionIdCooldowns || {};
|
||||
const actionId = type === "grass_bed" ? "build_grass_bed" : (type === "plushie" ? "build_plushie" : "build");
|
||||
t.actionCooldowns.fulfill = Math.max(t.actionCooldowns.fulfill || 0, 5.0);
|
||||
t.actionCooldowns.sleep = Math.max(t.actionCooldowns.sleep || 0, type === "grass_bed" ? 3.8 : 0);
|
||||
t.actionIdCooldowns[actionId] = Math.max(t.actionIdCooldowns[actionId] || 0, 9.0);
|
||||
if (type === "grass_bed") t.actionIdCooldowns.sleep_build_grass_bed = Math.max(t.actionIdCooldowns.sleep_build_grass_bed || 0, 9.0);
|
||||
t.behaviorSwitchCooldownUntil = Math.max(t.behaviorSwitchCooldownUntil || 0, (world?.time || 0) + 1.2);
|
||||
const text = reason || "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044";
|
||||
if (typeof clearActiveBehavior === "function") clearActiveBehavior(t, text);
|
||||
t.goIdle?.(text);
|
||||
return false;
|
||||
}
|
||||
|
||||
function findStructureBuildSpot(t, world, type) {
|
||||
const baseX = Number.isFinite(t?.x) ? t.x : 0;
|
||||
const baseY = Number.isFinite(t?.y) ? t.y : 0;
|
||||
|
|
@ -2117,8 +2157,7 @@ function continueBuildPlan(t, world, dt) {
|
|||
if (material && (material.dead || !roleMatches(material, "grassMaterial"))) material = null;
|
||||
if (!material) material = findNearbyMaterial(world, t, "grassMaterial", 520);
|
||||
if (!material) {
|
||||
t.buildPlan = null;
|
||||
return false;
|
||||
return stopFailedBuildPlan(t, world, plan.type, "\u6750\u6599\u304c\u898b\u3064\u304b\u3089\u306a\u3044");
|
||||
}
|
||||
plan.materialId = material.id;
|
||||
const label = globalThis.StructureRegistry.get(plan.type)?.label || "\u69cb\u9020\u7269";
|
||||
|
|
@ -2133,8 +2172,7 @@ function continueBuildPlan(t, world, dt) {
|
|||
const spot = findStructureBuildSpot(t, world, plan.type);
|
||||
const structure = globalThis.StructureRegistry.create(plan.type, t, spot.x, spot.y, world);
|
||||
if (!structure) {
|
||||
t.buildPlan = null;
|
||||
return false;
|
||||
return stopFailedBuildPlan(t, world, plan.type, "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044");
|
||||
}
|
||||
world.addItem?.(structure, `build:${plan.type}`) || world.items.push(structure);
|
||||
world.drawListDirty = true;
|
||||
|
|
@ -2859,9 +2897,12 @@ function protectSleepSession(tarinai, world, needs) {
|
|||
this.intentLockTimer = Math.max(0, (this.intentLockTimer || 0) - dt);
|
||||
this.aiTimer += dt;
|
||||
const urgentAi = this.fightTimer > 0.04 || this.intimidateTimer > 0.04 || this.intimidatedTimer > 0.04 || this.birthRitualTimer > 0.04 || this.defeatedTimer > 0.04 || this.eatTimer > 0.02 || this.hurtTimer > 0.02;
|
||||
const popPerf = this.world.performanceLevel ? this.world.performanceLevel() : 0;
|
||||
const aiInterval = urgentAi ? (popPerf >= 2 ? 0.105 : 0.075) : (0.18 + stableUnit(this.familyKey || this.id, "ai-interval") * 0.09 + popPerf * 0.055);
|
||||
if (this.aiTimer >= aiInterval) {
|
||||
const quality = this.world.performanceQuality ? this.world.performanceQuality() : {};
|
||||
const pressure = clamp(Number(quality.pressure || 0) || 0, 0, 1);
|
||||
const intervalScale = Number(quality.aiIntervalScale || 1) || 1;
|
||||
const phaseOffset = stableUnit(this.familyKey || this.id, "ai-phase") * 0.12 * intervalScale;
|
||||
const aiInterval = urgentAi ? (0.065 + pressure * 0.045) : ((0.18 + stableUnit(this.familyKey || this.id, "ai-interval") * 0.09) * intervalScale + phaseOffset);
|
||||
if (this.aiTimer >= aiInterval && this.world.spendScheduledWork?.("ai", urgentAi) !== false) {
|
||||
const aiDt = Math.min(this.aiTimer, 0.48);
|
||||
this.aiTimer = 0;
|
||||
this.resolveNeeds(aiDt);
|
||||
|
|
@ -2878,8 +2919,8 @@ function protectSleepSession(tarinai, world, needs) {
|
|||
this.updateFacing();
|
||||
}
|
||||
this.envTimer += dt;
|
||||
const envInterval = (this.world.performanceLevel && this.world.performanceLevel() >= 2) ? 0.82 : 0.48;
|
||||
if (this.envTimer >= envInterval) {
|
||||
const envInterval = (0.48 + stableUnit(this.familyKey || this.id, "env-phase") * 0.18) * (Number(quality.envIntervalScale || 1) || 1);
|
||||
if (this.envTimer >= envInterval && this.world.spendScheduledWork?.("env", urgentAi || this.world.selected === this) !== false) {
|
||||
const envDt = Math.min(this.envTimer, 1.2);
|
||||
this.envTimer = 0;
|
||||
this.applyEnvironment(envDt);
|
||||
|
|
@ -3210,9 +3251,19 @@ function protectSleepSession(tarinai, world, needs) {
|
|||
const canBite = !this.isZunchiSlave && this.eatCooldown <= 0 && mealReady && wantsFood && this.hunger > 18;
|
||||
const canSweetBite = !this.isZunchiSlave && this.eatCooldown <= 0 && mealReady && (wantsFood || wantsMedicine) && this.hunger > 7;
|
||||
const canZunchiBite = this.eatCooldown <= 0 && (this.isZunchiSlave ? this.hunger > 16 : (this.hunger > 88 || foodNeed > 82));
|
||||
const foodCandidates = this.world.nearbyItems(this.x, this.y, this.radius + 46).slice();
|
||||
if (this.target && !this.target.dead && !foodCandidates.includes(this.target) && (this.target.type === "duplicator" || roleMatches(this.target, "food") || roleMatches(this.target, "medicine"))) foodCandidates.unshift(this.target);
|
||||
foodCandidates.sort((a, b) => foodPriorityScore(this, b, b?.type === "duplicator" ? foodInteractionDistance(this, b) : dist(this, b)) - foodPriorityScore(this, a, a?.type === "duplicator" ? foodInteractionDistance(this, a) : dist(this, a)));
|
||||
const quality = this.world?.performanceQuality?.() || {};
|
||||
const scanLimit = Math.max(3, Number(quality.itemScanLimit || 28) || 28);
|
||||
const nearbyItems = this.world.nearbyItems(this.x, this.y, this.radius + 46);
|
||||
const foodCandidates = [];
|
||||
if (this.target && !this.target.dead && (this.target.type === "duplicator" || roleMatches(this.target, "food") || roleMatches(this.target, "medicine"))) foodCandidates.push(this.target);
|
||||
for (const it of nearbyItems) {
|
||||
if (!it || it.dead || foodCandidates.includes(it)) continue;
|
||||
foodCandidates.push(it);
|
||||
if (foodCandidates.length >= scanLimit) break;
|
||||
}
|
||||
if (foodCandidates.length > 1 && foodCandidates.length <= scanLimit) {
|
||||
foodCandidates.sort((a, b) => foodPriorityScore(this, b, b?.type === "duplicator" ? foodInteractionDistance(this, b) : dist(this, b)) - foodPriorityScore(this, a, a?.type === "duplicator" ? foodInteractionDistance(this, a) : dist(this, a)));
|
||||
}
|
||||
for (const it of foodCandidates) {
|
||||
const d = it?.type === "duplicator" ? foodInteractionDistance(this, it) : dist(this, it);
|
||||
const contactReach = foodInteractionReach(this, it, "food") + (it === this.target && foodActionActive && it?.type !== "duplicator" ? 4 : 0);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
draw(ctx, t, lighting = null) {
|
||||
if (this.dead) return;
|
||||
const lightState = lighting || getLightingState(this.world);
|
||||
const quality = this.world.performanceQuality?.() || {};
|
||||
const effectDensity = clamp(Number(quality.effectDensity || 1) || 1, 0.12, 1);
|
||||
const showDecor = effectDensity > 0.45 || this.world.selected === this;
|
||||
const sprite = this.spriteId();
|
||||
const img = typeof getRenderableImage === "function" ? getRenderableImage(sprite, "smile") : (images.get(sprite) || images.get(this.type));
|
||||
const metrics = getImageMetrics(sprite) || getImageMetrics(this.type) || getImageMetrics("smile");
|
||||
|
|
@ -63,18 +66,18 @@
|
|||
|
||||
const shadow = projectedShadowParams(lightState, 0.95 + visualScale * 0.8);
|
||||
const shadowContactY = this.y + bob + (drawH / squish) * 0.30;
|
||||
if (img) {
|
||||
if (showDecor && img) {
|
||||
drawImageProjectedShadow(ctx, img, this.x, shadowContactY, drawW * squish, drawH / squish, shadow, {
|
||||
faceRight,
|
||||
alpha: shadow.alpha * (this.state === "sleep" ? 1.10 : 0.92) * nestHideAlpha,
|
||||
widthScale: this.state === "sleep" ? 1.12 : 1,
|
||||
heightScale: this.state === "sleep" ? 0.88 : 1,
|
||||
});
|
||||
} else {
|
||||
} else if (showDecor) {
|
||||
drawProjectedShadow(ctx, this.x, shadowContactY, drawW * 0.52, drawH * 0.13, { ...shadow, alpha: shadow.alpha * 0.82 * nestHideAlpha });
|
||||
}
|
||||
drawCreatureGlow(ctx, this, drawW, drawH, lightState);
|
||||
if (this.powerItemMode === "protein" || this.powerItemMode === "niteropu") {
|
||||
if (showDecor) drawCreatureGlow(ctx, this, drawW, drawH, lightState);
|
||||
if (showDecor && (this.powerItemMode === "protein" || this.powerItemMode === "niteropu")) {
|
||||
ctx.save();
|
||||
const up = this.powerItemMode === "protein";
|
||||
const baseColor = up ? "rgba(224,54,40,0.92)" : "rgba(116,80,190,0.88)";
|
||||
|
|
@ -194,7 +197,7 @@
|
|||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(focusLabel, 0, ly);
|
||||
ctx.restore();
|
||||
} else if ((this.world.performanceLevel ? this.world.performanceLevel() : 0) < 2 && this.world.pointer?.inside && distXY(this.x, this.y, this.world.pointer.x, this.world.pointer.y) < this.radius * 2.0) {
|
||||
} else if (showDecor && this.world.pointer?.inside && distXY(this.x, this.y, this.world.pointer.x, this.world.pointer.y) < this.radius * 2.0) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.28;
|
||||
ctx.strokeStyle = this.state === "cursor_enemy" ? "rgba(180, 92, 92, 0.70)" : "rgba(255, 250, 220, 0.78)";
|
||||
|
|
@ -231,7 +234,7 @@
|
|||
ctx.restore();
|
||||
}
|
||||
|
||||
if ((this.world.performanceLevel ? this.world.performanceLevel() : 0) < 2 && (this.goodMode === "smile" || this.state === "cursor_friend") && this.mood > 58 && this.stress < 44 && this.state !== "sleep") {
|
||||
if (showDecor && (this.goodMode === "smile" || this.state === "cursor_friend") && this.mood > 58 && this.stress < 44 && this.state !== "sleep") {
|
||||
ctx.save();
|
||||
const cursorLove = this.state === "cursor_friend" ? Math.max(0, this.cursorPetting || 0) : 0;
|
||||
ctx.globalAlpha = this.state === "cursor_friend" ? clamp(0.82 + cursorLove * 0.16, 0.82, 1) : 0.34;
|
||||
|
|
@ -312,8 +315,10 @@
|
|||
ctx.font = `${starSize}px "Apple Color Emoji", "Segoe UI Emoji", sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.shadowColor = "rgba(255, 198, 28, 0.82)";
|
||||
ctx.shadowBlur = 14;
|
||||
if (showDecor) {
|
||||
ctx.shadowColor = "rgba(255, 198, 28, 0.82)";
|
||||
ctx.shadowBlur = 14;
|
||||
}
|
||||
ctx.lineWidth = Math.max(2, starSize * 0.12);
|
||||
ctx.strokeStyle = "rgba(92, 62, 0, 0.86)";
|
||||
ctx.fillStyle = "rgba(255, 218, 46, 1)";
|
||||
|
|
@ -324,7 +329,7 @@
|
|||
|
||||
const hpActive = (this.hpBarTimer || 0) > 0;
|
||||
const stressActive = (this.stressBarTimer || 0) > 0;
|
||||
if (hpActive || stressActive) {
|
||||
if ((hpActive || stressActive) && (showDecor || this.world.selected === this || this.hurtTimer > 0.04)) {
|
||||
const bw = Math.max(28, this.radius * 2.35);
|
||||
const bh = 5;
|
||||
const x = this.x - bw / 2;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,13 @@
|
|||
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
|
||||
interactWithOthers(dt) {
|
||||
let closeCount = 0;
|
||||
const quality = this.world?.performanceQuality?.() || {};
|
||||
const scanLimit = Math.max(1, Number(quality.socialScanLimit || 14) || 14);
|
||||
let scanned = 0;
|
||||
for (const o of this.world.nearbyTarinai(this.x, this.y, 78)) {
|
||||
if (o === this || o.dead) continue;
|
||||
if (scanned >= scanLimit && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04) break;
|
||||
scanned += 1;
|
||||
const dx = o.x - this.x, dy = o.y - this.y;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d < 0.01 || d > 74) continue;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
function resizeCanvas() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const perf = world?.performanceLevel ? world.performanceLevel() : 0;
|
||||
const maxDpr = perf >= 2 ? 1 : (perf === 1 ? 1.5 : 2);
|
||||
const dpr = Math.max(1, Math.min(window.devicePixelRatio || 1, maxDpr));
|
||||
const quality = world?.performanceQuality?.() || {};
|
||||
const perf = Number(quality.level ?? (world?.performanceLevel ? world.performanceLevel() : 0)) || 0;
|
||||
const maxDpr = Number(quality.maxDpr || 0) || (perf >= 2 ? 1 : (perf === 1 ? 1.25 : 2));
|
||||
const dpr = Math.max(0.6, Math.min(window.devicePixelRatio || 1, maxDpr));
|
||||
const nextW = Math.max(1, Math.floor(rect.width * dpr));
|
||||
const nextH = Math.max(1, Math.floor(rect.height * dpr));
|
||||
if (canvas.width === nextW && canvas.height === nextH && uiCache.canvasDpr === dpr) return;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
return chars.slice(0, Math.max(0, limit)).join("");
|
||||
}
|
||||
|
||||
function sanitizeSignboardText(value = "") {
|
||||
function sanitizeSignboardText(value = "", opts = {}) {
|
||||
const normalized = String(value || "").replace(/\r/g, "").replace(/[\t ]+/g, " ");
|
||||
const rawLines = normalized.split("\n").slice(0, SIGNBOARD_LINES);
|
||||
const out = [];
|
||||
|
|
@ -47,7 +47,8 @@
|
|||
if (rawLine == null) break;
|
||||
out.push(sliceGraphemes(String(rawLine), SIGNBOARD_CHARS_PER_LINE));
|
||||
}
|
||||
return out.join("\n").replace(/\n+$/g, "");
|
||||
const next = out.join("\n");
|
||||
return opts.trimTrailing === false ? next : next.replace(/\n+$/g, "");
|
||||
}
|
||||
|
||||
function signboardVisibleLength(text = "") {
|
||||
|
|
@ -95,7 +96,8 @@
|
|||
const currentLine = lines[lineIndex] || "";
|
||||
const replacing = selected.length > 0;
|
||||
if (e.key === "Enter") {
|
||||
if (lines.length >= SIGNBOARD_LINES) e.preventDefault();
|
||||
const selectedLineBreaks = (selected.match(/\n/g) || []).length;
|
||||
if (lines.length - selectedLineBreaks >= SIGNBOARD_LINES) e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.key && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
|
|
@ -110,12 +112,12 @@
|
|||
const text = e.clipboardData?.getData?.("text") || "";
|
||||
const start = Number(area.selectionStart || 0);
|
||||
const end = Number(area.selectionEnd || start);
|
||||
const next = sanitizeSignboardText((area.value || "").slice(0, start) + text + (area.value || "").slice(end));
|
||||
const next = sanitizeSignboardText((area.value || "").slice(0, start) + text + (area.value || "").slice(end), { trimTrailing: false });
|
||||
area.value = next;
|
||||
syncCount();
|
||||
};
|
||||
area.oninput = () => {
|
||||
const clean = sanitizeSignboardText(area.value || "");
|
||||
const clean = sanitizeSignboardText(area.value || "", { trimTrailing: false });
|
||||
if (area.value !== clean) area.value = clean;
|
||||
syncCount();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
performanceLevel() {
|
||||
if (window.TarinaiPerformance) return window.TarinaiPerformance.level || 0;
|
||||
if (window.TarinaiPerformance) return window.TarinaiPerformance.quality?.(this)?.level || window.TarinaiPerformance.level || 0;
|
||||
const fps = typeof window !== "undefined" ? Number(window.__tarinaiFps || 0) : 60;
|
||||
if (!fps || !Number.isFinite(fps)) return 0;
|
||||
if (fps < 22) return 3;
|
||||
|
|
@ -25,12 +25,51 @@
|
|||
return window.TarinaiPerformance?.quality?.(this) || { level: this.performanceLevel(), antStride: 1, effectLimit: CONFIG.effectLimit ?? 96 };
|
||||
},
|
||||
|
||||
beginFramePerformanceBudgets(quality = this.performanceQuality?.()) {
|
||||
const pressure = clamp(Number(quality?.pressure || 0) || 0, 0, 1);
|
||||
const density = clamp(Number(quality?.effectDensity || (1 - pressure)) || 0, 0.12, 1);
|
||||
this.workBudget = {
|
||||
ai: Math.max(4, Math.round(4 + density * 24)),
|
||||
env: Math.max(3, Math.round(3 + density * 15)),
|
||||
aiUsed: 0,
|
||||
envUsed: 0,
|
||||
};
|
||||
this.workStats = {
|
||||
aiRuns: 0,
|
||||
aiSkips: 0,
|
||||
envRuns: 0,
|
||||
envSkips: 0,
|
||||
collisionRuns: 0,
|
||||
collisionSkips: 0,
|
||||
bedConflictRuns: 0,
|
||||
bedConflictSkips: 0,
|
||||
};
|
||||
},
|
||||
|
||||
spendScheduledWork(kind = "ai", urgent = false) {
|
||||
if (!this.workBudget) this.beginFramePerformanceBudgets?.();
|
||||
const stats = this.workStats || {};
|
||||
const budget = this.workBudget || {};
|
||||
const usedKey = `${kind}Used`;
|
||||
const runKey = `${kind}Runs`;
|
||||
const skipKey = `${kind}Skips`;
|
||||
if (urgent || (budget[usedKey] || 0) < (budget[kind] || 0)) {
|
||||
budget[usedKey] = (budget[usedKey] || 0) + 1;
|
||||
stats[runKey] = (stats[runKey] || 0) + 1;
|
||||
return true;
|
||||
}
|
||||
stats[skipKey] = (stats[skipKey] || 0) + 1;
|
||||
return false;
|
||||
},
|
||||
|
||||
runtimeDiagnostics() {
|
||||
const behaviorCounts = {};
|
||||
let forcedBehaviors = 0;
|
||||
let lockedBehaviors = 0;
|
||||
let liveTarinai = 0;
|
||||
for (const t of this.tarinai || []) {
|
||||
if (!t || t.dead) continue;
|
||||
liveTarinai += 1;
|
||||
const behavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(t) : t.behavior;
|
||||
const id = behavior?.id || t.state || "none";
|
||||
behaviorCounts[id] = (behaviorCounts[id] || 0) + 1;
|
||||
|
|
@ -65,6 +104,18 @@
|
|||
locked: lockedBehaviors,
|
||||
counts: behaviorCounts,
|
||||
},
|
||||
performance: {
|
||||
liveTarinai,
|
||||
fps: window.TarinaiPerformance?.fps || window.__tarinaiFps || 0,
|
||||
pressure: this.performanceQuality?.().pressure || 0,
|
||||
level: this.performanceLevel?.() || 0,
|
||||
dpr: (typeof uiCache !== "undefined" ? uiCache?.canvasDpr : window.uiCache?.canvasDpr) || 0,
|
||||
quality: this.performanceQuality?.() || null,
|
||||
},
|
||||
work: {
|
||||
budget: this.workBudget || null,
|
||||
stats: this.workStats || null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,32 @@
|
|||
(function (global) {
|
||||
const World = global.World;
|
||||
if (!World) throw new Error("World is not available for mixin: world_update.js");
|
||||
function markSpatialDirtyIfMoved(worldRef, list, reason = "moved", threshold = 0.35) {
|
||||
if (!worldRef || !Array.isArray(list) || !list.length) return false;
|
||||
const thresholdSq = threshold * threshold;
|
||||
let moved = false;
|
||||
for (const entity of list) {
|
||||
if (!entity || entity.dead) continue;
|
||||
const x = Number.isFinite(entity.x) ? entity.x : 0;
|
||||
const y = Number.isFinite(entity.y) ? entity.y : 0;
|
||||
const oldX = entity._spatialTrackX;
|
||||
const oldY = entity._spatialTrackY;
|
||||
if (!Number.isFinite(oldX) || !Number.isFinite(oldY)) {
|
||||
entity._spatialTrackX = x;
|
||||
entity._spatialTrackY = y;
|
||||
moved = true;
|
||||
continue;
|
||||
}
|
||||
const dx = x - oldX;
|
||||
const dy = y - oldY;
|
||||
if (dx * dx + dy * dy <= thresholdSq) continue;
|
||||
entity._spatialTrackX = x;
|
||||
entity._spatialTrackY = y;
|
||||
moved = true;
|
||||
}
|
||||
if (moved) worldRef.markSpatialDirty?.(reason);
|
||||
return moved;
|
||||
}
|
||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
environmentScores() {
|
||||
const alive = this.tarinai || [];
|
||||
|
|
@ -227,29 +253,47 @@
|
|||
this.spatialDirtyMarksThisFrame = 0;
|
||||
this.ensureSpatial?.("update-start");
|
||||
|
||||
const perf = this.performanceLevel();
|
||||
const quality = this.performanceQuality?.() || {};
|
||||
const perf = Number(quality.level ?? this.performanceLevel?.() ?? 0) || 0;
|
||||
this.beginFramePerformanceBudgets?.(quality);
|
||||
const itemCountBefore = this.items.length;
|
||||
for (const it of this.items) it.update(dt, this);
|
||||
if (this.items.length) this.markSpatialDirty?.("items-updated");
|
||||
const itemsMoved = markSpatialDirtyIfMoved(this, this.items, "items-moved", 0.25);
|
||||
if (this.itemCounts?.ball) {
|
||||
this.ensureSpatial?.("ball-ball-collisions");
|
||||
this.resolveBallBallCollisions(dt);
|
||||
this.markSpatialDirty?.("ball-ball-collisions");
|
||||
}
|
||||
this.updateAnts?.(dt);
|
||||
if ((this.ants || []).length) this.markSpatialDirty?.("ants-updated");
|
||||
const antsMoved = markSpatialDirtyIfMoved(this, this.ants || [], "ants-moved", 0.25);
|
||||
if (itemsMoved || antsMoved || this.spatialDirty) this.ensureSpatial?.("post-mobile-item-update");
|
||||
|
||||
for (const ef of this.effects) ef.update(dt);
|
||||
|
||||
for (const t of this.tarinai) {
|
||||
t.update(dt);
|
||||
}
|
||||
this.resolveOwnedBedSleepConflicts?.();
|
||||
if (this.tarinai.length) this.markSpatialDirty?.("tarinai-updated");
|
||||
const now = this.time || 0;
|
||||
const bedConflictInterval = Number(quality.bedConflictInterval || 0.12) || 0.12;
|
||||
if (now >= (this.nextBedConflictCheckAt || 0)) {
|
||||
this.nextBedConflictCheckAt = now + bedConflictInterval;
|
||||
this.workStats && (this.workStats.bedConflictRuns = (this.workStats.bedConflictRuns || 0) + 1);
|
||||
this.resolveOwnedBedSleepConflicts?.();
|
||||
} else {
|
||||
this.workStats && (this.workStats.bedConflictSkips = (this.workStats.bedConflictSkips || 0) + 1);
|
||||
}
|
||||
markSpatialDirtyIfMoved(this, this.tarinai, "tarinai-moved", 0.25);
|
||||
this.ensureSpatial?.("post-tarinai-update");
|
||||
this.limitBallChasers();
|
||||
this.resolveBallInteractions(dt);
|
||||
this.resolveTarinaiHighSpeedCollisions(dt);
|
||||
const collisionInterval = Number(quality.collisionInterval || 0.04) || 0.04;
|
||||
if (now >= (this.nextTarinaiCollisionCheckAt || 0)) {
|
||||
this.nextTarinaiCollisionCheckAt = now + collisionInterval;
|
||||
this.workStats && (this.workStats.collisionRuns = (this.workStats.collisionRuns || 0) + 1);
|
||||
this.resolveTarinaiHighSpeedCollisions(dt);
|
||||
} else {
|
||||
this.workStats && (this.workStats.collisionSkips = (this.workStats.collisionSkips || 0) + 1);
|
||||
}
|
||||
|
||||
this.compactTimer += dt;
|
||||
const compactInterval = perf >= 2 ? 0.95 : 0.58;
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue