247 lines
13 KiB
JavaScript
247 lines
13 KiB
JavaScript
"use strict";
|
|
|
|
(function (global) {
|
|
const Tarinai = global.Tarinai;
|
|
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_action_state.js");
|
|
|
|
function prepareTarinaiIntimidatePresentation(tarinai) {
|
|
if (!tarinai || tarinai.dead) return "intimidate";
|
|
if (!tarinai.intimidateSpriteVariant) {
|
|
tarinai.intimidateSpriteVariant = tarinai.isAcquiredTarinai ? "intimidate_alt" : (Math.random() < 0.5 ? "intimidate_alt" : "intimidate");
|
|
}
|
|
if (tarinai.intimidateSpriteVariant === "intimidate_alt" && !tarinai._intimidateAltAnnounced) {
|
|
tarinai._intimidateAltAnnounced = true;
|
|
tarinai.bubble?.("ぷきゅー!", 1.0, "rgba(92,62,34,0.82)", { force: true });
|
|
const audio = global.TarinaiAudio;
|
|
if (audio?.playSample) audio.playSample("voice_intimidate_alt", 0.9, "voice") || audio.play?.("voice_intimidate_alt", { category: "voice", minGap: 0.9 });
|
|
else audio?.play?.("voice_intimidate_alt", { category: "voice", minGap: 0.9 });
|
|
}
|
|
return tarinai.intimidateSpriteVariant;
|
|
}
|
|
global.prepareTarinaiIntimidatePresentation = prepareTarinaiIntimidatePresentation;
|
|
|
|
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
|
|
setActionState(state, opts = {}) {
|
|
if (this.dead) return false;
|
|
const wasSleeping = this.state === "sleep" || this.sleeping;
|
|
const previousState = this.state;
|
|
const nextState = state || "idle";
|
|
this.state = nextState;
|
|
if (nextState === "intimidate" || nextState === "ant_intimidate") prepareTarinaiIntimidatePresentation(this);
|
|
else if ((previousState === "intimidate" || previousState === "ant_intimidate") && nextState !== previousState) {
|
|
this.intimidateSpriteVariant = "";
|
|
this._intimidateAltAnnounced = false;
|
|
}
|
|
if ("target" in opts) this.target = opts.target;
|
|
if (opts.reason != null) this.thought = String(opts.reason || "");
|
|
if (opts.clearTarget) this.target = null;
|
|
const wakingNow = wasSleeping && (Boolean(opts.wake) || (nextState !== "sleep" && opts.sleeping === false));
|
|
if (wakingNow) this.consumePendingGrassBedOnWake?.(opts.reason || "wake");
|
|
if (opts.sleeping != null) this.sleeping = Boolean(opts.sleeping);
|
|
if (opts.wake) {
|
|
this.sleeping = false;
|
|
this.sleepSession = null;
|
|
if (this.insideNestBoxId && this.leaveNestBox) this.leaveNestBox(this.world?.nestBoxById?.(this.insideNestBoxId));
|
|
this.insideNestBoxId = "";
|
|
}
|
|
if (nextState !== "sleep" && opts.sleeping === false) this.sleepSession = null;
|
|
|
|
const reason = String(opts.reason ?? this.thought ?? "");
|
|
if (nextState === "idle") {
|
|
clearBehavior(this, reason || "\u5f85\u3063\u3066\u3044\u308b");
|
|
return true;
|
|
}
|
|
|
|
const stateActionIds = {
|
|
seek_food: "eat_food",
|
|
eat: "eat_food",
|
|
seek_water: "drink_water",
|
|
seek_bed: "sleep_in_bed",
|
|
sleep: "sleep_anywhere",
|
|
panic: "panic_escape",
|
|
fight: "fight_rival",
|
|
intimidate: "intimidate_enemy",
|
|
birth_ritual: "birth_ritual",
|
|
sunbath: "sunbath",
|
|
wander: "wander_lightly",
|
|
play_ball: "play",
|
|
seek_material: "seek_material",
|
|
seek_enemy: "fight_rival",
|
|
build: "build_structure",
|
|
};
|
|
const actionId = String(opts.actionId || opts.behaviorId || stateActionIds[nextState] || nextState);
|
|
const target = opts.clearTarget ? null : ("target" in opts ? opts.target : this.target);
|
|
const phase = opts.phase || (nextState === "sleep" || nextState === "eat" || nextState === "fight" || nextState === "intimidate" || nextState === "birth_ritual" || nextState === "sunbath" ? "acting" : "approaching");
|
|
setBehaviorText(this, {
|
|
need: opts.need || null,
|
|
subNeed: opts.subNeed || "",
|
|
actionId,
|
|
actionLabel: opts.actionLabel || reason || nextState,
|
|
reasonText: reason || opts.actionLabel || nextState,
|
|
target,
|
|
phase,
|
|
source: opts.source || "state",
|
|
forced: opts.forced || null,
|
|
forcedId: opts.forcedId || "",
|
|
sourceReasonText: opts.sourceReasonText || opts.forcedReasonText || "",
|
|
causeText: opts.causeText || "",
|
|
});
|
|
return true;
|
|
},
|
|
|
|
beginEating(item = null, reason = "") {
|
|
const text = reason || (item?.type && typeof toolLabel === "function" ? `${toolLabel(item.type)}\u3092\u98df\u3079\u3066\u3044\u308b` : "\u98df\u3079\u3066\u3044\u308b");
|
|
const ok = this.setActionState("eat", {
|
|
target: item || null,
|
|
reason: text,
|
|
sleeping: false,
|
|
});
|
|
setBehaviorText(this, { need: "food", actionId: "eat_food", actionLabel: "\u98df\u3079\u3066\u3044\u308b", reasonText: text, target: item || null, phase: "perform", source: "behavior" });
|
|
return ok;
|
|
},
|
|
|
|
currentLodgedPin() {
|
|
return lodgedPinFor(this, this.world);
|
|
},
|
|
|
|
currentLodgedPinBehavior() {
|
|
const pin = this.currentLodgedPin ? this.currentLodgedPin() : null;
|
|
return pinBehaviorFor(pin?.type);
|
|
},
|
|
|
|
hasLodgedPinEffect(effectKey = "") {
|
|
return Boolean(this.currentLodgedPinBehavior?.()?.[effectKey]);
|
|
},
|
|
|
|
goIdle(reason = "") {
|
|
return this.setActionState("idle", { target: null, reason: reason || this.thought || "\u5f85\u3063\u3066\u3044\u308b", wake: this.state === "sleep" || this.sleeping, sleeping: false });
|
|
},
|
|
|
|
startSleeping(target = null, reason = "") {
|
|
const now = this.world?.time || 0;
|
|
if (!this.sleepSession) {
|
|
const inFurniture = Boolean(target);
|
|
this.sleepSession = {
|
|
startedAt: now,
|
|
minDuration: inFurniture ? rand(16, 28) : rand(10, 18),
|
|
targetEnergy: inFurniture ? rand(84, 94) : rand(74, 86),
|
|
maxDuration: inFurniture ? rand(42, 68) : rand(28, 48),
|
|
targetId: target?.id || null,
|
|
consumesGrassBedOnWake: target?.type === "grass_bed",
|
|
};
|
|
}
|
|
if (target?.type === "grass_bed") {
|
|
this.sleepSession.targetId = target.id || this.sleepSession.targetId || null;
|
|
this.sleepSession.consumesGrassBedOnWake = true;
|
|
this.pendingGrassBedWakeId = target.id || this.pendingGrassBedWakeId || "";
|
|
target.singleUsePending = true;
|
|
target.singleUseConsumedById = this.id || target.singleUseConsumedById || "";
|
|
}
|
|
const targetIsNestContainer = Boolean(target && global.TarinaiToolRuntime?.isNestContainerItem?.(target, { alive: false }));
|
|
if (target && !targetIsNestContainer) {
|
|
this._sleepExternalMotionUntil = 0;
|
|
this._sleepExternalWakeSuppressUntil = Math.max(Number(this._sleepExternalWakeSuppressUntil || 0) || 0, now + 2.4);
|
|
this.vx = 0;
|
|
this.vy = 0;
|
|
this.impulseVx = 0;
|
|
this.impulseVy = 0;
|
|
}
|
|
const text = reason || (target?.type === "pipe" ? "\u571f\u7ba1\u306e\u4e2d\u3067\u7720\u3063\u3066\u3044\u308b" : (target?.type === "nest_box" ? "\u5de3\u7bb1\u306e\u4e2d\u3067\u7720\u3063\u3066\u3044\u308b" : "\u7720\u3063\u3066\u3044\u308b"));
|
|
setBehaviorText(this, { need: "sleep", actionId: target ? "sleep_in_bed" : "sleep_anywhere", actionLabel: "\u7720\u3063\u3066\u3044\u308b", reasonText: text, target, phase: "perform", source: "behavior" });
|
|
return this.setActionState("sleep", { target, reason: text, sleeping: true });
|
|
},
|
|
|
|
consumePendingGrassBedOnWake(reason = "wake") {
|
|
const worldRef = this.world || null;
|
|
const session = this.sleepSession || null;
|
|
const sessionTargetId = session?.targetId || this.pendingGrassBedWakeId || null;
|
|
const bySession = sessionTargetId && worldRef?.itemById ? worldRef.itemById(sessionTargetId) : null;
|
|
const byItems = sessionTargetId ? (worldRef?.items || []).find(it => it && it.id === sessionTargetId) : null;
|
|
const candidates = [this.target, bySession, byItems];
|
|
let bed = candidates.find(it => it && !it.dead && it.type === "grass_bed" && (it.singleUsePending || it.singleUseConsumedById === this.id || (session?.consumesGrassBedOnWake && it.id === sessionTargetId)));
|
|
if (!bed && (session?.consumesGrassBedOnWake || sessionTargetId)) {
|
|
bed = (worldRef?.items || []).find(it => it && !it.dead && it.type === "grass_bed"
|
|
&& (it.id === sessionTargetId || (it.singleUsePending && (it.singleUseConsumedById === this.id || it.ownerId === this.id))));
|
|
}
|
|
if (!bed) return false;
|
|
const now = Number(worldRef?.time || 0) || 0;
|
|
const started = Number(session?.startedAt);
|
|
const sleptFor = Number.isFinite(started) ? Math.max(0, now - started) : 0;
|
|
const wasActuallySleeping = Boolean((this.state === "sleep" || this.sleeping || session) && session?.consumesGrassBedOnWake);
|
|
// Reserve-on-use must not consume the simple bed. It is consumed only by
|
|
// a real wake after sleep has had time to settle; immediate collision or
|
|
// movement wakeups just release the reservation and leave the bed intact.
|
|
if (!wasActuallySleeping || sleptFor < 2) {
|
|
if (bed.singleUseConsumedById === this.id) bed.singleUseConsumedById = "";
|
|
if (bed.singleUsePending && (!bed.singleUseConsumedById || bed.singleUseConsumedById === this.id)) bed.singleUsePending = false;
|
|
this.pendingGrassBedWakeId = "";
|
|
if (session) session.consumesGrassBedOnWake = false;
|
|
return false;
|
|
}
|
|
this.pendingGrassBedWakeId = "";
|
|
return window.TarinaiStructureLifecycle.consumeGrassBedOnWake(worldRef, bed, this, reason) || false;
|
|
},
|
|
|
|
forceBehavior(actionId = "", opts = {}) {
|
|
return queueForcedTarinaiBehavior(this, actionId, opts);
|
|
},
|
|
|
|
enterPanic(opts = {}) {
|
|
if (this.dead) return false;
|
|
const profile = this.personalityProfile ? this.personalityProfile() : { fear: 1 };
|
|
const fearMul = opts.fearMultiplier ?? profile.fear ?? 1;
|
|
const threat = opts.threat ?? opts.target ?? null;
|
|
if (threat && threat !== this && (threat.id || threat.type || threat.kind || threat.name)) this.lastPanicThreat = threat;
|
|
const safetyShock = Math.max(40, (opts.fearTimer ?? opts.fear ?? 0.55) * 32 * fearMul, Number(opts.stress || 0) * 1.8);
|
|
const healthShock = Math.max(0, Number(opts.hurtTimer || 0) > 0 ? 45 : 0);
|
|
if (typeof applyNeedShock === "function") {
|
|
applyNeedShock(this, { safety: safetyShock, health: healthShock }, threat);
|
|
} else {
|
|
this.needShock = this.needShock || {};
|
|
this.needShock.safety = Math.max(this.needShock.safety || 0, safetyShock);
|
|
if (healthShock > 0) this.needShock.health = Math.max(this.needShock.health || 0, healthShock);
|
|
}
|
|
let handled = false;
|
|
if (typeof triggerNeedShockReaction === "function") handled = triggerNeedShockReaction(this, threat, opts.reason || "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b");
|
|
if (!handled) {
|
|
let target = null;
|
|
if ("target" in opts) target = opts.target;
|
|
else if (opts.destination) target = opts.destination;
|
|
else if (this.panicDestination) target = this.panicDestination(threat, !!opts.forceDestination);
|
|
const panicReason = opts.reason || this.thought || "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b";
|
|
this.setActionState("panic", {
|
|
target,
|
|
reason: panicReason,
|
|
wake: opts.wake !== false,
|
|
sleeping: false,
|
|
});
|
|
const causeText = this.lastPanicDetail || (String(panicReason).includes("\u3001") ? String(panicReason).split("\u3001")[0].replace(/\u3066$/, "\u305f") : "\u6016\u3044");
|
|
setBehaviorText(this, { need: "safety", actionId: "panic_escape", actionLabel: "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", reasonText: panicReason, causeText, target, phase: "perform", source: "behavior" });
|
|
this.fearTimer = Math.max(this.fearTimer || 0, 2.4);
|
|
}
|
|
if (Number.isFinite(opts.surpriseTimer)) this.surpriseTimer = Math.max(this.surpriseTimer || 0, opts.surpriseTimer);
|
|
if (Number.isFinite(opts.hurtTimer)) this.hurtTimer = Math.max(this.hurtTimer || 0, opts.hurtTimer);
|
|
if (Number.isFinite(opts.awakeLockTimer)) this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, opts.awakeLockTimer);
|
|
if (Number.isFinite(opts.fallTimer)) {
|
|
this.fallTimer = Math.max(this.fallTimer || 0, opts.fallTimer);
|
|
this.fallMax = Math.max(this.fallMax || 0, this.fallTimer);
|
|
}
|
|
if (opts.bubble && !this.isAcquiredTarinai) this.bubble?.(opts.bubble, opts.bubbleCooldown ?? 0.9, opts.bubbleColor || "rgba(84,66,86,0.80)");
|
|
if (this.isAcquiredTarinai) {
|
|
const now = Number(this.world?.time || 0) || 0;
|
|
if (now >= Number(this.ochibiPanicVoiceAt || 0)) {
|
|
this.ochibiPanicVoiceAt = now + 2.4;
|
|
if (this.bubble?.("きょわいのだ...", 1.8, "rgba(84,66,86,0.80)", { force: true })) {
|
|
const a = global.TarinaiAudio;
|
|
if (a?.playSample) a.playSample("voice_ochibi_panic_005", 1.0, "voice") || a.play?.("voice_ochibi_panic_005", { category: "voice", minGap: 1.0 });
|
|
}
|
|
}
|
|
}
|
|
this.lastPanicCause = opts.cause || opts.reason || this.thought || "panic";
|
|
this.lastPanicAt = this.world?.time || 0;
|
|
if (this.state === "panic" && !Number.isFinite(this.panicStartedAt)) this.panicStartedAt = this.lastPanicAt;
|
|
if (this.state === "panic") this.panicHardStopAt = Math.max(this.panicHardStopAt || 0, this.lastPanicAt + 7.5);
|
|
return true;
|
|
},
|
|
}));
|
|
})(typeof window !== "undefined" ? window : globalThis);
|