tarinai/js/structures.js

293 lines
13 KiB
JavaScript

"use strict";
(function (global) {
function createStructureId(prefix = "structure") {
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`;
}
function clonePlain(value) {
return JSON.parse(JSON.stringify(value || {}));
}
class TarinaiStructure {
constructor(definition, owner, x, y, world) {
this.id = createStructureId("structure");
this.type = definition.type;
this.label = definition.label;
this.isStructure = true;
this.ownerId = owner?.id || "";
this.hp = definition.maxHp || 1;
this.maxHp = definition.maxHp || this.hp;
this.attachment = definition.type === "plushie" ? 30 : 20;
this.usedCount = 0;
this.x = x;
this.y = y;
this.r = definition.type === "plushie" ? 6.5 : (definition.type === "grass_bed" ? 18 : 24);
this.amount = this.hp;
this.seed = Math.random() * 1000;
this.roles = clonePlain(definition.roles);
this.needEffects = clonePlain(definition.needEffects);
this.carriedById = definition.type === "plushie" ? this.ownerId : "";
this.onHead = definition.type === "plushie";
if (definition.type === "plushie") {
this.plushieSpriteId = "plushie_bear";
}
this.dead = false;
this.world = world || null;
}
owner(worldRef = this.world) {
return (worldRef?.tarinai || []).find(t => t && !t.dead && t.id === this.ownerId) || null;
}
update(dt, worldRef = this.world) {
this.world = worldRef || this.world;
if (this.dead || this.hp <= 0) {
this.amount = 0;
return;
}
if (this.type === "plushie") {
const owner = this.owner(worldRef);
if (!owner) {
this.dead = true;
this.hp = 0;
this.amount = 0;
this.carriedById = "";
this.onHead = false;
worldRef && (worldRef.drawListDirty = true);
worldRef?.markItemBucketsDirty?.("plushie-owner-gone");
worldRef?.markSpatialDirty?.("plushie-owner-gone");
return;
}
this.ownerId = owner.id || this.ownerId;
this.carriedById = owner.id || this.ownerId || this.carriedById;
this.onHead = true;
this.x = owner.x;
this.y = owner.y - Math.max(22, (owner.radius || 24) * 1.02);
if (owner.needs) owner.needs.fulfill = Math.max(0, (owner.needs.fulfill || 0) - dt * 2.8);
}
if (this.type === "grass_bed") {
if (!Number.isFinite(this.hp) || this.hp <= 0) {
this.hp = Math.max(1, Number(this.maxHp || 100) || 100);
this.amount = this.hp;
}
const owner = this.owner(worldRef);
if (!owner && !this.singleUsePending) {
this.unownedDecayTimer = (this.unownedDecayTimer || 0) + dt;
const rainBoost = worldRef?.weather === "light_rain" ? 1.65 : 1;
const ageBoost = 1 + Math.min(1.8, this.unownedDecayTimer / 360);
this.damage(dt * 0.070 * rainBoost * ageBoost, worldRef, null);
if (this.dead) return;
} else {
this.unownedDecayTimer = 0;
}
}
this.amount = Math.max(0.001, Number(this.hp || this.amount || 1) || 1);
}
use(user, worldRef = this.world, options = {}) {
const definition = global.StructureRegistry?.get?.(this.type);
return definition?.onUse?.(user, this, worldRef, options) || false;
}
damage(amount = 1, worldRef = this.world, breaker = null) {
if (this.dead) return false;
this.hp = Math.max(0, (this.hp || 0) - Math.max(0, amount || 0));
this.amount = this.hp;
if (this.hp > 0) return false;
this.dead = true;
this.amount = 0;
global.StructureRegistry?.get?.(this.type)?.onDestroyed?.(this, worldRef, breaker);
global.TarinaiStructureLifecycle.triggerOwnedStructureDestroyedPanic(worldRef, this, breaker, "structure-damaged-destroyed");
return true;
}
draw(ctx, time = 0, lighting = {}) {
if (this.dead) return;
let ownerAlpha = 1;
if (this.type === "plushie") {
const owner = this.owner(this.world);
ownerAlpha = owner ? clamp(1 - (owner.nestFade || (owner.insideNestBoxId ? 1 : 0)), 0, 1) : 1;
if (ownerAlpha <= 0.02) return;
}
ctx.save();
if (this.type === "plushie") ctx.globalAlpha *= ownerAlpha;
ctx.translate(this.x, this.y);
const night = (lighting?.light || 1) < 0.42;
if (this.type === "grass_bed") {
// \u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9: \u7dd1\u8272\u306e\u5c0f\u3055\u3044\u5e72\u8349\u5bdd\u5e8a\u3002\u901a\u5e38\u306e\u5e72\u8349\u5bdd\u5e8a\u3088\u308a\u4e00\u56de\u308a\u5c0f\u3055\u304f\u3001\u8349\u675f\u611f\u3092\u5f37\u3081\u308b\u3002
const grassFill = night ? "rgba(78,132,67,0.90)" : "rgba(84,177,72,0.93)";
const grassStroke = night ? "rgba(39,78,43,0.76)" : "rgba(45,112,48,0.78)";
const strawStroke = night ? "rgba(168,207,119,0.42)" : "rgba(205,234,128,0.66)";
ctx.fillStyle = grassFill;
ctx.strokeStyle = grassStroke;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(0, 4, this.r * 1.72, this.r * 0.66, -0.08, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = strawStroke;
ctx.lineWidth = 1.45;
for (let i = 0; i < 15; i++) {
const x = -this.r * 1.14 + i * this.r * 0.16;
const y = this.r * 0.38 + Math.sin(i * 1.7 + this.seed) * 1.8;
const endX = x + Math.cos(-0.55 + i * 0.08) * this.r * 0.74;
const endY = -this.r * 0.30 + Math.sin(time * 0.7 + i) * 2.6;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.quadraticCurveTo(Math.sin(time + i) * 3.2, -this.r * 0.10, endX, endY);
ctx.stroke();
}
ctx.fillStyle = night ? "rgba(42,93,42,0.34)" : "rgba(66,145,47,0.36)";
ctx.beginPath();
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 spriteId = "plushie_bear";
this.plushieSpriteId = spriteId;
const img = getRenderableImage(spriteId, spriteId);
ctx.rotate(Math.sin(time * 2 + this.seed) * 0.05);
if (img) {
const metrics = getImageMetrics(spriteId) || getImageMetrics("smile");
const w = this.r * 3.35;
const h = w * (metrics?.ratio || 1.05);
ctx.drawImage(img, -w * 0.50, -h * 0.62, w, h);
} else {
ctx.fillStyle = "#f7dfc0";
ctx.strokeStyle = "rgba(86,62,48,0.70)";
ctx.lineWidth = 1.7;
ctx.beginPath();
ctx.arc(0, 0, this.r, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "rgba(52,43,38,0.88)";
ctx.beginPath(); ctx.arc(-this.r * 0.28, -this.r * 0.08, 1.5, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(this.r * 0.28, -this.r * 0.08, 1.5, 0, Math.PI * 2); ctx.fill();
}
}
ctx.restore();
}
}
const StructureRegistry = {
defs: {},
register(definition) { this.defs[definition.type] = definition; return definition; },
get(type) { return this.defs[type] || null; },
create(type, owner, x, y, world) {
const definition = this.get(type);
if (!definition) return null;
return definition.create ? definition.create(owner, x, y, world) : new TarinaiStructure(definition, owner, x, y, world);
},
};
function applyStructureNeedShock(owner, safetyGain, fulfillGain, breaker = null) {
if (!owner || owner.dead) return;
if (typeof global.applyNeedShock === "function") global.applyNeedShock(owner, { safety: safetyGain || 0, fulfill: fulfillGain || 0 }, breaker);
else {
owner.needShock = owner.needShock || {};
owner.needShock.safety = Math.max(owner.needShock.safety || 0, safetyGain || 0);
owner.needShock.fulfill = Math.max(owner.needShock.fulfill || 0, fulfillGain || 0);
owner.lastNeedShockBreaker = breaker || null;
}
if (typeof global.triggerNeedShockReaction === "function") {
const reason = breaker ? "\u81ea\u5206\u306e\u3082\u306e\u304c\u58ca\u308c\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b" : "\u81ea\u5206\u306e\u3082\u306e\u304c\u58ca\u308c\u3066\u3044\u308b";
global.triggerNeedShockReaction(owner, breaker, reason);
}
}
function damageNearbyStructures(worldRef, x, y, radius, amount, breaker = null) {
let hit = 0;
for (const structure of worldRef?.nearbyItems?.(x, y, radius + 80) || worldRef?.items || []) {
if (!structure?.isStructure || structure.dead) continue;
const d = distXY(x, y, structure.x, structure.y);
const p = clamp(1 - d / Math.max(1, radius + (structure.r || 12)), 0, 1);
if (p <= 0) continue;
if (structure.damage?.(Math.max(1, amount * (0.35 + p * 0.65)), worldRef, breaker)) hit++;
}
return hit;
}
StructureRegistry.register({
type: "grass_bed",
label: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9",
buildNeed: "fulfill",
buildTime: 4,
maxHp: 100,
materials: { grassMaterial: 4 },
roles: { sleepPlace: true, ownedStructure: true },
needEffects: { food: 0, sleep: 60, health: 5, safety: 25, social: 0, fulfill: 40 },
create(owner, x, y, world) { return new TarinaiStructure(this, owner, x, y, world); },
onUse(user, structure, worldRef, options = {}) {
if (user?.id && !structure.ownerId) {
structure.ownerId = user.id;
structure.attachment = Math.max(structure.attachment || 0, 24);
structure.unownedDecayTimer = 0;
structure.dead = false;
structure.maxHp = Math.max(1, Number(structure.maxHp || 100) || 100);
structure.hp = Math.max(1, Number(structure.hp || 0) || structure.maxHp);
structure.amount = Math.max(0.001, structure.hp);
worldRef?.markItemBucketsDirty?.("grass-bed-owner-adopted");
worldRef?.markTerrainDirty?.("grass-bed-owner-adopted");
worldRef?.emit?.("structure:ownerAdopted", { structure, owner: user });
}
const ownerUse = user?.id && user.id === structure.ownerId;
if (ownerUse) structure.attachment = Math.min(100, (structure.attachment || 0) + 2);
structure.usedCount = (structure.usedCount || 0) + 1;
if (options?.purpose === "sleep" || options?.sleep === true) {
// \u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u306f\u300c\u5bdd\u305f\u77ac\u9593\u300d\u3067\u306f\u306a\u304f\u300c\u8d77\u304d\u305f\u6642\u300d\u306b\u6d88\u3048\u308b\u3002
// \u7761\u7720\u4e2d\u306f\u5bdd\u5834\u6240\u3068\u3057\u3066\u6b8b\u3059\u3053\u3068\u3067\u3001\u898b\u305f\u76ee\u3068\u30db\u30d0\u30fc\u60c5\u5831\u304c\u81ea\u7136\u306b\u306a\u308b\u3002
structure.dead = false;
structure.maxHp = Math.max(1, Number(structure.maxHp || 100) || 100);
structure.hp = Math.max(1, Number(structure.hp || 0) || structure.maxHp);
structure.amount = Math.max(0.001, structure.hp);
structure.unownedDecayTimer = 0;
structure.singleUsePending = true;
structure.singleUseConsumedById = user?.id || "";
structure.singleUseWakeReason = "sleep";
structure.amount = Math.max(0.001, structure.hp || structure.amount || 1);
worldRef?.markItemBucketsDirty?.("grass-bed-sleep-reserved");
worldRef?.markTerrainDirty?.("grass-bed-sleep-reserved");
worldRef?.emit?.("structure:useReserved", { structure, user });
}
return true;
},
onDestroyed(structure, world, breaker) {
applyStructureNeedShock(structure.owner(world), 42, 46, breaker);
},
});
StructureRegistry.register({
type: "plushie",
label: "\u306c\u3044\u3050\u308b\u307f",
buildNeed: "fulfill",
buildTime: 4,
maxHp: 1,
materials: { grassMaterial: 4 },
roles: { carriedObject: true, ownedStructure: true, fulfillObject: true },
needEffects: { food: 0, sleep: 0, health: 0, safety: 5, social: 0, fulfill: 50 },
create(owner, x, y, world) { return new TarinaiStructure(this, owner, x, y, world); },
onUse(user, structure, worldRef) {
if (user?.id === structure.ownerId) structure.attachment = Math.min(100, (structure.attachment || 0) + 1);
structure.usedCount = (structure.usedCount || 0) + 1;
if (user?.id === structure.ownerId) {
structure.carriedById = user.id;
structure.onHead = true;
structure.x = user.x;
structure.y = user.y - Math.max(28, (user.radius || 24) * 1.28);
user.needShock = user.needShock || {};
user.needShock.fulfill = Math.min(0, (user.needShock.fulfill || 0) - 24);
user.needShock.safety = Math.min(0, (user.needShock.safety || 0) - 4);
worldRef?.markSpatialDirty?.("plushie-use-head");
worldRef && (worldRef.drawListDirty = true);
}
return true;
},
onDestroyed(structure, world, breaker) {
applyStructureNeedShock(structure.owner(world), 55, 64, breaker);
},
});
global.TarinaiStructure = TarinaiStructure;
global.StructureRegistry = StructureRegistry;
global.damageNearbyStructures = damageNearbyStructures;
})(typeof window !== "undefined" ? window : globalThis);