fulfillment

This commit is contained in:
33333-33333 2026-06-23 11:27:55 +09:00
commit 2e90add2e1
67 changed files with 5427 additions and 690 deletions

View file

@ -174,6 +174,43 @@ function drawBulletSprite(ctx, r) {
ctx.restore();
}
function machineDishPosition(machine) {
const r = machine?.r || 42;
return { x: machine.x, y: machine.y + r * 1.18 };
}
function machineMuzzlePosition(machine) {
const r = machine?.r || 42;
const a = Number.isFinite(machine?.cannonAngle) ? machine.cannonAngle : 0;
return { x: machine.x + Math.cos(a) * r * 0.98, y: machine.y + Math.sin(a) * r * 0.98 };
}
function makeMachineConsumableCopy(type, x, y, source = null, opts = {}) {
const it = new Item(type, x, y);
it.machineProduced = true;
it.machineProducerId = source?.id || "";
it.sourceMachineType = source?.type || "";
it.noDecay = true;
if (opts.infiniteServing) it.infiniteServing = true;
if (opts.projectile) {
it.cannonProjectile = true;
it.projectileSourceId = source?.id || "";
it.projectileTimer = 0;
it.projectileHitMemo = {};
}
if (typeof isServingFoodType === "function" && isServingFoodType(type)) {
it.toolSize = source?.machineSlotSize || "medium";
it.foodServingScale = 1;
it.foodServingsMax = typeof foodServingsForSize === "function" ? foodServingsForSize(it.toolSize) : 5;
it.foodServingsRemaining = it.foodServingsMax;
it.amount = it.foodServingsMax;
}
if (type === "water") it.amount = 120;
if (type === "grass") { it.amount = Math.max(it.amount || 0, 120); it.growth = Math.max(it.growth || 0, 0.82); }
if (type === "zunchi") { it.amount = 240; it.stage = "fresh"; it.stageTimer = 0; it.freshness = 1; }
return it;
}
class Item {
constructor(type, x, y) {
this.id = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`;
@ -206,6 +243,14 @@ class Item {
this.text = "";
this.textEditedAt = -999;
}
if (type === "duplicator" || type === "cannon") {
this.machineSlotType = "";
this.machineSlotSize = "medium";
this.machineSpawnTimer = rand(0.4, 1.1);
this.machineLastSlotType = "";
this.cannonAngle = type === "cannon" ? -Math.PI * 0.25 : 0;
this.cannonFireTimer = rand(0.8, 1.6);
}
if (type === "ball") {
this.vx = rand(-5, 5);
this.vy = rand(-5, 5);
@ -278,6 +323,8 @@ class Item {
this.passiveFoodDecayTimer = rand(0, passiveFoodDecayInterval());
this.amount = this.foodServingsRemaining;
}
if (window.TarinaiNeedSystem?.itemRolesFor) window.TarinaiNeedSystem.itemRolesFor(this);
if (window.TarinaiNeedSystem?.ensureItemNeedEffects) window.TarinaiNeedSystem.ensureItemNeedEffects(this);
}
update(dt, worldRef = world) {
this.age += dt;
@ -289,8 +336,8 @@ class Item {
if (worldRef.itemDropImpact) worldRef.itemDropImpact(this);
}
}
if (this.type === "water") this.amount -= dt * 0.9;
if (isServingFoodType(this.type)) {
if (this.type === "water" && !this.noDecay && !this.infiniteServing) this.amount -= dt * 0.9;
if (isServingFoodType(this.type) && !this.noDecay && !this.infiniteServing) {
const interval = passiveFoodDecayInterval(worldRef);
this.passiveFoodDecayTimer = (this.passiveFoodDecayTimer || 0) + dt;
let ticks = 0;
@ -344,6 +391,9 @@ class Item {
if (this.type === "ball") this.updateBall(dt, worldRef);
if (isPinType(this.type)) this.updatePushpin(dt, worldRef);
if (this.type === "zunchi") this.updateZunchiMotion(dt, worldRef);
if (this.cannonProjectile) this.updateConsumableProjectile(dt, worldRef);
if (this.type === "duplicator") this.updateDuplicator(dt, worldRef);
if (this.type === "cannon") this.updateCannon(dt, worldRef);
if (this.type === "grass") {
this.lifecycleTimer += dt;
const factor = worldRef.grassUpdateFactor ? worldRef.grassUpdateFactor() : 1;
@ -364,6 +414,152 @@ class Item {
}
}
machineDishPosition() {
return machineDishPosition(this);
}
clearMachineProducedItems(worldRef, keepType = "") {
for (const it of worldRef?.items || []) {
if (!it || it.dead || it.machineProducerId !== this.id) continue;
if (keepType && it.type === keepType && this.type === "duplicator") continue;
it.amount = 0;
}
}
updateDuplicator(dt, worldRef) {
if (!worldRef || !this.machineSlotType) return;
if (this.machineLastSlotType && this.machineLastSlotType !== this.machineSlotType) this.clearMachineProducedItems(worldRef);
this.machineLastSlotType = this.machineSlotType;
this.machineSpawnTimer = Math.max(0, (this.machineSpawnTimer || 0) - dt);
const dish = this.machineDishPosition();
let serving = null;
for (const it of worldRef.items || []) {
if (!it || it.dead || it.machineProducerId !== this.id || it.type !== this.machineSlotType) continue;
serving = it;
break;
}
if (!serving && this.machineSpawnTimer <= 0) {
serving = makeMachineConsumableCopy(this.machineSlotType, dish.x, dish.y, this, { infiniteServing: true });
serving.r = Math.max(6, (serving.r || 12) * 0.92);
worldRef.items.push(serving);
worldRef.itemCounts[serving.type] = (worldRef.itemCounts[serving.type] || 0) + 1;
this.machineSpawnTimer = 1.2;
worldRef.rebuildSpatial?.(true);
}
if (serving) {
serving.x = dish.x;
serving.y = dish.y;
serving.vx = 0;
serving.vy = 0;
serving.prevX = serving.x;
serving.prevY = serving.y;
serving.noDecay = true;
serving.infiniteServing = true;
serving.machineProduced = true;
serving.machineProducerId = this.id;
if (typeof isServingFoodType === "function" && isServingFoodType(serving.type)) {
if (!Number.isFinite(serving.foodServingsMax) || serving.foodServingsMax <= 0) serving.foodServingsMax = typeof foodServingsForSize === "function" ? foodServingsForSize(serving.toolSize || "medium") : 5;
serving.foodServingsRemaining = serving.foodServingsMax;
serving.amount = serving.foodServingsMax;
} else if (serving.type === "water") serving.amount = Math.max(serving.amount || 0, 120);
else if (serving.type === "grass") serving.amount = Math.max(serving.amount || 0, 120);
else if (serving.type === "zunchi") serving.amount = Math.max(serving.amount || 0, 240);
}
}
updateCannon(dt, worldRef) {
if (!worldRef || !this.machineSlotType) return;
this.cannonFireTimer = Math.max(0, (this.cannonFireTimer || 0) - dt);
if (this.cannonFireTimer > 0) return;
this.cannonFireTimer = rand(2.2, 3.4);
const a = Number.isFinite(this.cannonAngle) ? this.cannonAngle : 0;
const muzzle = machineMuzzlePosition(this);
const shot = makeMachineConsumableCopy(this.machineSlotType, muzzle.x, muzzle.y, this, { projectile: true });
shot.r = Math.max(5, (shot.r || 12) * 0.78);
const speed = rand(470, 620);
shot.vx = Math.cos(a) * speed + rand(-18, 18);
shot.vy = Math.sin(a) * speed + rand(-18, 18);
shot.prevX = shot.x;
shot.prevY = shot.y;
shot.spinVelocity = rand(-6, 6) + speed / Math.max(8, shot.r || 10) * 0.12;
shot.amount = Math.max(shot.amount || 0, typeof isServingFoodType === "function" && isServingFoodType(shot.type) ? (shot.foodServingsMax || 5) : itemAmountFor(shot.type, 60));
worldRef.items.push(shot);
worldRef.itemCounts[shot.type] = (worldRef.itemCounts[shot.type] || 0) + 1;
worldRef.effects?.push(new Effect("ring", muzzle.x, muzzle.y, { size: Math.max(16, (this.r || 42) * 0.42), life: 0.22, color: "rgba(255,238,160,0.52)" }));
worldRef.log?.(`大砲が${toolLabel(shot.type)}を撃ち出した。`, "accident");
audio.place?.(shot.type);
worldRef.rebuildSpatial?.(true);
}
updateConsumableProjectile(dt, worldRef) {
if (!worldRef) return;
this.projectileTimer = (this.projectileTimer || 0) + dt;
this.prevX = this.x;
this.prevY = this.y;
const speedBefore = Math.hypot(this.vx || 0, this.vy || 0);
if (speedBefore > 0.05) {
if (window.TarinaiPhysics?.applyKinematicItemMotion) {
window.TarinaiPhysics.applyKinematicItemMotion(this, worldRef, dt, { padding: 28, bounce: 0.46, frictionBase: 0.62, frictionRate: 1.25, spin: false, stopSpeed: 0.12, zeroBelow: 14 });
} else {
this.x += (this.vx || 0) * dt;
this.y += (this.vy || 0) * dt;
const p = Math.max(28, CONFIG.worldPadding || 30);
if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx || 0) * 0.46; }
if (this.x > worldRef.w - p) { this.x = worldRef.w - p; this.vx = -Math.abs(this.vx || 0) * 0.46; }
if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy || 0) * 0.46; }
if (this.y > worldRef.h - p) { this.y = worldRef.h - p; this.vy = -Math.abs(this.vy || 0) * 0.46; }
const slow = Math.pow(0.62, dt * 1.25);
this.vx *= slow;
this.vy *= slow;
}
this.spin = (this.spin || 0) + speedBefore * dt / Math.max(6, this.r || 10);
this.resolveProjectileTarinaiCollision(dt, worldRef, speedBefore);
}
const speedAfter = Math.hypot(this.vx || 0, this.vy || 0);
if (speedAfter < 18 && (this.projectileTimer || 0) > 0.30) {
this.cannonProjectile = false;
this.noDecay = false;
this.projectileHitMemo = {};
this.vx = 0;
this.vy = 0;
}
if ((this.projectileTimer || 0) > 28) this.cannonProjectile = false;
}
resolveProjectileTarinaiCollision(dt, worldRef, speed = 0) {
if (!worldRef || speed < 210 || (this.amount || 0) <= 0) return;
const range = Math.max(42, (this.r || 12) + speed * Math.max(dt || 0.016, 0.016) + 22);
for (const t of worldRef.nearbyTarinai?.(this.x, this.y, range, true) || []) {
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
const d = distXY(this.x, this.y, t.x, t.y);
if (d > (t.radius || 20) * 0.72 + (this.r || 12)) continue;
const now = worldRef.time || 0;
this.projectileHitMemo = this.projectileHitMemo || {};
if (this.projectileHitMemo[t.id] && this.projectileHitMemo[t.id] + 0.58 > now) continue;
this.projectileHitMemo[t.id] = now;
const nx = speed > 0 ? (this.vx || 1) / speed : rand(-1, 1);
const ny = speed > 0 ? (this.vy || 0) / speed : rand(-1, 1);
const impulse = clamp(speed * 0.95, 130, 560);
t.vx = (t.vx || 0) + nx * impulse + rand(-18, 18);
t.vy = (t.vy || 0) + ny * impulse * 0.70 + rand(-14, 12);
t.fallTimer = Math.max(t.fallTimer || 0, 0.72);
t.fallMax = Math.max(t.fallMax || 0.72, t.fallTimer);
t.fallDir = nx < 0 ? -1 : 1;
const label = toolLabel(this.type) || "食べ物";
if (t.enterPanic) t.enterPanic({ threat: this, reason: `大砲から飛んできた${label}にぶつかって吹き飛んでいる`, fear: 0.72, stress: 6, stressDuration: 3.0, surpriseTimer: 0.48, cause: "cannon_projectile_hit" });
else t.setActionState?.("panic", { target: { x: this.x, y: this.y }, reason: `大砲から飛んできた${label}にぶつかって吹き飛んでいる`, wake: true });
if (worldRef.applyImpactDamage) worldRef.applyImpactDamage(t, clamp((speed - 190) / 30, 1.4, 16), "大砲");
worldRef.spawnFallEffect?.(t.x, t.y + (t.radius || 20) * 0.50, 0.78);
worldRef.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(17, (this.r || 12) * 1.15), life: 0.22, color: "rgba(220,160,70,0.52)" }));
if (worldRef.relationNotice?.(t.id, this.id || "projectile", "cannon-projectile-hit", 2.8)) worldRef.log?.(`${t.name}は大砲から飛んできた${label}にぶつかって吹き飛ばされた。`, "accident", { participants: [t] });
this.vx *= -0.12;
this.vy *= -0.12;
this.cannonProjectile = false;
this.noDecay = false;
break;
}
}
updateZunchiMotion(dt, worldRef) {
const speed = Math.hypot(this.vx || 0, this.vy || 0);
if (speed < 0.08) { this.vx = 0; this.vy = 0; return; }
@ -668,9 +864,11 @@ class Item {
this.pinState = "lodged";
this.deletable = false;
this.pinTargetId = t.id;
this.pinAttachAngle = isOshibyo ? 0.62 : Math.atan2(dy || -1, dx || (t.facingDir ? t.facingDir() : 1));
this.pinAttachDistance = isOshibyo ? (t.radius || 16) * 0.58 : clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74);
this.pinOffsetY = isOshibyo ? (t.radius || 16) * 0.30 : clamp(dy, -(t.radius || 16) * 0.74, (t.radius || 16) * 0.38);
const visualSide = (t.facingDir ? t.facingDir() : (t.facing || 1)) > 0 ? -1 : 1;
this.pinAttachAngle = isOshibyo ? (visualSide < 0 ? -0.48 : 0.48) : Math.atan2(dy || -1, dx || visualSide);
this.pinAttachDistance = isOshibyo ? (t.radius || 16) * 0.76 : clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74);
this.pinOffsetY = isOshibyo ? (t.radius || 16) * 0.22 : clamp(dy, -(t.radius || 16) * 0.74, (t.radius || 16) * 0.38);
this.pinFlipX = visualSide;
this.pinDamageTick = 0;
this.pinFallCheckTimer = 0;
this.vx = 0;
@ -714,11 +912,13 @@ class Item {
const isOshibyo = Boolean(behavior?.blocksZunchi);
t.stuckPushpinId = this.id;
this.deletable = false;
this.x = isOshibyo ? t.x + (t.radius || 16) * 0.42 : t.x + (t.radius || 16) * 0.36;
this.y = isOshibyo ? t.y + (t.radius || 16) * 0.34 : t.y - (t.radius || 16) * 0.04;
const visualSide = (t.facingDir ? t.facingDir() : (t.facing || 1)) > 0 ? -1 : 1;
this.pinFlipX = visualSide;
this.x = t.x + visualSide * (isOshibyo ? (t.radius || 16) * 0.72 : (t.radius || 16) * 0.36);
this.y = isOshibyo ? t.y + (t.radius || 16) * 0.22 : t.y - (t.radius || 16) * 0.04;
this.prevX = this.x;
this.prevY = this.y;
this.spin = isOshibyo ? 0.52 : 0.28;
this.spin = (isOshibyo ? 0.42 : 0.28) * (this.pinFlipX < 0 ? -1 : 1);
this.vx = t.vx || 0;
this.vy = t.vy || 0;
if (isOshibyo) {
@ -1114,20 +1314,89 @@ class Item {
ctx.fill();
}
ctx.restore();
} else if (this.type === "duplicator" || this.type === "cannon") {
const r = this.r || 42;
const isCannon = this.type === "cannon";
ctx.save();
ctx.shadowColor = "rgba(40,35,30,0.24)";
ctx.shadowBlur = 6;
ctx.shadowOffsetY = 3;
if (!isCannon) {
// Upper machine body.
const g = ctx.createLinearGradient(0, -r * 1.0, 0, r * 0.32);
g.addColorStop(0, "#e7f1ff");
g.addColorStop(1, "#8aa2bd");
ctx.fillStyle = g;
ctx.strokeStyle = "rgba(48,68,92,0.70)";
ctx.lineWidth = Math.max(2, r * 0.06);
roundedRect(ctx, -r * 0.82, -r * 1.05, r * 1.64, r * 0.92, r * 0.18);
ctx.fill(); ctx.stroke();
ctx.fillStyle = this.machineSlotType ? "rgba(100,214,102,0.92)" : "rgba(255,180,72,0.92)";
ctx.beginPath(); ctx.arc(r * 0.48, -r * 0.76, r * 0.12, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = "rgba(42,55,68,0.54)";
ctx.lineWidth = Math.max(1.6, r * 0.04);
ctx.beginPath(); ctx.moveTo(-r * 0.48, -r * 0.78); ctx.lineTo(r * 0.20, -r * 0.78); ctx.stroke();
// Plate for tarinai below.
ctx.fillStyle = "rgba(245,244,233,0.96)";
ctx.strokeStyle = "rgba(116,96,75,0.58)";
ctx.lineWidth = Math.max(2, r * 0.05);
ctx.beginPath(); ctx.ellipse(0, r * 1.18, r * 0.95, r * 0.34, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.fillStyle = "rgba(196,180,154,0.22)";
ctx.beginPath(); ctx.ellipse(0, r * 1.18, r * 0.58, r * 0.16, 0, 0, Math.PI * 2); ctx.fill();
if (this.machineSlotType) {
ctx.fillStyle = "rgba(42,36,28,0.72)";
ctx.font = `${Math.max(10, Math.round(r * 0.25))}px ui-rounded, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(toolLabel(this.machineSlotType), 0, r * 0.52);
}
} else {
const a = Number.isFinite(this.cannonAngle) ? this.cannonAngle : 0;
// Base plate.
ctx.fillStyle = "#6c7685";
ctx.strokeStyle = "rgba(42,48,58,0.72)";
ctx.lineWidth = Math.max(2, r * 0.06);
ctx.beginPath(); ctx.ellipse(0, r * 0.72, r * 0.78, r * 0.30, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
// Barrel.
ctx.rotate(a);
const bg = ctx.createLinearGradient(-r * 0.34, -r * 0.26, r * 1.08, r * 0.26);
bg.addColorStop(0, "#596575");
bg.addColorStop(0.55, "#2f3946");
bg.addColorStop(1, "#1f2630");
ctx.fillStyle = bg;
ctx.strokeStyle = "rgba(16,20,26,0.82)";
roundedRect(ctx, -r * 0.28, -r * 0.26, r * 1.32, r * 0.52, r * 0.18);
ctx.fill(); ctx.stroke();
ctx.fillStyle = "rgba(10,12,16,0.88)";
ctx.beginPath(); ctx.ellipse(r * 1.02, 0, r * 0.16, r * 0.23, 0, 0, Math.PI * 2); ctx.fill();
ctx.rotate(-a);
ctx.fillStyle = "#8793a5";
ctx.beginPath(); ctx.arc(0, 0, r * 0.36, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
if (this.machineSlotType) {
ctx.fillStyle = "rgba(42,36,28,0.72)";
ctx.font = `${Math.max(10, Math.round(r * 0.23))}px ui-rounded, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(toolLabel(this.machineSlotType), 0, -r * 0.62);
}
}
ctx.restore();
} else if (isPinType(this.type)) {
const stuck = this.pinState === "lodged";
const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(this.type) : null;
const assetId = behavior ? (stuck ? behavior.lodgedAsset : behavior.looseAsset) : (stuck ? "pushpin_stuck" : "pushpin");
const isOshibyo = Boolean(behavior?.blocksZunchi);
const img = typeof getRenderableImage === "function" ? getRenderableImage(assetId, assetId) : images.get(assetId);
ctx.save();
if (!stuck) ctx.rotate((this.spin || 0) + Math.PI * 0.5);
else if ((this.pinFlipX || 1) < 0) ctx.scale(-1, 1);
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 = isOshibyo ? (stuck ? this.r * 3.1 : this.r * 1.55) : (stuck ? this.r * 2.15 : this.r * 1.78);
const w = isOshibyo ? (stuck ? this.r * 2.45 : this.r * 1.25) : (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) {