mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-18 13:20:02 +08:00
fixed # 修复手机端地图滑动异常的问题
fixed # 重构更新框架,支持选择是否下载梦三模式 fixed # 补充部分意图的资源图片与逻辑
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
attackHitCount,
|
||||
outgoingAttackDamage,
|
||||
} from "./intent-damage.js";
|
||||
|
||||
const pendingDeathBlows = new WeakSet();
|
||||
|
||||
export function isDeathBlowIntent(intent) {
|
||||
return intent?.intentType === "death_blow";
|
||||
}
|
||||
|
||||
export function hasPendingDeathBlow(source) {
|
||||
return pendingDeathBlows.has(source);
|
||||
}
|
||||
|
||||
export async function beginDeathBlow(source, target, intent) {
|
||||
if (!isDeathBlowIntent(intent)) {
|
||||
throw new TypeError("濒死一击需要 death_blow 意图类型");
|
||||
}
|
||||
if (attackHitCount(intent) !== 1) {
|
||||
throw new RangeError("濒死一击只能造成一次伤害");
|
||||
}
|
||||
const damage = outgoingAttackDamage(intent, source);
|
||||
if (!source?.isAlive() || !target?.isAlive()) return false;
|
||||
|
||||
const hit = target.damage(damage, source);
|
||||
hit.mengsanAttack_shuying = true;
|
||||
await hit;
|
||||
if (source.isAlive()) pendingDeathBlows.add(source);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function finishDeathBlow(source) {
|
||||
if (!hasPendingDeathBlow(source)) return false;
|
||||
pendingDeathBlows.delete(source);
|
||||
if (source.isAlive()) await source.die();
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export function attackHitCount(intent) {
|
||||
const hits = intent.hits ?? 1;
|
||||
if (!Number.isInteger(hits) || hits < 1) {
|
||||
throw new RangeError("攻击段数必须是正整数");
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
export function outgoingAttackDamage(intent, source) {
|
||||
if (!Number.isInteger(intent.damage) || intent.damage < 0) {
|
||||
throw new RangeError("预测伤害必须是非负整数");
|
||||
}
|
||||
const storedStrength = source?.storage?.mengsanStrength_shuying;
|
||||
const strength = Number.isInteger(storedStrength) ? storedStrength : 0;
|
||||
let damage = Math.max(0, intent.damage + strength);
|
||||
if (source?.storage?.mengsanWeak_shuying > 0) {
|
||||
damage = Math.floor(damage * 0.75);
|
||||
}
|
||||
return damage;
|
||||
}
|
||||
|
||||
export function previewAttackDamage(intent, source, target) {
|
||||
let damage = outgoingAttackDamage(intent, source);
|
||||
if (target?.storage?.mengsanVulnerable_shuying > 0) {
|
||||
damage = Math.ceil(damage * 1.5);
|
||||
}
|
||||
return damage;
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
// 只呈现已经选定的预测技能;不从手牌或原生技能猜测意图。
|
||||
import { attackHitCount, previewAttackDamage } from "./intent-damage.js";
|
||||
import { isDeathBlowIntent } from "./death-blow.js";
|
||||
import { isStunIntent } from "./stun-intent.js";
|
||||
|
||||
export function attackIconForDamage(damage) {
|
||||
if (!Number.isInteger(damage) || damage < 0) throw new RangeError("预测伤害必须是非负整数");
|
||||
if (!Number.isInteger(damage) || damage < 0) {
|
||||
throw new RangeError("预测伤害必须是非负整数");
|
||||
}
|
||||
if (damage < 5) return 1;
|
||||
if (damage < 10) return 2;
|
||||
if (damage < 20) return 3;
|
||||
@@ -8,56 +14,111 @@ export function attackIconForDamage(damage) {
|
||||
return 5;
|
||||
}
|
||||
|
||||
export function mountEnemyIntent(player, session, assetBase, document) {
|
||||
export function mountEnemyIntent(player, session, assetBase, document,
|
||||
getTarget = () => null) {
|
||||
const badge = document.createElement("div");
|
||||
badge.className = "mengsan-intent-shuying";
|
||||
badge.hidden = true;
|
||||
badge.setAttribute("aria-live", "polite");
|
||||
const name = document.createElement("span");
|
||||
name.className = "mengsan-intent-name-shuying";
|
||||
badge.setAttribute("aria-atomic", "true");
|
||||
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";
|
||||
icon.alt = "预计伤害";
|
||||
icon.alt = "Attack";
|
||||
icon.decoding = "async";
|
||||
damageLine.append(amount, icon);
|
||||
damageLine.append(icon, amount);
|
||||
const debuffLine = document.createElement("span");
|
||||
debuffLine.className = "mengsan-intent-debuff-shuying";
|
||||
const debuffIcon = document.createElement("img");
|
||||
debuffIcon.className = "mengsan-intent-icon-shuying";
|
||||
debuffIcon.alt = "预计施加负面状态";
|
||||
debuffIcon.alt = "Debuff";
|
||||
debuffIcon.decoding = "async";
|
||||
debuffIcon.src = `${assetBase}/assets/intent/Intent_debuff.webp`;
|
||||
const debuffAmount = document.createElement("span");
|
||||
debuffLine.append(debuffIcon, debuffAmount);
|
||||
badge.append(name, damageLine, debuffLine);
|
||||
badge.append(damageLine, debuffLine);
|
||||
player.appendChild(badge);
|
||||
session.ownResource(badge, () => badge.remove());
|
||||
return intent => {
|
||||
let currentIntent = null;
|
||||
let timer;
|
||||
session.ownResource(badge, () => {
|
||||
clearInterval(timer);
|
||||
badge.remove();
|
||||
});
|
||||
const refresh = () => {
|
||||
const intent = currentIntent;
|
||||
if (!intent) {
|
||||
badge.hidden = true;
|
||||
badge.removeAttribute("title");
|
||||
return;
|
||||
}
|
||||
if (typeof intent.name !== "string" || !intent.name.trim()) throw new TypeError("预测技能必须有名称");
|
||||
name.textContent = intent.name;
|
||||
const hasDamage = intent.damage != null;
|
||||
damageLine.hidden = !hasDamage;
|
||||
const hasDebuff = typeof intent.debuff === "string" && Number.isInteger(intent.stacks) && intent.stacks > 0;
|
||||
debuffLine.hidden = !hasDebuff;
|
||||
if (hasDebuff) debuffAmount.textContent = `${intent.debuff}×${intent.stacks}`;
|
||||
if (hasDamage) {
|
||||
const tier = attackIconForDamage(intent.damage);
|
||||
amount.textContent = String(intent.damage);
|
||||
const src = `${assetBase}/assets/intent/Intent_attack_${tier}.webp`;
|
||||
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;
|
||||
badge.title = `${intent.name}:预计造成 ${intent.damage} 点伤害(非最终伤害)${hasDebuff ? `,施加${intent.stacks}层${intent.debuff}` : ""}`;
|
||||
icon.alt = "Stunned";
|
||||
if (badge.getAttribute("aria-label") !== "Stunned") {
|
||||
badge.setAttribute("aria-label", "Stunned");
|
||||
}
|
||||
badge.hidden = false;
|
||||
return;
|
||||
}
|
||||
const hasDamage = intent.damage != null;
|
||||
const deathBlow = isDeathBlowIntent(intent);
|
||||
if (deathBlow && !hasDamage) {
|
||||
throw new TypeError("濒死一击必须有伤害数值");
|
||||
}
|
||||
damageLine.hidden = !hasDamage;
|
||||
const hasDebuff = typeof intent.debuff === "string" &&
|
||||
Number.isInteger(intent.stacks) && intent.stacks > 0;
|
||||
debuffLine.hidden = !hasDebuff;
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (hasDamage) {
|
||||
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` :
|
||||
`${assetBase}/assets/intent/Intent_attack_${tier}.webp`;
|
||||
if (icon.getAttribute("src") !== src) icon.src = src;
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
badge.title = hasDebuff ? `${intent.name}:施加${intent.stacks}层${intent.debuff}` : intent.name;
|
||||
const label = debuffAmount.textContent;
|
||||
if (badge.getAttribute("aria-label") !== label) {
|
||||
badge.setAttribute("aria-label", label);
|
||||
}
|
||||
}
|
||||
badge.hidden = false;
|
||||
};
|
||||
timer = setInterval(refresh, 100);
|
||||
return intent => {
|
||||
currentIntent = intent;
|
||||
refresh();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
const stunned = new WeakMap();
|
||||
|
||||
export const STUN_INTENT = Object.freeze({ intentType: "stun" });
|
||||
|
||||
export function isStunIntent(intent) {
|
||||
return intent?.intentType === "stun";
|
||||
}
|
||||
|
||||
export function isStunned(player) {
|
||||
return stunned.has(player);
|
||||
}
|
||||
|
||||
export function applyStun(player, intent, options = {}) {
|
||||
if (stunned.has(player)) return false;
|
||||
const { resume = "advance", recover } = options;
|
||||
if (resume !== "advance" && resume !== "retry") {
|
||||
throw new RangeError("击晕恢复方式无效");
|
||||
}
|
||||
if (typeof recover !== "function") {
|
||||
throw new TypeError("击晕需要恢复回调");
|
||||
}
|
||||
if (!player?.isAlive()) return false;
|
||||
stunned.set(player, { intent, resume, recover, skipped: false });
|
||||
return true;
|
||||
}
|
||||
|
||||
export function skipStunnedAction(player, phaseUse) {
|
||||
const state = stunned.get(player);
|
||||
if (!state || state.skipped) return false;
|
||||
phaseUse.cancel();
|
||||
state.skipped = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function finishStunnedTurn(player) {
|
||||
const state = stunned.get(player);
|
||||
if (!state?.skipped) return false;
|
||||
stunned.delete(player);
|
||||
if (player.isAlive()) await state.recover(state.resume, state.intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function clearStun(player) {
|
||||
return stunned.delete(player);
|
||||
}
|
||||
Reference in New Issue
Block a user