This commit is contained in:
33333-33333 2026-06-21 22:29:00 +09:00
commit 0a6148c73f
67 changed files with 2653 additions and 1519 deletions

View file

@ -1,8 +1,8 @@
// tarinai_colony_game build 15.6.2: food servings, nest box access, and editable tarinai data
"use strict";
const FOOD_SERVING_TYPES = new Set(["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]);
const PARAM_EFFECT_ITEM_TYPES = new Set(["laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]);
const FOOD_SERVING_TYPES = new Set(window.TarinaiFoodRegistry?.servingTypes?.() || ["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]);
const PARAM_EFFECT_ITEM_TYPES = new Set(window.TarinaiFoodRegistry?.paramEffectTypes?.() || ["laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]);
function isParamEffectItemType(type = "") {
return PARAM_EFFECT_ITEM_TYPES.has(type);
}
@ -23,7 +23,7 @@ function passiveFoodDecayInterval(worldRef = null) {
function passiveFoodDecayRate(item) {
const size = item?.toolSize || "medium";
const sizeMul = size === "small" ? 0.74 : (size === "large" ? 1.32 : 1);
const typeMul = item?.type === "sweet" ? 0.82 : (item?.type === "zunda_juice" ? 1.12 : 1);
const typeMul = window.TarinaiFoodRegistry?.passiveDecayMultiplier?.(item?.type) ?? (item?.type === "sweet" ? 0.82 : (item?.type === "zunda_juice" ? 1.12 : 1));
return PASSIVE_FOOD_DECAY_PER_SECOND * sizeMul * typeMul;
}
@ -232,6 +232,24 @@ class Item {
this.lingerTimer = 0;
this.lingerDuration = 2.0;
}
if (type === "pushpin") {
this.amount = 100;
this.vx = rand(-120, 120);
this.vy = rand(-90, 90);
this.prevX = x;
this.prevY = y;
this.spin = rand(-0.35, 0.35);
this.spinVelocity = rand(-5.4, 5.4);
this.pinState = "loose";
this.pinTargetId = "";
this.pinAttachAngle = 0;
this.pinAttachDistance = 0;
this.pinOffsetY = 0;
this.pinDamageTick = 0;
this.pinFallCheckTimer = 0;
this.pinLogAt = -999;
this.deletable = true;
}
if (isServingFoodType(type)) {
this.toolSize = "medium";
this.foodServingScale = 1;
@ -265,8 +283,10 @@ class Item {
if (lost > 0) {
this.foodServingsRemaining = Math.max(0, before - lost);
this.amount = this.foodServingsRemaining;
worldRef?.registerFoodSpoilage?.(lost * PASSIVE_FOOD_HYGIENE_PENALTY_PER_SERVING, this);
const penalty = window.TarinaiFoodRegistry?.hygienePenalty?.(this.type) ?? PASSIVE_FOOD_HYGIENE_PENALTY_PER_SERVING;
worldRef?.registerFoodSpoilage?.(lost * penalty, this);
worldRef?.markTerrainDirty?.("food-passive-decay");
worldRef?.emit?.("item:decayed", { item: this, type: this.type, amount: lost, passive: true });
if (this.foodServingsRemaining <= 0.015) this.amount = 0;
}
} else if (["sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"].includes(this.type)) {
@ -274,8 +294,10 @@ class Item {
const lost = Math.min(before, interval * 0.12);
this.amount -= lost;
if (lost > 0) {
worldRef?.registerFoodSpoilage?.(lost * 0.002, this);
const penalty = window.TarinaiFoodRegistry?.hygienePenalty?.(this.type) ?? 0.002;
worldRef?.registerFoodSpoilage?.(lost * penalty, this);
worldRef?.markTerrainDirty?.("food-passive-decay");
worldRef?.emit?.("item:decayed", { item: this, type: this.type, amount: lost, passive: true });
}
}
}
@ -300,6 +322,7 @@ class Item {
}
}
if (this.type === "ball") this.updateBall(dt, worldRef);
if (this.type === "pushpin") this.updatePushpin(dt, worldRef);
if (this.type === "zunchi") this.updateZunchiMotion(dt, worldRef);
if (this.type === "grass") {
this.lifecycleTimer += dt;
@ -542,6 +565,157 @@ class Item {
worldRef?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(13, (this.r || 18) * 0.82), life: 0.18, color: obstacle?.type === "stone" ? "rgba(165,165,150,0.45)" : "rgba(160,116,64,0.42)" }));
}
updatePushpin(dt, worldRef) {
if (!worldRef) return;
if (this.pinState === "lodged") {
this.updateLodgedPushpin(dt, worldRef);
return;
}
this.prevX = this.x;
this.prevY = this.y;
const moving = Math.hypot(this.vx || 0, this.vy || 0);
if (moving > 0.05) {
this.x += (this.vx || 0) * dt;
this.y += (this.vy || 0) * dt;
const p = Math.max(26, CONFIG.worldPadding || 30);
if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx || 0) * 0.44; this.spinVelocity *= -0.68; }
if (this.x > worldRef.w - p) { this.x = worldRef.w - p; this.vx = -Math.abs(this.vx || 0) * 0.44; this.spinVelocity *= -0.68; }
if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy || 0) * 0.44; this.spinVelocity *= -0.68; }
if (this.y > worldRef.h - p) { this.y = worldRef.h - p; this.vy = -Math.abs(this.vy || 0) * 0.44; this.spinVelocity *= -0.68; }
const friction = Math.pow(0.18, dt * 0.85);
this.vx *= friction;
this.vy *= friction;
this.spin = (this.spin || 0) + (this.spinVelocity || 0) * dt + (this.vx || 0) * dt / Math.max(7, this.r || 16);
this.spinVelocity *= Math.pow(0.38, dt);
if (Math.hypot(this.vx || 0, this.vy || 0) < 7.5) { this.vx = 0; this.vy = 0; this.spinVelocity *= 0.3; }
worldRef.drawListDirty = true;
} else {
this.vx = 0;
this.vy = 0;
this.spinVelocity *= Math.pow(0.08, dt);
this.spin = (this.spin || 0) + (this.spinVelocity || 0) * dt;
}
this.tryStickPushpin(worldRef);
}
tryStickPushpin(worldRef) {
if (!worldRef?.tarinai?.length) return false;
if (this.pinState === "lodged") return false;
let best = null;
let bestD = Infinity;
const hitRange = Math.max(10, (this.r || 8) * 1.25);
for (const t of worldRef.tarinai) {
if (!t || t.dead) continue;
const d = distXY(this.x, this.y, t.x, t.y);
const hit = (t.radius || 16) * 0.86 + hitRange;
if (d <= hit && d < bestD) { best = t; bestD = d; }
}
if (!best) return false;
return this.attachPushpin(best, worldRef, bestD);
}
attachPushpin(t, worldRef, d = null) {
if (!t || t.dead) return false;
if (t.stuckPushpinId && t.stuckPushpinId !== this.id) return false;
const dx = this.x - t.x;
const dy = this.y - t.y;
const distToTarget = Number.isFinite(d) ? d : Math.hypot(dx, dy);
this.pinState = "lodged";
this.deletable = false;
this.pinTargetId = t.id;
this.pinAttachAngle = Math.atan2(dy || -1, dx || (t.facingDir ? t.facingDir() : 1));
this.pinAttachDistance = clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74);
this.pinOffsetY = clamp(dy, -(t.radius || 16) * 0.74, (t.radius || 16) * 0.38);
this.pinDamageTick = 0;
this.pinFallCheckTimer = 0;
this.vx = 0;
this.vy = 0;
this.spinVelocity = 0;
this.spin = this.pinAttachAngle;
t.stuckPushpinId = this.id;
t.sleeping = false;
t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 7);
t.hurtTimer = Math.max(t.hurtTimer || 0, 1.2);
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.8);
t.fearTimer = Math.max(t.fearTimer || 0, 1.3);
t.state = "panic";
t.target = t.panicDestination ? t.panicDestination(this) : { x: t.x + rand(-80, 80), y: t.y + rand(-80, 80) };
t.addStress ? t.addStress(18, { threshold: 8 }) : (t.stress = clamp((t.stress || 0) + 18, 0, 130));
if (t.damage) t.damage(6, "\u753b\u92f2");
if ((worldRef.time || 0) >= (this.pinLogAt || -999) + 1.2) {
this.pinLogAt = worldRef.time || 0;
worldRef.log?.(`${t.name}\u306b\u753b\u92f2\u304c\u523a\u3055\u3063\u305f\u3002`, "accident", { participants: [t] });
}
worldRef.effects?.push(new Effect("ring", t.x, t.y - (t.radius || 16) * 0.12, { size: Math.max(18, (t.radius || 16) * 0.90), life: 0.22, color: "rgba(214,72,72,0.50)" }));
return true;
}
updateLodgedPushpin(dt, worldRef) {
const t = worldRef.tarinai?.find(o => o && !o.dead && o.id === this.pinTargetId) || null;
if (!t) {
this.detachPushpin(worldRef, "owner-lost");
return;
}
t.stuckPushpinId = this.id;
this.deletable = false;
this.x = t.x + (t.radius || 16) * 0.36;
this.y = t.y - (t.radius || 16) * 0.04;
this.prevX = this.x;
this.prevY = this.y;
this.spin = 0.28;
this.vx = t.vx || 0;
this.vy = t.vy || 0;
this.pinDamageTick = (this.pinDamageTick || 0) + dt;
while (this.pinDamageTick >= 0.55) {
this.pinDamageTick -= 0.55;
if (t.damage) t.damage(1.4, "\u753b\u92f2");
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.50);
t.addStress ? t.addStress(2.6, { threshold: 8, duration: 3.4 }) : (t.stress = clamp((t.stress || 0) + 2.6, 0, 130));
if (Math.random() < 0.22) t.bubble?.("!!", 0.6, "rgba(168,72,72,0.82)");
}
t.sleeping = false;
t.state = "panic";
t.target = t.panicDestination ? t.panicDestination(this) : { x: t.x + rand(-80, 80), y: t.y + rand(-80, 80) };
t.fearTimer = Math.max(t.fearTimer || 0, 0.65);
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.22);
t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 2.4);
t.thought = "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b";
this.pinFallCheckTimer = (this.pinFallCheckTimer || 0) + dt;
while (this.pinFallCheckTimer >= 1.0) {
this.pinFallCheckTimer -= 1.0;
if (Math.random() < 0.10) {
this.detachPushpin(worldRef, "fall");
return;
}
}
}
detachPushpin(worldRef, reason = "released") {
const t = worldRef?.tarinai?.find(o => o && o.id === this.pinTargetId) || null;
if (t && t.stuckPushpinId === this.id) t.stuckPushpinId = null;
this.pinState = "loose";
this.pinTargetId = "";
this.pinDamageTick = 0;
this.pinFallCheckTimer = 0;
this.deletable = true;
if (reason === "pinch") {
this.vx = 0;
this.vy = 0;
this.spinVelocity = 0;
} else {
this.vx = rand(-24, 24);
this.vy = rand(-10, 18);
this.spinVelocity = rand(-1.6, 1.6);
this.spin += rand(-0.25, 0.25);
if (t) {
this.x = clamp(t.x + rand(-(t.radius || 16) * 0.75, (t.radius || 16) * 0.75), CONFIG.worldPadding || 30, (worldRef?.w || this.x) - (CONFIG.worldPadding || 30));
this.y = clamp(t.y + rand((t.radius || 16) * 0.12, (t.radius || 16) * 0.72), CONFIG.worldPadding || 30, (worldRef?.h || this.y) - (CONFIG.worldPadding || 30));
}
worldRef?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(14, (this.r || 16) * 0.92), life: 0.16, color: "rgba(214,112,112,0.40)" }));
if (reason === "fall" && t) worldRef?.log?.(`${t.name}\u306e\u753b\u92f2\u304c\u629c\u3051\u843d\u3061\u305f\u3002`, "observe", { participants: [t] });
}
}
updateGrass(dt, worldRef) {
const light = worldRef.lightLevel ? worldRef.lightLevel() : 0.7;
const weather = worldRef.weather || "sunny";
@ -754,18 +928,16 @@ class Item {
}
}
} else if (typeof isParamEffectItemType === "function" && isParamEffectItemType(this.type)) {
const visual = typeof itemVisualDefinition === "function" ? itemVisualDefinition(this.type) : null;
const palette = visual?.palette || {};
const renderer = visual?.renderer || "split_pill";
const meta = {
laxative: { fill: "#f1d18a", stroke: "#774c1f", accent: "#5f3a1b", kind: "laxative" },
protein: { fill: "#f4a24f", stroke: "#b45b27", accent: "#ffe2b9", kind: "powder" },
niteropu: { fill: "#8a79d0", stroke: "#4c3f91", accent: "#d8d1ff", kind: "powder" },
ammo: { fill: "#f4c46a", stroke: "#6f431f", accent: "#ffe8a8", kind: "bullet" },
mystery_drug: { fill: "#dff4ff", stroke: "#27587b", accent: "#437c97", kind: "mystery" },
mercury: { fill: "rgba(210,216,222,0.94)", stroke: "rgba(112,122,132,0.62)", accent: "rgba(255,255,255,0.72)", kind: "droplet" },
giant_drug: { fill: "#ffe3cf", stroke: "#c9632f", accent: "#ffb47b", kind: "pill" },
dwarf_drug: { fill: "#e9e9ff", stroke: "#6967ac", accent: "#b8b5f4", kind: "pill" },
zunda_juice: { fill: "rgba(125,218,82,0.94)", stroke: "rgba(63,150,50,0.62)", accent: "rgba(238,255,226,0.72)", kind: "droplet" },
}[this.type] || { fill: "#fffaf1", stroke: "#8d6f48", accent: "#ddd", kind: "pill" };
const localR = this.r * (this.type === "giant_drug" ? 1.08 : (this.type === "dwarf_drug" ? 0.92 : 1));
fill: palette.fill || "#fffaf1",
stroke: palette.stroke || "#8d6f48",
accent: palette.highlight || palette.accent || "#ddd",
kind: renderer === "powder_pile" ? "powder" : renderer === "oval_droplet" ? "droplet" : renderer === "laxative_tablet" ? "laxative" : renderer === "mystery_tablet" ? "mystery" : renderer === "bullet" ? "bullet" : "pill",
};
const localR = this.r * (visual?.fieldScale || visual?.scale || 1);
ctx.rotate(Math.sin(this.seed) * 0.10);
if (meta.kind === "powder") {
ctx.save();
@ -873,6 +1045,50 @@ class Item {
ctx.fill();
}
ctx.restore();
} else if (this.type === "pushpin") {
const stuck = this.pinState === "lodged";
const assetId = stuck ? "pushpin_stuck" : "pushpin";
const img = typeof getRenderableImage === "function" ? getRenderableImage(assetId, assetId) : images.get(assetId);
ctx.save();
if (!stuck) ctx.rotate((this.spin || 0) + Math.PI * 0.5);
ctx.shadowColor = stuck ? "rgba(116,24,32,0.18)" : "rgba(54,42,31,0.16)";
ctx.shadowBlur = 3;
ctx.shadowOffsetY = 2;
if (img) {
const metrics = typeof getImageMetrics === "function" ? getImageMetrics(assetId) : null;
const ratio = metrics?.ratio || (stuck ? 1 : 2.0);
const w = stuck ? this.r * 2.15 : this.r * 1.78;
const h = w * ratio;
ctx.drawImage(img, -w * 0.5, -h * 0.5, w, h);
} else if (stuck) {
ctx.fillStyle = "#d92736";
ctx.strokeStyle = "rgba(125,18,30,0.78)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, this.r * 0.68, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, this.r * 0.26, 0, Math.PI * 2);
ctx.fillStyle = "rgba(176,20,30,0.82)";
ctx.fill();
} else {
ctx.fillStyle = "#d92736";
ctx.strokeStyle = "rgba(125,18,30,0.78)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(0, -this.r * 0.26, this.r * 0.70, this.r * 0.48, 0, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.ellipse(0, this.r * 0.18, this.r * 0.82, this.r * 0.30, 0, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
ctx.strokeStyle = "rgba(210,210,215,0.92)";
ctx.lineWidth = Math.max(1.5, this.r * 0.12);
ctx.beginPath();
ctx.moveTo(0, this.r * 0.42);
ctx.lineTo(0, this.r * 1.65);
ctx.stroke();
}
ctx.restore();
} else if (this.type === "water_bowl") {
ctx.fillStyle = "rgba(171, 120, 76, 0.72)";
ctx.strokeStyle = "rgba(94, 69, 48, 0.55)";