🎉 2.0.1.0版本发布

✨ 重构活动BOSS武将包并新增独立PVP武将包

✨ 完善天书动态难度、过关奖励与羁绊系统

🎨 新增自定义技能选择、羁绊谱册及移动端适配

🔊 整理BOSS配音、图片与模块资源路径

🐛 修复天书关卡、伤害保护及武将包加载问题
This commit is contained in:
2026-08-13 17:28:42 +08:00
parent 49cb75d813
commit 02e7a642ea
54 changed files with 8509 additions and 952 deletions
+2951
View File
File diff suppressed because it is too large Load Diff
+1326
View File
File diff suppressed because it is too large Load Diff
+18 -706
View File
@@ -5,7 +5,7 @@ import initShuYingMenu from "./menu.js";
export const type = "extension";
export let shuYing = new Object();
const shuYingLocalVersion = "2.0.0.9";
const shuYingLocalVersion = "2.0.1.0";
shuYing.name = "术樱包";
shuYing.editable = false;
@@ -82,7 +82,7 @@ shuYing.appendExtension = function (name, translate, obj) {
Object.assign(obj.translate, shuYing.voices || {});
oobj.translate = obj.translate;
game.import('character', () => { return oobj; });
lib.config.all.characters.push(name);
lib.config.characters.push(name);
if (!lib.config.characters.contains(name)) {
lib.config.characters.push(name);
}
@@ -112,26 +112,6 @@ shuYing.mergeCharacterPack = function (target, source, sourceName = "附加模
return target;
};
// 清理旧版本曾注册的独立武将包,防止历史配置让已合并模块继续出现在武将包列表。
shuYing.removeLegacyCharacterPack = function (name) {
const removeFrom = list => {
if (!Array.isArray(list)) return false;
let changed = false;
for (let index = list.indexOf(name); index >= 0; index = list.indexOf(name)) {
list.splice(index, 1);
changed = true;
}
return changed;
};
removeFrom(lib.config.all.characters);
if (removeFrom(lib.config.characters)) {
game.saveConfig("characters", lib.config.characters);
}
removeFrom(lib.connectCharacterPack);
delete lib.characterPack[name];
delete lib.translate[`${name}_character_config`];
};
shuYing.getVersion = (callback) => {
return updater.getOnlineVersion()
.then(version => {
@@ -196,46 +176,24 @@ shuYing.initCSS = () => {
//术樱包初始化Config配置文件
shuYing.initConfig = () => {
if (lib.config.show_splash != 'off') game.saveConfig('show_splash', 'off'); //关闭启动页
// //崩坏3包
// if (lib.config.extension_shuYing_bengHuai_Off == undefined) {
// game.saveConfig('extension_shuYing_bengHuai_Off', true);
// }
//崩坏3包
if (lib.config.extension_shuYing_bengHuai_Off == undefined) {
game.saveConfig('extension_shuYing_bengHuai_Off', true);
}
// //原神包
// if (lib.config.extension_术樱包_yuanShenOff == undefined) {
// game.saveConfig('extension_术樱包_yuanShenOff', true);
// }
//原神包
if (lib.config.extension_术樱包_yuanShenOff == undefined) {
game.saveConfig('extension_术樱包_yuanShenOff', true);
}
// //崩铁包
// if (lib.config.extension_术樱包_bengTieOff == undefined) {
// game.saveConfig('extension_术樱包_bengTieOff', true);
// }
//崩铁包
if (lib.config.extension_术樱包_bengTieOff == undefined) {
game.saveConfig('extension_术樱包_bengTieOff', true);
}
//天书包
if (lib.config.extension_shuYing_tianShu_Off == undefined) {
game.saveConfig('extension_shuYing_tianShu_Off', true);
}
//天书一共多少层
if (lib.config.extension_shuYing_tianShu_Instances == undefined) {
game.saveConfig('extension_shuYing_tianShu_Instances', 4);
}
//天书自定义Boss列表
if (lib.config.extension_shuYing_tianShu_DiyBoss == undefined) {
game.saveConfig('extension_shuYing_tianShu_DiyBoss', '');
}
//天书狂暴
if (lib.config.extension_shuYing_tianShu_KuangBao == undefined) {
game.saveConfig('extension_shuYing_tianShu_KuangBao', '');
}
//百战天书
if (lib.config.extension_shuYing_baiZhan_Off == undefined) {
game.saveConfig('extension_shuYing_baiZhan_Off', false);
//天书乱斗
if (lib.config.extension_术樱包_tianShu_Off == undefined) {
game.saveConfig('extension_术樱包_tianShu_Off', true);
}
//
@@ -243,14 +201,12 @@ shuYing.initConfig = () => {
//版本号
game.saveConfig('shuYing_local_version', shuYingLocalVersion);
};
//术樱包初始化武将包
shuYing.initCharacter = async () => {
let shuYingList;
if (lib.config.mode == 'boss') {
shuYing.removeLegacyCharacterPack("shuYing_pve_tianshu");
if (lib.config.mode == 'boss' && lib.config.extension_术樱包_tianShu_Off) {
let shuying_boss = await shuYing.loadModule("./pve/boss/index.js", "活动BOSS", true);
const initTianShu = await shuYing.loadModule("./tianshu/extension.js", "天书乱斗", true);
@@ -308,650 +264,6 @@ shuYing.help = () => {
};
//添加自定义Boss TODO
shuYing.newTianShuBossList = function () {
_status.buttonNode; //用于判断输入框回车之后响应事件
_status.nameList;
let manual = ui.create.div('.manual', manual);
let menu = ui.create.div('.menu', manual);
let input = menu.appendChild(document.createElement('input'));
input.onkeydown = function (e) {
if (e && e.keyCode == 13) {
if (_status.buttonNode == 2) {
this.deleteBossList(dialog, manual, input.value);
}
else {
this.searchBossList(dialog, manual, input.value);
}
}
};
let addNewBoss = ui.create.div('.addNewBoss', menu); //加入新boss
let newBossList = ui.create.div('.newBossList', menu);
let deleteNewBossAllList = ui.create.div('.deleteNewBossAllList', menu);
let deleteNewBoss = ui.create.div('.deleteNewBoss', menu);
let playerList = ui.create.div('.playerList', menu);
let close = ui.create.div('.close', menu);
let dialog = ui.create.dialog();
dialog.noImage = true;
dialog.style.backgroundImage = "";
let content = manual.appendChild(dialog);
content.classList.remove('nobutton');
content.classList.add('content');
content.style.transform = '';
content.style.opacity = '';
content.style.height = '';
newBossList.innerHTML = '列表';
newBossList.id = 'newBossList';
newBossList.addEventListener('click', function () {
this.addBossShow(dialog, manual);
});
deleteNewBossAllList.innerHTML = '清除';
deleteNewBossAllList.addEventListener('click', function () {
if (confirm('点击确定清除全部自定义Boss')) {
game.saveConfig('extension_shuYing_tianShu_DiyBoss', '');
this.addBossShow(dialog, manual);
alert('清除完毕!');
}
});
addNewBoss.innerHTML = '搜索';
addNewBoss.addEventListener('click', function () {
this.searchBossList(dialog, manual, input.value);
input.value = "";
_status.buttonNode = 1;
});
deleteNewBoss.innerHTML = '登陆'; //暂时弃用
deleteNewBoss.addEventListener('click', function () {
alert('暂未开放');
//deleteNewBossList(dialog,manual,input.value);
//input.value="";
//_status.buttonNode=2;
});
playerList.innerHTML = '排行榜';
playerList.addEventListener('click', function () {
alert('暂未开放');
});
dialog.addText("使用说明: 点击列表查看自定义BOSS列表<br><br>" +
"点击清空按钮清空自定义BOSS列表中全部Boss<br><br>" +
"点击列表列举出Boss列表,并可以删除列表中的Boss<br><br>" +
"点击搜索并在输入框内输入武将名称即可罗列相似武将<br><br>" +
"需要注意的是,输入框需要输入武将名称而不是英文<br>一旦启用自定义Boss,那么必须保持不禁用武将和Boss列表大于1");
// close1.addEventListener('click', function(){
// alert(close1.value);
// //game.tujianBegin(content , close , input.value , manual);
// //input.value="";
// });
close.innerHTML = '关闭';
close.addEventListener('click', function () {
manual.remove();
ui.arena.show();
ui.system.show();
game.resume2();
if (game.onresume2) {
game.onresume2();
}
ui.arena.classList.remove('menupaused');
ui.historybar.classList.remove('menupaused');
ui.window.classList.remove('touchinfohidden');
ui.config2.classList.remove('pressdown2');
ui.menuContainer.show();
});
_status.paused = true;
ui.arena.classList.remove('menupaused');
ui.arena.hide();
ui.system.hide();
ui.menuContainer.hide();
ui.window.appendChild(manual);
},
//添加自定义Boss TODO
shuYing.addBossShow = function (dialog, manual) {
dialog = ui.create.dialog();
dialog.noImage = true;
dialog.style.backgroundImage = "";
let content = manual.appendChild(dialog);
content.classList.remove('nobutton');
content.classList.add('content');
content.style.transform = '';
content.style.opacity = '';
content.style.height = '';
let list = lib.config.extension_术樱_tianshu_add_list;
if (list == undefined || list == '') {
dialog.addText("无BOSS");
} else {
list = list.split(",");
for (let i = 0; i < list.length; i++) {
for (let j in lib.character) {
if (j == list[i]) {
dialog.addSmall([[j], 'character']);
let del = ui.create.div('.shadowed.reduce_radius.pointerdiv.tdnode');
del.link = i;
del.innerHTML = '<span>' + '删除' + '</span>';
del.addEventListener(lib.config.touchscreen ? 'touchend' : 'click', function () {
if (lib.config.extension_术樱_tianshu_add_list.length <= 1) {
game.saveConfig('extension_术樱_tianshu_add_list', '');
} else if (lib.config.extension_术樱_tianshu_add_list.length == 2) {
if (this.link != 1) {
game.saveConfig('extension_术樱_tianshu_add_list', list[this.link]);
} else {
game.saveConfig('extension_术樱_tianshu_add_list', list[0]);
}
} else {
game.saveConfig('extension_术樱_tianshu_add_list', '');
for (let k = 0; k < list.length; k++) {
if (k == this.link) continue;
if (lib.config.extension_术樱_tianshu_add_list == undefined || lib.config.extension_术樱_tianshu_add_list == '') {
game.saveConfig('extension_术樱_tianshu_add_list', list[k]);
} else {
game.saveConfig('extension_术樱_tianshu_add_list', lib.config.extension_术樱_tianshu_add_list + ',' + list[k]);
}
}
document.getElementById('newBossList').click(); //刷新列表
}
});
dialog.addAuto(del);
}
}
}
}
},
//搜索Boss TODO
shuYing.searchBossList = function (dialog, manual, val) {
dialog = ui.create.dialog();
dialog.noImage = true;
dialog.style.backgroundImage = "";
let content = manual.appendChild(dialog);
content.classList.remove('nobutton');
content.classList.add('content');
content.style.transform = '';
content.style.opacity = '';
content.style.height = '';
let result = val;
let value = false;
let list = [];
if (result == "" || result == null) {
result = "请输入武将名称";
alert(result);
} else {
for (let a in lib.character) {
if (lib.translate[a] && lib.translate[a].indexOf(result) != -1) {
list.add(a);
value = true;
}
}
for (let i = 0; i < list.length; i++) {
dialog.addSmall([[list[i]], 'character']);
let add = ui.create.div('.shadowed.reduce_radius.pointerdiv.tdnode');
add.link = i;
add.innerHTML = '<span>' + '加入' + '</span>';
add.addEventListener(lib.config.touchscreen ? 'touchend' : 'click', function () {
if (lib.config.extension_术樱_tianshu_add_list == undefined || lib.config.extension_术樱_tianshu_add_list == '') {
game.saveConfig('extension_术樱_tianshu_add_list', list[this.link]);
} else {
game.saveConfig('extension_术樱_tianshu_add_list', lib.config.extension_术樱_tianshu_add_list + ',' + list[this.link]);
}
alert('成功加入至列表');
});
dialog.addAuto(add);
}
_status.nameList = list;
if (value == false) {
alert('找不到名为' + result + '的武将!');
}
}
},
//删除Boss TODO
shuYing.deleteBossList = function (dialog, manual, val) {
dialog = ui.create.dialog();
dialog.noImage = true;
dialog.style.backgroundImage = "";
let content = manual.appendChild(dialog);
content.classList.remove('nobutton');
content.classList.add('content');
content.style.transform = '';
content.style.opacity = '';
content.style.height = '';
let result = val;
if (result == "" || result == null) {
result = "你没有输入名称";
alert(result);
return;
}
let list = lib.config.extension_术樱_tianshu_add_list.split(",");
let value = false;
let newList = [];
for (let i = 0; i < list.length; i++) {
let pushBool = true;
if (String(lib.translate[list[i]]) == String(result)) {
if (confirm('已找到' + result + '武将,点击确定移除自定义Boss列表。')) {
pushBool = false;
}
value = true;
}
if (pushBool) newList.push(list[i]);
}
if (newList) {
game.saveConfig('extension_术樱_tianshu_add_list', '');
for (let j = 0; j < newList.length; j++) {
if (lib.config.extension_术樱_tianshu_add_list == undefined || lib.config.extension_术樱_tianshu_add_list == '') {
game.saveConfig('extension_术樱_tianshu_add_list', newList[j]);
} else {
game.saveConfig('extension_术樱_tianshu_add_list', lib.config.extension_术樱_tianshu_add_list + ',' + newList[j]);
}
}
}
if (value == false) {
alert('找不到名为' + result + '的武将或者武将不存在自定义Boss列表中。');
}
},
shuYing.download_beta_version = function () {
let online_version;
let httpRequest = new XMLHttpRequest();
httpRequest.open("GET", 'http://diuse.hao1237.top/Diuse/beta_extension/online_version.js', true);
httpRequest.send(null);
httpRequest.onreadystatechange = function () {
if (httpRequest.readyState == 4 && httpRequest.status == 200) {
online_version = httpRequest.responseText;
game.saveConfig('Diuse_online_version', httpRequest.responseText)
lib.init.js(url, 'version', function () {
try {
let local_version = Diuse_version;
let Diuse_num = 1;
} catch (error) {
if (confirm('本地资源不完整!点击确认重新获取!')) {
game.download('http://diuse.hao1237.top/Diuse/beta_extension/files.js', 'extension/术樱/files.js', function () { }, function () { });
game.download('http://diuse.hao1237.top/Diuse/beta_extension/version.js', 'extension/术樱/version.js', function () { }, function () { });
game.download('http://diuse.hao1237.top/Diuse/beta_extension/extension.js', 'extension/术樱/extension.js', function () {
game.saveConfig('Diuse_local_version', online_version);
Diuse_Button = true;
alert('下载完成,重启生效');
}, function () {
Diuse_Button = true;
alert('下载失败');
});
} else {
Diuse_Button = true;
}
}
if (local_version != online_version && Diuse_num == 1) {
if (confirm('检测到最新测试版本为:' + online_version + '本地版本为:' + local_version)) {
game.download('http://diuse.hao1237.top/Diuse/beta_extension/files.js', 'extension/术樱/files.js', function () { }, function () { });
game.download('http://diuse.hao1237.top/Diuse/beta_extension/version.js', 'extension/术樱/version.js', function () { }, function () { });
game.download('http://diuse.hao1237.top/Diuse/beta_extension/extension.js', 'extension/术樱/extension.js', function () {
game.saveConfig('Diuse_local_version', online_version);
Diuse_Button = true;
alert('下载完成,重启生效');
}, function () {
Diuse_Button = true;
alert('下载失败');
});
} else {
Diuse_Button = true;
}
} else {
if (Diuse_num == 1) {
Diuse_Button = true;
alert('本地版本为最新版');
}
}
}, function () {
if (confirm('本地资源不完整!点击确认重新获取!')) {
game.download('http://diuse.hao1237.top/Diuse/beta_extension/files.js', 'extension/术樱/files.js', function () { }, function () { });
game.download('http://diuse.hao1237.top/Diuse/beta_extension/version.js', 'extension/术樱/version.js', function () { }, function () { });
game.download('http://diuse.hao1237.top/Diuse/beta_extension/extension.js', 'extension/术樱/extension.js', function () {
game.saveConfig('Diuse_local_version', online_version);
Diuse_Button = true;
alert('下载完成,重启生效');
}, function () {
Diuse_Button = true;
alert('下载失败');
});
} else {
Diuse_Button = true;
}
});
}
};
};
shuYing.download_mp3 = function () {
lib.init.js(url, 'files', function () {
let list = Diuse_mp3;
let num = 0;
let num1 = list.length;
document.body.appendChild(Diuse_Text);
let download1 = function () {
game.download('http://diuse.hao1237.top/Diuse/skin/' + list[0], 'extension/术樱/' + list[0], function () {
num++
list.remove(list[0]);
if (list.length > 0) {
Diuse_Text.innerHTML = '正在下载(' + num + '/' + num1 + ')';
download1();
} else {
Diuse_Text.innerHTML = '下载完毕';
Diuse_Button = true;
alert('语音下载完毕!');
document.body.removeChild(Diuse_Text);
};
}, function () {
if (confirm('下载' + list[0] + '失败,是否继续下载?')) {
download1();
}
});
}
download1();
}, function () {
Diuse_Button = true;
alert('本地资源不完整!请检查文件完整性。');
});
};
shuYing.download_static = function () {
lib.init.js(url, 'files', function () {
let list = Diuse_static;
let num = 0;
let num1 = list.length;
document.body.appendChild(Diuse_Text);
let download1 = function () {
game.download('http://diuse.hao1237.top/Diuse/image/static/' + list[0], 'extension/术樱/' + list[0], function () {
num++
list.remove(list[0]);
if (list.length > 0) {
Diuse_Text.innerHTML = '正在下载(' + num + '/' + num1 + ')';
download1();
} else {
Diuse_Text.innerHTML = '下载完毕';
Diuse_Button = true;
alert('静态皮肤下载完毕!');
document.body.removeChild(Diuse_Text);
};
}, function () {
if (confirm('下载' + list[0] + '失败,是否继续下载?')) {
download1();
}
});
}
download1();
}, function () {
Diuse_Button = true;
alert('本地资源不完整!请检查文件完整性。');
});
};
shuYing.download_tianshu = function () {
lib.init.js(url, 'files', function () {
let list = Diuse_tianshu;
let num = 0;
let num1 = list.length;
document.body.appendChild(Diuse_Text);
let download1 = function () {
game.download('http://diuse.hao1237.top/Diuse/image/tianshu/' + list[0], 'extension/术樱/' + list[0], function () {
num++
list.remove(list[0]);
if (list.length > 0) {
Diuse_Text.innerHTML = '正在下载(' + num + '/' + num1 + ')';
download1();
} else {
Diuse_Text.innerHTML = '下载完毕';
Diuse_Button = true;
alert('天书皮肤下载完毕!');
document.body.removeChild(Diuse_Text);
};
}, function () {
if (confirm('下载' + list[0] + '失败,是否继续下载?')) {
download1();
}
});
}
download1();
}, function () {
Diuse_Button = true;
alert('本地资源不完整!请检查文件完整性。');
});
};
shuYing.download_dynamic = function (dynamic_name) {
if (lib.config.extension_术樱_Skin) {
let url_files = dynamic_name.substring(0, dynamic_name.length - 4);
skinList(url_files);
} else {
game.download('http://diuse.hao1237.top/Diuse/image/dynamic/' + dynamic_name, 'extension/术樱/' + dynamic_name, function () {
Diuse_Button = true;
alert('所选皮肤下载完毕!');
}, function () {
if (confirm('下载' + dynamic_name + '失败,是否继续下载?')) {
download_dynamic(dynamic_name);
}
});
}
};
shuYing.download_dynamic_all = function () {
lib.init.js(url, 'files', function () {
let list = Diuse_dynamic;
let num = 0;
let num1 = list.length;
document.body.appendChild(Diuse_Text);
let download1 = function () {
game.download('http://diuse.hao1237.top/Diuse/image/dynamic/' + list[0], 'extension/术樱/' + list[0], function () {
num++
list.remove(list[0]);
if (list.length > 0) {
Diuse_Text.innerHTML = '正在下载(' + num + '/' + num1 + ')';
download1();
} else {
Diuse_Text.innerHTML = '下载完毕';
Diuse_Button = true;
alert('动态皮肤下载完毕!');
document.body.removeChild(Diuse_Text);
};
}, function () {
if (confirm('下载' + list[0] + '失败,是否继续下载?')) {
download1();
}
});
}
download1();
}, function () {
Diuse_Button = true;
alert('本地资源不完整!请检查文件完整性。');
});
};
shuYing.download_files_all = function () {
lib.init.js(url, 'files', function () {
let list = Diuse_dynamic;
let num = 0;
let num1 = list.length;
document.body.appendChild(Diuse_Text);
let download1 = function () {
game.download('http://diuse.hao1237.top/Diuse/image/dynamic/' + list[0], 'extension/术樱/' + list[0], function () {
num++
list.remove(list[0]);
if (list.length > 0) {
Diuse_Text.innerHTML = '正在下载(' + num + '/' + num1 + ')';
download1();
} else {
Diuse_Text.innerHTML = '下载完毕';
Diuse_Button = true;
alert('动态皮肤下载完毕!');
document.body.removeChild(Diuse_Text);
};
}, function () {
if (confirm('下载' + list[0] + '失败,是否继续下载?')) {
download1();
}
});
}
download1();
}, function () {
Diuse_Button = true;
alert('本地资源不完整!请检查文件完整性。');
});
};
shuYing.skinList = function () {
lib.init.js(url, 'files', function () {
let num = 0;
let skin_list = Skin;
let skin_num;
let file_url = 'image/skin/';
let file_length = 0;
document.body.appendChild(Diuse_Text);
let down = function () {
switch (skin_list[0]) {
case 'Diuse_Fuhua': {
skin_num = Diuse_Fuhua_Skin;
break;
}
case 'Diuse_Bachongying': {
skin_num = Diuse_Bachongying_Skin;
break;
}
case 'Diuse_Buluoniya': {
skin_num = Diuse_Buluoniya_Skin;
break;
}
case 'Diuse_Kalian': {
skin_num = Diuse_Kalian_Skin;
break;
}
case 'Diuse_Shangxian': {
skin_num = Diuse_Shangxian_Skin;
break;
}
case 'Diuse_Shilv': {
skin_num = Diuse_Shilv_Skin;
break;
}
case 'Diuse_Xier': {
skin_num = Diuse_Xier_Skin;
break;
}
case 'Diuse_Yayi': {
skin_num = Diuse_Yayi_Skin;
break;
}
case 'Diuse_Yuexia': {
skin_num = Diuse_Yuexia_Skin;
break;
}
case 'Diuse_Konglv': {
skin_num = Diuse_Konglv_Skin;
break;
}
case 'Diuse_Heixi': {
skin_num = Diuse_Heixi_Skin;
break;
}
case 'Diuse_Bachongshenzi': {
skin_num = Diuse_Bachongshenzi_Skin;
break;
}
case 'Diuse_Keli': {
skin_num = Diuse_Keli_Skin;
break;
}
case 'Diuse_Shuangzi': {
skin_num = Diuse_Shuangzi_Skin;
break;
}
case 'Diuse_Lanmei': {
skin_num = Diuse_Lanmei_Skin;
break;
}
case 'Diuse_Yingtao': {
skin_num = Diuse_Yingtao_Skin;
break;
}
}
url_Diuse_search = file_url + skin_list[0] + '/' + skin_num[0] + '.jpg';
file_length += skin_list.length;
game.readFile(url_Diuse_search, function () {
num++;
skin_num.remove(skin_num[0]);
if (skin_list.length > 0 || skin_num.length > 0) {
if (skin_num.length <= 0) skin_list.remove(skin_list[0]);
Diuse_Text.innerHTML = '正在补充Skin(' + num + '/' + file_length + ')';
down();
} else {
Diuse_Text.innerHTML = '补充完毕';
Diuse_Button = true;
alert('皮肤下载完毕!');
document.body.removeChild(Diuse_Text);
}
}, function () {
num++;
skin_num.remove(skin_num[0]);
if (skin_list.length > 0 || skin_num.length > 0) {
if (skin_num.length <= 0) skin_list.remove(skin_list[0]);
Diuse_Text.innerHTML = '正在补充Skin(' + num + '/' + file_length + ')';
down();
} else {
Diuse_Text.innerHTML = '补充完毕';
Diuse_Button = true;
alert('皮肤下载完毕!');
document.body.removeChild(Diuse_Text);
};
// game.download('https://diuse.coding.net/p/extension/d/noname_Skin/git/raw/master/Skin/'+skin_list[0]+'/'+skin_num[0]+'.jpg','image/skin/'+skin_list[0]+'/'+skin_num[0]+'.jpg',function(){
// num++;
// skin_num.remove(skin_num[0]);
// if(skin_list.length>0||skin_num.length>0){
// if(skin_num.length<=0) skin_list.remove(skin_list[0]);
// Diuse_Text.innerHTML='正在补充Skin('+num+'/'+file_length+')';
// down();
// }else{
// Diuse_Text.innerHTML='补充完毕';
// Diuse_Button=true;
// alert('皮肤下载完毕!');
// document.body.removeChild(Diuse_Text);
// };
// },function(){
// if(confirm('下载'+skin_list[0]+'失败,是否继续下载?')){
// down();
// }
// });
});
}
down();
}, function () {
Diuse_Button = true;
alert('本地资源不完整!请检查文件完整性。');
});
};
//本地资源修复
shuYing.RepairBug = () => {
updater.repairCoreFiles(shuYing);
+338
View File
@@ -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 });
});
};
}
+241 -51
View File
@@ -1,28 +1,198 @@
import tianshuConfig from "./tianshu/config.js";
export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) {
const menu = lib.extensionMenu.extension_术樱包;
const createCollapseMenu = (title, endId) => ({
name: `<div class="hth_menu">▼ ${title}</div>`,
clear: true,
onclick() {
if (!this.shuyingCollapseNodes) {
const nodes = [];
let node = this.nextSibling;
while (node && !node.querySelector?.(`#${endId}`)) {
nodes.push(node);
node = node.nextSibling;
}
this.shuyingCollapseNodes = nodes;
// 创建可指定默认状态的折叠菜单,并在菜单节点延迟生成后应用初始状态。
const createCollapseMenu = (title, endId, defaultCollapsed = false) => {
const markerId = `${endId}_collapse`;
const renderTitle = collapsed => `<span><div id="${markerId}" class="hth_menu">${collapsed ? "▶" : "▼"} ${title}</div></span>`;
const getCollapseNodes = node => {
if (node.shuyingCollapseNodes) return node.shuyingCollapseNodes;
const nodes = [];
let current = node.nextSibling;
while (current && !current.querySelector?.(`#${endId}`)) {
nodes.push(current);
current = current.nextSibling;
}
const collapsed = !this.shuyingCollapsed;
this.shuyingCollapsed = collapsed;
this.shuyingCollapseNodes.forEach(node => {
node.style.display = collapsed ? "none" : "";
node.shuyingCollapseNodes = nodes;
return nodes;
};
const setCollapsed = (node, collapsed) => {
if (!node) return false;
node.shuyingCollapsed = Boolean(collapsed);
getCollapseNodes(node).forEach(current => {
current.style.display = node.shuyingCollapsed ? "none" : "";
});
this.innerHTML = `<span><div class="hth_menu">${collapsed ? "▶" : "▼"} ${title}</div></span>`;
},
});
node.innerHTML = renderTitle(node.shuyingCollapsed);
return true;
};
const config = {
name: `<div id="${markerId}" class="hth_menu">${defaultCollapsed ? "▶" : "▼"} ${title}</div>`,
clear: true,
onclick() {
setCollapsed(this, !this.shuyingCollapsed);
},
setCollapsed(collapsed) {
const node = document.getElementById(markerId)?.closest?.(".config");
return setCollapsed(node, collapsed);
},
};
if (defaultCollapsed) {
const observer = new MutationObserver(() => {
if (!config.setCollapsed(true)) return;
observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
queueMicrotask(() => {
if (!config.setCollapsed(true)) return;
observer.disconnect();
});
}
return config;
};
const getBondLevelCondition = (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 getBondSkillLabels = condition => {
if (!condition || typeof condition != "object") return [];
if (!condition.type) {
return Array.from(new Set([
...(condition.all || []).flatMap(getBondSkillLabels),
...(condition.any || []).flatMap(getBondSkillLabels),
...getBondSkillLabels(condition.not),
].filter(Boolean)));
}
if (condition.type == "keyword") return condition.keyword ? [`含【${condition.keyword}】的技能`] : [];
if (condition.type == "skill") return condition.skill ? [get.translation(condition.skill)] : [];
if (condition.type == "skillName") return condition.name ? [condition.name] : [];
if (condition.type == "skills") {
return Array.from(new Set([...(condition.all || []), ...(condition.any || [])].map(skill => get.translation(skill)).filter(Boolean)));
}
if (condition.type == "skillNames") {
return Array.from(new Set([...(condition.all || []), ...(condition.any || [])].filter(Boolean)));
}
if (condition.type == "characterSkillNames") {
return Array.from(new Set([...(condition.all || []), ...(condition.any || [])].filter(Boolean)));
}
return [];
};
const getBondRuleHint = condition => {
if (!condition || typeof condition != "object") return "";
if (condition.type == "skillNames") return "按不同译名统计;同译名的不同技能 ID 只计 1 种过关技能";
if (condition.type == "characterSkillNames") {
const characters = Array.isArray(condition.characters) ? condition.characters : [condition.character];
return `武将译名包含【${characters.filter(Boolean).join("】或【")}】且取得上述过关技能后触发`;
}
if (condition.type == "keyword") return `按描述中包含【${condition.keyword}】的不同技能译名统计`;
if (!condition.type && [...(condition.all || []), ...(condition.any || [])].some(item => item?.type == "skillNames")) {
return "按不同译名统计;同译名的不同技能 ID 只计 1 种过关技能";
}
return "";
};
const getBondLevelRequirement = (bond, level) => {
const condition = getBondLevelCondition(bond, level);
const count = Math.max(0, Math.floor(Number(condition?.count ?? level?.count) || 0));
if (count) return condition?.type == "skillNames" ? `需 ${count} 种` : `需 ${count} 个`;
if (condition?.type == "skills" && condition.all?.length) return `需 ${new Set(condition.all).size} 个`;
if (condition?.type == "skillNames" && condition.all?.length) return `需 ${new Set(condition.all).size} 种`;
return "";
};
const getBondRewardText = level => {
if (level?.text) return level.text;
const skills = Array.isArray(level?.skills) ? level.skills : (level?.skill ? [level.skill] : []);
return skills.length ? `获得技能${skills.map(skill => `【${get.translation(skill)}】`).join("、")}` : "未配置奖励效果";
};
let closeTianshuBondDialog = null;
// 创建独立羁绊谱册;每次打开都直接读取当前 tianshuConfig.bonds。
const openTianshuBondDialog = () => {
closeTianshuBondDialog?.();
const previousFocus = document.activeElement;
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 bonds = Array.isArray(tianshuConfig.bonds) ? tianshuConfig.bonds : [];
const mask = createNode("div", "shuYing-bond-book-mask", null, document.body);
const dialog = createNode("section", "shuYing-bond-book", null, mask);
dialog.setAttribute("role", "dialog");
dialog.setAttribute("aria-modal", "true");
dialog.setAttribute("aria-label", "天书羁绊谱册");
const header = createNode("header", "shuYing-bond-book-header", null, dialog);
const heading = createNode("div", "shuYing-bond-book-heading", null, header);
createNode("div", "shuYing-bond-book-eyebrow", "天书乱斗 · 羁绊谱册", heading);
createNode("h2", "shuYing-bond-book-title", "羁绊系统", heading);
createNode("div", "shuYing-bond-book-count", `${bonds.length} 条羁绊`, header);
createNode("p", "shuYing-bond-book-intro", "羁绊只统计玩家过关时选择且当前仍持有的技能;达到更高等级时,高等级效果会覆盖低等级。", dialog);
const content = createNode("div", "shuYing-bond-book-content", null, dialog);
if (!bonds.length) {
createNode("div", "shuYing-bond-book-empty", "config.js 中暂未配置可展示的羁绊。", content);
}
bonds.forEach((bond, bondIndex) => {
const card = createNode("article", "shuYing-bond-book-card", null, content);
const cardHead = createNode("div", "shuYing-bond-book-card-head", null, card);
createNode("span", "shuYing-bond-book-seal", String(bondIndex + 1).padStart(2, "0"), cardHead);
const nameWrap = createNode("div", "shuYing-bond-book-name-wrap", null, cardHead);
createNode("h3", "shuYing-bond-book-name", bond.name || "未命名羁绊", nameWrap);
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);
const summaryCondition = bond.condition || getBondLevelCondition(bond, levels[0]);
const skillLabels = getBondSkillLabels(summaryCondition);
if (skillLabels.length) {
const summary = createNode("div", "shuYing-bond-book-summary", null, card);
const tags = createNode("div", "shuYing-bond-book-tags", null, summary);
skillLabels.forEach(name => createNode("span", "shuYing-bond-book-tag", name, tags));
const hint = getBondRuleHint(summaryCondition);
if (hint) createNode("div", "shuYing-bond-book-rule", hint, summary);
}
const levelList = createNode("div", "shuYing-bond-book-levels", null, card);
if (!levels.length) createNode("div", "shuYing-bond-book-level-empty", "尚未配置等级", levelList);
levels.forEach(level => {
const row = createNode("div", "shuYing-bond-book-level", null, levelList);
createNode("span", "shuYing-bond-book-level-dot", "", row);
const detail = createNode("div", "shuYing-bond-book-level-detail", null, row);
const requirement = getBondLevelRequirement(bond, level);
createNode("div", "shuYing-bond-book-level-name", `第 ${level.rank} 级${requirement ? ` · ${requirement}` : ""}`, detail);
createNode("div", "shuYing-bond-book-reward", getBondRewardText(level), detail);
});
});
const controls = createNode("footer", "shuYing-bond-book-controls", null, dialog);
const closeButton = createNode("button", "shuYing-bond-book-close", "关闭谱册", controls);
closeButton.type = "button";
const close = () => {
document.removeEventListener("keydown", onKeydown);
mask.remove();
closeTianshuBondDialog = null;
previousFocus?.focus?.({ preventScroll: true });
};
const onKeydown = event => {
if (event.key == "Escape") close();
};
closeTianshuBondDialog = close;
closeButton.addEventListener("click", close);
mask.addEventListener("click", event => {
if (event.target == mask) close();
});
document.addEventListener("keydown", onKeydown);
closeButton.focus({ preventScroll: true });
};
menu.local_version = {
name: `扩展版本:${lib.config.shuYing_local_version}`,
@@ -48,37 +218,40 @@ export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) {
}
const logs = [
"-----< 2.0.0.9 改动 >-----",
"新增可以自定义虚拟偶像随机助战池功能",
"现在过关奖励提供的技能池会过滤武将牌上的技能",
"新增BOSS:子鼠、午马、日夜游神、董卓、旱魃",
"适配部分技能需要依赖BOSS的起始手牌",
"适配部分技能需要选择已经死亡的BOSS为目标时,提供一个按钮来列出已死亡的BOSS列表",
"-----< 2.0.0.9 修复 >-----",
"修复BOSS死亡尸体不会消失的BUG",
"修正曹操AI会更加正确判断何时发动【疑神】",
"修正过关后BOSS的初始手牌不再是摸牌事件",
"-----< 2.0.0.8 改动 >-----",
"新增BOSS:玄女!",
"不用担心困难,虚拟偶像前来援助!",
"新增BOSS选项开启,现在可以在扩展选项中开启新增1位BOSS来提高难度!",
"-----< 2.0.0.8 修复 >-----",
"修复当过关奖励执行完毕轮数和下轮角色异常的BUG",
"修复了一个BUG,该BUG会导致进入下关时BOSS的初始牌会被技能检索",
"修复黄蜂技能【冥虫】无法正常发动的BUG",
"修正司马懿AI向其友方发动【反馈】【狼顾】的问题",
"-----< 2.0.1.0 改动 >-----",
"重构PVE武将包目录,按照不同活动BOSS进行独立分包和统一加载",
"新增独立PVP武将包结构,并加入术樱左慈及其技能、图片和配音资源",
"天书乱斗新增动态难度BOSS,不同难度将使用准确的体力上限和追加技能",
"天书乱斗新增羁绊系统,根据玩家过关选择的技能激活并替换不同等级效果",
"羁绊条件支持技能ID、技能译名、描述关键词以及武将名称关键词组合",
"新增羁绊谱册和局内已激活羁绊查看功能",
"重铸过关技能选择界面,新增六个技能选项、确认按钮和单次刷新功能",
"新增可配置的额外技能奖励池,过关时与武将技能池同步检索",
"新增天书过关技能选择记录,羁绊仅统计玩家通过过关奖励取得且当前仍持有的技能",
"统一活动BOSS武将包入口,并补充十二生肖、青青子衿和捉鬼驱邪相关内容及配音资源",
"自定义界面现已兼容十周年UI、原生UI以及移动端显示",
"-----< 2.0.1.0 修复 >-----",
"修复天书动态BOSS体力、技能、武将复制和玩家变身识别异常的问题",
"修复天书武将包被普通武将列表识别或无法被挑战模式加载的问题",
"修复PVE分包后技能台词无法正确识别的问题",
"修复羁绊中多个同译名技能会被重复计算的问题",
"修复羁绊谱册和局内羁绊列表在部分分辨率下重叠或标题越界的问题",
"修复过关后行动顺序异常,当前角色回合结束后将行动权交给其下家",
"调整天书开局保护的伤害结算顺序,优先结算其他免伤和减伤效果",
"修复生成更新清单时会覆盖原有remove和removeDirectories内容的问题",
];
const keywordColors = {
BOSS: "#ff6b6b",
武将牌: "#ff6b6b",
BUG: "#ff9800",
虚拟偶像: "#66ccff",
玄女: "#d77cff",
"【冥虫】": "#7ed957",
"【反馈】": "#7ed957",
"【狼顾】": "#7ed957",
黄蜂: "#d77cff",
司马懿: "#d77cff",
PVE: "#ff6b6b",
PVP: "#ff6b6b",
天书乱斗: "#66ccff",
羁绊: "#d77cff",
动态难度: "#d77cff",
过关技能: "#7ed957",
十周年UI: "#ffb74d",
移动端: "#ffb74d",
removeDirectories: "#ff9800",
remove: "#ff9800",
};
const highlightKeywords = text => {
for (const [keyword, color] of Object.entries(keywordColors)) {
@@ -157,12 +330,29 @@ export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) {
name: "--------------------",
};
menu.tianShuGroup = createCollapseMenu("天书乱斗选项", "shuying_tianshu_menu_end");
menu.tianShuGroup = createCollapseMenu(
"天书乱斗选项",
"shuying_tianshu_menu_end",
lib.config.extension_术樱包_tianShu_Off === false
);
menu.tianShuOff = {
menu.tianShu_Off = {
name: "天书乱斗开关",
init: true,
intro: "关闭后,关闭天书乱斗模式(在非挑战模式下自动关闭)",
onclick(enabled) {
game.saveConfig("extension_术樱包_tianShu_Off", enabled);
if (!enabled) menu.tianShuGroup.setCollapsed(true);
},
};
menu.tianShu_bonds = {
name: "查看羁绊谱册",
clear: true,
intro: "同步读取天书配置,展示当前可激活的羁绊、等级条件和奖励效果。",
onclick() {
openTianshuBondDialog();
},
};
menu.giveAiSkill = {
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -6,7 +6,7 @@ import tianshu from "./sets/tianshu/index.js";
// 活动 BOSS 主题清单;新增主题时只需在此接入一次。
const bossSets = [
["十二生肖", shiershengxiao],
["捉鬼驱邪", zhuoguiquxie],
["驱鬼逐邪", zhuoguiquxie],
["青青子衿", qingqingzijin],
["天书乱斗", tianshu],
];
+25
View File
@@ -9,6 +9,7 @@ const characters = {
img: "extension/术樱包/pve/images/青青子衿/caocao_boss_shuying.jpg",
dieAudios: ["ext:术樱包/pve/audio/dead/青青子衿/caocao_boss_shuying.mp3"]
},
simayi_boss_shuying: {
sex: "male",
group: "wei",
@@ -19,6 +20,7 @@ const characters = {
img: "extension/术樱包/pve/images/青青子衿/simayi_boss_shuying.jpg",
dieAudios: ["ext:术樱包/pve/audio/dead/青青子衿/simayi_boss_shuying.mp3"]
},
lvbu_boss_shuying: {
sex: "male",
group: "qun",
@@ -29,6 +31,7 @@ const characters = {
img: "extension/术樱包/pve/images/青青子衿/lvbu_boss_shuying.jpg",
dieAudios: ["ext:术樱包/pve/audio/dead/青青子衿/lvbu_boss_shuying.mp3"]
},
dongzhuo_boss_shuying: {
sex: "male",
group: "qun",
@@ -39,6 +42,28 @@ const characters = {
img: "extension/术樱包/pve/images/青青子衿/dongzhuo_boss_shuying.jpg",
dieAudios: ["ext:术樱包/pve/audio/dead/青青子衿/dongzhuo_boss_shuying.mp3"]
},
yuanshu_boss_shuying: {
sex: "male",
group: "qun",
hp: 4,
skills: ["yongsi_shuying", "wangzun_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/青青子衿/yuanshu_boss_shuying.jpg",
dieAudios: ["ext:术樱包/pve/audio/dead/青青子衿/yuanshu_boss_shuying.mp3"]
},
zhangjiao_boss_shuying: {
sex: "male",
group: "qun",
hp: 4,
skills: ["guidao_shuying", "leiji_shuying", "zhuzheng_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/青青子衿/zhangjiao_boss_shuying.jpg",
dieAudios: ["ext:术樱包/pve/audio/dead/青青子衿/zhangjiao_boss_shuying.mp3"]
},
};
export default characters;
@@ -66,6 +66,28 @@ const dynamicTranslates = {
return `锁定技,回合开始时,你摸X张牌并对至多X名角色造成1点伤害,然后你失去1点体力。X为你已损失体力且最大为${num}。`
},
//袁术
wangzun_shuying(player, skill) {
const num =
getTianshuDifficulty() == "nightmare" ? 2 : 1;
return `锁定技,其他敌方角色的结束阶段,若其本回合:1.没有对你造成伤害,则其弃置${num}张牌;2.对你造成的伤害数超过1点,则你对其造成1点伤害。`;
},
duoxi_shuying(player, skill) {
const num =
getTianshuDifficulty() == "nightmare" ? 2 : 1;
return `其他角色的摸牌阶段,你可以失去1点体力,改为你与其各摸${num}张牌。`;
},
//张角
zhuzheng_shuying(player, skill) {
if (getTianshuDifficulty() == "nightmare") {
return "当一名其他角色使用【杀】指定目标时,若你在其攻击范围内且你不是目标,则你可以将一张手牌置于牌堆顶,取消所有目标,然后你成为目标。";
}
return "其他角色使用【杀】指定除你外的角色时,若你在其攻击范围内,你可以将一张手牌置于牌堆顶,取消所有目标,然后若此【杀】不为黑色,你成为目标。";
},
};
export default dynamicTranslates;
+510
View File
@@ -670,6 +670,516 @@ const skills = {
},
},
//袁术
yongsi_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill/青青子衿:1",
forced: true,
locked: true,
group: [
"yongsi_shuying_draw",
"yongsi_shuying_discard",
],
mod: {
maxHandcard(player, num) {
if (_status.currentPhase != player) return;
const damageNum = player
.getHistory("sourceDamage")
.reduce((sum, evt) => sum + (evt.num || 0), 0);
if (damageNum > 1) {
return player.getDamagedHp();
}
},
},
subSkill: {
draw: {
trigger: { player: "phaseDrawBegin2" },
forced: true,
locked: true,
filter(event, player) {
return !event.numFixed;
},
async content(event, trigger, player) {
trigger.num = game.countGroup();
},
},
discard: {
trigger: { player: "phaseDiscardBegin" },
forced: true,
locked: true,
filter(event, player) {
const damageNum = player
.getHistory("sourceDamage")
.reduce((sum, evt) => sum + (evt.num || 0), 0);
return (
damageNum == 0 &&
player.countCards("h") < player.hp
);
},
async content(event, trigger, player) {
await player.drawTo(player.hp);
},
},
},
ai: {
threaten: 1.8,
},
},
wangzun_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill/青青子衿:1",
trigger: { global: "phaseJieshuBegin" },
forced: true,
locked: true,
logTarget: "player",
filter(event, player) {
const target = event.player;
if (
!target?.isIn() ||
!player.getEnemies(null, false).includes(target)
) {
return false;
}
const damageNum = player
.getHistory("damage")
.reduce((sum, evt) => {
if (evt.source == target) {
return sum + (evt.num || 0);
}
return sum;
}, 0);
if (damageNum > 1) return true;
return (
damageNum == 0 &&
target.countDiscardableCards(target, "he") > 0
);
},
async content(event, trigger, player) {
const target = trigger.player;
const damageNum = player
.getHistory("damage")
.reduce((sum, evt) => {
if (evt.source == target) {
return sum + (evt.num || 0);
}
return sum;
}, 0);
player.line(target);
if (damageNum == 0) {
const discardNum =
getTianshuDifficulty() == "nightmare" ? 2 : 1;
const num = Math.min(
discardNum,
target.countDiscardableCards(target, "he")
);
if (num > 0) {
await target.chooseToDiscard(num, "he", true);
}
} else if (damageNum > 1 && target.isIn()) {
await target.damage(1, player);
}
},
ai: {
threaten: 1.6,
},
},
duoxi_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill/青青子衿:1",
trigger: { global: "phaseDrawBegin1" },
filter(event, player) {
return (
getTianshuDifficulty() != "normal" &&
!event.numFixed &&
event.player?.isIn() &&
event.player != player
);
},
async cost(event, trigger, player) {
const num =
getTianshuDifficulty() == "nightmare" ? 2 : 1;
event.result = await player
.chooseBool(
get.prompt(event.skill, trigger.player),
`失去1点体力,将${get.translation(trigger.player)}的摸牌阶段改为你与其各摸${get.cnNumber(num)}张牌`
)
.set("ai", () => {
const player = get.player();
const trigger = get.event().getTrigger();
const target = trigger.player;
const num =
getTianshuDifficulty() == "nightmare"
? 2
: 1;
if (player.hp <= 1) return false;
const isEnemy = player
.getEnemies(null, false)
.includes(target);
// 避免额外增加敌方摸牌,或削减友方原本的摸牌数
if (isEnemy && trigger.num < num) return false;
if (!isEnemy && trigger.num > num) return false;
return player.hp > 2;
})
.forResult();
},
async content(event, trigger, player) {
const target = trigger.player;
const num =
getTianshuDifficulty() == "nightmare" ? 2 : 1;
player.line(target);
trigger.changeToZero();
await player.loseHp();
const targets = [player, target].filter(
current => current.isIn()
);
if (targets.length) {
await game.asyncDraw(targets, num);
}
},
ai: {
threaten: 1.3,
},
},
//张角
guidao_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill/青青子衿:1",
trigger: { global: "judge" },
filter(event, player) {
return player.countCards("hes", {
color: "black",
}) > 0;
},
async cost(event, trigger, player) {
event.result = await player
.chooseCard({
prompt:
`${get.translation(trigger.player)}的` +
`${trigger.judgestr || ""}判定为` +
`${get.translation(trigger.player.judging[0])},` +
`${get.prompt(event.skill)}`,
position: "hes",
filterCard(card, player) {
if (get.color(card, player) != "black") {
return false;
}
const enabled = game.checkMod(
card,
player,
"unchanged",
"cardEnabled2",
player
);
if (enabled != "unchanged") {
return !!enabled;
}
const respondable = game.checkMod(
card,
player,
"unchanged",
"cardRespondable",
player
);
if (respondable != "unchanged") {
return !!respondable;
}
return true;
},
ai(card) {
const trigger = get.event().getTrigger();
const player = get.player();
const judging = get.event().judging;
const result =
trigger.judge(card) -
trigger.judge(judging);
const attitude = get.attitude(
player,
trigger.player
);
if (attitude == 0 || result == 0) {
return 0;
}
let value = get.value(card);
if (get.subtype(card) == "equip2") {
value /= 2;
} else {
value /= 6;
}
return attitude > 0
? result - value
: -result - value;
},
})
.set("judging", trigger.player.judging[0])
.forResult();
},
popup: false,
async content(event, trigger, player) {
const next = player.respond({
cards: event.cards,
skill: event.name,
highlight: true,
noOrdering: true,
});
await next;
const cards = next.cards;
if (!cards?.length) return;
const judgingCard = trigger.player.judging[0];
player.$gain2(judgingCard);
await player.gain(judgingCard);
trigger.player.judging[0] = cards[0];
trigger.orderingCards.addArray(cards);
game.log(trigger.player, "的判定牌改为", cards);
await game.delay(2);
},
ai: {
rejudge: true,
tag: {
rejudge: 1,
},
},
},
leiji_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill/青青子衿:1",
trigger: { player: ["useCard", "respond"] },
filter(event, player) {
return (
event.card.name == "shan" &&
player
.getEnemies(null, false)
.some(target => target.isIn())
);
},
line: "thunder",
async cost(event, trigger, player) {
event.result = await player
.chooseTarget(
get.prompt2(event.skill),
(card, player, target) => {
return (
target != player &&
player
.getEnemies(null, false)
.includes(target)
);
}
)
.set("ai", target => {
const player = get.player();
if (target.hasSkill("hongyan")) return 0;
return get.damageEffect(
target,
player,
player,
"thunder"
);
})
.forResult();
},
async content(event, trigger, player) {
const target = event.targets[0];
const judge = target.judge(card => {
const suit = get.suit(card);
if (suit == "spade") return -4;
if (suit == "club") return -2;
return 0;
});
judge.judge2 = result => result.bool == false;
const result = await judge.forResult();
if (result.suit == "spade") {
await target.damage(2, "thunder", player);
} else if (result.suit == "club") {
if (player.isDamaged()) {
await player.recover();
}
if (target.isIn()) {
await target.damage(1, "thunder", player);
}
}
},
ai: {
useShan: true,
threaten: 1.8,
},
},
zhuzheng_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill/青青子衿:1",
trigger: { global: "useCard2" },
filter(event, player) {
const source = event.player;
if (
source == player ||
event.card.name != "sha" ||
!event.targets?.length ||
event.targets.includes(player) ||
!source.inRange(player) ||
player.countCards("h") == 0 ||
!player.getEnemies(null, false).includes(source)
) {
return false;
}
const enemies = player.getEnemies(null, false);
return event.targets.some(target => {
return target.isIn() && !enemies.includes(target);
});
},
async cost(event, trigger, player) {
event.result = await player
.chooseCard(
"h",
get.prompt(event.skill, trigger.player),
"将一张手牌置于牌堆顶,取消此【杀】的所有目标"
)
.set("ai", card => {
const player = get.player();
const trigger = get.event().getTrigger();
const enemies = player.getEnemies(null, false);
const protectValue = trigger.targets
.filter(target => !enemies.includes(target))
.reduce((sum, target) => {
return (
sum -
get.effect(
target,
trigger.card,
trigger.player,
player
)
);
}, 0);
const becomeTarget =
getTianshuDifficulty() == "nightmare" ||
get.color(trigger.card) != "black";
const selfValue = becomeTarget
? get.effect(
player,
trigger.card,
trigger.player,
player
)
: 0;
if (protectValue + selfValue <= 0) {
return 0;
}
return 7 - get.value(card);
})
.forResult();
},
async content(event, trigger, player) {
await player.lose(event.cards, ui.cardPile, "insert");
trigger.targets.length = 0;
const becomeTarget =
getTianshuDifficulty() == "nightmare" ||
get.color(trigger.card) != "black";
if (becomeTarget && player.isIn()) {
trigger.all_excluded = false;
trigger.targets.add(player);
game.log(player, "成为了", trigger.card, "的目标");
} else {
trigger.all_excluded = true;
game.log(trigger.card, "失去了所有目标");
}
},
ai: {
threaten: 1.5,
},
},
yinlei_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill/青青子衿:1",
trigger: {
player: "loseAfter",
global: [
"equipAfter",
"addJudgeAfter",
"gainAfter",
"loseAsyncAfter",
"addToExpansionAfter",
],
},
forced: true,
locked: true,
filter(event, player) {
if (
getTianshuDifficulty() == "normal" ||
_status.currentPhase == player
) {
return false;
}
const loseEvent = event.getl?.(player);
return (
loseEvent?.cards2?.length > 0 &&
game.hasPlayer(target => {
return target.isIn() && !target.isLinked();
})
);
},
logTarget(event, player) {
return game
.filterPlayer(target => {
return target.isIn() && !target.isLinked();
})
.randomGet();
},
async content(event, trigger, player) {
const targets = game.filterPlayer(target => {
return target.isIn() && !target.isLinked();
});
if (!targets.length) return;
const target = targets.randomGet();
if (target != player) {
player.line(target);
}
await target.link(true);
},
ai: {
threaten: 1.3,
},
},
};
+16
View File
@@ -35,7 +35,23 @@ const translates = {
qubu_shuying: "驭布",
qubu_shuying_info: "锁定技,当友方角色使用【杀】指定目标时,进行一次判定:若结果为黑桃,你对此【杀】的所有目标造成1点伤害。",
yuanshu_boss_shuying: "袁术",
yongsi_shuying: "庸肆",
yongsi_shuying_info: "锁定技,摸牌阶段,你改为摸X张牌(X为存活势力数);弃牌阶段,若你本回合:1.没有造成伤害,将手牌摸至当前体力值;2.造成伤害数超过1点,本回合手牌上限改为已损失体力值。",
wangzun_shuying: "妄尊",
wangzun_shuying_info: "锁定技,其他角色其他敌方角色的结束阶段,若其本回合:1.没有对你造成伤害,则其弃置1张牌;2.对你造成伤害数超过1点,则你对其造成1点伤害。",
duoxi_shuying: "夺玺",
duoxi_shuying_info: "其他角色的摸牌阶段,你可以失去1点体力改为你与其各摸一张牌。",
zhangjiao_boss_shuying: "张角",
guidao_shuying: "鬼道",
guidao_shuying_info: "当一名角色的判定牌生效前,你可以打出一张黑色牌替换之。",
leiji_shuying: "雷击",
leiji_shuying_info: "每当你使用或打出【闪】时,你可以令一名其他角色进行判定,若结果为:黑桃,你对该角色造成2点雷电伤害;梅花,你回复1点体力,然后对该角色造成1点雷电伤害。",
zhuzheng_shuying: "谏征",
zhuzheng_shuying_info: "其他角色使用【杀】指定除你外的角色时,若你在其攻击范围内,你可以将一张手牌置于牌堆顶,取消所有目标,然后若此【杀】不为黑色,你成为目标。",
yinlei_shuying: "引雷",
yinlei_shuying_info: "锁定技,当你失去牌时,随机横置一名角色。",
};
export default translates;
+20 -4
View File
@@ -9,27 +9,41 @@ const characters = {
isBossAllowed: true,
img: "extension/术樱包/pve/images/天书乱斗/shuishengonggong_boss_shuying.png"
},
// huoshenzhurong_boss_shuying: {},
huoshenzhurong_boss_shuying:
{
sex: "male",
group: "shen",
hp: 4,
skills: ["xingxia_shuying", "baoyan_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/天书乱斗/huoshenzhurong_boss_shuying.png"
},
baiqi_boss_shuying:
{
sex: "male",
group: "shen",
hp: 4,
skills: ["changsheng_shuying", "shashen_shuying", "wuan_shuying"],
skills: ["wuan_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/天书乱斗/baiqi_boss_shuying.png"
},
kuafu_boss_shuying:
{
sex: "male",
group: "shen",
hp: 4,
skills: ["shenqu_shuying", "lieben_shuying", "yinjiang_shuying", "zhuri_shuying"],
skills: ["zhuri_shuying", "yinjiang_shuying", "lieben_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/天书乱斗/kuafu_boss_shuying.png"
img: "extension/术樱包/pve/images/天书乱斗/kuafu_boss_shuying.png",
dieAudios: ["ext:术樱包/pve/audio/dead/天书乱斗/kuafu_boss_shuying.mp3"]
},
shaohao_boss_shuying:
{
sex: "male",
@@ -40,6 +54,7 @@ const characters = {
isBossAllowed: true,
img: "extension/术樱包/pve/images/天书乱斗/shaohao_boss_shuying.png"
},
xuannv_boss_shuying: {
sex: "female",
group: "shen",
@@ -50,6 +65,7 @@ const characters = {
img: "extension/术樱包/pve/images/天书乱斗/xuannv_boss_shuying.png",
dieAudios: ["ext:术樱包/pve/audio/dead/天书乱斗/xuannv_boss_shuying.mp3"]
},
hanba_boss_shuying: {
sex: "female",
group: "shen",
+60
View File
@@ -14,6 +14,29 @@ const dynamicTranslates = {
return `锁定技,结束阶段,若本回合进入弃牌堆的卡牌数量:大于4,你回复${hp}点体力;大于5,你摸${card}张牌;大于9,你对所有敌方角色造成${damage}点伤害。`
},
//火神祝融
xingxia_shuying(player, skill) {
const difficulty = getTianshuDifficulty();
let discardNum = 1;
let damageNum = 1;
if (difficulty == "hard") {
discardNum = 2;
} else if (difficulty == "nightmare") {
discardNum = 3;
damageNum = 2;
}
return `锁定技,出牌阶段开始时,你对随机一名友方角色造成1点火焰伤害,然后令所有敌方角色选择一项:1.弃置${discardNum}张红色牌;2.受到你造成的${damageNum}点火焰伤害。`;
},
baoyan_shuying(player, skill) {
if (getTianshuDifficulty() == "nightmare") {
return "锁定技,每当有角色造成火焰伤害后,你获得1个“炎”标记。你的回合结束时,弃置所有“炎”标记,随机对一名敌方角色造成X点火焰伤害。(X为弃置的“炎”标记数)";
}
return "锁定技,每当有角色造成火焰伤害后,你获得1个“炎”标记。你的回合结束时,弃置所有“炎”标记,随机对X名敌方角色各造成1点火焰伤害。(X为弃置的“炎”标记数)";
},
//少昊
baiyi_shuying(player, skill) {
let num = 1;
@@ -27,6 +50,43 @@ const dynamicTranslates = {
return `锁定技,每名敌方角色的回合开始时,若当前轮数小于3,你随机获得其${num}张牌;若当前轮数小于5,对其造成${num}点雷电伤害;若当前轮数小于7,其随机弃置${num}张牌。`;
},
//白起
wuan_shuying(player, skill) {
const difficulty = getTianshuDifficulty();
let num = 1;
if (difficulty == "hard") {
num = 2;
} else if (difficulty == "nightmare") {
num = 3;
}
return `锁定技,你可使用的【杀】的次数+${num},【杀】造成的伤害+1。`;
},
shashen_shuying(player, skill) {
if (getTianshuDifficulty() == "nightmare") {
return "你可以将手牌中的任意牌当【杀】使用或打出。你使用的【杀】造成伤害后,摸3张牌。";
}
return "你可以将手牌中的任意牌当【杀】使用或打出。每回合你使用的第一张【杀】造成伤害后,摸2张牌。";
},
//夸父
yinjiang_shuying(player, skill) {
if (getTianshuDifficulty() == "nightmare") {
return "锁定技,当你在出牌阶段摸牌后,获得牌堆底的牌。若该牌为红色,则对所有敌方角色造成1点伤害。当你在同一阶段内以此法造成过两次或更多的伤害后,该技能失效直到回合结束。";
}
return "锁定技,当你在出牌阶段摸牌后,获得牌堆底的牌。若该牌为红色,则随机对一名敌方角色造成1点伤害。当你在同一阶段内以此法造成过两次或更多的伤害后,该技能失效直到回合结束。";
},
lieben_shuying(player, skill) {
if (getTianshuDifficulty() == "nightmare") {
return "锁定技,当你使用【杀】指定目标后,使用牌堆底的牌进行一次判定:若判定结果为红色,则此【杀】不计入出牌阶段使用次数且伤害+1。";
}
return "锁定技,当你使用【杀】指定目标后,使用牌堆底的牌进行一次判定:若判定结果为红色,则此【杀】不计入出牌阶段使用次数。";
},
//玄女
xuanlie_shuying(player, skill) {
const difficulty = getTianshuDifficulty();
+372 -35
View File
@@ -89,26 +89,229 @@ const skills = {
},
},
//白起
//火神祝融 over
xingxia_shuying: {
mode: ["boss"],
audio: false,
trigger: {
player: "phaseUseBegin",
},
forced: true,
locked: true,
filter(event, player) {
const enemies = player.getEnemies(null, false);
return (
game.hasPlayer(target => {
return target.isIn() && !enemies.includes(target);
}) &&
enemies.some(target => target.isIn())
);
},
async content(event, trigger, player) {
const difficulty = getTianshuDifficulty();
const enemies = player
.getEnemies(null, false)
.filter(target => target.isIn());
const friends = game.filterPlayer(target => {
return target.isIn() && !enemies.includes(target);
});
if (!friends.length || !enemies.length) return;
const friend = friends.randomGet();
player.line(friend, "fire");
await friend.damage(1, player, "fire");
let discardNum = 1;
let damageNum = 1;
if (difficulty == "hard") {
discardNum = 2;
} else if (difficulty == "nightmare") {
discardNum = 3;
damageNum = 2;
}
for (const target of enemies) {
if (!target.isIn()) continue;
const canDiscard =
target.countCards("he", card => {
return (
get.color(card, target) == "red" &&
lib.filter.cardDiscardable(card, target)
);
}) >= discardNum;
let discarded = false;
if (canDiscard) {
const result = await target
.chooseToDiscard(
"he",
discardNum,
`行夏:弃置${get.cnNumber(discardNum)}张红色牌,或受到${get.cnNumber(damageNum)}点火焰伤害`
)
.set("filterCard", (card, target) => {
return (
get.color(card, target) == "red" &&
lib.filter.cardDiscardable(card, target)
);
})
.set("source", player)
.set("damageNum", damageNum)
.set("ai", card => {
const target = get.player();
const damageNum = get.event().damageNum;
if (target.hp <= damageNum) {
return 12 - get.value(card);
}
return 7 - get.value(card);
})
.forResult();
discarded = result.bool;
}
if (!discarded && target.isIn()) {
player.line(target, "fire");
await target.damage(damageNum, player, "fire");
}
}
},
ai: {
threaten: 1.8,
},
},
baoyan_shuying: {
mode: ["boss"],
audio: false,
trigger: {
global: "damageSource",
},
forced: true,
locked: true,
filter(event, player) {
return event.num > 0 && event.hasNature("fire");
},
async content(event, trigger, player) {
player.addMark("baoyan_shuying", 1, false);
},
marktext: "炎",
intro: {
name: "炎",
content: "mark",
},
group: "baoyan_shuying_burst",
subSkill: {
burst: {
audio: false,
trigger: {
player: "phaseEnd",
},
forced: true,
locked: true,
filter(event, player) {
return (
player.countMark("baoyan_shuying") > 0 &&
player
.getEnemies(null, false)
.some(target => target.isIn())
);
},
async content(event, trigger, player) {
const num = player.countMark("baoyan_shuying");
player.removeMark("baoyan_shuying", num, false);
const enemies = player
.getEnemies(null, false)
.filter(target => target.isIn());
if (!enemies.length) return;
if (getTianshuDifficulty() == "nightmare") {
const target = enemies.randomGet();
player.line(target, "fire");
await target.damage(num, player, "fire");
return;
}
const targets = enemies.randomGets(
Math.min(num, enemies.length)
);
for (const target of targets) {
if (!target.isIn()) continue;
player.line(target, "fire");
await target.damage(1, player, "fire");
}
},
},
},
ai: {
threaten: 1.7,
},
},
huoshen_shuying: {
mode: ["boss"],
audio: false,
trigger: {
global: "damageSource",
},
forced: true,
locked: true,
filter(event, player) {
return (
getTianshuDifficulty() == "nightmare" &&
event.num > 0 &&
event.hasNature("fire")
);
},
async content(event, trigger, player) {
if (player.isDamaged()) {
await player.recover();
await player.draw();
} else {
await player.draw(3);
}
},
ai: {
threaten: 2,
},
},
//白起 over
changsheng_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill:1",
audio: false,
forced: true,
locked: true,
mod: {
targetInRange(card, player, target) {
if (card.name == "sha") return true;
const difficulty = getTianshuDifficulty();
if (
difficulty != "hard" &&
card.name == "sha"
) {
return true;
}
},
},
ai: {
unequip: true,
threaten: 1.2,
},
},
shashen_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill:1",
audio: false,
enable: ["chooseToUse", "chooseToRespond"],
filterCard: true,
position: "h",
viewAs: { name: "sha" },
viewAsFilter(player) {
return player.countCards("h") > 0;
return (
getTianshuDifficulty() != "normal" &&
player.countCards("h") > 0
);
},
prompt: "将一张手牌当【杀】使用或打出",
check(card) {
@@ -118,29 +321,77 @@ const skills = {
subSkill: {
draw: {
trigger: { source: "damageEnd" },
forced: true,
locked: true,
usable(skill, player) {
return getTianshuDifficulty() == "hard"
? 1
: Infinity;
},
filter(event, player) {
return event.card?.name == "sha";
const difficulty = getTianshuDifficulty();
if (
difficulty == "normal" ||
event.card?.name != "sha"
) {
return false;
}
if (difficulty == "nightmare") {
return true;
}
// 困难难度只检查本回合使用的第一张【杀】
const useEvent = event.getParent("useCard");
if (!useEvent) return false;
const firstSha = player.getHistory(
"useCard",
evt => evt.card.name == "sha"
)[0];
return useEvent == firstSha;
},
async content(event, trigger, player) {
await player.draw(3);
const num =
getTianshuDifficulty() == "nightmare"
? 3
: 2;
await player.draw(num);
},
},
},
ai: {
respondSha: true,
skillTagFilter(player) {
return player.countCards("h") > 0;
return (
getTianshuDifficulty() != "normal" &&
player.countCards("h") > 0
);
},
order(item, player) {
return get.order({ name: "sha" }, player) - 0.1;
},
order: 4,
},
},
wuan_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill:1",
audio: false,
forced: true,
locked: true,
mod: {
cardUsable(card, player, num) {
if (card.name == "sha") return num + 3;
if (card.name != "sha") return;
const difficulty = getTianshuDifficulty();
if (difficulty == "nightmare") {
return num + 3;
}
if (difficulty == "hard") {
return num + 2;
}
return num + 1;
},
},
trigger: { source: "damageBegin1" },
@@ -150,62 +401,129 @@ const skills = {
async content(event, trigger, player) {
trigger.num++;
},
ai: {
damageBonus: true,
threaten: 2,
},
},
//盘古
//夸父 over
shenqu_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill:1",
audio: false,
trigger: { player: "damageEnd" },
forced: true,
locked: true,
filter(event, player) {
return player.countCards("h", card => get.color(card) == "red") > 0;
return (
getTianshuDifficulty() == "nightmare" &&
player.countCards("h", card => get.color(card) == "red") > 0
);
},
async content(event, trigger, player) {
const cards = player.getCards("h", card => get.color(card) == "red");
await player.lose(cards, ui.cardPile, "insert");
await player.lose(cards, ui.cardPile);
await player.draw(cards.length);
},
ai: {
threaten: 1.5,
},
},
lieben_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill:1",
audio: "ext:术樱包/pve/audio/skill/天书乱斗:1",
trigger: { player: "useCardToPlayered" },
forced: true,
locked: true,
filter(event, player) {
return event.card.name == "sha" && ui.cardPile.lastChild;
return (
event.card.name == "sha" &&
event.isFirstTarget &&
ui.cardPile.lastChild
);
},
async content(event, trigger, player) {
const judge = player.judge(card => get.color(card) == "red" ? 2 : -1);
const judge = player.judge(card => {
return get.color(card) == "red" ? 2 : -1;
});
judge.directresult = ui.cardPile.lastChild;
judge.judge2 = result => result.bool;
const result = await judge.forResult();
if (result.bool) {
trigger.getParent().addCount = false;
trigger.getParent().baseDamage++;
if (!result.bool) return;
const useEvent = trigger.getParent();
if (useEvent.addCount !== false) {
useEvent.addCount = false;
const stat = player.getStat().card;
if (typeof stat.sha == "number") {
stat.sha--;
}
}
if (getTianshuDifficulty() == "nightmare") {
useEvent.baseDamage = (useEvent.baseDamage || 1) + 1;
}
},
ai: {
threaten: 1.4,
},
},
yinjiang_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill:1",
audio: "ext:术樱包/pve/audio/skill/天书乱斗:1",
trigger: { player: "drawEnd" },
forced: true,
locked: true,
filter(event, player) {
return player.isPhaseUsing() && !player.hasSkill("yinjiang_shuying_disabled") && ui.cardPile.lastChild;
return (
player.isPhaseUsing() &&
!player.hasSkill("yinjiang_shuying_disabled") &&
ui.cardPile.lastChild
);
},
async content(event, trigger, player) {
const card = ui.cardPile.lastChild;
await player.gain(card, "gain2");
if (get.color(card) == "red") {
let count = 0;
for (const target of player.getEnemies(null, false).filter(target => target.isIn())) {
player.line(target);
await target.damage(player);
if (get.color(card) != "red") return;
const enemies = player
.getEnemies(null, false)
.filter(target => target.isIn());
if (!enemies.length) return;
const targets =
getTianshuDifficulty() == "nightmare"
? enemies
: [enemies.randomGet()];
let count = 0;
for (const target of targets) {
if (!target?.isIn()) continue;
player.line(target);
const damageEvent = target.damage(1, player);
await damageEvent;
// 只统计实际成立的伤害事件
if (player.getHistory("sourceDamage").includes(damageEvent)) {
count++;
}
player.storage.yinjiang_shuying_count = (player.storage.yinjiang_shuying_count || 0) + count;
if (player.storage.yinjiang_shuying_count >= 2) player.addTempSkill("yinjiang_shuying_disabled", { player: "phaseUseEnd" });
}
if (!count) return;
player.storage.yinjiang_shuying_count =
(player.storage.yinjiang_shuying_count || 0) + count;
if (player.storage.yinjiang_shuying_count >= 2) {
player.addTempSkill("yinjiang_shuying_disabled", {
player: "phaseAfter",
});
}
},
group: "yinjiang_shuying_clear",
@@ -213,22 +531,30 @@ const skills = {
clear: {
trigger: { player: "phaseUseEnd" },
silent: true,
charlotte: true,
async content(event, trigger, player) {
delete player.storage.yinjiang_shuying_count;
},
},
disabled: { charlotte: true },
disabled: {
charlotte: true,
},
},
ai: {
threaten: 2,
},
},
zhuri_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill:1",
audio: "ext:术樱包/pve/audio/skill/天书乱斗:1",
forced: true,
locked: true,
group: ["zhuri_shuying_draw", "zhuri_shuying_bottom"],
subSkill: {
draw: {
trigger: { player: "useCard" },
forced: true,
locked: true,
filter(event, player) {
return get.color(event.card) == "red";
},
@@ -239,16 +565,27 @@ const skills = {
bottom: {
trigger: { global: "useCardAfter" },
forced: true,
locked: true,
filter(event, player) {
return event.targets?.includes(player) && (event.card.name == "sha" || get.type(event.card) == "trick") && event.cards?.filterInD("d").length;
return (
event.targets?.includes(player) &&
(event.card.name == "sha" ||
get.type(event.card) == "trick") &&
event.cards?.someInD("d")
);
},
async content(event, trigger, player) {
const cards = trigger.cards.filterInD("d");
if (!cards.length) return;
game.log(cards, "被置于牌堆底");
for (const card of cards) ui.cardPile.appendChild(card);
await game.cardsGotoPile(cards);
},
},
},
ai: {
threaten: 1.8,
},
},
//少昊 over
@@ -482,7 +819,7 @@ const skills = {
},
},
//旱魃
//旱魃 over
xinji_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill/天书乱斗:2",
+14 -8
View File
@@ -7,23 +7,31 @@ const translates = {
juehong_shuying: "决洪",
juehong_shuying_info: "锁定技,准备阶段,你令所有敌方角色自己弃置自己的装备区内的所有牌,若其装备区内没有牌,则改为随机弃置2张手牌",
huoshenzhurong_boss_shuying: "火神祝融",
xingxia_shuying: "行夏",
xingxia_shuying_info: "锁定技,出牌阶段开始时,你对随机1名友方角色造成1点火焰伤害,然后令所有敌方角色选择一项:1.弃置1张红色牌;2.受到你造成的1点火焰伤害",
baoyan_shuying: "爆炎",
baoyan_shuying_info: "锁定技,每当有角色造成火焰伤害后,你获得1个”炎‘标记。你的回合结束时,弃置所有“炎”标记,随机对X名敌人各造成1点火焰伤害。(X为弃置的炎标记数量)",
huoshen_shuying: "火神",
huoshen_shuying_info: "锁定技,每当有角色造成火焰伤害后,你回复1点体力并摸1张牌,若你的体力值为满,改为摸3张牌",
baiqi_boss_shuying: "白起",
changsheng_shuying: "常胜",
changsheng_shuying_info: "锁定技,你使用【杀】无距离限制。",
shashen_shuying: "杀神",
shashen_shuying_info: "你可以将手牌中的任意牌当【杀】使用或打出。你使用的【杀】造成伤害后,摸三张牌。",
shashen_shuying_info: "你可以将手牌中的任意牌当【杀】使用或打出。每回合你使用的第一张【杀】造成伤害后,摸2张牌。",
wuan_shuying: "武安",
wuan_shuying_info: "锁定技,你可使用的【杀】的次数+3,【杀】造成的伤害+1。",
wuan_shuying_info: "锁定技,你可使用的【杀】的次数+1,【杀】造成的伤害+1。",
kuafu_boss_shuying: "盘古",
kuafu_boss_shuying: "夸父",
shenqu_shuying: "神躯",
shenqu_shuying_info: "锁定技,当你受到伤害后,将所有红色手牌置于牌堆底,并摸等量的牌。",
lieben_shuying: "烈奔",
lieben_shuying_info: "锁定技,当你使用【杀】指定目标后,使用牌堆底的牌进行一次判定:若判定结果为红色,则此杀不计入出牌阶段使用次数且伤害+1。",
lieben_shuying_info: "锁定技,当你使用【杀】指定目标后,使用牌堆底的牌进行一次判定:若判定结果为红色,则此杀不计入出牌阶段使用次数。",
yinjiang_shuying: "饮江",
yinjiang_shuying_info: "锁定技,当你在出牌阶段摸牌后,获得牌堆底的1张牌。若该牌为红色,则对所有敌方角色造成1点伤害。当你在同一个阶段内以此法造成过两次活更多的伤害后,该技能失效直到回合结束。",
yinjiang_shuying_info: "锁定技,当你在出牌阶段摸牌后,获得牌堆底的牌。若该牌为红色,则随机对一名敌方角色造成1点伤害。当你在同一阶段内以此法造成过两次或更多的伤害后,该技能失效直到回合结束。",
zhuri_shuying: "逐日",
zhuri_shuying_info: "锁定技,你使用红色牌时摸1张牌。当你成为【杀】或普通锦囊牌的目标后,结算完毕后将该牌放置在牌堆底。",
zhuri_shuying_info: "锁定技,你使用红色牌时摸一张牌。当你成为【杀】或普通锦囊牌的目标后,结算完毕后将该牌放置在牌堆底。",
shaohao_boss_shuying: "少昊",
baiyi_shuying: "白仪",
@@ -31,7 +39,6 @@ const translates = {
shenen_shuying: "神恩",
shenen_shuying_info: "锁定技,所有己方角色使用牌锁定技,所有己方角色使用牌没有距离限制。",
xuannv_boss_shuying: "玄女",
xuanlie_shuying: "玄烈",
xuanlie_shuying_info: "锁定技,回合结束时,对所有本回合你获得过其牌的敌方角色依次造成1点伤害。",
@@ -47,7 +54,6 @@ const translates = {
zhiri_shuying_info: "锁定技,当敌方角色使用红色锦囊牌指定目标后,你摸1张牌。",
fenshi_shuying: "焚世",
fenshi_shuying_info: "锁定技,准备阶段,若你的手牌数小于体力值,则将手牌摸至于体力值相等;若你的手牌数大于体力值,则你对敌方角色造成共计X点伤害,点数随机分配(X为手牌数减体力值)。",
};
export default translates;
+10 -1
View File
@@ -1,3 +1,12 @@
const voices = {};
const voices = {
"#zhuri_shuying1": "狂飙八万里,逐日九重天!",
"#yinjiang_shuying1": "大江三千顷,豪饮一息间!",
"#lieben_shuying1": "奔雷袭紫电,驰羽断青锋!",
"#kuafu_boss_shuying:die": "大泽将涸,金乌难触。",
"#dishi_shuying1": "慈航化众生,司南引轩辕。",
"#jiutian_shuying1": "天上九宫十二城,仙云袅袅授长生。",
"#xuanlie_shuying1": "碧霄一鹤引霞至,玄女箜篌九天来。",
"#xuannv_boss_shuying:die": "世间何来两全法,不负仙旅不负卿。",
};
export default voices;
+15 -15
View File
@@ -6,7 +6,7 @@ const characters = {
skills: ["aotang_shuying", "yunju_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/捉鬼驱邪/mengpo_boss_shuying.png"
img: "extension/术樱包/pve/images/驱鬼逐邪/mengpo_boss_shuying.png"
},
baowei_boss_shuying:
@@ -17,7 +17,7 @@ const characters = {
skills: ["eli_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/捉鬼驱邪/baowei_boss_shuying.png"
img: "extension/术樱包/pve/images/驱鬼逐邪/baowei_boss_shuying.png"
},
niaozui_boss_shuying:
@@ -28,7 +28,7 @@ const characters = {
skills: ["suoxue_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/捉鬼驱邪/niaozui_boss_shuying.png"
img: "extension/术樱包/pve/images/驱鬼逐邪/niaozui_boss_shuying.png"
},
heibaiwuchang_boss_shuying:
@@ -39,8 +39,8 @@ const characters = {
skills: ["mizui_shuying", "qiangzheng_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/捉鬼驱邪/heibaiwuchang_boss_shuying.png",
dieAudios: ["ext:术樱包/pve/audio/dead/捉鬼驱邪/heibaiwuchang.mp3"]
img: "extension/术樱包/pve/images/驱鬼逐邪/heibaiwuchang_boss_shuying.png",
dieAudios: ["ext:术樱包/pve/audio/dead/驱鬼逐邪/heibaiwuchang.mp3"]
},
niutoumamian_boss_shuying: {
@@ -50,8 +50,8 @@ const characters = {
skills: ["manji_shuying", "shiyv_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/捉鬼驱邪/niutoumamian_boss_shuying.png",
dieAudios: ["ext:术樱包/pve/audio/dead/捉鬼驱邪/niutoumamian.mp3"]
img: "extension/术樱包/pve/images/驱鬼逐邪/niutoumamian_boss_shuying.png",
dieAudios: ["ext:术樱包/pve/audio/dead/驱鬼逐邪/niutoumamian.mp3"]
},
yvsai_boss_shuying: {
@@ -61,7 +61,7 @@ const characters = {
skills: ["anchao_shuying", "guixi_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/捉鬼驱邪/yvsai_boss_shuying.png"
img: "extension/术樱包/pve/images/驱鬼逐邪/yvsai_boss_shuying.png"
},
huangfeng_boss_shuying: {
@@ -71,7 +71,7 @@ const characters = {
skills: ["mingchong_shuying", "duzhen_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/捉鬼驱邪/huangfeng_boss_shuying.png"
img: "extension/术樱包/pve/images/驱鬼逐邪/huangfeng_boss_shuying.png"
},
riyeyoushen_boss_shuying: {
@@ -81,8 +81,8 @@ const characters = {
skills: ["huiyun_shuying", "yezhong_shuying", "zhoucha_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/捉鬼驱邪/riyeyoushen_boss_shuying.png",
dieAudios: ["ext:术樱包/pve/audio/dead/捉鬼驱邪/riyeyoushen.mp3"]
img: "extension/术樱包/pve/images/驱鬼逐邪/riyeyoushen_boss_shuying.png",
dieAudios: ["ext:术樱包/pve/audio/dead/驱鬼逐邪/riyeyoushen.mp3"]
},
guiwang_boss_shuying: {
@@ -92,18 +92,18 @@ const characters = {
skills: ["jizhou_shuying", "danshi_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/捉鬼驱邪/guiwang_boss_shuying.png",
dieAudios: ["ext:术樱包/pve/audio/dead/捉鬼驱邪/guiwang.mp3"]
img: "extension/术樱包/pve/images/驱鬼逐邪/guiwang_boss_shuying.png",
dieAudios: ["ext:术樱包/pve/audio/dead/驱鬼逐邪/guiwang.mp3"]
},
yanluowang_boss_shuying: {
sex: "male",
group: "qun",
hp: 4,
skills: ["jizhou_shuying", "danshi_shuying"],
skills: ["dianwei_shuying", "xingpan_shuying"],
isHiddenBoss: true,
isBossAllowed: true,
img: "extension/术樱包/pve/images/捉鬼驱邪/yanluowang_boss_shuying.png"
img: "extension/术樱包/pve/images/驱鬼逐邪/yanluowang_boss_shuying.png"
},
};
+1 -1
View File
@@ -11,7 +11,7 @@ import perfectPairs from "./perfectPairs.js";
import voices from "./voices.js";
import { characterSort, characterSortTranslate } from "./sort.js";
// 捉鬼驱邪主题数据片段,由活动 BOSS 根入口统一注册。
// 驱鬼逐邪主题数据片段,由活动 BOSS 根入口统一注册。
export default {
character: characters,
card: cards,
+91 -5
View File
@@ -511,6 +511,7 @@ const skills = {
return player.getEnemies(null, false).some(target => target.isIn());
},
async content(event, trigger, player) {
const difficulty = getTianshuDifficulty();
const enemies = player.getEnemies(null, false).filter(target => target.isIn());
if (difficulty == "nightmare") {
@@ -667,7 +668,7 @@ const skills = {
},
},
},
shiyu_shuying: {
shiyv_shuying: {
mode: ["boss"],
audio: "ext:术樱包/pve/audio/skill/驱鬼逐邪:1",
trigger: {
@@ -1457,22 +1458,107 @@ const skills = {
},
forced: true,
locked: true,
silent: true,
popup: false,
mark: true,
filter(event, player) {
return (
getTianshuDifficulty() != "normal" &&
!player.storage.zhennu_shuying_used &&
event.changedHp < 0 &&
player.hp <= player.maxHp / 2 &&
player.hp - event.changedHp > player.maxHp / 2
event.originalHp > event.originalMaxHp / 2 &&
event.originalHp + event.changedHp <= event.originalMaxHp / 2
);
},
async content(event, trigger, player) {
player.storage.zhennu_shuying_used = true;
player.storage.zhennu_shuying_pending = true;
player.storage.zhennu_shuying_pending_type = trigger.getParent()?.name;
trigger.zhennu_shuying_pending = true;
player.markSkill("zhennu_shuying");
},
group: "zhennu_shuying_takeover",
subSkill: {
takeover: {
charlotte: true,
forced: true,
silent: true,
popup: false,
forceDie: true,
trigger: {
player: [
"changeHpAfter",
"damageAfter",
"loseHpAfter",
"dyingAfter",
],
},
filter(event, player, name) {
if (
!player.storage.zhennu_shuying_pending ||
!player.isIn() ||
player.hp <= 0
) {
return false;
}
await player.draw(4);
player.insertPhase();
if (name == "dyingAfter") return true;
if (name == "changeHpAfter") {
return (
event.zhennu_shuying_pending === true &&
!["damage", "loseHp"].includes(
player.storage.zhennu_shuying_pending_type
)
);
}
return (
(name == "damageAfter" &&
player.storage.zhennu_shuying_pending_type ==
"damage") ||
(name == "loseHpAfter" &&
player.storage.zhennu_shuying_pending_type ==
"loseHp")
);
},
async content(event, trigger, player) {
delete player.storage.zhennu_shuying_pending;
delete player.storage.zhennu_shuying_pending_type;
player.logSkill("zhennu_shuying");
const phase = event.getParent("phase");
const phaseLoop = event.getParent("phaseLoop");
if (phase && phaseLoop) {
player.insertPhase("zhennu_shuying");
phaseLoop.player = player;
let activeChild = event;
let current = event.getParent();
while (current && current != phaseLoop) {
const pendingEvents = [
...(current.next || []),
...(current.after || []),
];
for (const pendingEvent of pendingEvents) {
if (pendingEvent == activeChild) continue;
pendingEvent.finish();
pendingEvent.untrigger(true);
}
current.finish();
current.untrigger(true);
if (current == phase) break;
activeChild = current;
current = current.getParent();
}
}
await player.draw(4);
if (!phase || !phaseLoop) {
player.insertPhase("zhennu_shuying", true);
}
},
},
},
intro: {
content(storage, player) {
+1 -1
View File
@@ -9,7 +9,7 @@ const characterSort = {
};
const characterSortTranslate = {
shuYing_pve_zhuoguiquxie: "捉鬼驱邪",
shuYing_pve_zhuoguiquxie: "驱鬼逐邪",
};
export { characterSort, characterSortTranslate };
+34 -1
View File
@@ -1,3 +1,36 @@
const voices = {};
const voices = {
"#aotang_shuying1": "品汤消愁,情怨断念。",
"#yunju_shuying1": "云雾缭绕,飓风意动。",
"#guimei_shuying1": "鬼魅生惑,迷乱心神。",
"#eli_shuying1": "豹贲之象,先锋之将。",
"#bingyi_shuying1": "疑难杂病,入天则安。",
"#suoxue_shuying1": "鸟穴虽小,亦有探索之地。",
"#guixi_shuying1": "吸灵唤魂,游鱼而走。",
"#anchao_shuying1": "暗潮涌动,感水知行。",
"#duzhen_shuying1": "毒针入骨,百药难医。",
"#mingchong_shuying1": "虫身虽小,威力无限。",
"#xingpan_shuying1": "你的罪过,只能用刑偿还。",
"#dianwei_shuying1": "阎罗立矩,斧钺杀威!",
"#xuanpan_shuying1": "阎罗殿上杀威棒,判惩凶宵降猢狲!",
"#xuanpan_shuying2": "本王要尔三更死,安能留汝到五更!",
"#xixing_shuying1": "勾魂夺命,震骨吸星!",
"#taiping_shuying1": "一见发财,天下太平!",
"#qiangzheng_shuying1": "敲骨吸髓,征财索命!",
"#mizui_shuying1": "凡间人世百般苦,唯有西方最极乐!",
"#heibaiwuchang_boss_shuying:die": "索命鬼,终被捉!",
"#xiaoshou_shuying1": "冥府鬼域,岂容尔等放肆!",
"#manji_shuying1": "阴兵蛮霸,力大欺理!",
"#shiyu_shuying1": "生财自有道,香烛狱中取!",
"#guizhao_shuying1": "凡夫俗子,安识诡域之计!",
"#niutoumamian_boss_shuying:die": "当牛做马数百载,只求为人一朝夕!",
"#zhoucha_shuying1": "红煞蔽日,凶兵择噬!",
"#yezhong_shuying1": "黑煞出冢,百鬼夜行!",
"#huiyun_shuying1": "附骨之厄,追魂夺魄!",
"#duane_shuying1": "幽冥之馈,尔等安敢弃之!",
"#riyeyoushen_boss_shuying:die": "山河倒悬,阴阳相逆。",
"#jizhou_shuying1": "鬼王凝咒,言出法随!",
"#danshi_shuying1": "猛鬼食人,择魂而噬!",
"#guiwang_boss_shuying:die": "阎罗殿里说枉死,忘川桥头叹奈何!",
};
export default voices;
+14 -2
View File
@@ -1,6 +1,18 @@
import bossSets from "./sets.js";
import { mergeBossSection } from "./merge.js";
const voices = mergeBossSection("语音台词", bossSets, "voices");
// 技能语音按实际 ext 音频目录生成台词键;阵亡语音仍使用“#角色ID:die”。
const voices = {};
for (const [setName, set] of bossSets) {
for (const [key, text] of Object.entries(set.voices || {})) {
const voiceKey = key.endsWith(":die")
? key
: `#ext:术樱包/pve/audio/skill/${setName}/${key.replace(/^#/, "")}`;
if (Object.prototype.hasOwnProperty.call(voices, voiceKey)) {
console.warn(`活动BOSS:跳过${setName}中重复的语音台词 ID“${voiceKey}”`);
continue;
}
voices[voiceKey] = text;
}
}
export default voices;

Before

Width:  |  Height:  |  Size: 579 KiB

After

Width:  |  Height:  |  Size: 579 KiB

Before

Width:  |  Height:  |  Size: 590 KiB

After

Width:  |  Height:  |  Size: 590 KiB

Before

Width:  |  Height:  |  Size: 680 KiB

After

Width:  |  Height:  |  Size: 680 KiB

Before

Width:  |  Height:  |  Size: 687 KiB

After

Width:  |  Height:  |  Size: 687 KiB

Before

Width:  |  Height:  |  Size: 503 KiB

After

Width:  |  Height:  |  Size: 503 KiB

Before

Width:  |  Height:  |  Size: 621 KiB

After

Width:  |  Height:  |  Size: 621 KiB

Before

Width:  |  Height:  |  Size: 608 KiB

After

Width:  |  Height:  |  Size: 608 KiB

Before

Width:  |  Height:  |  Size: 598 KiB

After

Width:  |  Height:  |  Size: 598 KiB

Before

Width:  |  Height:  |  Size: 617 KiB

After

Width:  |  Height:  |  Size: 617 KiB

Before

Width:  |  Height:  |  Size: 570 KiB

After

Width:  |  Height:  |  Size: 570 KiB

Before

Width:  |  Height:  |  Size: 717 KiB

After

Width:  |  Height:  |  Size: 717 KiB

Before

Width:  |  Height:  |  Size: 705 KiB

After

Width:  |  Height:  |  Size: 705 KiB

Before

Width:  |  Height:  |  Size: 638 KiB

After

Width:  |  Height:  |  Size: 638 KiB

Before

Width:  |  Height:  |  Size: 654 KiB

After

Width:  |  Height:  |  Size: 654 KiB

File diff suppressed because it is too large Load Diff
+525 -60
View File
@@ -1,41 +1,41 @@
const difficultyOrder = [normal, hard, nightmare];
const difficultyOrder = ["normal", "hard", "nightmare"];
const stages = [
{
id: stage1,
name: 第一关,
// bosses: [caocao_boss_shuying, dongzhuo_boss_shuying],
bosses: [zishu_boss_shuying, chouniu_boss_shuying, yinhu_boss_shuying, maotu_boss_shuying, chenlong_boss_shuying, sishe_boss_shuying, wuma_boss_shuying, weiyang_boss_shuying, shenhou_boss_shuying, youji_boss_shuying, xvgou_boss_shuying, haizhu_boss_shuying],
id: "stage1",
name: "第一关",
// bosses: ["zishu_boss_shuying", "yanluowang_boss_shuying"],
bosses: ["zishu_boss_shuying", "chouniu_boss_shuying", "yinhu_boss_shuying", "maotu_boss_shuying", "chenlong_boss_shuying", "sishe_boss_shuying", "wuma_boss_shuying", "weiyang_boss_shuying", "shenhou_boss_shuying", "youji_boss_shuying", "xvgou_boss_shuying", "haizhu_boss_shuying"],
},
{
id: stage2,
name: 第二关,
bosses: [mengpo_boss_shuying, baowei_boss_shuying, niaozui_boss_shuying, heibaiwuchang_boss_shuying, niutoumamian_boss_shuying, yvsai_boss_shuying, huangfeng_boss_shuying, riyeyoushen_boss_shuying, guiwang_boss_shuying, yanluowang_boss_shuying],
id: "stage2",
name: "第二关",
bosses: ["mengpo_boss_shuying", "baowei_boss_shuying", "niaozui_boss_shuying", "heibaiwuchang_boss_shuying", "niutoumamian_boss_shuying", "yvsai_boss_shuying", "huangfeng_boss_shuying", "riyeyoushen_boss_shuying", "guiwang_boss_shuying", "yanluowang_boss_shuying"],
},
{
id: stage3,
name: 第三关,
bosses: [caocao_boss_shuying, simayi_boss_shuying, lvbu_boss_shuying, dongzhuo_boss_shuying],
id: "stage3",
name: "第三关",
bosses: ["caocao_boss_shuying", "simayi_boss_shuying", "lvbu_boss_shuying", "dongzhuo_boss_shuying", "zhangjiao_boss_shuying", "yuanshu_boss_shuying"],
},
{
id: stage4,
name: 第四关,
bosses: [shuishengonggong_boss_shuying, shaohao_boss_shuying, xuannv_boss_shuying, hanba_boss_shuying],
id: "stage4",
name: "第四关",
bosses: ["shuishengonggong_boss_shuying", "huoshenzhurong_boss_shuying", "baiqi_boss_shuying", "kuafu_boss_shuying", "shaohao_boss_shuying", "xuannv_boss_shuying", "hanba_boss_shuying"],
},
];
const bossList = Object.fromEntries(stages.map(stage => [stage.id, stage.bosses]));
const virtualIdols = {
Xiaotao: vtb_xiaotao,
Xiaosha: vtb_xiaosha,
Xiaoshan: vtb_xiaoshan,
Xiaole: vtb_xiaole,
Xiaojiu: vtb_xiaojiu,
Xiaotao: "vtb_xiaotao",
Xiaosha: "vtb_xiaosha",
Xiaoshan: "vtb_xiaoshan",
Xiaole: "vtb_xiaole",
Xiaojiu: "vtb_xiaojiu",
};
const virtualIdolList = Object.values(virtualIdols);
// Boss 的体力与体力上限始终使用相同加成,只需在配置中填写一次。
// Boss 的体力与体力上限始终使用相同加成
const createDifficultyConfig = (startCards, hpBonus = 0) => ({
startCards,
hpBonus,
@@ -88,56 +88,75 @@ const bossDifficulty = {
bosses: {
...createBossGroup(stages[0].bosses, bossDifficultyTemplates.shiershengxiao),
...createBossGroup([yvsai_boss_shuying, huangfeng_boss_shuying], bossDifficultyTemplates.zhuoguiquxie),
...createBossGroup(["yvsai_boss_shuying", "huangfeng_boss_shuying"], bossDifficultyTemplates.zhuoguiquxie),
mengpo_boss_shuying: createBossConfig(bossDifficultyTemplates.zhuoguiquxie, {
nightmare: [guimei_shuying],
nightmare: ["guimei_shuying"],
}),
baowei_boss_shuying: createBossConfig(bossDifficultyTemplates.zhuoguiquxie, addSkillFromHard(yinsha_shuying)),
niaozui_boss_shuying: createBossConfig(bossDifficultyTemplates.zhuoguiquxie, addSkillFromHard(bingyi_shuying)),
baowei_boss_shuying: createBossConfig(bossDifficultyTemplates.zhuoguiquxie, addSkillFromHard("yinsha_shuying")),
niaozui_boss_shuying: createBossConfig(bossDifficultyTemplates.zhuoguiquxie, addSkillFromHard("bingyi_shuying")),
heibaiwuchang_boss_shuying: createBossConfig(bossDifficultyTemplates.zhuoguiquxie, {
hard: [xixing_shuying],
nightmare: [xixing_shuying, taiping_shuying],
hard: ["xixing_shuying"],
nightmare: ["xixing_shuying", "taiping_shuying"],
}),
riyeyoushen_boss_shuying: createBossConfig(bossDifficultyTemplates.zhuoguiquxie, {
nightmare: [duane_shuying],
nightmare: ["duane_shuying"],
}),
niutoumamian_boss_shuying: createBossConfig(bossDifficultyTemplates.zhuoguiquxie, {
hard: [xiaoshou_shuying],
nightmare: [xiaoshou_shuying, guizhao_shuying],
hard: ["xiaoshou_shuying"],
nightmare: ["xiaoshou_shuying", "guizhao_shuying"],
}),
guiwang_boss_shuying: createBossConfig(bossDifficultyTemplates.zhuoguiquxie, {
nightmare: [chihu_shuying],
nightmare: ["chihu_shuying"],
}),
yanluowang_boss_shuying: createBossConfig(bossDifficultyTemplates.zhuoguiquxie, {
hard: [zhennu_shuying],
nightmare: [zhennu_shuying, xuanpan_shuying],
hard: ["zhennu_shuying"],
nightmare: ["zhennu_shuying", "xuanpan_shuying"],
}),
caocao_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard(yishen_shuying)),
simayi_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard(yuanlv_shuying)),
lvbu_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard(zhanjia_shuying)),
dongzhuo_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard(qubu_shuying)),
caocao_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard("yishen_shuying")),
simayi_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard("yuanlv_shuying")),
lvbu_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard("zhanjia_shuying")),
dongzhuo_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard("qubu_shuying")),
yuanshu_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard("duoxi_shuying")),
zhangjiao_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard("yinlei_shuying")),
...createBossGroup([shaohao_boss_shuying, xuannv_boss_shuying, hanba_boss_shuying], bossDifficultyTemplates.tianshuluandou),
shuishengonggong_boss_shuying: createBossConfig(bossDifficultyTemplates.tianshuluandou, addSkillFromHard(shuishen_shuying)),
baiqi_boss_shuying: {},
pangu_boss_shuying: {},
...createBossGroup(["shaohao_boss_shuying", "xuannv_boss_shuying", "hanba_boss_shuying"], bossDifficultyTemplates.tianshuluandou),
shuishengonggong_boss_shuying: createBossConfig(bossDifficultyTemplates.tianshuluandou, {
nightmare: ["shuishen_shuying"],
}),
huoshenzhurong_boss_shuying: createBossConfig(bossDifficultyTemplates.tianshuluandou, {
nightmare: ["huoshen_shuying"],
}),
baiqi_boss_shuying: createBossConfig(bossDifficultyTemplates.tianshuluandou, {
normal: ["changsheng_shuying"],
hard: ["shashen_shuying"],
nightmare: ["changsheng_shuying", "shashen_shuying"],
}),
kuafu_boss_shuying: createBossConfig(bossDifficultyTemplates.tianshuluandou, {
nightmare: ["shenqu_shuying"],
}),
},
};
const tianshuConfig = {
bossList,
stages,
difficulties: {
normal: {
name: 普通,
name: "普通",
tag: "初入天书",
description: "较低的起始手牌与体力加成,适合熟悉四关流程与奖励机制。",
},
hard: {
name: 困难,
name: "困难",
tag: "渐入险境",
description: "Boss拥有更多起始手牌、额外体力,部分Boss会取得追加技能。",
},
nightmare: {
name: 阴间,
name: "阴间",
tag: "九死一生",
description: "Boss获得最高体力和手牌强化,并启用当前配置中的完整追加技能。",
},
},
difficultyOrder,
@@ -152,40 +171,486 @@ const tianshuConfig = {
killDrawIfFullHp: 2,
stageRecover: 1,
stageDraw: 2,
skillChoiceCount: 5,
skillChoiceCount: 6,
},
settings: {
virtualIdolConfigKey: extension_术樱包_tianShu_Xvni,
virtualIdolRandomPoolConfigKey: extension_术樱包_tianShu_XvniRandomPool,
virtualIdolReviveConfigKey: extension_术樱包_tianShu_dead,
addBossConfigKey: extension_术樱包_tianShu_addBoss,
revivePlayersConfigKey: extension_术樱包_tianShu_revivePlayers,
difficultyStatusKey: shuYing_tianshuDifficulty,
virtualIdolConfigKey: "extension_术樱包_tianShu_Xvni",
virtualIdolRandomPoolConfigKey: "extension_术樱包_tianShu_XvniRandomPool",
virtualIdolReviveConfigKey: "extension_术樱包_tianShu_dead",
addBossConfigKey: "extension_术樱包_tianShu_addBoss",
revivePlayersConfigKey: "extension_术樱包_tianShu_revivePlayers",
difficultyStatusKey: "shuYing_tianshuDifficulty",
},
skillPool: {
banned: [huoxin, jueqing, qinqing, beige, huashen, drlt_zhiti, olzhiti, xinfu_pdgyingshi, rebeige],
additional: [
"reguanxing", "rehuoji", "kongcheng", //蜀中无大将
"rezhiheng", "sbjiang", "sbyingzi", "kurou", "repojun", //江东基业
"xinshenxing", "rezhijian", "pothaoshi", "xinzhiyan", //舌战群儒
"sbwushuang", "sbliyu", //天下无双
"ollongdan", "xinliegong", "new_rewusheng", "new_repaoxiao", "new_yajiao", "sbtieji", //五虎上将
"relieren", "decadezhennan", "gzjili", "spjiedao", "rezhiman", "mansi", "spxizhan", //南蛮入侵
"olchengxiang", "sbjizhi", "sbyiji", "zhiyu", //奇策智囊
"rejieming", "kaikang", "new_rejianxiong", "repindi", "xinwangxi", //天下归心
"shencai", "wuling", "reshuishi", "mbtiantao", "mbxinghun", "qiexie", "yuli", "tingwei", //神武再世
"nzry_shicai", "remingce", "mizhao", "xinfu_tushe", "rejigong", //群雄逐鹿
"xiaoji", "liangzhu", "mozhi", "olzhenlie", "zhiren", //乱世佳人
],
banned: ["huoxin", "jueqing", "qinqing", "beige", "huashen", "drlt_zhiti", "olzhiti", "xinfu_pdgyingshi", "rebeige"],
},
keywordBuffs: [
bonds: [
{
keyword: 杀,
id: "shafa",
name: "杀伐",
keyword: "杀",
levels: [
{ count: 2, damageBonus: 1 },
{ count: 3, damageBonus: 2 },
{
level: 1,
count: 2,
condition: { type: "keyword", keyword: "杀", count: 2 },
skills: ["shuYing_Tianshu_BondSha1"],
text: "使用【杀】的次数上限+1",
},
{
level: 2,
count: 3,
condition: { type: "keyword", keyword: "杀", count: 3 },
skills: ["shuYing_Tianshu_BondSha2"],
text: "使用【杀】的次数上限+2",
},
],
},
{
keyword: 闪,
id: "skill_01",
name: "蜀中无大将",
condition: {
type: "skillNames",
any: ["观星", "火计", "空城"],
},
levels: [
{ count: 2, drawAfterShan: 1 },
{
level: 1,
count: 1,
skills: ["dcjincui", "dcqingshi"],
text: "你获得“尽瘁”和“情势”",
},
{
level: 2,
count: 3,
skills: ["dcjincui", "dcqingshi", "dczhizhe", "shuYing_Tianshu_Skill_01"],
text: "你获得“尽瘁”、“情势”和“智哲”,防止你受到的所有无属性伤害,你造成的伤害增加你体力值点",
},
],
},
{
keyword: 桃,
id: "skill_02",
name: "江东基业",
condition: {
type: "skillNames",
any: ["制衡", "激昂", "英姿", "锐军", "英魂", "反间", "诈降", "弓骑", "苦肉", "旋风", "破军", "奇袭"],
},
levels: [
{ count: 2, recoverBonus: 1 },
{
level: 1,
count: 1,
skills: ["shuYing_Tianshu_Skill_02_Skill1"],
text: "你的摸牌阶段额外已损失体力值张牌",
},
{
level: 2,
count: 2,
skills: ["shuYing_Tianshu_Skill_02_Skill2"],
text: "你的摸牌阶段额外已损失体力值张牌,你弃置敌方牌时对其造成1点伤害",
},
{
level: 3,
count: 3,
skills: ["shuYing_Tianshu_Skill_02_Skill3"],
text: "你的摸牌阶段额外已损失体力值张牌,你弃置敌方牌时对其造成等量点伤害,敌方获得你的牌时对其造成等量点伤害",
},
],
},
{
id: "skill_03",
name: "舌战群儒",
condition: {
type: "skillNames",
any: ["慎行", "直谏", "好施", "直言"],
},
levels: [
{
level: 1,
count: 1,
skills: ["shuYing_Tianshu_Skill_03_Skill1"],
text: "你使用牌指定敌方时,随机弃置其1张牌",
},
{
level: 2,
count: 2,
skills: ["shuYing_Tianshu_Skill_03_Skill2"],
text: "你使用牌指定敌方时,随机弃置其2张牌;你使用牌指定友方时其摸1张牌",
}
],
},
{
id: "skill_04",
name: "天下无双",
condition: {
type: "skillNames",
any: ["无双", "利驭"],
},
levels: [
{
level: 1,
count: 1,
skills: ["shuYing_Tianshu_Skill_04_Skill1"],
text: "你使用的【决斗】伤害增加你体力值点",
},
{
level: 2,
count: 2,
skills: ["shuYing_Tianshu_Skill_04_Skill2"],
text: "你使用的【杀】和【决斗】伤害增加你体力值点",
}
],
},
{
id: "skill_05",
name: "五虎上将",
condition: {
type: "skillNames",
any: ["龙胆", "烈弓", "武圣", "咆哮", "涯角", "铁骑"],
},
levels: [
{
level: 1,
count: 1,
skills: ["shuYing_Tianshu_Skill_05_Skill1"],
text: "你使用【杀】伤害+1",
},
{
level: 2,
count: 2,
skills: ["shuYing_Tianshu_Skill_05_Skill2"],
text: "你使用【杀】伤害+1且不限距离和次数",
},
{
level: 3,
count: 3,
skills: ["shuYing_Tianshu_Skill_05_Skill3"],
text: "你使用【杀】伤害+1且不限距离和次数,你的手牌均视为【杀】",
},
],
},
{
id: "skill_06",
name: "南蛮入侵",
condition: {
type: "skillNames",
any: ["烈刃", "镇南", "蒺藜", "截刀", "制蛮", "蛮嗣", "嬉战"],
},
levels: [
{
level: 1,
count: 1,
skills: ["shuYing_Tianshu_Skill_06_Skill1"],
text: "你使用【南蛮入侵】伤害增加你已损失的体力值点且对友方无效",
},
{
level: 2,
count: 2,
skills: ["shuYing_Tianshu_Skill_06_Skill2"],
text: "你使用【南蛮入侵】伤害增加你已损失的体力值点且对友方无效,你的锦囊牌均视为【南蛮入侵】",
},
{
level: 3,
count: 3,
skills: ["shuYing_Tianshu_Skill_06_Skill3"],
text: "你使用【南蛮入侵】伤害增加你已损失的体力值点且对友方无效,你的手牌牌均视为【南蛮入侵】",
},
],
},
{
id: "skill_07",
name: "奇策智囊",
condition: {
type: "skillNames",
any: ["驱虎", "连环", "奇策", "称象", "集智", "鬼才", "遗计", "智愚"],
},
levels: [
{
level: 1,
count: 1,
skills: ["shuYing_Tianshu_Skill_07_Skill1"],
text: "你的锦囊牌无法被响应",
},
{
level: 2,
count: 2,
skills: ["shuYing_Tianshu_Skill_07_Skill2"],
text: "你的锦囊牌无法被响应,且伤害值+1",
},
{
level: 3,
count: 3,
skills: ["shuYing_Tianshu_Skill_07_Skill3"],
text: "你的锦囊牌无法被响应,且伤害值+1,你使用的锦囊牌造成伤害后你摸1张牌。",
},
],
},
{
id: "skill_08",
name: "天下归心",
condition: {
type: "skillNames",
any: ["节命", "慷忾", "贲育", "归心", "据守", "解围", "奸雄", "刚烈", "品第", "忘隙"],
},
levels: [
{
level: 1,
count: 2,
skills: ["shuYing_Tianshu_Skill_08_Skill1"],
text: "你受到敌方造成伤害后,随机获得所有敌方1张手牌,然后重置你的武将牌",
},
{
level: 2,
count: 3,
skills: ["shuYing_Tianshu_Skill_08_Skill2"],
text: "你受到敌方造成的伤害后,随机获得所有敌方伤害值张手牌并对伤害来源造成2点伤害,然后重置你的武将牌",
},
],
},
{
id: "skill_09",
name: "神武再世",
condition: {
type: "skillNames",
any: ["魄袭", "涉猎", "攻心", "龙魂", "连破", "横骛", "神裁", "三首", "斩决", "凤燎", "武神", "五灵", "慧识", "天涛", "星魂", "挈挟", "驭雳", "霆威"],
},
levels: [
{
level: 1,
count: 1,
skills: ["shuYing_Tianshu_Skill_09_Skill1"],
text: "你对敌方造成伤害后其流失1点体力",
},
{
level: 2,
count: 2,
skills: ["shuYing_Tianshu_Skill_09_Skill2"],
text: "你对敌方造成伤害后其流失等量点体力",
},
{
level: 3,
count: 3,
skills: ["shuYing_Tianshu_Skill_09_Skill3"],
text: "你对敌方造成伤害后其流失等量点体力,若有敌方一回合内流失的体力值大于你的体力值,其失去所有体力",
},
],
},
{
id: "skill_10",
name: "群雄逐鹿",
condition: {
type: "skillNames",
any: ["乱击", "酒池", "恃才", "从谏", "庸肆", "明策", "天命", "密诏", "饰非", "绝策", "图射", "狼袭", "自守", "宗室", "急攻", "矢北"],
},
levels: [
{
level: 1,
count: 2,
skills: ["shuYing_Tianshu_Skill_10_Skill1"],
text: "你的锦囊牌均视为【万箭齐发】且对友方无效",
},
{
level: 2,
count: 3,
skills: ["shuYing_Tianshu_Skill_10_Skill2"],
text: "你的锦囊牌均视为【万箭齐发】且对友方无效,你使用【万箭齐发】时随机获得每名敌方目标的各1张手牌",
},
],
},
{
id: "skill_11",
name: "乱世佳人",
condition: {
type: "skillNames",
any: ["枭姬", "倾国", "流离", "离间", "悲歌", "良助", "陈情", "默识", "贞烈", "秘计", "结姻", "闭月", "怠宴", "织纴", "己诫"],
},
levels: [
{
level: 1,
count: 2,
skills: ["shuYing_Tianshu_Skill_11_Skill1"],
text: "你每回合首次使用牌指定异性敌方时,其不能响应此牌;你每回合首对异性敌方造成的伤害+1",
},
{
level: 2,
count: 3,
skills: ["shuYing_Tianshu_Skill_11_Skill2"],
text: "你使用牌指定异性敌方时,其不能响应此牌;你对异性敌方造成的伤害+1",
},
],
},
],
};
// // 羁绊配置模板,仅供复制参考,不会被系统加载。
// // 所有 condition 都只检查玩家过关选择且当前仍持有的技能。
// const bondTemplates = {
// // 技能描述关键词计数,并以最高等级覆盖低等级。
// keywordLevels: {
// id: "keyword_levels_example",
// name: "关键词多等级模板",
// keyword: "杀",
// levels: [
// {
// level: 1,
// count: 2,
// condition: { type: "keyword", keyword: "杀", count: 2 },
// skills: ["reward_skill_level1"],
// text: "两个包含【杀】的过关技能时获得一级效果",
// },
// {
// level: 2,
// count: 3,
// condition: { type: "keyword", keyword: "杀", count: 3 },
// skills: ["reward_skill_level2"],
// text: "三个包含【杀】的过关技能时以二级效果替换一级效果",
// },
// ],
// },
// // 选择一个指定技能后,发放一个羁绊奖励技能。
// singleSkill: {
// id: "single_skill_example",
// name: "单技能模板",
// levels: [{
// level: 1,
// condition: { type: "skill", skill: "guanxing" },
// skill: "reward_skill",
// text: "选择【观星】后获得一个奖励技能",
// }],
// },
// // 按技能译名匹配:guanxing、reguanxing、sbguanxing 等译名均为“观星”时都会满足。
// translatedSkillName: {
// id: "translated_skill_name_example",
// name: "同名技能模板",
// levels: [{
// level: 1,
// condition: { type: "skillName", name: "观星" },
// skill: "reward_skill",
// text: "选择任意版本的【观星】后获得奖励技能",
// }],
// },
// // 多个译名组合:各名称只需命中任意一个同名技能 ID。
// translatedSkillNames: {
// id: "translated_skill_names_example",
// name: "同名技能组合模板",
// levels: [{
// level: 1,
// condition: { type: "skillNames", all: ["观星", "空城"] },
// skills: ["reward_skill_a", "reward_skill_b"],
// text: "选择任意版本的【观星】和【空城】后获得奖励",
// }],
// },
// // 按不同译名计数:观星、界观星等同译名版本合计只算一种,不能重复推进等级。
// translatedSkillNameLevels: {
// id: "translated_skill_name_levels_example",
// name: "蜀中无大将",
// condition: { type: "skillNames", any: ["观星", "火计", "空城"] },
// levels: [
// { level: 1, count: 1, skill: "reward_skill_level1", text: "具有其中一种不同译名时获得一级效果" },
// { level: 2, count: 3, skill: "reward_skill_level2", text: "集齐观星、火计、空城时以二级效果覆盖一级效果" },
// ],
// },
// // 按武将译名关键词检索:配置“甘宁”可匹配☆甘宁、界甘宁、谋甘宁、极·甘宁等名称。
// translatedCharacterSingleSkill: {
// id: "translated_character_single_skill_example",
// name: "武将名称检索单技能模板",
// levels: [{
// level: 1,
// condition: { type: "characterSkillNames", character: "甘宁", all: ["奇袭"] },
// skill: "reward_skill",
// text: "武将译名包含“甘宁”且取得任意版本的【奇袭】后获得奖励技能",
// }],
// },
// // 名称匹配武将取得多个指定译名技能;双将时主将、副将任一名称命中即可。
// translatedCharacterMultipleSkills: {
// id: "translated_character_multiple_skills_example",
// name: "武将名称检索多技能模板",
// levels: [{
// level: 1,
// condition: { type: "characterSkillNames", character: "甘宁", all: ["奇袭", "奋威"] },
// skills: ["reward_skill_a", "reward_skill_b"],
// text: "任意名称包含“甘宁”的武将取得【奇袭】和【奋威】后获得两个奖励技能",
// }],
// },
// // characters 可填写多个名称关键词;any 表示取得列出的任意一个技能即可触发。
// translatedCharacterAnySkill: {
// id: "translated_character_any_skill_example",
// name: "多武将关键词任选技能模板",
// levels: [{
// level: 1,
// condition: { type: "characterSkillNames", characters: ["甘宁", "吕蒙"], any: ["奇袭", "克己"] },
// skill: "reward_skill",
// text: "武将名称包含“甘宁”或“吕蒙”,并取得【奇袭】或【克己】后获得奖励技能",
// }],
// },
// // 必须同时选择全部指定技能;可一次发放多个羁绊奖励技能。
// allSkills: {
// id: "all_skills_example",
// name: "诸葛亮羁绊模板",
// levels: [{
// level: 1,
// condition: { type: "skills", all: ["guanxing", "kongcheng"] },
// skills: ["reward_skill_a", "reward_skill_b"],
// text: "同时拥有过关选择的【观星】和【空城】时获得两个奖励技能",
// }],
// },
// // 任意选择一个指定技能即可满足,适合原版/界限突破等同类技能。
// anySkill: {
// id: "any_skill_example",
// name: "任一技能模板",
// levels: [{
// level: 1,
// condition: { type: "skills", any: ["guanxing", "reguanxing"] },
// skills: ["reward_skill"],
// text: "选择【观星】或【界观星】中的任意一个时获得奖励技能",
// }],
// },
// // all/any 可以嵌套,适合多个技能组共同组成羁绊。
// nestedCombination: {
// id: "nested_combination_example",
// name: "嵌套组合模板",
// levels: [{
// level: 1,
// condition: {
// all: [
// { any: [{ type: "skill", skill: "guanxing" }, { type: "skill", skill: "reguanxing" }] },
// { type: "skill", skill: "kongcheng" },
// { type: "keyword", keyword: "锦囊", count: 2 },
// ],
// },
// skills: ["reward_skill"],
// text: "满足任一观星、空城和两个【锦囊】关键词技能时获得奖励",
// }],
// },
// // not 用于排除指定技能;需要与其他条件组合时将其放进 all 数组。
// excludedSkill: {
// id: "excluded_skill_example",
// name: "排除条件模板",
// levels: [{
// level: 1,
// condition: {
// all: [
// { type: "keyword", keyword: "桃", count: 2 },
// { not: { type: "skill", skill: "forbidden_skill" } },
// ],
// },
// skills: ["reward_skill"],
// text: "有两个【桃】关键词技能且未选择排除技能时获得奖励",
// }],
// },
// };
export default tianshuConfig;
+384 -60
View File
@@ -1,15 +1,15 @@
import tianshuConfig from "./config.js";
import bondSkillPack from "./bondSkills.js";
import { lib, game, ui, get, ai, _status } from "../../../noname.js";
export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
if (!lib.config.extension_术樱包_tianShuOff) return;
if (!lib.config.extension_术樱包_tianShu_Off) return;
let tianshu = new Object();
const state = {
difficulty: "normal",
stageIndex: 0,
selectedSkills: {},
selectedSkillSet: {},
difficultyBossMap: {},
originalBossMap: {},
@@ -20,6 +20,7 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
roundFilterInstalled: false,
originalGameCheck: null,
checkHookInstalled: false,
bondHooksInstalled: false,
corpseChooseCleanup: null,
corpseChooseEvent: null,
initialBoss: null,
@@ -27,6 +28,41 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
running: false,
pendingSpawn: false,
clearingStage: false,
bondOverviewButton: null,
bondOverviewDialog: null,
};
const rewardSkillSource = "shuYing_tianshu_reward";
const selectedSkillStorage = "shuYing_tianshu_selectedSkills";
const bondSkillSourcePrefix = "shuYing_tianshu_bond_";
const bondSyncingPlayers = new WeakSet();
const bondQueuedPlayers = new WeakSet();
// 读取玩家当前仍持有的天书过关选择技能;羁绊只能使用此列表判定。
const getTianshuRewardSkills = player => {
const skills = player?.additionalSkills?.[rewardSkillSource];
if (Array.isArray(skills)) return skills.filter(skill => typeof skill == "string" && skill);
return typeof skills == "string" && skills ? [skills] : [];
};
// 读取天书过关选择历史;技能永久失去后仍保留,用于识别是否需要刷新羁绊。
const getTianshuRewardSkillHistory = player => {
const skills = player?.storage?.[selectedSkillStorage];
return Array.isArray(skills) ? skills.filter(skill => typeof skill == "string" && skill) : [];
};
// 捕获一次过关技能选择,同时登记实际技能来源和不可见的选择历史。
const captureTianshuRewardSkill = async (player, skill) => {
if (!player || typeof skill != "string" || !skill || !lib.skill[skill]) return false;
if (!getTianshuRewardSkills(player).includes(skill)) {
await player.addAdditionalSkills(rewardSkillSource, skill, true);
}
const history = getTianshuRewardSkillHistory(player);
if (!history.includes(skill)) {
history.push(skill);
player.storage[selectedSkillStorage] = history;
game.broadcast((current, key, value) => current.storage[key] = value, player, selectedSkillStorage, history);
player.syncStorage?.(selectedSkillStorage);
}
return true;
};
// 生成玩家的稳定标识,用于跨死亡/复活状态追踪玩家方成员。
@@ -223,9 +259,20 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
// 开局弹出难度选择,并把难度写入运行状态与全局状态。
const chooseDifficulty = async (player = game.me) => {
const keys = Object.keys(tianshuConfig.difficulties);
const controls = keys.map(key => tianshuConfig.difficulties[key].name);
const result = await player.chooseControl(controls).set("prompt", "请选择天书乱斗难度").set("ai", () => 0).forResult();
state.difficulty = keys[Math.max(0, controls.indexOf(result.control))] || "normal";
const canChoose = player == game.me || player?.isUnderControl?.();
if (canChoose && typeof shuYing.chooseSingleOptionDialog == "function") {
state.difficulty = await shuYing.chooseSingleOptionDialog({
title: "请选择天书乱斗难度",
intro: "难度将改变Boss的起始手牌、体力上限与追加技能,本局确定后不可更改。",
defaultKey: "normal",
options: keys.map(key => ({ key, tone: key, ...tianshuConfig.difficulties[key] })),
}) || "normal";
}
else {
const controls = keys.map(key => tianshuConfig.difficulties[key].name);
const result = await player.chooseControl(controls).set("prompt", "请选择天书乱斗难度").set("ai", () => 0).forResult();
state.difficulty = keys[Math.max(0, controls.indexOf(result.control))] || "normal";
}
_status[tianshuConfig.settings.difficultyStatusKey] = state.difficulty;
};
// 在指定座位创建当前难度的动态 Boss;初始主 Boss 已静默死亡,所以关卡 Boss 全部使用忠臣身份。
@@ -528,11 +575,23 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
}
return skills;
};
// 从全武将技能中构建可供指定玩家过关选择的技能池。
// 检查技能能否进入指定玩家的过关奖励池,并统一处理去重与规则过滤。
const canAddRewardSkill = (skill, skills, ownCharacterSkills) => {
if (typeof skill != "string" || !skill || skills.includes(skill)) return false;
const banned = tianshuConfig.skillPool.banned || [];
if (banned.includes(skill) || state.selectedSkillSet[skill] || ownCharacterSkills.includes(skill)) return false;
const info = get.info(skill);
if (!info || info.charlotte || info.equipSkill || info.cardSkill || info.ruleSkill || info.temp || info.sub || info.silent) return false;
if (info.unique || info.juexingji || info.limited || info.zhuSkill || info.hiddenSkill || info.dutySkill || info.boss || info.superCharlotte) return false;
return !skill.includes("_boss") && !skill.startsWith("boss_") && !skill.startsWith("_") && !skill.includes("_sub");
};
// 每次过关同步合并 config 额外奖励池和当前已加载的全武将技能池。
const getAllSkillPool = player => {
const skills = [];
const banned = tianshuConfig.skillPool.banned || [];
const ownCharacterSkills = getCharacterCardSkills(player);
for (const skill of tianshuConfig.skillPool.additional || []) {
if (canAddRewardSkill(skill, skills, ownCharacterSkills)) skills.push(skill);
}
for (const name in lib.character) {
const character = lib.character[name];
if (!character || lib.filter.characterDisabled(name) || name.indexOf("boss_") == 0) continue;
@@ -546,12 +605,7 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
else if (Array.isArray(info.derivation)) list.addArray(info.derivation);
}
for (const skill of list) {
if (skills.includes(skill) || banned.includes(skill) || state.selectedSkillSet[skill] || ownCharacterSkills.includes(skill)) continue;
const info = get.info(skill);
if (!info || info.charlotte || info.equipSkill || info.cardSkill || info.ruleSkill || info.temp || info.sub || info.silent) continue;
if (info.unique || info.juexingji || info.limited || info.zhuSkill || info.hiddenSkill || info.dutySkill || info.boss || info.superCharlotte) continue;
if (skill.includes("_boss") || skill.startsWith("boss_") || skill.startsWith("_") || skill.includes("_sub")) continue;
skills.push(skill);
if (canAddRewardSkill(skill, skills, ownCharacterSkills)) skills.push(skill);
}
}
return skills;
@@ -565,47 +619,303 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
if (lib.config.extension_术樱包_giveAiSkill && main && player != main && (underControl || tianshuSingleControl)) chooser = main;
return chooser;
};
// 让指定玩家从 UI 中选择一个奖励技能。
const chooseRewardSkill = async (player, choices) => {
// 读取指定 additionalSkills 来源当前持有的技能。
const getAdditionalSourceSkills = (player, source) => {
const skills = player?.additionalSkills?.[source];
if (Array.isArray(skills)) return skills.filter(skill => typeof skill == "string" && skill);
return typeof skills == "string" && skills ? [skills] : [];
};
// 羁绊只读取玩家当前仍持有的过关选择技能,不读取武将牌或其他来源技能。
const getBondConditionSkills = player => getTianshuRewardSkills(player);
// 将过关技能按译名归一化;同译名的不同技能 ID 只贡献一次羁绊进度。
const getUniqueBondSkillNames = skills => Array.from(new Set(
skills.map(skill => lib.translate[skill]).filter(name => typeof name == "string" && name)
));
// 获取玩家主副将译名,供武将名称关键词条件检索不同前后缀版本。
const getBondCharacterNames = player => 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)
));
// 通用羁绊条件解释器,支持关键词、指定技能以及 all/any/not 组合。
const matchesBondCondition = (player, condition) => {
if (!condition || typeof condition != "object") return false;
if (!condition.type && Array.isArray(condition.all)) return condition.all.every(item => matchesBondCondition(player, item));
if (!condition.type && Array.isArray(condition.any)) return condition.any.some(item => matchesBondCondition(player, item));
if (!condition.type && condition.not) return !matchesBondCondition(player, condition.not);
const owned = getBondConditionSkills(player);
if (condition.type == "keyword") {
if (!condition.keyword) return false;
const marker = `【${condition.keyword}】`;
const matchedNames = new Set();
owned.forEach(skill => {
const info = typeof get.skillInfoTranslation == "function" ? get.skillInfoTranslation(skill, player) : lib.translate[`${skill}_info`];
if (String(info || "").includes(marker)) matchedNames.add(lib.translate[skill] || skill);
});
return matchedNames.size >= Math.max(1, Number(condition.count) || 1);
}
if (condition.type == "skill") return typeof condition.skill == "string" && owned.includes(condition.skill);
if (condition.type == "skillName") {
return typeof condition.name == "string" && owned.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 => owned.includes(skill))) return false;
if (any.length && !any.some(skill => owned.includes(skill))) return false;
return Boolean(all.length || any.length);
}
if (condition.type == "skillNames") {
const names = new Set(getUniqueBondSkillNames(owned));
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.length || !characters.some(keyword => getBondCharacterNames(player).some(name => name.includes(keyword)))) return false;
const names = new Set(getUniqueBondSkillNames(owned));
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;
};
// 兼容当前关键词配置,同时允许以后直接为等级填写任意 condition。
const getBondLevelCondition = (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;
}
if (bond?.keyword) {
return {
type: "keyword",
keyword: bond.keyword,
count: level?.count,
};
}
return null;
};
// 只选取条件成立且等级最高的一档,低等级效果不会与高等级叠加。
const getActiveBondLevel = (player, bond) => {
let active = null;
(bond.levels || []).forEach((level, index) => {
if (!matchesBondCondition(player, getBondLevelCondition(bond, level))) return;
const rank = Number(level.level ?? level.count ?? index + 1) || index + 1;
if (!active || rank > active.rank) active = { config: level, rank };
});
return active;
};
const getBondLevelSkills = level => {
if (Array.isArray(level?.skills)) return level.skills.filter(Boolean);
return level?.skill ? [level.skill] : [];
};
// 打开当前玩家方羁绊概览;只展示名称、等级和该等级的实际效果。
const openBondOverview = () => {
state.bondOverviewDialog?.remove();
const createNode = (tag, className, text, parent) => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined && text !== null) node.textContent = text;
parent?.appendChild(node);
return node;
};
const mask = createNode("div", "shuYing-bond-overview-mask", null, document.body);
const dialog = createNode("section", "shuYing-bond-overview", null, mask);
state.bondOverviewDialog = mask;
dialog.setAttribute("role", "dialog");
dialog.setAttribute("aria-modal", "true");
createNode("h2", "shuYing-bond-overview-title", "已激活羁绊", dialog);
const content = createNode("div", "shuYing-bond-overview-content", null, dialog);
let total = 0;
for (const current of getPlayerSidePlayers({ aliveOnly: false, includeVirtual: false })) {
const levels = current.storage?.shuYing_tianshu_bondLevels || {};
const activeBonds = (tianshuConfig.bonds || []).map(bond => {
const rank = Number(levels[bond.id]) || 0;
const level = (bond.levels || []).find((item, index) => (Number(item.level ?? item.count ?? index + 1) || index + 1) == rank);
return rank && level ? { bond, level, rank } : null;
}).filter(Boolean);
if (!activeBonds.length) continue;
total += activeBonds.length;
const group = createNode("article", "shuYing-bond-overview-player", null, content);
createNode("h3", "shuYing-bond-overview-player-name", get.translation(current), group);
activeBonds.forEach(({ bond, level, rank }) => {
const row = createNode("div", "shuYing-bond-overview-item", null, group);
const head = createNode("div", "shuYing-bond-overview-item-head", null, row);
createNode("span", "shuYing-bond-overview-name", bond.name || "未命名羁绊", head);
createNode("span", "shuYing-bond-overview-level", `第 ${rank} 级`, head);
createNode("div", "shuYing-bond-overview-effect", level.text || "该等级暂未配置效果说明", row);
});
}
if (!total) createNode("div", "shuYing-bond-overview-empty", "当前玩家方尚未激活羁绊", content);
const close = createNode("button", "shuYing-bond-overview-close", "关闭", dialog);
close.type = "button";
const finish = () => {
mask.remove();
if (state.bondOverviewDialog == mask) state.bondOverviewDialog = null;
};
close.addEventListener("click", finish);
mask.addEventListener("click", event => {
if (event.target == mask) finish();
});
close.focus({ preventScroll: true });
};
// 创建独立于具体 UI 扩展的羁绊入口按钮。
const installBondOverviewButton = () => {
state.bondOverviewButton?.remove();
const button = document.createElement("button");
button.className = "shuYing-bond-overview-button";
button.type = "button";
button.textContent = "羁绊";
button.title = "查看玩家方当前激活的羁绊效果";
button.addEventListener("click", openBondOverview);
document.body.appendChild(button);
state.bondOverviewButton = button;
lib.onover.push(() => {
button.remove();
state.bondOverviewDialog?.remove();
if (state.bondOverviewButton == button) state.bondOverviewButton = null;
state.bondOverviewDialog = null;
});
};
const sameSkillList = (first, second) => first.length == second.length && first.every(skill => second.includes(skill));
// 将一个羁绊来源精确同步为目标技能,保留武将牌或其他来源持有的同名技能。
const syncAdditionalSkillSource = (player, source, desiredSkills) => {
const desired = Array.from(new Set(desiredSkills)).filter(skill => {
if (lib.skill[skill]) return true;
console.warn(`[天书羁绊] 未找到奖励技能:${skill}`);
return false;
});
const current = getAdditionalSourceSkills(player, source);
if (sameSkillList(current, desired)) return false;
const removable = new Set(player.getRemovableAdditionalSkills(source));
for (const skill of current.filter(skill => !desired.includes(skill))) {
if (removable.has(skill)) player.removeSkill(skill);
else player.$removeAdditionalSkills(source, skill);
}
for (const skill of desired.filter(skill => !current.includes(skill))) {
if (!player.hasSkill(skill, true, false, false)) player.addSkill(skill, null, true, true);
}
if (desired.length) player.additionalSkills[source] = desired.slice();
else delete player.additionalSkills[source];
game.broadcast((current, map) => current.additionalSkills = map, player, player.additionalSkills);
player.checkConflict();
_status.event?.clearStepCache?.();
return true;
};
// 依据当前真实技能所有权重算一名玩家的全部羁绊来源和隐藏等级缓存。
const refreshTianshuBonds = player => {
if (!state.running || !player || !isCapturedPlayerSide(player) || isVirtualIdol(player) || bondSyncingPlayers.has(player)) return;
bondSyncingPlayers.add(player);
try {
delete player.storage.shuYing_tianshu_keywordBondLevels;
const levelCache = {};
const activeSources = new Set();
for (const bond of tianshuConfig.bonds || []) {
if (!bond?.id) continue;
const source = `${bondSkillSourcePrefix}${bond.id}`;
const active = getActiveBondLevel(player, bond);
const skills = active ? getBondLevelSkills(active.config) : [];
activeSources.add(source);
syncAdditionalSkillSource(player, source, skills);
if (active) levelCache[bond.id] = active.rank;
}
for (const source of Object.keys(player.additionalSkills || {})) {
if (source.startsWith(bondSkillSourcePrefix) && !activeSources.has(source)) syncAdditionalSkillSource(player, source, []);
}
if (Object.keys(levelCache).length) player.storage.shuYing_tianshu_bondLevels = levelCache;
else delete player.storage.shuYing_tianshu_bondLevels;
}
finally {
bondSyncingPlayers.delete(player);
}
};
// 合并同一调用栈内的技能变化,确保 additionalSkills 映射更新完成后再重算。
const queueTianshuBondRefresh = player => {
if (!state.running || !player || bondSyncingPlayers.has(player) || bondQueuedPlayers.has(player)) return;
bondQueuedPlayers.add(player);
Promise.resolve().then(() => {
bondQueuedPlayers.delete(player);
refreshTianshuBonds(player);
});
};
// 兜底监听直接 removeSkill,但只响应曾被捕获的天书过关选择技能。
const installTianshuBondHooks = () => {
if (state.bondHooksInstalled) return;
state.bondHooksInstalled = true;
const hook = (skill, player) => {
if (getTianshuRewardSkillHistory(player).includes(skill)) queueTianshuBondRefresh(player);
};
(lib.hooks.removeSkillCheck ||= []).push(hook);
};
// 从当前奖励池随机生成候选;刷新时优先排除上一组技能。
const createRewardSkillChoices = (player, excluded = []) => {
const excludedSet = new Set(excluded);
const pool = getAllSkillPool(player).filter(skill => !excludedSet.has(skill));
const choices = [];
while (choices.length < tianshuConfig.reward.skillChoiceCount && pool.length) {
const choice = pool.randomRemove();
if (choice) choices.push(choice);
}
return choices;
};
// 让指定玩家从 UI 中选择一个奖励技能,并返回刷新请求或最终技能。
const chooseRewardSkill = async (player, choices, canRefresh) => {
const chooser = getRewardSkillChooser(player);
const controls = choices.map(skill => get.translation(skill));
const result = await chooser.chooseControl(controls)
.set("prompt", `<span style="color:#f6d365">${get.translation(player)}</span> 请选择获得一个技能`)
.set("choiceList", choices.map(skill => `<div class="skill" style="white-space:normal;word-break:break-all;line-height:22px;">【${get.translation(skill)}】</div><div class="popup text" style="width:calc(100% - 10px);display:inline-block;white-space:normal;word-break:break-all;">${lib.translate[`${skill}_info`] || "暂无技能描述"}</div>`))
.set("displayIndex", false)
.set("ai", () => controls[0])
.forResult();
const index = controls.indexOf(result.control);
return choices[index] || null;
const canChoose = chooser == game.me || chooser?.isUnderControl?.();
if (!canChoose || typeof shuYing.chooseSkillRewardDialog != "function") return choices[0] || null;
const playerName = get.translation(player);
return shuYing.chooseSkillRewardDialog({
title: `第${state.stageIndex + 1}关 ${playerName}选择技能`,
highlightText: playerName,
choices,
selectedSkills: getBondConditionSkills(player),
bonds: tianshuConfig.bonds || [],
player,
canRefresh,
});
};
// 过关后让每个存活玩家方角色依次选择奖励技能。
const chooseSkillsForStageClear = async () => {
for (const player of getPlayerSidePlayers({ aliveOnly: true, includeVirtual: false })) {
const pool = getAllSkillPool(player);
const choices = [];
while (choices.length < tianshuConfig.reward.skillChoiceCount && pool.length) {
const choice = pool.randomRemove();
if (choice) choices.push(choice);
}
let choices = createRewardSkillChoices(player);
if (!choices.length) continue;
const skill = await chooseRewardSkill(player, choices);
let refreshed = false;
let skill = await chooseRewardSkill(player, choices, true);
if (skill?.refresh) {
refreshed = true;
const oldChoices = choices;
const handCards = player.getCards("h");
const discardCount = Math.ceil(handCards.length / 2);
if (discardCount) await player.discard(handCards.randomGets(discardCount));
choices = createRewardSkillChoices(player, oldChoices);
if (!choices.length) choices = createRewardSkillChoices(player);
if (!choices.length) continue;
skill = await chooseRewardSkill(player, choices, !refreshed);
}
if (!skill || !choices.includes(skill)) continue;
await player.addSkill(skill);
const id = getPlayerKey(player);
state.selectedSkills[id] ??= [];
state.selectedSkills[id].push(skill);
if (!await captureTianshuRewardSkill(player, skill)) continue;
state.selectedSkillSet[skill] = true;
player.storage.shuYing_tianshu_selectedSkills = state.selectedSkills[id].slice();
game.log(player, "获得了技能", `#g【${get.translation(skill)}】`);
refreshTianshuBonds(player);
}
};
// 获取 1 号位上家的最终 Boss,用于让 Boss 回合结束后立刻回到 1 号位玩家。
const getFinalBossBeforeSeatOne = () => {
const firstPlayer = getPlayerSeatOne();
if (!firstPlayer) return game.boss || null;
const target = firstPlayer.previous || game.boss || null;
return target;
};
// 完成关卡清算:发奖励、选技能、进入下一关或结束游戏。
const clearStage = async () => {
if (state.clearingStage) return;
@@ -641,17 +951,8 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
const currentTurnPlayer = getParentEvent("phase")?.player || _status.currentPhase;
state.stageIndex++;
await spawnStage(state.stageIndex);
let nextTurnTarget = null;
if (currentTurnPlayer && isCapturedPlayerSide(currentTurnPlayer)) {
if (isVirtualIdol(currentTurnPlayer)) {
nextTurnTarget = getFinalBossBeforeSeatOne();
}
else {
const next = currentTurnPlayer.next;
nextTurnTarget = next && !next.isDead() && isCapturedPlayerSide(next) && !isVirtualIdol(next) ? next : getFinalBossBeforeSeatOne();
}
}
if (nextTurnTarget) finishCurrentPhaseTo(nextTurnTarget);
// 过关后立即结束当前回合,并将新关的首个行动权交给当前角色的实际下家。
if (currentTurnPlayer?.next) finishCurrentPhaseTo(currentTurnPlayer.next);
};
// 处理 Boss 死亡事件:击杀奖励、隐藏尸体、判断是否清关。
const handleBossDie = async event => {
@@ -676,6 +977,8 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
installHiddenBossCorpseCheckHook();
spawnVirtualIdol();
capturePlayerSide();
installTianshuBondHooks();
installBondOverviewButton();
state.pendingSpawn = true;
await chooseDifficulty(player);
prepareDifficultyBossCharacters();
@@ -716,6 +1019,8 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
checkPlayerDefeat() {
if (this.isPlayerSideDefeated()) game.over(game.me === game.boss);
},
refreshBonds: refreshTianshuBonds,
queueBondRefresh: queueTianshuBondRefresh,
getPlayerSeatOne,
getVirtualIdolSeat,
};
@@ -753,6 +1058,7 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
},
};
tianshu.skill = {
...bondSkillPack.skill,
shuYing_Skill_Tianshu_Go:
{
mode: ["boss"],
@@ -768,6 +1074,25 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
game.addGlobalSkill("shuYing_Tianshu_NewSpawnStage");
game.addGlobalSkill("shuYing_Tianshu_NewBossDie");
game.addGlobalSkill("shuYing_Tianshu_NewPlayerDie");
game.addGlobalSkill("shuYing_Tianshu_BondManager");
},
},
shuYing_Tianshu_BondManager: {
mode: ["boss"],
trigger: { global: "changeSkillsAfter" },
forced: true,
silent: true,
popup: false,
charlotte: true,
firstDo: true,
filter(event) {
if (!state.running || !event.player || !isCapturedPlayerSide(event.player) || isVirtualIdol(event.player)) return false;
const removed = Array.isArray(event.removeSkill) ? event.removeSkill : [];
const history = getTianshuRewardSkillHistory(event.player);
return removed.some(skill => history.includes(skill));
},
content(event, trigger) {
refreshTianshuBonds(trigger.player);
},
},
shuYing_Tianshu_NewNoStartCards: {
@@ -831,16 +1156,14 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
},
shuYing_Tianshu_Protection: {
mode: ["boss"],
trigger: { player: "damageBefore" },
trigger: { player: "damageBegin4" },
forced: true,
priority: 100,
priority: -1000,
charlotte: true,
popup: false,
// 仅玩家方/虚拟偶像可触发一次性保护。
filter(event, player) {
return Boolean(player && player.isIn() && !isCurrentStageBoss(player) && (isCapturedPlayerSide(player) || isVirtualIdol(player)));
return Boolean(event.num > 0 && player && player.isIn() && !isCurrentStageBoss(player) && (isCapturedPlayerSide(player) || isVirtualIdol(player)));
},
// 防止本次伤害并移除保护技能。
async content(event, trigger, player) {
trigger.cancel();
player.removeSkill("shuYing_Tianshu_Protection");
@@ -867,6 +1190,7 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
shuYing_Skill_Tianshu_intro5_info: "共四关。击杀Boss获得奖励;过关后玩家方回复、摸牌并选择技能。",
shuYing_Tianshu_Protection: "保护",
shuYing_Tianshu_Protection_info: "锁定技。防止你受到的一次伤害,然后移除此技能。",
shuYing_Tianshu_BondManager: "天书羁绊",
};
tianshu.perfectPair = {};
tianshu.characterTitle = {};
+204
View File
@@ -0,0 +1,204 @@
from __future__ import annotations
import argparse
import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path
EXCLUDE_DIRS = {".git", ".update_tmp", "dist", "tools", "__pycache__"}
EXCLUDE_FILES = {".gitignore", "log.txt"}
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def should_include(path: Path, root: Path) -> bool:
relative = path.relative_to(root)
return (
path.is_file()
and relative.name not in EXCLUDE_FILES
and not any(part in EXCLUDE_DIRS for part in relative.parts)
)
def scan_files(root: Path) -> list[Path]:
return sorted(
(path for path in root.rglob("*") if should_include(path, root)),
key=lambda path: str(path.relative_to(root)).replace("\\", "/"),
)
def normalize_path(path: str) -> str:
return path.replace("\\", "/").strip("/")
def validate_relative_paths(paths: list[str], label: str) -> None:
unsafe = [
path for path in paths
if not path
or any(part in {"", ".", ".."} for part in path.split("/"))
or any(":" in part for part in path.split("/"))
]
if unsafe:
raise ValueError(f"{label} contains unsafe paths: {', '.join(unsafe)}")
def load_config(root: Path, config_path: str) -> dict:
path = Path(config_path)
if not path.is_absolute():
path = root / path
if not path.exists():
return {
"core": [],
"remove": [],
"removeDirectories": [],
}
with path.open("r", encoding="utf-8") as file:
config = json.load(file)
return {
"core": [normalize_path(item) for item in config.get("core", [])],
"remove": [normalize_path(item) for item in config.get("remove", [])],
"removeDirectories": [normalize_path(item) for item in config.get("removeDirectories", [])],
}
def preserve_removals(config: dict, output: Path) -> dict:
"""合并现有清单中的删除项,避免重新生成时覆盖人工维护的内容。"""
if not output.is_file():
return config
with output.open("r", encoding="utf-8") as file:
previous = json.load(file)
for key in ("remove", "removeDirectories"):
existing = [normalize_path(item) for item in previous.get(key, [])]
config[key] = list(dict.fromkeys([*existing, *config[key]]))
return config
def validate_config_files(root: Path, files: list[str], label: str) -> None:
missing = [file for file in files if not (root / file).is_file()]
if missing:
raise FileNotFoundError(f"{label} contains missing files: {', '.join(missing)}")
def set_nested_file(tree: dict, relative_path: str, info: dict) -> None:
parts = relative_path.split("/")
node = tree
for folder in parts[:-1]:
node = node.setdefault(folder, {
"type": "directory",
"children": {},
})["children"]
node[parts[-1]] = {
"type": "file",
**info,
}
def build_manifest(root: Path, version: str, config: dict) -> dict:
flat_files = {}
tree = {
"type": "directory",
"name": root.name,
"children": {},
}
core_files = config["core"]
core_file_set = set(core_files)
remove_files = config["remove"]
remove_directories = config["removeDirectories"]
validate_config_files(root, core_files, "core")
validate_relative_paths(remove_files, "remove")
validate_relative_paths(remove_directories, "removeDirectories")
for path in scan_files(root):
relative_path = str(path.relative_to(root)).replace("\\", "/")
info = {
"size": path.stat().st_size,
"sha256": sha256_file(path),
}
flat_files[relative_path] = info
set_nested_file(tree["children"], relative_path, info)
remove_file_conflicts = [file for file in remove_files if file in flat_files]
if remove_file_conflicts:
raise ValueError(f"remove still contains current files: {', '.join(remove_file_conflicts)}")
remove_directory_conflicts = [
directory for directory in remove_directories
if any(file == directory or file.startswith(f"{directory}/") for file in flat_files)
]
if remove_directory_conflicts:
raise ValueError(
"removeDirectories still contain current files: "
+ ", ".join(remove_directory_conflicts)
)
folder_digest = hashlib.sha256()
for relative_path in sorted(flat_files):
info = flat_files[relative_path]
folder_digest.update(relative_path.encode("utf-8"))
folder_digest.update(str(info["size"]).encode("ascii"))
folder_digest.update(info["sha256"].encode("ascii"))
return {
"version": version,
"generated_at": datetime.now(timezone.utc).isoformat(),
"algorithm": "sha256",
"file_count": len(flat_files),
"folder_sha256": folder_digest.hexdigest(),
"core": core_files,
"assets": [file for file in sorted(flat_files) if file not in core_file_set],
"remove": remove_files,
"removeDirectories": remove_directories,
"files": flat_files,
"tree": tree,
}
def main() -> None:
parser = argparse.ArgumentParser(description="Generate shuying manifest.json")
parser.add_argument("--root", default=".", help="extension root")
parser.add_argument("--version", help="version, for example 2.0.0.4")
parser.add_argument("--out", default="dist/manifest.json", help="output json path")
parser.add_argument("--config", default="tools/manifest_config.json", help="manifest config path")
args = parser.parse_args()
root = Path(args.root).resolve()
version = args.version or input("Input version, for example 2.0.0.4: ").strip()
if not version:
raise ValueError("Version cannot be empty")
output = (root / args.out).resolve()
config = preserve_removals(load_config(root, args.config), output)
manifest = build_manifest(root, version, config)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"version: {version}")
print(f"file_count: {manifest['file_count']}")
print(f"core_count: {len(manifest['core'])}")
print(f"asset_count: {len(manifest['assets'])}")
print(f"remove_count: {len(manifest['remove'])}")
print(f"remove_directory_count: {len(manifest['removeDirectories'])}")
print(f"folder_sha256: {manifest['folder_sha256']}")
print(f"generated: {output}")
if __name__ == "__main__":
main()
+21
View File
@@ -0,0 +1,21 @@
{
"core": [
"extension.js",
"menu.js",
"function.js",
"update.js",
"mengsan/mode.js",
"mengsan/config.js",
"mengsan/state.js",
"mengsan/style.css",
"tianshu/extension.js",
"tianshu/config.js",
"pve/boss/index.js",
"pve/boss/character.js",
"pve/boss/skill.js",
"pve/boss/dynamicTranslate.js",
"pve/boss/translate.js"
],
"remove": [],
"removeDirectories": []
}
+21
View File
@@ -1,3 +1,24 @@
"-----< 2.0.1.0 改动 >-----",
"重构PVE武将包目录,按照不同活动BOSS进行独立分包和统一加载",
"新增独立PVP武将包结构,并加入术樱左慈及其技能、图片和配音资源",
"天书乱斗新增动态难度BOSS,不同难度将使用准确的体力上限和追加技能",
"天书乱斗新增羁绊系统,根据玩家过关选择的技能激活并替换不同等级效果",
"羁绊条件支持技能ID、技能译名、描述关键词以及武将名称关键词组合",
"新增羁绊谱册和局内已激活羁绊查看功能",
"重铸过关技能选择界面,新增六个技能选项、确认按钮和单次刷新功能",
"新增可配置的额外技能奖励池,过关时与武将技能池同步检索",
"新增天书过关技能选择记录,羁绊仅统计玩家通过过关奖励取得且当前仍持有的技能",
"统一活动BOSS武将包入口,并补充十二生肖、青青子衿和捉鬼驱邪相关内容及配音资源",
"自定义界面现已兼容十周年UI、原生UI以及移动端显示",
"-----< 2.0.1.0 修复 >-----",
"修复天书动态BOSS体力、技能、武将复制和玩家变身识别异常的问题",
"修复天书武将包被普通武将列表识别或无法被挑战模式加载的问题",
"修复PVE分包后技能台词无法正确识别的问题",
"修复羁绊中多个同译名技能会被重复计算的问题",
"修复羁绊谱册和局内羁绊列表在部分分辨率下重叠或标题越界的问题",
"修复过关后行动顺序异常,当前角色回合结束后将行动权交给其下家",
"调整天书开局保护的伤害结算顺序,优先结算其他免伤和减伤效果",
"修复生成更新清单时会覆盖原有remove和removeDirectories内容的问题",
"-----< 2.0.0.9 改动 >-----",
"新增可以自定义虚拟偶像随机助战池功能",
"现在过关奖励提供的技能池会过滤武将牌上的技能",