2026-09-29 00:29:00 +08:00
|
|
|
|
// 只呈现已经选定的预测技能;不从手牌或原生技能猜测意图。
|
2026-09-29 17:44:21 +08:00
|
|
|
|
import { attackHitCount, previewAttackDamage } from "./intent-damage.js";
|
|
|
|
|
|
import { isDeathBlowIntent } from "./death-blow.js";
|
|
|
|
|
|
import { isStunIntent } from "./stun-intent.js";
|
|
|
|
|
|
|
2026-09-29 00:29:00 +08:00
|
|
|
|
export function attackIconForDamage(damage) {
|
2026-09-29 17:44:21 +08:00
|
|
|
|
if (!Number.isInteger(damage) || damage < 0) {
|
|
|
|
|
|
throw new RangeError("预测伤害必须是非负整数");
|
|
|
|
|
|
}
|
2026-09-29 00:29:00 +08:00
|
|
|
|
if (damage < 5) return 1;
|
|
|
|
|
|
if (damage < 10) return 2;
|
|
|
|
|
|
if (damage < 20) return 3;
|
|
|
|
|
|
if (damage < 40) return 4;
|
|
|
|
|
|
return 5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-29 17:44:21 +08:00
|
|
|
|
export function mountEnemyIntent(player, session, assetBase, document,
|
|
|
|
|
|
getTarget = () => null) {
|
2026-09-29 00:29:00 +08:00
|
|
|
|
const badge = document.createElement("div");
|
|
|
|
|
|
badge.className = "mengsan-intent-shuying";
|
|
|
|
|
|
badge.hidden = true;
|
|
|
|
|
|
badge.setAttribute("aria-live", "polite");
|
2026-09-29 17:44:21 +08:00
|
|
|
|
badge.setAttribute("aria-atomic", "true");
|
2026-09-29 00:29:00 +08:00
|
|
|
|
const damageLine = document.createElement("span");
|
|
|
|
|
|
damageLine.className = "mengsan-intent-damage-shuying";
|
|
|
|
|
|
const amount = document.createElement("span");
|
|
|
|
|
|
amount.className = "mengsan-intent-amount-shuying";
|
|
|
|
|
|
const icon = document.createElement("img");
|
|
|
|
|
|
icon.className = "mengsan-intent-icon-shuying";
|
2026-09-29 17:44:21 +08:00
|
|
|
|
icon.alt = "Attack";
|
2026-09-29 00:29:00 +08:00
|
|
|
|
icon.decoding = "async";
|
2026-09-29 17:44:21 +08:00
|
|
|
|
damageLine.append(icon, amount);
|
2026-09-29 00:29:00 +08:00
|
|
|
|
const debuffLine = document.createElement("span");
|
|
|
|
|
|
debuffLine.className = "mengsan-intent-debuff-shuying";
|
|
|
|
|
|
const debuffIcon = document.createElement("img");
|
|
|
|
|
|
debuffIcon.className = "mengsan-intent-icon-shuying";
|
2026-09-29 17:44:21 +08:00
|
|
|
|
debuffIcon.alt = "Debuff";
|
2026-09-29 00:29:00 +08:00
|
|
|
|
debuffIcon.decoding = "async";
|
2026-09-29 18:15:38 +08:00
|
|
|
|
debuffIcon.src = `${assetBase}/assets/intent/Intent_debuff.png`;
|
2026-09-29 00:29:00 +08:00
|
|
|
|
const debuffAmount = document.createElement("span");
|
|
|
|
|
|
debuffLine.append(debuffIcon, debuffAmount);
|
2026-09-29 17:44:21 +08:00
|
|
|
|
badge.append(damageLine, debuffLine);
|
2026-09-29 00:29:00 +08:00
|
|
|
|
player.appendChild(badge);
|
2026-09-29 17:44:21 +08:00
|
|
|
|
let currentIntent = null;
|
|
|
|
|
|
let timer;
|
|
|
|
|
|
session.ownResource(badge, () => {
|
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
|
badge.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
const refresh = () => {
|
|
|
|
|
|
const intent = currentIntent;
|
2026-09-29 00:29:00 +08:00
|
|
|
|
if (!intent) {
|
|
|
|
|
|
badge.hidden = true;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-29 17:44:21 +08:00
|
|
|
|
if (isStunIntent(intent)) {
|
|
|
|
|
|
damageLine.hidden = false;
|
|
|
|
|
|
debuffLine.hidden = true;
|
|
|
|
|
|
amount.textContent = "";
|
|
|
|
|
|
const src = `${assetBase}/assets/intent/Intent_stun.png`;
|
|
|
|
|
|
if (icon.getAttribute("src") !== src) icon.src = src;
|
|
|
|
|
|
icon.alt = "Stunned";
|
|
|
|
|
|
if (badge.getAttribute("aria-label") !== "Stunned") {
|
|
|
|
|
|
badge.setAttribute("aria-label", "Stunned");
|
|
|
|
|
|
}
|
|
|
|
|
|
badge.hidden = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-29 00:29:00 +08:00
|
|
|
|
const hasDamage = intent.damage != null;
|
2026-09-29 17:44:21 +08:00
|
|
|
|
const deathBlow = isDeathBlowIntent(intent);
|
|
|
|
|
|
if (deathBlow && !hasDamage) {
|
|
|
|
|
|
throw new TypeError("濒死一击必须有伤害数值");
|
|
|
|
|
|
}
|
2026-09-29 00:29:00 +08:00
|
|
|
|
damageLine.hidden = !hasDamage;
|
2026-09-29 17:44:21 +08:00
|
|
|
|
const hasDebuff = typeof intent.debuff === "string" &&
|
|
|
|
|
|
Number.isInteger(intent.stacks) && intent.stacks > 0;
|
2026-09-29 00:29:00 +08:00
|
|
|
|
debuffLine.hidden = !hasDebuff;
|
2026-09-29 17:44:21 +08:00
|
|
|
|
if (hasDebuff) {
|
|
|
|
|
|
const label = intent.id === "frail" ? "Frail" :
|
|
|
|
|
|
intent.id === "vulnerable" ? "Vulnerable" : "Debuff";
|
|
|
|
|
|
const debuffText = `${label} ×${intent.stacks}`;
|
|
|
|
|
|
if (debuffAmount.textContent !== debuffText) {
|
|
|
|
|
|
debuffAmount.textContent = debuffText;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-29 00:29:00 +08:00
|
|
|
|
if (hasDamage) {
|
2026-09-29 17:44:21 +08:00
|
|
|
|
const hits = attackHitCount(intent);
|
|
|
|
|
|
if (deathBlow && hits !== 1) {
|
|
|
|
|
|
throw new RangeError("濒死一击只能造成一次伤害");
|
|
|
|
|
|
}
|
|
|
|
|
|
const damage = previewAttackDamage(intent, player, getTarget());
|
|
|
|
|
|
const tier = deathBlow ? null :
|
|
|
|
|
|
attackIconForDamage(damage * hits);
|
|
|
|
|
|
const damageText = hits > 1 ? `${damage}×${hits}` :
|
|
|
|
|
|
String(damage);
|
|
|
|
|
|
if (amount.textContent !== damageText) {
|
|
|
|
|
|
amount.textContent = damageText;
|
|
|
|
|
|
}
|
|
|
|
|
|
const src = deathBlow ?
|
|
|
|
|
|
`${assetBase}/assets/intent/Intent_death_blow.png` :
|
2026-09-29 18:15:38 +08:00
|
|
|
|
`${assetBase}/assets/intent/Intent_attack_${tier}.png`;
|
2026-09-29 00:29:00 +08:00
|
|
|
|
if (icon.getAttribute("src") !== src) icon.src = src;
|
2026-09-29 17:44:21 +08:00
|
|
|
|
icon.alt = deathBlow ? "Death Blow" : "Attack";
|
|
|
|
|
|
const label = `${deathBlow ? "Death Blow" : "Attack"}: ` +
|
|
|
|
|
|
`${damage} damage${hits > 1 ?
|
|
|
|
|
|
` × ${hits} hits` : ""}${hasDebuff ?
|
|
|
|
|
|
`, ${debuffAmount.textContent}` : ""}`;
|
|
|
|
|
|
if (badge.getAttribute("aria-label") !== label) {
|
|
|
|
|
|
badge.setAttribute("aria-label", label);
|
|
|
|
|
|
}
|
2026-09-29 00:29:00 +08:00
|
|
|
|
} else {
|
2026-09-29 17:44:21 +08:00
|
|
|
|
const label = debuffAmount.textContent;
|
|
|
|
|
|
if (badge.getAttribute("aria-label") !== label) {
|
|
|
|
|
|
badge.setAttribute("aria-label", label);
|
|
|
|
|
|
}
|
2026-09-29 00:29:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
badge.hidden = false;
|
|
|
|
|
|
};
|
2026-09-29 17:44:21 +08:00
|
|
|
|
timer = setInterval(refresh, 100);
|
|
|
|
|
|
return intent => {
|
|
|
|
|
|
currentIntent = intent;
|
|
|
|
|
|
refresh();
|
|
|
|
|
|
};
|
2026-09-29 00:29:00 +08:00
|
|
|
|
}
|