547 lines
28 KiB
JavaScript
547 lines
28 KiB
JavaScript
"use strict";
|
|
|
|
(function (global) {
|
|
const NEED_KEYS = Object.freeze(["food", "sleep", "health", "safety", "social", "fulfill"]);
|
|
const NEED_LABELS = Object.freeze({
|
|
food: "\u6442\u990c",
|
|
sleep: "\u7761\u7720",
|
|
health: "\u5065\u5eb7",
|
|
safety: "\u5b89\u5168",
|
|
social: "\u95a2\u4fc2",
|
|
fulfill: "\u5145\u8db3",
|
|
});
|
|
const NEED_PHRASES = Object.freeze({
|
|
food: "\u304a\u306a\u304b\u304c\u3059\u3044\u3066\u3044\u308b",
|
|
sleep: "\u306d\u3080\u3044",
|
|
health: "\u8abf\u5b50\u304c\u60aa\u3044",
|
|
safety: "\u3042\u3076\u306a\u3044",
|
|
social: "\u3060\u308c\u304b\u304c\u6c17\u306b\u306a\u308b",
|
|
fulfill: "\u81ea\u5206\u306e\u5834\u6240\u304c\u6c17\u306b\u306a\u308b",
|
|
});
|
|
const NEED_PRIORITY = Object.freeze(["safety", "health", "food", "sleep", "social", "fulfill"]);
|
|
|
|
function createId(prefix = "id") {
|
|
if (global.crypto?.randomUUID) return `${prefix}_${global.crypto.randomUUID()}`;
|
|
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
}
|
|
|
|
function createDefaultNeeds() {
|
|
return { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 };
|
|
}
|
|
|
|
function quantizeNeed(value) {
|
|
return Math.max(0, Math.min(100, Math.round((Number(value) || 0) / 10) * 10));
|
|
}
|
|
|
|
function getNeedDisplayValue(value) {
|
|
return Math.round(quantizeNeed(value) / 10);
|
|
}
|
|
|
|
function personalityValue(t, key) {
|
|
if (typeof global.effectivePersonalityValue === "function") return Number(global.effectivePersonalityValue(t, key)) || 0;
|
|
return Number(t?.currentPersonality?.[key] ?? t?.birthPersonality?.[key] ?? 0) || 0;
|
|
}
|
|
|
|
function needPressure(value) {
|
|
const x = clamp((Number(value) || 0) / 100, 0, 1);
|
|
return x * x * 100;
|
|
}
|
|
|
|
function deriveStressFromNeeds(needs = {}) {
|
|
return clamp(
|
|
needPressure(needs.food) * 0.10 +
|
|
needPressure(needs.sleep) * 0.12 +
|
|
needPressure(needs.health) * 0.25 +
|
|
needPressure(needs.safety) * 0.30 +
|
|
needPressure(needs.social) * 0.08 +
|
|
needPressure(needs.fulfill) * 0.15,
|
|
0,
|
|
130
|
|
);
|
|
}
|
|
|
|
function itemRolesFor(item) {
|
|
if (!item) return {};
|
|
if (!item.roles) item.roles = {};
|
|
const type = String(item.type || "");
|
|
const roles = item.roles;
|
|
if (["sweet", "food", "grass", "ant_corpse", "zunchi", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"].includes(type)) roles.food = true;
|
|
if (type === "water" || type === "water_bowl") roles.drink = true;
|
|
if (["sweet", "protein", "water", "water_bowl", "zunda_juice"].includes(type)) roles.medicine = true;
|
|
if (type === "bed" || type === "nest_box") roles.sleepPlace = true;
|
|
if (["firecracker", "genkotsu", "pushpin", "oshibyo", "splat", "zunchi", "ant_nest", "cannon"].includes(type)) roles.danger = true;
|
|
if (type === "grass") roles.grassMaterial = true;
|
|
if (type === "ball") roles.playObject = true;
|
|
if (item.isStructure) roles.ownedStructure = true;
|
|
return roles;
|
|
}
|
|
|
|
function ensureItemNeedEffects(item) {
|
|
if (!item) return createDefaultNeeds();
|
|
if (!item.needEffects) item.needEffects = createDefaultNeeds();
|
|
return item.needEffects;
|
|
}
|
|
|
|
function findNearestItemWithRole(world, t, role, maxDist = 640, predicate = null) {
|
|
let best = null;
|
|
let bestD = maxDist;
|
|
const items = Number.isFinite(maxDist) && world?.nearbyItems ? world.nearbyItems(t.x, t.y, maxDist) : (world?.items || []);
|
|
for (const it of items) {
|
|
if (!it || it.dead || (it.amount != null && it.amount <= 0)) continue;
|
|
const roles = itemRolesFor(it);
|
|
if (!roles[role]) continue;
|
|
if (predicate && !predicate(it)) continue;
|
|
if (t?.shouldAvoidTarget?.(it)) continue;
|
|
const d = distXY(t.x, t.y, it.x, it.y);
|
|
if (d < bestD) { best = it; bestD = d; }
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function findNearestSleepPlace(world, t, maxDist = 560) {
|
|
const own = findOwnedStructure(world, t, "grass_bed", maxDist);
|
|
return own || findNearestItemWithRole(world, t, "sleepPlace", maxDist);
|
|
}
|
|
|
|
function findNearbyDanger(world, t, maxDist = 180) {
|
|
const ant = (world?.nearbyAnts?.(t.x, t.y, maxDist, true) || []).find(a => a && !a.dead);
|
|
if (ant) return ant;
|
|
return findNearestItemWithRole(world, t, "danger", maxDist);
|
|
}
|
|
|
|
function findNearbyMaterial(world, t, materialRole, maxDist = 520) {
|
|
return findNearestItemWithRole(world, t, materialRole, maxDist);
|
|
}
|
|
|
|
function ownedStructures(world, t, type = "") {
|
|
return (world?.items || []).filter(it => it && !it.dead && it.isStructure && it.ownerId === t?.id && (!type || it.type === type));
|
|
}
|
|
|
|
function findOwnedStructure(world, t, type = "", maxDist = Infinity) {
|
|
let best = null;
|
|
let bestD = maxDist;
|
|
for (const it of ownedStructures(world, t, type)) {
|
|
const d = distXY(t.x, t.y, it.x, it.y);
|
|
if (d < bestD) { best = it; bestD = d; }
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function hasOwnedStructure(world, t, type) {
|
|
return Boolean(findOwnedStructure(world, t, type));
|
|
}
|
|
|
|
function canActNormally(t) {
|
|
return !t.dead && t.state !== "sleep" && t.state !== "fight" && t.state !== "panic" && t.state !== "intimidate" && t.birthRitualTimer <= 0.04 && t.fightTimer <= 0.04 && t.defeatedTimer <= 0.04;
|
|
}
|
|
|
|
function updateAttachedStructures(world, t) {
|
|
for (const s of ownedStructures(world, t)) {
|
|
if (s.type !== "plushie" || s.carriedById !== t.id || !s.onHead) continue;
|
|
s.x = t.x + (t.facingDir ? t.facingDir() : 1) * t.radius * 0.12;
|
|
s.y = t.y - t.radius * 1.10;
|
|
s.amount = Math.max(1, s.hp || 1);
|
|
if (t.needs) t.needShock = { ...(t.needShock || {}), fulfill: Math.max(0, Number(t.needShock?.fulfill || 0) - 0.08) };
|
|
}
|
|
}
|
|
|
|
function updateNeeds(t, world) {
|
|
const prev = t.needsInitialized && t.needs ? { ...t.needs } : null;
|
|
const pAgg = personalityValue(t, "aggression");
|
|
const pOpen = personalityValue(t, "openness");
|
|
const pSoc = personalityValue(t, "sociability");
|
|
const pNeu = personalityValue(t, "neuroticism");
|
|
const raw = createDefaultNeeds();
|
|
raw.food = clamp((t.hunger || 0) * 0.88 + (findNearestItemWithRole(world, t, "drink", 120) ? 6 : 0), 0, 100);
|
|
const dayP = world?.dayProgress?.() ?? 0.5;
|
|
const night = dayP >= 0.62 || dayP < 0.18 ? 12 : 0;
|
|
raw.sleep = clamp((100 - (t.energy || 0)) * 0.98 + night, 0, 100);
|
|
raw.health = clamp(
|
|
(t.zunchiDisease ? 38 : 0) + (t.sleepDisease ? 22 : 0) + (t.explosionDisease ? 30 : 0) + (t.fightDisease ? 34 : 0) +
|
|
clamp((100 - (t.energy || 100)) * 0.22, 0, 26) + clamp((t.zunchiStain || 0) * 0.34, 0, 34) + Number(t.needShock?.health || 0),
|
|
0,
|
|
100
|
|
);
|
|
const danger = findNearbyDanger(world, t, 210);
|
|
const recentDamage = (world?.time || 0) - (t.lastDamageAt || -999) < 8 ? clamp((t.lastDamageAmount || 0) * 2.4, 0, 52) : 0;
|
|
const fear = clamp((t.fearTimer || 0) * 22, 0, 45);
|
|
raw.safety = clamp((danger ? 42 : 0) + recentDamage + fear + Number(t.needShock?.safety || 0), 0, 100);
|
|
raw.social = clamp((t.loneliness || 0) * (0.78 + Math.max(0, pSoc) * 0.16) + (t.reproductionTimer <= 4 ? 18 : 0), 0, 100);
|
|
const hasBed = hasOwnedStructure(world, t, "grass_bed");
|
|
const hasPlushie = hasOwnedStructure(world, t, "plushie");
|
|
const play = findNearestItemWithRole(world, t, "playObject", 280) ? 10 : 0;
|
|
raw.fulfill = clamp((hasBed ? 0 : 26) + (hasPlushie ? 0 : 20) + play + Math.max(0, pOpen) * 16 + Number(t.needShock?.fulfill || 0), 0, 100);
|
|
raw.safety *= clamp(1 - Math.max(0, pAgg) * 0.12 + Math.max(0, pNeu) * 0.16, 0.78, 1.22);
|
|
raw.health *= clamp(1 + Math.max(0, pNeu) * 0.12, 0.9, 1.16);
|
|
raw.social *= clamp(1 + Math.max(0, pSoc) * 0.12, 0.9, 1.16);
|
|
raw.fulfill *= clamp(1 + Math.max(0, pOpen) * 0.12, 0.9, 1.18);
|
|
const needs = createDefaultNeeds();
|
|
for (const key of NEED_KEYS) needs[key] = quantizeNeed(raw[key]);
|
|
t.previousNeeds = prev;
|
|
t.needs = needs;
|
|
t.needsInitialized = true;
|
|
t.stress = deriveStressFromNeeds(needs);
|
|
handleNeedShockReaction(t, prev, needs);
|
|
return needs;
|
|
}
|
|
|
|
function handleNeedShockReaction(t, prev, needs, breaker = null) {
|
|
if (!prev || !needs || t.birthRitualTimer > 0.04 || t.fightTimer > 0.04) return false;
|
|
const safetyDelta = (needs.safety || 0) - (prev.safety || 0);
|
|
const healthDelta = (needs.health || 0) - (prev.health || 0);
|
|
const fulfillDelta = (needs.fulfill || 0) - (prev.fulfill || 0);
|
|
if (safetyDelta < 40 && healthDelta < 45 && fulfillDelta < 50) return false;
|
|
const attacker = breaker || t.needShockBreaker || null;
|
|
if (attacker && personalityValue(t, "aggression") > 0.6) {
|
|
t.intimidateTimer = Math.max(t.intimidateTimer || 0, 2.6);
|
|
t.intimidateTargetId = attacker.id || null;
|
|
t.setActionState?.("intimidate", { target: attacker, reason: "\u5927\u4e8b\u306a\u3082\u306e\u3092\u58ca\u3055\u308c\u3066\u5a01\u5687\u3057\u3066\u3044\u308b", wake: true });
|
|
} else {
|
|
t.panicTimer = Math.max(t.panicTimer || 0, 3.0);
|
|
t.enterPanic?.({ target: t.panicDestination?.(attacker, true) || null, reason: "\u304a\u3069\u308d\u3044\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 0.8, wake: true, cause: "need_shock" });
|
|
}
|
|
t.needShockBreaker = null;
|
|
return true;
|
|
}
|
|
|
|
function addNeedShock(t, values = {}, breaker = null) {
|
|
if (!t) return;
|
|
const shock = { ...(t.needShock || {}) };
|
|
for (const key of NEED_KEYS) shock[key] = clamp(Number(shock[key] || 0) + Number(values[key] || 0), 0, 100);
|
|
t.needShock = shock;
|
|
t.needShockBreaker = breaker || t.needShockBreaker || null;
|
|
const prev = t.needsInitialized && t.needs ? { ...t.needs } : null;
|
|
updateNeeds(t, t.world);
|
|
handleNeedShockReaction(t, prev, t.needs, breaker);
|
|
}
|
|
|
|
function decayNeedShock(t, dt) {
|
|
if (!t?.needShock) return;
|
|
for (const key of NEED_KEYS) t.needShock[key] = Math.max(0, Number(t.needShock[key] || 0) - dt * (key === "fulfill" ? 2.2 : 2.8));
|
|
}
|
|
|
|
function chooseTopNeedRandom(needs) {
|
|
const maxNeed = Math.max(...NEED_KEYS.map(key => Number(needs?.[key] || 0)));
|
|
const tiedNeeds = NEED_KEYS.filter(key => Number(needs?.[key] || 0) === maxNeed);
|
|
return tiedNeeds[Math.floor(Math.random() * tiedNeeds.length)] || "fulfill";
|
|
}
|
|
|
|
function weightedRandom(items) {
|
|
const total = items.reduce((sum, a) => sum + Math.max(0.01, Number(a.weight || 1)), 0);
|
|
let pickPoint = Math.random() * total;
|
|
for (const item of items) {
|
|
pickPoint -= Math.max(0.01, Number(item.weight || 1));
|
|
if (pickPoint <= 0) return item;
|
|
}
|
|
return items[items.length - 1] || null;
|
|
}
|
|
|
|
function buildReasonText(chosenNeed, tiedNeeds, action) {
|
|
const tied = Array.isArray(tiedNeeds) ? tiedNeeds : [chosenNeed];
|
|
const ignored = NEED_PRIORITY.find(key => key !== chosenNeed && tied.includes(key));
|
|
const phrase = action?.phrase || "\u5f85\u3063\u305f";
|
|
if (ignored) return `${NEED_PHRASES[ignored]}\u3051\u3069\u3001${phrase}\u3002`;
|
|
return `${NEED_PHRASES[chosenNeed]}\u306e\u3067\u3001${phrase}\u3002`;
|
|
}
|
|
|
|
function moveTowardOrUse(t, target, state, label) {
|
|
if (!target) return false;
|
|
if (distXY(t.x, t.y, target.x, target.y) > Math.max(34, (target.r || 12) + t.radius * 0.75)) {
|
|
t.setActionState?.(state, { target, reason: label });
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function createStructure(world, owner, type, x, y) {
|
|
const def = StructureRegistry[type];
|
|
if (!def) return null;
|
|
const structure = def.create(owner, x, y, world);
|
|
structure.update = updateStructure;
|
|
structure.draw = drawStructure;
|
|
structure.damage = damageStructure;
|
|
world.items.push(structure);
|
|
world.itemCounts[structure.type] = (world.itemCounts[structure.type] || 0) + 1;
|
|
world.markTerrainDirty?.(`structure:${structure.type}`);
|
|
world.rebuildSpatial?.(true);
|
|
return structure;
|
|
}
|
|
|
|
function consumeGrassMaterial(grass) {
|
|
if (!grass || grass.dead) return false;
|
|
grass.amount = Math.max(0, Number(grass.amount || 0) - 34);
|
|
grass.growth = clamp((grass.growth || 0.5) - 0.24, 0.04, 1.2);
|
|
if (grass.amount <= 1) grass.dead = true;
|
|
return true;
|
|
}
|
|
|
|
function finishBuild(t, type, grass) {
|
|
const world = t.world;
|
|
const x = clamp(t.x + rand(-18, 18), CONFIG.worldPadding, world.w - CONFIG.worldPadding);
|
|
const y = clamp(t.y + rand(10, 30), CONFIG.worldPadding, world.h - CONFIG.worldPadding);
|
|
consumeGrassMaterial(grass);
|
|
const s = createStructure(world, t, type, x, y);
|
|
if (s) {
|
|
t.buildTimer = 0;
|
|
t.buildType = "";
|
|
t.buildMaterialId = "";
|
|
t.needShock = { ...(t.needShock || {}), fulfill: Math.max(0, Number(t.needShock?.fulfill || 0) - 26) };
|
|
StructureRegistry[type].onUse?.(t, s, world);
|
|
}
|
|
return Boolean(s);
|
|
}
|
|
|
|
function runBuildAction(t, type, label) {
|
|
const grass = findNearbyMaterial(t.world, t, "grassMaterial", 520);
|
|
if (!grass) return false;
|
|
if (moveTowardOrUse(t, grass, "seek_food", label)) {
|
|
t.buildType = type;
|
|
t.buildMaterialId = grass.id;
|
|
return true;
|
|
}
|
|
t.buildType = type;
|
|
t.buildMaterialId = grass.id;
|
|
t.buildTimer = Math.max(0, Number(t.buildTimer || 0)) + 1;
|
|
t.setActionState?.("idle", { target: grass, reason: label });
|
|
if (t.buildTimer >= (StructureRegistry[type]?.buildTime || 4)) return finishBuild(t, type, grass);
|
|
return true;
|
|
}
|
|
|
|
const TARINAI_ACTIONS = [
|
|
{ id: "eat_food", need: "food", label: "\u98df\u3079\u7269\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u98df\u3079\u305f", weight: 50,
|
|
condition: (t, world) => Boolean(findNearestItemWithRole(world, t, "food", 620, it => !t.foodItemHasServingLeft || t.foodItemHasServingLeft(it))),
|
|
run(t, world) { const item = findNearestItemWithRole(world, t, "food", 620, it => !t.foodItemHasServingLeft || t.foodItemHasServingLeft(it)); return moveTowardOrUse(t, item, "seek_food", this.label) || t.beginEating?.(item, this.label); } },
|
|
{ id: "drink_water", need: "food", label: "\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u6c34\u3092\u98f2\u3093\u3060", weight: 22,
|
|
condition: (t, world) => Boolean(findNearestItemWithRole(world, t, "drink", 620)),
|
|
run(t, world) { const item = findNearestItemWithRole(world, t, "drink", 620); return moveTowardOrUse(t, item, "seek_water", this.label) || (t.setActionState?.("seek_water", { target: item, reason: this.label }), true); } },
|
|
{ id: "sleep_in_bed", need: "sleep", label: "\u5bdd\u5e8a\u3078\u5411\u304b\u3063\u3066\u3044\u308b", phrase: "\u5bdd\u5e8a\u3078\u623b\u3063\u305f", weight: 42,
|
|
condition: (t, world) => Boolean(findNearestSleepPlace(world, t, 620)),
|
|
run(t, world) { const bed = findNearestSleepPlace(world, t, 620); if (moveTowardOrUse(t, bed, "seek_bed", this.label)) return true; return t.startSleeping?.(bed, this.label); } },
|
|
{ id: "sleep_anywhere", need: "sleep", label: "\u305d\u306e\u5834\u3067\u4f11\u3093\u3067\u3044\u308b", phrase: "\u4f11\u3093\u3060", weight: 20, condition: () => true,
|
|
run(t) { return t.startSleeping?.(null, this.label); } },
|
|
{ id: "use_medicine", need: "health", label: "\u4f53\u306b\u3088\u3055\u305d\u3046\u306a\u3082\u306e\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u624b\u5f53\u3066\u3057\u305f", weight: 35,
|
|
condition: (t, world) => Boolean(findNearestItemWithRole(world, t, "medicine", 620)),
|
|
run(t, world) { const item = findNearestItemWithRole(world, t, "medicine", 620); return moveTowardOrUse(t, item, itemRolesFor(item).drink ? "seek_water" : "seek_food", this.label) || t.beginEating?.(item, this.label); } },
|
|
{ id: "rest_to_recover", need: "health", label: "\u4f53\u3092\u4f11\u3081\u3066\u3044\u308b", phrase: "\u4f11\u3093\u3060", weight: 20, condition: () => true,
|
|
run(t) { t.setActionState?.("idle", { target: null, reason: this.label }); t.energy = clamp((t.energy || 0) + 1.0, 0, 100); return true; } },
|
|
{ id: "flee", need: "safety", label: "\u96e2\u308c\u308b\u5834\u6240\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u9003\u3052\u305f", weight: 32,
|
|
condition: (t, world) => Boolean(findNearbyDanger(world, t, 260)),
|
|
run(t, world) { const danger = findNearbyDanger(world, t, 260); t.setActionState?.("panic", { target: t.panicDestination?.(danger, true) || null, reason: this.label, wake: true }); t.panicTimer = Math.max(t.panicTimer || 0, 2.0); return true; } },
|
|
{ id: "hide_at_owned_structure", need: "safety", label: "\u81ea\u5206\u306e\u5834\u6240\u3078\u623b\u308d\u3046\u3068\u3057\u3066\u3044\u308b", phrase: "\u81ea\u5206\u306e\u5bdd\u5e8a\u3078\u623b\u3063\u305f", weight: 28,
|
|
condition: (t, world) => Boolean(findOwnedStructure(world, t, "grass_bed", 760)),
|
|
run(t, world) { const bed = findOwnedStructure(world, t, "grass_bed", 760); if (moveTowardOrUse(t, bed, "seek_bed", this.label)) return true; StructureRegistry.grass_bed.onUse(t, bed, world); return t.startSleeping?.(bed, this.label); } },
|
|
{ id: "approach_friend", need: "social", label: "\u4ef2\u9593\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b", phrase: "\u4ef2\u9593\u306b\u8fd1\u3065\u3044\u305f", weight: 38,
|
|
condition: (t, world) => Boolean(world.nearestOther?.(t, 560)),
|
|
run(t, world) { const other = world.nearestOther?.(t, 560); t.setActionState?.(t.parentToFollow?.() === other ? "follow_parent" : "seek_friend", { target: other, reason: this.label }); return true; } },
|
|
{ id: "intimidate_enemy", need: "social", label: "\u6c17\u306b\u306a\u308b\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b", phrase: "\u5a01\u5687\u3057\u305f", weight: 8,
|
|
condition: (t, world) => personalityValue(t, "aggression") > 0.45 && Boolean(world.nearestOther?.(t, 220)),
|
|
run(t, world) { const other = world.nearestOther?.(t, 220); t.intimidateTimer = Math.max(t.intimidateTimer || 0, 2.0); t.intimidateTargetId = other?.id || null; t.setActionState?.("intimidate", { target: other, reason: this.label }); return true; } },
|
|
{ id: "play", need: "fulfill", label: "\u904a\u3093\u3067\u3044\u308b", phrase: "\u904a\u3093\u3060", weight: 26,
|
|
condition: (t, world) => Boolean(findNearestItemWithRole(world, t, "playObject", 460)),
|
|
run(t, world) { const ball = findNearestItemWithRole(world, t, "playObject", 460); t.setActionState?.("play_ball", { target: ball, reason: this.label }); t.goodMode = "smile"; return true; } },
|
|
{ id: "build_grass_bed", need: "fulfill", label: "\u8349\u306e\u7c21\u6613\u30d9\u30c3\u30c9\u3092\u4f5c\u3063\u3066\u3044\u308b", phrase: "\u81ea\u5206\u306e\u5bdd\u5e8a\u3092\u4f5c\u3063\u305f", weight: 24,
|
|
condition: (t, world) => canActNormally(t) && !hasOwnedStructure(world, t, "grass_bed") && Boolean(findNearbyMaterial(world, t, "grassMaterial", 520)),
|
|
run(t) { return runBuildAction(t, "grass_bed", this.label); } },
|
|
{ id: "build_plushie", need: "fulfill", label: "\u306c\u3044\u3050\u308b\u307f\u3092\u4f5c\u3063\u3066\u3044\u308b", phrase: "\u306c\u3044\u3050\u308b\u307f\u3092\u4f5c\u3063\u305f", weight: 18,
|
|
condition: (t, world) => canActNormally(t) && !hasOwnedStructure(world, t, "plushie") && Boolean(findNearbyMaterial(world, t, "grassMaterial", 520)),
|
|
run(t) { return runBuildAction(t, "plushie", this.label); } },
|
|
{ id: "return_owned_structure", need: "fulfill", label: "\u81ea\u5206\u306e\u5834\u6240\u306b\u623b\u3063\u3066\u3044\u308b", phrase: "\u81ea\u5206\u306e\u5834\u6240\u3078\u623b\u3063\u305f", weight: 18,
|
|
condition: (t, world) => Boolean(findOwnedStructure(world, t, "grass_bed", 760)),
|
|
run(t, world) { const bed = findOwnedStructure(world, t, "grass_bed", 760); if (moveTowardOrUse(t, bed, "seek_bed", this.label)) return true; StructureRegistry.grass_bed.onUse(t, bed, world); return true; } },
|
|
{ id: "use_plushie", need: "fulfill", label: "\u306c\u3044\u3050\u308b\u307f\u3092\u5927\u4e8b\u306b\u3057\u3066\u3044\u308b", phrase: "\u306c\u3044\u3050\u308b\u307f\u3067\u843d\u3061\u7740\u3044\u305f", weight: 22,
|
|
condition: (t, world) => Boolean(findOwnedStructure(world, t, "plushie", Infinity)),
|
|
run(t, world) { const plushie = findOwnedStructure(world, t, "plushie", Infinity); StructureRegistry.plushie.onUse(t, plushie, world); t.setActionState?.("idle", { target: plushie, reason: this.label }); return true; } },
|
|
{ id: "wander_lightly", need: "fulfill", label: "\u8efd\u304f\u6563\u6b69\u3057\u3066\u3044\u308b", phrase: "\u6563\u6b69\u3057\u305f", weight: 12, condition: () => true,
|
|
run(t) { t.wanderAngle += rand(-1.7, 1.7); t.setActionState?.("idle", { target: null, reason: this.label }); return true; } },
|
|
];
|
|
|
|
function chooseActionForNeed(need, t, world) {
|
|
const candidates = TARINAI_ACTIONS.filter(a => a.need === need && (!a.condition || a.condition(t, world)));
|
|
return weightedRandom(candidates) || TARINAI_ACTIONS.find(a => a.id === "wander_lightly");
|
|
}
|
|
|
|
function runNeedDecision(t, world, dt = 0.1) {
|
|
updateAttachedStructures(world, t);
|
|
decayNeedShock(t, dt);
|
|
const needs = updateNeeds(t, world);
|
|
const maxNeed = Math.max(...NEED_KEYS.map(key => needs[key]));
|
|
const tiedNeeds = NEED_KEYS.filter(key => needs[key] === maxNeed);
|
|
const chosenNeed = chooseTopNeedRandom(needs);
|
|
const action = chooseActionForNeed(chosenNeed, t, world);
|
|
if (!action || !action.run(t, world, dt)) {
|
|
t.setActionState?.("idle", { target: null, reason: "\u5f85\u3063\u3066\u3044\u308b" });
|
|
}
|
|
const reasonText = buildReasonText(chosenNeed, tiedNeeds, action);
|
|
t.intent = {
|
|
need: chosenNeed,
|
|
tiedNeeds: tiedNeeds.slice(),
|
|
actionId: action?.id || "idle",
|
|
actionLabel: action?.label || "\u5f85\u3063\u3066\u3044\u308b",
|
|
reasonText,
|
|
};
|
|
t.need = chosenNeed;
|
|
t.thought = reasonText;
|
|
return t.intent;
|
|
}
|
|
|
|
function updateStructure(dt, world) {
|
|
if (this.dead) return;
|
|
this.age = (this.age || 0) + dt;
|
|
if (this.type === "plushie" && this.carriedById) {
|
|
const owner = (world?.tarinai || []).find(t => t && !t.dead && t.id === this.carriedById);
|
|
if (owner) {
|
|
this.x = owner.x + (owner.facingDir ? owner.facingDir() : 1) * owner.radius * 0.12;
|
|
this.y = owner.y - owner.radius * 1.10;
|
|
} else {
|
|
this.carriedById = "";
|
|
this.onHead = false;
|
|
}
|
|
}
|
|
this.amount = Math.max(0, this.hp || 0);
|
|
}
|
|
|
|
function damageStructure(amount = 1, breaker = null, world = null) {
|
|
this.hp = Math.max(0, Number(this.hp || 0) - Math.max(0, Number(amount) || 0));
|
|
if (this.hp > 0) return false;
|
|
const w = world || global.world;
|
|
const def = StructureRegistry[this.type];
|
|
def?.onDestroyed?.(this, w, breaker);
|
|
this.dead = true;
|
|
this.amount = 0;
|
|
w?.markTerrainDirty?.(`structure-destroyed:${this.type}`);
|
|
return true;
|
|
}
|
|
|
|
function drawStructure(ctx, time, lighting) {
|
|
const r = this.r || (this.type === "plushie" ? 14 : 28);
|
|
ctx.save();
|
|
ctx.translate(this.x, this.y);
|
|
if (this.type === "grass_bed") {
|
|
ctx.fillStyle = "rgba(175, 196, 102, 0.88)";
|
|
ctx.strokeStyle = "rgba(90, 118, 55, 0.72)";
|
|
ctx.lineWidth = 2.2;
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 3, r * 1.55, r * 0.74, -0.08, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.strokeStyle = "rgba(71, 112, 48, 0.55)";
|
|
ctx.lineWidth = 1.6;
|
|
for (let i = 0; i < 15; i++) {
|
|
const x = randSeed((this.seed || 1) + i, -r * 1.1, r * 1.1);
|
|
const y = randSeed((this.seed || 1) + i + 20, -r * 0.32, r * 0.46);
|
|
const len = randSeed((this.seed || 1) + i + 40, r * 0.30, r * 0.68);
|
|
const a = randSeed((this.seed || 1) + i + 60, -0.7, 0.7);
|
|
ctx.beginPath();
|
|
ctx.moveTo(x - Math.cos(a) * len * 0.5, y - Math.sin(a) * len * 0.5);
|
|
ctx.lineTo(x + Math.cos(a) * len * 0.5, y + Math.sin(a) * len * 0.5);
|
|
ctx.stroke();
|
|
}
|
|
} else if (this.type === "plushie") {
|
|
const img = typeof getRenderableImage === "function" ? getRenderableImage("smile", "smile") : null;
|
|
ctx.rotate(Math.sin(time * 2.2 + (this.seed || 0)) * 0.08);
|
|
if (img) ctx.drawImage(img, -r * 0.8, -r * 0.8, r * 1.6, r * 1.45);
|
|
else {
|
|
ctx.fillStyle = "#f4ead6";
|
|
ctx.strokeStyle = "rgba(92,71,49,0.48)";
|
|
ctx.lineWidth = 1.4;
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 0, r * 0.68, r * 0.58, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
ctx.restore();
|
|
}
|
|
|
|
const StructureRegistry = {
|
|
grass_bed: {
|
|
type: "grass_bed",
|
|
label: "\u8349\u306e\u7c21\u6613\u30d9\u30c3\u30c9",
|
|
buildNeed: "fulfill",
|
|
buildTime: 4,
|
|
maxHp: 100,
|
|
materials: { grassMaterial: 1 },
|
|
roles: { sleepPlace: true, ownedStructure: true },
|
|
needEffects: { food: 0, sleep: 60, health: 5, safety: 25, social: 0, fulfill: 40 },
|
|
create(owner, x, y) {
|
|
return { id: createId("structure"), type: "grass_bed", isStructure: true, ownerId: owner.id, hp: 100, attachment: 20, usedCount: 0, x, y, r: 28, amount: 100, seed: Math.random() * 1000, roles: { ...this.roles }, needEffects: { ...this.needEffects } };
|
|
},
|
|
onUse(user, structure) {
|
|
const owner = user?.id === structure?.ownerId;
|
|
structure.usedCount = (structure.usedCount || 0) + 1;
|
|
structure.attachment = clamp((structure.attachment || 0) + (owner ? 2 : 0.4), 0, 100);
|
|
user.needShock = { ...(user.needShock || {}) };
|
|
user.needShock.sleep = Math.max(0, Number(user.needShock.sleep || 0) - (owner ? 18 : 8));
|
|
user.needShock.safety = Math.max(0, Number(user.needShock.safety || 0) - (owner ? 12 : 4));
|
|
user.needShock.fulfill = Math.max(0, Number(user.needShock.fulfill || 0) - (owner ? 18 : 6));
|
|
},
|
|
onDestroyed(structure, world, breaker) {
|
|
const owner = (world?.tarinai || []).find(t => t && !t.dead && t.id === structure.ownerId);
|
|
if (owner) addNeedShock(owner, { safety: 45, fulfill: 52 }, breaker);
|
|
},
|
|
},
|
|
plushie: {
|
|
type: "plushie",
|
|
label: "\u306c\u3044\u3050\u308b\u307f",
|
|
buildNeed: "fulfill",
|
|
buildTime: 4,
|
|
maxHp: 1,
|
|
materials: { grassMaterial: 1 },
|
|
roles: { carriedObject: true, ownedStructure: true, fulfillObject: true },
|
|
needEffects: { food: 0, sleep: 0, health: 0, safety: 5, social: 0, fulfill: 50 },
|
|
create(owner, x, y) {
|
|
return { id: createId("structure"), type: "plushie", isStructure: true, ownerId: owner.id, hp: 1, attachment: 30, usedCount: 0, x, y, r: 14, amount: 1, carriedById: owner.id, onHead: true, seed: Math.random() * 1000, roles: { ...this.roles }, needEffects: { ...this.needEffects } };
|
|
},
|
|
onUse(user, structure) {
|
|
if (!user || !structure) return;
|
|
structure.attachment = clamp((structure.attachment || 0) + 1.5, 0, 100);
|
|
user.needShock = { ...(user.needShock || {}), fulfill: Math.max(0, Number(user.needShock?.fulfill || 0) - 20), safety: Math.max(0, Number(user.needShock?.safety || 0) - 3) };
|
|
},
|
|
onDestroyed(structure, world, breaker) {
|
|
const owner = (world?.tarinai || []).find(t => t && !t.dead && t.id === structure.ownerId);
|
|
if (owner) addNeedShock(owner, { safety: 58, fulfill: 70 }, breaker);
|
|
},
|
|
},
|
|
};
|
|
|
|
global.TarinaiNeedSystem = {
|
|
NEED_KEYS,
|
|
NEED_LABELS,
|
|
NEED_PHRASES,
|
|
TARINAI_ACTIONS,
|
|
StructureRegistry,
|
|
createDefaultNeeds,
|
|
quantizeNeed,
|
|
updateNeeds,
|
|
chooseTopNeedRandom,
|
|
chooseActionForNeed,
|
|
buildReasonText,
|
|
getNeedDisplayValue,
|
|
findNearestItemWithRole,
|
|
findNearestSleepPlace,
|
|
findNearbyDanger,
|
|
findNearbyMaterial,
|
|
findOwnedStructure,
|
|
ownedStructures,
|
|
itemRolesFor,
|
|
ensureItemNeedEffects,
|
|
deriveStressFromNeeds,
|
|
addNeedShock,
|
|
runNeedDecision,
|
|
createStructure,
|
|
damageStructure,
|
|
};
|
|
|
|
global.createDefaultNeeds = createDefaultNeeds;
|
|
global.quantizeNeed = quantizeNeed;
|
|
global.updateNeeds = updateNeeds;
|
|
global.chooseTopNeedRandom = chooseTopNeedRandom;
|
|
global.chooseActionForNeed = chooseActionForNeed;
|
|
global.buildReasonText = buildReasonText;
|
|
global.getNeedDisplayValue = getNeedDisplayValue;
|
|
global.findNearestItemWithRole = findNearestItemWithRole;
|
|
global.findNearestSleepPlace = findNearestSleepPlace;
|
|
global.findNearbyDanger = findNearbyDanger;
|
|
global.findNearbyMaterial = findNearbyMaterial;
|
|
global.StructureRegistry = StructureRegistry;
|
|
})(typeof window !== "undefined" ? window : globalThis);
|