--
This commit is contained in:
parent
5ed977788c
commit
f500279abd
93 changed files with 1893 additions and 1198 deletions
120
js/health.js
120
js/health.js
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
const HEALTH = (() => {
|
||||
const MAJOR_DAMAGE_WINDOW = 24;
|
||||
const MAJOR_DAMAGE_REACTION_WINDOW = 1.0;
|
||||
const WEAKENED_TRACE_WINDOW = 45;
|
||||
const WEAKENED_SUFFIX = "\u3067\u8870\u5f31";
|
||||
const CAUSES = Object.freeze({
|
||||
|
|
@ -11,10 +12,10 @@ const HEALTH = (() => {
|
|||
poke: "\u3064\u3064\u304b\u308c\u3059\u304e",
|
||||
firecracker: "\u7206\u7af9",
|
||||
genkotsu: "\u3052\u3093\u3053\u3064",
|
||||
shooting: "射撃",
|
||||
stoneCrush: "石に潰された",
|
||||
weakness: "衰弱",
|
||||
poisonBlock: "毒ブロック",
|
||||
shooting: "\u5c04\u6483",
|
||||
stoneCrush: "\u77f3\u306b\u6f70\u3055\u308c\u305f",
|
||||
weakness: "\u8870\u5f31",
|
||||
poisonBlock: "\u6bd2\u30d6\u30ed\u30c3\u30af",
|
||||
accident: "\u4e8b\u6545",
|
||||
collision: "\u885d\u7a81",
|
||||
outOfBounds: "\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664",
|
||||
|
|
@ -33,18 +34,18 @@ const HEALTH = (() => {
|
|||
const text = String(reason || "");
|
||||
if (!text) return "";
|
||||
if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, "");
|
||||
if (/射撃|shoot/.test(text)) return CAUSES.shooting;
|
||||
if (/石に潰された|石.*潰|stone.*crush|crushed.*stone/.test(text)) return CAUSES.stoneCrush;
|
||||
const collisionWith = text.match(/([^、。\s]+)に衝突/);
|
||||
if (collisionWith) return `${collisionWith[1]}に衝突`;
|
||||
if (/\u5c04\u6483|shoot/.test(text)) return CAUSES.shooting;
|
||||
if (/\u77f3\u306b\u6f70\u3055\u308c\u305f|\u77f3.*\u6f70|stone.*crush|crushed.*stone/.test(text)) return CAUSES.stoneCrush;
|
||||
const collisionWith = text.match(/([^\u3001\u3002\s]+?)(?:\u306b|\u3068\u306e)\u885d\u7a81/);
|
||||
if (collisionWith) return `${collisionWith[1]}\u3068\u306e\u885d\u7a81`;
|
||||
if (/\u55a7\u5629|fight|headbutt/.test(text)) return CAUSES.fight;
|
||||
if (/\u3052\u3093\u3053\u3064|genkotsu/.test(text)) return CAUSES.genkotsu;
|
||||
if (/毒ブロック|poison.?block/.test(text)) return CAUSES.poisonBlock;
|
||||
if (/\u6bd2\u30d6\u30ed\u30c3\u30af|poison.?block/.test(text)) return CAUSES.poisonBlock;
|
||||
if (/\u5de8\u5927\u85ac|giant/.test(text)) return CAUSES.giantDrug;
|
||||
if (/\u77ee\u5c0f\u85ac|dwarf/.test(text)) return CAUSES.dwarfDrug;
|
||||
if (/\u7206\u7af9|firecracker|explosion/.test(text)) return CAUSES.firecracker;
|
||||
if (/\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664|\u753b\u9762\u5916\u3067\u524a\u9664|out.*bounds|offscreen/.test(text)) return CAUSES.outOfBounds;
|
||||
if (/\u885d\u7a81|collision|急激な速度変化/.test(text)) return CAUSES.collision;
|
||||
if (/\u885d\u7a81|collision|\u6025\u6fc0\u306a\u901f\u5ea6\u5909\u5316/.test(text)) return CAUSES.collision;
|
||||
if (/\u3064\u3064\u304b\u308c|\u3064\u3064\u304d|poke/.test(text)) return CAUSES.poke;
|
||||
if (/\u306d\u3080\u308a\u75c5|sleep.*disease/.test(text)) return CAUSES.sleepDisease;
|
||||
if (/\u7206\u767a\u75c5|explosion.*disease/.test(text)) return CAUSES.explosionDisease;
|
||||
|
|
@ -146,6 +147,89 @@ const HEALTH = (() => {
|
|||
}
|
||||
|
||||
|
||||
const MAJOR_DAMAGE_REACTIONS = Object.freeze([
|
||||
{ id: "voice_major_damage_01", text: "\u3042\u3042\u3042\u3042\uff01" },
|
||||
{ id: "voice_major_damage_02", text: "\u3048\u3048\u3048\u3048\uff01" },
|
||||
{ id: "voice_major_damage_03", text: "\u306f\u3046\u3046\u3046\uff01" },
|
||||
{ id: "voice_major_damage_04", text: "\u306f\u3046\u306f\u3046\uff01" },
|
||||
]);
|
||||
|
||||
function spawnSilentDamageBubble(t, text = "!", color = "rgba(142,58,58,0.86)") {
|
||||
const world = t?.world;
|
||||
if (!world) return false;
|
||||
// Use the normal Tarinai bubble path first so positioning, draw order, and
|
||||
// throttling match other player-visible speech bubbles. Major-damage
|
||||
// bubbles are forced because they must win over incidental bubbles such as
|
||||
// \u300c\u3076\u308a\u3085\u3063\u300d fired by the same damage event.
|
||||
if (typeof t.bubble === "function" && t.bubble(text, 1.65, color, { force: true })) {
|
||||
t._damageBubblePriorityUntil = Math.max(Number(t._damageBubblePriorityUntil || 0), (world.time || 0) + 1.55);
|
||||
return true;
|
||||
}
|
||||
if (typeof Effect === "undefined") return false;
|
||||
if ((world.effectCounts?.bubble || 0) >= (CONFIG?.bubbleLimit || 80)) return false;
|
||||
const r = Math.max(16, t.radius || 22);
|
||||
world.effects?.push(new Effect("bubble", t.x, t.y - r * 1.38, {
|
||||
vx: (Math.random() - 0.5) * 2.4,
|
||||
vy: -4.8 - Math.random() * 2.8,
|
||||
life: 2.4 + Math.random() * 0.6,
|
||||
size: 9,
|
||||
text,
|
||||
color,
|
||||
}));
|
||||
world.effectCounts.bubble = (world.effectCounts.bubble || 0) + 1;
|
||||
t.nextBubbleAt = Math.max(t.nextBubbleAt || 0, (world.time || 0) + 1.55);
|
||||
t._damageBubblePriorityUntil = Math.max(Number(t._damageBubblePriorityUntil || 0), (world.time || 0) + 1.55);
|
||||
world.drawListDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function maybeTriggerMajorDamageReaction(t, actualLoss = 0, cause = "") {
|
||||
if (!t || !t.world || t.dead) return false;
|
||||
const maxEnergy = Math.max(1, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100));
|
||||
const loss = Math.max(0, Number(actualLoss) || 0);
|
||||
if (loss <= 0.01) return false;
|
||||
const now = t.world.time || 0;
|
||||
const cutoff = now - MAJOR_DAMAGE_REACTION_WINDOW;
|
||||
const entries = Array.isArray(t.recentDamageWindow) ? t.recentDamageWindow : [];
|
||||
entries.push({ at: now, amount: loss, cause: cause || "" });
|
||||
const kept = [];
|
||||
let total = 0;
|
||||
let dominant = { amount: 0, cause: cause || "" };
|
||||
for (const entry of entries) {
|
||||
const at = Number(entry?.at || 0);
|
||||
const amount = Math.max(0, Number(entry?.amount) || 0);
|
||||
if (at < cutoff || amount <= 0) continue;
|
||||
kept.push({ at, amount, cause: entry?.cause || "" });
|
||||
total += amount;
|
||||
if (amount >= dominant.amount) dominant = { amount, cause: entry?.cause || "" };
|
||||
}
|
||||
t.recentDamageWindow = kept.slice(-20);
|
||||
if (total < maxEnergy * 0.30) return false;
|
||||
const majorCause = causeLabel(dominant.cause || cause) || causeLabel(cause) || CAUSES.accident;
|
||||
t.lastMajorDamageCause = majorCause;
|
||||
t.lastMajorDamageAmount = total;
|
||||
t.lastMajorDamageAt = now;
|
||||
if (now < Number(t.nextMajorDamageVoiceAt || -999)) return false;
|
||||
t.nextMajorDamageVoiceAt = now + 1.0;
|
||||
const list = MAJOR_DAMAGE_REACTIONS;
|
||||
const reaction = list[Math.floor(Math.random() * list.length)] || list[0];
|
||||
audio.play?.(reaction.id, { category: "voice", minGap: 0.85 });
|
||||
spawnSilentDamageBubble(t, reaction.text, "rgba(146,58,58,0.88)");
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 1.55);
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 2.6);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.55);
|
||||
if (t.enterPanic) t.enterPanic({
|
||||
threat: null,
|
||||
reason: "\u5927\u304d\u306a\u30c0\u30e1\u30fc\u30b8\u3067\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b",
|
||||
fear: 1.12,
|
||||
wake: true,
|
||||
hurtTimer: 1.45,
|
||||
surpriseTimer: 0.55,
|
||||
cause: "major_damage",
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function maybeReleaseZunchiFromDamage(t, actualLoss = 0, cause = "") {
|
||||
if (!t || !t.world || t.dead) return false;
|
||||
const loss = Math.max(0, Number(actualLoss) || 0);
|
||||
|
|
@ -157,7 +241,7 @@ const HEALTH = (() => {
|
|||
t.nextDamageZunchiAt = now + Math.max(0.55, 1.55 - Math.min(0.9, loss / 38));
|
||||
if (t.hasLodgedPinEffect?.("blocksZunchi")) {
|
||||
t.oshiriByoZunchiStock = Math.min(24, Math.max(0, t.oshiriByoZunchiStock || 0) + 1);
|
||||
t.thought = "ダメージでずんちを我慢している";
|
||||
t.thought = "\u30c0\u30e1\u30fc\u30b8\u3067\u305a\u3093\u3061\u3092\u6211\u6162\u3057\u3066\u3044\u308b";
|
||||
return false;
|
||||
}
|
||||
const side = t.facingDir ? t.facingDir() : (t.facing || 1);
|
||||
|
|
@ -168,10 +252,14 @@ const HEALTH = (() => {
|
|||
t.poopCount = (t.poopCount || 0) + 1;
|
||||
t.digest = Math.max(0, Number(t.digest || 0) - 1.0);
|
||||
t.nextPoopBubbleAt = now + 2.0;
|
||||
t.bubble?.("ぶりゅっ", 1.1, "rgba(74,91,50,0.78)", { force: true });
|
||||
// If this zunchi release was caused by a major-damage event, keep the pain
|
||||
// voice bubble visible and suppress the incidental \u300c\u3076\u308a\u3085\u3063\u300d bubble.
|
||||
if (now >= Number(t._damageBubblePriorityUntil || -999)) {
|
||||
t.bubble?.("\u3076\u308a\u3085\u3063", 1.1, "rgba(74,91,50,0.78)", { force: true });
|
||||
}
|
||||
if (now > Number(t.nextDamageZunchiLogAt || -999)) {
|
||||
t.nextDamageZunchiLogAt = now + 10;
|
||||
t.world.log?.(`${t.name}はダメージでずんちを放出した。`, "accident", { participants: [t] });
|
||||
t.world.log?.(`${t.name}\u306f\u30c0\u30e1\u30fc\u30b8\u3067\u305a\u3093\u3061\u3092\u653e\u51fa\u3057\u305f\u3002`, "accident", { participants: [t] });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -189,7 +277,8 @@ const HEALTH = (() => {
|
|||
const plushie = (t.world?.itemsOfType?.("plushie") || t.world?.items || []).find(item => item && !item.dead && item.isStructure && item.type === "plushie" && item.ownerId === t.id && item.carriedById === t.id);
|
||||
if (plushie && actualLoss >= 1) plushie.damage(actualLoss, t.world, null);
|
||||
if (cause === CAUSES.fight || /\u55a7\u5629|fight/.test(String(reason || ""))) t.recordBleedExposure();
|
||||
audio.damage?.(actualLoss);
|
||||
const majorDamageReacted = maybeTriggerMajorDamageReaction(t, actualLoss, cause || reason);
|
||||
if (!majorDamageReacted) audio.damage?.(actualLoss);
|
||||
maybeReleaseZunchiFromDamage(t, actualLoss, cause || reason);
|
||||
if (t.sleepDisease) t.recoverSleepDisease("\u30c0\u30e1\u30fc\u30b8\u3067\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f");
|
||||
if (t.wakeFromDamage) t.wakeFromDamage(cause || reason);
|
||||
|
|
@ -197,7 +286,7 @@ const HEALTH = (() => {
|
|||
t.showHpBar();
|
||||
}
|
||||
if (t.energy <= 0.5) {
|
||||
t.die(normalizeDeathReason(t, reason || cause, { fromDamage: true }));
|
||||
t.die(reason || cause, { fromDamage: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -221,6 +310,7 @@ const HEALTH = (() => {
|
|||
applyDamage,
|
||||
briefDeathReason,
|
||||
maybeReleaseZunchiFromDamage,
|
||||
maybeTriggerMajorDamageReaction,
|
||||
});
|
||||
})();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue