mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-09-29 11:33:02 +08:00
🎉 2.0.1.0版本发布
✨ 重构活动BOSS武将包并新增独立PVP武将包 ✨ 完善天书动态难度、过关奖励与羁绊系统 🎨 新增自定义技能选择、羁绊谱册及移动端适配 🔊 整理BOSS配音、图片与模块资源路径 🐛 修复天书关卡、伤害保护及武将包加载问题
This commit is contained in:
+338
@@ -223,4 +223,342 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
|
||||
renderGrid();
|
||||
});
|
||||
};
|
||||
|
||||
// 创建可复用的对局单选界面;单击选中卡片,确认后返回对应键值。
|
||||
shuYing.chooseSingleOptionDialog = ({ title = "请选择", intro = "", options = [], defaultKey = null } = {}) => {
|
||||
return new Promise(resolve => {
|
||||
const items = options.map(option => typeof option == "string" ? { key: option, name: option } : option)
|
||||
.filter(option => option?.key);
|
||||
if (!items.length) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
const createNode = (tag, className, text, parent) => {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text !== undefined && text !== null) node.textContent = text;
|
||||
if (parent) parent.appendChild(node);
|
||||
return node;
|
||||
};
|
||||
const mask = createNode("div", "shuYing-option-dialog-mask", null, document.body);
|
||||
const dialog = createNode("div", "shuYing-option-dialog", null, mask);
|
||||
createNode("div", "shuYing-option-dialog-title", title, dialog);
|
||||
if (intro) createNode("div", "shuYing-option-dialog-intro", intro, dialog);
|
||||
const optionArea = createNode("div", "shuYing-option-dialog-options", null, dialog);
|
||||
const controls = createNode("div", "shuYing-option-dialog-controls", null, dialog);
|
||||
let closed = false;
|
||||
let selectedKey = defaultKey && items.some(item => item.key == defaultKey) ? defaultKey : items[0].key;
|
||||
const finish = key => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
mask.remove();
|
||||
resolve(key);
|
||||
};
|
||||
const refreshSelection = () => {
|
||||
optionArea.querySelectorAll(".shuYing-option-dialog-card").forEach(card => {
|
||||
const selected = card.dataset.key == selectedKey;
|
||||
card.classList.toggle("selected", selected);
|
||||
card.setAttribute("aria-pressed", String(selected));
|
||||
});
|
||||
};
|
||||
items.forEach((option, index) => {
|
||||
const card = createNode("button", "shuYing-option-dialog-card", null, optionArea);
|
||||
card.type = "button";
|
||||
card.dataset.key = option.key;
|
||||
card.dataset.tone = option.tone || option.key;
|
||||
createNode("span", "shuYing-option-dialog-order", String(index + 1).padStart(2, "0"), card);
|
||||
createNode("span", "shuYing-option-dialog-name", option.name || option.key, card);
|
||||
if (option.tag) createNode("span", "shuYing-option-dialog-tag", option.tag, card);
|
||||
if (option.description) createNode("span", "shuYing-option-dialog-description", option.description, card);
|
||||
card.addEventListener("click", event => {
|
||||
event.stopPropagation();
|
||||
selectedKey = option.key;
|
||||
refreshSelection();
|
||||
});
|
||||
});
|
||||
const confirm = createNode("button", "shuYing-option-dialog-confirm", "确定", controls);
|
||||
confirm.type = "button";
|
||||
confirm.addEventListener("click", event => {
|
||||
event.stopPropagation();
|
||||
finish(selectedKey);
|
||||
});
|
||||
mask.addEventListener("click", event => event.stopPropagation());
|
||||
dialog.addEventListener("click", event => event.stopPropagation());
|
||||
refreshSelection();
|
||||
const initial = Array.from(optionArea.querySelectorAll(".shuYing-option-dialog-card")).find(node => node.dataset.key == selectedKey)
|
||||
|| optionArea.querySelector(".shuYing-option-dialog-card");
|
||||
initial?.focus({ preventScroll: true });
|
||||
});
|
||||
};
|
||||
|
||||
// 创建可复用的技能奖励选择界面;悬停预览,单击选中,确认后返回技能。
|
||||
shuYing.chooseSkillRewardDialog = ({ title = "请选择技能", highlightText = "", choices = [], selectedSkills = [], bonds = [], player = null, canRefresh = false } = {}) => {
|
||||
return new Promise(resolve => {
|
||||
const skills = choices.filter(skill => typeof skill == "string" && skill);
|
||||
if (!skills.length) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
const createNode = (tag, className, text, parent) => {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text !== undefined && text !== null) node.textContent = text;
|
||||
if (parent) parent.appendChild(node);
|
||||
return node;
|
||||
};
|
||||
const getSkillInfo = skill => {
|
||||
if (typeof get.skillInfoTranslation == "function") {
|
||||
const info = get.skillInfoTranslation(skill, player);
|
||||
if (info) return info;
|
||||
}
|
||||
return lib.translate[`${skill}_info`] || "暂无技能描述";
|
||||
};
|
||||
const getPreviewSkills = previewSkill => Array.from(new Set(selectedSkills.concat(previewSkill || []).filter(Boolean)));
|
||||
const getUniqueSkillNames = currentSkills => Array.from(new Set(
|
||||
currentSkills.map(skill => lib.translate[skill]).filter(name => typeof name == "string" && name)
|
||||
));
|
||||
const getCharacterNames = () => Array.from(new Set(
|
||||
[player?.name, player?.name1, player?.name2]
|
||||
.filter(name => typeof name == "string" && name)
|
||||
.map(name => lib.translate[name] || get.translation(name))
|
||||
.filter(name => typeof name == "string" && name)
|
||||
));
|
||||
const getKeywordCount = (keyword, currentSkills) => {
|
||||
const marker = `【${keyword}】`;
|
||||
const matchedNames = new Set();
|
||||
currentSkills.forEach(skill => {
|
||||
if (getSkillInfo(skill).includes(marker)) matchedNames.add(lib.translate[skill] || skill);
|
||||
});
|
||||
return matchedNames.size;
|
||||
};
|
||||
const getLevelCondition = (bond, level) => {
|
||||
const source = level?.condition || bond?.condition;
|
||||
if (source) {
|
||||
const condition = { ...source };
|
||||
if (condition.count == null && level?.count != null) condition.count = level.count;
|
||||
return condition;
|
||||
}
|
||||
return bond?.keyword ? { type: "keyword", keyword: bond.keyword, count: level?.count } : null;
|
||||
};
|
||||
const matchesCondition = (condition, currentSkills) => {
|
||||
if (!condition || typeof condition != "object") return false;
|
||||
if (!condition.type && Array.isArray(condition.all)) return condition.all.every(item => matchesCondition(item, currentSkills));
|
||||
if (!condition.type && Array.isArray(condition.any)) return condition.any.some(item => matchesCondition(item, currentSkills));
|
||||
if (!condition.type && condition.not) return !matchesCondition(condition.not, currentSkills);
|
||||
if (condition.type == "keyword") return getKeywordCount(condition.keyword, currentSkills) >= Math.max(1, Number(condition.count) || 1);
|
||||
if (condition.type == "skill") return currentSkills.includes(condition.skill);
|
||||
if (condition.type == "skillName") return currentSkills.some(skill => lib.translate[skill] == condition.name);
|
||||
if (condition.type == "skills") {
|
||||
const all = Array.isArray(condition.all) ? condition.all : [];
|
||||
const any = Array.isArray(condition.any) ? condition.any : [];
|
||||
if (all.length && !all.every(skill => currentSkills.includes(skill))) return false;
|
||||
if (any.length && !any.some(skill => currentSkills.includes(skill))) return false;
|
||||
return Boolean(all.length || any.length);
|
||||
}
|
||||
if (condition.type == "skillNames") {
|
||||
const names = new Set(getUniqueSkillNames(currentSkills));
|
||||
const all = Array.from(new Set(Array.isArray(condition.all) ? condition.all.filter(Boolean) : []));
|
||||
const any = Array.from(new Set(Array.isArray(condition.any) ? condition.any.filter(Boolean) : []));
|
||||
if (all.length && !all.every(name => names.has(name))) return false;
|
||||
const matchedAny = any.filter(name => names.has(name));
|
||||
const need = Number(condition.count);
|
||||
if (Number.isFinite(need) && need > 0) {
|
||||
const matched = new Set(all.filter(name => names.has(name)).concat(matchedAny));
|
||||
return matched.size >= Math.max(1, Math.floor(need));
|
||||
}
|
||||
if (any.length && !matchedAny.length) return false;
|
||||
return Boolean(all.length || any.length);
|
||||
}
|
||||
if (condition.type == "characterSkillNames") {
|
||||
const characters = Array.from(new Set((Array.isArray(condition.characters) ? condition.characters : [condition.character]).filter(Boolean)));
|
||||
if (!characters.some(keyword => getCharacterNames().some(name => name.includes(keyword)))) return false;
|
||||
const names = new Set(getUniqueSkillNames(currentSkills));
|
||||
const all = Array.from(new Set(Array.isArray(condition.all) ? condition.all.filter(Boolean) : []));
|
||||
const any = Array.from(new Set(Array.isArray(condition.any) ? condition.any.filter(Boolean) : []));
|
||||
if (all.length && !all.every(name => names.has(name))) return false;
|
||||
if (any.length && !any.some(name => names.has(name))) return false;
|
||||
return Boolean(all.length || any.length);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const isConditionRelevant = (condition, currentSkills) => {
|
||||
if (!condition || typeof condition != "object") return false;
|
||||
if (!condition.type && Array.isArray(condition.all)) return condition.all.some(item => isConditionRelevant(item, currentSkills));
|
||||
if (!condition.type && Array.isArray(condition.any)) return condition.any.some(item => isConditionRelevant(item, currentSkills));
|
||||
if (!condition.type && condition.not) return false;
|
||||
if (condition.type == "keyword") return getKeywordCount(condition.keyword, currentSkills) > 0;
|
||||
if (condition.type == "skill") return currentSkills.includes(condition.skill);
|
||||
if (condition.type == "skillName") return currentSkills.some(skill => lib.translate[skill] == condition.name);
|
||||
if (condition.type == "skills") return [...(condition.all || []), ...(condition.any || [])].some(skill => currentSkills.includes(skill));
|
||||
if (condition.type == "skillNames") {
|
||||
const names = new Set(getUniqueSkillNames(currentSkills));
|
||||
return [...(condition.all || []), ...(condition.any || [])].some(name => names.has(name));
|
||||
}
|
||||
if (condition.type == "characterSkillNames") {
|
||||
const characters = Array.isArray(condition.characters) ? condition.characters : [condition.character];
|
||||
return characters.some(keyword => getCharacterNames().some(name => name.includes(keyword)))
|
||||
&& [...(condition.all || []), ...(condition.any || [])].some(name => getUniqueSkillNames(currentSkills).includes(name));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const getConditionText = (condition, currentSkills) => {
|
||||
if (!condition || typeof condition != "object") return "未配置激活条件";
|
||||
if (!condition.type && Array.isArray(condition.all)) return condition.all.map(item => getConditionText(item, currentSkills)).join(";");
|
||||
if (!condition.type && Array.isArray(condition.any)) return `满足任意一项:${condition.any.map(item => getConditionText(item, currentSkills)).join(";")}`;
|
||||
if (!condition.type && condition.not) return `排除:${getConditionText(condition.not, currentSkills)}`;
|
||||
if (condition.type == "keyword") {
|
||||
const count = getKeywordCount(condition.keyword, currentSkills);
|
||||
const need = Math.max(1, Number(condition.count) || 1);
|
||||
return `包含【${condition.keyword}】的技能 ${Math.min(count, need)}/${need}`;
|
||||
}
|
||||
if (condition.type == "skill") return `${currentSkills.includes(condition.skill) ? "已" : "未"}选择【${get.translation(condition.skill)}】`;
|
||||
if (condition.type == "skillName") return `${currentSkills.some(skill => lib.translate[skill] == condition.name) ? "已" : "未"}选择任意版本的【${condition.name}】`;
|
||||
if (condition.type == "skills") {
|
||||
const all = Array.isArray(condition.all) ? condition.all : [];
|
||||
const any = Array.isArray(condition.any) ? condition.any : [];
|
||||
if (all.length) return `指定技能 ${all.filter(skill => currentSkills.includes(skill)).length}/${all.length}`;
|
||||
if (any.length) return `任选其一:${any.map(skill => `【${get.translation(skill)}】`).join("、")}`;
|
||||
}
|
||||
if (condition.type == "skillNames") {
|
||||
const names = new Set(getUniqueSkillNames(currentSkills));
|
||||
const all = Array.from(new Set(Array.isArray(condition.all) ? condition.all.filter(Boolean) : []));
|
||||
const any = Array.from(new Set(Array.isArray(condition.any) ? condition.any.filter(Boolean) : []));
|
||||
const candidates = Array.from(new Set(all.concat(any)));
|
||||
const matched = candidates.filter(name => names.has(name)).length;
|
||||
const need = Number(condition.count);
|
||||
if (Number.isFinite(need) && need > 0) return `不同技能名称 ${Math.min(matched, Math.floor(need))}/${Math.floor(need)}`;
|
||||
if (all.length) return `同名技能 ${all.filter(name => names.has(name)).length}/${all.length}`;
|
||||
if (any.length) return `任选任意版本:${any.map(name => `【${name}】`).join("、")}`;
|
||||
}
|
||||
if (condition.type == "characterSkillNames") {
|
||||
const characters = Array.from(new Set((Array.isArray(condition.characters) ? condition.characters : [condition.character]).filter(Boolean)));
|
||||
const names = new Set(getUniqueSkillNames(currentSkills));
|
||||
const all = Array.from(new Set(Array.isArray(condition.all) ? condition.all.filter(Boolean) : []));
|
||||
const any = Array.from(new Set(Array.isArray(condition.any) ? condition.any.filter(Boolean) : []));
|
||||
const characterReady = characters.some(keyword => getCharacterNames().some(name => name.includes(keyword)));
|
||||
const skillText = all.length
|
||||
? `指定技能 ${all.filter(name => names.has(name)).length}/${all.length}`
|
||||
: `任选技能:${any.map(name => `【${name}】`).join("、")}`;
|
||||
return `${characterReady ? "武将名称已匹配" : `武将名称需包含【${characters.join("】或【")}】`};${skillText}`;
|
||||
}
|
||||
return "未配置激活条件";
|
||||
};
|
||||
const getLevelSkills = level => {
|
||||
if (Array.isArray(level?.skills)) return level.skills.filter(Boolean);
|
||||
return level?.skill ? [level.skill] : [];
|
||||
};
|
||||
const getLevelText = level => {
|
||||
if (level?.text) return level.text;
|
||||
const rewardSkills = getLevelSkills(level);
|
||||
if (rewardSkills.length) return `获得技能【${rewardSkills.map(skill => get.translation(skill)).join("、")}】`;
|
||||
return "羁绊效果待配置";
|
||||
};
|
||||
|
||||
const mask = createNode("div", "shuYing-skill-reward-mask", null, document.body);
|
||||
const dialog = createNode("div", "shuYing-skill-reward-dialog", null, mask);
|
||||
const titleNode = createNode("div", "shuYing-skill-reward-title", null, dialog);
|
||||
const highlightIndex = highlightText ? title.indexOf(highlightText) : -1;
|
||||
if (highlightIndex < 0) titleNode.textContent = title;
|
||||
else {
|
||||
titleNode.append(document.createTextNode(title.slice(0, highlightIndex)));
|
||||
createNode("span", "shuYing-skill-reward-title-character", highlightText, titleNode);
|
||||
titleNode.append(document.createTextNode(title.slice(highlightIndex + highlightText.length)));
|
||||
}
|
||||
const content = createNode("div", "shuYing-skill-reward-content", null, dialog);
|
||||
const preview = createNode("section", "shuYing-skill-reward-preview", null, content);
|
||||
const previewName = createNode("div", "shuYing-skill-reward-name", "", preview);
|
||||
const previewInfo = createNode("div", "shuYing-skill-reward-info", "", preview);
|
||||
const bondPanel = createNode("aside", "shuYing-skill-reward-bonds", null, content);
|
||||
createNode("div", "shuYing-skill-reward-bond-title", "羁绊", bondPanel);
|
||||
const bondList = createNode("div", "shuYing-skill-reward-bond-list", null, bondPanel);
|
||||
const choiceArea = createNode("div", "shuYing-skill-reward-choices", null, dialog);
|
||||
const controls = createNode("div", "shuYing-skill-reward-controls", null, dialog);
|
||||
let closed = false;
|
||||
let previewSkill = skills[0];
|
||||
let selectedSkill = skills[0];
|
||||
|
||||
const renderBonds = () => {
|
||||
bondList.innerHTML = "";
|
||||
if (!bonds.length) {
|
||||
createNode("div", "shuYing-skill-reward-bond-empty", "暂无可激活羁绊", bondList);
|
||||
return;
|
||||
}
|
||||
bonds.forEach(bond => {
|
||||
const levels = (bond.levels || []).map((level, index) => ({
|
||||
...level,
|
||||
rank: Number(level.level ?? level.count ?? index + 1) || index + 1,
|
||||
})).sort((a, b) => a.rank - b.rank);
|
||||
if (!levels.length) return;
|
||||
const currentSkills = getPreviewSkills(previewSkill);
|
||||
if (!levels.some(level => isConditionRelevant(getLevelCondition(bond, level), currentSkills))) return;
|
||||
const reached = levels.filter(level => matchesCondition(getLevelCondition(bond, level), currentSkills));
|
||||
const active = reached[reached.length - 1] || null;
|
||||
const next = levels.find(level => level.rank > (active?.rank || 0)) || null;
|
||||
const item = createNode("div", `shuYing-skill-reward-bond${active ? " active" : ""}`, null, bondList);
|
||||
const head = createNode("div", "shuYing-skill-reward-bond-head", null, item);
|
||||
createNode("span", "shuYing-skill-reward-bond-name", bond.name || (bond.keyword ? `【${bond.keyword}】羁绊` : bond.id || "未命名羁绊"), head);
|
||||
createNode("span", "shuYing-skill-reward-bond-count", active ? `第${active.rank}级` : "待激活", head);
|
||||
const status = active ? `已激活:${getLevelText(active)}` : getConditionText(getLevelCondition(bond, next), currentSkills);
|
||||
createNode("div", "shuYing-skill-reward-bond-status", status, item);
|
||||
if (next) {
|
||||
createNode("div", "shuYing-skill-reward-bond-effect", `下一阶段:${getLevelText(next)}`, item);
|
||||
if (active) createNode("div", "shuYing-skill-reward-bond-effect", getConditionText(getLevelCondition(bond, next), currentSkills), item);
|
||||
}
|
||||
});
|
||||
if (!bondList.childNodes.length) createNode("div", "shuYing-skill-reward-bond-empty", "该技能暂无相关羁绊", bondList);
|
||||
};
|
||||
|
||||
const renderPreview = skill => {
|
||||
previewSkill = skill;
|
||||
previewName.textContent = `【${get.translation(skill)}】`;
|
||||
previewInfo.innerHTML = getSkillInfo(skill);
|
||||
choiceArea.querySelectorAll(".shuYing-skill-reward-choice").forEach(node => {
|
||||
node.classList.toggle("previewing", node.dataset.skill == skill);
|
||||
node.classList.toggle("selected", node.dataset.skill == selectedSkill);
|
||||
node.setAttribute("aria-pressed", String(node.dataset.skill == selectedSkill));
|
||||
});
|
||||
renderBonds();
|
||||
};
|
||||
const finish = skill => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
mask.remove();
|
||||
resolve(skill);
|
||||
};
|
||||
|
||||
skills.forEach(skill => {
|
||||
const button = createNode("button", "shuYing-skill-reward-choice", get.translation(skill), choiceArea);
|
||||
button.type = "button";
|
||||
button.dataset.skill = skill;
|
||||
button.addEventListener("mouseenter", () => renderPreview(skill));
|
||||
button.addEventListener("focus", () => renderPreview(skill));
|
||||
button.addEventListener("click", event => {
|
||||
event.stopPropagation();
|
||||
selectedSkill = skill;
|
||||
renderPreview(skill);
|
||||
});
|
||||
});
|
||||
const confirm = createNode("button", "shuYing-skill-reward-confirm", "确定", controls);
|
||||
confirm.type = "button";
|
||||
confirm.addEventListener("click", event => {
|
||||
event.stopPropagation();
|
||||
finish(selectedSkill);
|
||||
});
|
||||
if (canRefresh) {
|
||||
const handCount = player?.countCards?.("h") || 0;
|
||||
const discardCount = Math.ceil(handCount / 2);
|
||||
const refresh = createNode("button", "shuYing-skill-reward-refresh", `刷新(弃${discardCount}张)`, controls);
|
||||
refresh.type = "button";
|
||||
refresh.title = "随机弃置一半手牌(向上取整),重新生成技能选项;本次只能使用一次";
|
||||
refresh.addEventListener("click", event => {
|
||||
event.stopPropagation();
|
||||
finish({ refresh: true });
|
||||
});
|
||||
}
|
||||
mask.addEventListener("click", event => event.stopPropagation());
|
||||
dialog.addEventListener("click", event => event.stopPropagation());
|
||||
renderPreview(previewSkill);
|
||||
choiceArea.querySelector(".shuYing-skill-reward-choice")?.focus({ preventScroll: true });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user