many
This commit is contained in:
parent
0b9ff1363b
commit
0a6148c73f
67 changed files with 2653 additions and 1519 deletions
248
js/items.js
248
js/items.js
|
|
@ -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)";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue