hmm
This commit is contained in:
parent
e6507288ce
commit
8eb66f2e66
12 changed files with 2715 additions and 522 deletions
473
js/tarinai.js
473
js/tarinai.js
|
|
@ -1,4 +1,4 @@
|
|||
"use strict";
|
||||
"use strict";
|
||||
|
||||
class Tarinai {
|
||||
constructor(world, opts = {}) {
|
||||
|
|
@ -33,8 +33,8 @@ class Tarinai {
|
|||
this.affection = opts.affection ?? rand(0, 30);
|
||||
this.need = opts.need || pick(NEEDS);
|
||||
this.state = opts.state || "idle";
|
||||
this.dead = false;
|
||||
this.deathReason = "";
|
||||
this.dead = !!opts.dead;
|
||||
this.deathReason = opts.deathReason || "";
|
||||
this.thought = "";
|
||||
this.target = null;
|
||||
this.targetKey = "";
|
||||
|
|
@ -69,8 +69,17 @@ class Tarinai {
|
|||
this.birthRitualRole = opts.birthRitualRole || 0;
|
||||
this.birthRitualLeader = !!opts.birthRitualLeader;
|
||||
this.pokeFlashTimer = opts.pokeFlashTimer ?? 0;
|
||||
this.zunchiStain = 0;
|
||||
this.zunchiStain = opts.zunchiStain ?? 0;
|
||||
this.zunchiStainSeed = opts.zunchiStainSeed ?? stableUnit(this.id, "zunchi-stain");
|
||||
this.zunchiDisease = Boolean(opts.zunchiDisease);
|
||||
this.zunchiDiseaseSeverity = opts.zunchiDiseaseSeverity ?? (this.zunchiDisease ? 1 : 0);
|
||||
this.zunchiDiseaseCooldown = opts.zunchiDiseaseCooldown ?? 0;
|
||||
this.totalFightLosses = opts.totalFightLosses || 0;
|
||||
this.isZunchiSlave = Boolean(opts.isZunchiSlave);
|
||||
this.formerName = opts.formerName || null;
|
||||
this.loveMochiTimer = opts.loveMochiTimer ?? 0;
|
||||
this.fightMochiTimer = opts.fightMochiTimer ?? 0;
|
||||
if (this.isZunchiSlave) this.name = "ずんちどれい";
|
||||
this.tempComfort = opts.tempComfort ?? 0.62;
|
||||
this.tempTimer = opts.tempTimer ?? stableUnit(this.id, "temp-start") * 0.55;
|
||||
this.parents = opts.parents || [];
|
||||
|
|
@ -105,16 +114,20 @@ class Tarinai {
|
|||
this.nextBubbleAt = 0;
|
||||
this.nextSleepBubbleAt = 0;
|
||||
this.nextFearBubbleAt = 0;
|
||||
this.nextCursorHeartAt = opts.nextCursorHeartAt ?? 0;
|
||||
this.cursorPetting = opts.cursorPetting ?? 0;
|
||||
this.aiTimer = opts.aiTimer ?? stableUnit(this.id, "ai-start") * 0.22;
|
||||
this.envTimer = opts.envTimer ?? stableUnit(this.id, "env-start") * 0.55;
|
||||
this.visibleSpriteId = opts.visibleSpriteId || null;
|
||||
this.spriteLockUntil = opts.spriteLockUntil ?? 0;
|
||||
this.entryTimer = opts.entryTimer ?? 0;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {
|
||||
id: this.id, name: this.name, type: this.type, x: this.x, y: this.y, vx: this.vx, vy: this.vy,
|
||||
scale: this.scale, age: this.age, lifeSpan: this.lifeSpan, generation: this.generation, hasPaired: this.hasPaired,
|
||||
dead: this.dead, deathReason: this.deathReason,
|
||||
hunger: this.hunger, loneliness: this.loneliness, energy: this.energy, stress: this.stress, hpBarTimer: this.hpBarTimer,
|
||||
affection: this.affection, need: this.need, state: this.state,
|
||||
goodMode: this.goodMode, facing: this.facing, visualFacing: this.visualFacing, facingLockUntil: this.facingLockUntil, eatTimer: this.eatTimer,
|
||||
|
|
@ -123,6 +136,8 @@ class Tarinai {
|
|||
birthRitualTimer: this.birthRitualTimer, birthRitualMax: this.birthRitualMax, birthPartnerId: this.birthPartnerId,
|
||||
birthRitualRole: this.birthRitualRole, birthRitualLeader: this.birthRitualLeader,
|
||||
pokeFlashTimer: this.pokeFlashTimer, zunchiStain: this.zunchiStain, zunchiStainSeed: this.zunchiStainSeed,
|
||||
totalFightLosses: this.totalFightLosses, isZunchiSlave: this.isZunchiSlave, formerName: this.formerName, loveMochiTimer: this.loveMochiTimer, fightMochiTimer: this.fightMochiTimer,
|
||||
zunchiDisease: this.zunchiDisease, zunchiDiseaseSeverity: this.zunchiDiseaseSeverity, zunchiDiseaseCooldown: this.zunchiDiseaseCooldown,
|
||||
tempComfort: this.tempComfort, tempTimer: this.tempTimer,
|
||||
parents: this.parents, parentNames: this.parentNames, children: this.children, birthTime: this.birthTime,
|
||||
fightTimer: this.fightTimer, fightCooldown: this.fightCooldown, fightTargetId: this.fightTargetId, fightTargetIds: this.fightTargetIds, targetGiveupKey: this.targetGiveupKey, targetGiveupUntil: this.targetGiveupUntil,
|
||||
|
|
@ -132,7 +147,8 @@ class Tarinai {
|
|||
defeatedTimer: this.defeatedTimer, defeatedById: this.defeatedById, fightWinnerId: this.fightWinnerId,
|
||||
relationships: this.relationships, personality: this.personality, fallTimer: this.fallTimer, fallMax: this.fallMax, fallDir: this.fallDir,
|
||||
blastSpinTimer: this.blastSpinTimer, blastSpinMax: this.blastSpinMax, postBirthPeaceTimer: this.postBirthPeaceTimer,
|
||||
lastMealColor: this.lastMealColor, aiTimer: this.aiTimer, envTimer: this.envTimer,
|
||||
lastMealColor: this.lastMealColor, aiTimer: this.aiTimer, envTimer: this.envTimer, nextCursorHeartAt: this.nextCursorHeartAt, cursorPetting: this.cursorPetting,
|
||||
entryTimer: this.entryTimer || 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -283,6 +299,21 @@ class Tarinai {
|
|||
return Boolean(this.hasPaired || (this.parents || []).length || (this.children || []).length);
|
||||
}
|
||||
|
||||
nearestCoParent(maxRange = 220) {
|
||||
let best = null;
|
||||
let bestD = maxRange;
|
||||
for (const o of this.world.tarinai || []) {
|
||||
if (!o || o === this || o.dead) continue;
|
||||
if (!(this.world.areCoParents?.(this, o))) continue;
|
||||
const d = dist(this, o);
|
||||
if (d < bestD) {
|
||||
best = o;
|
||||
bestD = d;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
displayGeneration() {
|
||||
return this.familyJoined() ? this.generation : null;
|
||||
}
|
||||
|
|
@ -378,13 +409,26 @@ class Tarinai {
|
|||
}
|
||||
|
||||
lowHealthSprite() {
|
||||
const critical = this.lack > 96 || this.hunger > 112 || this.energy < 7 || this.stress > 118;
|
||||
// This is physical weakness only. Stress is handled by the stress sprite branch
|
||||
// below so high-stress individuals do not incorrectly render as low-health.
|
||||
const critical = this.hunger > 112 || this.energy < 7;
|
||||
if (critical) return this.stretchTimer > 0 ? "stretch" : "weak";
|
||||
const weak = this.lack > 88 || this.hunger > 102 || this.energy < 18 || this.stress > 104;
|
||||
const weak = this.hunger > 102 || this.energy < 18;
|
||||
if (weak) return "weak";
|
||||
return null;
|
||||
}
|
||||
|
||||
briefDeathReason(reason = this.deathReason) {
|
||||
const text = String(reason || "");
|
||||
if (!text) return "";
|
||||
if (text.includes("食べ物がたりない")) return "飢え";
|
||||
if (text.includes("寿命")) return "寿命";
|
||||
if (text.includes("ストレス")) return "過ストレス";
|
||||
if (text.includes("疲れ")) return "衰弱";
|
||||
if (text.includes("つつかれ")) return "つつき";
|
||||
return text.length > 10 ? `${text.slice(0, 10)}…` : text;
|
||||
}
|
||||
|
||||
variantSprite(category) {
|
||||
const pools = {
|
||||
hurt: ["hurt", "hurt2"],
|
||||
|
|
@ -400,27 +444,120 @@ class Tarinai {
|
|||
return stableChoice(this.id, `variant-${category}`, options) || options[0] || this.goodMode || "smile";
|
||||
}
|
||||
|
||||
infectZunchiDisease(source = null, force = false) {
|
||||
if (this.dead || this.zunchiDisease) return false;
|
||||
if (!force && (this.zunchiDiseaseCooldown || 0) > 0) return false;
|
||||
this.zunchiDisease = true;
|
||||
this.zunchiDiseaseSeverity = Math.max(this.zunchiDiseaseSeverity || 0, source ? 0.72 : 0.86);
|
||||
this.zunchiDiseaseCooldown = 12;
|
||||
this.fearTimer = Math.max(this.fearTimer || 0, 0.35);
|
||||
this.surpriseTimer = Math.max(this.surpriseTimer || 0, 0.45);
|
||||
this.thought = "ずんち病にかかった";
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.20, "うぅ…", "rgba(38,82,38,0.82)");
|
||||
return true;
|
||||
}
|
||||
|
||||
recoverZunchiDisease(reason = "") {
|
||||
if (!this.zunchiDisease) return false;
|
||||
this.zunchiDisease = false;
|
||||
this.zunchiDiseaseSeverity = 0;
|
||||
this.zunchiDiseaseCooldown = 18;
|
||||
this.zunchiStain = clamp((this.zunchiStain || 0) - 35, 0, 100);
|
||||
this.stress = clamp(this.stress - 8, 0, 130);
|
||||
this.thought = reason || "ずんち病が治まった";
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "はう", "rgba(62,104,62,0.78)");
|
||||
return true;
|
||||
}
|
||||
|
||||
emitZunchiDiseaseEffect(dt) {
|
||||
if (!this.zunchiDisease || !this.world?.effects) return;
|
||||
if (Math.random() < dt * (1.2 + (this.zunchiDiseaseSeverity || 0) * 1.4)) {
|
||||
this.world.effects.push(new Effect("zunchi_miasma", this.x + rand(-this.radius * 0.45, this.radius * 0.45), this.y - this.radius * rand(0.05, 0.72), {
|
||||
vx: rand(-5, 5),
|
||||
vy: rand(-10, -3),
|
||||
life: rand(1.4, 2.4),
|
||||
size: rand(8, 18) * (0.8 + (this.zunchiDiseaseSeverity || 0) * 0.45),
|
||||
color: "rgba(16,78,28,0.70)",
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
updateZunchiDisease(dt) {
|
||||
this.zunchiStain = clamp(this.zunchiStain || 0, 0, 100);
|
||||
this.zunchiDiseaseCooldown = Math.max(0, (this.zunchiDiseaseCooldown || 0) - dt);
|
||||
if (this.world?.weather === "light_rain") {
|
||||
this.zunchiStain = clamp(this.zunchiStain - dt * 2.4, 0, 100);
|
||||
if (this.zunchiDisease) this.zunchiDiseaseSeverity = Math.max(0, (this.zunchiDiseaseSeverity || 1) - dt * 0.006);
|
||||
}
|
||||
if (!this.zunchiDisease && this.zunchiStain > 82 && Math.random() < dt * clamp((this.zunchiStain - 82) / 18, 0, 1) * 0.045) {
|
||||
this.infectZunchiDisease(null, true);
|
||||
this.world?.log?.(`${this.name}はずんち病を発症した。`, "accident");
|
||||
}
|
||||
if (this.zunchiDisease) {
|
||||
this.zunchiDiseaseSeverity = clamp((this.zunchiDiseaseSeverity || 0.7) + dt * 0.002, 0, 1.25);
|
||||
this.stress = clamp(this.stress + dt * 0.22 * this.zunchiDiseaseSeverity, 0, 130);
|
||||
this.energy = clamp(this.energy - dt * 0.035 * this.zunchiDiseaseSeverity, 0, 100);
|
||||
if (this.energy > 82) this.zunchiDiseaseSeverity = Math.max(0, this.zunchiDiseaseSeverity - dt * (this.energy > 94 ? 0.020 : 0.010));
|
||||
this.emitZunchiDiseaseEffect(dt);
|
||||
if (this.zunchiDiseaseSeverity <= 0.04) {
|
||||
if (this.recoverZunchiDisease("体力が戻り、ずんち病が治まった")) this.world?.log?.(`${this.name}のずんち病が治まった。`, "accident");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canEatItemType(type) {
|
||||
return this.isZunchiSlave ? type === "zunchi" : true;
|
||||
}
|
||||
|
||||
hasLoveMochiEffect() {
|
||||
return (this.loveMochiTimer || 0) > 0.04;
|
||||
}
|
||||
|
||||
hasFightMochiEffect() {
|
||||
return (this.fightMochiTimer || 0) > 0.04;
|
||||
}
|
||||
|
||||
becomeZunchiSlave() {
|
||||
if (this.isZunchiSlave) return false;
|
||||
this.formerName = this.formerName || this.name;
|
||||
this.isZunchiSlave = true;
|
||||
this.name = "ずんちどれい";
|
||||
this.birthRitualTimer = 0;
|
||||
this.birthPartnerId = null;
|
||||
this.birthRitualLeader = false;
|
||||
this.birthRitualRole = 0;
|
||||
this.target = null;
|
||||
this.reproductionTimer = Math.max(this.reproductionTimer || 0, CONFIG.reproductionCooldown + rand(8, 18));
|
||||
this.loveMochiTimer = 0;
|
||||
this.stress = clamp(this.stress + 14, 0, 130);
|
||||
this.hunger = Math.max(this.hunger, 40);
|
||||
this.visibleSpriteId = "zunchi_slave";
|
||||
this.spriteLockUntil = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
rawSpriteId() {
|
||||
if (this.dead) return this.variantSprite("fear");
|
||||
if (this.type === "cry") return "cry";
|
||||
if (this.type === "stretch") return "stretch";
|
||||
if (this.isZunchiSlave) return "zunchi_slave";
|
||||
if (this.type === "cry" && (this.mood < 26 || this.stress > 78 || this.hunger > 96 || this.energy < 12)) return "cry";
|
||||
if ((this.state === "eat" || this.eatTimer > 0.04) && this.target?.type === "sweet") return "zunda_eat";
|
||||
if ((this.state === "seek_food" || this.foodReactTimer > 0.04) && this.target?.type === "sweet" && dist(this, this.target) < 80) return "zunda_eat";
|
||||
if (this.hurtTimer > 0.08 || this.pokeFlashTimer > 0.08) return this.variantSprite("hurt");
|
||||
if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") return "birth_ritual";
|
||||
const low = this.lowHealthSprite();
|
||||
if (low) return low;
|
||||
if (low === "stretch") return low;
|
||||
if (this.defeatedTimer > 0.04) return this.variantSprite("flee");
|
||||
if (this.state === "intimidate" || this.intimidateTimer > 0.04) return this.variantSprite("intimidate");
|
||||
if (this.state === "panic" || this.state === "cursor_enemy" || this.fearTimer > 0.08 || this.intimidatedTimer > 0.08) return this.variantSprite("fear");
|
||||
if (this.state === "sleep" || this.energy < 18) return this.variantSprite("sleep");
|
||||
if (this.state === "fight" || this.fightTimer > 0.04) return "angry";
|
||||
if (this.stress > 62 || (this.stress > 50 && this.mood < 46)) return this.variantSprite("stress");
|
||||
if (low) return low;
|
||||
if (this.state === "sleep" || this.energy < 18) return this.variantSprite("sleep");
|
||||
if (this.surpriseTimer > 0.08) return "pokan";
|
||||
if (this.state === "cursor_friend") return "smile";
|
||||
if (this.state === "eat" || this.eatTimer > 0.04) return this.goodMode;
|
||||
if ((this.state === "seek_food" || this.state === "idle" || this.state === "follow_parent") && (this.vy < -18 || (this.target && this.target.y < this.y - 36 && Math.abs(this.target.y - this.y) > Math.abs(this.target.x - this.x) * 0.55))) return "back";
|
||||
if (this.state === "seek_food") return "drool";
|
||||
if (this.stress > 62 || (this.stress > 50 && this.mood < 46)) return this.variantSprite("stress");
|
||||
if (this.mood >= 66 && this.stress < 24 && this.hunger < 58 && this.loneliness < 62) return this.variantSprite("low_stress");
|
||||
if (this.mood >= 63 && this.stress < 44 && this.hunger < 58) return this.goodMode;
|
||||
if (this.mood < 38 || this.hunger > 72 || this.loneliness > 74) return "angry";
|
||||
|
|
@ -460,6 +597,9 @@ class Tarinai {
|
|||
this.blastSpinTimer = Math.max(0, (this.blastSpinTimer || 0) - dt);
|
||||
this.postBirthPeaceTimer = Math.max(0, (this.postBirthPeaceTimer || 0) - dt);
|
||||
this.hpBarTimer = Math.max(0, (this.hpBarTimer || 0) - dt);
|
||||
this.updateZunchiDisease(dt);
|
||||
this.loveMochiTimer = Math.max(0, (this.loveMochiTimer || 0) - dt);
|
||||
this.fightMochiTimer = Math.max(0, (this.fightMochiTimer || 0) - dt);
|
||||
if (this.intimidateTimer <= 0.04) this.intimidateTargetId = null;
|
||||
if (this.birthRitualTimer > 0) {
|
||||
const beforeBirthTimer = this.birthRitualTimer;
|
||||
|
|
@ -471,6 +611,7 @@ class Tarinai {
|
|||
}
|
||||
}
|
||||
this.pokeFlashTimer = Math.max(0, this.pokeFlashTimer - dt);
|
||||
this.cursorPetting = Math.max(0, this.cursorPetting - dt * 1.2);
|
||||
const wasFighting = this.fightTimer > 0.04;
|
||||
this.fightTimer = Math.max(0, this.fightTimer - dt);
|
||||
this.fightCooldown = Math.max(0, this.fightCooldown - dt);
|
||||
|
|
@ -492,7 +633,6 @@ class Tarinai {
|
|||
this.grassEatTimer = Math.max(0, this.grassEatTimer - dt);
|
||||
this.hurtTimer = Math.max(0, this.hurtTimer - dt);
|
||||
this.fallTimer = Math.max(0, this.fallTimer - dt);
|
||||
this.zunchiStain = 0;
|
||||
if (this.stress > 92 && this.state !== "sleep" && this.state !== "fight" && this.state !== "eat" && Math.random() < dt * 0.050) {
|
||||
this.bubble("\u306f\u3063...\u306f\u3063...", 3.4, "rgba(58,48,63,0.80)");
|
||||
}
|
||||
|
|
@ -612,6 +752,14 @@ class Tarinai {
|
|||
};
|
||||
}
|
||||
|
||||
isSleepFurniture(target) {
|
||||
return Boolean(target && target.type === "bed");
|
||||
}
|
||||
|
||||
sleepFurnitureLabel(target) {
|
||||
return "干草寝床";
|
||||
}
|
||||
|
||||
sleepSpotFor(bed) {
|
||||
if (!bed) return { x: this.x, y: this.y };
|
||||
const angle = stableUnit(this.id, `bed-angle-${bed.id || bed.seed || "bed"}`) * Math.PI * 2;
|
||||
|
|
@ -650,6 +798,7 @@ class Tarinai {
|
|||
}
|
||||
|
||||
reactToFreshZunchi(dt) {
|
||||
if (this.isZunchiSlave) return;
|
||||
if (this.state === "panic" || this.fearTimer > 0.2) return;
|
||||
const z = this.world.nearest(this, ["zunchi"], 90);
|
||||
if (!z || z.stage !== "fresh") return;
|
||||
|
|
@ -738,6 +887,41 @@ class Tarinai {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.zunchiDisease && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) {
|
||||
const water = this.world.nearest(this, ["water"], 360);
|
||||
if (water && !water.dead) {
|
||||
this.state = "seek_water";
|
||||
this.target = water;
|
||||
this.thought = "ずんち病で水を探している";
|
||||
return;
|
||||
}
|
||||
this.state = "zunchi_sick";
|
||||
this.target = null;
|
||||
this.thought = "ずんち病で当てもなく彷徨っている";
|
||||
if (Math.random() < dt * 0.35) this.wanderAngle += rand(-2.4, 2.4);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isZunchiSlave) {
|
||||
const specialMochi = this.world.nearest(this, ["love_mochi", "fight_mochi"], wasSleeping ? 520 : 390);
|
||||
if (specialMochi && specialMochi.amount > 1.2 && this.hunger > 10 && !this.shouldAvoidTarget(specialMochi)) {
|
||||
this.state = "seek_food";
|
||||
this.target = specialMochi;
|
||||
this.foodReactTimer = Math.max(this.foodReactTimer, 0.34);
|
||||
this.thought = specialMochi.type === "love_mochi" ? "媚薬餅を探している" : "喧嘩餅を探している";
|
||||
return;
|
||||
}
|
||||
} else if (this.hunger > 20) {
|
||||
const zOnly = this.world.nearest(this, ["zunchi"], 520);
|
||||
if (zOnly && zOnly.amount > 1.2) {
|
||||
this.state = "seek_food";
|
||||
this.target = zOnly;
|
||||
this.foodReactTimer = Math.max(this.foodReactTimer, 0.30);
|
||||
this.thought = "ずんちを探している";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const zundaRange = wasSleeping ? 540 : 430 + 70 * this.personalityProfile().zunda;
|
||||
const zunda = this.world.nearest(this, ["sweet"], zundaRange);
|
||||
const zundaHungry = this.hunger > Math.max(7, 18 - 10 * (this.personalityProfile().zunda - 1));
|
||||
|
|
@ -760,15 +944,15 @@ class Tarinai {
|
|||
const sleepBias = this.personalityProfile().sleep;
|
||||
const keepSleeping = wasSleeping && (personalSleep || this.energy < 66 + 8 * sleepBias);
|
||||
if (keepSleeping || (personalSleep && this.energy < 90 + 8 * sleepBias) || this.energy < 28 + 8 * sleepBias) {
|
||||
let bed = previousTarget && previousTarget.type === "bed" ? previousTarget : null;
|
||||
if (!bed && friendNearby && friendNearby.target?.type === "bed" && (friendNearby.state === "sleep" || friendNearby.state === "seek_bed")) bed = friendNearby.target;
|
||||
let bed = this.isSleepFurniture(previousTarget) ? previousTarget : null;
|
||||
if (!bed && friendNearby && this.isSleepFurniture(friendNearby.target) && (friendNearby.state === "sleep" || friendNearby.state === "seek_bed")) bed = friendNearby.target;
|
||||
if (!bed && !wasSleeping) bed = this.world.bestBedFor(this, 360);
|
||||
if (bed && !wasSleeping && this.energy > 18) {
|
||||
const spot = this.sleepSpotFor(bed);
|
||||
if (distXY(this.x, this.y, spot.x, spot.y) > this.radius + 10) {
|
||||
this.state = "seek_bed";
|
||||
this.target = bed;
|
||||
this.thought = "\u5e72\u8349\u5bdd\u5e8a\u3078\u5411\u304b\u3063\u3066\u3044\u308b";
|
||||
this.thought = `${this.sleepFurnitureLabel(bed)}へ向かっている`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -779,7 +963,7 @@ class Tarinai {
|
|||
this.nextSleepBubbleAt = this.world.time + 4.5;
|
||||
this.world.spawnBubble(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)");
|
||||
}
|
||||
this.thought = bed ? "\u30d9\u30c3\u30c9\u306e\u8fd1\u304f\u3067\u7720\u3063\u3066\u3044\u308b" : "\u7720\u3063\u3066\u3044\u308b";
|
||||
this.thought = bed ? "ベッドの近くで眠っている" : "眠っている";
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -810,6 +994,24 @@ class Tarinai {
|
|||
}
|
||||
}
|
||||
|
||||
const playTrait = this.personalityProfile().play ?? 1;
|
||||
const ballRange = this.personality === "playful" ? 540 : 240 + 60 * playTrait;
|
||||
const ball = this.world.nearest(this, ["ball"], ballRange);
|
||||
if (ball && this.energy > 26 && this.hunger < 98 && !this.shouldAvoidTarget(ball)) {
|
||||
const ballSpeed = Math.hypot(ball.vx || 0, ball.vy || 0);
|
||||
const chasers = this.world.countBallChasers ? this.world.countBallChasers(ball, this) : 0;
|
||||
const baseChance = this.personality === "playful" ? 0.74 : 0.08;
|
||||
const movingBonus = clamp(ballSpeed / 120, 0, 0.42);
|
||||
const socialBonus = this.loneliness > 48 ? 0.10 : 0;
|
||||
if (chasers < 5 && (this.personality === "playful" || ballSpeed > 7 || Math.random() < baseChance * playTrait + movingBonus + socialBonus)) {
|
||||
this.state = "play_ball";
|
||||
this.target = ball;
|
||||
this.goodMode = "smile";
|
||||
this.thought = this.personality === "playful" ? "ボールに引き寄せられている" : "転がるものが気になっている";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const parent = this.parentToFollow();
|
||||
if (parent && !this.shouldAvoidTarget(parent)) {
|
||||
const d = dist(this, parent);
|
||||
|
|
@ -822,7 +1024,7 @@ class Tarinai {
|
|||
}
|
||||
}
|
||||
|
||||
if (friendNearby && this.hunger > 44 && friendNearby.target && ["sweet", "grass"].includes(friendNearby.target.type) && !friendNearby.target.dead && !this.shouldAvoidTarget(friendNearby.target)) {
|
||||
if (friendNearby && this.hunger > 44 && friendNearby.target && (this.isZunchiSlave ? ["zunchi"] : ["sweet", "love_mochi", "fight_mochi", "grass"]).includes(friendNearby.target.type) && !friendNearby.target.dead && !this.shouldAvoidTarget(friendNearby.target)) {
|
||||
this.state = "seek_food";
|
||||
this.target = friendNearby.target;
|
||||
this.foodReactTimer = Math.max(this.foodReactTimer, 0.24);
|
||||
|
|
@ -831,7 +1033,7 @@ class Tarinai {
|
|||
}
|
||||
|
||||
if (this.hunger > 62) {
|
||||
const itemTypes = ["sweet", "grass"];
|
||||
const itemTypes = this.isZunchiSlave ? ["zunchi"] : ["sweet", "love_mochi", "fight_mochi", "grass"];
|
||||
const item = this.world.nearest(this, itemTypes, 420);
|
||||
if (item) {
|
||||
this.state = "seek_food";
|
||||
|
|
@ -877,8 +1079,9 @@ class Tarinai {
|
|||
}
|
||||
|
||||
interactWithItems(dt) {
|
||||
const canBite = this.eatCooldown <= 0 && this.hunger > 18;
|
||||
const canSweetBite = this.eatCooldown <= 0 && this.hunger > 7;
|
||||
const canBite = !this.isZunchiSlave && this.eatCooldown <= 0 && this.hunger > 18;
|
||||
const canSweetBite = !this.isZunchiSlave && this.eatCooldown <= 0 && this.hunger > 7;
|
||||
const canZunchiBite = this.eatCooldown <= 0 && this.hunger > (this.isZunchiSlave ? 16 : 104);
|
||||
for (const it of this.world.nearbyItems(this.x, this.y, this.radius + 42)) {
|
||||
const d = dist(this, it);
|
||||
if (d > this.radius + it.r + 12) continue;
|
||||
|
|
@ -917,6 +1120,42 @@ class Tarinai {
|
|||
}
|
||||
}
|
||||
|
||||
if (canSweetBite && (it.type === "love_mochi" || it.type === "fight_mochi") && it.amount > 1.2) {
|
||||
const eating = Math.min(it.amount, 6.8);
|
||||
this.state = "eat";
|
||||
this.target = it;
|
||||
this.vx *= Math.pow(0.25, dt * 60);
|
||||
this.vy *= Math.pow(0.25, dt * 60);
|
||||
it.amount -= eating;
|
||||
this.hunger -= eating * 0.82;
|
||||
this.recoverHealth(eating * 0.24);
|
||||
this.affection += eating * 0.04;
|
||||
this.stress = clamp(this.stress - eating * 0.25, 0, 130);
|
||||
if (it.type === "love_mochi") {
|
||||
this.loveMochiTimer = Math.max(this.loveMochiTimer || 0, 42 + rand(4, 12));
|
||||
this.bubble("♡", 2.4, "rgba(190,82,132,0.78)");
|
||||
this.lastMealColor = "#ff7bab";
|
||||
} else {
|
||||
this.fightMochiTimer = Math.max(this.fightMochiTimer || 0, 36 + rand(4, 10));
|
||||
this.bubble("!", 2.0, "rgba(180,86,52,0.80)");
|
||||
this.lastMealColor = "#e07c43";
|
||||
}
|
||||
this.eatTimer = Math.max(this.eatTimer, 0.30);
|
||||
this.eatCooldown = rand(0.38, 0.68);
|
||||
this.foodReactTimer = Math.max(this.foodReactTimer, 0.16);
|
||||
if (Math.random() < dt * 2.2) {
|
||||
const mouth = this.mouthPosition(it);
|
||||
this.world.spawnEatEffect(mouth.x, mouth.y, it.type === "love_mochi" ? "#ff7bab" : "#e07c43");
|
||||
}
|
||||
this.makePoop(eating * 0.20);
|
||||
if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.42; }
|
||||
if (this.world.time - this.lastLog > 9 && Math.random() < 0.02) {
|
||||
this.world.log(`${this.name}は${it.type === "love_mochi" ? "媚薬餅" : "喧嘩餅"}をしばらく味わった。`, "food");
|
||||
this.lastLog = this.world.time;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (canBite && it.type === "grass") {
|
||||
const bite = Math.min(it.amount, rand(4.2, 7.2));
|
||||
this.state = "eat";
|
||||
|
|
@ -945,21 +1184,23 @@ class Tarinai {
|
|||
|
||||
if (it.type === "zunchi") {
|
||||
const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1);
|
||||
this.stress = clamp(this.stress + touch * dt * 3.4, 0, 130);
|
||||
this.zunchiStain = 0;
|
||||
if (canBite && this.hunger > 104) {
|
||||
const bite = Math.min(it.amount, rand(3.4, 5.8));
|
||||
if (!this.isZunchiSlave) this.stress = clamp(this.stress + touch * dt * 3.4, 0, 130);
|
||||
this.zunchiStain = clamp((this.zunchiStain || 0) + touch * dt * (this.isZunchiSlave ? 5.2 : 9.5), 0, 100);
|
||||
if (canZunchiBite) {
|
||||
const bite = Math.min(it.amount, this.isZunchiSlave ? rand(4.8, 7.4) : rand(3.4, 5.8));
|
||||
this.state = "eat";
|
||||
this.target = it;
|
||||
this.vx *= Math.pow(0.18, dt * 60);
|
||||
this.vy *= Math.pow(0.18, dt * 60);
|
||||
it.amount -= bite;
|
||||
this.hunger -= bite * 0.18;
|
||||
this.stress = clamp(this.stress + bite * 0.52, 0, 130);
|
||||
this.zunchiStain = 0;
|
||||
this.hunger -= bite * (this.isZunchiSlave ? 0.66 : 0.18);
|
||||
this.recoverHealth(bite * (this.isZunchiSlave ? 0.10 : 0.02));
|
||||
this.stress = clamp(this.stress + bite * (this.isZunchiSlave ? 0.06 : 0.52), 0, 130);
|
||||
this.zunchiStain = clamp((this.zunchiStain || 0) + bite * (this.isZunchiSlave ? 0.75 : 1.75), 0, 100);
|
||||
this.lastMealColor = "#6b8d3f";
|
||||
this.eatTimer = Math.max(this.eatTimer, 0.24);
|
||||
this.eatCooldown = rand(0.9, 1.4);
|
||||
this.bubble("...", 2.2, "rgba(74,91,50,0.78)");
|
||||
this.bubble(this.isZunchiSlave ? "もぐ" : "...", 2.2, "rgba(74,91,50,0.78)");
|
||||
const mouth = this.mouthPosition(it);
|
||||
this.world.spawnEatEffect(mouth.x, mouth.y, "#6b8d3f");
|
||||
if (this.world.time > this.nextEatSound) { audio.eat(); this.nextEatSound = this.world.time + 0.7; }
|
||||
|
|
@ -971,6 +1212,13 @@ class Tarinai {
|
|||
this.stress -= dt * 2.4;
|
||||
this.energy += dt * 0.5;
|
||||
this.hunger -= dt * 0.2;
|
||||
this.zunchiStain = clamp((this.zunchiStain || 0) - dt * 10.5, 0, 100);
|
||||
if (this.zunchiDisease) {
|
||||
this.zunchiDiseaseSeverity = Math.max(0, (this.zunchiDiseaseSeverity || 1) - dt * (this.energy > 70 ? 0.050 : 0.028));
|
||||
if (this.zunchiDiseaseSeverity <= 0.04 && this.recoverZunchiDisease("水でずんち病が治まった")) {
|
||||
this.world.log(`${this.name}は水でずんち病が治まった。`, "accident");
|
||||
}
|
||||
}
|
||||
it.amount -= dt * 2.4;
|
||||
if (this.type === "teary" || this.type === "cry") this.affection += dt * 0.18;
|
||||
}
|
||||
|
|
@ -981,15 +1229,17 @@ class Tarinai {
|
|||
if (this.need === "\u3042\u305f\u305f\u304b\u3055") this.affection += dt * 0.25;
|
||||
}
|
||||
|
||||
if (it.type === "bed") {
|
||||
if (this.isSleepFurniture(it)) {
|
||||
if (this.energy < 74 + 6 * this.personalityProfile().sleep) {
|
||||
const occ = this.world.bedOccupancy(it);
|
||||
const comfort = this.world.bedComfort(it);
|
||||
this.state = "sleep";
|
||||
this.target = it;
|
||||
this.sleeping = true;
|
||||
if (this.world.time > this.nextSleepBubbleAt) {
|
||||
this.nextSleepBubbleAt = this.world.time + 4.5;
|
||||
this.world.spawnBubble(this.x, this.y - this.radius * 1.18, occ > 5 ? "mm..." : "Zzz...", "rgba(65,70,92,0.72)");
|
||||
const crowded = occ > 5;
|
||||
this.world.spawnBubble(this.x, this.y - this.radius * 1.18, crowded ? "mm..." : "Zzz...", "rgba(65,70,92,0.72)");
|
||||
}
|
||||
this.energy += dt * (1.9 + comfort * 1.4);
|
||||
this.stress += dt * (occ > 5 ? 0.55 : -1.3 * comfort);
|
||||
|
|
@ -997,6 +1247,16 @@ class Tarinai {
|
|||
}
|
||||
}
|
||||
|
||||
if (it.type === "ball") {
|
||||
const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1);
|
||||
if (this.state === "play_ball" || this.target === it || this.personality === "playful") {
|
||||
this.goodMode = "smile";
|
||||
this.stress = clamp(this.stress - dt * (0.9 + touch * 1.8), 0, 130);
|
||||
this.loneliness = clamp(this.loneliness - dt * (0.4 + touch), 0, 110);
|
||||
if (this.state === "play_ball") this.foodReactTimer = Math.max(this.foodReactTimer, 0.10);
|
||||
}
|
||||
}
|
||||
|
||||
if (it.type === "trace") {
|
||||
this.loneliness -= dt * 0.25;
|
||||
this.stress += dt * 0.06;
|
||||
|
|
@ -1017,12 +1277,24 @@ class Tarinai {
|
|||
if (d < 0.01 || d > 74) continue;
|
||||
closeCount += 1;
|
||||
|
||||
if (this.zunchiDisease && !o.zunchiDisease && (o.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && Math.random() < dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) {
|
||||
if (o.infectZunchiDisease(this)) this.world.log(`${this.name}のずんち病が${o.name}にうつった。`, "accident");
|
||||
}
|
||||
if (o.zunchiDisease && !this.zunchiDisease && (this.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && Math.random() < dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) {
|
||||
if (this.infectZunchiDisease(o)) this.world.log(`${o.name}のずんち病が${this.name}にうつった。`, "accident");
|
||||
}
|
||||
|
||||
const pairBond = this.world.areCoParents?.(this, o);
|
||||
const overlap = this.radius + o.radius - d;
|
||||
if (overlap > -2) {
|
||||
const push = (overlap + 6) * 0.17;
|
||||
const push = (overlap + 6) * (pairBond ? 0.08 : 0.17);
|
||||
this.vx -= (dx / d) * push;
|
||||
this.vy -= (dy / d) * push;
|
||||
if (Math.random() < dt * 1.2) this.surpriseTimer = Math.max(this.surpriseTimer, 0.12);
|
||||
if (!pairBond && Math.random() < dt * 1.2) this.surpriseTimer = Math.max(this.surpriseTimer, 0.12);
|
||||
}
|
||||
if (pairBond && d > 26 && d < 120) {
|
||||
this.vx += (dx / d) * dt * 4.8;
|
||||
this.vy += (dy / d) * dt * 4.8;
|
||||
}
|
||||
|
||||
if (d < 60) {
|
||||
|
|
@ -1077,19 +1349,24 @@ class Tarinai {
|
|||
|
||||
const relA = this.relationTo(o.id);
|
||||
const relB = o.relationTo(this.id);
|
||||
const fightRisk = (this.mood < 55 || o.mood < 55 || this.stress > 46 || o.stress > 46 || this.type === "angry" || o.type === "angry" || closeCount > 3 || this.personality === "irritable" || o.personality === "irritable");
|
||||
const fearBrake = clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 120, 0.34, 1);
|
||||
const friendBrake = clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 90, 0.48, 1);
|
||||
const battleDrug = this.hasFightMochiEffect() || o.hasFightMochiEffect();
|
||||
const loveDrug = this.hasLoveMochiEffect() || o.hasLoveMochiEffect();
|
||||
const fightRisk = (this.mood < 55 || o.mood < 55 || this.stress > 46 || o.stress > 46 || this.type === "angry" || o.type === "angry" || closeCount > 3 || this.personality === "irritable" || o.personality === "irritable" || battleDrug || o.isZunchiSlave || this.isZunchiSlave);
|
||||
const fearBrake = battleDrug ? clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 220, 0.74, 1.15) : clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 120, 0.34, 1);
|
||||
const friendBrake = battleDrug ? clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 180, 0.72, 1.12) : clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 90, 0.48, 1);
|
||||
const personalityRisk = this.personalityProfile().fight * o.personalityProfile().fight;
|
||||
const conflictBias = (battleDrug ? 2.9 : 1) * (o.isZunchiSlave ? 2.2 : 1) * (this.isZunchiSlave ? 1.15 : 1);
|
||||
const birthPeace = (this.postBirthPeaceTimer || 0) > 0 || (o.postBirthPeaceTimer || 0) > 0;
|
||||
if (d < 56 && !birthPeace && fightRisk && this.fightCooldown <= 0 && o.fightCooldown <= 0 && Math.random() < dt * 0.23 * personalityRisk * fearBrake * friendBrake) {
|
||||
const familyFightBlocked = this.world.areParentChild ? this.world.areParentChild(this, o) : false;
|
||||
const pairFightBlocked = this.world.areCoParents ? this.world.areCoParents(this, o) : false;
|
||||
if (d < 56 && !familyFightBlocked && !pairFightBlocked && !birthPeace && fightRisk && this.fightCooldown <= 0 && o.fightCooldown <= 0 && Math.random() < dt * 0.23 * personalityRisk * fearBrake * friendBrake * conflictBias) {
|
||||
this.world.startConflict(this, o);
|
||||
}
|
||||
|
||||
if (d < 36 && this.reproductionTimer <= 0 && o.reproductionTimer <= 0 && this.fightTimer <= 0 && o.fightTimer <= 0) {
|
||||
const enough = this.hunger < 55 && o.hunger < 60 && this.energy > 44 && o.energy > 44;
|
||||
const lonelyBond = this.loneliness > 24 || o.loneliness > 24;
|
||||
if (enough && lonelyBond && Math.random() < dt * 0.25) {
|
||||
if (!this.isZunchiSlave && !o.isZunchiSlave && d < (loveDrug ? 44 : 36) && this.reproductionTimer <= 0 && o.reproductionTimer <= 0 && this.fightTimer <= 0 && o.fightTimer <= 0) {
|
||||
const enough = loveDrug ? (this.hunger < 90 && o.hunger < 92 && this.energy > 18 && o.energy > 18) : (this.hunger < 55 && o.hunger < 60 && this.energy > 44 && o.energy > 44);
|
||||
const lonelyBond = loveDrug ? (this.loneliness > 4 || o.loneliness > 4 || this.affection > 0 || o.affection > 0) : (this.loneliness > 24 || o.loneliness > 24);
|
||||
if (enough && lonelyBond && Math.random() < dt * (loveDrug ? 0.72 : 0.25)) {
|
||||
this.world.startBirthRitual(this, o);
|
||||
}
|
||||
}
|
||||
|
|
@ -1108,23 +1385,27 @@ class Tarinai {
|
|||
|
||||
move(dt) {
|
||||
let ax = 0, ay = 0;
|
||||
const baseSpeed = 31 * this.trait.speed * (this.energy < 20 ? 0.42 : 1) * (this.fallTimer > 0 ? 0.35 : 1);
|
||||
const baseSpeed = 46.5 * 1.5 * this.trait.speed * (this.energy < 20 ? 0.42 : 1) * (this.fallTimer > 0 ? 0.35 : 1);
|
||||
|
||||
if (this.state === "sleep") {
|
||||
const bedComfort = this.target?.type === "bed" ? this.world.bedComfort(this.target) : 0.72;
|
||||
const bedCrowd = this.target?.type === "bed" ? this.world.bedOccupancy(this.target) : 0;
|
||||
const bedComfort = this.isSleepFurniture(this.target) ? this.world.bedComfort(this.target) : 0.72;
|
||||
const bedCrowd = this.isSleepFurniture(this.target) ? this.world.bedOccupancy(this.target) : 0;
|
||||
const crowdedSleep = bedCrowd > 5;
|
||||
this.energy = clamp(this.energy + dt * (this.target ? 1.8 + bedComfort * 1.35 : 2.1), 0, 100);
|
||||
this.stress = clamp(this.stress + dt * (bedCrowd > 5 ? 0.18 * (bedCrowd - 5) : -0.12 * bedComfort), 0, 130);
|
||||
this.stress = clamp(this.stress + dt * (crowdedSleep ? 0.18 * Math.max(1, bedCrowd - 5) : -0.12 * bedComfort), 0, 130);
|
||||
this.hunger = clamp(this.hunger + dt * 0.018, 0, 115);
|
||||
if (this.target && !this.target.dead && this.target.type === "bed") {
|
||||
if (this.target && !this.target.dead && this.isSleepFurniture(this.target)) {
|
||||
const spot = this.sleepSpotFor(this.target);
|
||||
const dx = spot.x - this.x;
|
||||
const dy = spot.y - this.y;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d > 1.6) {
|
||||
if (d > 2.2) {
|
||||
const step = Math.min(d, dt * (12 + bedComfort * 14));
|
||||
this.x += dx / d * step;
|
||||
this.y += dy / d * step;
|
||||
} else {
|
||||
this.x = spot.x;
|
||||
this.y = spot.y;
|
||||
}
|
||||
}
|
||||
this.vx *= Math.pow(0.55, dt * 60);
|
||||
|
|
@ -1137,15 +1418,21 @@ class Tarinai {
|
|||
if (partner) {
|
||||
const midX = (this.x + partner.x) / 2;
|
||||
const midY = (this.y + partner.y) / 2;
|
||||
const offset = (this.birthRitualRole || 1) * this.radius * 0.44;
|
||||
const dx = midX - this.x;
|
||||
const dy = (midY + offset) - this.y;
|
||||
this.vx += dx * dt * 2.8;
|
||||
this.vy += dy * dt * 2.8;
|
||||
const pdx = partner.x - this.x;
|
||||
const pdy = partner.y - this.y;
|
||||
const plen = Math.max(0.001, Math.hypot(pdx, pdy));
|
||||
const nx = -pdy / plen;
|
||||
const ny = pdx / plen;
|
||||
const desiredGap = this.radius * 0.36;
|
||||
const side = this.birthRitualRole || 1;
|
||||
const tx = midX + nx * desiredGap * side;
|
||||
const ty = midY + ny * desiredGap * side;
|
||||
this.vx += (tx - this.x) * dt * 3.8;
|
||||
this.vy += (ty - this.y) * dt * 3.8;
|
||||
}
|
||||
this.vx *= Math.pow(0.56, dt * 60);
|
||||
this.vy *= Math.pow(0.56, dt * 60);
|
||||
this.x += Math.sin(this.world.time * 26 + this.age) * 0.32;
|
||||
this.vx *= Math.pow(0.48, dt * 60);
|
||||
this.vy *= Math.pow(0.48, dt * 60);
|
||||
this.x += Math.sin(this.world.time * 26 + this.age) * 0.20;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1171,9 +1458,20 @@ class Tarinai {
|
|||
}
|
||||
|
||||
if (this.target && !this.target.dead) {
|
||||
const dx = this.target.x - this.x;
|
||||
const dy = this.target.y - this.y;
|
||||
const moveTarget = this.state === "seek_bed" && this.isSleepFurniture(this.target) ? this.sleepSpotFor(this.target) : this.target;
|
||||
const dx = moveTarget.x - this.x;
|
||||
const dy = moveTarget.y - this.y;
|
||||
const d = Math.hypot(dx, dy) || 1;
|
||||
if (this.state === "seek_bed" && this.isSleepFurniture(this.target)) {
|
||||
const bedDx = this.target.x - this.x;
|
||||
const bedDy = this.target.y - this.y;
|
||||
const bedDist = Math.hypot(bedDx, bedDy);
|
||||
if (d <= Math.max(this.radius + 8, 26) || bedDist <= Math.max(this.target.r * 0.58, 20)) {
|
||||
this.state = "sleep";
|
||||
this.sleeping = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let sign = (this.state === "panic" || this.state === "cursor_enemy") ? -1 : 1;
|
||||
if (this.target?.detour) sign = 1;
|
||||
let strength = 1;
|
||||
|
|
@ -1186,6 +1484,9 @@ class Tarinai {
|
|||
} else if (this.state === "cursor_enemy") {
|
||||
strength = d < 150 ? 1.35 : 0.35;
|
||||
sign = -1;
|
||||
} else if (this.state === "play_ball") {
|
||||
strength = d < this.radius + 18 ? 1.75 : 1.28;
|
||||
sign = 1;
|
||||
} else if (this.state === "fight") {
|
||||
const headbutt = Math.sin(this.world.time * 18 + this.age * 0.37);
|
||||
if (d < 18) {
|
||||
|
|
@ -1202,6 +1503,10 @@ class Tarinai {
|
|||
this.wanderAngle += rand(-3.8, 3.8) * dt;
|
||||
ax += Math.cos(this.wanderAngle) * baseSpeed * 1.5;
|
||||
ay += Math.sin(this.wanderAngle) * baseSpeed * 1.5;
|
||||
} else if (this.state === "zunchi_sick") {
|
||||
this.wanderAngle += rand(-2.4, 2.4) * dt + Math.sin(this.world.time * 1.7 + this.age) * dt * 0.8;
|
||||
ax += Math.cos(this.wanderAngle) * baseSpeed * 0.72;
|
||||
ay += Math.sin(this.wanderAngle) * baseSpeed * 0.72;
|
||||
} else {
|
||||
ax += Math.cos(this.wanderAngle) * baseSpeed * 0.34;
|
||||
ay += Math.sin(this.wanderAngle) * baseSpeed * 0.34;
|
||||
|
|
@ -1212,7 +1517,7 @@ class Tarinai {
|
|||
this.vx *= Math.pow(0.84, dt * 8);
|
||||
this.vy *= Math.pow(0.84, dt * 8);
|
||||
|
||||
const maxV = this.state === "panic" ? 116 : 76;
|
||||
const maxV = this.state === "panic" ? 116 : (this.state === "zunchi_sick" ? 54 : 76);
|
||||
const v = Math.hypot(this.vx, this.vy);
|
||||
if (v > maxV) {
|
||||
this.vx = this.vx / v * maxV;
|
||||
|
|
@ -1222,11 +1527,19 @@ class Tarinai {
|
|||
this.x += this.vx * dt;
|
||||
this.y += this.vy * dt;
|
||||
|
||||
const p = CONFIG.worldPadding;
|
||||
if (this.entryTimer > 0) {
|
||||
this.entryTimer = Math.max(0, this.entryTimer - dt);
|
||||
if (this.x >= CONFIG.worldPadding && this.y >= CONFIG.worldPadding && this.x <= this.world.w - CONFIG.worldPadding && this.y <= this.world.h - CONFIG.worldPadding) {
|
||||
this.entryTimer = 0;
|
||||
}
|
||||
}
|
||||
const p = this.entryTimer > 0 ? -this.radius * 2.4 : CONFIG.worldPadding;
|
||||
const maxX = this.entryTimer > 0 ? this.world.w + this.radius * 2.4 : this.world.w - CONFIG.worldPadding;
|
||||
const maxY = this.entryTimer > 0 ? this.world.h + this.radius * 2.4 : this.world.h - CONFIG.worldPadding;
|
||||
if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx) * 0.5; this.wanderAngle = rand(-0.8, 0.8); this.surpriseTimer = 0.15; }
|
||||
if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy) * 0.5; this.wanderAngle = rand(0.3, 1.8); this.surpriseTimer = 0.15; }
|
||||
if (this.x > this.world.w - p) { this.x = this.world.w - p; this.vx = -Math.abs(this.vx) * 0.5; this.wanderAngle = rand(2.5, 3.8); this.surpriseTimer = 0.15; }
|
||||
if (this.y > this.world.h - p) { this.y = this.world.h - p; this.vy = -Math.abs(this.vy) * 0.5; this.wanderAngle = rand(-2.2, -0.7); this.surpriseTimer = 0.15; }
|
||||
if (this.x > maxX) { this.x = maxX; this.vx = -Math.abs(this.vx) * 0.5; this.wanderAngle = rand(2.5, 3.8); this.surpriseTimer = 0.15; }
|
||||
if (this.y > maxY) { this.y = maxY; this.vy = -Math.abs(this.vy) * 0.5; this.wanderAngle = rand(-2.2, -0.7); this.surpriseTimer = 0.15; }
|
||||
}
|
||||
|
||||
checkLife() {
|
||||
|
|
@ -1277,8 +1590,13 @@ class Tarinai {
|
|||
const blastSpin = this.blastSpinTimer > 0.04 ? this.fallDir * ((this.blastSpinMax || 1) - this.blastSpinTimer) * Math.PI * 70.0 : 0;
|
||||
const fallSpin = this.blastSpinTimer > 0.04 ? blastSpin : (fallPose > 0 ? this.fallDir * (1 - fallPose) * Math.PI * 2 : 0);
|
||||
const motionTilt = clamp(this.vx / 240, -0.22, 0.22) + (this.state === "fight" ? headbuttPulse * 0.05 : 0) + eatingShake * 0.105 + this.fallDir * fallPose * 0.18 + fallSpin;
|
||||
const insideCardboard = false;
|
||||
let drawW = w * this.scale * 0.30;
|
||||
let drawH = h * this.scale * 0.30;
|
||||
if (insideCardboard) {
|
||||
drawW *= 0.72;
|
||||
drawH *= 0.72;
|
||||
}
|
||||
if (sprite === "stretch") {
|
||||
drawW *= 1.16;
|
||||
drawH *= 1.24;
|
||||
|
|
@ -1324,6 +1642,7 @@ class Tarinai {
|
|||
ctx.save();
|
||||
ctx.translate(this.x + intimidateShake, this.y + bob + fearShakeY);
|
||||
ctx.rotate(angle);
|
||||
if (insideCardboard) ctx.globalAlpha *= 0.58;
|
||||
|
||||
if (this.world.selected === this) {
|
||||
ctx.save();
|
||||
|
|
@ -1375,13 +1694,16 @@ class Tarinai {
|
|||
|
||||
if ((this.world.performanceLevel ? this.world.performanceLevel() : 0) < 2 && (this.goodMode === "smile" || this.state === "cursor_friend") && this.mood > 58 && this.stress < 44 && this.state !== "sleep") {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = this.state === "cursor_friend" ? 0.82 : 0.34;
|
||||
const cursorLove = this.state === "cursor_friend" ? Math.max(0, this.cursorPetting || 0) : 0;
|
||||
ctx.globalAlpha = this.state === "cursor_friend" ? clamp(0.82 + cursorLove * 0.16, 0.82, 1) : 0.34;
|
||||
ctx.fillStyle = this.state === "cursor_friend" ? "#f05b87" : (lightState.nightStrength > 0.20 ? "#eef5ff" : (lightState.goldenStrength > 0.35 ? "#ffe398" : "#d6f5a9"));
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const px = Math.cos(t * 2.2 + this.age + i * 2.4) * drawW * 0.34;
|
||||
const py = -drawH * 0.24 + Math.sin(t * 2.9 + this.age * 0.2 + i) * drawH * 0.11;
|
||||
const count = this.state === "cursor_friend" ? (cursorLove > 0.18 ? 5 : 3) : 2;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const orbit = 0.24 + (i % 3) * 0.08 + cursorLove * 0.05;
|
||||
const px = Math.cos(t * 2.2 + this.age + i * 1.42) * drawW * orbit;
|
||||
const py = -drawH * (0.22 + (i % 2) * 0.06) + Math.sin(t * 2.9 + this.age * 0.2 + i) * drawH * (0.08 + cursorLove * 0.02);
|
||||
if (this.state === "cursor_friend") {
|
||||
const s = Math.max(5, drawW * 0.075);
|
||||
const s = Math.max(5, drawW * (0.070 + (i % 3) * 0.012 + cursorLove * 0.01));
|
||||
drawHeartShape(ctx, px, py, s, { fill: "rgba(240,91,135,0.90)", stroke: "rgba(255,252,246,0.92)", alpha: 1, rotation: Math.sin(t * 3 + i) * 0.18 });
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
|
|
@ -1402,7 +1724,24 @@ class Tarinai {
|
|||
const cacheH = Math.max(1, Math.round((dh * renderScale) / 8) * 8);
|
||||
const spriteCanvas = typeof getCachedSpriteCanvas === "function" ? getCachedSpriteCanvas(sprite, img, cacheW, cacheH) : img;
|
||||
ctx.drawImage(spriteCanvas, -dw / 2, -dh / 2, dw, dh);
|
||||
this.zunchiStain = 0;
|
||||
if ((this.zunchiStain || 0) > 3 && typeof getCachedStainOverlayCanvas === "function") {
|
||||
const stainCanvas = getCachedStainOverlayCanvas(sprite, img, cacheW, cacheH, clamp((this.zunchiStain || 0) / 100, 0, 1), this.zunchiStainSeed || 0.5);
|
||||
if (stainCanvas) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = clamp((this.zunchiStain || 0) / 100, 0.10, 0.88);
|
||||
ctx.drawImage(stainCanvas, -dw / 2, -dh / 2, dw, dh);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
if (this.zunchiDisease) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.12 + clamp(this.zunchiDiseaseSeverity || 0, 0, 1) * 0.10;
|
||||
ctx.fillStyle = "rgba(9, 74, 23, 0.75)";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, drawH * 0.05, drawW * 0.56, drawH * 0.38, Math.sin(t + this.age) * 0.12, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
drawDawnRimLight(ctx, dw, dh, lightState);
|
||||
if (faceRight) ctx.scale(-1, 1);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue