diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index a3589ed1..c32fb5d2 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -32,6 +32,7 @@ jobs: target: 'desktop' arch: 'win64_mingw' modules: 'qtmultimedia qt5compat qtshadertools' + tools: 'tools_opensslv3_x64' - name: Disable PCH shell: bash @@ -46,6 +47,9 @@ jobs: working-directory: ${{github.workspace}} env: CMAKE_PREFIX_PATH: ${{env.Qt6_Dir}} + OPENSSL_ROOT_DIR: ${{env.Qt6_Dir}}/../../Tools/OpenSSLv3/Win_x64 + OPENSSL_INCLUDE_DIR: ${{env.OPENSSL_ROOT_DIR}}/include + OPENSSL_CRYPTO_LIBRARY: ${{env.OPENSSL_ROOT_DIR}}/bin/libcrypto-3-x64.dll run: | cmake -DCMAKE_BUILD_TYPE=MinSizeRel -G "MinGW Makefiles" -B ${{github.workspace}}/build @@ -74,7 +78,7 @@ jobs: cp build/zh_CN.qm FreeKill-release cp build/en_US.qm FreeKill-release cp ../Qt/6.5.3/mingw_64/bin/li*.dll FreeKill-release - cp '/c/Program Files/OpenSSL/bin/libcrypto-1_1-x64.dll' FreeKill-release + cp ../Qt/Tools/OpenSSLv3/Win_x64/bin/libcrypto-3-x64.dll FreeKill-release 7z a -t7z FreeKill-release.7z FreeKill-release -r -mx=9 -m0=LZMA2 -ms=10m -mf=on -mhc=on -mmt=on - name: Upload Release diff --git a/CHANGELOG.md b/CHANGELOG.md index 1295912c..4a18f2f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,81 @@ # ChangeLog +## v0.4.8 & v0.4.9 + +- Qml: 新增leval函数可获得lua表达式的值 +- 新增AbstractRoom类 去除冗余 +- 修gameOver相关bug(或许) +- 从Utility那里搬运了askForYiji和doYiji两个函数,负责分配 + 虽然暂时没实现单烧条,但先这么用着 +- 修复了askForCardAndPlayers的选择中可以选择复数张牌的bug +- 为prohibitDiscard添加了输入id选项 +- 正式添加对多后缀标记的支持 +- 添加了一点注释 +- 搬运了moveCardIntoEquip和canMoveCardIntoEquip +- 为选牌的默认prompt添加了目标 +- 完善了朱雀羽扇的判定 +- 修复了抽选武将牌堆时未删除已选武将的bug +- 修复了maxCard标记不识别“-turn”以外标记的bug +- 修复了obtaincard实际不能接受id数组的bug +- CardItem一律可长按,除了卡牌一览 +- Qml Mark在QML中可获得主人的id +- Qml Mark可实现某某视角完全不可见 +- 隐藏#开头的pile +- 可自定义interaction了 +- LogMessage新增toast成员 +- 修复投降杀人bug + +___ + +## v0.4.6 & v0.4.7 + +- 攻击范围状态技类新增基础值修正函数 +- 伤害值在一个技能处理后小于1会终止当前事件 +- 不向不能使用【无懈可击】的角色询问使用【无懈可击】 +- 修正在濒死插结中有人死亡后仍然会向该角色求桃的情况 +- 将PreCardUse和PreCardRespond时机移至实体牌移动之前 +- 调整改判函数原判定牌置入弃牌堆的原因 +- 修正【朱雀羽扇】、【借刀杀人】、【酒】 +- 为使用流程和Aim流程增加属性additionalEffect,用于指定额外结算次数(OL版),顺带移动【五谷丰登】开启和关 +闭AG的位置; +- 为视为技新增after_use方法处理转化牌后的后续操作; +- 修复伤害流程时机触发者不变问题; +- 修复旁观休整的问题; +- 修复可移动场上牌判断函数未判断虚拟牌名的问题。 +- 修复传入数组的extraPile无法收回 +- 被弃置牌的log添加操作者 +- beforeMaxHpChanged的num可以被修改 +- 额外回合增加skillName +- 修复亮将技能和禁止亮将 +- 水一些注释和格式 +- git报错优化 +- 防止反复shutdown同一事件 +- 将Utility如canUseCardTo的一些函数搬运到了本体 +- 为技能添加hooked_piles属性,当失去技能时自动弃置hooked_piles内的所有私人牌堆 +- 修复了添加技能没写source_skill的bug +- 修复了ActiveSkill的interaction不传入Skill本身而是metatable的bug +- 修复了主动询问canUse时没有传入extra_data的bug +- 修复了多选时按钮选项变回空白的bug +- 修复了判定阶段被中途拿走判定牌后报错的bug + +___ + +## v0.4.4 & 0.4.5 + +禁将增强;修复bug + +UsableSkill的expand_pile功能加强 + +___ + +## v0.4.3 + +1. 事件栈和实际的函数调用栈分离 +2. 2v2选将专用的MiniGame +3. 各种小修小补 + +___ + ## v0.4.2 && v0.4.1 1. 修复和完善qml mark diff --git a/CMakeLists.txt b/CMakeLists.txt index 84812c68..dbe3a5c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later +# ------------------------------------------------------------ +# 此为新月杀的项目组织文件,采用CMake+QT +# 2022-01-24 新建文件夹 2023-02-21 发布v0.0.1版本 +# ------------------------------------------------------------ cmake_minimum_required(VERSION 3.16) -project(FreeKill VERSION 0.4.2) +project(FreeKill VERSION 0.4.9) add_definitions(-DFK_VERSION=\"${CMAKE_PROJECT_VERSION}\") find_package(Qt6 REQUIRED COMPONENTS diff --git a/Fk/ChatAnim/Egg.qml b/Fk/ChatAnim/Egg.qml index f9ef7260..f5e68da1 100644 --- a/Fk/ChatAnim/Egg.qml +++ b/Fk/ChatAnim/Egg.qml @@ -46,7 +46,8 @@ Item { } ScriptAction { - script: Backend.playSound("./audio/system/fly" + (Math.floor(Math.random() * 2) + 1)); + script: Backend.playSound("./audio/system/fly" + + (Math.floor(Math.random() * 2) + 1)); } ParallelAnimation { @@ -91,7 +92,8 @@ Item { } ScriptAction { - script: Backend.playSound("./audio/system/egg" + (Math.floor(Math.random() * 2) + 1)); + script: Backend.playSound("./audio/system/egg" + + (Math.floor(Math.random() * 2) + 1)); } ParallelAnimation { diff --git a/Fk/ChatAnim/Flower.qml b/Fk/ChatAnim/Flower.qml index 179d744b..738e434e 100644 --- a/Fk/ChatAnim/Flower.qml +++ b/Fk/ChatAnim/Flower.qml @@ -43,7 +43,8 @@ Item { id: pointToAnimation running: false ScriptAction { - script: Backend.playSound("./audio/system/fly" + (Math.floor(Math.random() * 2) + 1)); + script: Backend.playSound("./audio/system/fly" + + (Math.floor(Math.random() * 2) + 1)); } ParallelAnimation { @@ -80,7 +81,8 @@ Item { } ScriptAction { - script: Backend.playSound("./audio/system/flower" + (Math.floor(Math.random() * 2) + 1)); + script: Backend.playSound("./audio/system/flower" + + (Math.floor(Math.random() * 2) + 1)); } ParallelAnimation { diff --git a/Fk/ChatAnim/GiantEgg.qml b/Fk/ChatAnim/GiantEgg.qml index ee784761..26a44869 100644 --- a/Fk/ChatAnim/GiantEgg.qml +++ b/Fk/ChatAnim/GiantEgg.qml @@ -46,7 +46,8 @@ Item { } ScriptAction { - script: Backend.playSound("./audio/system/fly" + (Math.floor(Math.random() * 2) + 1)); + script: Backend.playSound("./audio/system/fly" + + (Math.floor(Math.random() * 2) + 1)); } ParallelAnimation { @@ -91,7 +92,8 @@ Item { } ScriptAction { - script: Backend.playSound("./audio/system/egg" + (Math.floor(Math.random() * 2) + 1)); + script: Backend.playSound("./audio/system/egg" + + (Math.floor(Math.random() * 2) + 1)); } ParallelAnimation { diff --git a/Fk/ChatAnim/Shoe.qml b/Fk/ChatAnim/Shoe.qml index 269d4594..365606cc 100644 --- a/Fk/ChatAnim/Shoe.qml +++ b/Fk/ChatAnim/Shoe.qml @@ -64,7 +64,8 @@ Item { script: { egg.opacity = 0; whip.opacity = 1; - Backend.playSound("./audio/system/egg" + (Math.floor(Math.random() * 2) + 1)); + Backend.playSound("./audio/system/egg" + + (Math.floor(Math.random() * 2) + 1)); } } PropertyAnimation { diff --git a/Fk/ChatAnim/Wine.qml b/Fk/ChatAnim/Wine.qml index e5ebde6e..85b105a1 100644 --- a/Fk/ChatAnim/Wine.qml +++ b/Fk/ChatAnim/Wine.qml @@ -27,7 +27,8 @@ Item { y: start.y - height / 2 + yOffset scale: 0.7 opacity: 0 - rotation: (Math.atan(Math.abs(end.y - start.y) / Math.abs(end.x - start.x)) + rotation: (Math.atan(Math.abs(end.y - start.y) + / Math.abs(end.x - start.x)) / Math.PI * 180 - 90) * (end.x > start.x ? -1 : 1) } diff --git a/Fk/Cheat/CardDetail.qml b/Fk/Cheat/CardDetail.qml index cd069808..5a0eb8c1 100644 --- a/Fk/Cheat/CardDetail.qml +++ b/Fk/Cheat/CardDetail.qml @@ -57,7 +57,7 @@ Flickable { if (!card) return; cardPic.setData(card.toData()); const name = card.virt_name ? card.virt_name : card.name; - screenName.text = Backend.translate(name); - skillDesc.text = Backend.translate(":" + name); + screenName.text = luatr(name); + skillDesc.text = luatr(":" + name); } } diff --git a/Fk/Cheat/FreeAssign.qml b/Fk/Cheat/FreeAssign.qml index 6a610f42..1efcf805 100644 --- a/Fk/Cheat/FreeAssign.qml +++ b/Fk/Cheat/FreeAssign.qml @@ -21,12 +21,12 @@ Item { ToolButton { opacity: stack.depth > 1 ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 100 } } - text: Backend.translate("Back") + text: luatr("Back") onClicked: stack.pop() } Label { - text: Backend.translate("Enable free assign") + text: luatr("Enable free assign") elide: Label.ElideRight horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter @@ -46,12 +46,11 @@ Item { } ToolButton { - text: Backend.translate("Search") + text: luatr("Search") enabled: word.text !== "" onClicked: { if (stack.depth > 1) stack.pop(); - generalModel = JSON.parse(Backend.callLuaFunction("SearchAllGenerals", - [word.text])); + generalModel = lcall("SearchAllGenerals", word.text); stack.push(generalList); word.text = ""; } @@ -88,14 +87,13 @@ Item { height: 40 Text { - text: Backend.translate(name) + text: luatr(name) color: "#E4D5A0" anchors.centerIn: parent } onClicked: { - generalModel = JSON.parse(Backend.callLuaFunction("GetGenerals", - [packages.get(index).name])); + generalModel = lcall("GetGenerals", packages.get(index).name); stack.push(generalList); } } @@ -134,7 +132,7 @@ Item { } function load() { - const packs = JSON.parse(Backend.callLuaFunction("GetAllGeneralPack", [])); + const packs = lcall("GetAllGeneralPack"); packs.forEach((name) => packages.append({ name: name })); } diff --git a/Fk/Cheat/GeneralDetail.qml b/Fk/Cheat/GeneralDetail.qml index 428c3d33..707f7068 100644 --- a/Fk/Cheat/GeneralDetail.qml +++ b/Fk/Cheat/GeneralDetail.qml @@ -39,21 +39,23 @@ Flickable { skillDesc.text = ""; extra_data.generals.forEach((g) => { - const data = JSON.parse(Backend.callLuaFunction("GetGeneralDetail", [g])); - skillDesc.append(Backend.translate(data.kingdom) + " " + Backend.translate(g) + " " + data.hp + "/" + data.maxHp); + const data = lcall("GetGeneralDetail", g); + skillDesc.append(luatr(data.kingdom) + " " + luatr(g) + " " + data.hp + + "/" + data.maxHp); if (data.companions.length > 0){ let ret = ''; - ret += "" + Backend.translate("Companions") + ": "; + ret +="" + luatr("Companions") + ": "; data.companions.forEach(t => { - ret += Backend.translate(t) + ' ' + ret += luatr(t) + ' ' }); skillDesc.append(ret) } data.skill.forEach(t => { - skillDesc.append("" + Backend.translate(t.name) + ": " + t.description) + skillDesc.append("" + luatr(t.name) + ": " + t.description) }); data.related_skill.forEach(t => { - skillDesc.append("" + Backend.translate(t.name) + ": " + t.description + "") + skillDesc.append("" + luatr(t.name) + + ": " + t.description + "") }); skillDesc.append("\n"); }); diff --git a/Fk/Cheat/PlayerDetail.qml b/Fk/Cheat/PlayerDetail.qml index a582a993..38088397 100644 --- a/Fk/Cheat/PlayerDetail.qml +++ b/Fk/Cheat/PlayerDetail.qml @@ -38,7 +38,7 @@ Flickable { RowLayout { MetroButton { - text: Backend.translate("Give Flower") + text: luatr("Give Flower") onClicked: { enabled = false; root.givePresent("Flower"); @@ -47,7 +47,7 @@ Flickable { } MetroButton { - text: Backend.translate("Give Egg") + text: luatr("Give Egg") onClicked: { enabled = false; if (Math.random() < 0.03) { @@ -60,7 +60,7 @@ Flickable { } MetroButton { - text: Backend.translate("Give Wine") + text: luatr("Give Wine") enabled: Math.random() < 0.3 onClicked: { enabled = false; @@ -70,7 +70,7 @@ Flickable { } MetroButton { - text: Backend.translate("Give Shoe") + text: luatr("Give Shoe") enabled: Math.random() < 0.3 onClicked: { enabled = false; @@ -80,7 +80,10 @@ Flickable { } MetroButton { - text: config.blockedUsers.indexOf(screenName.text) === -1 ? Backend.translate("Block Chatter") : Backend.translate("Unblock Chatter") + text: { + const blocked = !config.blockedUsers.includes(screenName.text); + return blocked ? luatr("Block Chatter") : luatr("Unblock Chatter"); + } enabled: pid !== Self.id && pid > 0 onClicked: { const idx = config.blockedUsers.indexOf(screenName.text); @@ -94,7 +97,7 @@ Flickable { } MetroButton { - text: Backend.translate("Kick From Room") + text: luatr("Kick From Room") visible: !roomScene.isStarted && roomScene.isOwner enabled: pid !== Self.id onClicked: { @@ -157,7 +160,7 @@ Flickable { skillDesc.text = ""; const id = extra_data.photo.playerid; - if (id == 0) return; + if (id === 0) return; root.pid = id; screenName.text = extra_data.photo.screenName; @@ -165,36 +168,34 @@ Flickable { deputyChara.name = extra_data.photo.deputyGeneral; if (!config.observing) { - const gamedata = JSON.parse(Backend.callLuaFunction("GetPlayerGameData", [id])); + const gamedata = lcall("GetPlayerGameData", id); const total = gamedata[0]; const win = gamedata[1]; const run = gamedata[2]; const totalTime = gamedata[3]; const winRate = (win / total) * 100; const runRate = (run / total) * 100; - playerGameData.text = total === 0 ? Backend.translate("Newbie") : - Backend.translate("Win=%1 Run=%2 Total=%3").arg(winRate.toFixed(2)) + playerGameData.text = total === 0 ? luatr("Newbie") : + luatr("Win=%1 Run=%2 Total=%3").arg(winRate.toFixed(2)) .arg(runRate.toFixed(2)).arg(total); const h = (totalTime / 3600).toFixed(2); const m = Math.floor(totalTime / 60); if (m < 100) { - playerGameData.text += " " + Backend.translate("TotalGameTime: %1 min").arg(m); + playerGameData.text += " " + luatr("TotalGameTime: %1 min").arg(m); } else { - playerGameData.text += " " + Backend.translate("TotalGameTime: %1 h").arg(h); + playerGameData.text += " " + luatr("TotalGameTime: %1 h").arg(h); } } - const data = JSON.parse(Backend.callLuaFunction("GetPlayerSkills", [id])); - data.forEach(t => { - skillDesc.append("" + Backend.translate(t.name) + ": " + t.description) + lcall("GetPlayerSkills", id).forEach(t => { + skillDesc.append("" + luatr(t.name) + ": " + t.description) }); - const equips = JSON.parse(Backend.callLuaFunction("GetPlayerEquips", [id])); - equips.forEach(cid => { - const t = JSON.parse(Backend.callLuaFunction("GetCardData", [cid])); + lcall("GetPlayerEquips", id).forEach(cid => { + const t = lcall("GetCardData", cid); skillDesc.append("--------------------"); - skillDesc.append("" + Backend.translate(t.name) + ": " + Backend.translate(":" + t.name)); + skillDesc.append("" + luatr(t.name) + ": " + luatr(":" + t.name)); }); } } diff --git a/Fk/Cheat/SameConvert.qml b/Fk/Cheat/SameConvert.qml index 6617ec50..31ccf4ca 100644 --- a/Fk/Cheat/SameConvert.qml +++ b/Fk/Cheat/SameConvert.qml @@ -28,13 +28,13 @@ Item { ColumnLayout { Text { color: "#E4D5A0" - text: Backend.translate(gname) + text: luatr(gname) } GridLayout { columns: 6 Repeater { - model: JSON.parse(Backend.callLuaFunction("GetSameGenerals", [gname])) + model: lcall("GetSameGenerals", gname) GeneralCardItem { name: modelData diff --git a/Fk/Common/Avatar.qml b/Fk/Common/Avatar.qml index 87eebd52..98444e05 100644 --- a/Fk/Common/Avatar.qml +++ b/Fk/Common/Avatar.qml @@ -6,7 +6,8 @@ Image { width: 64 height: 64 - source: SkinBank.getGeneralExtraPic(general, "avatar/") ?? SkinBank.getGeneralPicture(general) + source: SkinBank.getGeneralExtraPic(general, "avatar/") + ?? SkinBank.getGeneralPicture(general) // sourceSize.width: 250 // sourceSize.height: 292 property bool useSmallPic: !!SkinBank.getGeneralExtraPic(general, "avatar/") diff --git a/Fk/Common/AvatarChatBox.qml b/Fk/Common/AvatarChatBox.qml index b79c4d6a..84f757ac 100644 --- a/Fk/Common/AvatarChatBox.qml +++ b/Fk/Common/AvatarChatBox.qml @@ -18,7 +18,8 @@ Rectangle { avatar = "__observer"; } chatLogBox.append({ - avatar: data.general || roomScene.getPhoto(data.sender)?.general || avatar || "unknown", + avatar: data.general || roomScene.getPhoto(data.sender)?.general || + avatar || "unknown", general: general, msg: data.msg, userName: data.userName, @@ -28,7 +29,7 @@ Rectangle { } function loadSkills() { - for (let i = 1; i <= 16; i++) { + for (let i = 1; i <= 23; i++) { skills.append({ name: "fastchat_m", idx: i }); } } @@ -60,7 +61,7 @@ Rectangle { anchors.right: !isSelf ? undefined : avatarPic.left anchors.margins: 6 font.pixelSize: 14 - text: userName + (general ? (" (" + Backend.translate(general) + ")") : "") + text: userName + (general ? (" (" + luatr(general) + ")") : "") + ' [' + time + "]" } @@ -123,7 +124,8 @@ Rectangle { anchors.centerIn: parent source: "../../image/emoji/" + index } - onClicked: chatEdit.insert(chatEdit.cursorPosition, "{emoji" + index + "}"); + onClicked: chatEdit.insert(chatEdit.cursorPosition, + "{emoji" + index + "}"); } } @@ -142,14 +144,14 @@ Rectangle { delegate: ItemDelegate { width: soundSelector.width height: 30 - text: Backend.translate("$" + name + (idx ? idx.toString() : "")) + text: luatr("$" + name + (idx ? idx.toString() : "")) onClicked: { opTimer.start(); const general = roomScene.getPhoto(Self.id).general; let skill = "fastchat_m"; if (general !== "") { - const data = JSON.parse(Backend.callLuaFunction("GetGeneralDetail", [general])); + const data = lcall("GetGeneralDetail", general); const gender = data.gender; if (gender !== 1) { skill = "fastchat_f"; diff --git a/Fk/Common/ChatBox.qml b/Fk/Common/ChatBox.qml index 058dae1c..417192f6 100644 --- a/Fk/Common/ChatBox.qml +++ b/Fk/Common/ChatBox.qml @@ -13,7 +13,7 @@ Rectangle { } function loadSkills() { - for (let i = 1; i <= 16; i++) { + for (let i = 1; i <= 23; i++) { skills.append({ name: "fastchat_m", idx: i }); } } @@ -54,7 +54,8 @@ Rectangle { anchors.centerIn: parent source: "../../image/emoji/" + index } - onClicked: chatEdit.insert(chatEdit.cursorPosition, "{emoji" + index + "}"); + onClicked: chatEdit.insert(chatEdit.cursorPosition, + "{emoji" + index + "}"); } } @@ -73,14 +74,14 @@ Rectangle { delegate: ItemDelegate { width: soundSelector.width height: 30 - text: Backend.translate("$" + name + (idx ? idx.toString() : "")) + text: luatr("$" + name + (idx ? idx.toString() : "")) onClicked: { opTimer.start(); const general = roomScene.getPhoto(Self.id).general; let skill = "fastchat_m"; if (general !== "") { - const data = JSON.parse(Backend.callLuaFunction("GetGeneralDetail", [general])); + const data = lcall("GetGeneralDetail", general); const gender = data.gender; if (gender !== 1) { skill = "fastchat_f"; diff --git a/Fk/Config.qml b/Fk/Config.qml index 1d5c079c..c488af08 100644 --- a/Fk/Config.qml +++ b/Fk/Config.qml @@ -15,7 +15,7 @@ QtObject { property string roomBg property string bgmFile property string language - property list disabledPack: [] + // property list disabledPack: [] property string preferedMode property int preferedPlayerNum property int preferredGeneralNum @@ -23,9 +23,12 @@ QtObject { property real bgmVolume property bool disableMsgAudio property bool hideUseless - property list disabledGenerals: [] - property list disableGeneralSchemes: [] - property int disableSchemeIdx: 0 + // property list disabledGenerals: [] + // property list disableGeneralSchemes: [] + // property int disableSchemeIdx: 0 + property list disableSchemes: [] + property int currentDisableIdx: 0 + property var curScheme property int preferredTimeout property int preferredLuckTime @@ -52,9 +55,9 @@ QtObject { property list blockedUsers: [] property int totalTime: 0 // FIXME: only for notifying - onDisabledGeneralsChanged: { - disableGeneralSchemes[disableSchemeIdx] = disabledGenerals; - } + // onDisabledGeneralsChanged: { + // disableGeneralSchemes[disableSchemeIdx] = disabledGenerals; + // } function loadConf() { conf = JSON.parse(Backend.loadConf()); @@ -75,7 +78,7 @@ QtObject { return 'en_US'; } })(); - disabledPack = conf.disabledPack ?? [ "test_p_0" ]; + // disabledPack = conf.disabledPack ?? [ "test_p_0" ]; preferedMode = conf.preferedMode ?? "aaa_role_mode"; preferedPlayerNum = conf.preferedPlayerNum ?? 2; preferredGeneralNum = conf.preferredGeneralNum ?? 3; @@ -87,9 +90,17 @@ QtObject { preferredTimeout = conf.preferredTimeout ?? 15; preferredLuckTime = conf.preferredLuckTime ?? 0; firstRun = conf.firstRun ?? true; - disabledGenerals = conf.disabledGenerals ?? []; - disableGeneralSchemes = conf.disableGeneralSchemes ?? [ disabledGenerals ]; - disableSchemeIdx = conf.disableSchemeIdx ?? 0; + // disabledGenerals = conf.disabledGenerals ?? []; + // disableGeneralSchemes = conf.disableGeneralSchemes ?? [ disabledGenerals ]; + // disableSchemeIdx = conf.disableSchemeIdx ?? 0; + disableSchemes = conf.disableSchemes ?? [{ + name: "", + banPkg: {}, // 被禁用的包,内部数据为 包名: 白名单武将名数组 + normalPkg: {}, // 未被禁用的包,内部数据为 包名: 黑名单武将名数组 + banCardPkg: [], // 被禁用的卡包 + }]; + currentDisableIdx = conf.currentDisableIdx ?? 0; + curScheme = disableSchemes[currentDisableIdx]; blockedUsers = conf.blockedUsers ?? []; } @@ -104,7 +115,7 @@ QtObject { conf.roomBg = roomBg; conf.bgmFile = bgmFile; conf.language = language; - conf.disabledPack = disabledPack; + // conf.disabledPack = disabledPack; conf.preferedMode = preferedMode; conf.preferedPlayerNum = preferedPlayerNum; conf.ladyImg = ladyImg; @@ -116,9 +127,12 @@ QtObject { conf.preferredTimeout = preferredTimeout; conf.preferredLuckTime = preferredLuckTime; conf.firstRun = firstRun; - conf.disabledGenerals = disabledGenerals; - conf.disableGeneralSchemes = disableGeneralSchemes; - conf.disableSchemeIdx = disableSchemeIdx; + // conf.disabledGenerals = disabledGenerals; + // conf.disableGeneralSchemes = disableGeneralSchemes; + // conf.disableSchemeIdx = disableSchemeIdx; + disableSchemes[currentDisableIdx] = curScheme; + conf.disableSchemes = disableSchemes; + conf.currentDisableIdx = currentDisableIdx; conf.blockedUsers = blockedUsers; Backend.saveConf(JSON.stringify(conf, undefined, 2)); diff --git a/Fk/LobbyElement/AudioSetting.qml b/Fk/LobbyElement/AudioSetting.qml index 15ef8489..94a5e6c1 100644 --- a/Fk/LobbyElement/AudioSetting.qml +++ b/Fk/LobbyElement/AudioSetting.qml @@ -9,7 +9,7 @@ ColumnLayout { anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("BGM Volume") + text: luatr("BGM Volume") } Slider { Layout.rightMargin: 16 @@ -25,7 +25,7 @@ ColumnLayout { anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("Effect Volume") + text: luatr("Effect Volume") } Slider { Layout.rightMargin: 16 @@ -38,13 +38,13 @@ ColumnLayout { } Switch { - text: Backend.translate("Disable message audio") + text: luatr("Disable message audio") checked: config.disableMsgAudio onCheckedChanged: config.disableMsgAudio = checked; } Switch { - text: Backend.translate("Hide unselectable cards") + text: luatr("Hide unselectable cards") checked: config.hideUseless onCheckedChanged: { config.hideUseless = checked; diff --git a/Fk/LobbyElement/BGSetting.qml b/Fk/LobbyElement/BGSetting.qml index f6b279fb..5076ade5 100644 --- a/Fk/LobbyElement/BGSetting.qml +++ b/Fk/LobbyElement/BGSetting.qml @@ -10,7 +10,7 @@ ColumnLayout { anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("Lobby BG") + text: luatr("Lobby BG") } TextField { Layout.fillWidth: true @@ -30,7 +30,7 @@ ColumnLayout { anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("Room BG") + text: luatr("Room BG") } TextField { Layout.fillWidth: true @@ -50,7 +50,7 @@ ColumnLayout { anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("Game BGM") + text: luatr("Game BGM") } TextField { Layout.fillWidth: true @@ -70,7 +70,7 @@ ColumnLayout { anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("Poster Girl") + text: luatr("Poster Girl") } TextField { Layout.fillWidth: true diff --git a/Fk/LobbyElement/BanGeneralSetting.qml b/Fk/LobbyElement/BanGeneralSetting.qml index 7eac2671..59ef8439 100644 --- a/Fk/LobbyElement/BanGeneralSetting.qml +++ b/Fk/LobbyElement/BanGeneralSetting.qml @@ -11,73 +11,103 @@ Item { ColumnLayout { anchors.fill: parent RowLayout { + Layout.fillWidth: true anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("Ban List") + text: luatr("Ban List") } ComboBox { id: banCombo textRole: "name" + Layout.fillWidth: true model: ListModel { id: banComboList } onCurrentIndexChanged: { - config.disableSchemeIdx = currentIndex; - config.disabledGenerals = config.disableGeneralSchemes[currentIndex]; + word.text = ""; + config.disableSchemes[config.currentDisableIdx] = config.curScheme; + config.currentDisableIdx = currentIndex; + config.curScheme = config.disableSchemes[currentIndex]; } } - Button { - text: Backend.translate("New") - onClicked: { - const i = config.disableGeneralSchemes.length; - banComboList.append({ - name: Backend.translate("List") + (i + 1), - }); - config.disableGeneralSchemes.push([]); - } - } + GridLayout { + columns: 2 - Button { - text: Backend.translate("Clear") - onClicked: { - config.disabledGenerals = []; - } - } - - Button { - text: Backend.translate("Export") - onClicked: { - Backend.copyToClipboard(JSON.stringify(config.disabledGenerals)); - toast.show(Backend.translate("Export Success")); - } - } - - Button { - text: Backend.translate("Import") - onClicked: { - const str = Backend.readClipboard(); - let data; - try { - data = JSON.parse(str); - } catch (e) { - toast.show(Backend.translate("Not Legal")); - return; + Button { + text: luatr("New") + onClicked: { + const i = config.disableSchemes.length; + banComboList.append({ + name: luatr("List") + (i + 1), + }); + config.disableSchemes.push({ + name: "", + banPkg: {}, + normalPkg: {}, + banCardPkg: [], + }); } - if (!data instanceof Array) { - toast.show(Backend.translate("Not JSON")); - return; + } + + Button { + text: luatr("Clear") + onClicked: { + config.curScheme.banPkg = {}; + config.curScheme.normalPkg = {}; + config.curScheme.banCardPkg = []; + config.curSchemeChanged(); } - let d = []; - for (let e of data) { - if (typeof e === "string" && Backend.translate(e) !== e) { - d.push(e); + } + + Button { + text: luatr("Export") + onClicked: { + Backend.copyToClipboard(JSON.stringify(config.curScheme)); + toast.show(luatr("Export Success")); + } + } + + Button { + text: luatr("Import") + onClicked: { + const str = Backend.readClipboard(); + let data; + try { + data = JSON.parse(str); + } catch (e) { + toast.show(luatr("Not Legal")); + return; + } + if (!data instanceof Object || !data.banPkg || !data.normalPkg + || !data.banCardPkg) { + toast.show(luatr("Not JSON")); + return; + } + config.curScheme = data; + if (data.name) { + banComboList.get(banCombo.currentIndex).name = data.name; } } - config.disabledGenerals = d; - toast.show(Backend.translate("Import Success")); + } + } + + TextField { + id: word + clip: true + leftPadding: 5 + rightPadding: 5 + } + + Button { + text: luatr("Rename") + enabled: word.text !== "" + onClicked: { + banComboList.get(banCombo.currentIndex).name = word.text; + config.curScheme.name = word.text; + word.text = ""; } } } @@ -86,38 +116,122 @@ Item { Layout.fillWidth: true Layout.margins: 8 wrapMode: Text.WrapAnywhere - text: Backend.translate("Help_Ban_List") + text: luatr("Help_Ban_List") } - GridView { - id: listView + GridLayout { + id: grid + flow: GridLayout.TopToBottom + rows: 2 Layout.fillWidth: true Layout.fillHeight: true - clip: true - cellWidth: width / 4 - cellHeight: 24 - model: config.disabledGenerals - delegate: Text { - width: listView.width - text: { - const prefix = modelData.split("__")[0]; - let name = Backend.translate(modelData); - if (prefix !== modelData) { - name += (" (" + Backend.translate(prefix) + ")"); - } - return name; - } - font.pixelSize: 16 + + Text { + wrapMode: Text.WrapAnywhere + text: luatr("Ban_Generals") + font.pixelSize: 18 + font.bold: true } + + GridView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + cellWidth: width / 2 + cellHeight: 24 + model: { + let ret = [], k; + const s = config.curScheme; + for (k in s.normalPkg) { + ret.push(...s.normalPkg[k]); + } + return ret; + } + delegate: Text { + //width: banChara.width + text: { + const prefix = modelData.split("__")[0]; + let name = luatr(modelData); + if (prefix !== modelData) { + name += (" (" + luatr(prefix) + ")"); + } + return name; + } + font.pixelSize: 16 + } + } + + Text { + wrapMode: Text.WrapAnywhere + text: luatr("Ban_Packages") + font.pixelSize: 18 + font.bold: true + } + + GridView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + cellWidth: width / 2 + cellHeight: 24 + model: { + let ret = [], k; + const s = config.curScheme; + for (k in s.banPkg) { + ret.push(k); + } + ret.push(...s.banCardPkg) + return ret; + } + delegate: Text { + text: luatr(modelData) + font.pixelSize: 16 + } + } + + Text { + wrapMode: Text.WrapAnywhere + text: luatr("Whitelist_Generals") + font.pixelSize: 18 + font.bold: true + } + + GridView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + cellWidth: width / 2 + cellHeight: 24 + model: { + let ret = [], k; + const s = config.curScheme; + for (k in s.banPkg) { + ret.push(...s.banPkg[k]); + } + return ret; + } + delegate: Text { + text: { + const prefix = modelData.split("__")[0]; + let name = luatr(modelData); + if (prefix !== modelData) { + name += (" (" + luatr(prefix) + ")"); + } + return name; + } + font.pixelSize: 16 + } + } + } } Component.onCompleted: { - for (let i = 0; i < config.disableGeneralSchemes.length; i++) { + for (let i = 0; i < config.disableSchemes.length; i++) { banComboList.append({ - name: Backend.translate("List") + (i + 1), + name: config.disableSchemes[i]?.name || (luatr("List") + (i + 1)), }); } - banCombo.currentIndex = config.disableSchemeIdx; + banCombo.currentIndex = config.currentDisableIdx; } } diff --git a/Fk/LobbyElement/CreateRoom.qml b/Fk/LobbyElement/CreateRoom.qml index 2449f536..64437deb 100644 --- a/Fk/LobbyElement/CreateRoom.qml +++ b/Fk/LobbyElement/CreateRoom.qml @@ -18,13 +18,13 @@ Item { width: root.height background: Rectangle { color: "#EEEEEEEE" } TabButton { - text: Backend.translate("General Settings") + text: luatr("General Settings") } TabButton { - text: Backend.translate("Package Settings") + text: luatr("Package Settings") } TabButton { - text: Backend.translate("Ban General Settings") + text: luatr("Ban General Settings") } } diff --git a/Fk/LobbyElement/EditProfile.qml b/Fk/LobbyElement/EditProfile.qml index c64651e2..12eeb658 100644 --- a/Fk/LobbyElement/EditProfile.qml +++ b/Fk/LobbyElement/EditProfile.qml @@ -17,13 +17,13 @@ Item { width: root.height background: Rectangle { color: "#EEEEEEEE" } TabButton { - text: Backend.translate("Userinfo Settings") + text: luatr("Userinfo Settings") } TabButton { - text: Backend.translate("BG Settings") + text: luatr("BG Settings") } TabButton { - text: Backend.translate("Audio Settings") + text: luatr("Audio Settings") } } diff --git a/Fk/LobbyElement/PersonalSettings.qml b/Fk/LobbyElement/PersonalSettings.qml index 70bba354..6a852614 100644 --- a/Fk/LobbyElement/PersonalSettings.qml +++ b/Fk/LobbyElement/PersonalSettings.qml @@ -23,14 +23,14 @@ Item { Text { text: { config.totalTime; - const gamedata = JSON.parse(Backend.callLuaFunction("GetPlayerGameData", [Self.id])); + const gamedata = lcall("GetPlayerGameData", Self.id); const totalTime = gamedata[3]; const h = (totalTime / 3600).toFixed(2); const m = Math.floor(totalTime / 60); if (m < 100) { - return Backend.translate("TotalGameTime: %1 min").arg(m); + return luatr("TotalGameTime: %1 min").arg(m); } else { - return Backend.translate("TotalGameTime: %1 h").arg(h); + return luatr("TotalGameTime: %1 h").arg(h); } } x: 12; y: 1 diff --git a/Fk/LobbyElement/RoomGeneralSettings.qml b/Fk/LobbyElement/RoomGeneralSettings.qml index 3e418f3b..0da41eda 100644 --- a/Fk/LobbyElement/RoomGeneralSettings.qml +++ b/Fk/LobbyElement/RoomGeneralSettings.qml @@ -16,7 +16,7 @@ Flickable { anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("Room Name") + text: luatr("Room Name") } TextField { id: roomName @@ -24,7 +24,7 @@ Flickable { font.pixelSize: 18 Layout.rightMargin: 16 Layout.fillWidth: true - text: Backend.translate("$RoomName").arg(Self.screenName) + text: luatr("$RoomName").arg(Self.screenName) } } @@ -32,7 +32,7 @@ Flickable { anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("Game Mode") + text: luatr("Game Mode") } ComboBox { id: gameModeCombo @@ -57,16 +57,16 @@ Flickable { columnSpacing: 20 columns: 4 Text { - text: Backend.translate("Player num") + text: luatr("Player num") } Text { - text: Backend.translate("Select generals num") + text: luatr("Select generals num") } Text { - text: Backend.translate("Operation timeout") + text: luatr("Operation timeout") } Text { - text: Backend.translate("Luck Card Times") + text: luatr("Luck Card Times") } SpinBox { id: playerNum @@ -109,24 +109,26 @@ Flickable { } } + /* Text { id: warning anchors.rightMargin: 8 visible: { - //config.disabledPack; // 没什么用,只是为了禁包刷新时刷新visible罢了 - const avail = JSON.parse(Backend.callLuaFunction("GetAvailableGeneralsNum", [])); - const ret = avail < config.preferredGeneralNum * config.preferedPlayerNum; + const avail = lcall("GetAvailableGeneralsNum"); + const ret = avail < + config.preferredGeneralNum * config.preferedPlayerNum; return ret; } - text: Backend.translate("No enough generals") + text: luatr("No enough generals") color: "red" } + */ RowLayout { anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("Room Password") + text: luatr("Room Password") } TextField { id: roomPassword @@ -143,13 +145,13 @@ Flickable { Switch { id: freeAssignCheck checked: Debugging ? true : false - text: Backend.translate("Enable free assign") + text: luatr("Enable free assign") } Switch { id: deputyCheck checked: Debugging ? true : false - text: Backend.translate("Enable deputy general") + text: luatr("Enable deputy general") } } @@ -157,44 +159,43 @@ Flickable { anchors.rightMargin: 8 spacing: 16 Button { - text: Backend.translate("OK") - enabled: !(warning.visible) + text: luatr("OK") + // enabled: !(warning.visible) onClicked: { config.saveConf(); root.finished(); mainWindow.busy = true; + let k, arr; - let disabledGenerals = config.disabledGenerals.slice(); - if (disabledGenerals.length) { - const availablePack = JSON.parse(Backend.callLuaFunction("GetAllGeneralPack", [])). - filter((pack) => !config.disabledPack.includes(pack)); - disabledGenerals = disabledGenerals.filter((general) => { - return availablePack.find((pack) => JSON.parse(Backend.callLuaFunction("GetGenerals", [pack])).includes(general)); - }); - - disabledGenerals = Array.from(new Set(disabledGenerals)); + let disabledGenerals = []; + for (k in config.curScheme.banPkg) { + arr = config.curScheme.banPkg[k]; + if (arr.length !== 0) { + const generals = lcall("GetGenerals", k); + disabledGenerals.push(...generals.filter(g => !arr.includes(g))); + } + } + for (k in config.curScheme.normalPkg) { + arr = config.curScheme.normalPkg[k] ?? []; + if (arr.length !== 0) + disabledGenerals.push(...arr); } - let disabledPack = config.disabledPack.slice(); + let disabledPack = config.curScheme.banCardPkg.slice(); + for (k in config.curScheme.banPkg) { + if (config.curScheme.banPkg[k].length === 0) + disabledPack.push(k); + } config.serverHiddenPacks.forEach(p => { if (!disabledPack.includes(p)) { disabledPack.push(p); } }); - const generalPacks = JSON.parse(Backend.callLuaFunction("GetAllGeneralPack", [])); - for (let pk of generalPacks) { - if (disabledPack.includes(pk)) continue; - let generals = JSON.parse(Backend.callLuaFunction("GetGenerals", [pk])); - let t = generals.filter(g => !disabledGenerals.includes(g)); - if (t.length === 0) { - disabledPack.push(pk); - disabledGenerals = disabledGenerals.filter(g1 => !generals.includes(g1)); - } - } ClientInstance.notifyServer( "CreateRoom", - JSON.stringify([roomName.text, playerNum.value, config.preferredTimeout, { + JSON.stringify([roomName.text, playerNum.value, + config.preferredTimeout, { enableFreeAssign: freeAssignCheck.checked, enableDeputy: deputyCheck.checked, gameMode: config.preferedMode, @@ -208,7 +209,7 @@ Flickable { } } Button { - text: Backend.translate("Cancel") + text: luatr("Cancel") onClicked: { root.finished(); } @@ -216,11 +217,11 @@ Flickable { } Component.onCompleted: { - const mode_data = JSON.parse(Backend.callLuaFunction("GetGameModes", [])); + const mode_data = lcall("GetGameModes"); let i = 0; for (let d of mode_data) { gameModeList.append(d); - if (d.orig_name == config.preferedMode) { + if (d.orig_name === config.preferedMode) { gameModeCombo.currentIndex = i; } i += 1; @@ -228,10 +229,12 @@ Flickable { playerNum.value = config.preferedPlayerNum; - config.disabledPack.forEach(p => { - Backend.callLuaFunction("UpdatePackageEnable", [p, false]); - }); - config.disabledPackChanged(); + for (let k in config.curScheme.banPkg) { + lcall("UpdatePackageEnable", k, false); + } + config.curScheme.banCardPkg.forEach(p => + lcall("UpdatePackageEnable", p, false)); + config.curSchemeChanged(); } } } diff --git a/Fk/LobbyElement/RoomPackageSettings.qml b/Fk/LobbyElement/RoomPackageSettings.qml index 7b7edb13..3b03385b 100644 --- a/Fk/LobbyElement/RoomPackageSettings.qml +++ b/Fk/LobbyElement/RoomPackageSettings.qml @@ -23,16 +23,16 @@ Flickable { anchors.topMargin: 8 Switch { - text: Backend.translate("Disable Extension") + text: luatr("Disable Extension") } RowLayout { Text { - text: Backend.translate("General Packages") + text: luatr("General Packages") font.bold: true } Button { - text: Backend.translate("Select All") + text: luatr("Select All") onClicked: { for (let i = 0; i < gpacks.count; i++) { const item = gpacks.itemAt(i); @@ -41,7 +41,7 @@ Flickable { } } Button { - text: Backend.translate("Revert Selection") + text: luatr("Revert Selection") onClicked: { for (let i = 0; i < gpacks.count; i++) { const item = gpacks.itemAt(i); @@ -76,11 +76,11 @@ Flickable { RowLayout { Text { - text: Backend.translate("Card Packages") + text: luatr("Card Packages") font.bold: true } Button { - text: Backend.translate("Select All") + text: luatr("Select All") onClicked: { for (let i = 0; i < cpacks.count; i++) { const item = cpacks.itemAt(i); @@ -89,7 +89,7 @@ Flickable { } } Button { - text: Backend.translate("Revert Selection") + text: luatr("Revert Selection") onClicked: { for (let i = 0; i < cpacks.count; i++) { const item = cpacks.itemAt(i); @@ -113,7 +113,15 @@ Flickable { checked: pkg_enabled onCheckedChanged: { - checkPackage(orig_name, checked); + const packs = config.curScheme.banCardPkg; + if (checked) { + const idx = packs.indexOf(orig_name); + if (idx !== -1) packs.splice(idx, 1); + } else { + packs.push(orig_name); + } + lcall("UpdatePackageEnable", orig_name, checked); + config.curSchemeChanged(); } } } @@ -121,40 +129,42 @@ Flickable { } function checkPackage(orig_name, checked) { - const packs = config.disabledPack; - if (checked) { - const idx = packs.indexOf(orig_name); - if (idx !== -1) packs.splice(idx, 1); + const s = config.curScheme; + if (!checked) { + s.banPkg[orig_name] = []; + delete s.normalPkg[orig_name]; } else { - packs.push(orig_name); + delete s.normalPkg[orig_name]; + delete s.banPkg[orig_name]; } - Backend.callLuaFunction("UpdatePackageEnable", [orig_name, checked]); - config.disabledPackChanged(); + lcall("UpdatePackageEnable", orig_name, checked); + config.curSchemeChanged(); } Component.onCompleted: { loading = true; - const g = JSON.parse(Backend.callLuaFunction("GetAllGeneralPack", [])); - for (let orig of g) { + const g = lcall("GetAllGeneralPack"); + let orig; + for (orig of g) { if (config.serverHiddenPacks.includes(orig)) { continue; } gpacklist.append({ - name: Backend.translate(orig), + name: luatr(orig), orig_name: orig, - pkg_enabled: !config.disabledPack.includes(orig), + pkg_enabled: !config.curScheme.banPkg[orig], }); } - const c = JSON.parse(Backend.callLuaFunction("GetAllCardPack", [])); - for (let orig of c) { + const c = lcall("GetAllCardPack"); + for (orig of c) { if (config.serverHiddenPacks.includes(orig)) { continue; } cpacklist.append({ - name: Backend.translate(orig), + name: luatr(orig), orig_name: orig, - pkg_enabled: !config.disabledPack.includes(orig), + pkg_enabled: !config.curScheme.banCardPkg.includes(orig), }); } loading = false; diff --git a/Fk/LobbyElement/UserInfo.qml b/Fk/LobbyElement/UserInfo.qml index c70fa7ac..a634ec81 100644 --- a/Fk/LobbyElement/UserInfo.qml +++ b/Fk/LobbyElement/UserInfo.qml @@ -11,7 +11,7 @@ ColumnLayout { anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("Username") + text: luatr("Username") } Text { text: Self.screenName @@ -28,7 +28,7 @@ ColumnLayout { anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("Avatar") + text: luatr("Avatar") } TextField { id: avatarName @@ -38,7 +38,7 @@ ColumnLayout { Layout.fillWidth: true } Button { - text: Backend.translate("Update Avatar") + text: luatr("Update Avatar") enabled: avatarName.text !== "" && !opTimer.running onClicked: { mainWindow.busy = true; @@ -55,7 +55,7 @@ ColumnLayout { anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("Old Password") + text: luatr("Old Password") } TextField { id: oldPassword @@ -70,7 +70,7 @@ ColumnLayout { anchors.rightMargin: 8 spacing: 16 Text { - text: Backend.translate("New Password") + text: luatr("New Password") } TextField { id: newPassword @@ -79,8 +79,9 @@ ColumnLayout { Layout.fillWidth: true } Button { - text: Backend.translate("Update Password") - enabled: oldPassword.text !== "" && newPassword.text !== "" && !opTimer.running + text: luatr("Update Password") + enabled: oldPassword.text !== "" && newPassword.text !== "" + && !opTimer.running onClicked: { mainWindow.busy = true; opTimer.start(); diff --git a/Fk/Logic.js b/Fk/Logic.js index 01eebe73..5e2ad89d 100644 --- a/Fk/Logic.js +++ b/Fk/Logic.js @@ -46,8 +46,8 @@ callbacks["NetworkDelayTest"] = (jsonData) => { // jsonData: RSA pub key let cipherText; let aeskey; - if (config.savedPassword[config.serverAddr] !== undefined - && config.savedPassword[config.serverAddr].shorten_password === config.password) { + const savedPw = config.savedPassword[config.serverAddr]; + if (savedPw?.shorten_password === config.password) { cipherText = config.savedPassword[config.serverAddr].password; aeskey = config.savedPassword[config.serverAddr].key; config.aeskey = aeskey ?? ""; @@ -173,7 +173,7 @@ callbacks["Chat"] = (jsonData) => { const data = JSON.parse(jsonData); const pid = data.sender; const userName = data.userName; - const general = Backend.translate(data.general); + const general = luatr(data.general); const time = data.time; const msg = data.msg; @@ -181,10 +181,13 @@ callbacks["Chat"] = (jsonData) => { return; } + let text; if (general === "") - current.addToChat(pid, data, `[${time}] ${userName}: ${msg}`); + text = `[${time}] ${userName}: ${msg}`; else - current.addToChat(pid, data, `[${time}] ${userName}(${general}): ${msg}`); + text = `[${time}] ${userName}` + + `(${general}): ${msg}`; + current.addToChat(pid, data, text); } callbacks["ServerMessage"] = (jsonData) => { diff --git a/Fk/ModMaker/CreateSomething.qml b/Fk/ModMaker/CreateSomething.qml index d3cc7485..6db0988c 100644 --- a/Fk/ModMaker/CreateSomething.qml +++ b/Fk/ModMaker/CreateSomething.qml @@ -36,7 +36,9 @@ ColumnLayout { id: edit font.pixelSize: 18 Layout.fillWidth: true - validator: RegularExpressionValidator { regularExpression: /[0-9A-Za-z_]+/ } + validator: RegularExpressionValidator { + regularExpression: /[0-9A-Za-z_]+/ + } } Button { diff --git a/Fk/ModMaker/ModConfig.qml b/Fk/ModMaker/ModConfig.qml index 51bfc119..2ac5e04a 100644 --- a/Fk/ModMaker/ModConfig.qml +++ b/Fk/ModMaker/ModConfig.qml @@ -19,7 +19,8 @@ QtObject { conf.email = email; conf.modList = modList; - ModBackend.saveToFile("mymod/config.json", JSON.stringify(conf, undefined, 2)); + ModBackend.saveToFile("mymod/config.json", + JSON.stringify(conf, undefined, 2)); } function addMod(mod) { diff --git a/Fk/ModMaker/ModDetail.qml b/Fk/ModMaker/ModDetail.qml index 780d392a..779ec4c9 100644 --- a/Fk/ModMaker/ModDetail.qml +++ b/Fk/ModMaker/ModDetail.qml @@ -62,7 +62,8 @@ Item { SwipeDelegate.onClicked: deletePackage(modelData); background: Rectangle { - color: deleteLabel.SwipeDelegate.pressed ? Qt.darker("tomato", 1.1) : "tomato" + color: deleteLabel.SwipeDelegate.pressed ? Qt.darker("tomato", 1.1) + : "tomato" } } } @@ -118,14 +119,19 @@ Item { toast.show(qsTr("cannot use this package name")); return; } + const path = modPath + new_name + "/"; ModBackend.mkdir(path); mod.packages.push(new_name); - ModBackend.saveToFile(modPath + "mod.json", JSON.stringify(mod, undefined, 2)); + ModBackend.saveToFile(modPath + "mod.json", + JSON.stringify(mod, undefined, 2)); + const pkgInfo = { name: new_name, }; - ModBackend.saveToFile(path + "pkg.json", JSON.stringify(pkgInfo, undefined, 2)); + ModBackend.saveToFile(path + "pkg.json", + JSON.stringify(pkgInfo, undefined, 2)); + root.modChanged(); } @@ -133,7 +139,9 @@ Item { const path = modPath + name + "/"; ModBackend.rmrf(path); mod.packages.splice(mod.packages.indexOf(name), 1); - ModBackend.saveToFile(modPath + "mod.json", JSON.stringify(mod, undefined, 2)); + ModBackend.saveToFile(modPath + "mod.json", + JSON.stringify(mod, undefined, 2)); + root.modChanged(); } } diff --git a/Fk/ModMaker/ModInit.qml b/Fk/ModMaker/ModInit.qml index 80a6b569..dd6a6fbe 100644 --- a/Fk/ModMaker/ModInit.qml +++ b/Fk/ModMaker/ModInit.qml @@ -84,7 +84,8 @@ Item { SwipeDelegate.onClicked: deleteMod(modelData); background: Rectangle { - color: deleteLabel.SwipeDelegate.pressed ? Qt.darker("tomato", 1.1) : "tomato" + color: deleteLabel.SwipeDelegate.pressed ? Qt.darker("tomato", 1.1) + : "tomato" } } } @@ -136,7 +137,8 @@ Item { function createNewMod(name) { const banned = [ "test", "standard", "standard_cards", "maneuvering" ]; - if (banned.indexOf(name) !== -1 || modConfig.modList.indexOf(name) !== -1) { + if (banned.indexOf(name) !== -1 || + modConfig.modList.indexOf(name) !== -1) { toast.show(qsTr("cannot use this mod name")); return; } @@ -146,10 +148,12 @@ Item { descrption: "", author: modConfig.userName, }; - ModBackend.saveToFile(`mymod/${name}/mod.json`, JSON.stringify(modInfo, undefined, 2)); + ModBackend.saveToFile(`mymod/${name}/mod.json`, + JSON.stringify(modInfo, undefined, 2)); ModBackend.saveToFile(`mymod/${name}/.gitignore`, "init.lua"); ModBackend.stageFiles(name); - ModBackend.commitChanges(name, "Initial commit", modConfig.userName, modConfig.email); + ModBackend.commitChanges(name, "Initial commit", modConfig.userName, + modConfig.email); modConfig.addMod(name); } diff --git a/Fk/Pages/About.qml b/Fk/Pages/About.qml index 19e16c21..a49b6b6c 100644 --- a/Fk/Pages/About.qml +++ b/Fk/Pages/About.qml @@ -50,7 +50,7 @@ Item { anchors.left: logo.right anchors.leftMargin: 16 width: parent.width * 0.65 - text: Backend.translate("about_" + dest + "_description") + text: luatr("about_" + dest + "_description") wrapMode: Text.WordWrap textFormat: Text.MarkdownText font.pixelSize: 18 @@ -73,7 +73,7 @@ Item { } Button { - text: Backend.translate("Quit") + text: luatr("Quit") anchors.right: parent.right onClicked: { swipe.opacity = 0; diff --git a/Fk/Pages/CardsOverview.qml b/Fk/Pages/CardsOverview.qml index a2716b58..8a2fb3ab 100644 --- a/Fk/Pages/CardsOverview.qml +++ b/Fk/Pages/CardsOverview.qml @@ -3,6 +3,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls +import Fk import Fk.RoomElement Item { @@ -35,7 +36,7 @@ Item { height: 40 Text { - text: Backend.translate(name) + text: luatr(name) anchors.centerIn: parent } @@ -62,6 +63,7 @@ Item { delegate: CardItem { autoBack: false + showDetail: false property int dupCount: 0 Text { @@ -115,10 +117,9 @@ Item { easing.type: Easing.InOutQuad } onFinished: { - const pkg = [listView.model.get(listView.currentIndex).name]; - const idList = JSON.parse(Backend.callLuaFunction("GetCards", pkg)); - const cardList = idList.map(id => JSON.parse(Backend.callLuaFunction - ("GetCardData",[id]))); + const pkg = listView.model.get(listView.currentIndex).name; + const idList = lcall("GetCards", pkg); + const cardList = idList.map(id => lcall("GetCardData", id)); const groupedCardList = []; let groupedCards = {}; @@ -194,25 +195,11 @@ Item { property int cid: 1 property var cards function updateCard() { - const data = JSON.parse(Backend.callLuaFunction("GetCardData", [cid])); + const data = lcall("GetCardData", cid); const suitTable = { spade: "♠", heart: '', club: "♣", diamond: '', } - const getNumString = n => { - switch (n) { - case 1: - return "A"; - case 11: - return "J"; - case 12: - return "Q"; - case 13: - return "K"; - default: - return n.toString(); - } - } if (!cards) { detailCard.setData(data); @@ -227,22 +214,21 @@ Item { detailCard.known = true; cardText.clear(); audioRow.clear(); - cardText.append(Backend.translate(":" + data.name)); + cardText.append(luatr(":" + data.name)); addCardAudio(data) - const skills = JSON.parse(Backend.callLuaFunction - ("GetCardSpecialSkills", [cid])); + const skills = lcall("GetCardSpecialSkills", cid); if (skills.length > 0) { - cardText.append("
" + Backend.translate("Special card skills:")); + cardText.append("
" + luatr("Special card skills:")); skills.forEach(t => { - cardText.append("" + Backend.translate(t) + ": " - + Backend.translate(":" + t)); + cardText.append("" + luatr(t) + ": " + + luatr(":" + t)); }); } if (cards) { - cardText.append("
" + Backend.translate("Every suit & number:")); + cardText.append("
" + luatr("Every suit & number:")); cardText.append(cards.map(c => { - return (suitTable[c.suit] + getNumString(c.number)) + return (suitTable[c.suit] + Util.convertNumber(c.number)) }).join(", ")); } } @@ -265,6 +251,7 @@ Item { Layout.alignment: Qt.AlignHCenter cid: 1 known: false + showDetail: false property int dupCount: 0 Text { @@ -303,16 +290,17 @@ Item { verticalAlignment: Text.AlignVCenter text: { if (gender === "male") { - return Backend.translate("Male Audio"); + return luatr("Male Audio"); } else { - return Backend.translate("Female Audio"); + return luatr("Female Audio"); } } font.pixelSize: 14 } onClicked: { - const data = JSON.parse(Backend.callLuaFunction("GetCardData", [cardDetail.cid])); - Backend.playSound("./packages/" + extension + "/audio/card/" + gender + "/" + data.name); + const data = lcall("GetCardData", cardDetail.cid); + Backend.playSound("./packages/" + extension + "/audio/card/" + + gender + "/" + data.name); } } } @@ -322,7 +310,7 @@ Item { } Button { - text: Backend.translate("Quit") + text: luatr("Quit") anchors.right: parent.right onClicked: { mainStack.pop(); @@ -331,30 +319,32 @@ Item { function addCardAudio(card) { const extension = card.extension; - const orig_extension = Backend.callLuaFunction("GetCardExtensionByName", [card.name]); - let fname = AppPath + "/packages/" + extension + "/audio/card/male/" + card.name + ".mp3"; + const orig_extension = lcall("GetCardExtensionByName", card.name); + const prefix = AppPath + "/packages/"; + const suffix = card.name + ".mp3"; + let fname = prefix + extension + "/audio/card/male/" + suffix; if (Backend.exists(fname)) { - audioRow.append( {gender: "male", extension: extension} ); + audioRow.append( { gender: "male", extension: extension } ); } else { - fname = AppPath + "/packages/" + orig_extension + "/audio/card/male/" + card.name + ".mp3"; + fname = prefix + orig_extension + "/audio/card/male/" + suffix; if (Backend.exists(fname)) { audioRow.append( {gender: "male", extension: orig_extension} ); } } - fname = AppPath + "/packages/" + extension + "/audio/card/female/" + card.name + ".mp3"; + fname = prefix + extension + "/audio/card/female/" + suffix; if (Backend.exists(fname)) { - audioRow.append( {gender: "female", extension: extension} ); + audioRow.append( { gender: "female", extension: extension } ); }else { - fname = AppPath + "/packages/" + orig_extension + "/audio/card/female/" + card.name + ".mp3"; + fname = prefix + orig_extension + "/audio/card/female/" + suffix; if (Backend.exists(fname)) { - audioRow.append( {gender: "female", extension: orig_extension} ); + audioRow.append( { gender: "female", extension: orig_extension } ); } } } function loadPackages() { if (loaded) return; - const packs = JSON.parse(Backend.callLuaFunction("GetAllCardPack", [])); + const packs = lcall("GetAllCardPack"); packs.forEach(name => { if (!config.serverHiddenPacks.includes(name)) { packages.append({ name: name }); diff --git a/Fk/Pages/GeneralsOverview.qml b/Fk/Pages/GeneralsOverview.qml index 51fc56a9..51daf516 100644 --- a/Fk/Pages/GeneralsOverview.qml +++ b/Fk/Pages/GeneralsOverview.qml @@ -3,6 +3,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls +import Fk import Fk.RoomElement import "RoomLogic.js" as RoomLogic @@ -10,39 +11,98 @@ Item { id: root property bool loaded: false + property int stat: 0 // 0=normal 1=banPkg 2=banChara Rectangle { - anchors.fill: listView - color: "#88EEEEEE" + id: listBg + width: 260; height: parent.height + color: "snow" radius: 6 } ListView { - id: listView + id: modList + width: 130; height: parent.height + anchors.top: listBg.top; anchors.left: listBg.left clip: true - width: 130 - height: parent.height - 20 - y: 10 - ScrollBar.vertical: ScrollBar {} model: ListModel { - id: packages + id: mods } - highlight: Rectangle { color: "#E91E63"; radius: 5 } + Rectangle { + anchors.fill: parent + color: "#A48959" + z: -1 + } + + highlight: Rectangle { color: "snow" } highlightMoveDuration: 500 delegate: Item { - width: listView.width + width: modList.width height: 40 Text { - text: Backend.translate(name) + text: luatr(name) + color: modList.currentIndex === index ? "black" : "white" anchors.centerIn: parent } TapHandler { onTapped: { - listView.currentIndex = index; + modList.currentIndex = index; + } + } + } + } + + ListView { + id: pkgList + width: 130; height: parent.height + anchors.top: listBg.top; anchors.left: modList.right + + clip: true + model: JSON.parse(mods.get(modList.currentIndex)?.pkgs ?? "[]") + + highlight: Rectangle { color: "#FFCC3F"; radius: 5; scale: 0.8 } + highlightMoveDuration: 500 + + delegate: Item { + width: pkgList.width + height: 40 + + Text { + text: luatr(modelData) + color: !config.curScheme.banPkg[modelData] ? "black" : "grey" + Behavior on color { ColorAnimation { duration: 200 } } + anchors.centerIn: parent + } + + Image { + source: AppPath + "/image/button/skill/locked.png" + opacity: !config.curScheme.banPkg[modelData] ? 0 : 1 + Behavior on opacity { NumberAnimation { duration: 200 } } + anchors.centerIn: parent + scale: 0.8 + } + + TapHandler { + onTapped: { + if (stat === 1) { + const name = modelData; + let s = config.curScheme; + if (s.banPkg[name]) { + delete s.banPkg[name]; + delete s.normalPkg[name]; + } else { + delete s.normalPkg[name]; + s.banPkg[name] = []; + } + console.log(JSON.stringify(config.curScheme)) + config.curSchemeChanged(); + } else { + pkgList.currentIndex = index; + } } } } @@ -50,13 +110,106 @@ Item { onCurrentIndexChanged: { vanishAnim.start(); } } + ToolBar { + id: bar + width: root.width - listBg.width - 16 + anchors.left: listBg.right + anchors.leftMargin: 8 + y: 8 + + background: Rectangle { + color: stat === 0 ? "#5cb3cc" : "#869d9d" + Behavior on color { ColorAnimation { duration: 200 } } + } + + RowLayout { + anchors.fill: parent + Item { Layout.preferredWidth: 20 } + + Label { + text: { + switch (stat) { + case 0: return luatr("Generals Overview"); + case 1: return luatr("$BanPkgHelp"); + case 2: return luatr("$BanCharaHelp"); + } + } + elide: Label.ElideLeft + verticalAlignment: Qt.AlignVCenter + font.pixelSize: 28 + } + + Item { Layout.fillWidth: true } + + TextField { + id: word + clip: true + leftPadding: 5 + rightPadding: 5 + } + + ToolButton { + text: luatr("Search") + font.pixelSize: 20 + enabled: word.text !== "" + onClicked: { + pkgList.currentIndex = 0; + vanishAnim.start(); + } + } + + ToolButton { + id: banButton + font.pixelSize: 20 + text: { + if (stat === 2) return luatr("OK"); + return luatr("BanGeneral"); + } + enabled: stat !== 1 + onClicked: { + if (stat === 0) { + stat = 2; + } else { + stat = 0; + } + } + } + + ToolButton { + id: banPkgButton + font.pixelSize: 20 + text: { + if (stat === 1) return luatr("OK"); + return luatr("BanPackage"); + } + enabled: stat !== 2 + onClicked: { + if (stat === 0) { + stat = 1; + } else { + stat = 0; + } + } + } + + ToolButton { + text: luatr("Quit") + font.pixelSize: 20 + onClicked: { + mainStack.pop(); + config.saveConf(); + } + } + } + } + GridView { id: gridView clip: true - width: root.width - listView.width - generalDetail.width - 16 - height: parent.height - 20 - y: 10 - anchors.left: listView.right + width: root.width - listBg.width - 16 + height: parent.height - bar.height - 24 + y: 16 + bar.height + anchors.left: listBg.right anchors.leftMargin: 8 + (width % 100) / 2 cellHeight: 140 cellWidth: 100 @@ -65,24 +218,77 @@ Item { autoBack: false name: modelData onClicked: { - generalText.clear(); - generalDetail.general = modelData; - generalDetail.updateGeneral(); - // generalDetail.open(); + if (stat === 2) { + const s = config.curScheme; + const gdata = lcall("GetGeneralData", modelData); + const pack = gdata.package; + let arr; + if (s.banPkg[pack]) { + arr = s.banPkg[pack]; + } else { + if (!s.normalPkg[pack]) { + s.normalPkg[pack] = []; + } + arr = s.normalPkg[pack]; + } + // TODO: 根据手动全禁/全白名单自动改为禁包 + const idx = arr.indexOf(modelData); + if (idx !== -1) { + arr.splice(idx, 1); + } else { + arr.push(modelData); + } + config.curSchemeChanged(); + } else { + generalText.clear(); + generalDetail.general = modelData; + generalDetail.updateGeneral(); + generalDetail.open(); + } } Rectangle { anchors.fill: parent color: "black" - opacity: config.disabledGenerals.includes(modelData) ? 0.7 : 0 + opacity: { + const s = config.curScheme; + const gdata = lcall("GetGeneralData", modelData); + const pack = gdata.package; + if (s.banPkg[pack]) { + if (!s.banPkg[pack].includes(modelData)) return 0.5; + } else { + if (!!s.normalPkg[pack]?.includes(modelData)) return 0.5; + } + return 0; + } Behavior on opacity { NumberAnimation {} } } GlowText { - visible: config.disabledGenerals.includes(modelData) - text: '禁' + id: banText + visible: { + const s = config.curScheme; + const gdata = lcall("GetGeneralData", modelData); + const pack = gdata.package; + if (s.banPkg[pack]) { + return s.banPkg[pack].includes(modelData); + } else { + return !!s.normalPkg[pack]?.includes(modelData); + } + } + text: { + if (!visible) return ''; + const s = config.curScheme; + const gdata = lcall("GetGeneralData", modelData); + const pack = gdata.package; + if (s.banPkg[pack]) { + if (s.banPkg[pack].includes(modelData)) return '启用'; + } else { + if (!!s.normalPkg[pack]?.includes(modelData)) return '禁'; + } + } anchors.centerIn: parent font.family: fontLi2.name color: "#E4D5A0" @@ -107,17 +313,16 @@ Item { PropertyAnimation { target: gridView property: "y" - to: 30 + to: 36 + bar.height duration: 150 easing.type: Easing.InOutQuad } onFinished: { if (word.text !== "") { - gridView.model = JSON.parse(Backend.callLuaFunction("SearchAllGenerals", - [word.text])); + gridView.model = lcall("SearchAllGenerals", word.text); } else { - gridView.model = JSON.parse(Backend.callLuaFunction("SearchGenerals", - [listView.model.get(listView.currentIndex).name, word.text])); + gridView.model = lcall("SearchGenerals", + pkgList.model[pkgList.currentIndex], word.text); } word.text = ""; appearAnim.start(); @@ -138,28 +343,83 @@ Item { PropertyAnimation { target: gridView property: "y" - from: 20 - to: 10 + from: 36 + bar.height + to: 16 + bar.height duration: 150 easing.type: Easing.InOutQuad } } } - Rectangle { + Component { + id: skillAudioBtn + Button { + Layout.fillWidth: true + contentItem: ColumnLayout { + Text { + Layout.fillWidth: true + text: { + if (name.endsWith("_win_audio")) { + return luatr("Win audio"); + } + return luatr(name) + (idx ? " (" + idx.toString() + ")" + : ""); + } + font.bold: true + font.pixelSize: 14 + } + Text { + Layout.fillWidth: true + text: { + const orig = '$' + name + (idx ? idx.toString() : ""); + const orig_trans = luatr(orig); + + // try general specific + const orig_g = '$' + name + '_' + detailGeneralCard.name + + (idx ? idx.toString() : ""); + const orig_g_trans = luatr(orig_g); + + if (orig_g_trans !== orig_g) { + return orig_g_trans; + } + + if (orig_trans !== orig) { + return orig_trans; + } + + return ""; + } + wrapMode: Text.WordWrap + } + } + + onClicked: { + callbacks["LogEvent"](JSON.stringify({ + type: "PlaySkillSound", + name: name, + general: detailGeneralCard.name, + i: idx, + })); + } + } + } + + Popup { id: generalDetail - width: 310 - height: parent.height - searcher.height - 20 - y: 10 - anchors.right: parent.right - anchors.rightMargin: 10 - color: "#88EEEEEE" - radius: 8 + width: realMainWin.width * 0.6 + height: realMainWin.height * 0.8 + anchors.centerIn: parent + background: Rectangle { + color: "#EEEEEEEE" + radius: 5 + border.color: "#A6967A" + border.width: 1 + } property string general: "caocao" function addSpecialSkillAudio(skill) { - const gdata = JSON.parse(Backend.callLuaFunction("GetGeneralData", [general])); + const gdata = lcall("GetGeneralData", general); const extension = gdata.extension; let ret = false; for (let i = 0; i < 999; i++) { @@ -178,7 +438,7 @@ Item { function addSkillAudio(skill) { if (addSpecialSkillAudio(skill)) return; - const skilldata = JSON.parse(Backend.callLuaFunction("GetSkillData", [skill])); + const skilldata = lcall("GetSkillData", skill); if (!skilldata) return; const extension = skilldata.extension; for (let i = 0; i < 999; i++) { @@ -194,8 +454,9 @@ Item { } function findDeathAudio(general) { - const extension = JSON.parse(Backend.callLuaFunction("GetGeneralData", [general])).extension; - const fname = AppPath + "/packages/" + extension + "/audio/death/" + general + ".mp3"; + const extension = lcall("GetGeneralData", general).extension; + const fname = AppPath + "/packages/" + extension + "/audio/death/" + + general + ".mp3"; if (Backend.exists(fname)) { audioDeath.visible = true; } else { @@ -205,27 +466,27 @@ Item { function updateGeneral() { detailGeneralCard.name = general; - const data = JSON.parse(Backend.callLuaFunction("GetGeneralDetail", [general])); + const data = lcall("GetGeneralDetail", general); generalText.clear(); audioModel.clear(); if (data.companions.length > 0){ - let ret = ''; - ret += "" + Backend.translate("Companions") + ": "; + let ret = "" + luatr("Companions") + + ": "; data.companions.forEach(t => { - ret += Backend.translate(t) + ' ' + ret += luatr(t) + ' ' }); generalText.append(ret) } data.skill.forEach(t => { - generalText.append("" + Backend.translate(t.name) + + generalText.append("" + luatr(t.name) + ": " + t.description); addSkillAudio(t.name); }); data.related_skill.forEach(t => { - generalText.append("" + Backend.translate(t.name) + + generalText.append("" + luatr(t.name) + ": " + t.description + ""); addSkillAudio(t.name); @@ -235,210 +496,153 @@ Item { addSkillAudio(general + "_win_audio"); } - Flickable { - flickableDirection: Flickable.VerticalFlick - contentHeight: detailLayout.height - width: parent.width - 40 - height: parent.height - 40 - clip: true + Item { anchors.centerIn: parent - ScrollBar.vertical: ScrollBar {} + width: parent.width / mainWindow.scale + height: parent.height / mainWindow.scale + scale: mainWindow.scale - ColumnLayout { - id: detailLayout - width: parent.width - - GeneralCardItem { - id: detailGeneralCard - Layout.alignment: Qt.AlignHCenter - name: "caocao" - } - - TextEdit { - id: generalText - - Layout.fillWidth: true - readOnly: true - selectByKeyboard: true - selectByMouse: false - wrapMode: TextEdit.WordWrap - textFormat: TextEdit.RichText - font.pixelSize: 16 - } - - Repeater { - model: ListModel { - id: audioModel + Item { + id: generalInfo + width: 150 + ColumnLayout { + width: parent.width + GeneralCardItem { + id: detailGeneralCard + name: "caocao" + scale: 1.5; transformOrigin: Item.TopLeft } + + Item { Layout.preferredHeight: 130 * 0.5 } + + Text { + Layout.fillWidth: true + textFormat: TextEdit.RichText + font.pixelSize: 16 + function trans(str) { + const ret = luatr(str); + if (ret === str) { + return luatr("Official"); + } + return ret; + } + text: { + const general = generalDetail.general; + return [ + luatr(lcall("GetGeneralData", general).package), + luatr("Title") + trans("#" + general), + luatr("Designer") + trans("designer:" + general), + luatr("Voice Actor") + trans("cv:" + general), + luatr("Illustrator") + trans("illustrator:" + general), + ].join("
"); + } + } + + Timer { + id: opTimer + interval: 4000 + } + Button { + text: luatr("Set as Avatar") + enabled: detailGeneralCard.name !== "" && !opTimer.running + && Self.avatar !== detailGeneralCard.name + onClicked: { + mainWindow.busy = true; + opTimer.start(); + ClientInstance.notifyServer( + "UpdateAvatar", + JSON.stringify([detailGeneralCard.name]) + ); + } + } + } + } + + Flickable { + flickableDirection: Flickable.VerticalFlick + contentHeight: detailLayout.height + width: parent.width - 40 - generalInfo.width + height: parent.height - 40 + clip: true + anchors.left: generalInfo.right + anchors.leftMargin: 20 + y: 20 + + ColumnLayout { + id: detailLayout + width: parent.width + + TextEdit { + id: generalText + + Layout.fillWidth: true + readOnly: true + selectByKeyboard: true + selectByMouse: false + wrapMode: TextEdit.WordWrap + textFormat: TextEdit.RichText + font.pixelSize: 18 + } + + GridLayout { + Layout.fillWidth: true + columns: 2 + Repeater { + model: ListModel { + id: audioModel + } + delegate: skillAudioBtn + } + } + + Button { + id: audioDeath Layout.fillWidth: true contentItem: ColumnLayout { Text { Layout.fillWidth: true - text: { - if (name.endsWith("_win_audio")) { - return "胜利语音"; - } - return Backend.translate(name) + (idx ? " (" + idx.toString() + ")" : ""); - } + text: luatr("Death audio") font.bold: true font.pixelSize: 14 } Text { Layout.fillWidth: true text: { - const orig = '$' + name + (idx ? idx.toString() : ""); - const orig_trans = Backend.translate(orig); - - // try general specific - const orig_g = '$' + name + '_' + detailGeneralCard.name + (idx ? idx.toString() : ""); - const orig_g_trans = Backend.translate(orig_g); - - if (orig_g_trans !== orig_g) { - return orig_g_trans; + const orig = "~" + generalDetail.general; + const tr = luatr(orig); + if (tr === orig) { + return ""; } - - if (orig_trans !== orig) { - return orig_trans; - } - - return ""; + return tr; } wrapMode: Text.WordWrap } } onClicked: { - callbacks["LogEvent"](JSON.stringify({ - type: "PlaySkillSound", - name: name, - general: detailGeneralCard.name, - i: idx, - })); + const general = generalDetail.general + const extension = lcall("GetGeneralData", general).extension; + Backend.playSound("./packages/" + extension + "/audio/death/" + + general); } } } - - Button { - id: audioDeath - Layout.fillWidth: true - contentItem: ColumnLayout { - Text { - Layout.fillWidth: true - text: Backend.translate("Death audio") - font.bold: true - font.pixelSize: 14 - } - Text { - Layout.fillWidth: true - text: Backend.translate("~" + generalDetail.general) == "~" + generalDetail.general ? "" : Backend.translate("~" + generalDetail.general) - wrapMode: Text.WordWrap - } - } - - onClicked: { - const general = generalDetail.general - const extension = JSON.parse(Backend.callLuaFunction("GetGeneralData", [general])).extension; - Backend.playSound("./packages/" + extension + "/audio/death/" + general); - } - } - } - } - Rectangle { - id: searcher - width: parent.width - height: childrenRect.height - color: "snow" - opacity: 0.75 - anchors.top: parent.bottom - radius: 8 - - RowLayout { - width: parent.width - TextField { - id: word - Layout.fillWidth: true - clip: true - leftPadding: 5 - rightPadding: 5 - } - - Button { - text: Backend.translate("Search") - enabled: word.text !== "" - onClicked: { - listView.currentIndex = 0; - vanishAnim.start(); - } - } - } - } - } - - ColumnLayout { - anchors.right: parent.right - Button { - text: Backend.translate("Quit") - onClicked: { - mainStack.pop(); - config.saveConf(); - } - } - - Button { - id: banButton - text: Backend.translate(config.disabledGenerals.includes(detailGeneralCard.name) ? 'ResumeGeneral' : 'BanGeneral') - visible: detailGeneralCard.name - onClicked: { - const { disabledGenerals } = config; - const { name } = detailGeneralCard; - - if (banButton.text === Backend.translate('ResumeGeneral')) { - const deleteIndex = disabledGenerals.findIndex((general) => general === name); - if (deleteIndex === -1) { - return; - } - - disabledGenerals.splice(deleteIndex, 1); - } else { - if (disabledGenerals.includes(name)) { - return; - } - - disabledGenerals.push(name); - } - config.disabledGeneralsChanged(); - } - } - - Timer { - id: opTimer - interval: 4000 - } - - Button { - text: Backend.translate("Set as Avatar") - enabled: detailGeneralCard.name !== "" && !opTimer.running && Self.avatar !== detailGeneralCard.name - onClicked: { - mainWindow.busy = true; - opTimer.start(); - ClientInstance.notifyServer( - "UpdateAvatar", - JSON.stringify([detailGeneralCard.name]) - ); } } } function loadPackages() { if (loaded) return; - const packs = JSON.parse(Backend.callLuaFunction("GetAllGeneralPack", [])); - packs.forEach(name => { - if (!config.serverHiddenPacks.includes(name)) { - packages.append({ name: name }); - } + const _mods = lcall("GetAllModNames") + const modData = lcall("GetAllMods") + const packs = lcall("GetAllGeneralPack"); + _mods.forEach(name => { + const pkgs = modData[name].filter(p => packs.includes(p) + && !config.serverHiddenPacks.includes(p)); + if (pkgs.length > 0) + mods.append({ name: name, pkgs: JSON.stringify(pkgs) }); }); - generalDetail.updateGeneral(); loaded = true; } } diff --git a/Fk/Pages/Init.qml b/Fk/Pages/Init.qml index 88eb55b9..ea636a9e 100644 --- a/Fk/Pages/Init.qml +++ b/Fk/Pages/Init.qml @@ -118,7 +118,8 @@ Item { TapHandler { onTapped: { - mainStack.push(Qt.createComponent("../Tutorial.qml").createObject()); + mainStack.push(Qt.createComponent("../Tutorial.qml") + .createObject()); } } } diff --git a/Fk/Pages/JoinServer.qml b/Fk/Pages/JoinServer.qml index 70414668..4a716f2b 100644 --- a/Fk/Pages/JoinServer.qml +++ b/Fk/Pages/JoinServer.qml @@ -204,7 +204,8 @@ Item { Layout.fillWidth: true placeholderText: qsTr("Password") text: "" - echoMode: showPasswordCheck.checked ? TextInput.Normal : TextInput.Password + echoMode: showPasswordCheck.checked ? TextInput.Normal + : TextInput.Password passwordCharacter: "*" } @@ -215,10 +216,12 @@ Item { Button { Layout.fillWidth: true - enabled: serverAddrEdit.text !== "" && screenNameEdit.text !== "" && passwordEdit.text !== "" + enabled: serverAddrEdit.text !== "" && screenNameEdit.text !== "" + && passwordEdit.text !== "" text: "OK" onClicked: { - root.addNewServer(serverAddrEdit.text, screenNameEdit.text, passwordEdit.text); + root.addNewServer(serverAddrEdit.text, screenNameEdit.text, + passwordEdit.text); finished(); } } @@ -259,7 +262,8 @@ Item { Layout.fillWidth: true placeholderText: qsTr("Password") text: "" - echoMode: showPasswordCheck.checked ? TextInput.Normal : TextInput.Password + echoMode: showPasswordCheck.checked ? TextInput.Normal + : TextInput.Password passwordCharacter: "*" } @@ -371,7 +375,11 @@ Item { const item = serverModel.get(i); const ip = item.serverIP; if (addr.endsWith(ip)) { // endsWith是为了应付IPv6格式的ip - item.misMatchMsg = FkVersion === ver ? "" : qsTr("@VersionMismatch").arg(ver); + item.misMatchMsg = ""; + if (FkVersion !== ver) { + item.misMatchMsg = qsTr("@VersionMismatch").arg(ver); + } + item.description = desc; item.favicon = icon; item.online = count.toString(); diff --git a/Fk/Pages/Lobby.qml b/Fk/Pages/Lobby.qml index 9e73489c..8da9162c 100644 --- a/Fk/Pages/Lobby.qml +++ b/Fk/Pages/Lobby.qml @@ -41,7 +41,7 @@ Item { width: parent.width wrapMode: TextEdit.WordWrap textFormat: Text.MarkdownText - text: config.serverMotd + "\n___\n" + Backend.translate('Bulletin Info') + text: config.serverMotd + "\n___\n" + luatr('Bulletin Info') } } } @@ -64,13 +64,23 @@ Item { Text { horizontalAlignment: Text.AlignLeft Layout.fillWidth: true - text: (hasPassword ? Backend.translate("Has Password") : "") + roomName + text: roomName font.pixelSize: 20 elide: Label.ElideRight } + Item { + Layout.preferredWidth: 16 + Image { + source: AppPath + "/image/button/skill/locked.png" + visible: hasPassword + anchors.centerIn: parent + scale: 0.8 + } + } + Text { - text: Backend.translate(gameMode) + text: luatr(gameMode) } Text { @@ -81,8 +91,8 @@ Item { } Button { - text: (playerNum < capacity) ? Backend.translate("Enter") : - Backend.translate("Observe") + text: (playerNum < capacity) ? luatr("Enter") : + luatr("Observe") enabled: !opTimer.running @@ -124,7 +134,7 @@ Item { height: root.height - 80 Button { Layout.alignment: Qt.AlignRight - text: Backend.translate("Refresh Room List") + text: luatr("Refresh Room List") enabled: !opTimer.running onClicked: { opTimer.start(); @@ -142,7 +152,7 @@ Item { Text { width: parent.width horizontalAlignment: Text.AlignHCenter - text: Backend.translate("Room List").arg(roomModel.count) + text: luatr("Room List").arg(roomModel.count) } ListView { id: roomList @@ -166,9 +176,10 @@ Item { width: 120 display: AbstractButton.TextUnderIcon icon.name: "media-playback-start" - text: Backend.translate("Create Room") + text: luatr("Create Room") onClicked: { - lobby_dialog.sourceComponent = Qt.createComponent("../LobbyElement/CreateRoom.qml"); + lobby_dialog.sourceComponent = + Qt.createComponent("../LobbyElement/CreateRoom.qml"); lobby_drawer.open(); config.observing = false; config.replaying = false; @@ -180,33 +191,33 @@ Item { anchors.right: parent.right anchors.bottom: parent.bottom Button { - text: Backend.translate("Generals Overview") + text: luatr("Generals Overview") onClicked: { mainStack.push(mainWindow.generalsOverviewPage); mainStack.currentItem.loadPackages(); } } Button { - text: Backend.translate("Cards Overview") + text: luatr("Cards Overview") onClicked: { mainStack.push(mainWindow.cardsOverviewPage); mainStack.currentItem.loadPackages(); } } Button { - text: Backend.translate("Scenarios Overview") + text: luatr("Scenarios Overview") onClicked: { mainStack.push(mainWindow.modesOverviewPage); } } Button { - text: Backend.translate("Replay") + text: luatr("Replay") onClicked: { mainStack.push(mainWindow.replayPage); } } Button { - text: Backend.translate("About") + text: luatr("About") onClicked: { mainStack.push(mainWindow.aboutPage); } @@ -216,7 +227,7 @@ Item { Button { id: exitButton anchors.right: parent.right - text: Backend.translate("Exit Lobby") + text: luatr("Exit Lobby") display: AbstractButton.TextBesideIcon icon.name: "application-exit" onClicked: { @@ -269,7 +280,7 @@ Item { anchors.margins: 16 Text { - text: Backend.translate("Please input room's password") + text: luatr("Please input room's password") } TextField { @@ -295,7 +306,7 @@ Item { config.replaying = false; if (playerNum < capacity) { config.observing = false; - Backend.callLuaFunction("SetObserving", [false]); + lcall("SetObserving", false); mainWindow.busy = true; ClientInstance.notifyServer( "EnterRoom", @@ -303,7 +314,7 @@ Item { ); } else { config.observing = true; - Backend.callLuaFunction("SetObserving", [true]); + lcall("SetObserving", true); mainWindow.busy = true; ClientInstance.notifyServer( "ObserveRoom", @@ -334,7 +345,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter x: 4; y: 2 font.pixelSize: 16 - text: Backend.translate("$OnlineInfo") + text: luatr("$OnlineInfo") .arg(lobbyPlayerNum).arg(serverPlayerNum) + "\n" + "Powered by FreeKill " + FkVersion } @@ -357,8 +368,10 @@ Item { function addToChat(pid, raw, msg) { if (raw.type !== 1) return; - msg = msg.replace(/\{emoji([0-9]+)\}/g, ''); - raw.msg = raw.msg.replace(/\{emoji([0-9]+)\}/g, ''); + msg = msg.replace(/\{emoji([0-9]+)\}/g, + ''); + raw.msg = raw.msg.replace(/\{emoji([0-9]+)\}/g, + ''); lobbyChat.append(msg); danmaku.sendLog("" + raw.userName + ": " + raw.msg); } @@ -369,6 +382,6 @@ Item { } Component.onCompleted: { - toast.show(Backend.translate("$WelcomeToLobby")); + toast.show(luatr("$WelcomeToLobby")); } } diff --git a/Fk/Pages/ModesOverview.qml b/Fk/Pages/ModesOverview.qml index 6001b4f9..d97c84a5 100644 --- a/Fk/Pages/ModesOverview.qml +++ b/Fk/Pages/ModesOverview.qml @@ -51,7 +51,7 @@ Item { id: modeDesc width: parent.width - 16 wrapMode: Text.WordWrap - text: Backend.translate(":" + modeList.get(listView.currentIndex).orig_name) + text: luatr(":" + modeList.get(listView.currentIndex).orig_name) textFormat: Text.MarkdownText font.pixelSize: 16 } @@ -60,7 +60,7 @@ Item { } Button { - text: Backend.translate("Quit") + text: luatr("Quit") anchors.bottom: parent.bottom onClicked: { mainStack.pop(); @@ -68,7 +68,7 @@ Item { } Component.onCompleted: { - const mode_data = JSON.parse(Backend.callLuaFunction("GetGameModes", [])); + const mode_data = lcall("GetGameModes"); for (let d of mode_data) { modeList.append(d); } diff --git a/Fk/Pages/Replay.qml b/Fk/Pages/Replay.qml index 17261b65..daa26b89 100644 --- a/Fk/Pages/Replay.qml +++ b/Fk/Pages/Replay.qml @@ -20,7 +20,7 @@ Item { onClicked: mainStack.pop(); } Label { - text: Backend.translate("Replay Manager") + text: luatr("Replay Manager") horizontalAlignment: Qt.AlignHCenter Layout.fillWidth: true } @@ -75,8 +75,9 @@ Item { Text { text: { const win = winner.split("+").indexOf(role) !== -1; - const winStr = win ? Backend.translate("Game Win") : Backend.translate("Game Lose"); - return "" + Backend.translate(_general) + " " + Backend.translate(role) + " " + winStr; + const winStr = win ? luatr("Game Win") : luatr("Game Lose"); + return "" + luatr(_general) + " " + luatr(role) + + " " + winStr; } font.pixelSize: 20 textFormat: Text.RichText @@ -89,16 +90,16 @@ Item { const h = repDate.slice(8,10); const m = repDate.slice(10,12); const s = repDate.slice(12,14); - const dateStr = y + "-" + month + "-" + d + " " + h + ":" + m + ":" + s; + const dateStr = `${y}-${month}-${d} ${h}:${m}:${s}`; - return playerName + " " + Backend.translate(gameMode) + " " + dateStr + return playerName + " " + luatr(gameMode) + " " + dateStr } } } Button { id: replayBtn - text: Backend.translate("Play the Replay") + text: luatr("Play the Replay") anchors.right: delBtn.left anchors.rightMargin: 8 onClicked: { @@ -110,7 +111,7 @@ Item { Button { id: delBtn - text: Backend.translate("Delete Replay") + text: luatr("Delete Replay") anchors.right: parent.right anchors.rightMargin: 8 onClicked: { diff --git a/Fk/Pages/Room.qml b/Fk/Pages/Room.qml index 501e23bc..0e02cd1f 100644 --- a/Fk/Pages/Room.qml +++ b/Fk/Pages/Room.qml @@ -73,6 +73,7 @@ Item { onIsStartedChanged: { if (isStarted) { + Backend.playSound("./audio/system/gamestart"); bgm.play(); canKickOwner = false; kickOwnerTimer.stop(); @@ -87,7 +88,7 @@ Item { anchors.topMargin: 12 anchors.right: parent.right anchors.rightMargin: 12 - text: Backend.translate("Menu") + text: luatr("Menu") onClicked: { if (menuContainer.visible){ menuContainer.close(); @@ -103,7 +104,7 @@ Item { MenuItem { id: quitButton - text: Backend.translate("Quit") + text: luatr("Quit") onClicked: { if (config.replaying) { Backend.controlReplayer("shutdown"); @@ -119,14 +120,18 @@ Item { MenuItem { id: surrenderButton enabled: !config.observing && !config.replaying - text: Backend.translate("Surrender") + text: luatr("Surrender") onClicked: { - if (isStarted && !getPhoto(Self.id).dead) { - const surrenderCheck = JSON.parse(Backend.callLuaFunction('CheckSurrenderAvailable', [miscStatus.playedTime])); + const photo = getPhoto(Self.id); + if (isStarted && !(photo.dead && photo.rest <= 0)) { + const surrenderCheck = lcall('CheckSurrenderAvailable', miscStatus.playedTime); if (!surrenderCheck.length) { - surrenderDialog.informativeText = Backend.translate('Surrender is disabled in this mode'); + surrenderDialog.informativeText = + luatr('Surrender is disabled in this mode'); } else { - surrenderDialog.informativeText = surrenderCheck.map(str => `${Backend.translate(str.text)}(${str.passed ? '√' : '×'})`).join('
'); + surrenderDialog.informativeText = surrenderCheck + .map(str => `${luatr(str.text)}(${str.passed ? '√' : '×'})`) + .join('
'); } surrenderDialog.open(); } @@ -135,7 +140,7 @@ Item { MenuItem { id: volumeButton - text: Backend.translate("Audio Settings") + text: luatr("Audio Settings") onClicked: { volumeDialog.open(); } @@ -144,7 +149,7 @@ Item { } Button { - text: Backend.translate("Add Robot") + text: luatr("Add Robot") visible: isOwner && !isStarted && !isFull anchors.centerIn: parent enabled: config.serverEnableBot @@ -153,7 +158,7 @@ Item { } } Button { - text: Backend.translate("Start Game") + text: luatr("Start Game") visible: isOwner && !isStarted && isFull enabled: isAllReady anchors.centerIn: parent @@ -166,7 +171,7 @@ Item { interval: 1000 } Button { - text: isReady ? Backend.translate("Cancel Ready") : Backend.translate("Ready") + text: isReady ? luatr("Cancel Ready") : luatr("Ready") visible: !isOwner && !isStarted enabled: !opTimer.running anchors.centerIn: parent @@ -205,6 +210,7 @@ Item { canKickOwner = false; kickOwnerTimer.stop(); } else { + Backend.playSound("./audio/system/ready"); kickOwnerTimer.start(); } } @@ -237,25 +243,25 @@ Item { width: parent.width wrapMode: TextEdit.WordWrap Component.onCompleted: { - const data = JSON.parse(Backend.callLuaFunction("GetRoomConfig", [])); - let cardpack = JSON.parse(Backend.callLuaFunction("GetAllCardPack", [])); + const data = lcall("GetRoomConfig"); + let cardpack = lcall("GetAllCardPack"); cardpack = cardpack.filter(p => !data.disabledPack.includes(p)); - text = Backend.translate("GameMode") + Backend.translate(data.gameMode) + "
" - + Backend.translate("LuckCardNum") + "" + data.luckTime + "
" - + Backend.translate("ResponseTime") + "" + config.roomTimeout + "
" - + Backend.translate("GeneralBoxNum") + "" + data.generalNum + "" - + (data.enableFreeAssign ? "
" + Backend.translate("IncludeFreeAssign") : "") - + (data.enableDeputy ? " " + Backend.translate("IncludeDeputy") : "") - + '
' + Backend.translate('CardPackages') + cardpack.map(e => { - let ret = Backend.translate(e); - if (ret.search(/特殊牌|衍生牌/) === -1) { // TODO: 这种东西最好还是变量名规范化= = + text = luatr("GameMode") + luatr(data.gameMode) + "
" + + luatr("LuckCardNum") + "" + data.luckTime + "
" + + luatr("ResponseTime") + "" + config.roomTimeout + "
" + + luatr("GeneralBoxNum") + "" + data.generalNum + "" + + (data.enableFreeAssign ? "
" + luatr("IncludeFreeAssign") + : "") + + (data.enableDeputy ? " " + luatr("IncludeDeputy") : "") + + '
' + luatr('CardPackages') + cardpack.map(e => { + let ret = luatr(e); + // TODO: 这种东西最好还是变量名规范化= = + if (ret.search(/特殊牌|衍生牌/) === -1) { ret = "" + ret + ""; } return ret; - }).join(',') - //+ '
禁包:' + data.disabledPack.map(e => Backend.translate(e)).join(',') - //+ '
禁将:' + data.disabledGenerals.map(e => Backend.translate(e)).join(',') + }).join(','); } } } @@ -436,13 +442,14 @@ Item { anchors.leftMargin: 8 ColumnLayout { MetroButton { - text: Backend.translate("Choose one handcard") + text: luatr("Choose one handcard") textFont.pixelSize: 28 visible: { if (dashboard.handcardArea.length <= 15) { return false; } - if (roomScene.state == "notactive" || roomScene.state == "replying") { + if (roomScene.state === "notactive" + || roomScene.state === "replying") { return false; } return true; @@ -450,21 +457,21 @@ Item { onClicked: roomScene.startCheat("../RoomElement/ChooseHandcard"); } MetroButton { - text: Backend.translate("Revert Selection") + text: luatr("Revert Selection") textFont.pixelSize: 28 enabled: dashboard.pending_skill !== "" onClicked: dashboard.revertSelection(); } // MetroButton { - // text: Backend.translate("Trust") + // text: luatr("Trust") // } MetroButton { - text: Backend.translate("Sort Cards") + text: luatr("Sort Cards") textFont.pixelSize: 28 onClicked: Logic.resortHandcards(); } MetroButton { - text: Backend.translate("Chat") + text: luatr("Chat") textFont.pixelSize: 28 onClicked: roomDrawer.open(); } @@ -479,21 +486,21 @@ Item { onCardSelected: function(card) { Logic.enableTargets(card); - if (typeof card === "number" && card !== -1 && roomScene.state === "playing" - && JSON.parse(Backend.callLuaFunction("GetPlayerHandcards", [Self.id])).includes(card)) { + if (typeof card === "number" && card !== -1 + && roomScene.state === "playing" + && lcall("GetPlayerHandcards", Self.id).includes(card)) { - const skills = JSON.parse(Backend.callLuaFunction("GetCardSpecialSkills", [card])); - if (JSON.parse(Backend.callLuaFunction("CanUseCard", [card, Self.id]))) { + const skills = lcall("GetCardSpecialSkills", card); + if (lcall("CanUseCard", card, Self.id, + JSON.stringify(roomScene.extra_data))) { skills.unshift("_normal_use"); } specialCardSkills.model = skills; - const skillName = Backend.callLuaFunction("GetCardSkill", [card]); - const prompt = JSON.parse(Backend.callLuaFunction( - "ActiveSkillPrompt", - [skillName, card, selected_targets] - )); + const skillName = lcall("GetCardSkill", card); + const prompt = lcall("ActiveSkillPrompt", skillName, card, + selected_targets); if (prompt !== "") { - roomScene.setPrompt(processPrompt(prompt)); + roomScene.setPrompt(Util.processPrompt(prompt)); } } else { specialCardSkills.model = []; @@ -524,18 +531,19 @@ Item { const totalMin = Math.floor(replayerDuration / 60); const totalSec = replayerDuration % 60; - return elapsedMin.toString() + ":" + elapsedSec + "/" + totalMin + ":" + totalSec; + return elapsedMin.toString() + ":" + elapsedSec + "/" + totalMin + + ":" + totalSec; } } Switch { - text: Backend.translate("Speed Resume") + text: luatr("Speed Resume") checked: false onCheckedChanged: Backend.controlReplayer("uniform"); } Button { - text: Backend.translate("Speed Down") + text: luatr("Speed Down") onClicked: Backend.controlReplayer("slowdown"); } @@ -546,13 +554,13 @@ Item { } Button { - text: Backend.translate("Speed Up") + text: luatr("Speed Up") onClicked: Backend.controlReplayer("speedup"); } Button { property bool running: true - text: Backend.translate(running ? "Pause" : "Resume") + text: luatr(running ? "Pause" : "Resume") onClicked: { running = !running; Backend.controlReplayer("toggle"); @@ -657,7 +665,7 @@ Item { id: specialCardSkills RadioButton { property string orig_text: modelData - text: Backend.translate(modelData) + text: luatr(modelData) checked: index === 0 onCheckedChanged: { roomScene.resetPrompt(); @@ -665,23 +673,19 @@ Item { let prompt = "" if (modelData === "_normal_use") { Logic.enableTargets(card); - const skillName = Backend.callLuaFunction("GetCardSkill", [card]); - prompt = JSON.parse(Backend.callLuaFunction( - "ActiveSkillPrompt", - [skillName, card, selected_targets] - )); + const skillName = lcall("GetCardSkill", card); + prompt = lcall("ActiveSkillPrompt", skillName, card, + selected_targets); } else { Logic.enableTargets(JSON.stringify({ skill: modelData, subcards: [card], })); - prompt = JSON.parse(Backend.callLuaFunction( - "ActiveSkillPrompt", - [modelData, card, selected_targets] - )); + prompt = lcall("ActiveSkillPrompt", modelData, card, + selected_targets); } if (prompt !== "") { - roomScene.setPrompt(processPrompt(prompt)); + roomScene.setPrompt(Util.processPrompt(prompt)); } } } @@ -707,8 +711,9 @@ Item { Button { id: skipNullificationButton - text: Backend.translate("SkipNullification") - visible: !!extra_data.useEventId && !skippedUseEventId.find(id => id === extra_data.useEventId) + text: luatr("SkipNullification") + visible: !!extra_data.useEventId + && !skippedUseEventId.find(id => id === extra_data.useEventId) onClicked: { skippedUseEventId.push(extra_data.useEventId); Logic.doCancelButton(); @@ -717,20 +722,20 @@ Item { Button { id: okButton - text: Backend.translate("OK") + text: luatr("OK") onClicked: Logic.doOkButton(); } Button { id: cancelButton - text: Backend.translate("Cancel") + text: luatr("Cancel") onClicked: Logic.doCancelButton(); } } Button { id: endPhaseButton - text: Backend.translate("End") + text: luatr("End") anchors.bottom: parent.bottom anchors.bottomMargin: 40 anchors.right: parent.right @@ -793,12 +798,13 @@ Item { function activateSkill(skill_name, pressed) { if (pressed) { - const data = JSON.parse(Backend.callLuaFunction("GetInteractionOfSkill", [skill_name])); + const data = lcall("GetInteractionOfSkill", skill_name); if (data) { - Backend.callLuaFunction("SetInteractionDataOfSkill", [skill_name, "null"]); + lcall("SetInteractionDataOfSkill", skill_name, "null"); switch (data.type) { case "combo": - skillInteraction.sourceComponent = Qt.createComponent("../SkillInteraction/SkillCombo.qml"); + skillInteraction.sourceComponent = + Qt.createComponent("../SkillInteraction/SkillCombo.qml"); skillInteraction.item.skill = skill_name; skillInteraction.item.default_choice = data["default"]; skillInteraction.item.choices = data.choices; @@ -807,11 +813,18 @@ Item { // skillInteraction.item.clicked(); break; case "spin": - skillInteraction.sourceComponent = Qt.createComponent("../SkillInteraction/SkillSpin.qml"); + skillInteraction.sourceComponent = + Qt.createComponent("../SkillInteraction/SkillSpin.qml"); skillInteraction.item.skill = skill_name; skillInteraction.item.from = data.from; skillInteraction.item.to = data.to; break; + case "custom": + skillInteraction.sourceComponent = + Qt.createComponent(AppPath + "/" + data.qml_path + ".qml"); + skillInteraction.item.skill = skill_name; + skillInteraction.item.extra_data = data; + break; default: skillInteraction.sourceComponent = undefined; break; @@ -866,11 +879,11 @@ Item { width: roomDrawer.width TabButton { width: roomDrawer.width / 2 - text: Backend.translate("Log") + text: luatr("Log") } TabButton { width: roomDrawer.width / 2 - text: Backend.translate("Chat") + text: luatr("Chat") } } } @@ -913,8 +926,8 @@ Item { MessageDialog { id: quitDialog - title: Backend.translate("Quit") - informativeText: Backend.translate("Are you sure to quit?") + title: luatr("Quit") + informativeText: luatr("Are you sure to quit?") buttons: MessageDialog.Ok | MessageDialog.Cancel onButtonClicked: function (button) { switch (button) { @@ -931,14 +944,17 @@ Item { MessageDialog { id: surrenderDialog - title: Backend.translate("Surrender") + title: luatr("Surrender") informativeText: '' buttons: MessageDialog.Ok | MessageDialog.Cancel onButtonClicked: function (button, role) { switch (button) { case MessageDialog.Ok: { - const surrenderCheck = JSON.parse(Backend.callLuaFunction('CheckSurrenderAvailable', [miscStatus.playedTime])); - if (surrenderCheck.length && !surrenderCheck.find(check => !check.passed)) { + const surrenderCheck = + lcall('CheckSurrenderAvailable', miscStatus.playedTime); + if (surrenderCheck.length && + !surrenderCheck.find(check => !check.passed)) { + ClientInstance.notifyServer("PushRequest", [ "surrender", true ]); @@ -977,7 +993,7 @@ Item { GlowText { anchors.centerIn: dashboard visible: Logic.getPhoto(Self.id).rest > 0 && !config.observing - text: Backend.translate("Resting, don't leave!") + text: luatr("Resting, don't leave!") color: "#DBCC69" font.family: fontLibian.name font.pixelSize: 28 @@ -991,60 +1007,13 @@ Item { color: "transparent" GlowText { anchors.centerIn: parent - text: Backend.translate("Observing ...") + text: luatr("Observing ...") color: "#4B83CD" font.family: fontLi2.name font.pixelSize: 48 } } - /* 这东西似乎一直不好使啊 - Rectangle { - id: easyChat - width: parent.width - height: 28 - anchors.bottom: parent.bottom - visible: false - color: "#040403" - radius: 3 - border.width: 1 - border.color: "#A6967A" - - TextInput { - id: easyChatEdit - anchors.fill: parent - anchors.margins: 6 - color: "white" - clip: true - font.pixelSize: 14 - - onAccepted: { - if (text != "") { - ClientInstance.notifyServer( - "Chat", - JSON.stringify({ - type: 0, - msg: text - }) - ); - text = ""; - easyChat.visible = false; - easyChatEdit.enabled = false; - } - } - } - } - - Shortcut { - sequence: "T" - onActivated: { - easyChat.visible = true; - easyChatEdit.enabled = true; - easyChatEdit.forceActiveFocus(); - } - } - */ - MiscStatus { id: miscStatus anchors.right: menuButton.left @@ -1077,10 +1046,9 @@ Item { } Shortcut { - sequence: "Esc" + sequence: "T" onActivated: { - easyChat.visible = false; - easyChatEdit.enabled = false; + roomDrawer.open(); } } @@ -1096,20 +1064,9 @@ Item { onActivated: Logic.doCancelButton(); } - function processPrompt(prompt) { - const data = prompt.split(":"); - let raw = Backend.translate(data[0]); - const src = parseInt(data[1]); - const dest = parseInt(data[2]); - if (raw.match("%src")) raw = raw.replace(/%src/g, Backend.translate(getPhoto(src).general)); - if (raw.match("%dest")) raw = raw.replace(/%dest/g, Backend.translate(getPhoto(dest).general)); - if (raw.match("%arg2")) raw = raw.replace(/%arg2/g, Backend.translate(data[4])); - if (raw.match("%arg")) raw = raw.replace(/%arg/g, Backend.translate(data[3])); - return raw; - } - function getCurrentCardUseMethod() { - if (specialCardSkills.count === 1 && specialCardSkills.model[0] !== "_normal_use") { + if (specialCardSkills.count === 1 + && specialCardSkills.model[0] !== "_normal_use") { return specialCardSkills.model[0]; } @@ -1125,8 +1082,10 @@ Item { function addToChat(pid, raw, msg) { if (raw.type === 1) return; - msg = msg.replace(/\{emoji([0-9]+)\}/g, ''); - raw.msg = raw.msg.replace(/\{emoji([0-9]+)\}/g, ''); + msg = msg.replace(/\{emoji([0-9]+)\}/g, + ''); + raw.msg = raw.msg.replace(/\{emoji([0-9]+)\}/g, + ''); if (raw.msg.startsWith("$")) { if (specialChat(pid, raw, raw.msg.slice(1))) return; @@ -1149,7 +1108,7 @@ Item { const time = data.time; const userName = data.userName; - const general = Backend.translate(data.general); + const general = luatr(data.general); if (msg.startsWith("!")) { const splited = msg.split(":"); @@ -1167,10 +1126,15 @@ Item { // return false; const fromItem = Logic.getPhotoOrDashboard(fromId); - const fromPos = mapFromItem(fromItem, fromItem.width / 2, fromItem.height / 2); + const fromPos = mapFromItem(fromItem, fromItem.width / 2, + fromItem.height / 2); const toItem = Logic.getPhoto(toId); - const toPos = mapFromItem(toItem, toItem.width / 2, toItem.height / 2); - const egg = component.createObject(roomScene, { start: fromPos, end: toPos }); + const toPos = mapFromItem(toItem, toItem.width / 2, + toItem.height / 2); + const egg = component.createObject(roomScene, { + start: fromPos, + end: toPos + }); egg.finished.connect(() => egg.destroy()); egg.running = true; @@ -1181,11 +1145,11 @@ Item { } } else if (msg.startsWith("~")) { const g = msg.slice(1); - const extension = JSON.parse(Backend.callLuaFunction("GetGeneralData", [g])).extension; + const extension = lcall("GetGeneralData", g).extension; if (!config.disableMsgAudio) Backend.playSound("./packages/" + extension + "/audio/death/" + g); - const m = Backend.translate("~" + g); + const m = luatr("~" + g); data.msg = m; if (general === "") chat.append(`[${time}] ${userName}: ${m}`, data); @@ -1207,15 +1171,17 @@ Item { const idx = parseInt(splited[1]); const gene = splited[2]; - try { - callbacks["LogEvent"](JSON.stringify({ - type: "PlaySkillSound", - name: skill, - general: gene, - i: idx, - })); - } catch (e) {} - const m = Backend.translate("$" + skill + (gene ? "_" + gene : "") + (idx ? idx.toString() : "")); + if (!config.disableMsgAudio) + try { + callbacks["LogEvent"](JSON.stringify({ + type: "PlaySkillSound", + name: skill, + general: gene, + i: idx, + })); + } catch (e) {} + const m = luatr("$" + skill + (gene ? "_" + gene : "") + + (idx ? idx.toString() : "")); data.msg = m; if (general === "") chat.append(`[${time}] ${userName}: ${m}`, data); @@ -1253,8 +1219,7 @@ Item { for (let i = 0; i < photoModel.count; i++) { const item = photos.itemAt(i); if (show) { - const dis = Backend.callLuaFunction("DistanceTo",[Self.id, item.playerid]); - item.distance = parseInt(dis); + item.distance = lcall("DistanceTo", Self.id, item.playerid); } else { item.distance = -1; } @@ -1273,7 +1238,7 @@ Item { const item = photoModel.get(i); let gameData; try { - gameData = JSON.parse(Backend.callLuaFunction("GetPlayerGameData", [item.id])); + gameData = lcall("GetPlayerGameData", item.id); } catch (e) { console.log(e); gameData = [0, 0, 0, 0]; @@ -1290,7 +1255,7 @@ Item { } } mainStack.pop(); - Backend.callLuaFunction("ResetClientLua", []); + lcall("ResetClientLua"); mainStack.push(room); mainStack.currentItem.loadPlayerData(datalist); } @@ -1306,12 +1271,13 @@ Item { function loadPlayerData(datalist) { datalist.forEach(d => { - if (d.id == Self.id) { + if (d.id === Self.id) { roomScene.isOwner = d.isOwner; } else { - Backend.callLuaFunction("ResetAddPlayer", [JSON.stringify([d.id, d.name, d.avatar, d.ready, d.gameData[3]])]); + lcall("ResetAddPlayer", + JSON.stringify([d.id, d.name, d.avatar, d.ready, d.gameData[3]])); } - Backend.callLuaFunction("SetPlayerGameData", [d.id, d.gameData]); + lcall("SetPlayerGameData", d.id, d.gameData); Logic.getPhotoModel(d.id).isOwner = d.isOwner; }); } @@ -1321,7 +1287,7 @@ Item { } Component.onCompleted: { - toast.show(Backend.translate("$EnterRoom")); + toast.show(luatr("$EnterRoom")); playerNum = config.roomCapacity; for (let i = 0; i < playerNum; i++) { diff --git a/Fk/Pages/RoomLogic.js b/Fk/Pages/RoomLogic.js index aaebc51b..46fb9508 100644 --- a/Fk/Pages/RoomLogic.js +++ b/Fk/Pages/RoomLogic.js @@ -28,11 +28,13 @@ function arrangeManyPhotos() { const roomAreaPadding = -16; let horizontalSpacing = 8; - let photoWidth = (roomArea.width - horizontalSpacing * playerNum) / (playerNum - 1); + let photoWidth = (roomArea.width - horizontalSpacing * playerNum) + / (playerNum - 1); let photoScale = 0.75; if (photoWidth > photoMaxWidth) { photoWidth = photoMaxWidth; - horizontalSpacing = (roomArea.width - photoWidth * (playerNum - 1)) / playerNum; + horizontalSpacing = (roomArea.width - photoWidth * (playerNum - 1)) + / playerNum; } else { photoScale = photoWidth / photoBaseWidth; } @@ -134,14 +136,13 @@ function arrangePhotos() { function doOkButton() { if (roomScene.state === "playing" || roomScene.state === "responding") { - const reply = JSON.stringify( - { - card: dashboard.getSelectedCard(), - targets: selected_targets, - special_skill: roomScene.getCurrentCardUseMethod(), - interaction_data: roomScene.skillInteraction.item ? roomScene.skillInteraction.item.answer : undefined, - } - ); + const reply = JSON.stringify({ + card: dashboard.getSelectedCard(), + targets: selected_targets, + special_skill: roomScene.getCurrentCardUseMethod(), + interaction_data: roomScene.skillInteraction.item ? + roomScene.skillInteraction.item.answer : undefined, + }); replyToServer(reply); return; } @@ -244,7 +245,8 @@ function getPhotoOrDashboard(id) { function getAreaItem(area, id) { if (area === Card.DrawPile) { return drawPile; - } else if (area === Card.DiscardPile || area === Card.Processing || area === Card.Void) { + } else if (area === Card.DiscardPile || area === Card.Processing || + area === Card.Void) { return tablePile; } else if (area === Card.AG) { return popupBox.item; @@ -372,7 +374,8 @@ function setEmotion(id, emotion, isCardId) { } } - const animation = component.createObject(photo, {source: (OS === "Win" ? "file:///" : "") + path}); + const animation = component.createObject(photo, + { source: (OS === "Win" ? "file:///" : "") + path }); animation.anchors.centerIn = photo; if (isCardId) { animation.started.connect(() => photo.busy = true); @@ -452,7 +455,8 @@ function doIndicate(from, tos) { return; const fromItem = getPhotoOrDashboard(from); - const fromPos = mapFromItem(fromItem, fromItem.width / 2, fromItem.height / 2); + const fromPos = mapFromItem(fromItem, fromItem.width / 2, + fromItem.height / 2); const end = []; for (let i = 0; i < tos.length; i++) { @@ -464,11 +468,31 @@ function doIndicate(from, tos) { } const color = "#96943D"; - const line = component.createObject(roomScene, {start: fromPos, end: end, color: color}); + const line = component.createObject(roomScene, { + start: fromPos, + end: end, + color: color + }); line.finished.connect(() => line.destroy()); line.running = true; } +function processPrompt(prompt) { + const data = prompt.split(":"); + let raw = luatr(data[0]); + const src = parseInt(data[1]); + const dest = parseInt(data[2]); + if (raw.match("%src")) + raw = raw.replace(/%src/g, luatr(getPhoto(src).general)); + if (raw.match("%dest")) + raw = raw.replace(/%dest/g, luatr(getPhoto(dest).general)); + if (raw.match("%arg2")) + raw = raw.replace(/%arg2/g, luatr(data[4])); + if (raw.match("%arg")) + raw = raw.replace(/%arg/g, luatr(data[3])); + return raw; +} + callbacks["MaxCard"] = (jsonData) => { const data = JSON.parse(jsonData); const id = data.id; @@ -480,7 +504,7 @@ callbacks["MaxCard"] = (jsonData) => { } function changeSelf(id) { - Backend.callLuaFunction("ChangeSelf", [id]); + lcall("ChangeSelf", id); // move new selfPhoto to dashboard let order = new Array(photoModel.count); @@ -533,15 +557,15 @@ callbacks["AddPlayer"] = (jsonData) => { } } -function enableTargets(card) { // card: int | { skill: string, subcards: int[] } +// card: int | { skill: string, subcards: int[] } +function enableTargets(card) { if (roomScene.respond_play) { - const candidate = (!isNaN(card) && card !== -1) || typeof(card) === "string"; + const candidate = (!isNaN(card) && card !== -1) + || typeof(card) === "string"; if (candidate) { - okButton.enabled = JSON.parse(Backend.callLuaFunction( - "CardFitPattern", - [card, roomScene.responding_card] - )) && !JSON.parse(Backend.callLuaFunction( - "CardProhibitedResponse", [card])); + okButton.enabled = + lcall("CardFitPattern", card, roomScene.responding_card) && + !lcall("CardProhibitedResponse", card); } else { okButton.enabled = false; } @@ -568,10 +592,8 @@ function enableTargets(card) { // card: int | { skill: string, subcards: int[] } all_photos.forEach(photo => { photo.state = "candidate"; const id = photo.playerid; - const ret = JSON.parse(Backend.callLuaFunction( - "CanUseCardToTarget", - [card, id, selected_targets] - )); + const ret = lcall("CanUseCardToTarget", card, id, selected_targets, + JSON.stringify(roomScene.extra_data)); photo.selectable = ret; if (roomScene.extra_data instanceof Object) { const must = roomScene.extra_data.must_targets; @@ -588,22 +610,20 @@ function enableTargets(card) { // card: int | { skill: string, subcards: int[] } if (included instanceof Array) { if (included.filter((val) => { return selected_targets.indexOf(val) !== -1; - }).length === 0 && included.indexOf(id) === -1) photo.selectable = false; + }).length === 0 && included.indexOf(id) === -1) + photo.selectable = false; } } }) - okButton.enabled = JSON.parse(Backend.callLuaFunction( - "CardFeasible", [card, selected_targets] - )); + okButton.enabled = lcall("CardFeasible", card, selected_targets); if (okButton.enabled && roomScene.state === "responding") { - okButton.enabled = JSON.parse(Backend.callLuaFunction( - "CardFitPattern", - [card, roomScene.responding_card] - )) && (roomScene.autoPending || !JSON.parse(Backend.callLuaFunction( - "CardProhibitedUse", [card]))); + okButton.enabled = + lcall("CardFitPattern", card, roomScene.responding_card) && + (roomScene.autoPending || !lcall("CardProhibitedUse", card)); } else if (okButton.enabled && roomScene.state === "playing") { - okButton.enabled = JSON.parse(Backend.callLuaFunction("CanUseCard", [card, Self.id])); + okButton.enabled = lcall("CanUseCard", card, Self.id, + JSON.stringify(roomScene.extra_data)); } if (okButton.enabled) { if (roomScene.extra_data instanceof Object) { @@ -646,10 +666,8 @@ function updateSelectedTargets(playerid, selected) { all_photos.forEach(photo => { if (photo.selected) return; const id = photo.playerid; - const ret = JSON.parse(Backend.callLuaFunction( - "CanUseCardToTarget", - [card, id, selected_targets] - )); + const ret = lcall("CanUseCardToTarget", card, id, selected_targets, + JSON.stringify(roomScene.extra_data)); photo.selectable = ret; if (roomScene.extra_data instanceof Object) { const must = roomScene.extra_data.must_targets; @@ -666,22 +684,20 @@ function updateSelectedTargets(playerid, selected) { if (included instanceof Array) { if (included.filter((val) => { return selected_targets.indexOf(val) !== -1; - }).length === 0 && included.indexOf(id) === -1) photo.selectable = false; + }).length === 0 && included.indexOf(id) === -1) + photo.selectable = false; } } }) - okButton.enabled = JSON.parse(Backend.callLuaFunction( - "CardFeasible", [card, selected_targets] - )); + okButton.enabled = lcall("CardFeasible", card, selected_targets); if (okButton.enabled && roomScene.state === "responding") { - okButton.enabled = JSON.parse(Backend.callLuaFunction( - "CardFitPattern", - [card, roomScene.responding_card] - )) && (roomScene.autoPending || !JSON.parse(Backend.callLuaFunction( - "CardProhibitedUse", [card]))); + okButton.enabled = + lcall("CardFitPattern", card, roomScene.responding_card) && + (roomScene.autoPending || !lcall("CardProhibitedUse", card)); } else if (okButton.enabled && roomScene.state === "playing") { - okButton.enabled = JSON.parse(Backend.callLuaFunction("CanUseCard", [card, Self.id])); + okButton.enabled = lcall("CanUseCard", card, Self.id, + JSON.stringify(roomScene.extra_data)); } if (okButton.enabled) { if (roomScene.extra_data instanceof Object) { @@ -819,8 +835,7 @@ callbacks["UpdateCard"] = (j) => { return; } - const data = JSON.parse(Backend.callLuaFunction("GetCardData", [id])); - card.setData(data); + card.setData(lcall("GetCardData", id)); } callbacks["StartGame"] = (jsonData) => { @@ -878,8 +893,8 @@ callbacks["MoveFocus"] = (jsonData) => { if (focuses.indexOf(model.id) != -1) { item = photos.itemAt(i); item.progressBar.visible = true; - item.progressTip = Backend.translate(command) - + Backend.translate(" thinking..."); + item.progressTip = luatr(command) + + luatr(" thinking..."); /* if (command === "PlayCard") { @@ -914,9 +929,10 @@ callbacks["AskForGeneral"] = (jsonData) => { const n = data[1]; const convert = data[2]; const heg = data[3]; - roomScene.setPrompt(Backend.translate("#AskForGeneral"), true); + roomScene.setPrompt(luatr("#AskForGeneral"), true); roomScene.state = "replying"; - roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/ChooseGeneralBox.qml"); + roomScene.popupBox.sourceComponent = + Qt.createComponent("../RoomElement/ChooseGeneralBox.qml"); const box = roomScene.popupBox.item; box.accepted.connect(() => { replyToServer(JSON.stringify(box.choices)); @@ -934,8 +950,8 @@ callbacks["AskForSkillInvoke"] = (jsonData) => { const data = JSON.parse(jsonData); const skill = data[0]; const prompt = data[1]; - roomScene.promptText = prompt ? processPrompt(prompt) : Backend.translate("#AskForSkillInvoke") - .arg(Backend.translate(skill)); + roomScene.promptText = prompt ? processPrompt(prompt) + : luatr("#AskForSkillInvoke").arg(luatr(skill)); roomScene.state = "replying"; roomScene.okCancel.visible = true; roomScene.okButton.enabled = true; @@ -953,26 +969,24 @@ callbacks["AskForGuanxing"] = (jsonData) => { const bottom_area_name = data.bottom_area_name; const prompt = data.prompt; roomScene.state = "replying"; - roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/GuanxingBox.qml"); - data.cards.forEach(id => { - const d = Backend.callLuaFunction("GetCardData", [id]); - cards.push(JSON.parse(d)); - }); + roomScene.popupBox.sourceComponent = + Qt.createComponent("../RoomElement/GuanxingBox.qml"); + data.cards.forEach(id => cards.push(lcall("GetCardData", id))); const box = roomScene.popupBox.item; box.prompt = prompt; if (max_top_cards === 0) { box.areaCapacities = [max_bottom_cards]; box.areaLimits = [min_bottom_cards]; - box.areaNames = [Backend.translate(bottom_area_name)]; + box.areaNames = [luatr(bottom_area_name)]; } else { if (max_bottom_cards === 0) { box.areaCapacities = [max_top_cards]; box.areaLimits = [min_top_cards]; - box.areaNames = [Backend.translate(top_area_name)]; + box.areaNames = [luatr(top_area_name)]; } else { box.areaCapacities = [max_top_cards, max_bottom_cards]; box.areaLimits = [min_top_cards, min_bottom_cards]; - box.areaNames = [Backend.translate(top_area_name), Backend.translate(bottom_area_name)]; + box.areaNames = [luatr(top_area_name), luatr(bottom_area_name)]; } } box.cards = cards; @@ -989,18 +1003,16 @@ callbacks["AskForExchange"] = (jsonData) => { const capacities = []; const limits = []; roomScene.state = "replying"; - roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/GuanxingBox.qml"); + roomScene.popupBox.sourceComponent = + Qt.createComponent("../RoomElement/GuanxingBox.qml"); let for_i = 0; const box = roomScene.popupBox.item; data.piles.forEach(ids => { if (ids.length > 0) { - ids.forEach(id => { - const d = Backend.callLuaFunction("GetCardData", [id]); - cards.push(JSON.parse(d)); - }); + ids.forEach(id => cards.push(lcall("GetCardData", id))); capacities.push(ids.length); limits.push(0); - cards_name.push(Backend.translate(data.piles_name[for_i])); + cards_name.push(luatr(data.piles_name[for_i])); for_i ++; } }); @@ -1024,8 +1036,8 @@ callbacks["AskForChoice"] = (jsonData) => { const prompt = data[3]; const detailed = data[4]; if (prompt === "") { - roomScene.promptText = Backend.translate("#AskForChoice") - .arg(Backend.translate(skill_name)); + roomScene.promptText = luatr("#AskForChoice") + .arg(luatr(skill_name)); } else { roomScene.setPrompt(processPrompt(prompt), true); } @@ -1059,8 +1071,8 @@ callbacks["AskForChoices"] = (jsonData) => { const prompt = data[5]; const detailed = data[6]; if (prompt === "") { - roomScene.promptText = Backend.translate("#AskForChoices") - .arg(Backend.translate(skill_name)); + roomScene.promptText = luatr("#AskForChoices") + .arg(luatr(skill_name)); } else { roomScene.setPrompt(processPrompt(prompt), true); } @@ -1095,13 +1107,14 @@ callbacks["AskForCardChosen"] = (jsonData) => { const reason = data._reason; const prompt = data._prompt; if (prompt === "") { - roomScene.promptText = Backend.translate("#AskForChooseCard") - .arg(Backend.translate(reason)); + roomScene.promptText = luatr(processPrompt("#AskForChooseCard:" + data._id)) + .arg(luatr(reason)); } else { roomScene.setPrompt(processPrompt(prompt), true); } roomScene.state = "replying"; - roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/PlayerCardBox.qml"); + roomScene.popupBox.sourceComponent = + Qt.createComponent("../RoomElement/PlayerCardBox.qml"); const box = roomScene.popupBox.item; box.prompt = prompt; @@ -1109,17 +1122,12 @@ callbacks["AskForCardChosen"] = (jsonData) => { const arr = []; const ids = d[1]; - ids.forEach(id => { - const card_data = JSON.parse(Backend.callLuaFunction("GetCardData", [id])); - arr.push(card_data); - }); + ids.forEach(id => arr.push(lcall("GetCardData", id))); box.addCustomCards(d[0], arr); } roomScene.popupBox.moveToCenter(); - box.cardSelected.connect(function(cid){ - replyToServer(cid); - }); + box.cardSelected.connect(cid => replyToServer(cid)); } callbacks["AskForCardsChosen"] = (jsonData) => { @@ -1131,14 +1139,15 @@ callbacks["AskForCardsChosen"] = (jsonData) => { const reason = data._reason; const prompt = data._prompt; if (prompt === "") { - roomScene.promptText = Backend.translate("#AskForChooseCards") - .arg(Backend.translate(reason)).arg(min).arg(max); + roomScene.promptText = luatr(processPrompt("#AskForChooseCards:" + data._id)) + .arg(luatr(reason)).arg(min).arg(max); } else { roomScene.setPrompt(processPrompt(prompt), true); } roomScene.state = "replying"; - roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/PlayerCardBox.qml"); + roomScene.popupBox.sourceComponent = + Qt.createComponent("../RoomElement/PlayerCardBox.qml"); const box = roomScene.popupBox.item; box.multiChoose = true; box.min = min; @@ -1148,10 +1157,7 @@ callbacks["AskForCardsChosen"] = (jsonData) => { const arr = []; const ids = d[1]; - ids.forEach(id => { - const card_data = JSON.parse(Backend.callLuaFunction("GetCardData", [id])); - arr.push(card_data); - }); + ids.forEach(id => arr.push(lcall("GetCardData", id))); box.addCustomCards(d[0], arr); } @@ -1165,7 +1171,8 @@ callbacks["AskForPoxi"] = (jsonData) => { const { type, data, extra_data, cancelable } = JSON.parse(jsonData); roomScene.state = "replying"; - roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/PoxiBox.qml"); + roomScene.popupBox.sourceComponent = + Qt.createComponent("../RoomElement/PoxiBox.qml"); const box = roomScene.popupBox.item; box.poxi_type = type; box.card_data = data; @@ -1175,10 +1182,7 @@ callbacks["AskForPoxi"] = (jsonData) => { const arr = []; const ids = d[1]; - ids.forEach(id => { - const card_data = JSON.parse(Backend.callLuaFunction("GetCardData", [id])); - arr.push(card_data); - }); + ids.forEach(id => arr.push(lcall("GetCardData", id))); box.addCustomCards(d[0], arr); } @@ -1193,13 +1197,14 @@ callbacks["AskForMoveCardInBoard"] = (jsonData) => { const { cards, cardsPosition, generalNames, playerIds } = data; roomScene.state = "replying"; - roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/MoveCardInBoardBox.qml"); + roomScene.popupBox.sourceComponent = + Qt.createComponent("../RoomElement/MoveCardInBoardBox.qml"); const boxCards = []; cards.forEach(id => { const cardPos = cardsPosition[cards.findIndex(cid => cid === id)]; - const d = Backend.callLuaFunction("GetCardData", [id, playerIds[cardPos]]); - boxCards.push(JSON.parse(d)); + const d = lcall("GetCardData", id, playerIds[cardPos]); + boxCards.push(d); }); const box = roomScene.popupBox.item; @@ -1208,7 +1213,10 @@ callbacks["AskForMoveCardInBoard"] = (jsonData) => { box.playerIds = playerIds; box.generalNames = generalNames.map(name => { const namesSplited = name.split('/'); - return namesSplited.length > 1 ? namesSplited.map(nameSplited => Backend.translate(nameSplited)).join('/') : Backend.translate(name) + if (namesSplited.length > 1) { + return namesSplited.map(nameSplited => luatr(nameSplited)).join('/'); + } + return luatr(name); }); box.arrangeCards(); @@ -1227,7 +1235,7 @@ callbacks["PlayCard"] = (jsonData) => { // jsonData: int playerId const playerId = parseInt(jsonData); if (playerId === Self.id) { - roomScene.setPrompt(Backend.translate("#PlayCard"), true); + roomScene.setPrompt(luatr("#PlayCard"), true); roomScene.state = "playing"; okButton.enabled = false; } @@ -1263,19 +1271,6 @@ callbacks["PrelightSkill"] = (jsonData) => { dashboard.prelightSkill(skill_name, prelight); } -// prompt: 'string::::' -function processPrompt(prompt) { - const data = prompt.split(":"); - let raw = Backend.translate(data[0]); - const src = parseInt(data[1]); - const dest = parseInt(data[2]); - if (raw.match("%src")) raw = raw.replace(/%src/g, Backend.translate(getPhoto(src).general)); - if (raw.match("%dest")) raw = raw.replace(/%dest/g, Backend.translate(getPhoto(dest).general)); - if (raw.match("%arg2")) raw = raw.replace(/%arg2/g, Backend.translate(data[4])); - if (raw.match("%arg")) raw = raw.replace(/%arg/g, Backend.translate(data[3])); - return raw; -} - callbacks["AskForUseActiveSkill"] = (jsonData) => { // jsonData: string skill_name, string prompt const data = JSON.parse(jsonData); @@ -1284,8 +1279,8 @@ callbacks["AskForUseActiveSkill"] = (jsonData) => { const cancelable = data[2]; const extra_data = data[3] ?? {}; if (prompt === "") { - roomScene.promptText = Backend.translate("#AskForUseActiveSkill") - .arg(Backend.translate(skill_name)); + roomScene.promptText = luatr("#AskForUseActiveSkill") + .arg(luatr(skill_name)); } else { roomScene.setPrompt(processPrompt(prompt), true); } @@ -1293,7 +1288,7 @@ callbacks["AskForUseActiveSkill"] = (jsonData) => { roomScene.respond_play = false; roomScene.state = "responding"; - if (JSON.parse(Backend.callLuaFunction('GetSkillData', [skill_name])).isViewAsSkill) { + if (lcall('GetSkillData', skill_name).isViewAsSkill) { roomScene.responding_card = "."; } @@ -1321,7 +1316,8 @@ callbacks["AskForUseCard"] = (jsonData) => { const extra_data = data[4]; const disabledSkillNames = data[5]; if (extra_data != null) { - if (extra_data.effectTo !== Self.id && roomScene.skippedUseEventId.find(id => id === extra_data.useEventId)) { + if (extra_data.effectTo !== Self.id && + roomScene.skippedUseEventId.find(id => id === extra_data.useEventId)) { doCancelButton(); return; } else { @@ -1330,8 +1326,8 @@ callbacks["AskForUseCard"] = (jsonData) => { } if (prompt === "") { - roomScene.promptText = Backend.translate("#AskForUseCard") - .arg(Backend.translate(cardname)); + roomScene.promptText = luatr("#AskForUseCard") + .arg(luatr(cardname)); } else { roomScene.setPrompt(processPrompt(prompt), true); } @@ -1352,8 +1348,8 @@ callbacks["AskForResponseCard"] = (jsonData) => { const disabledSkillNames = data[5]; if (prompt === "") { - roomScene.promptText = Backend.translate("#AskForResponseCard") - .arg(Backend.translate(cardname)); + roomScene.promptText = luatr("#AskForResponseCard") + .arg(luatr(cardname)); } else { roomScene.setPrompt(processPrompt(prompt), true); } @@ -1422,7 +1418,8 @@ callbacks["Animate"] = (jsonData) => { } case "InvokeSkill": { const id = data.player; - const component = Qt.createComponent("../RoomElement/SkillInvokeAnimation.qml"); + const component = + Qt.createComponent("../RoomElement/SkillInvokeAnimation.qml"); if (component.status !== Component.Ready) return; @@ -1432,7 +1429,7 @@ callbacks["Animate"] = (jsonData) => { } const animation = component.createObject(photo, { - skill_name: Backend.translate(data.name), + skill_name: luatr(data.name), skill_type: (data.skill_type ? data.skill_type : "special"), }); animation.anchors.centerIn = photo; @@ -1467,7 +1464,8 @@ callbacks["LogEvent"] = (jsonData) => { setEmotion(data.to, "damage"); item.tremble(); data.damageType = data.damageType || "normal_damage"; - Backend.playSound("./audio/system/" + data.damageType + (data.damageNum > 1 ? "2" : "")); + Backend.playSound("./audio/system/" + data.damageType + + (data.damageNum > 1 ? "2" : "")); break; } case "LoseHP": { @@ -1489,9 +1487,10 @@ callbacks["LogEvent"] = (jsonData) => { // try main general if (data.general) { - dat = JSON.parse(Backend.callLuaFunction("GetGeneralData", [data.general])); + dat = lcall("GetGeneralData", data.general); extension = dat.extension; - path = "./packages/" + extension + "/audio/skill/" + skill + "_" + data.general; + path = "./packages/" + extension + "/audio/skill/" + skill + "_" + + data.general; if (Backend.exists(path + ".mp3") || Backend.exists(path + "1.mp3")) { Backend.playSound(path, data.i); break; @@ -1500,9 +1499,10 @@ callbacks["LogEvent"] = (jsonData) => { // secondly try deputy general if (data.deputy) { - dat = JSON.parse(Backend.callLuaFunction("GetGeneralData", [data.deputy])); + dat = lcall("GetGeneralData", data.deputy); extension = dat.extension; - path = "./packages/" + extension + "/audio/skill/" + skill + "_" + data.deputy; + path = "./packages/" + extension + "/audio/skill/" + skill + "_" + + data.deputy; if (Backend.exists(path + ".mp3") || Backend.exists(path + "1.mp3")) { Backend.playSound(path, data.i); break; @@ -1510,7 +1510,7 @@ callbacks["LogEvent"] = (jsonData) => { } // finally normal skill - dat = JSON.parse(Backend.callLuaFunction("GetSkillData", [skill])); + dat = lcall("GetSkillData", skill); extension = dat.extension; path = "./packages/" + extension + "/audio/skill/" + skill; Backend.playSound(path, data.i); @@ -1522,8 +1522,10 @@ callbacks["LogEvent"] = (jsonData) => { } case "Death": { const item = getPhoto(data.to); - const extension = JSON.parse(Backend.callLuaFunction("GetGeneralData", [item.general])).extension; - Backend.playSound("./packages/" + extension + "/audio/death/" + item.general); + const extension = lcall("GetGeneralData", item.general).extension; + Backend.playSound("./packages/" + extension + "/audio/death/" + + item.general); + break; } default: break; @@ -1532,7 +1534,8 @@ callbacks["LogEvent"] = (jsonData) => { callbacks["GameOver"] = (jsonData) => { roomScene.state = "notactive"; - roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/GameOverBox.qml"); + roomScene.popupBox.sourceComponent = + Qt.createComponent("../RoomElement/GameOverBox.qml"); const box = roomScene.popupBox.item; box.winner = jsonData; // roomScene.isStarted = false; @@ -1541,7 +1544,8 @@ callbacks["GameOver"] = (jsonData) => { callbacks["FillAG"] = (j) => { const data = JSON.parse(j); const ids = data[0]; - roomScene.manualBox.sourceComponent = Qt.createComponent("../RoomElement/AG.qml"); + roomScene.manualBox.sourceComponent = + Qt.createComponent("../RoomElement/AG.qml"); roomScene.manualBox.item.addIds(ids); } @@ -1556,7 +1560,7 @@ callbacks["TakeAG"] = (j) => { const pid = data[0]; const cid = data[1]; const item = getPhoto(pid); - const general = Backend.translate(item.general); + const general = luatr(item.general); // the item should be AG box roomScene.manualBox.item.takeAG(general, cid); @@ -1575,6 +1579,25 @@ callbacks["CustomDialog"] = (j) => { } } +callbacks["MiniGame"] = (j) => { + const data = JSON.parse(j); + const game = data.type; + const dat = data.data; + const gdata = lcall("GetMiniGame", game, Self.id, JSON.stringify(dat)); + roomScene.state = "replying"; + roomScene.popupBox.source = AppPath + "/" + gdata.qml_path + ".qml"; + if (dat) { + roomScene.popupBox.item.loadData(dat); + } +} + +callbacks["UpdateMiniGame"] = (j) => { + const data = JSON.parse(j); + if (roomScene.popupBox.item) { + roomScene.popupBox.item.updateData(data); + } +} + callbacks["UpdateLimitSkill"] = (j) => { const data = JSON.parse(j); const id = data[0]; @@ -1633,7 +1656,7 @@ callbacks["AskForLuckCard"] = (j) => { // jsonData: int time if (config.observing || config.replaying) return; const time = parseInt(j); - roomScene.setPrompt(Backend.translate("#AskForLuckCard").arg(time), true); + roomScene.setPrompt(luatr("#AskForLuckCard").arg(time), true); roomScene.state = "replying"; roomScene.extra_data = { luckCard: true, diff --git a/Fk/PhotoElement/DelayedTrickArea.qml b/Fk/PhotoElement/DelayedTrickArea.qml index 1b09672a..0d468130 100644 --- a/Fk/PhotoElement/DelayedTrickArea.qml +++ b/Fk/PhotoElement/DelayedTrickArea.qml @@ -35,7 +35,7 @@ Item { Image { height: 55 * 0.8 width: 47 * 0.8 - source: SkinBank.getDelayedTrickPicture(name) // SkinBank.DELAYED_TRICK_DIR + name + source: SkinBank.getDelayedTrickPicture(name) } } } @@ -47,7 +47,7 @@ Item { inputs = [inputs]; } inputs.forEach(card => { - const v = JSON.parse(Backend.callLuaFunction("GetVirtualEquip", [parent.playerid, card.cid])); + const v = lcall("GetVirtualEquip", parent.playerid, card.cid); if (v !== null) { cards.append(v); } else { diff --git a/Fk/PhotoElement/EquipArea.qml b/Fk/PhotoElement/EquipArea.qml index 01dd735f..b862ab12 100644 --- a/Fk/PhotoElement/EquipArea.qml +++ b/Fk/PhotoElement/EquipArea.qml @@ -17,9 +17,15 @@ Item { height: 70 width: 138 - property int itemHeight: (treasureItem.name === "" && !treasureItem.sealed) ? height / 3 : height / 4 - property var items: [treasureItem, weaponItem, armorItem, defensiveHorseItem, offensiveHorseItem] - property var subtypes: ["treasure", "weapon", "armor", "defensive_horse", "offensive_horse"] + property int itemHeight: { + if (treasureItem.name === "" && !treasureItem.sealed) + return height / 3; + return height / 4; + } + property var items: [treasureItem, weaponItem, armorItem, + defensiveHorseItem, offensiveHorseItem] + property var subtypes: ["treasure", "weapon", "armor", + "defensive_horse", "offensive_horse"] property int length: area.length // FIXME: Qt 6.6 diff --git a/Fk/PhotoElement/EquipItem.qml b/Fk/PhotoElement/EquipItem.qml index f519c985..b5be6f90 100644 --- a/Fk/PhotoElement/EquipItem.qml +++ b/Fk/PhotoElement/EquipItem.qml @@ -30,7 +30,11 @@ Item { anchors.verticalCenter: parent.verticalCenter x: 3 - source: sealed ? (SkinBank.EQUIP_ICON_DIR + "sealed") : (icon ? SkinBank.getEquipIcon(cid, icon) : "") + source: { + if (sealed) + return SkinBank.EQUIP_ICON_DIR + "sealed"; + return icon ? SkinBank.getEquipIcon(cid, icon) : ""; + } } Image { @@ -44,7 +48,7 @@ Item { GlowText { id: numberItem visible: !sealed && number > 0 && number < 14 - text: Utility.convertNumber(number) + text: Util.convertNumber(number) color: "white" font.family: fontLibian.name font.pixelSize: 16 @@ -132,7 +136,7 @@ Item { text = "-1" icon = "horse"; } else { - text = Backend.translate(name); + text = luatr(name); if (card.virt_name) { icon = card.virt_name; } else { @@ -159,6 +163,6 @@ Item { x = 0; opacity = sealed ? 1 : 0; - text = ' ' + Backend.translate(subtype + "_sealed") + text = ' ' + luatr(subtype + "_sealed") } } diff --git a/Fk/PhotoElement/HpBar.qml b/Fk/PhotoElement/HpBar.qml index d78435b7..a494ec7a 100644 --- a/Fk/PhotoElement/HpBar.qml +++ b/Fk/PhotoElement/HpBar.qml @@ -20,13 +20,24 @@ Column { id: repeater model: column.visible ? 0 : maxValue Magatama { - state: (maxValue - 1 - index) >= value ? 0 : (value >= 3 || value >= maxValue ? 3 : (value <= 0 ? 0 : value)) + state: { + if (maxValue - 1 - index >= value) { + return 0; + } else if (value >= 3 || value >= maxValue) { + return 3; + } else if (value <= 0) { + return 0; + } else { + return value; + } + } } } Column { id: column - visible: maxValue > 4 || value > maxValue || (shieldNum > 0 && maxValue > 3) + visible: maxValue > 4 || value > maxValue || + (shieldNum > 0 && maxValue > 3) spacing: -4 Magatama { @@ -37,7 +48,17 @@ Column { id: hpItem width: root.width text: value - color: root.colors[(value >= 3 || value >= maxValue) ? 3 : (value <= 0 ? 0 : value)] + color: { + let idx; + if (value >= 3 || value >= maxValue) { + idx = 3; + } else if (value <= 0) { + idx = 0; + } else { + idx = value; + } + return root.colors[idx]; + } font.family: fontLibian.name font.pixelSize: 22 font.bold: true diff --git a/Fk/PhotoElement/LimitSkillItem.qml b/Fk/PhotoElement/LimitSkillItem.qml index 9493a0a7..41c7a62e 100644 --- a/Fk/PhotoElement/LimitSkillItem.qml +++ b/Fk/PhotoElement/LimitSkillItem.qml @@ -25,7 +25,7 @@ Item { font.family: fontLi2.name style: Text.Outline styleColor: "#3D2D1C" - text: Backend.translate(skillname); + text: luatr(skillname); } Text { @@ -39,8 +39,7 @@ Item { } onSkillnameChanged: { - let data = Backend.callLuaFunction("GetSkillData", [skillname]); - data = JSON.parse(data); + let data = lcall("GetSkillData", skillname); if (data.frequency || data.switchSkillName) { skilltype = data.switchSkillName ? 'switch' : data.frequency; visible = true; @@ -64,7 +63,8 @@ Item { } } else if (skilltype === 'switch') { visible = true; - bg.source = SkinBank.LIMIT_SKILL_DIR + (usedtimes < 1 ? 'switch' : 'switch-yin'); + bg.source = SkinBank.LIMIT_SKILL_DIR + + (usedtimes < 1 ? 'switch' : 'switch-yin'); } else if (skilltype === 'quest') { visible = true if (usedtimes > 1) { diff --git a/Fk/PhotoElement/MarkArea.qml b/Fk/PhotoElement/MarkArea.qml index 8a1d625a..ba36da59 100644 --- a/Fk/PhotoElement/MarkArea.qml +++ b/Fk/PhotoElement/MarkArea.qml @@ -34,7 +34,8 @@ Item { width: childrenRect.width height: 22 Text { - text: Backend.translate(mark_name) + ' ' + (special_value !== '' ? special_value : mark_extra) + text: luatr(mark_name) + ' ' + + (special_value !== '' ? special_value : mark_extra) font.family: fontLibian.name font.pixelSize: 22 font.letterSpacing: -0.6 @@ -76,15 +77,17 @@ Item { const mark_type = mark_name.slice(2, close_br); const _data = mark_extra; - let data = JSON.parse(Backend.callLuaFunction("GetQmlMark", [mark_type, mark_name, _data, root.parent?.playerid])); + let data = lcall("GetQmlMark", mark_type, mark_name, _data, + root.parent?.playerid); if (data && data.qml_path) { params.data = JSON.parse(_data); + params.owner = root.parent?.playerid; roomScene.startCheat("../../" + data.qml_path, params); } return; } else { if (!root.parent.playerid) return; - let data = JSON.parse(Backend.callLuaFunction("GetPile", [root.parent.playerid, mark_name])); + let data = lcall("GetPile", root.parent.playerid, mark_name); data = data.filter((e) => e !== -1); if (data.length === 0) return; @@ -92,7 +95,7 @@ Item { params.ids = data; } - // Just for using room's right drawer + // Just for using right drawer of the room roomScene.startCheat("../RoomElement/ViewPile", params); } } @@ -123,13 +126,16 @@ Item { if (close_br !== -1) { const mark_type = mark.slice(2, close_br); data = JSON.stringify(data); - const _data = JSON.parse(Backend.callLuaFunction("GetQmlMark", [mark_type, mark, data, root.parent?.playerid])); + const _data = lcall("GetQmlMark", mark_type, mark, data, + root.parent?.playerid); if (_data && _data.text) { special_value = _data.text; } } } else { - data = data instanceof Array ? data.map((markText) => Backend.translate(markText)).join(' ') : Backend.translate(data); + data = data instanceof Array + ? data.map((markText) => luatr(markText)).join(' ') + : luatr(data); } if (modelItem) { diff --git a/Fk/RoomElement/AG.qml b/Fk/RoomElement/AG.qml index 40ac9b7a..ce899e6a 100644 --- a/Fk/RoomElement/AG.qml +++ b/Fk/RoomElement/AG.qml @@ -8,7 +8,7 @@ GraphicsBox { property bool interactive: false id: root - title.text: Backend.translate("Please choose cards") + title.text: luatr("Please choose cards") width: cards.count * 100 + spacing * (cards.count - 1) + 25 height: 180 @@ -46,8 +46,7 @@ GraphicsBox { function addIds(ids) { ids.forEach((id) => { - let data = Backend.callLuaFunction("GetCardData", [id]); - data = JSON.parse(data); + let data = lcall("GetCardData", id); data.selectable = true; data.footnote = ""; cards.append(data); diff --git a/Fk/RoomElement/CardArea.qml b/Fk/RoomElement/CardArea.qml index 7066a40d..f9d39708 100644 --- a/Fk/RoomElement/CardArea.qml +++ b/Fk/RoomElement/CardArea.qml @@ -26,7 +26,7 @@ Item { for (let j = 0; j < outputs.length; j++) { for (let i = cards.length - 1; i >= 0; i--) { if (outputs[j] === cards[i].cid) { - const state = JSON.parse(Backend.callLuaFunction("GetCardData", [cards[i].cid])); + const state = lcall("GetCardData", cards[i].cid); cards[i].setData(state); result.push(cards[i]); cards.splice(i, 1); diff --git a/Fk/RoomElement/CardItem.qml b/Fk/RoomElement/CardItem.qml index bd9cdc9f..e4cd9453 100644 --- a/Fk/RoomElement/CardItem.qml +++ b/Fk/RoomElement/CardItem.qml @@ -51,7 +51,7 @@ Item { property bool selected: false property bool draggable: false property bool autoBack: true - property bool showDetail: false + property bool showDetail: true property int origX: 0 property int origY: 0 property int initialZ: 0 @@ -76,7 +76,7 @@ Item { signal hoverChanged(bool enter) onRightClicked: { - if (!showDetail) return; + if (!showDetail || !known) return; roomScene.startCheat("CardDetail", { card: this }); } @@ -102,7 +102,8 @@ Item { Image { id: suitItem visible: known - source: (suit !== "" && suit !== "nosuit") ? SkinBank.CARD_SUIT_DIR + suit : "" + source: (suit !== "" && suit !== "nosuit") ? SkinBank.CARD_SUIT_DIR + suit + : "" x: 3 y: 19 width: 21 @@ -122,8 +123,10 @@ Item { Image { id: colorItem - visible: known && (suit === "" || suit === "nosuit") // && number <= 0 // <- FIXME: 需要区分“黑色有点数”和“无色有点数” - source: (visible && color !== "") ? SkinBank.CARD_SUIT_DIR + "/" + color : "" + visible: known && (suit === "" || suit === "nosuit") + // && number <= 0 // <- FIXME: 需要区分“黑色有点数”和“无色有点数” + source: (visible && color !== "") ? SkinBank.CARD_SUIT_DIR + "/" + color + : "" x: 1 } @@ -146,7 +149,7 @@ Item { font.pixelSize: 16 font.family: fontLibian.name font.letterSpacing: -0.6 - text: Backend.translate(root.virt_name) + text: luatr(root.virt_name) } Text { @@ -195,7 +198,7 @@ Item { font.family: fontLibian.name font.letterSpacing: -0.6 text: { - let ret = Backend.translate(modelData.k); + let ret = luatr(modelData.k); if (!modelData.k.startsWith("@@")) { ret += modelData.v.toString(); } diff --git a/Fk/RoomElement/CheckBox.qml b/Fk/RoomElement/CheckBox.qml index dcbdc892..61059631 100644 --- a/Fk/RoomElement/CheckBox.qml +++ b/Fk/RoomElement/CheckBox.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts +import Fk import Fk.Pages GraphicsBox { @@ -14,22 +15,10 @@ GraphicsBox { property var result: [] id: root - title.text: Backend.translate("$Choice").arg(Backend.translate(skill_name)) + title.text: luatr("$Choice").arg(luatr(skill_name)) width: Math.max(140, body.width + 20) height: buttons.height + body.height + title.height + 20 - function processPrompt(prompt) { - const data = prompt.split(":"); - let raw = Backend.translate(data[0]); - const src = parseInt(data[1]); - const dest = parseInt(data[2]); - if (raw.match("%src")) raw = raw.replace(/%src/g, Backend.translate(getPhoto(src).general)); - if (raw.match("%dest")) raw = raw.replace(/%dest/g, Backend.translate(getPhoto(dest).general)); - if (raw.match("%arg2")) raw = raw.replace(/%arg2/g, Backend.translate(data[4])); - if (raw.match("%arg")) raw = raw.replace(/%arg/g, Backend.translate(data[3])); - return raw; - } - GridLayout { id: body // x: 10 @@ -43,9 +32,10 @@ GraphicsBox { model: all_options MetroToggleButton { - // Layout.fillWidth: true - text: processPrompt(modelData) - enabled: options.indexOf(modelData) !== -1 && (root.result.length < max_num || triggered) + Layout.fillWidth: true + text: Util.processPrompt(modelData) + enabled: options.indexOf(modelData) !== -1 + && (root.result.length < max_num || triggered) onClicked: { if (triggered) { @@ -68,7 +58,7 @@ GraphicsBox { MetroButton { Layout.fillWidth: true - text: processPrompt("OK") + text: luatr("OK") enabled: root.result.length >= min_num onClicked: { @@ -78,7 +68,7 @@ GraphicsBox { MetroButton { Layout.fillWidth: true - text: processPrompt("Cancel") + text: luatr("Cancel") visible: cancelable onClicked: { diff --git a/Fk/RoomElement/ChoiceBox.qml b/Fk/RoomElement/ChoiceBox.qml index cf0106b6..364e05c1 100644 --- a/Fk/RoomElement/ChoiceBox.qml +++ b/Fk/RoomElement/ChoiceBox.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts +import Fk import Fk.Pages GraphicsBox { @@ -11,22 +12,10 @@ GraphicsBox { property int result id: root - title.text: Backend.translate("$Choice").arg(Backend.translate(skill_name)) + title.text: luatr("$Choice").arg(luatr(skill_name)) width: Math.max(140, body.width + 20) height: body.height + title.height + 20 - function processPrompt(prompt) { - const data = prompt.split(":"); - let raw = Backend.translate(data[0]); - const src = parseInt(data[1]); - const dest = parseInt(data[2]); - if (raw.match("%src")) raw = raw.replace(/%src/g, Backend.translate(getPhoto(src).general)); - if (raw.match("%dest")) raw = raw.replace(/%dest/g, Backend.translate(getPhoto(dest).general)); - if (raw.match("%arg2")) raw = raw.replace(/%arg2/g, Backend.translate(data[4])); - if (raw.match("%arg")) raw = raw.replace(/%arg/g, Backend.translate(data[3])); - return raw; - } - GridLayout { id: body x: 10 @@ -40,7 +29,7 @@ GraphicsBox { MetroButton { Layout.fillWidth: true - text: processPrompt(modelData) + text: Util.processPrompt(modelData) enabled: options.indexOf(modelData) !== -1 onClicked: { diff --git a/Fk/RoomElement/ChooseGeneralBox.qml b/Fk/RoomElement/ChooseGeneralBox.qml index ba5d2a6f..50c6adf8 100644 --- a/Fk/RoomElement/ChooseGeneralBox.qml +++ b/Fk/RoomElement/ChooseGeneralBox.qml @@ -19,10 +19,11 @@ GraphicsBox { } id: root - title.text: Backend.translate("$ChooseGeneral").arg(choiceNum) + - (config.enableFreeAssign ? "(" + Backend.translate("Enable free assign") + ")" : "") + title.text: luatr("$ChooseGeneral").arg(choiceNum) + + (config.enableFreeAssign ? "(" + luatr("Enable free assign") + ")" : "") width: generalArea.width + body.anchors.leftMargin + body.anchors.rightMargin - height: body.implicitHeight + body.anchors.topMargin + body.anchors.bottomMargin + height: body.implicitHeight + body.anchors.topMargin + + body.anchors.bottomMargin Column { id: body @@ -32,7 +33,8 @@ GraphicsBox { Item { id: generalArea - width: (generalList.count > 8 ? Math.ceil(generalList.count / 2) : Math.max(3, generalList.count)) * 97 + width: (generalList.count > 8 ? Math.ceil(generalList.count / 2) + : Math.max(3, generalList.count)) * 97 height: generalList.count > 8 ? 290 : 150 z: 1 @@ -43,8 +45,23 @@ GraphicsBox { Item { width: 93 height: 130 - x: (index % Math.ceil(generalList.count / (generalList.count > 8 ? 2 : 1))) * 98 + (generalList.count > 8 && index > generalList.count / 2 && generalList.count % 2 == 1 ? 50 : 0) - y: generalList.count <= 8 ? 0 : (index < generalList.count / 2 ? 0 : 135) + x: { + const count = generalList.count; + let columns = generalList.count; + if (columns > 8) { + columns = Math.ceil(columns / 2); + } + + let ret = (index % columns) * 98; + if (count > 8 && index > count / 2 && count % 2 == 1) + ret += 50; + return ret; + } + y: { + if (generalList.count <= 8) + return 0; + return index < generalList.count / 2 ? 0 : 135; + } } } } @@ -93,13 +110,15 @@ GraphicsBox { MetroButton { id: convertBtn visible: !convertDisabled - text: Backend.translate("Same General Convert") - onClicked: roomScene.startCheat("SameConvert", { cards: generalList }); + text: luatr("Same General Convert") + onClicked: { + roomScene.startCheat("SameConvert", { cards: generalList }); + } } MetroButton { id: fightButton - text: Backend.translate("Fight") + text: luatr("Fight") width: 120 height: 35 enabled: false @@ -110,7 +129,7 @@ GraphicsBox { MetroButton { id: detailBtn enabled: choices.length > 0 - text: Backend.translate("Show General Detail") + text: luatr("Show General Detail") onClicked: roomScene.startCheat( "GeneralDetail", { generals: choices } @@ -233,7 +252,8 @@ GraphicsBox { for (i = 0; i < generalCardList.count; i++) { item = generalCardList.itemAt(i); - item.selectable = needSameKingdom ? isHegPair(selectedItem[0], item) : true; + item.selectable = needSameKingdom ? isHegPair(selectedItem[0], item) + : true; if (selectedItem.indexOf(item) != -1) continue; @@ -247,9 +267,7 @@ GraphicsBox { } for (let i = 0; i < generalList.count; i++) { - if (JSON.parse(Backend.callLuaFunction( - "GetSameGenerals", [generalList.get(i).name]) - ).length > 0) { + if (lcall("GetSameGenerals", generalList.get(i).name).length > 0) { convertBtn.enabled = true; return; } diff --git a/Fk/RoomElement/ChooseHandcard.qml b/Fk/RoomElement/ChooseHandcard.qml index 45281e18..5a4e9a36 100644 --- a/Fk/RoomElement/ChooseHandcard.qml +++ b/Fk/RoomElement/ChooseHandcard.qml @@ -13,7 +13,7 @@ ColumnLayout { property var cards: [] Text { - text: Backend.translate("Handcard selector") + text: luatr("Handcard selector") Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter font.pixelSize: 18 @@ -46,7 +46,7 @@ ColumnLayout { } } Component.onCompleted: { - setData(JSON.parse(Backend.callLuaFunction("GetCardData", [modelData.cid]))); + setData(lcall("GetCardData", modelData.cid)); } } } diff --git a/Fk/RoomElement/Dashboard.qml b/Fk/RoomElement/Dashboard.qml index 53986a8f..e91a05e0 100644 --- a/Fk/RoomElement/Dashboard.qml +++ b/Fk/RoomElement/Dashboard.qml @@ -3,6 +3,7 @@ import QtQuick import QtQuick.Layouts import Qt5Compat.GraphicalEffects +import Fk RowLayout { id: root @@ -18,6 +19,7 @@ RowLayout { property alias skillButtons: skillPanel.skill_buttons property var expanded_piles: ({}) // name -> int[] + property var extra_cards: [] property var disabledSkillNames: [] @@ -70,7 +72,7 @@ RowLayout { handcardAreaItem.unselectAll(expectId); } - function expandPile(pile) { + function expandPile(pile, extra_ids, extra_footnote) { const expanded_pile_names = Object.keys(expanded_piles); if (expanded_pile_names.indexOf(pile) !== -1) return; @@ -79,30 +81,28 @@ RowLayout { const parentPos = roomScene.mapFromItem(self, 0, 0); expanded_piles[pile] = []; + let ids, footnote; if (pile === "_equip") { - const equips = self.equipArea.getAllCards(); - equips.forEach(data => { - data.x = parentPos.x; - data.y = parentPos.y; - const card = component.createObject(roomScene, data); - card.footnoteVisible = true; - card.footnote = Backend.translate("$Equip"); - handcardAreaItem.add(card); - }) - handcardAreaItem.updateCardPosition(); + ids = self.equipArea.getAllCards().map(e => e.cid); + footnote = "$Equip"; + } else if (pile === "_extra") { + ids = extra_ids; + extra_cards = ids; + footnote = extra_footnote; } else { - const ids = JSON.parse(Backend.callLuaFunction("GetPile", [self.playerid, pile])); - ids.forEach(id => { - const data = JSON.parse(Backend.callLuaFunction("GetCardData", [id])); - data.x = parentPos.x; - data.y = parentPos.y; - const card = component.createObject(roomScene, data); - card.footnoteVisible = true; - card.footnote = Backend.translate(pile); - handcardAreaItem.add(card); - }); - handcardAreaItem.updateCardPosition(); + ids = lcall("GetPile", self.playerid, pile); + footnote = pile; } + ids.forEach(id => { + const data = lcall("GetCardData", id); + data.x = parentPos.x; + data.y = parentPos.y; + const card = component.createObject(roomScene, data); + card.footnoteVisible = true; + card.footnote = luatr(footnote); + handcardAreaItem.add(card); + }); + handcardAreaItem.updateCardPosition(); } function retractPile(pile) { @@ -124,7 +124,13 @@ RowLayout { }) handcardAreaItem.updateCardPosition(); } else { - const ids = JSON.parse(Backend.callLuaFunction("GetPile", [self.playerid, pile])); + let ids = []; + if (pile === "_extra") { + ids = extra_cards; + extra_cards = []; + } else { + ids = lcall("GetPile", self.playerid, pile); + } ids.forEach(id => { const card = handcardAreaItem.remove([id])[0]; card.origX = parentPos.x; @@ -145,25 +151,23 @@ RowLayout { // If cname is set, we are responding card. function enableCards(cname) { const cardValid = (cid, cname) => { - let ret = JSON.parse(Backend.callLuaFunction( - "CardFitPattern", [cid, cname])); + let ret = lcall("CardFitPattern", cid, cname); if (ret) { if (roomScene.respond_play) { - ret = ret && !JSON.parse(Backend.callLuaFunction( - "CardProhibitedResponse", [cid])); + ret = ret && !lcall("CardProhibitedResponse", cid); } else { - ret = ret && !JSON.parse(Backend.callLuaFunction( - "CardProhibitedUse", [cid])); + ret = ret && !lcall("CardProhibitedUse", cid); } } return ret; } - const pile_data = JSON.parse(Backend.callLuaFunction("GetAllPiles", [self.playerid])); + const pile_data = lcall("GetAllPiles", self.playerid); extractWoodenOx(); + const handleMethod = roomScene.respond_play ? "response" : "use"; if (cname) { const ids = []; let cards = handcardAreaItem.cards; @@ -172,10 +176,8 @@ RowLayout { if (cardValid(cards[i].cid, cname)) { ids.push(cards[i].cid); } else { - const prohibitReason = Backend.callLuaFunction( - "GetCardProhibitReason", - [cards[i].cid, roomScene.respond_play ? "response" : "use", cname] - ); + const prohibitReason = lcall("GetCardProhibitReason", cards[i].cid, + handleMethod, cname); if (prohibitReason) { cards[i].prohibitReason = prohibitReason; } @@ -190,10 +192,8 @@ RowLayout { expandPile("_equip"); } } else { - const prohibitReason = Backend.callLuaFunction( - "GetCardProhibitReason", - [c.cid, roomScene.respond_play ? "response" : "use", cname] - ); + const prohibitReason = lcall("GetCardProhibitReason", c.cid, + handleMethod, cname); if (prohibitReason) { c.prohibitReason = prohibitReason; } @@ -223,14 +223,15 @@ RowLayout { const ids = [], cards = handcardAreaItem.cards; for (let i = 0; i < cards.length; i++) { cards[i].prohibitReason = ""; - if (JSON.parse(Backend.callLuaFunction("CanUseCard", [cards[i].cid, Self.id]))) { + if (lcall("CanUseCard", cards[i].cid, Self.id, + JSON.stringify(roomScene.extra_data))) { ids.push(cards[i].cid); } else { // cannot use? considering special_skills - const skills = JSON.parse(Backend.callLuaFunction("GetCardSpecialSkills", [cards[i].cid])); + const skills = lcall("GetCardSpecialSkills", cards[i].cid); for (let j = 0; j < skills.length; j++) { const s = skills[j]; - if (JSON.parse(Backend.callLuaFunction("ActiveCanUse", [s]))) { + if (lcall("ActiveCanUse", s, JSON.stringify(roomScene.extra_data))) { ids.push(cards[i].cid); break; } @@ -238,7 +239,8 @@ RowLayout { // still cannot use? show message on card if (!ids.includes(cards[i].cid)) { - const prohibitReason = Backend.callLuaFunction("GetCardProhibitReason", [cards[i].cid, "play"]); + const prohibitReason = lcall("GetCardProhibitReason", cards[i].cid, + "play"); if (prohibitReason) { cards[i].prohibitReason = prohibitReason; } @@ -304,21 +306,10 @@ RowLayout { } } - function processPrompt(prompt) { - const data = prompt.split(":"); - let raw = Backend.translate(data[0]); - const src = parseInt(data[1]); - const dest = parseInt(data[2]); - if (raw.match("%src")) raw = raw.replace(/%src/g, Backend.translate(getPhoto(src).general)); - if (raw.match("%dest")) raw = raw.replace(/%dest/g, Backend.translate(getPhoto(dest).general)); - if (raw.match("%arg2")) raw = raw.replace(/%arg2/g, Backend.translate(data[4])); - if (raw.match("%arg")) raw = raw.replace(/%arg/g, Backend.translate(data[3])); - return raw; - } - function extractWoodenOx() { - const pile_data = JSON.parse(Backend.callLuaFunction("GetAllPiles", [self.playerid])); - if (!roomScene.autoPending) { // 先屏蔽AskForUseActiveSkill再说,这下只剩使用打出以及出牌阶段了 + const pile_data = lcall("GetAllPiles", self.playerid); + if (!roomScene.autoPending) { + // 先屏蔽AskForUseActiveSkill再说,这下只剩使用打出以及出牌阶段了 for (let name in pile_data) { if (name.endsWith("&")) expandPile(name); } @@ -331,28 +322,21 @@ RowLayout { const enabled_cards = []; const targets = roomScene.selected_targets; - const prompt = JSON.parse(Backend.callLuaFunction( - "ActiveSkillPrompt", - [pending_skill, pendings, targets] - )); + const prompt = lcall("ActiveSkillPrompt", pending_skill, pendings, + targets); if (prompt !== "") { - roomScene.setPrompt(processPrompt(prompt)); + roomScene.setPrompt(Util.processPrompt(prompt)); } handcardAreaItem.cards.forEach((card) => { - if (card.selected || JSON.parse(Backend.callLuaFunction( - "ActiveCardFilter", - [pending_skill, card.cid, pendings, targets] - ))) + if (card.selected || lcall("ActiveCardFilter", pending_skill, card.cid, + pendings, targets)) enabled_cards.push(card.cid); }); const cards = self.equipArea.getAllCards(); cards.forEach(c => { - if (JSON.parse(Backend.callLuaFunction( - "ActiveCardFilter", - [pending_skill, c.cid, pendings, targets] - ))) { + if (lcall("ActiveCardFilter", pending_skill, c.cid, pendings, targets)) { enabled_cards.push(c.cid); if (!expanded_piles["_equip"]) { expandPile("_equip"); @@ -360,26 +344,26 @@ RowLayout { } }) - const pile = Backend.callLuaFunction("GetExpandPileOfSkill", [pending_skill]); - const pile_ids = JSON.parse(Backend.callLuaFunction("GetPile", [self.playerid, pile])); + let pile = lcall("GetExpandPileOfSkill", pending_skill); + let pile_ids = pile; + if (typeof pile === "string") { + pile_ids = lcall("GetPile", self.playerid, pile); + } else { + pile = "_extra"; + } + pile_ids.forEach(cid => { - if (JSON.parse(Backend.callLuaFunction( - "ActiveCardFilter", - [pending_skill, cid, pendings, targets] - ))) { + if (lcall("ActiveCardFilter", pending_skill, cid, pendings, targets)) { enabled_cards.push(cid); }; if (!expanded_piles[pile]) { - expandPile(pile); + expandPile(pile, pile_ids, pending_skill); } }); handcardAreaItem.enableCards(enabled_cards); - if (JSON.parse(Backend.callLuaFunction( - "CanViewAs", - [pending_skill, pendings] - ))) { + if (lcall("CanViewAs", pending_skill, pendings)) { pending_card = { skill: pending_skill, subcards: pendings @@ -462,8 +446,8 @@ RowLayout { continue; } - const fitpattern = JSON.parse(Backend.callLuaFunction("SkillFitPattern", [item.orig, cname])); - const canresp = JSON.parse(Backend.callLuaFunction("SkillCanResponse", [item.orig, cardResponsing])); + const fitpattern = lcall("SkillFitPattern", item.orig, cname); + const canresp = lcall("SkillCanResponse", item.orig, cardResponsing); item.enabled = fitpattern && canresp; } return; @@ -475,7 +459,8 @@ RowLayout { continue; } - item.enabled = JSON.parse(Backend.callLuaFunction("ActiveCanUse", [item.orig])); + item.enabled = lcall("ActiveCanUse", item.orig, + JSON.stringify(roomScene.extra_data)); } } @@ -490,10 +475,9 @@ RowLayout { } function updateHandcards() { - Backend.callLuaFunction("FilterMyHandcards", []); + lcall("FilterMyHandcards"); handcardAreaItem.cards.forEach(v => { - const data = JSON.parse(Backend.callLuaFunction("GetCardData", [v.cid])); - v.setData(data); + v.setData(lcall("GetCardData", v.cid)); }); } @@ -514,12 +498,12 @@ RowLayout { skillPanel.clearSkills(); - const skills = JSON.parse(Backend.callLuaFunction("GetPlayerSkills", [Self.id])); + const skills = lcall("GetPlayerSkills", Self.id); for (let s of skills) { addSkill(s.name); } - cards = roomScene.drawPile.remove(JSON.parse(Backend.callLuaFunction("GetPlayerHandcards", [Self.id]))); + cards = roomScene.drawPile.remove(lcall("GetPlayerHandcards", Self.id)); handcardAreaItem.add(cards); } } diff --git a/Fk/RoomElement/DetailedCheckBox.qml b/Fk/RoomElement/DetailedCheckBox.qml index f9bae3a0..c7c94ad3 100644 --- a/Fk/RoomElement/DetailedCheckBox.qml +++ b/Fk/RoomElement/DetailedCheckBox.qml @@ -15,7 +15,7 @@ GraphicsBox { property var result: [] id: root - title.text: Backend.translate("$Choice").arg(Backend.translate(skill_name)) + title.text: luatr("$Choice").arg(luatr(skill_name)) width: Math.max(140, body.width + 20) height: buttons.height + body.height + title.height + 20 @@ -38,8 +38,10 @@ GraphicsBox { MetroToggleButton { id: choicetitle width: parent.width - text: Backend.translate(modelData) - enabled: options.indexOf(modelData) !== -1 && (root.result.length < max_num || triggered) + text: luatr(modelData) + triggered: root.result.includes(index) + enabled: options.indexOf(modelData) !== -1 + && (root.result.length < max_num || triggered) textFont.pixelSize: 24 anchors.top: choiceDetail.bottom anchors.topMargin: 8 @@ -64,7 +66,7 @@ GraphicsBox { Text { id: detail width: parent.width - text: Backend.translate(":" + modelData) + text: luatr(":" + modelData) color: "white" wrapMode: Text.WordWrap font.pixelSize: 16 @@ -84,7 +86,7 @@ GraphicsBox { MetroButton { width: 120 height: 35 - text: Backend.translate("OK") + text: luatr("OK") enabled: root.result.length >= min_num onClicked: { @@ -95,7 +97,7 @@ GraphicsBox { MetroButton { width: 120 height: 35 - text: Backend.translate("Cancel") + text: luatr("Cancel") visible: root.cancelable onClicked: { diff --git a/Fk/RoomElement/DetailedChoiceBox.qml b/Fk/RoomElement/DetailedChoiceBox.qml index 43c139a2..ddcd9b44 100644 --- a/Fk/RoomElement/DetailedChoiceBox.qml +++ b/Fk/RoomElement/DetailedChoiceBox.qml @@ -12,7 +12,7 @@ GraphicsBox { property int result id: root - title.text: Backend.translate("$Choice").arg(Backend.translate(skill_name)) + title.text: luatr("$Choice").arg(luatr(skill_name)) width: Math.max(140, body.width + 20) height: body.height + title.height + 20 @@ -35,7 +35,7 @@ GraphicsBox { MetroButton { id: choicetitle width: parent.width - text: Backend.translate(modelData) + text: luatr(modelData) enabled: options.indexOf(modelData) !== -1 textFont.pixelSize: 24 anchors.top: choiceDetail.bottom @@ -57,7 +57,7 @@ GraphicsBox { Text { id: detail width: parent.width - text: Backend.translate(":" + modelData) + text: luatr(":" + modelData) color: "white" wrapMode: Text.WordWrap font.pixelSize: 16 diff --git a/Fk/RoomElement/GameOverBox.qml b/Fk/RoomElement/GameOverBox.qml index f5a91bad..6928fd95 100644 --- a/Fk/RoomElement/GameOverBox.qml +++ b/Fk/RoomElement/GameOverBox.qml @@ -7,7 +7,7 @@ GraphicsBox { property string winner: "" id: root - title.text: Backend.translate("$GameOver") + title.text: luatr("$GameOver") width: Math.max(140, body.width + 20) height: body.height + title.height + 20 @@ -18,12 +18,13 @@ GraphicsBox { spacing: 10 Text { - text: winner !== "" ? Backend.translate("$Winner").arg(Backend.translate(winner)) : Backend.translate("$NoWinner") + text: winner !== "" ? luatr("$Winner").arg(luatr(winner)) + : luatr("$NoWinner") color: "#E4D5A0" } MetroButton { - text: Backend.translate("Back To Room") + text: luatr("Back To Room") anchors.horizontalCenter: parent.horizontalCenter visible: !config.observing @@ -34,7 +35,7 @@ GraphicsBox { } MetroButton { - text: Backend.translate("Back To Lobby") + text: luatr("Back To Lobby") anchors.horizontalCenter: parent.horizontalCenter onClicked: { @@ -49,13 +50,13 @@ GraphicsBox { MetroButton { id: repBtn - text: Backend.translate("Save Replay") + text: luatr("Save Replay") anchors.horizontalCenter: parent.horizontalCenter visible: !config.replaying onClicked: { repBtn.visible = false; - Backend.callLuaFunction("SaveRecord", []); + lcall("SaveRecord"); toast.show("OK."); } } diff --git a/Fk/RoomElement/GeneralCardItem.qml b/Fk/RoomElement/GeneralCardItem.qml index 70bac4e5..87332f2a 100644 --- a/Fk/RoomElement/GeneralCardItem.qml +++ b/Fk/RoomElement/GeneralCardItem.qml @@ -29,11 +29,13 @@ CardItem { suit: "" number: 0 footnote: "" - card.source: known ? SkinBank.getGeneralPicture(name) : (SkinBank.GENERALCARD_DIR + 'card-back') + card.source: known ? SkinBank.getGeneralPicture(name) + : (SkinBank.GENERALCARD_DIR + 'card-back') glow.color: "white" //Engine.kingdomColor[kingdom] // FIXME: 藕!! - property bool heg: name.startsWith('hs__') || name.startsWith('ld__') || name.includes('heg__') + property bool heg: name.startsWith('hs__') || name.startsWith('ld__') || + name.includes('heg__') Image { source: known ? (SkinBank.GENERALCARD_DIR + "border") : "" @@ -51,7 +53,8 @@ CardItem { scale: 0.6; x: 9; y: 12 transformOrigin: Item.TopLeft width: 34; fillMode: Image.PreserveAspectFit - source: subkingdom ? SkinBank.getGeneralCardDir(subkingdom) + subkingdom : "" + source: subkingdom ? SkinBank.getGeneralCardDir(subkingdom) + subkingdom + : "" visible: detailed && known } @@ -72,7 +75,8 @@ CardItem { Image { id: subkingdomMagatama visible: false - source: subkingdom ? SkinBank.getGeneralCardDir(subkingdom) + subkingdom + "-magatama" : "" + source: subkingdom ? SkinBank.getGeneralCardDir(subkingdom) + + subkingdom + "-magatama" : "" } LinearGradient { id: subkingdomMask @@ -153,8 +157,8 @@ CardItem { height: 80 x: 2 y: lineCount > 6 ? 30 : 34 - text: name !== "" ? Backend.translate(name) : "nil" - visible: Backend.translate(name).length <= 6 && detailed && known + text: name !== "" ? luatr(name) : "nil" + visible: luatr(name).length <= 6 && detailed && known color: "white" font.family: fontLibian.name font.pixelSize: 18 @@ -168,8 +172,8 @@ CardItem { y: 12 rotation: 90 transformOrigin: Item.BottomLeft - text: Backend.translate(name) - visible: Backend.translate(name).length > 6 && detailed && known + text: luatr(name) + visible: luatr(name).length > 6 && detailed && known color: "white" font.family: fontLibian.name font.pixelSize: 18 @@ -191,7 +195,7 @@ CardItem { border.color: "white" border.width: 1 Text { - text: Backend.translate(pkgName) + text: luatr(pkgName) x: 2; y: 1 font.family: fontLibian.name font.pixelSize: 14 @@ -202,7 +206,7 @@ CardItem { } onNameChanged: { - const data = JSON.parse(Backend.callLuaFunction("GetGeneralData", [name])); + const data = lcall("GetGeneralData", name); kingdom = data.kingdom; subkingdom = (data.subkingdom !== kingdom && data.subkingdom) || ""; hp = data.hp; diff --git a/Fk/RoomElement/GuanxingBox.qml b/Fk/RoomElement/GuanxingBox.qml index b70d34d9..0023ad34 100644 --- a/Fk/RoomElement/GuanxingBox.qml +++ b/Fk/RoomElement/GuanxingBox.qml @@ -14,7 +14,7 @@ GraphicsBox { property var areaNames: [] property int padding: 25 - title.text: Backend.translate(prompt !== "" ? prompt : "Please arrange cards") + title.text: luatr(prompt !== "" ? prompt : "Please arrange cards") width: body.width + padding * 2 height: title.height + body.height + padding * 2 @@ -32,7 +32,8 @@ GraphicsBox { spacing: 5 property int areaCapacity: modelData - property string areaName: index < areaNames.length ? qsTr(areaNames[index]) : "" + property string areaName: index < areaNames.length + ? qsTr(areaNames[index]) : "" Rectangle { anchors.verticalCenter: parent.verticalCenter @@ -81,7 +82,7 @@ GraphicsBox { MetroButton { Layout.alignment: Qt.AlignHCenter id: buttonConfirm - text: Backend.translate("OK") + text: luatr("OK") width: 120 height: 35 diff --git a/Fk/RoomElement/HandcardArea.qml b/Fk/RoomElement/HandcardArea.qml index 2d96bea1..3cd0b53f 100644 --- a/Fk/RoomElement/HandcardArea.qml +++ b/Fk/RoomElement/HandcardArea.qml @@ -34,7 +34,6 @@ Item { card.autoBack = true; card.draggable = true; card.selectable = false; - card.showDetail = true; card.clicked.connect(adjustCards); } @@ -46,7 +45,6 @@ Item { card = result[i]; card.draggable = false; card.selectable = false; - card.showDetail = false; card.selectedChanged.disconnect(adjustCards); card.prohibitReason = ""; } @@ -57,7 +55,7 @@ Item { { let card, i; cards.forEach(card => { - card.selectable = cardIds.contains(card.cid); + card.selectable = cardIds.includes(card.cid); if (!card.selectable) { card.selected = false; unselectCard(card); @@ -93,10 +91,10 @@ Item { for (let i = 0; i < cards.length; i++) { const card = cards[i]; if (card.selected) { - if (!selectedCards.contains(card)) + if (!selectedCards.includes(card)) selectCard(card); } else { - if (selectedCards.contains(card)) + if (selectedCards.includes(card)) unselectCard(card); } } diff --git a/Fk/RoomElement/IndicatorLine.qml b/Fk/RoomElement/IndicatorLine.qml index 00531b70..14ea6511 100644 --- a/Fk/RoomElement/IndicatorLine.qml +++ b/Fk/RoomElement/IndicatorLine.qml @@ -20,7 +20,8 @@ Item { Rectangle { width: 6 - height: Math.sqrt(Math.pow(modelData.x - start.x, 2) + Math.pow(modelData.y - start.y, 2)) * ratio + height: Math.sqrt(Math.pow(modelData.x - start.x, 2) + + Math.pow(modelData.y - start.y, 2)) * ratio x: start.x y: start.y antialiasing: true diff --git a/Fk/RoomElement/InvisibleCardArea.qml b/Fk/RoomElement/InvisibleCardArea.qml index 14e0a570..4d533f77 100644 --- a/Fk/RoomElement/InvisibleCardArea.qml +++ b/Fk/RoomElement/InvisibleCardArea.qml @@ -59,7 +59,7 @@ Item { const items = []; for (let i = 0; i < outputs.length; i++) { if (_contains(outputs[i])) { - const state = JSON.parse(Backend.callLuaFunction("GetCardData", [outputs[i]])) + const state = lcall("GetCardData", outputs[i]); state.x = parentPos.x; state.y = parentPos.y; state.opacity = 0; @@ -93,7 +93,8 @@ Item { const parentPos = roomScene.mapFromItem(root, 0, 0); for (i = 0; i < pendingInput.length; i++) { card = pendingInput[i]; - card.origX = parentPos.x - card.width / 2 + ((i - pendingInput.length / 2) * 15); + card.origX = parentPos.x - card.width / 2 + + ((i - pendingInput.length / 2) * 15); card.origY = parentPos.y - card.height / 2; card.origOpacity = 0; card.destroyOnStop(); diff --git a/Fk/RoomElement/MiscStatus.qml b/Fk/RoomElement/MiscStatus.qml index 00116dbb..9e7e7630 100644 --- a/Fk/RoomElement/MiscStatus.qml +++ b/Fk/RoomElement/MiscStatus.qml @@ -19,7 +19,7 @@ Item { Text { id: roundTxt anchors.right: parent.right - text: Backend.translate("#currentRoundNum").arg(roundNum) + text: luatr("#currentRoundNum").arg(roundNum) color: "#F0E5DA" font.pixelSize: 18 font.family: fontLibian.name diff --git a/Fk/RoomElement/MoveCardInBoardBox.qml b/Fk/RoomElement/MoveCardInBoardBox.qml index f88f3773..eea07571 100644 --- a/Fk/RoomElement/MoveCardInBoardBox.qml +++ b/Fk/RoomElement/MoveCardInBoardBox.qml @@ -13,7 +13,7 @@ GraphicsBox { property var result property int padding: 25 - title.text: Backend.translate("Please click to move card") + title.text: luatr("Please click to move card") width: body.width + padding * 2 height: title.height + body.height + padding * 2 @@ -65,7 +65,7 @@ GraphicsBox { Text { horizontalAlignment: Text.AlignHCenter anchors.centerIn: parent - text: Backend.translate(modelData.subtype) + text: luatr(modelData.subtype) color: "#90765F" font.family: fontLibian.name font.pixelSize: 16 @@ -81,7 +81,7 @@ GraphicsBox { MetroButton { Layout.alignment: Qt.AlignHCenter id: buttonConfirm - text: Backend.translate("OK") + text: luatr("OK") width: 120 height: 35 enabled: false @@ -131,7 +131,8 @@ GraphicsBox { const index = cards.findIndex(data => item.cid === data.cid); result && (result.pos = cardsPosition[index]); - const cardPos = cardsPosition[index] === 0 ? (result ? 1 : 0) : (result ? 0 : 1); + const cardPos = cardsPosition[index] === 0 ? (result ? 1 : 0) + : (result ? 0 : 1); const curArea = areaRepeater.itemAt(cardPos); const curBox = curArea.cardRepeater.itemAt(index); const curPos = mapFromItem(curArea, curBox.x, curBox.y); diff --git a/Fk/RoomElement/Photo.qml b/Fk/RoomElement/Photo.qml index 7c111b94..40bc5c58 100644 --- a/Fk/RoomElement/Photo.qml +++ b/Fk/RoomElement/Photo.qml @@ -179,6 +179,7 @@ Item { Image { id: generalImage width: deputyGeneral ? parent.width / 2 : parent.width + Behavior on width { NumberAnimation { duration: 100 } } height: parent.height smooth: true fillMode: Image.PreserveAspectCrop @@ -187,7 +188,8 @@ Item { return ""; } if (deputyGeneral) { - return SkinBank.getGeneralExtraPic(general, "dual/") ?? SkinBank.getGeneralPicture(general); + return SkinBank.getGeneralExtraPic(general, "dual/") + ?? SkinBank.getGeneralPicture(general); } else { return SkinBank.getGeneralPicture(general) } @@ -204,7 +206,8 @@ Item { source: { const general = deputyGeneral; if (deputyGeneral != "") { - return SkinBank.getGeneralExtraPic(general, "dual/") ?? SkinBank.getGeneralPicture(general); + return SkinBank.getGeneralExtraPic(general, "dual/") + ?? SkinBank.getGeneralPicture(general); } else { return ""; } @@ -231,7 +234,7 @@ Item { color: "white" width: 24 wrapMode: Text.WrapAnywhere - text: Backend.translate(deputyGeneral) + text: luatr(deputyGeneral) style: Text.Outline } } @@ -247,16 +250,18 @@ Item { } OpacityMask { + id: photoMaskEffect anchors.fill: photoMask source: generalImgItem maskSource: photoMask } Colorize { - anchors.fill: photoMask - source: generalImgItem + anchors.fill: photoMaskEffect + source: photoMaskEffect saturation: 0 - visible: root.dead || root.surrendered + opacity: (root.dead || root.surrendered) ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: 300 } } } Rectangle { @@ -266,9 +271,10 @@ Item { height: 222 radius: 8 - visible: root.drank > 0 + // visible: root.drank > 0 color: "red" - opacity: 0.4 + Math.log(root.drank) * 0.12 + opacity: (root.drank <= 0 ? 0 : 0.4) + Math.log(root.drank) * 0.12 + Behavior on opacity { NumberAnimation { duration: 300 } } } ColumnLayout { @@ -279,7 +285,7 @@ Item { GlowText { Layout.alignment: Qt.AlignCenter - text: Backend.translate("resting...") + text: luatr("resting...") font.family: fontLibian.name font.pixelSize: 40 font.bold: true @@ -303,7 +309,7 @@ Item { GlowText { Layout.alignment: Qt.AlignCenter visible: root.rest > 0 && root.rest < 999 - text: Backend.translate("rest round num") + text: luatr("rest round num") font.family: fontLibian.name font.pixelSize: 28 color: "#F0E5D6" @@ -333,11 +339,11 @@ Item { style: Text.Outline text: { if (totalGame === 0) { - return Backend.translate("Newbie"); + return luatr("Newbie"); } const winRate = (winGame / totalGame) * 100; const runRate = (runGame / totalGame) * 100; - return Backend.translate("Win=%1\nRun=%2\nTotal=%3") + return luatr("Win=%1\nRun=%2\nTotal=%3") .arg(winRate.toFixed(2)) .arg(runRate.toFixed(2)) .arg(totalGame); @@ -350,7 +356,8 @@ Item { anchors.right: parent.right anchors.bottomMargin: -8 anchors.rightMargin: 4 - source: SkinBank.PHOTO_DIR + (isOwner ? "owner" : (ready ? "ready" : "notready")) + source: SkinBank.PHOTO_DIR + + (isOwner ? "owner" : (ready ? "ready" : "notready")) visible: screenName != "" && !roomScene.isStarted } @@ -392,7 +399,8 @@ Item { } function updatePileInfo(areaName) { - const data = JSON.parse(Backend.callLuaFunction("GetPile", [root.playerid, areaName])); + if (areaName.startsWith('#')) return; + const data = lcall("GetPile", root.playerid, areaName); if (data.length === 0) { root.markArea.removeMark(areaName); } else { @@ -434,10 +442,12 @@ Item { // id: saveme visible: (root.dead && !root.rest) || root.dying || root.surrendered source: { - if (root.dead) { + if (root.surrendered) { + return SkinBank.DEATH_DIR + "surrender"; + } else if (root.dead) { return SkinBank.getRoleDeathPic(root.role); } - return SkinBank.DEATH_DIR + (root.surrendered ? "surrender" : "saveme") + return SkinBank.DEATH_DIR + "saveme"; } anchors.centerIn: photoMask } @@ -459,7 +469,14 @@ Item { x: -6 Text { - text: (root.maxCard === root.hp || root.hp < 0 ) ? (root.handcards) : (root.handcards + "/" + (root.maxCard < 900 ? root.maxCard : "∞")) + text: { + if (root.maxCard === root.hp || root.hp < 0) { + return root.handcards; + } else { + const maxCard = root.maxCard < 900 ? root.maxCard : "∞"; + return root.handcards + "/" + maxCard; + } + } font.family: fontLibian.name font.pixelSize: (root.maxCard === root.hp || root.hp < 0 ) ? 32 : 27 //font.weight: 30 @@ -471,10 +488,11 @@ Item { } TapHandler { - enabled: (root.state != "candidate" || !root.selectable) && root.playerid !== Self.id + enabled: (root.state != "candidate" || !root.selectable) + && root.playerid !== Self.id onTapped: { const params = { name: "hand_card" }; - let data = JSON.parse(Backend.callLuaFunction("GetPlayerHandcards", [root.playerid])); + let data = lcall("GetPlayerHandcards", root.playerid); data = data.filter((e) => e !== -1); if (data.length === 0) return; @@ -531,7 +549,12 @@ Item { anchors.topMargin: 2 font.pixelSize: 16 - text: (config.blockedUsers && config.blockedUsers.includes(screenName) ? Backend.translate(" ") : "") + screenName + text: { + let ret = screenName; + if (config.blockedUsers?.includes(screenName)) + ret = luatr(" ") + ret; + return ret; + } glow.radius: 8 } @@ -548,7 +571,10 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: -32 - property var seatChr: ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"] + property var seatChr: [ + "一", "二", "三", "四", "五", "六", + "七", "八", "九", "十", "十一", "十二", + ] font.family: fontLi2.name font.pixelSize: 32 text: seatChr[seatNumber - 1] @@ -704,7 +730,7 @@ Item { onGeneralChanged: { if (!roomScene.isStarted) return; - const text = Backend.translate(general); + const text = luatr(general); if (text.length > 6) { generalName.text = ""; longGeneralName.text = text; @@ -712,8 +738,6 @@ Item { generalName.text = text; longGeneralName.text = ""; } - // let data = JSON.parse(Backend.callLuaFunction("GetGeneralData", [general])); - // kingdom = data.kingdom; } function chat(msg) { diff --git a/Fk/RoomElement/PlayerCardBox.qml b/Fk/RoomElement/PlayerCardBox.qml index 1139dd8f..1d4fcfdb 100644 --- a/Fk/RoomElement/PlayerCardBox.qml +++ b/Fk/RoomElement/PlayerCardBox.qml @@ -2,13 +2,18 @@ import QtQuick import QtQuick.Layouts +import Fk import Fk.Pages GraphicsBox { id: root property string prompt - title.text: prompt === "" ? (root.multiChoose ? Backend.translate("$ChooseCards").arg(root.min).arg(root.max) : Backend.translate("$ChooseCard")) : processPrompt(prompt) + title.text: prompt === "" ? + (root.multiChoose ? + luatr("$ChooseCards").arg(root.min).arg(root.max) + : luatr("$ChooseCard")) + : Util.processPrompt(prompt) // TODO: Adjust the UI design in case there are more than 7 cards width: 70 + 700 @@ -50,7 +55,7 @@ GraphicsBox { Text { color: "#E4D5A0" - text: Backend.translate(areaName) + text: luatr(areaName) anchors.fill: parent wrapMode: Text.WrapAnywhere verticalAlignment: Text.AlignVCenter @@ -95,26 +100,15 @@ GraphicsBox { MetroButton { anchors.bottom: parent.bottom - text: Backend.translate("OK") + text: luatr("OK") visible: root.multiChoose - enabled: root.selected_ids.length <= root.max && root.selected_ids.length >= root.min + enabled: root.selected_ids.length <= root.max + && root.selected_ids.length >= root.min onClicked: root.cardsSelected(root.selected_ids) } onCardSelected: finished(); - function processPrompt(prompt) { - const data = prompt.split(":"); - let raw = Backend.translate(data[0]); - const src = parseInt(data[1]); - const dest = parseInt(data[2]); - if (raw.match("%src")) raw = raw.replace(/%src/g, Backend.translate(getPhoto(src).general)); - if (raw.match("%dest")) raw = raw.replace(/%dest/g, Backend.translate(getPhoto(dest).general)); - if (raw.match("%arg2")) raw = raw.replace(/%arg2/g, Backend.translate(data[4])); - if (raw.match("%arg")) raw = raw.replace(/%arg/g, Backend.translate(data[3])); - return raw; - } - function findAreaModel(name) { let ret; for (let i = 0; i < cardModel.count; i++) { diff --git a/Fk/RoomElement/PoxiBox.qml b/Fk/RoomElement/PoxiBox.qml index 3abdd791..27340916 100644 --- a/Fk/RoomElement/PoxiBox.qml +++ b/Fk/RoomElement/PoxiBox.qml @@ -7,7 +7,7 @@ import Fk.Pages GraphicsBox { id: root - title.text: Backend.callLuaFunction("PoxiPrompt", [poxi_type, card_data, extra_data]) + title.text: lcall("PoxiPrompt", poxi_type, card_data, extra_data) // TODO: Adjust the UI design in case there are more than 7 cards width: 70 + 700 @@ -50,7 +50,7 @@ GraphicsBox { Text { color: "#E4D5A0" - text: Backend.translate(areaName) + text: luatr(areaName) anchors.fill: parent wrapMode: Text.WrapAnywhere verticalAlignment: Text.AlignVCenter @@ -71,12 +71,10 @@ GraphicsBox { number: model.number || 0 autoBack: false known: model.cid !== -1 - selectable: { - return root.selected_ids.includes(model.cid) || JSON.parse(Backend.callLuaFunction( - "PoxiFilter", - [root.poxi_type, model.cid, root.selected_ids, root.card_data, root.extra_data] - )); - } + selectable: root.selected_ids.includes(model.cid) || + lcall("PoxiFilter", root.poxi_type, model.cid, root.selected_ids, + root.card_data, root.extra_data); + onSelectedChanged: { if (selected) { chosenInBox = true; @@ -102,20 +100,16 @@ GraphicsBox { MetroButton { width: 120 height: 35 - text: Backend.translate("OK") - enabled: { - return JSON.parse(Backend.callLuaFunction( - "PoxiFeasible", - [root.poxi_type, root.selected_ids, root.card_data, root.extra_data] - )); - } + text: luatr("OK") + enabled: lcall("PoxiFeasible", root.poxi_type, root.selected_ids, + root.card_data, root.extra_data); onClicked: root.cardsSelected(root.selected_ids) } MetroButton { width: 120 height: 35 - text: Backend.translate("Cancel") + text: luatr("Cancel") visible: root.cancelable onClicked: root.cardsSelected([]) } diff --git a/Fk/RoomElement/SkillArea.qml b/Fk/RoomElement/SkillArea.qml index 6aa1f8b7..42c08542 100644 --- a/Fk/RoomElement/SkillArea.qml +++ b/Fk/RoomElement/SkillArea.qml @@ -112,10 +112,7 @@ Flickable { return false; }; - const data = JSON.parse(Backend.callLuaFunction( - "GetSkillData", - [skill_name] - )); + const data = lcall("GetSkillData", skill_name); if (prelight) { if (!modelContains(prelight_skills, data)) diff --git a/Fk/RoomElement/UltSkillAnimation.qml b/Fk/RoomElement/UltSkillAnimation.qml index 9c95489b..04c41fa3 100644 --- a/Fk/RoomElement/UltSkillAnimation.qml +++ b/Fk/RoomElement/UltSkillAnimation.qml @@ -28,12 +28,12 @@ Item { Text { text: { let o = "$" + skillName + "_" + generalName + (index % 2 + 1); - let p = Backend.translate(o); + let p = luatr(o); if (o !== p) { return p; } o = "$" + skillName + (index % 2 + 1); - p = Backend.translate(o); + p = luatr(o); if (o === p) { return "Ultimate Skill Invoked!"; } @@ -59,12 +59,12 @@ Item { Text { text: { let o = "$" + skillName + "_" + generalName + ((index + 1) % 2 + 1); - let p = Backend.translate(o); + let p = luatr(o); if (o !== p) { return p; } o = "$" + skillName + ((index + 1) % 2 + 1); - p = Backend.translate(o); + p = luatr(o); if (o === p) { return "Ultimate Skill Invoked!"; } @@ -90,7 +90,7 @@ Item { Text { topPadding: 5 id: skill - text: Backend.translate(skillName) + text: luatr(skillName) font.family: fontLi2.name font.pixelSize: 40 x: root.width / 2 + 100 diff --git a/Fk/RoomElement/ViewGeneralPile.qml b/Fk/RoomElement/ViewGeneralPile.qml index 46645f41..36e1fd1e 100644 --- a/Fk/RoomElement/ViewGeneralPile.qml +++ b/Fk/RoomElement/ViewGeneralPile.qml @@ -13,7 +13,7 @@ ColumnLayout { Layout.fillWidth: true Layout.preferredHeight: childrenRect.height + 4 - text: Backend.translate(extra_data.name) + text: luatr(extra_data.name) } GridView { diff --git a/Fk/RoomElement/ViewPile.qml b/Fk/RoomElement/ViewPile.qml index 4b0bb269..f44c8610 100644 --- a/Fk/RoomElement/ViewPile.qml +++ b/Fk/RoomElement/ViewPile.qml @@ -13,7 +13,7 @@ ColumnLayout { Layout.fillWidth: true Layout.preferredHeight: childrenRect.height + 4 - text: Backend.translate(extra_data.name) + text: luatr(extra_data.name) } GridView { @@ -32,7 +32,7 @@ ColumnLayout { Component.onCompleted: { let data = {} if (extra_data.ids) { - data = JSON.parse(Backend.callLuaFunction("GetCardData", [modelData])); + data = lcall("GetCardData", modelData); } else { data.cid = 0; data.name = modelData; diff --git a/Fk/SkillInteraction/SkillCombo.qml b/Fk/SkillInteraction/SkillCombo.qml index 97eb8c4a..ccf15719 100644 --- a/Fk/SkillInteraction/SkillCombo.qml +++ b/Fk/SkillInteraction/SkillCombo.qml @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later import QtQuick +import Fk import Fk.Pages MetroButton { @@ -12,34 +13,21 @@ MetroButton { property string answer: default_choice property bool detailed: false - function processPrompt(prompt) { - const data = prompt.split(":"); - let raw = Backend.translate(data[0]); - const src = parseInt(data[1]); - const dest = parseInt(data[2]); - if (raw.match("%src")) raw = raw.replace(/%src/g, Backend.translate(getPhoto(src).general)); - if (raw.match("%dest")) raw = raw.replace(/%dest/g, Backend.translate(getPhoto(dest).general)); - if (raw.match("%arg2")) raw = raw.replace(/%arg2/g, Backend.translate(data[4])); - if (raw.match("%arg")) raw = raw.replace(/%arg/g, Backend.translate(data[3])); - return raw; - } - - text: processPrompt(answer) + text: Util.processPrompt(answer) onAnswerChanged: { if (!answer) return; - Backend.callLuaFunction( - "SetInteractionDataOfSkill", - [skill, JSON.stringify(answer)] - ); + lcall("SetInteractionDataOfSkill", skill, JSON.stringify(answer)); roomScene.dashboard.startPending(skill); } onClicked: { if (detailed) { - roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/DetailedChoiceBox.qml"); + roomScene.popupBox.sourceComponent = + Qt.createComponent("../RoomElement/DetailedChoiceBox.qml"); } else { - roomScene.popupBox.sourceComponent = Qt.createComponent("../RoomElement/ChoiceBox.qml"); + roomScene.popupBox.sourceComponent = + Qt.createComponent("../RoomElement/ChoiceBox.qml"); } const box = roomScene.popupBox.item; box.options = choices; diff --git a/Fk/SkillInteraction/SkillSpin.qml b/Fk/SkillInteraction/SkillSpin.qml index a58368d9..924501fa 100644 --- a/Fk/SkillInteraction/SkillSpin.qml +++ b/Fk/SkillInteraction/SkillSpin.qml @@ -10,11 +10,7 @@ SpinBox { // from, to onValueChanged: { - Backend.callLuaFunction( - "SetInteractionDataOfSkill", - [skill, JSON.stringify(answer)] - ); + lcall("SetInteractionDataOfSkill", skill, JSON.stringify(answer)); roomScene.dashboard.startPending(skill); } - } diff --git a/Fk/Splash.qml b/Fk/Splash.qml index 4d9cb736..c88370f5 100644 --- a/Fk/Splash.qml +++ b/Fk/Splash.qml @@ -89,8 +89,14 @@ Rectangle { id: textAni running: false loops: Animation.Infinite - NumberAnimation { from: 0; to: 1; duration: 1600; easing.type: Easing.InOutQuad; } - NumberAnimation { from: 1; to: 0; duration: 1600; easing.type: Easing.InOutQuad; } + NumberAnimation { + from: 0; to: 1; duration: 1600 + easing.type: Easing.InOutQuad + } + NumberAnimation { + from: 1; to: 0; duration: 1600 + easing.type: Easing.InOutQuad + } } } diff --git a/Fk/Toast.qml b/Fk/Toast.qml index e65ddb09..dee94a74 100644 --- a/Fk/Toast.qml +++ b/Fk/Toast.qml @@ -15,7 +15,8 @@ Rectangle { property real time: defaultTime readonly property real fadeTime: 300 - anchors.horizontalCenter: parent != null ? parent.horizontalCenter : undefined + anchors.horizontalCenter: parent != null ? parent.horizontalCenter + : undefined height: message.height + 20 width: message.width + 40 radius: 16 diff --git a/Fk/ToastManager.qml b/Fk/ToastManager.qml index 3649c42a..78bf6bd3 100644 --- a/Fk/ToastManager.qml +++ b/Fk/ToastManager.qml @@ -2,8 +2,8 @@ import QtQuick -// copy from https://gist.github.com/jonmcclung/bae669101d17b103e94790341301c129 -// and modified some code +// https://gist.github.com/jonmcclung/bae669101d17b103e94790341301c129 +// modified some code ListView { function show(text, duration) { if (duration === undefined) { diff --git a/Fk/main.qml b/Fk/main.qml index 02b4f81b..54a913a8 100644 --- a/Fk/main.qml +++ b/Fk/main.qml @@ -45,7 +45,8 @@ Window { StackView { id: mainStack visible: !mainWindow.busy - // If error occurs during loading initialItem, the program will fall into "polish()" loop + // If error occurs during loading initialItem + // the program will fall into "polish()" loop // initialItem: init anchors.fill: parent } @@ -159,7 +160,10 @@ Window { return; } if (mainWindow.is_pending && command !== "ChangeSelf") { - mainWindow.pending_message.push({ command: command, jsonData: jsonData }); + mainWindow.pending_message.push({ + command: command, + jsonData: jsonData, + }); } else { if (command === "StartChangeSelf") { mainWindow.is_pending = true; @@ -254,4 +258,27 @@ Window { exitMessageDialog.open(); } } + + // fake global functions + function lcall(funcName, ...params) { + const ret = Backend.callLuaFunction(funcName, [...params]); + try { + return JSON.parse(ret); + } catch (e) { + return ret; + } + } + + function leval(lua) { + const ret = Backend.evalLuaExp(`return json.encode(${lua})`); + try { + return JSON.parse(ret); + } catch (e) { + return ret; + } + } + + function luatr(src) { + return Backend.translate(src); + } } diff --git a/Fk/qmldir b/Fk/qmldir index 3a502eb2..452ae67b 100644 --- a/Fk/qmldir +++ b/Fk/qmldir @@ -1,3 +1,3 @@ module Fk SkinBank 1.0 skin-bank.js -Utility 1.0 util.js +Util 1.0 util.js diff --git a/Fk/skin-bank.js b/Fk/skin-bank.js index 74c608d0..ca305c01 100644 --- a/Fk/skin-bank.js +++ b/Fk/skin-bank.js @@ -31,18 +31,20 @@ const searchPkgResource = function(path, name, suffix) { } function getGeneralExtraPic(name, extra) { - const data = JSON.parse(Backend.callLuaFunction("GetGeneralData", [name])); + const data = lcall("GetGeneralData", name); const extension = data.extension; - const path = AppPath + "/packages/" + extension + "/image/generals/" + extra + name + ".jpg"; + const path = AppPath + "/packages/" + extension + "/image/generals/" + + extra + name + ".jpg"; if (Backend.exists(path)) { return path; } } function getGeneralPicture(name) { - const data = JSON.parse(Backend.callLuaFunction("GetGeneralData", [name])); + const data = lcall("GetGeneralData", name); const extension = data.extension; - const path = AppPath + "/packages/" + extension + "/image/generals/" + name + ".jpg"; + const path = AppPath + "/packages/" + extension + "/image/generals/" + + name + ".jpg"; if (Backend.exists(path)) { return path; } @@ -54,14 +56,15 @@ function getCardPicture(cidOrName) { let name = "unknown"; if (typeof cidOrName === 'string') { name = cidOrName; - extension = Backend.callLuaFunction("GetCardExtensionByName", [cidOrName]); + extension = lcall("GetCardExtensionByName", cidOrName); } else { - const data = JSON.parse(Backend.callLuaFunction("GetCardData", [cid])); + const data = lcall("GetCardData", cid); extension = data.extension; name = data.name; } - let path = AppPath + "/packages/" + extension + "/image/card/" + name + ".png"; + let path = AppPath + "/packages/" + extension + "/image/card/" + + name + ".png"; if (Backend.exists(path)) { return path; } else { @@ -72,9 +75,10 @@ function getCardPicture(cidOrName) { } function getDelayedTrickPicture(name) { - const extension = Backend.callLuaFunction("GetCardExtensionByName", [name]); + const extension = lcall("GetCardExtensionByName", name); - let path = AppPath + "/packages/" + extension + "/image/card/delayedTrick/" + name + ".png"; + let path = AppPath + "/packages/" + extension + "/image/card/delayedTrick/" + + name + ".png"; if (Backend.exists(path)) { return path; } else { @@ -86,10 +90,11 @@ function getDelayedTrickPicture(name) { function getEquipIcon(cid, icon) { - const data = JSON.parse(Backend.callLuaFunction("GetCardData", [cid])); + const data = lcall("GetCardData", cid); const extension = data.extension; const name = icon || data.name; - let path = AppPath + "/packages/" + extension + "/image/card/equipIcon/" + name + ".png"; + let path = AppPath + "/packages/" + extension + "/image/card/equipIcon/" + + name + ".png"; if (Backend.exists(path)) { return path; } else { diff --git a/Fk/util.js b/Fk/util.js index 785d29d0..e7f53fa1 100644 --- a/Fk/util.js +++ b/Fk/util.js @@ -1,7 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -.pragma library - function convertNumber(number) { if (number === 1) return "A"; @@ -14,8 +12,20 @@ function convertNumber(number) { return ""; } -Array.prototype.contains = function(element) { - return this.indexOf(element) != -1; +function processPrompt(prompt) { + const data = prompt.split(":"); + let raw = luatr(data[0]); + const src = parseInt(data[1]); + const dest = parseInt(data[2]); + if (raw.match("%src")) + raw = raw.replace(/%src/g, luatr(getPhoto(src).general)); + if (raw.match("%dest")) + raw = raw.replace(/%dest/g, luatr(getPhoto(dest).general)); + if (raw.match("%arg2")) + raw = raw.replace(/%arg2/g, luatr(data[4])); + if (raw.match("%arg")) + raw = raw.replace(/%arg/g, luatr(data[3])); + return raw; } Array.prototype.prepend = function() { diff --git a/README.md b/README.md index 4ab0d5d0..040e914f 100644 --- a/README.md +++ b/README.md @@ -14,21 +14,11 @@ ___ 新月杀(FreeKill)是一款开源的三国杀游戏,但其目的不在于补完官方所有武将,而是着力于提供一个最适合DIY的框架。 +___ + ## 项目文档 -[新月杀文档](https://qsgs-fans.github.io/FreeKill/usr/index.html) - -### 依赖的库 - -以下是新月杀运行所必不可少的依赖库: - -* [![](https://img.shields.io/badge/qt6-50D160?style=for-the-badge&logo=qt&logoColor=white)](https://www.qt.io) -* [![](https://img.shields.io/badge/lua5.4-030380?style=for-the-badge&logo=lua)](https://www.lua.org) -* [![](https://img.shields.io/badge/sqlite3-7ABEEA?style=for-the-badge&logo=sqlite)](https://www.sqlite.org) -* [![](https://img.shields.io/badge/libgit2-FFFFFF?style=for-the-badge&logo=git)](https://www.libgit2.org) -* [![](https://img.shields.io/badge/openssl-721412?style=for-the-badge&logo=openssl)](https://www.openssl.org) - -新月杀在编译过程中,需要用到cmake, flex, bison, swig。 +https://fkbook-all-in-one.readthedocs.io/ ___ @@ -46,7 +36,7 @@ ___ ## 如何构建 -关于如何从头构建新月杀,详见[编译教程](https://qsgs-fans.github.io/FreeKill/inner/01-compile.html)。 +https://fkbook-all-in-one.readthedocs.io/zh-cn/latest/develop/02-env.html ___ diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 09a828f4..45896229 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -3,8 +3,8 @@ + android:versionCode="409" + android:versionName="0.4.9"> diff --git a/audio/system/gamestart.mp3 b/audio/system/gamestart.mp3 new file mode 100644 index 00000000..45b3057b Binary files /dev/null and b/audio/system/gamestart.mp3 differ diff --git a/audio/system/ice_damage.mp3 b/audio/system/ice_damage.mp3 new file mode 100644 index 00000000..bb69be22 Binary files /dev/null and b/audio/system/ice_damage.mp3 differ diff --git a/audio/system/ice_damage2.mp3 b/audio/system/ice_damage2.mp3 new file mode 100644 index 00000000..049ebf35 Binary files /dev/null and b/audio/system/ice_damage2.mp3 differ diff --git a/audio/system/ready.mp3 b/audio/system/ready.mp3 new file mode 100644 index 00000000..c1b8cf6e Binary files /dev/null and b/audio/system/ready.mp3 differ diff --git a/image/button/skill/locked.png b/image/button/skill/locked.png new file mode 100644 index 00000000..ce89fb8b Binary files /dev/null and b/image/button/skill/locked.png differ diff --git a/lang/zh_CN.ts b/lang/zh_CN.ts index e5dbf625..371eae03 100644 --- a/lang/zh_CN.ts +++ b/lang/zh_CN.ts @@ -107,6 +107,10 @@ [%1/%2] upgrading package '%3' [%1/%2] 更新拓展包 '%3' + + packages/%1: some error occured. + 拓展包 %1 出了点问题,尝试在管理拓展包中删除之再试试 + diff --git a/lua/client/client.lua b/lua/client/client.lua index c4352732..71ae114a 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -1,16 +1,14 @@ -- SPDX-License-Identifier: GPL-3.0-or-later ----@class Client +---@class Client : AbstractRoom ---@field public client fk.Client ---@field public players ClientPlayer[] @ 所有参战玩家的数组 ---@field public alive_players ClientPlayer[] @ 所有存活玩家的数组 ---@field public observers ClientPlayer[] @ 观察者的数组 ---@field public current ClientPlayer @ 当前回合玩家 ---@field public discard_pile integer[] @ 弃牌堆 ----@field public status_skills Skill[] @ 状态技总和 ----@field public banners table @ 左上角显示的东西 ---@field public observing boolean -Client = class('Client') +Client = AbstractRoom:subclass('Client') -- load client classes ClientPlayer = require "client.clientplayer" @@ -26,6 +24,7 @@ local pattern_refresh_commands = { } function Client:initialize() + AbstractRoom.initialize(self) self.client = fk.ClientInstance self.notifyUI = function(self, command, jsonData) fk.Backend:emitNotifyUI(command, jsonData) @@ -49,21 +48,8 @@ function Client:initialize() end end - self.players = {} -- ClientPlayer[] - self.alive_players = {} - self.observers = {} self.discard_pile = {} - self.status_skills = {} - for class, skills in pairs(Fk.global_status_skill) do - self.status_skills[class] = {table.unpack(skills)} - end - self.banners = {} - - self.skill_costs = {} - self.card_marks = {} - self.filtered_cards = {} - self.printed_cards = {} self.disabled_packs = {} self.disabled_generals = {} @@ -71,7 +57,7 @@ function Client:initialize() end ---@param id integer ----@return ClientPlayer +---@return ClientPlayer? function Client:getPlayerById(id) if id == Self.id then return Self end for _, p in ipairs(self.players) do @@ -225,7 +211,11 @@ end ---@param msg LogMessage function Client:appendLog(msg) - self:notifyUI("GameLog", parseMsg(msg)) + local text = parseMsg(msg) + self:notifyUI("GameLog", text) + if msg.toast then + self:notifyUI("ShowToast", text) + end end ---@param msg LogMessage @@ -237,15 +227,6 @@ function Client:setCardNote(ids, msg) end end -function Client:setBanner(name, value) - if value == 0 then value = nil end - self.banners[name] = value -end - -function Client:getBanner(name) - return self.banners[name] -end - fk.client_callback["SetCardFootnote"] = function(jsonData) local data = json.decode(jsonData) ClientInstance:setCardNote(data[1], data[2]); @@ -401,6 +382,7 @@ fk.client_callback["AskForCardChosen"] = function(jsonData) judge = {} end ui_data = { + _id = id, _reason = reason, card_data = {}, _prompt = prompt, @@ -409,6 +391,7 @@ fk.client_callback["AskForCardChosen"] = function(jsonData) if #equip ~= 0 then table.insert(ui_data.card_data, { "$Equip", equip }) end if #judge ~= 0 then table.insert(ui_data.card_data, { "$Judge", judge }) end else + ui_data._id = id ui_data._reason = reason ui_data._prompt = prompt end @@ -436,6 +419,7 @@ fk.client_callback["AskForCardsChosen"] = function(jsonData) judge = {} end ui_data = { + _id = id, _min = min, _max = max, _reason = reason, @@ -446,6 +430,7 @@ fk.client_callback["AskForCardsChosen"] = function(jsonData) if #equip ~= 0 then table.insert(ui_data.card_data, { "$Equip", equip }) end if #judge ~= 0 then table.insert(ui_data.card_data, { "$Judge", judge }) end else + ui_data._id = id ui_data._min = min ui_data._max = max ui_data._reason = reason @@ -469,6 +454,7 @@ local function separateMoves(moves) moveReason = move.moveReason, specialName = move.specialName, fromSpecialName = info.fromSpecialName, + proposer = move.proposer, }) end end @@ -480,9 +466,9 @@ local function mergeMoves(moves) local ret = {} local temp = {} for _, move in ipairs(moves) do - local info = string.format("%q,%q,%q,%q,%s,%s", + local info = string.format("%q,%q,%q,%q,%s,%s,%q", move.from, move.to, move.fromArea, move.toArea, - move.specialName, move.fromSpecialName) + move.specialName, move.fromSpecialName, move.proposer) if temp[info] == nil then temp[info] = { ids = {}, @@ -493,6 +479,7 @@ local function mergeMoves(moves) moveReason = move.moveReason, specialName = move.specialName, fromSpecialName = move.fromSpecialName, + proposer = move.proposer, } end table.insert(temp[info].ids, move.ids[1]) @@ -504,12 +491,96 @@ local function mergeMoves(moves) end local function sendMoveCardLog(move) - local client = ClientInstance + local client = ClientInstance ---@class Client if #move.ids == 0 then return end local hidden = table.contains(move.ids, -1) local msgtype - if move.from and move.toArea == Card.DrawPile then + if move.toArea == Card.PlayerHand then + if move.fromArea == Card.PlayerSpecial then + client:appendLog{ + type = "$GetCardsFromPile", + from = move.to, + arg = move.fromSpecialName, + arg2 = #move.ids, + card = move.ids, + } + elseif move.fromArea == Card.DrawPile then + client:appendLog{ + type = "$DrawCards", + from = move.to, + card = move.ids, + arg = #move.ids, + } + elseif move.fromArea == Card.Processing then + client:appendLog{ + type = "$GotCardBack", + from = move.to, + card = move.ids, + arg = #move.ids, + } + elseif move.fromArea == Card.DiscardPile then + client:appendLog{ + type = "$RecycleCard", + from = move.to, + card = move.ids, + arg = #move.ids, + } + elseif move.from then + client:appendLog{ + type = "$MoveCards", + from = move.from, + to = { move.to }, + arg = #move.ids, + card = move.ids, + } + else + client:appendLog{ + type = "$PreyCardsFromPile", + from = move.to, + card = move.ids, + arg = #move.ids, + } + end + elseif move.toArea == Card.PlayerEquip then + client:appendLog{ + type = "$InstallEquip", + from = move.to, + card = move.ids, + } + elseif move.toArea == Card.PlayerJudge then + if move.from ~= move.to and move.fromArea == Card.PlayerJudge then + client:appendLog{ + type = "$LightningMove", + from = move.from, + to = { move.to }, + card = move.ids, + } + elseif move.from then + client:appendLog{ + type = "$PasteCard", + from = move.from, + to = { move.to }, + card = move.ids, + } + end + elseif move.toArea == Card.PlayerSpecial then + client:appendLog{ + type = "$AddToPile", + arg = move.specialName, + arg2 = #move.ids, + from = move.to, + card = move.ids, + } + elseif move.fromArea == Card.PlayerEquip then + client:appendLog{ + type = "$UninstallEquip", + from = move.from, + card = move.ids, + } + -- elseif move.toArea == Card.Processing then + -- nop + elseif move.from and move.toArea == Card.DrawPile then msgtype = hidden and "$PutCard" or "$PutKnownCard" client:appendLog{ type = msgtype, @@ -521,85 +592,37 @@ local function sendMoveCardLog(move) type = "$$PutCard", from = move.from, }) - elseif move.toArea == Card.PlayerSpecial then - msgtype = hidden and "$RemoveCardFromGame" or "$AddToPile" - client:appendLog{ - type = msgtype, - arg = move.specialName, - arg2 = #move.ids, - card = move.ids, - } - elseif move.fromArea == Card.PlayerSpecial and move.to then - client:appendLog{ - type = "$GetCardsFromPile", - from = move.to, - arg = move.fromSpecialName, - arg2 = #move.ids, - card = move.ids, - } - elseif move.moveReason == fk.ReasonDraw then - client:appendLog{ - type = "$DrawCards", - from = move.to, - card = move.ids, - arg = #move.ids, - } - elseif (move.fromArea == Card.DrawPile or move.fromArea == Card.DiscardPile) - and move.moveReason == fk.ReasonPrey then - client:appendLog{ - type = "$PreyCardsFromPile", - from = move.to, - card = move.ids, - arg = #move.ids, - } - elseif (move.fromArea == Card.Processing or move.fromArea == Card.PlayerJudge) - and move.toArea == Card.PlayerHand then - client:appendLog{ - type = "$GotCardBack", - from = move.to, - card = move.ids, - arg = #move.ids, - } - elseif move.fromArea == Card.DiscardPile and move.toArea == Card.PlayerHand then - client:appendLog{ - type = "$RecycleCard", - from = move.to, - card = move.ids, - arg = #move.ids, - } - elseif move.from and move.fromArea ~= Card.PlayerJudge and - move.toArea ~= Card.PlayerJudge and move.to and move.from ~= move.to then - client:appendLog{ - type = "$MoveCards", - from = move.from, - to = { move.to }, - arg = #move.ids, - card = move.ids, - } - elseif move.from and move.to and move.toArea == Card.PlayerJudge then - if move.fromArea == Card.PlayerJudge and move.from ~= move.to then - msgtype = "$LightningMove" - elseif move.fromArea ~= Card.PlayerJudge then - msgtype = "$PasteCard" - end - if msgtype then + elseif move.toArea == Card.DiscardPile then + if move.moveReason == fk.ReasonDiscard then + if move.proposer and move.proposer ~= move.from then + client:appendLog{ + type = "$DiscardOther", + from = move.from, + to = {move.proposer}, + card = move.ids, + arg = #move.ids, + } + else + client:appendLog{ + type = "$DiscardCards", + from = move.from, + card = move.ids, + arg = #move.ids, + } + end + elseif move.moveReason == fk.ReasonPutIntoDiscardPile then client:appendLog{ - type = msgtype, - from = move.from, - to = { move.to }, + type = "$PutToDiscard", card = move.ids, + arg = #move.ids, } end + -- elseif move.toArea == Card.Void then + -- nop end - -- TODO ... + -- TODO: footnote if move.moveReason == fk.ReasonDiscard then - client:appendLog{ - type = "$DiscardCards", - from = move.from, - card = move.ids, - arg = #move.ids, - } client:setCardNote(move.ids, { type = "$$DiscardCards", from = move.from @@ -753,9 +776,7 @@ fk.client_callback["AskForUseActiveSkill"] = function(jsonData) local data = json.decode(jsonData) local skill = Fk.skills[data[1]] local extra_data = data[4] - for k, v in pairs(extra_data) do - skill[k] = v - end + skill._extra_data = extra_data Fk.currentResponseReason = extra_data.skillName ClientInstance:notifyUI("AskForUseActiveSkill", jsonData) @@ -775,9 +796,19 @@ fk.client_callback["SetPlayerMark"] = function(jsonData) -- jsonData: [ int id, string mark, int value ] local data = json.decode(jsonData) local player, mark, value = data[1], data[2], data[3] - ClientInstance:getPlayerById(player):setMark(mark, value) + local p = ClientInstance:getPlayerById(player) + p:setMark(mark, value) if string.sub(mark, 1, 1) == "@" then + if mark:startsWith("@[") and mark:find(']') then + local close = mark:find(']') + local mtype = mark:sub(3, close - 1) + local spec = Fk.qml_marks[mtype] + if spec then + local text = spec.how_to_show(mark, value, p) + if text == "#hidden" then return end + end + end ClientInstance:notifyUI("SetPlayerMark", jsonData) end end diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua index 26f4ebe4..071e2bc7 100644 --- a/lua/client/client_util.lua +++ b/lua/client/client_util.lua @@ -127,6 +127,14 @@ function GetCardExtensionByName(cardName) return card and card.package.extensionName or "" end +function GetAllMods() + return json.encode(Fk.extensions) +end + +function GetAllModNames() + return json.encode(Fk.extension_names) +end + function GetAllGeneralPack() local ret = {} for _, name in ipairs(Fk.package_names) do @@ -138,6 +146,7 @@ function GetAllGeneralPack() end function GetGenerals(pack_name) + if not Fk.packages[pack_name] then return "{}" end local ret = {} for _, g in ipairs(Fk.packages[pack_name].generals) do if not g.total_hidden then @@ -251,8 +260,10 @@ end ---@param card string | integer ---@param player integer -function CanUseCard(card, player) +---@param extra_data_str string +function CanUseCard(card, player, extra_data_str) local c ---@type Card + local extra_data = extra_data_str == "" and nil or json.decode(extra_data_str) if type(card) == "number" then c = Fk:getCardById(card) else @@ -271,13 +282,13 @@ function CanUseCard(card, player) end player = ClientInstance:getPlayerById(player) - local ret = c.skill:canUse(player, c) + local ret = c.skill:canUse(player, c, extra_data) ret = ret and not player:prohibitUse(c) if ret then local min_target = c.skill:getMinTargetNum() if min_target > 0 then for _, p in ipairs(ClientInstance.players) do - if c.skill:targetFilter(p.id, {}, {}, c) then + if c.skill:targetFilter(p.id, {}, {}, c, extra_data) then return "true" end end @@ -311,7 +322,9 @@ end ---@param card string | integer ---@param to_select integer @ id of the target ---@param selected integer[] @ ids of selected targets -function CanUseCardToTarget(card, to_select, selected) +---@param extra_data_str string @ extra data +function CanUseCardToTarget(card, to_select, selected, extra_data_str) + local extra_data = extra_data_str == "" and nil or json.decode(extra_data_str) if ClientInstance:getPlayerById(to_select).dead then return "false" end @@ -322,10 +335,10 @@ function CanUseCardToTarget(card, to_select, selected) selected_cards = {card} else local t = json.decode(card) - return ActiveTargetFilter(t.skill, to_select, selected, t.subcards) + return ActiveTargetFilter(t.skill, to_select, selected, t.subcards, extra_data) end - local ret = c.skill:targetFilter(to_select, selected, selected_cards, c) + local ret = c.skill:targetFilter(to_select, selected, selected_cards, c, extra_data) ret = ret and not Self:isProhibited(Fk:currentRoom():getPlayerById(to_select), c) return json.encode(ret) end @@ -392,12 +405,13 @@ function GetSkillData(skill_name) } end -function ActiveCanUse(skill_name) +function ActiveCanUse(skill_name, extra_data_str) + local extra_data = extra_data_str == "" and nil or json.decode(extra_data_str) local skill = Fk.skills[skill_name] local ret = false if skill then if skill:isInstanceOf(ActiveSkill) then - ret = skill:canUse(Self) + ret = skill:canUse(Self, extra_data) elseif skill:isInstanceOf(ViewAsSkill) then ret = skill:enabledAtPlay(Self) if ret then @@ -414,7 +428,7 @@ function ActiveCanUse(skill_name) for _, n in ipairs(cnames) do local c = Fk:cloneCard(n) c.skillName = skill_name - ret = c.skill:canUse(Self, c) + ret = c.skill:canUse(Self, c, extra_data) if ret then break end end end @@ -449,7 +463,7 @@ function ActiveCardFilter(skill_name, to_select, selected, selected_targets) return json.encode(ret) end -function ActiveTargetFilter(skill_name, to_select, selected, selected_cards) +function ActiveTargetFilter(skill_name, to_select, selected, selected_cards, extra_data) local skill = Fk.skills[skill_name] local ret = false if skill then @@ -458,7 +472,7 @@ function ActiveTargetFilter(skill_name, to_select, selected, selected_cards) elseif skill:isInstanceOf(ViewAsSkill) then local card = skill:viewAs(selected_cards) if card then - ret = card.skill:targetFilter(to_select, selected, selected_cards, card) + ret = card.skill:targetFilter(to_select, selected, selected_cards, card, extra_data) ret = ret and not Self:isProhibited(Fk:currentRoom():getPlayerById(to_select), card) end end @@ -572,7 +586,17 @@ end function GetExpandPileOfSkill(skillName) local skill = Fk.skills[skillName] - return skill and (skill.expand_pile or "") or "" + if not skill then return "" end + local e = skill.expand_pile + if type(e) == "function" then + e = e(skill) + end + + if type(e) == "table" then + return json.encode(e) + else + return e or "" + end end function GetGameModes() @@ -722,7 +746,7 @@ function PoxiPrompt(poxi_type, data, extra_data) local poxi = Fk.poxi_methods[poxi_type] if not poxi or not poxi.prompt then return "" end if type(poxi.prompt) == "string" then return Fk:translate(poxi.prompt) end - return poxi.prompt(data, extra_data) + return Fk:translate(poxi.prompt(data, extra_data)) end function PoxiFilter(poxi_type, to_select, selected, data, extra_data) @@ -748,6 +772,15 @@ function GetQmlMark(mtype, name, value, p) } end +function GetMiniGame(gtype, p, data) + local spec = Fk.mini_games[gtype] + p = ClientInstance:getPlayerById(p) + data = json.decode(data) + return json.encode { + qml_path = type(spec.qml_path) == "function" and spec.qml_path(p, data) or spec.qml_path, + } +end + function YuqiPrompt(yuqi_type, data, extra_data) local yuqi = Fk.yuqi_methods[yuqi_type] if not yuqi or not yuqi.prompt then return "" end diff --git a/lua/client/i18n/en_US.lua b/lua/client/i18n/en_US.lua index 1da0ad40..ee356e21 100644 --- a/lua/client/i18n/en_US.lua +++ b/lua/client/i18n/en_US.lua @@ -134,8 +134,17 @@ Fk:loadTranslationTable({ -- ["Quit"] = "退出", ["BanGeneral"] = "Ban", ["ResumeGeneral"] = "Unban", + ["BanPackage"] = "Ban packages", + ["$BanPkgHelp"] = "Banning packages", + ["$BanCharaHelp"] = "Banning characters", -- ["Companions"] = "珠联璧合", -- ["Death audio"] = "阵亡", + -- ["Win audio"] = "胜利语音", + -- ["Official"] = "官方", + ["Title"] = "Title: ", + ["Designer"] = "Designer: ", + ["Voice Actor"] = "Voice Actor: ", + ["Illustrator"] = "Illustrator: ", ["$WelcomeToLobby"] = "Welcome to FreeKill lobby!", ["GameMode"] = "Game mode: ", @@ -176,12 +185,13 @@ Fk:loadTranslationTable({ ["AskForKingdom"] = "Choosing kingdom", ["AskForPindian"] = "Point fight", ["AskForMoveCardInBoard"] = "Moving cards", + ["replaceEquip"] = "Replacing Equip", ["PlayCard"] = "Playing card", ["AskForCardChosen"] = "Choosing card", ["AskForCardsChosen"] = "Choosing card", - ["#AskForChooseCard"] = "%1: please choose a card", - ["#AskForChooseCards"] = "%1: please choose %2~%3 cards", + ["#AskForChooseCard"] = "%1: please choose a card from %src", + ["#AskForChooseCards"] = "%1: please choose %2~%3 cards from %src", ["$ChooseCard"] = "Choose a card", ["$ChooseCards"] = "Choose %1~%2 cards", ["$Hand"] = "Hand", @@ -196,8 +206,11 @@ Fk:loadTranslationTable({ ["#AskForPeaches"] = "%src is dying, please use %arg Peach(es) to save him", ["#AskForPeachesSelf"] = "You are dying, please use %arg Peach(es)/Alcohol to save yourself", - ["#AskForDiscard"] = "Please discard %arg cards (at least %arg2)", - ["#AskForCard"] = "Please choose %arg cards (at least %arg2)", + ["#AskForDiscard"] = "Please discard %arg cards (%arg2 at least)", + ["#AskForCard"] = "Please choose %arg cards (%arg2 at least)", + ["#AskForDistribution"] = "Please distribute cards (%arg at least , %arg2 total)", + ["@DistributionTo"] = "", + ["#replaceEquip"] = "Please Choose a Equip Card to be replaced", ["#askForPindian"] = "%arg: please choose a hand card for point fight", ["#StartPindianReason"] = "%from started point fight (%arg)", ["#ShowPindianCard"] = "The point fight card of %from is %card", @@ -268,6 +281,7 @@ Fk:loadTranslationTable({ ["thunder_damage"] = "Thunder", ["ice_damage"] = "Ice", ["hp_lost"] = "HP lost", + ["lose_hp"] = "lose HP", ["phase_start"] = "Prepare phase", ["phase_judge"] = "Judge phase", @@ -343,6 +357,7 @@ Fk:loadTranslationTable({ ["$LightningMove"] = "%card transfered from %from to %to", ["$PasteCard"] = "%from used %card to %to", ["$DiscardCards"] = "%from discarded %arg card(s) %card", + ["$DiscardOther"] = "%to discarded %arg card(s) %card from %from", ["$InstallEquip"] = "%from equipped %card", ["$UninstallEquip"] = "%from uninstalled %card", diff --git a/lua/client/i18n/zh_CN.lua b/lua/client/i18n/zh_CN.lua index 02ef90f3..b9400988 100644 --- a/lua/client/i18n/zh_CN.lua +++ b/lua/client/i18n/zh_CN.lua @@ -79,12 +79,16 @@ Fk:loadTranslationTable{ ["Clear"] = "清空", ["Help_Ban_List"] = "导出键会将这个方案的内容复制到剪贴板中;" .. "导入键会自动读取剪贴板,若可以导入则导入,不能导入则报错。", + ["Ban_Generals"] = "已禁用武将", + ["Ban_Packages"] = "禁用拓展包", + ["Whitelist_Generals"] = "白名单武将", ["Export"] = "导出", ["Export Success"] = "禁将方案已经复制到剪贴板。", ["Import"] = "导入", ["Not Legal"] = "导入失败:不是合法的JSON字符串。", ["Not JSON"] = "导入失败:数据格式不对。", ["Import Success"] = "从剪贴板导入禁将方案成功。", + ["Rename"] = "重命名", ["$OnlineInfo"] = "大厅人数:%1,总在线人数:%2", @@ -184,8 +188,17 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下 -- ["Quit"] = "退出", ["BanGeneral"] = "禁将", ["ResumeGeneral"] = "解禁", + ["BanPackage"] = "禁拓展包", + ["$BanPkgHelp"] = "正在禁用拓展包", + ["$BanCharaHelp"] = "正在禁用武将", ["Companions"] = "珠联璧合", ["Death audio"] = "阵亡", + ["Win audio"] = "胜利语音", + ["Official"] = "官方", + ["Title"] = "称号:", + ["Designer"] = "设计:", + ["Voice Actor"] = "配音:", + ["Illustrator"] = "画师:", ["$WelcomeToLobby"] = "欢迎进入新月杀游戏大厅!", ["GameMode"] = "游戏模式:", @@ -226,12 +239,13 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下 ["AskForKingdom"] = "选择势力", ["AskForPindian"] = "拼点", ["AskForMoveCardInBoard"] = "移动卡牌", + ["replaceEquip"] = "替换装备", ["PlayCard"] = "出牌", ["AskForCardChosen"] = "选牌", ["AskForCardsChosen"] = "选牌", - ["#AskForChooseCard"] = "%1:请选择其一张卡牌", - ["#AskForChooseCards"] = "%1:请选择其%2至%3张卡牌", + ["#AskForChooseCard"] = "%1:请选择%src的一张卡牌", + ["#AskForChooseCards"] = "%1:请选择%src的%2至%3张卡牌", ["$ChooseCard"] = "请选择一张卡牌", ["$ChooseCards"] = "请选择%1至%2张卡牌", ["$Hand"] = "手牌区", @@ -248,6 +262,9 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下 ["#AskForDiscard"] = "请弃置 %arg 张牌,最少 %arg2 张", ["#AskForCard"] = "请选择 %arg 张牌,最少 %arg2 张", + ["#AskForDistribution"] = "请分配这些牌,至少 %arg 张,至多 %arg2 张", + ["@DistributionTo"] = "", + ["#replaceEquip"] = "选择一张装备牌替换之", ["#askForPindian"] = "%arg:请选择一张手牌作为拼点牌", ["#StartPindianReason"] = "%from 由于 %arg 而发起拼点", ["#ShowPindianCard"] = "%from 的拼点牌是 %card", @@ -304,9 +321,9 @@ FreeKill使用的是libgit2的C API,与此同时使用Git完成拓展包的下 ["Resume"] = "继续", ["Bulletin Info"] = [==[ - ## v0.4.2 + ## v0.4.9 - 增加游玩计时和屏蔽提示。 + 修复投降莫名奇妙把对面杀了的bug ]==], } @@ -323,7 +340,8 @@ Fk:loadTranslationTable{ ["fire_damage"] = "火属性", ["thunder_damage"] = "雷属性", ["ice_damage"] = "冰属性", - ["hp_lost"] = "体力流失", + ["hp_lost"] = "失去体力", + ["lose_hp"] = "失去体力", ["phase_start"] = "准备阶段", ["phase_judge"] = "判定阶段", @@ -386,22 +404,27 @@ Fk:loadTranslationTable{ ["#LoseSkill"] = "%from 失去了技能 “%arg”", -- moveCards (they are sent by notifyMoveCards) - ["$PutCard"] = "%from 的 %arg 张牌被置于牌堆", - ["$PutKnownCard"] = "%from 的牌 %card 被置于牌堆", - ["$RemoveCardFromGame"] = "%arg2 张牌被作为 %arg 移出游戏", - ["$AddToPile"] = "%card 被作为 %arg 移出游戏", ["$GetCardsFromPile"] = "%from 从 %arg 中获得了 %arg2 张牌 %card", ["$DrawCards"] = "%from 摸了 %arg 张牌 %card", + ["$MoveCards"] = "%to 从 %from 处获得了 %arg 张牌 %card", ["$PreyCardsFromPile"] = "%from 获得了 %arg 张牌 %card", ["$GotCardBack"] = "%from 收回了 %arg 张牌 %card", ["$RecycleCard"] = "%from 从弃牌堆回收了 %arg 张牌 %card", - ["$MoveCards"] = "%to 从 %from 处获得了 %arg 张牌 %card", - ["$LightningMove"] = "%card 从 %from 转移到了 %to", - ["$PasteCard"] = "%from 给 %to 贴了张 %card", - ["$DiscardCards"] = "%from 弃置了 %arg 张牌 %card", + ["$InstallEquip"] = "%from 装备了 %card", ["$UninstallEquip"] = "%from 卸载了 %card", + ["$LightningMove"] = "%card 从 %from 转移到了 %to", + ["$PasteCard"] = "%from 给 %to 贴了张 %card", + + ["$AddToPile"] = "%arg2 张牌 %card 被作为 %from 的 %arg 移出游戏", + + ["$PutCard"] = "%from 的 %arg 张牌被置于牌堆", + ["$PutKnownCard"] = "%from 的牌 %card 被置于牌堆", + ["$DiscardCards"] = "%from 弃置了 %arg 张牌 %card", + ["$DiscardOther"] = "%to 弃置了 %from 的 %arg 张牌 %card", + ["$PutToDiscard"] = "%arg 张牌 %card 被置入弃牌堆", + ["#ShowCard"] = "%from 展示了牌 %card", ["#Recast"] = "%from 重铸了 %card", ["#RecastBySkill"] = "%from 发动了 “%arg” 重铸了 %card", diff --git a/lua/core/abstract_room.lua b/lua/core/abstract_room.lua new file mode 100644 index 00000000..0a4d837b --- /dev/null +++ b/lua/core/abstract_room.lua @@ -0,0 +1,52 @@ +-- 作Room和Client的基类,这二者有不少共通之处 +---@class AbstractRoom : Object +---@fiele public players Player[] @ 房内参战角色们 +---@field public alive_players Player[] @ 所有存活玩家的数组 +---@field public observers Player[] @ 看戏的 +---@field public current Player @ 当前行动者 +---@field public status_skills table @ 这个房间中含有的状态技列表 +---@field public filtered_cards table @ 见于Engine,其实在这 +---@field public printed_cards table @ 同上 +---@field public skill_costs table @ 用来存skill.cost_data +---@field public card_marks table @ 用来存实体卡的card.mark +---@field public banners table @ 全局mark +local AbstractRoom = class("AbstractRoom") + +function AbstractRoom:initialize() + self.players = {} + self.alive_players = {} + self.observers = {} + self.current = nil + + self.status_skills = {} + for class, skills in pairs(Fk.global_status_skill) do + self.status_skills[class] = {table.unpack(skills)} + end + + self.filtered_cards = {} + self.printed_cards = {} + self.skill_costs = {} + self.card_marks = {} + self.banners = {} +end + +-- 仅供注释,其余空函数一样 +---@param id integer +---@return Player? +function AbstractRoom:getPlayerById(id) end + +--- 获取一张牌所处的区域。 +---@param cardId integer | Card @ 要获得区域的那张牌,可以是Card或者一个id +---@return CardArea @ 这张牌的区域 +function AbstractRoom:getCardArea(cardId) end + +function AbstractRoom:setBanner(name, value) + if value == 0 then value = nil end + self.banners[name] = value +end + +function AbstractRoom:getBanner(name) + return self.banners[name] +end + +return AbstractRoom diff --git a/lua/core/card.lua b/lua/core/card.lua index 6cb51c34..10faad65 100644 --- a/lua/core/card.lua +++ b/lua/core/card.lua @@ -24,6 +24,7 @@ ---@field public special_skills? string[] @ 衍生技能,如重铸 ---@field public is_damage_card boolean @ 是否为会造成伤害的牌 ---@field public multiple_targets boolean @ 是否为指定多个目标的牌 +---@field public is_passive? boolean @ 是否只能在响应时使用或打出 ---@field public is_derived? boolean @ 判断是否为衍生牌 local Card = class("Card") @@ -145,11 +146,12 @@ function Card:clone(suit, number) newCard.special_skills = self.special_skills newCard.is_damage_card = self.is_damage_card newCard.multiple_targets = self.multiple_targets + newCard.is_passive = self.is_passive newCard.is_derived = self.is_derived return newCard end ---- 检测是否为虚拟卡牌,如果其ID为0及以下,则为虚拟卡牌。 +--- 检测是否为虚拟卡牌,如果其ID为0,则为虚拟卡牌。 function Card:isVirtual() return self.id == 0 end @@ -171,7 +173,7 @@ local function updateColorAndNumber(card) local different_color = false for i, id in ipairs(card.subcards) do local c = Fk:getCardById(id) - number = math.min(number + c.number, 13) + number = #card.subcards == 1 and math.min(number + c.number, 13) or 0 if i == 1 then card.suit = c.suit else @@ -406,17 +408,12 @@ function Card:getMark(mark) if (not self:isVirtual()) and next(self.mark) == nil then self.mark = nil end + if type(ret) == "table" then + ret = table.simpleClone(ret) + end return ret end ---- 判定卡牌是否拥有对应的Mark。 ----@param mark string @ 标记 ----@return boolean -function Card:hasMark(mark) - fk.qWarning("hasMark will be deleted in future version!") - return self:getMark(mark) ~= 0 -end - --- 获取卡牌有哪些Mark。 function Card:getMarkNames() local ret = {} diff --git a/lua/core/engine.lua b/lua/core/engine.lua index 5004ed0d..b25df73d 100644 --- a/lua/core/engine.lua +++ b/lua/core/engine.lua @@ -7,6 +7,8 @@ --- 同时也提供了许多常用的函数。 --- ---@class Engine : Object +---@field public extensions table @ 所有mod列表及其包含的拓展包 +---@field public extension_names string[] @ Mod名字的数组,为了方便排序 ---@field public packages table @ 所有拓展包的列表 ---@field public package_names string[] @ 含所有拓展包名字的数组,为了方便排序 ---@field public skills table @ 所有的技能 @@ -28,6 +30,7 @@ ---@field private _custom_events any[] @ 自定义事件列表 ---@field public poxi_methods table @ “魄袭”框操作方法表 ---@field public qml_marks table @ 自定义Qml标记的表 +---@field public mini_games table @ 自定义多人交互表 ---@field public yuqi_methods table @ “隅泣”框操作方法表 local Engine = class("Engine") @@ -44,6 +47,12 @@ function Engine:initialize() Fk = self + self.extensions = { + ["standard"] = { "standard" }, + ["standard_cards"] = { "standard_cards" }, + ["maneuvering"] = { "maneuvering" }, + } + self.extension_names = { "standard", "standard_cards", "maneuvering" } self.packages = {} -- name --> Package self.package_names = {} self.skills = {} -- name --> Skill @@ -62,6 +71,7 @@ function Engine:initialize() self._custom_events = {} self.poxi_methods = {} self.qml_marks = {} + self.mini_games = {} self:loadPackages() self:loadDisabled() @@ -69,20 +79,20 @@ function Engine:initialize() end local _foreign_keys = { - "currentResponsePattern", - "currentResponseReason", - "filtered_cards", - "printed_cards", + ["currentResponsePattern"] = true, + ["currentResponseReason"] = true, + ["filtered_cards"] = true, + ["printed_cards"] = true, } function Engine:__index(k) - if table.contains(_foreign_keys, k) then + if _foreign_keys[k] then return self:currentRoom()[k] end end function Engine:__newindex(k, v) - if table.contains(_foreign_keys, k) then + if _foreign_keys[k] then self:currentRoom()[k] = v else rawset(self, k, v) @@ -140,11 +150,15 @@ function Engine:loadPackages() -- Note that instance of Package is a table too -- so dont use type(pack) == "table" here if type(pack) == "table" then + table.insert(self.extension_names, dir) if pack[1] ~= nil then + self.extensions[dir] = {} for _, p in ipairs(pack) do + table.insert(self.extensions[dir], p.name) self:loadPackage(p) end else + self.extensions[dir] = { pack.name } self:loadPackage(pack) end end @@ -274,8 +288,8 @@ end ---@param name string @ 要查询的武将名字 ---@return string[] @ 这个武将对应的同名武将列表 function Engine:getSameGenerals(name) - local tmp = name:split("__") - local tName = tmp[#tmp] + if not self.generals[name] then return {} end + local tName = self.generals[name].trueName local ret = self.same_generals[tName] or {} return table.filter(ret, function(g) return g ~= name and self.generals[g] ~= nil and self:canUseGeneral(g) @@ -358,7 +372,7 @@ function Engine:addPoxiMethod(spec) spec.post_select = spec.post_select or function(s) return s end end ---- 向Engine中添加一个隅泣用方法。 +--- 向Engine中添加一个QML标记方法。 ---@param spec QmlMarkSpec function Engine:addQmlMark(spec) assert(type(spec.name) == "string") @@ -368,6 +382,16 @@ function Engine:addQmlMark(spec) self.qml_marks[spec.name] = spec end +--- 向Engine中添加一个多人交互用方法。 +---@param spec MiniGameSpec +function Engine:addMiniGame(spec) + assert(type(spec.name) == "string") + if self.mini_games[spec.name] then + fk.qCritical("Warning: duplicated mini game type " .. spec.name) + end + self.mini_games[spec.name] = spec +end + --- 向Engine中添加一个隅泣用方法。 ---@param spec YuqiSpec function Engine:addYuqiMethod(spec) @@ -538,7 +562,7 @@ function Engine:_addPrintedCard(card) end --- 获知当前的Engine是跑在服务端还是客户端,并返回相应的实例。 ----@return Room | Client +---@return AbstractRoom function Engine:currentRoom() if RoomInstance then return RoomInstance diff --git a/lua/core/exppattern.lua b/lua/core/exppattern.lua index 1cf1f9fd..1a959bd1 100644 --- a/lua/core/exppattern.lua +++ b/lua/core/exppattern.lua @@ -10,7 +10,7 @@ 2. 对于 Matcher 字符串,它是用 ('|') 分割的 3. 然后在 Matcher 的每一个细分中,又可以用 ',' 来进行更进一步的分割 - 其中 Matcher 的格式为 牌名|花色|点数|位置|详细牌名|类型|牌的id + 其中 Matcher 的格式为 牌名|点数|花色|区域|完整牌名|牌类型|牌id 更进一步,“点数” 可以用 '~' 符号表示数字的范围,并且可以用 AJQK 表示对应点数 例如: diff --git a/lua/core/game_mode.lua b/lua/core/game_mode.lua index d8480b98..2062a1ff 100644 --- a/lua/core/game_mode.lua +++ b/lua/core/game_mode.lua @@ -27,7 +27,7 @@ end ---@param victim ServerPlayer @ 死者 ---@return string @ 胜者阵营 function GameMode:getWinner(victim) - if victim.rest > 0 then + if not victim.surrendered and victim.rest > 0 then return "" end diff --git a/lua/core/general.lua b/lua/core/general.lua index 3ab607d3..24008eee 100644 --- a/lua/core/general.lua +++ b/lua/core/general.lua @@ -71,7 +71,7 @@ function General:__tostring() end --- 为武将增加技能,需要注意增加其他武将技能时的处理方式。 ----@param skill Skill @ (单个)武将技能 +---@param skill Skill|string @ (单个)武将技能 function General:addSkill(skill) if (type(skill) == "string") then table.insert(self.other_skills, skill) @@ -82,7 +82,7 @@ function General:addSkill(skill) end --- 为武将增加相关技能,需要注意增加其他武将技能时的处理方式。 ----@param skill Skill @ (单个)武将技能 +---@param skill Skill|string @ (单个)武将技能 function General:addRelatedSkill(skill) if (type(skill) == "string") then table.insert(self.related_other_skills, skill) @@ -94,12 +94,19 @@ function General:addRelatedSkill(skill) end --- 获取武将所有技能。 +---@param include_lord? boolean +---@return string[] function General:getSkillNameList(include_lord) - local ret = table.map(self.skills, Util.NameMapper) - table.insertTable(ret, self.other_skills) - - if not include_lord then + local ret = {} + local other_skills = table.map(self.other_skills, Util.Name2SkillMapper) + local skills = table.connect(self.skills, other_skills) + for _, skill in ipairs(skills) do + if include_lord or not skill.lordSkill then + table.insert(ret, skill.name) + end end + + -- table.insertTable(ret, self.other_skills) return ret end diff --git a/lua/core/player.lua b/lua/core/player.lua index 4e86703b..ceb536b7 100644 --- a/lua/core/player.lua +++ b/lua/core/player.lua @@ -215,15 +215,10 @@ end ---@param mark string @ 标记 ---@return any function Player:getMark(mark) - return (self.mark[mark] or 0) -end - ---- 判定角色是否拥有对应的Mark。 ----@param mark string @ 标记 ----@return boolean -function Player:hasMark(mark) - fk.qWarning("hasMark will be deleted in future version!") - return self:getMark(mark) ~= 0 + local mark = self.mark[mark] + if not mark then return 0 end + if type(mark) == "table" then return table.simpleClone(mark) end + return mark end --- 获取角色有哪些Mark。 @@ -363,13 +358,13 @@ function Player:getCardIds(playerAreas, specialName) return cardIds end ---- 通过名字检索获取玩家是否存在对应私人牌堆。 +--- 通过名字检索获取玩家对应的私人牌堆。 ---@param name string @ 私人牌堆名 function Player:getPile(name) - return self.special_cards[name] or {} + return table.simpleClone(self.special_cards[name] or {}) end ---- 通过ID检索获取玩家是否存在对应私人牌堆。 +--- 通过ID检索获取玩家对应的私人牌堆。 ---@param id integer @ 私人牌堆ID ---@return string? function Player:getPileNameOfId(id) @@ -454,23 +449,43 @@ end --- 获取玩家攻击范围。 function Player:getAttackRange() - local weapon = Fk:getCardById(self:getEquipment(Card.SubtypeWeapon)) - local baseAttackRange = math.max(weapon and weapon.attack_range or 1, 0) - - local status_skills = Fk:currentRoom().status_skills[AttackRangeSkill] or Util.DummyTable - for _, skill in ipairs(status_skills) do - local correct = skill:getCorrect(self) - baseAttackRange = baseAttackRange + (correct or 0) + local baseValue = 1 + local weapons = self:getEquipments(Card.SubtypeWeapon) + if #weapons > 0 then + baseValue = 0 + for _, id in ipairs(weapons) do + local weapon = Fk:getCardById(id) + baseValue = math.max(baseValue, weapon.attack_range or 1) + end end - return math.max(baseAttackRange, 0) + local status_skills = Fk:currentRoom().status_skills[AttackRangeSkill] or Util.DummyTable + local max_fixed, correct = nil, 0 + for _, skill in ipairs(status_skills) do + local f = skill:getFixed(self) + if f ~= nil then + max_fixed = max_fixed and math.max(max_fixed, f) or f + end + local c = skill:getCorrect(self) + correct = correct + (c or 0) + end + + return math.max(math.max(baseValue, (max_fixed or 0)) + correct, 0) end --- 获取角色是否被移除。 function Player:isRemoved() - return self:getMark(MarkEnum.PlayerRemoved) ~= 0 or table.find(MarkEnum.TempMarkSuffix, function(s) - return self:getMark(MarkEnum.PlayerRemoved .. s) ~= 0 - end) + for mark, _ in pairs(self.mark) do + if mark == MarkEnum.PlayerRemoved then return true end + if mark:startsWith(MarkEnum.PlayerRemoved .. "-") then + for _, suffix in ipairs(MarkEnum.TempMarkSuffix) do + if mark:find(suffix, 1, true) then return true end + end + end + end + -- return self:getMark(MarkEnum.PlayerRemoved) ~= 0 or table.find(MarkEnum.TempMarkSuffix, function(s) + -- return self:getMark(MarkEnum.PlayerRemoved .. s) ~= 0 + -- end) end --- 修改玩家与其他角色的固定距离。 @@ -857,9 +872,20 @@ end --- 确认玩家是否可以使用特定牌。 ---@param card Card @ 特定牌 -function Player:canUse(card) - assert(card, "Error: No Card") - return card.skill:canUse(self, card) +---@param extra_data? UseExtraData @ 额外数据 +function Player:canUse(card, extra_data) + return card.skill:canUse(self, card, extra_data) +end + +--- 确认玩家是否可以对特定玩家使用特定牌。 +---@param card Card @ 特定牌 +---@param to Player @ 特定玩家 +---@param extra_data? UseExtraData @ 额外数据 +function Player:canUseTo(card, to, extra_data) + if self:prohibitUse(card) or self:isProhibited(to, card) then return false end + local distance_limited = not (extra_data and extra_data.bypass_distances) + local can_use = self:canUse(card, extra_data) + return can_use and card.skill:modTargetFilter(to.id, {}, self.id, card, distance_limited) end --- 确认玩家是否被禁止对特定玩家使用特定牌。 @@ -911,8 +937,12 @@ function Player:prohibitResponse(card) end --- 确认玩家是否被禁止弃置特定牌。 ----@param card Card @ 特定的牌 +---@param card Card|integer @ 特定的牌 function Player:prohibitDiscard(card) + if type(card) == "number" then + card = Fk:getCardById(card) + end + local status_skills = Fk:currentRoom().status_skills[ProhibitSkill] or Util.DummyTable for _, skill in ipairs(status_skills) do if skill:prohibitDiscard(self, card) then @@ -928,13 +958,21 @@ function Player:prohibitReveal(isDeputy) if type(self:getMark(MarkEnum.RevealProhibited)) == "table" and table.contains(self:getMark(MarkEnum.RevealProhibited), place) then return true end - for _, m in ipairs(table.map(MarkEnum.TempMarkSuffix, function(s) - return self:getMark(MarkEnum.RevealProhibited .. s) - end)) do - if type(m) == "table" and table.contains(m, place) then - return true + + for mark, value in pairs(self.mark) do + if mark:startsWith(MarkEnum.RevealProhibited .. "-") and type(value) == "table" then + for _, suffix in ipairs(MarkEnum.TempMarkSuffix) do + if mark:find(suffix, 1, true) then return true end + end end end + -- for _, m in ipairs(table.map(MarkEnum.TempMarkSuffix, function(s) + -- return self:getMark(MarkEnum.RevealProhibited .. s) + -- end)) do + -- if type(m) == "table" and table.contains(m, place) then + -- return true + -- end + -- end return false end @@ -959,6 +997,21 @@ function Player:canPindian(to, ignoreFromKong, ignoreToKong) return true end +--- 判断一张牌能否移动至某角色的装备区 +---@param cardId integer @ 移动的牌 +---@param convert? boolean @ 是否可以替换装备(默认可以) +---@return boolean +function Player:canMoveCardIntoEquip(cardId, convert) + convert = (convert == nil) and true or convert + local card = Fk:getCardById(cardId) + if not (card.sub_type >= 3 and card.sub_type <= 7) then return false end + if self.dead or table.contains(self:getCardIds("e"), cardId) then return false end + if self:hasEmptyEquipSlot(card.sub_type) or (#self:getEquipments(card.sub_type) > 0 and convert) then + return true + end + return false +end + --转换技状态阳 fk.SwitchYang = 0 --转换技状态阴 @@ -991,7 +1044,7 @@ function Player:canMoveCardInBoardTo(to, id) return not ( table.find(to:getCardIds(Player.Judge), function(cardId) - return Fk:getCardById(cardId).name == card.name + return (to:getVirualEquip(cardId) or Fk:getCardById(cardId)).name == card.name end) or table.contains(to.sealedSlots, Player.JudgeSlot) ) diff --git a/lua/core/skill.lua b/lua/core/skill.lua index a3d18389..c03405e2 100644 --- a/lua/core/skill.lua +++ b/lua/core/skill.lua @@ -44,6 +44,7 @@ function Skill:initialize(name, frequency) self.anim_type = "" self.related_skills = {} self.attachedKingdom = {} + self._extra_data = {} local name_splited = name:split("__") self.trueName = name_splited[#name_splited] @@ -63,6 +64,8 @@ end function Skill:__index(k) if k == "cost_data" then return Fk:currentRoom().skill_costs[self.name] + else + return self._extra_data[k] end end diff --git a/lua/core/skill_type/active.lua b/lua/core/skill_type/active.lua index 643a8e47..7609879c 100644 --- a/lua/core/skill_type/active.lua +++ b/lua/core/skill_type/active.lua @@ -27,7 +27,7 @@ end --- Determine whether the skill can be used in playing phase ---@param player Player ---@param card Card @ helper -function ActiveSkill:canUse(player, card) +function ActiveSkill:canUse(player, card, extra_data) return self:isEffectable(player) end @@ -46,7 +46,8 @@ end ---@param selected integer[] @ ids of selected targets ---@param selected_cards integer[] @ ids of selected cards ---@param card Card @ helper -function ActiveSkill:targetFilter(to_select, selected, selected_cards, card) +---@param extra_data? any @ extra_data +function ActiveSkill:targetFilter(to_select, selected, selected_cards, card, extra_data) return false end @@ -142,6 +143,52 @@ function ActiveSkill:getDistanceLimit(player, card, to) return ret end +function ActiveSkill:withinDistanceLimit(player, isattack, card, to) + if to and to.dead then return false end + local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable + if not card and self.name:endsWith("_skill") then + card = Fk:cloneCard(self.name:sub(1, #self.name - 6)) + end + for _, skill in ipairs(status_skills) do + if skill:bypassDistancesCheck(player, self, card, to) then return true end + end + + local temp_suf = table.simpleClone(MarkEnum.TempMarkSuffix) + local card_temp_suf = table.simpleClone(MarkEnum.CardTempMarkSuffix) + + ---@param object Card|Player + ---@param markname string + ---@param suffixes string[] + ---@return boolean + local function hasMark(object, markname, suffixes) + if not object then return false end + for mark, _ in pairs(object.mark) do + if mark == markname then return true end + if mark:startsWith(markname .. "-") then + for _, suffix in ipairs(suffixes) do + if mark:find(suffix, 1, true) then return true end + end + end + end + return false + end + + return (isattack and player:inMyAttackRange(to)) or + (player:distanceTo(to) > 0 and player:distanceTo(to) <= self:getDistanceLimit(player, card, to)) or + hasMark(card, MarkEnum.BypassDistancesLimit, card_temp_suf) or + hasMark(player, MarkEnum.BypassDistancesLimit, temp_suf) or + hasMark(to, MarkEnum.BypassDistancesLimitTo, temp_suf) + -- (card and table.find(card_temp_suf, function(s) + -- return card:getMark(MarkEnum.BypassDistancesLimit .. s) ~= 0 + -- end)) or + -- (table.find(temp_suf, function(s) + -- return player:getMark(MarkEnum.BypassDistancesLimit .. s) ~= 0 + -- end)) or + -- (to and (table.find(temp_suf, function(s) + -- return to:getMark(MarkEnum.BypassDistancesLimitTo .. s) ~= 0 + -- end))) +end + --- Determine if selected cards and targets are valid for this skill --- If returns true, the OK button should be enabled --- only used in skill of players @@ -160,6 +207,11 @@ end ---@param cardUseEvent CardUseStruct function ActiveSkill:onUse(room, cardUseEvent) end +---@param room Room +---@param cardUseEvent CardUseStruct +---@param isEnding? bool +function ActiveSkill:onAction(room, cardUseEvent, finished) end + ---@param room Room ---@param cardEffectEvent CardEffectEvent | SkillEffectEvent function ActiveSkill:aboutToEffect(room, cardEffectEvent) end @@ -173,8 +225,8 @@ function ActiveSkill:onEffect(room, cardEffectEvent) end ---@param cardEffectEvent CardEffectEvent | SkillEffectEvent function ActiveSkill:onNullified(room, cardEffectEvent) end ----@param selected integer[] @ ids of selected players ---@param selected_cards integer[] @ ids of selected cards -function ActiveSkill:prompt(selected, selected_cards) return "" end +---@param selected_targets integer[] @ ids of selected players +function ActiveSkill:prompt(selected_cards, selected_targets) return "" end return ActiveSkill diff --git a/lua/core/skill_type/attack_range.lua b/lua/core/skill_type/attack_range.lua index 6a646532..aa48de04 100644 --- a/lua/core/skill_type/attack_range.lua +++ b/lua/core/skill_type/attack_range.lua @@ -9,6 +9,12 @@ function AttackRangeSkill:getCorrect(from) return 0 end +---@param from Player +---@return integer|nil +function AttackRangeSkill:getFixed(from) + return nil +end + function AttackRangeSkill:withinAttackRange(from, to) return false end diff --git a/lua/core/skill_type/distance.lua b/lua/core/skill_type/distance.lua index 681dfc01..b0d6db42 100644 --- a/lua/core/skill_type/distance.lua +++ b/lua/core/skill_type/distance.lua @@ -12,7 +12,7 @@ end ---@param from Player ---@param to Player ----@return integer +---@return integer|nil function DistanceSkill:getFixed(from, to) return nil end diff --git a/lua/core/skill_type/usable_skill.lua b/lua/core/skill_type/usable_skill.lua index f3429296..7fbbb082 100644 --- a/lua/core/skill_type/usable_skill.lua +++ b/lua/core/skill_type/usable_skill.lua @@ -3,7 +3,8 @@ ---@class UsableSkill : Skill ---@field public main_skill UsableSkill ---@field public max_use_time integer[] ----@field public expand_pile string +---@field public expand_pile? string | integer[] | fun(self: UsableSkill): integer[]|string? +---@field public derived_piles? string | string[] local UsableSkill = Skill:subclass("UsableSkill") function UsableSkill:initialize(name, frequency) @@ -35,41 +36,41 @@ function UsableSkill:withinTimesLimit(player, scope, card, card_name, to) for _, skill in ipairs(status_skills) do if skill:bypassTimesCheck(player, self, scope, card, to) then return true end end + card_name = card_name or card.trueName local temp_suf = table.simpleClone(MarkEnum.TempMarkSuffix) - table.insert(temp_suf, "-tmp") - return player:usedCardTimes(card_name, scope) < self:getMaxUseTime(player, scope, card, to) or - (player:getMark(MarkEnum.BypassTimesLimit) ~= 0 or - table.find(temp_suf, function(s) - return player:getMark(MarkEnum.BypassTimesLimit .. s) ~= 0 - end)) or - (to and (to:getMark(MarkEnum.BypassTimesLimitTo) ~= 0 or - table.find(temp_suf, function(s) - return to:getMark(MarkEnum.BypassTimesLimitTo .. s) ~= 0 - end))) -end + local card_temp_suf = table.simpleClone(MarkEnum.CardTempMarkSuffix) -function UsableSkill:withinDistanceLimit(player, isattack, card, to) - if to and to.dead then return false end - local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or Util.DummyTable - if not card and self.name:endsWith("_skill") then - card = Fk:cloneCard(self.name:sub(1, #self.name - 6)) + ---@param object Card|Player + ---@param markname string + ---@param suffixes string[] + ---@return boolean + local function hasMark(object, markname, suffixes) + if not object then return false end + for mark, _ in pairs(object.mark) do + if mark == markname then return true end + if mark:startsWith(markname .. "-") then + for _, suffix in ipairs(suffixes) do + if mark:find(suffix, 1, true) then return true end + end + end + end + return false end - for _, skill in ipairs(status_skills) do - if skill:bypassDistancesCheck(player, self, card, to) then return true end - end - local temp_suf = table.simpleClone(MarkEnum.TempMarkSuffix) - table.insert(temp_suf, "-tmp") - return (isattack and player:inMyAttackRange(to)) or - (player:distanceTo(to) > 0 and player:distanceTo(to) <= self:getDistanceLimit(player, card, to)) or - (player:getMark(MarkEnum.BypassDistancesLimit) ~= 0 or - table.find(temp_suf, function(s) - return player:getMark(MarkEnum.BypassDistancesLimit .. s) ~= 0 - end)) or - (to and (to:getMark(MarkEnum.BypassDistancesLimitTo) ~= 0 or - table.find(temp_suf, function(s) - return to:getMark(MarkEnum.BypassDistancesLimitTo .. s) ~= 0 - end))) + + return player:usedCardTimes(card_name, scope) < self:getMaxUseTime(player, scope, card, to) or + hasMark(card, MarkEnum.BypassTimesLimit, card_temp_suf) or + hasMark(player, MarkEnum.BypassTimesLimit, temp_suf) or + hasMark(to, MarkEnum.BypassTimesLimitTo, temp_suf) + -- (card and table.find(card_temp_suf, function(s) + -- return card:getMark(MarkEnum.BypassTimesLimit .. s) ~= 0 + -- end)) or + -- (table.find(temp_suf, function(s) + -- return player:getMark(MarkEnum.BypassTimesLimit .. s) ~= 0 + -- end)) or + -- (to and (table.find(temp_suf, function(s) + -- return to:getMark(MarkEnum.BypassTimesLimitTo .. s) ~= 0 + -- end))) end return UsableSkill diff --git a/lua/core/skill_type/view_as.lua b/lua/core/skill_type/view_as.lua index b8e695e9..e87e3b09 100644 --- a/lua/core/skill_type/view_as.lua +++ b/lua/core/skill_type/view_as.lua @@ -39,8 +39,12 @@ end ---@param cardUseStruct CardUseStruct function ViewAsSkill:beforeUse(player, cardUseStruct) end ----@param selected integer[] @ ids of selected players +---@param player Player +---@param cardUseStruct CardUseStruct +function ViewAsSkill:afterUse(player, cardUseStruct) end + ---@param selected_cards integer[] @ ids of selected cards -function ViewAsSkill:prompt(selected, selected_cards) return "" end +---@param selected_targets integer[] @ ids of selected players +function ViewAsSkill:prompt(selected_cards, selected_targets) return "" end return ViewAsSkill diff --git a/lua/core/util.lua b/lua/core/util.lua index ded584c7..1e2a823c 100644 --- a/lua/core/util.lua +++ b/lua/core/util.lua @@ -114,6 +114,73 @@ function fk.sorted_pairs(t, val_func, reverse) return iter, nil, 1 end +-- frequenly used filter & map functions + +--- 返回ID +Util.IdMapper = function(e) return e.id end +--- 根据卡牌ID返回卡牌 +Util.Id2CardMapper = function(id) return Fk:getCardById(id) end +--- 根据玩家ID返回玩家 +Util.Id2PlayerMapper = function(id) + return Fk:currentRoom():getPlayerById(id) +end +--- 返回武将名 +Util.NameMapper = function(e) return e.name end +--- 根据武将名返回武将 +Util.Name2GeneralMapper = function(e) return Fk.generals[e] end +--- 根据技能名返回技能 +Util.Name2SkillMapper = function(e) return Fk.skills[e] end +--- 返回译文 +Util.TranslateMapper = function(str) return Fk:translate(str) end + +-- for card preset + +--- 全局卡牌(包括自己)的canUse +Util.GlobalCanUse = function(self, player, card) + local room = Fk:currentRoom() + for _, p in ipairs(room.alive_players) do + if not (card and player:isProhibited(p, card)) then + return true + end + end +end + +--- AOE卡牌(不包括自己)的canUse +Util.AoeCanUse = function(self, player, card) + local room = Fk:currentRoom() + for _, p in ipairs(room.alive_players) do + if p ~= player and not (card and player:isProhibited(p, card)) then + return true + end + end +end + +--- 全局卡牌(包括自己)的onUse +Util.GlobalOnUse = function(self, room, cardUseEvent) + if not cardUseEvent.tos or #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then + cardUseEvent.tos = {} + for _, player in ipairs(room:getAlivePlayers()) do + if not room:getPlayerById(cardUseEvent.from):isProhibited(player, cardUseEvent.card) then + TargetGroup:pushTargets(cardUseEvent.tos, player.id) + end + end + end +end + +--- AOE卡牌(不包括自己)的onUse +Util.AoeOnUse = function(self, room, cardUseEvent) + if not cardUseEvent.tos or #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then + cardUseEvent.tos = {} + for _, player in ipairs(room:getOtherPlayers(room:getPlayerById(cardUseEvent.from))) do + if not room:getPlayerById(cardUseEvent.from):isProhibited(player, cardUseEvent.card) then + TargetGroup:pushTargets(cardUseEvent.tos, player.id) + end + end + end +end + +-- Table + ---@param func fun(element, index, array) function table:forEach(func) for i, v in ipairs(self) do @@ -164,66 +231,6 @@ function table:map(func) return ret end --- frequenly used filter & map functions - ---- 返回ID -Util.IdMapper = function(e) return e.id end ---- 根据卡牌ID返回卡牌 -Util.Id2CardMapper = function(id) return Fk:getCardById(id) end ---- 根据玩家ID返回玩家 -Util.Id2PlayerMapper = function(id) - return Fk:currentRoom():getPlayerById(id) -end ---- 返回武将名 -Util.NameMapper = function(e) return e.name end ---- 根据武将名返回武将 -Util.Name2GeneralMapper = function(e) return Fk.generals[e] end ---- 根据技能名返回技能 -Util.Name2SkillMapper = function(e) return Fk.skills[e] end ---- 返回译文 -Util.TranslateMapper = function(str) return Fk:translate(str) end - --- for card preset -Util.GlobalCanUse = function(self, player, card) - local room = Fk:currentRoom() - for _, p in ipairs(room.alive_players) do - if not (card and player:isProhibited(p, card)) then - return true - end - end -end - -Util.AoeCanUse = function(self, player, card) - local room = Fk:currentRoom() - for _, p in ipairs(room.alive_players) do - if p ~= player and not (card and player:isProhibited(p, card)) then - return true - end - end -end - -Util.GlobalOnUse = function(self, room, cardUseEvent) - if not cardUseEvent.tos or #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then - cardUseEvent.tos = {} - for _, player in ipairs(room:getAlivePlayers()) do - if not room:getPlayerById(cardUseEvent.from):isProhibited(player, cardUseEvent.card) then - TargetGroup:pushTargets(cardUseEvent.tos, player.id) - end - end - end -end - -Util.AoeOnUse = function(self, room, cardUseEvent) - if not cardUseEvent.tos or #TargetGroup:getRealTargets(cardUseEvent.tos) == 0 then - cardUseEvent.tos = {} - for _, player in ipairs(room:getOtherPlayers(room:getPlayerById(cardUseEvent.from))) do - if not room:getPlayerById(cardUseEvent.from):isProhibited(player, cardUseEvent.card) then - TargetGroup:pushTargets(cardUseEvent.tos, player.id) - end - end - end -end - ---@generic T ---@param self T[] ---@return T[] @@ -328,6 +335,7 @@ end function table:insertIfNeed(element) if not table.contains(self, element) then table.insert(self, element) + return true end end @@ -505,7 +513,9 @@ end function Stack:pop() if self.p == 0 then return nil end self.p = self.p - 1 - return self.t[self.p + 1] + local ret = self.t[self.p + 1] + self.t[self.p + 1] = nil + return ret end diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua index 821104ba..38d74e74 100644 --- a/lua/fk_ex.lua +++ b/lua/fk_ex.lua @@ -48,6 +48,11 @@ end local function readUsableSpecToSkill(skill, spec) readCommonSpecToSkill(skill, spec) assert(spec.main_skill == nil or spec.main_skill:isInstanceOf(UsableSkill)) + if type(spec.derived_piles) == "string" then + skill.derived_piles = {spec.derived_piles} + else + skill.derived_piles = spec.derived_piles or {} + end skill.main_skill = spec.main_skill skill.target_num = spec.target_num or skill.target_num skill.min_target_num = spec.min_target_num or skill.min_target_num @@ -170,11 +175,12 @@ function fk.CreateTriggerSkill(spec) end ---@class ActiveSkillSpec: UsableSkillSpec ----@field public can_use? fun(self: ActiveSkill, player: Player, card: Card): boolean? +---@field public can_use? fun(self: ActiveSkill, player: Player, card: Card, extra_data: any): boolean? ---@field public card_filter? fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_targets: integer[]): boolean? ----@field public target_filter? fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[], card: Card): boolean? +---@field public target_filter? fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[], card: Card, extra_data: any): boolean? ---@field public feasible? fun(self: ActiveSkill, selected: integer[], selected_cards: integer[]): boolean? ---@field public on_use? fun(self: ActiveSkill, room: Room, cardUseEvent: CardUseStruct): boolean? +---@field public on_action? fun(self: ActiveSkill, room: Room, cardUseEvent: CardUseStruct, finished: boolean): boolean? ---@field public about_to_effect? fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean? ---@field public on_effect? fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean? ---@field public on_nullified? fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean? @@ -190,8 +196,8 @@ function fk.CreateActiveSkill(spec) readUsableSpecToSkill(skill, spec) if spec.can_use then - skill.canUse = function(curSkill, player, card) - return spec.can_use(curSkill, player, card) and curSkill:isEffectable(player) + skill.canUse = function(curSkill, player, card, extra_data) + return spec.can_use(curSkill, player, card, extra_data) and curSkill:isEffectable(player) end end if spec.card_filter then skill.cardFilter = spec.card_filter end @@ -202,6 +208,7 @@ function fk.CreateActiveSkill(spec) skill.feasible = spec.feasible end if spec.on_use then skill.onUse = spec.on_use end + if spec.on_action then skill.onAction = spec.on_action end if spec.about_to_effect then skill.aboutToEffect = spec.about_to_effect end if spec.on_effect then skill.onEffect = spec.on_effect end if spec.on_nullified then skill.onNullified = spec.on_nullified end @@ -209,9 +216,9 @@ function fk.CreateActiveSkill(spec) if spec.interaction then skill.interaction = setmetatable({}, { - __call = function(self) + __call = function() if type(spec.interaction) == "function" then - return spec.interaction(self) + return spec.interaction(skill) else return spec.interaction end @@ -274,6 +281,10 @@ function fk.CreateViewAsSkill(spec) skill.beforeUse = spec.before_use end + if spec.after_use and type(spec.after_use) == "function" then + skill.afterUse = spec.after_use + end + return skill end @@ -320,19 +331,23 @@ end ---@class AttackRangeSpec: StatusSkillSpec ---@field public correct_func? fun(self: AttackRangeSkill, from: Player, to: Player): number? +---@field public fixed_func? fun(self: AttackRangeSkill, player: Player): number? ---@field public within_func? fun(self: AttackRangeSkill, from: Player, to: Player): boolean? ---@param spec AttackRangeSpec ---@return AttackRangeSkill function fk.CreateAttackRangeSkill(spec) assert(type(spec.name) == "string") - assert(type(spec.correct_func) == "function" or type(spec.within_func) == "function") + assert(type(spec.correct_func) == "function" or type(spec.fixed_func) == "function" or type(spec.within_func) == "function") local skill = AttackRangeSkill:new(spec.name) readStatusSpecToSkill(skill, spec) if spec.correct_func then skill.getCorrect = spec.correct_func end + if spec.fixed_func then + skill.getFixed = spec.fixed_func + end if spec.within_func then skill.withinAttackRange = spec.within_func end @@ -435,6 +450,7 @@ end ---@field public special_skills? string[] ---@field public is_damage_card? boolean ---@field public multiple_targets? boolean +---@field public is_passive? boolean local defaultCardSkill = fk.CreateActiveSkill{ name = "default_card_skill", @@ -477,6 +493,7 @@ local function readCardSpecToCard(card, spec) card.special_skills = spec.special_skills card.is_damage_card = spec.is_damage_card card.multiple_targets = spec.multiple_targets + card.is_passive = spec.is_passive end ---@param spec CardSpec @@ -609,9 +626,16 @@ end ---@field qml_path string | fun(name: string, value?: any, player?: Player): string ---@field how_to_show fun(name: string, value?: any, player?: Player): string? +-- TODO: 断连 不操作的人观看 现在只做了专为22设计的框 +---@class MiniGameSpec +---@field name string +---@field qml_path string | fun(player: Player, data: any): string +---@field update_func? fun(player: ServerPlayer, data: any) +---@field default_choice? fun(player: ServerPlayer, data: any): any + ---@class YuqiSpec ---@field name string ---@field feasible fun(current_data: any, old_data: any, extra_data: any): bool ---@field entry_filter fun(card: int, pos: int, pile: int[], data: any, extra_data: any): bool ---@field out_filter fun(card: int, data: any, extra_data: any): bool ----@field prompt? string | fun(data: any, extra_data: any): string \ No newline at end of file +---@field prompt? string | fun(data: any, extra_data: any): string diff --git a/lua/freekill.lua b/lua/freekill.lua index 27c169d9..24a90185 100644 --- a/lua/freekill.lua +++ b/lua/freekill.lua @@ -33,6 +33,7 @@ UsableSkill = require "core.skill_type.usable_skill" StatusSkill = require "core.skill_type.status_skill" Player = require "core.player" GameMode = require "core.game_mode" +AbstractRoom = require "core.abstract_room" UI = require "ui-util" -- 读取配置文件。 diff --git a/lua/server/ai/random_ai.lua b/lua/server/ai/random_ai.lua index 1108b17b..cfb12ac9 100644 --- a/lua/server/ai/random_ai.lua +++ b/lua/server/ai/random_ai.lua @@ -27,6 +27,7 @@ function RandomAI:useActiveSkill(skill, card) -- local max = skill:getMaxTargetNum(player, card) -- local min_card = skill:getMinCardNum() -- local max_card = skill:getMaxCardNum() + -- FIXME: ViewAsSkill can be buggy here for _ = 0, max_try_times do if skill:feasible(selected_targets, selected_cards, self.player, card) then break end local avail_targets = table.filter(room:getAlivePlayers(), function(p) @@ -121,6 +122,9 @@ random_cb["AskForUseActiveSkill"] = function(self, jsonData) for k, v in pairs(extra_data) do skill[k] = v end + if skill:isInstanceOf(ViewAsSkill) then + return RandomAI.useVSSkill(skill) + end return RandomAI.useActiveSkill(self, skill) end diff --git a/lua/server/ai/smart_ai.lua b/lua/server/ai/smart_ai.lua index 9d834459..b1020034 100644 --- a/lua/server/ai/smart_ai.lua +++ b/lua/server/ai/smart_ai.lua @@ -114,9 +114,7 @@ smart_cb["AskForUseActiveSkill"] = function(self, jsonData) local skillName, prompt, cancelable, extra_data = table.unpack(data) local skill = Fk.skills[skillName] - for k, v in pairs(extra_data) do - skill[k] = v - end + skill._extra_data = extra_data local ret = self:callFromTable(fk.ai_active_skill, nil, skillName, self, prompt, cancelable, extra_data) @@ -132,9 +130,24 @@ end -- 真的要考虑ViewAsSkill吗,害怕 --------------------------------------------------------- +-- 使用牌相关——同时见于主动使用和响应式使用。 --- 键是prompt的第一项或者牌名,优先prompt,其次name,实在不行trueName。 ---@type table -fk.ai_use_card = {} +fk.ai_use_card = setmetatable({}, { + __index = function(_, k) + -- FIXME: 感觉不妥 + local c = Fk.all_card_types[k] + if not c then return nil end + if c.type == Card.TypeEquip then + return function(self, pattern, prompt, cancelable, extra_data) + local slashes = self:getCards(k, "use", extra_data) + if #slashes == 0 then return nil end + + return self:buildUseReply(slashes[1].id) + end + end + end, +}) local defauld_use_card = function(self, pattern, _, cancelable, exdata) if cancelable then return nil end @@ -219,6 +232,9 @@ smart_cb["PlayCard"] = function(self) local card_names = {} for _, cd in ipairs(cards) do -- TODO: 视为技 + -- 视为技对应的function一般会返回一张印出来的卡,又要纳入新的考虑范围了 + -- 不过这种根据牌名判断的逻辑而言 可能需要调用多次视为技函数了 + -- 要用好空间换时间 table.insertIfNeed(card_names, cd.name) end -- TODO: 主动技 @@ -277,7 +293,7 @@ function SmartAI:isFriend(target) if Self.role == target.role then return true end local t = { "lord", "loyalist" } if table.contains(t, Self.role) and table.contains(t, target.role) then return true end - if Self.role == "renegade" or target.role == "renegade" then return math.random() < 0.5 end + if Self.role == "renegade" or target.role == "renegade" then return math.random() < 0.6 end return false end diff --git a/lua/server/event.lua b/lua/server/event.lua index 1936bfdf..1a977f02 100644 --- a/lua/server/event.lua +++ b/lua/server/event.lua @@ -33,6 +33,7 @@ fk.PreHpLost = 17 fk.HpLost = 18 fk.BeforeHpChanged = 19 fk.HpChanged = 20 +fk.BeforeMaxHpChanged = 97 fk.MaxHpChanged = 21 fk.EventLoseSkill = 22 diff --git a/lua/server/events/death.lua b/lua/server/events/death.lua index 67652760..884b775b 100644 --- a/lua/server/events/death.lua +++ b/lua/server/events/death.lua @@ -17,8 +17,10 @@ GameEvent.functions[GameEvent.Dying] = function(self) -- room.logic:trigger(fk.Dying, dyingPlayer, dyingStruct) local savers = room:getAlivePlayers() for _, p in ipairs(savers) do - if dyingPlayer.hp > 0 or dyingPlayer.dead or logic:trigger(fk.AskForPeaches, p, dyingStruct) then - break + if not p.dead then + if dyingPlayer.hp > 0 or dyingPlayer.dead or logic:trigger(fk.AskForPeaches, p, dyingStruct) then + break + end end end logic:trigger(fk.AskForPeachesDone, dyingPlayer, dyingStruct) diff --git a/lua/server/events/gameflow.lua b/lua/server/events/gameflow.lua index fe66b5b8..47370508 100644 --- a/lua/server/events/gameflow.lua +++ b/lua/server/events/gameflow.lua @@ -174,7 +174,7 @@ GameEvent.cleaners[GameEvent.Round] = function(self) p:setCardUseHistory("", 0, Player.HistoryRound) p:setSkillUseHistory("", 0, Player.HistoryRound) for name, _ in pairs(p.mark) do - if name:endsWith("-round") then + if name:find("-round", 1, true) then room:setPlayerMark(p, name, 0) end end @@ -182,11 +182,16 @@ GameEvent.cleaners[GameEvent.Round] = function(self) for cid, cmark in pairs(room.card_marks) do for name, _ in pairs(cmark) do - if name:endsWith("-round") then + if name:find("-round", 1, true) then room:setCardMark(Fk:getCardById(cid), name, 0) end end end + + for _, p in ipairs(room.players) do + p:filterHandcards() + room:broadcastProperty(p, "MaxCards") + end end GameEvent.prepare_funcs[GameEvent.Turn] = function(self) @@ -249,7 +254,7 @@ GameEvent.cleaners[GameEvent.Turn] = function(self) p:setCardUseHistory("", 0, Player.HistoryTurn) p:setSkillUseHistory("", 0, Player.HistoryTurn) for name, _ in pairs(p.mark) do - if name:endsWith("-turn") then + if name:find("-turn", 1, true) then room:setPlayerMark(p, name, 0) end end @@ -257,18 +262,23 @@ GameEvent.cleaners[GameEvent.Turn] = function(self) for cid, cmark in pairs(room.card_marks) do for name, _ in pairs(cmark) do - if name:endsWith("-turn") then + if name:find("-turn", 1, true) then room:setCardMark(Fk:getCardById(cid), name, 0) end end end + + for _, p in ipairs(room.players) do + p:filterHandcards() + room:broadcastProperty(p, "MaxCards") + end end GameEvent.functions[GameEvent.Phase] = function(self) local room = self.room local logic = room.logic - local player = self.data[1] + local player = self.data[1] ---@type Player if not logic:trigger(fk.EventPhaseStart, player) then if player.phase ~= Player.NotActive then logic:trigger(fk.EventPhaseProceeding, player) @@ -285,23 +295,26 @@ GameEvent.functions[GameEvent.Phase] = function(self) end, [Player.Judge] = function() local cards = player:getCardIds(Player.Judge) - for i = #cards, 1, -1 do - local card - card = player:removeVirtualEquip(cards[i]) + while #cards > 0 do + local cid = table.remove(cards) + if not cid then return end + local card = player:removeVirtualEquip(cid) if not card then - card = Fk:getCardById(cards[i]) + card = Fk:getCardById(cid) end - room:moveCardTo(card, Card.Processing, nil, fk.ReasonPut, self.name) + if table.contains(player:getCardIds(Player.Judge), cid) then + room:moveCardTo(card, Card.Processing, nil, fk.ReasonPut, self.name) - ---@type CardEffectEvent - local effect_data = { - card = card, - to = player.id, - tos = { {player.id} }, - } - room:doCardEffect(effect_data) - if effect_data.isCancellOut and card.skill then - card.skill:onNullified(room, effect_data) + ---@type CardEffectEvent + local effect_data = { + card = card, + to = player.id, + tos = { {player.id} }, + } + room:doCardEffect(effect_data) + if effect_data.isCancellOut and card.skill then + card.skill:onNullified(room, effect_data) + end end end end, @@ -369,7 +382,7 @@ GameEvent.cleaners[GameEvent.Phase] = function(self) p:setCardUseHistory("", 0, Player.HistoryPhase) p:setSkillUseHistory("", 0, Player.HistoryPhase) for name, _ in pairs(p.mark) do - if name:endsWith("-phase") then + if name:find("-phase", 1, true) then room:setPlayerMark(p, name, 0) end end @@ -377,9 +390,14 @@ GameEvent.cleaners[GameEvent.Phase] = function(self) for cid, cmark in pairs(room.card_marks) do for name, _ in pairs(cmark) do - if name:endsWith("-phase") then + if name:find("-phase", 1, true) then room:setCardMark(Fk:getCardById(cid), name, 0) end end end + + for _, p in ipairs(room.players) do + p:filterHandcards() + room:broadcastProperty(p, "MaxCards") + end end diff --git a/lua/server/events/hp.lua b/lua/server/events/hp.lua index 037b8c89..e62f786a 100644 --- a/lua/server/events/hp.lua +++ b/lua/server/events/hp.lua @@ -74,14 +74,14 @@ GameEvent.functions[GameEvent.ChangeHp] = function(self) room:sendLog{ type = "#LoseHP", from = player.id, - arg = 0 - num, + arg = 0 - data.num, } room:sendLogEvent("LoseHP", {}) elseif reason == "recover" then room:sendLog{ type = "#HealHP", from = player.id, - arg = num, + arg = data.num, } end @@ -136,16 +136,16 @@ GameEvent.functions[GameEvent.Damage] = function(self) assert(damageStruct.to:isInstanceOf(ServerPlayer)) local stages = { - {fk.PreDamage, damageStruct.from}, + {fk.PreDamage, "from"}, } if not damageStruct.isVirtualDMG then - table.insertTable(stages, { { fk.DamageCaused, damageStruct.from }, { fk.DamageInflicted, damageStruct.to } }) + table.insertTable(stages, { { fk.DamageCaused, "from" }, { fk.DamageInflicted, "to" } }) end for _, struct in ipairs(stages) do local event, player = table.unpack(struct) - if logic:trigger(event, player, damageStruct) or damageStruct.damage < 1 then + if logic:trigger(event, damageStruct[player], damageStruct) or damageStruct.damage < 1 then logic:breakEvent(false) end @@ -184,13 +184,13 @@ GameEvent.functions[GameEvent.Damage] = function(self) stages = { - {fk.Damage, damageStruct.from}, - {fk.Damaged, damageStruct.to}, + {fk.Damage, "from"}, + {fk.Damaged, "to"}, } for _, struct in ipairs(stages) do local event, player = table.unpack(struct) - logic:trigger(event, player, damageStruct) + logic:trigger(event, damageStruct[player], damageStruct) end return true @@ -290,12 +290,19 @@ end GameEvent.functions[GameEvent.ChangeMaxHp] = function(self) local player, num = table.unpack(self.data) local room = self.room - if num == 0 then + + ---@type MaxHpChangedData + local data = { + num = num, + } + + if room.logic:trigger(fk.BeforeMaxHpChanged, player, data) or data.num == 0 then return false end - player.maxHp = math.max(player.maxHp + num, 0) - room:broadcastProperty(player, "maxHp") + num = data.num + + room:setPlayerProperty(player, "maxHp", math.max(player.maxHp + num, 0)) room:sendLogEvent("ChangeMaxHp", { player = player.id, num = num, diff --git a/lua/server/events/init.lua b/lua/server/events/init.lua index a5e308f5..dbbe92b5 100644 --- a/lua/server/events/init.lua +++ b/lua/server/events/init.lua @@ -5,6 +5,8 @@ -- 某类事件对应的结束事件,其id刚好就是那个事件的相反数 -- GameEvent.EventFinish = -1 +GameEvent.Game = 0 + GameEvent.ChangeHp = 1 GameEvent.Damage = 2 GameEvent.LoseHp = 3 @@ -44,10 +46,10 @@ dofile "lua/server/events/pindian.lua" -- 20 = CardEffect GameEvent.ChangeProperty = 21 -dofile "lua/server/events/misc.lua" --- TODO: fix this -GameEvent.BreakEvent = 999 +-- 新的clear函数专用 +GameEvent.ClearEvent = 9999 +dofile "lua/server/events/misc.lua" for _, l in ipairs(Fk._custom_events) do local name, p, m, c, e = l.name, l.p, l.m, l.c, l.e @@ -58,6 +60,8 @@ for _, l in ipairs(Fk._custom_events) do end local eventTranslations = { + [GameEvent.Game] = "GameEvent.Game", + [GameEvent.ChangeHp] = "GameEvent.ChangeHp", [GameEvent.Damage] = "GameEvent.Damage", [GameEvent.LoseHp] = "GameEvent.LoseHp", @@ -80,7 +84,7 @@ local eventTranslations = { [GameEvent.ChangeProperty] = "GameEvent.ChangeProperty", - [GameEvent.BreakEvent] = "GameEvent.BreakEvent", + [GameEvent.ClearEvent] = "GameEvent.ClearEvent", } function GameEvent.static:translate(id) diff --git a/lua/server/events/misc.lua b/lua/server/events/misc.lua index ff30ffbc..db18d589 100644 --- a/lua/server/events/misc.lua +++ b/lua/server/events/misc.lua @@ -1,5 +1,9 @@ -- SPDX-License-Identifier: GPL-3.0-or-later +GameEvent.functions[GameEvent.Game] = function(self) + self.room.logic:run() +end + GameEvent.functions[GameEvent.ChangeProperty] = function(self) local data = table.unpack(self.data) local room = self.room @@ -15,12 +19,12 @@ GameEvent.functions[GameEvent.ChangeProperty] = function(self) if data.general and data.general ~= "" and data.general ~= player.general then local originalGeneral = Fk.generals[player.general] or Fk.generals["blank_shibing"] - local originalSkills = originalGeneral and originalGeneral:getSkillNameList() or Util.DummyTable + local originalSkills = originalGeneral and originalGeneral:getSkillNameList(true) or Util.DummyTable table.insertTableIfNeed(skills, table.map(originalSkills, function(e) return "-" .. e end)) local newGeneral = Fk.generals[data.general] or Fk.generals["blank_shibing"] - for _, name in ipairs(newGeneral:getSkillNameList()) do + for _, name in ipairs(newGeneral:getSkillNameList(data.isLord)) do local s = Fk.skills[name] if not s.relate_to_place or s.relate_to_place == "m" then table.insertIfNeed(skills, name) @@ -41,14 +45,14 @@ GameEvent.functions[GameEvent.ChangeProperty] = function(self) if data.deputyGeneral and data.deputyGeneral ~= player.deputyGeneral then local originalDeputy = Fk.generals[player.deputyGeneral] or Fk.generals["blank_shibing"] - local originalSkills = originalDeputy and originalDeputy:getSkillNameList() or Util.DummyTable + local originalSkills = originalDeputy and originalDeputy:getSkillNameList(true) or Util.DummyTable table.insertTableIfNeed(skills, table.map(originalSkills, function(e) return "-" .. e end)) if data.deputyGeneral ~= "" then local newDeputy = Fk.generals[data.deputyGeneral] or Fk.generals["blank_shibing"] - for _, name in ipairs(newDeputy:getSkillNameList()) do + for _, name in ipairs(newDeputy:getSkillNameList(data.isLord)) do local s = Fk.skills[name] if not s.relate_to_place or s.relate_to_place == "d" then table.insertIfNeed(skills, name) @@ -120,3 +124,26 @@ GameEvent.functions[GameEvent.ChangeProperty] = function(self) logic:trigger(fk.AfterPropertyChange, player, data) end + +GameEvent.functions[GameEvent.ClearEvent] = function(self) + local event = self.data[1] + local logic = self.room.logic + -- 不可中断 + Pcall(event.clear_func, event) + for _, f in ipairs(event.extra_clear_funcs) do + if type(f) == "function" then Pcall(f, event) end + end + + -- cleaner顺利执行完了,出栈吧 + local end_id = logic.current_event_id + 1 + if event.id ~= end_id - 1 then + logic.all_game_events[end_id] = event.event + logic.current_event_id = end_id + event.end_id = end_id + else + event.end_id = event.id + end + + logic.game_event_stack:pop() + logic.cleaner_stack:pop() +end diff --git a/lua/server/events/movecard.lua b/lua/server/events/movecard.lua index 764a388d..69e7b48b 100644 --- a/lua/server/events/movecard.lua +++ b/lua/server/events/movecard.lua @@ -166,7 +166,7 @@ GameEvent.functions[GameEvent.MoveCards] = function(self) local currentCard = Fk:getCardById(info.cardId) for name, _ in pairs(currentCard.mark) do - if name:endsWith("-inhand") and + if name:find("-inhand", 1, true) and realFromArea == Player.Hand and data.from then diff --git a/lua/server/events/usecard.lua b/lua/server/events/usecard.lua index b540c3bf..e1e0a0a8 100644 --- a/lua/server/events/usecard.lua +++ b/lua/server/events/usecard.lua @@ -164,12 +164,30 @@ GameEvent.functions[GameEvent.UseCard] = function(self) local room = self.room local logic = room.logic + if type(cardUseEvent.attachedSkillAndUser) == "table" then + local attachedSkillAndUser = table.simpleClone(cardUseEvent.attachedSkillAndUser) + self:addExitFunc(function() + if + type(attachedSkillAndUser) == "table" and + Fk.skills[attachedSkillAndUser.skillName] and + Fk.skills[attachedSkillAndUser.skillName].afterUse + then + Fk.skills[attachedSkillAndUser.skillName]:afterUse(room:getPlayerById(attachedSkillAndUser.user), cardUseEvent) + end + end) + cardUseEvent.attachedSkillAndUser = nil + end + if cardUseEvent.card.skill then cardUseEvent.card.skill:onUse(room, cardUseEvent) end local _card = sendCardEmotionAndLog(room, cardUseEvent) + if logic:trigger(fk.PreCardUse, room:getPlayerById(cardUseEvent.from), cardUseEvent) then + logic:breakEvent() + end + room:moveCardTo(cardUseEvent.card, Card.Processing, nil, fk.ReasonUse) local card = cardUseEvent.card @@ -196,10 +214,6 @@ GameEvent.functions[GameEvent.UseCard] = function(self) end end - if logic:trigger(fk.PreCardUse, room:getPlayerById(cardUseEvent.from), cardUseEvent) then - logic:breakEvent() - end - if not cardUseEvent.extraUse then room:getPlayerById(cardUseEvent.from):addCardUseHistory(cardUseEvent.card.trueName, 1) end @@ -270,6 +284,10 @@ GameEvent.functions[GameEvent.RespondCard] = function(self) playCardEmotionAndSound(room, room:getPlayerById(from), card) + if logic:trigger(fk.PreCardRespond, room:getPlayerById(cardResponseEvent.from), cardResponseEvent) then + logic:breakEvent() + end + room:moveCardTo(card, Card.Processing, nil, fk.ReasonResonpse) if #cardIds > 0 then room:sendFootnote(cardIds, { @@ -281,10 +299,6 @@ GameEvent.functions[GameEvent.RespondCard] = function(self) end end - if logic:trigger(fk.PreCardRespond, room:getPlayerById(cardResponseEvent.from), cardResponseEvent) then - logic:breakEvent() - end - logic:trigger(fk.CardResponding, room:getPlayerById(cardResponseEvent.from), cardResponseEvent) end @@ -335,9 +349,16 @@ GameEvent.functions[GameEvent.CardEffect] = function(self) if event == fk.PreCardEffect then if cardEffectEvent.from and logic:trigger(event, room:getPlayerById(cardEffectEvent.from), cardEffectEvent) then + if cardEffectEvent.to then + cardEffectEvent.nullifiedTargets = cardEffectEvent.nullifiedTargets or {} + table.insert(cardEffectEvent.nullifiedTargets, cardEffectEvent.to) + end logic:breakEvent() end elseif cardEffectEvent.to and logic:trigger(event, room:getPlayerById(cardEffectEvent.to), cardEffectEvent) then + cardEffectEvent.nullifiedTargets = cardEffectEvent.nullifiedTargets or {} + table.insert(cardEffectEvent.nullifiedTargets, cardEffectEvent.to) + logic:breakEvent() end diff --git a/lua/server/gameevent.lua b/lua/server/gameevent.lua index f7d3840b..b5401faf 100644 --- a/lua/server/gameevent.lua +++ b/lua/server/gameevent.lua @@ -13,9 +13,10 @@ ---@field public extra_clear_funcs fun(self:GameEvent)[] @ 事件结束时执行的自定义函数列表 ---@field public exit_func fun(self: GameEvent) @ 事件结束后执行的函数 ---@field public extra_exit_funcs fun(self:GameEvent)[] @ 事件结束后执行的自定义函数 +---@field public exec_ret boolean? @ exec函数的返回值,可能不存在 +---@field public status string @ ready, running, exiting, dead ---@field public interrupted boolean @ 事件是否是因为被中断而结束的,可能是防止事件或者被杀 ---@field public killed boolean @ 事件因为终止一切结算而被中断(所谓的“被杀”) ----@field public revived boolean @ 事件被killed,但因为在cleaner中发生而被复活 local GameEvent = class("GameEvent") ---@type (fun(self: GameEvent): bool)[] @@ -49,6 +50,7 @@ function GameEvent:initialize(event, ...) self.extra_clear_funcs = Util.DummyTable self.exit_func = GameEvent.exit_funcs[event] or dummyFunc self.extra_exit_funcs = Util.DummyTable + self.status = "ready" self.interrupted = false end @@ -85,10 +87,10 @@ end function GameEvent:findParent(eventType, includeSelf) if includeSelf and self.event == eventType then return self end local e = self.parent - repeat + while e do if e.event == eventType then return e end e = e.parent - until not e + end return nil end @@ -163,193 +165,35 @@ function GameEvent:searchEvents(eventType, n, func, endEvent) return ret end -function GameEvent:clear() - local clear_co = coroutine.create(function() - self:clear_func() - for _, f in ipairs(self.extra_clear_funcs) do - if type(f) == "function" then f(self) end - end - end) +function GameEvent:exec() + local room = self.room + local logic = room.logic + if self.status ~= "ready" then return true end - local zhuran_jmp, zhuran_msg -- SB老朱然 + self.parent = logic:getCurrentEvent() - while true do - local err, yield_result, extra_yield_result = coroutine.resume(clear_co) + if self:prepare_func() then return true end - if err == false then - -- handle error, then break - if not string.find(yield_result, "__manuallyBreak") then - fk.qCritical(yield_result .. "\n" .. debug.traceback(clear_co)) - end - coroutine.close(clear_co) - break - end + logic:pushEvent(self) - if yield_result == "__handleRequest" then - -- yield to requestLoop - coroutine.yield(yield_result, extra_yield_result) + local co = coroutine.create(self.main_func) + self._co = co + self.status = "running" - elseif type(yield_result) == "table" and yield_result.class - and yield_result:isInstanceOf(GameEvent) and self ~= yield_result then + coroutine.yield(self, "__newEvent") - -- 不是,谁TM还在cleaner里面玩老朱然啊 - -- 总之,cleaner不能断 - -- 倒是没必要手动resume,新一轮while true会自动resume,只要把返回值 - -- 传回去就行 - - -- 一般来说都是由cleaner中的trigger引起 - -- 以胆守合击为例就是trigger -> SkillEffect事件 -> UseCard事件 -> 胆守 - -- 此时胆守的话最后从SkillEffect事件的exec内部yield出来 - -- 当前协程就应该正在执行room:useSkill函数,resume会去只会让那个函数返回 - - if zhuran_jmp == nil or zhuran_jmp.id > yield_result.id then - zhuran_jmp = yield_result - zhuran_msg = extra_yield_result - end - - -- 自己本来应该被杀的但是因为自己正在执行self:clear()而逃过一劫啊 - -- 还是得标记一下被杀才行,顺便因为实际上没死所以标记被复活 - self.killed = true - self.revived = true - -- 什么都不做,等下轮while自己resume - else - coroutine.close(clear_co) - break - end - end - - -- cleaner顺利执行完了,出栈吧 - local logic = RoomInstance.logic - local end_id = logic.current_event_id + 1 - if self.id ~= end_id - 1 then - logic.all_game_events[end_id] = self.event - logic.current_event_id = end_id - self.end_id = end_id - else - self.end_id = self.id - end - - logic.game_event_stack:pop() - - -- 好了确保cleaner走完了,此时中断就会进入下层事件的正常中断处理 - if zhuran_jmp then - coroutine.close(self._co) - coroutine.yield(zhuran_jmp, zhuran_msg) - - -- 此时仍可能出现在插结在其他事件的clear函数中 - -- 但就算被交付回去了,也能安然返回而不是继续while - -- 但愿如此吧 - end - - -- 保险而已,其实如果被杀的话应该已经在前面的yield终止了 - -- 但担心cleaner嵌套(三国杀是这样的)还是补一刀 - if self.killed then return end - - -- 恭喜没被杀掉,我们来执行一些事件结束之后的结算吧 Pcall(self.exit_func, self) for _, f in ipairs(self.extra_exit_funcs) do if type(f) == "function" then Pcall(f, self) end end -end -local function breakEvent(self, extra_yield_result) - local cancelEvent = GameEvent:new(GameEvent.BreakEvent, self) - cancelEvent.toId = self.id - local notcanceled = cancelEvent:exec() - local ret, extra_ret = false, nil - if not notcanceled then - self.interrupted = true - self:clear() - ret = true - extra_ret = extra_yield_result - end - return ret, extra_ret -end - -function GameEvent:exec() - local room = self.room - local logic = room.logic - local ret = false -- false or nil means this event is running normally - local extra_ret - self.parent = logic:getCurrentEvent() - - if self:prepare_func() then return true end - - logic.game_event_stack:push(self) - - logic.current_event_id = logic.current_event_id + 1 - self.id = logic.current_event_id - logic.all_game_events[self.id] = self - logic.event_recorder[self.event] = logic.event_recorder[self.event] or {} - table.insert(logic.event_recorder[self.event], self) - - local co = coroutine.create(self.main_func) - self._co = co - while true do - local err, yield_result, extra_yield_result = coroutine.resume(co) - - if err == false then - -- handle error, then break - if not string.find(yield_result, "__manuallyBreak") then - fk.qCritical(yield_result .. "\n" .. debug.traceback(co)) - end - self.interrupted = true - self:clear() - ret = true - coroutine.close(co) - break - end - - if yield_result == "__handleRequest" then - -- yield to requestLoop - coroutine.yield(yield_result, extra_yield_result) - - elseif type(yield_result) == "table" and yield_result.class - and yield_result:isInstanceOf(GameEvent) then - - if self ~= yield_result then - -- yield to corresponding GameEvent, first pop self from stack - self.interrupted = true - self.killed = true -- 老朱然!你不得好死 - self:clear() - -- logic.game_event_stack:pop(self) - coroutine.close(co) - - -- then, call yield - coroutine.yield(yield_result, extra_yield_result) - - -- 如果是在cleaner/exit里面发生此类中断的话是会被cleaner原地返回的 - -- 此时正常执行程序流就变成继续while循环了,这是不行的 - break - elseif extra_yield_result == "__breakEvent" then - if breakEvent(self) then - coroutine.close(co) - break - end - end - - elseif yield_result == "__breakEvent" then - -- try to break this event - if breakEvent(self) then - coroutine.close(co) - break - end - - else - -- normally exit, simply break the loop - self:clear() - extra_ret = yield_result - coroutine.close(co) - break - end - end - - return ret, extra_ret + return self.interrupted, self.exec_ret end function GameEvent:shutdown() + if self.status ~= "running" then return end -- yield to self and break coroutine.yield(self, "__breakEvent") end diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua index 6818aedf..71c556d7 100644 --- a/lua/server/gamelogic.lua +++ b/lua/server/gamelogic.lua @@ -7,6 +7,7 @@ ---@field public refresh_skill_table table ---@field public skills string[] ---@field public game_event_stack Stack +---@field public cleaner_stack Stack ---@field public role_table string[][] ---@field public all_game_events GameEvent[] ---@field public event_recorder table @@ -20,11 +21,12 @@ function GameLogic:initialize(room) self.refresh_skill_table = {} self.skills = {} -- skillName[] self.game_event_stack = Stack:new() + self.cleaner_stack = Stack:new() self.all_game_events = {} self.event_recorder = {} self.current_event_id = 0 self.specific_events_id = { - [GameEvent.Damage] = 0, + [GameEvent.Damage] = 1, } self.role_table = { @@ -216,6 +218,10 @@ function GameLogic:attachSkillToPlayers() local addRoleModSkills = function(player, skillName) local skill = Fk.skills[skillName] + if not skill then + fk.qCritical("Skill: "..skillName.." doesn't exist!") + return + end if skill.lordSkill and (player.role ~= "lord" or #room.players < 5) then return end @@ -389,9 +395,9 @@ function GameLogic:trigger(event, target, data, refresh_only) skill_names = table.map(table.filter(skills, filter_func), Util.NameMapper) broken = broken or (event == fk.AskForPeaches - and room:getPlayerById(data.who).hp > 0) or cur_event.revived - -- ^^^^^^^^^^^^^^^^^ - -- 如果事件复活了,那么其实说明事件已经死过了,赶紧break掉 + and room:getPlayerById(data.who).hp > 0) or + (table.contains({fk.PreDamage, fk.DamageCaused, fk.DamageInflicted}, event) and data.damage < 1) or + cur_event.killed if broken then break end end @@ -408,6 +414,146 @@ function GameLogic:trigger(event, target, data, refresh_only) return broken end +-- 此为启动事件管理器并启动第一个事件的初始函数 +function GameLogic:start() + local root_event = GameEvent:new(GameEvent.Game) + + self:pushEvent(root_event) + + -- 此时的协程:room.main_co + -- 事件管理器协程,同时也是Game事件 + -- 当新事件想要exec时,就切回此处,由这里负责调度协程 + -- 一个事件结束后也切回此处,然后resume + local co = coroutine.create(root_event.main_func) + root_event._co = co + + local jump_to -- shutdown函数用 + + while true do + -- 对于cleaner和正常事件,处理更后面来的 + local ne = self:getCurrentEvent() + local ce = self:getCurrentCleaner() + local e = ce and (ce.id >= ne.id and ce or ne) or ne + + -- 如果正在jump的话,判断是否需要继续clean,否则正常继续 + if e == ne and jump_to ~= nil then + e.interrupted = true + e.killed = e ~= jump_to + self:clearEvent(e) + coroutine.close(e._co) + e.status = "dead" + if e == jump_to then jump_to = nil end -- shutdown结束了 + e = self:getCurrentCleaner() + end + + if not e then -- 没有事件,按理说不应该,平局处理 + self.room:gameOver("") + end + + -- ret, evt解释: + -- * true, nil: 中止 + -- * false, nil: 正常结束 + -- * true, GameEvent: 中止直到某event + -- * false, GameEvent: 未结束,插入新event + -- 若jump_to不为nil,表示正在中断至某某事件 + local ret, evt = self:resumeEvent(e) + if evt == nil then + e.interrupted = ret + self:clearEvent(e) + coroutine.close(e._co) + e.status = "dead" + elseif ret == true then + -- 跳到越早发生的事件越好 + if not jump_to then + jump_to = evt + else + jump_to = jump_to.id < evt.id and jump_to or evt + end + end + end +end + +---@param event GameEvent +function GameLogic:pushEvent(event) + self.game_event_stack:push(event) + + self.current_event_id = self.current_event_id + 1 + event.id = self.current_event_id + self.all_game_events[event.id] = event + self.event_recorder[event.event] = self.event_recorder[event.event] or {} + table.insert(self.event_recorder[event.event], event) +end + +-- 一般来说从GameEvent:exec切回start再被start调用 +-- 作用是启动新事件 都是结构差不多的函数 +---@param event GameEvent +---@return boolean, GameEvent? +function GameLogic:resumeEvent(event, ...) + local ret, evt + + local co = event._co + + while true do + local err, yield_result, extra_yield_result = coroutine.resume(co, ...) + + if err == false then + -- handle error, then break + if not string.find(yield_result, "__manuallyBreak") then + fk.qCritical(yield_result .. "\n" .. debug.traceback(co)) + end + ret = true + break + end + + if yield_result == "__handleRequest" then + -- yield to requestLoop + coroutine.yield(yield_result, extra_yield_result) + + elseif type(yield_result) == "table" and yield_result.class + and yield_result:isInstanceOf(GameEvent) then + + if extra_yield_result == "__newEvent" then + ret, evt = false, yield_result + break + elseif extra_yield_result == "__breakEvent" then + ret, evt = true, yield_result + if event.event ~= GameEvent.ClearEvent then break end + end + + elseif yield_result == "__breakEvent" then + ret = true + if event.event ~= GameEvent.ClearEvent then break end + + else + ret = false + event.exec_ret = yield_result + break + end + end + + return ret, evt +end + +---@return GameEvent +function GameLogic:getCurrentCleaner() + return self.cleaner_stack.t[self.cleaner_stack.p] +end + +-- 事件中的清理。 +-- cleaner单独开协程运行,exitFunc须转到上个事件的协程内执行 +-- 注意插入新event +---@param event GameEvent +function GameLogic:clearEvent(event) + if event.event == GameEvent.ClearEvent then return end + if event.status == "exiting" then return end + event.status = "exiting" + local ce = GameEvent(GameEvent.ClearEvent, event) + ce.id = self.current_event_id + local co = coroutine.create(ce.main_func) + ce._co = co + self.cleaner_stack:push(ce) +end + ---@return GameEvent function GameLogic:getCurrentEvent() return self.game_event_stack.t[self.game_event_stack.p] @@ -453,6 +599,151 @@ function GameLogic:getEventsOfScope(eventType, n, func, scope) return start_event:searchEvents(eventType, n, func) end +-- 在指定历史范围中找符合条件的事件(逆序) +---@param eventType integer @ 要查找的事件类型 +---@param func fun(e: GameEvent): boolean @ 过滤用的函数 +---@param n integer @ 最多找多少个 +---@param end_id integer @ 查询历史范围:从最后的事件开始逆序查找直到id为end_id的事件(不含) +---@return GameEvent[] @ 找到的符合条件的所有事件,最多n个但不保证有n个 +function GameLogic:getEventsByRule(eventType, n, func, end_id) + local ret = {} + local events = self.event_recorder[eventType] or Util.DummyTable + for i = #events, 1, -1 do + local e = events[i] + if e.id <= end_id then break end + if func(e) then + table.insert(ret, e) + if #ret >= n then break end + end + end + return ret +end + + +--- 获取实际的伤害事件 +---@param n integer @ 最多找多少个 +---@param func fun(e: GameEvent): boolean @ 过滤用的函数 +---@param scope? integer @ 查询历史范围,只能是当前阶段/回合/轮次 +---@param end_id? integer @ 查询历史范围:从最后的事件开始逆序查找直到id为end_id的事件(不含) +---@return GameEvent[] @ 找到的符合条件的所有事件,最多n个但不保证有n个 +function GameLogic:getActualDamageEvents(n, func, scope, end_id) + if not end_id then + scope = scope or Player.HistoryTurn + end + + n = n or 1 + func = func or Util.TrueFunc + + local eventType = GameEvent.Damage + local ret = {} + local endIdRecorded + local tempEvents = {} + + local addTempEvents = function(reverse) + if #tempEvents > 0 and #ret < n then + table.sort(tempEvents, function(a, b) + if reverse then + return a.data[1].dealtRecorderId > b.data[1].dealtRecorderId + else + return a.data[1].dealtRecorderId < b.data[1].dealtRecorderId + end + end) + + for _, e in ipairs(tempEvents) do + table.insert(ret, e) + if #ret >= n then return true end + end + end + + endIdRecorded = nil + tempEvents = {} + + return false + end + + if scope then + local event = self:getCurrentEvent() + local start_event ---@type GameEvent + if scope == Player.HistoryGame then + start_event = self.all_game_events[1] + elseif scope == Player.HistoryRound then + start_event = event:findParent(GameEvent.Round, true) + elseif scope == Player.HistoryTurn then + start_event = event:findParent(GameEvent.Turn, true) + elseif scope == Player.HistoryPhase then + start_event = event:findParent(GameEvent.Phase, true) + end + + if not start_event then return {} end + + local events = self.event_recorder[eventType] or Util.DummyTable + local from = start_event.id + local to = start_event.end_id + if math.abs(to) == 1 then to = #self.all_game_events end + + for _, v in ipairs(events) do + local damageStruct = v.data[1] + if damageStruct.dealtRecorderId then + if endIdRecorded and v.id > endIdRecorded then + local result = addTempEvents() + if result then + return ret + end + end + + if v.id >= from and v.id <= to then + if not endIdRecorded and v.end_id > -1 and v.end_id > v.id then + endIdRecorded = v.end_id + end + + if func(v) then + if endIdRecorded then + table.insert(tempEvents, v) + else + table.insert(ret, v) + end + end + end + if #ret >= n then break end + end + end + + addTempEvents() + else + local events = self.event_recorder[eventType] or Util.DummyTable + + for i = #events, 1, -1 do + local e = events[i] + if e.id <= end_id then break end + + local damageStruct = e.data[1] + if damageStruct.dealtRecorderId then + if e.end_id == -1 or (endIdRecorded and endIdRecorded > e.end_id) then + local result = addTempEvents(true) + if result then + return ret + end + + if func(e) then + table.insert(ret, e) + end + else + endIdRecorded = e.end_id + if func(e) then + table.insert(tempEvents, e) + end + end + + if #ret >= n then break end + end + end + + addTempEvents(true) + end + + return ret +end + function GameLogic:dumpEventStack(detailed) local top = self:getCurrentEvent() local i = self.game_event_stack.p @@ -515,6 +806,7 @@ end function GameLogic:breakTurn() local event = self:getCurrentEvent():findParent(GameEvent.Turn) + if not event then return end event:shutdown() end diff --git a/lua/server/mark_enum.lua b/lua/server/mark_enum.lua index e9308841..f13cb258 100644 --- a/lua/server/mark_enum.lua +++ b/lua/server/mark_enum.lua @@ -19,23 +19,38 @@ MarkEnum.MinusMaxCards = "MinusMaxCards" ---于本回合内减少标记值数量的手牌上限 MarkEnum.MinusMaxCardsInTurn = "MinusMaxCards-turn" ----使用牌无次数限制,可带清除标记后缀(-tmp为请求专用) +---使用牌无次数限制 MarkEnum.BypassTimesLimit = "BypassTimesLimit" ----使用牌无距离限制,可带清除标记后缀(-tmp为请求专用) +---使用牌无距离限制 MarkEnum.BypassDistancesLimit = "BypassDistancesLimit" ----对其使用牌无次数限制,可带清除标记后缀 +---对其使用牌无次数限制 MarkEnum.BypassTimesLimitTo = "BypassTimesLimitTo" ----对其使用牌无距离限制,可带清除标记后缀 +---对其使用牌无距离限制 MarkEnum.BypassDistancesLimitTo = "BypassDistancesLimitTo" ----非锁定技失效,可带清除标记后缀 +---非锁定技失效 MarkEnum.UncompulsoryInvalidity = "UncompulsoryInvalidity" ----不可明置,可带清除标记后缀(值为表,m - 主将, d - 副将) +---不可明置(值为表,m - 主将, d - 副将) MarkEnum.RevealProhibited = "RevealProhibited" ----不计入距离、座次后缀,可带清除标记后缀 +---不计入距离、座次后缀 MarkEnum.PlayerRemoved = "PlayerRemoved" ---各种清除标记后缀 +--- +---phase:阶段结束后 +--- +---turn:回合结束后 +--- +---round:轮次结束后 MarkEnum.TempMarkSuffix = { "-phase", "-turn", "-round" } ---卡牌标记版本的清除标记后缀 -MarkEnum.CardTempMarkSuffix = { "-phase", "-turn", "-round", "-inhand" } +--- +---phase:阶段结束后 +--- +---turn:回合结束后 +--- +---round:轮次结束后 +--- +---inhand:离开手牌区后 +MarkEnum.CardTempMarkSuffix = { "-phase", "-turn", "-round", + "-inhand" } diff --git a/lua/server/request.lua b/lua/server/request.lua index 329aa148..2d36437d 100644 --- a/lua/server/request.lua +++ b/lua/server/request.lua @@ -159,27 +159,22 @@ end request_handlers["surrender"] = function(room, id, reqlist) local player = room:getPlayerById(id) if not player then return end - local logic = room.logic - local curEvent = logic:getCurrentEvent() - if curEvent then - curEvent:addCleaner( - function() - player.surrendered = true - room:broadcastProperty(player, "surrendered") - local mode = Fk.game_modes[room.settings.gameMode] - local winner = Pcall(mode.getWinner, mode, player) - if winner ~= nil then - room:gameOver(winner) - end - -- 以防万一 - player.surrendered = false - room.hasSurrendered = false - end - ) - room.hasSurrendered = true - room:doBroadcastNotify("CancelRequest", "") - end + room.hasSurrendered = true + player.surrendered = true + room:doBroadcastNotify("CancelRequest", "") +end + +request_handlers["updatemini"] = function(room, pid, reqlist) + local player = room:getPlayerById(pid) + local data = player.mini_game_data + if not data then return end + local game = Fk.mini_games[data.type] + if not (game and game.update_func) then return end + local dat = table.simpleClone(reqlist) + table.remove(dat, 1) + table.remove(dat, 1) + game.update_func(player, dat) end request_handlers["newroom"] = function(s, id) diff --git a/lua/server/room.lua b/lua/server/room.lua index ab836da6..5f6b9688 100644 --- a/lua/server/room.lua +++ b/lua/server/room.lua @@ -3,7 +3,7 @@ --- Room是fk游戏逻辑运行的主要场所,同时也提供了许多API函数供编写技能使用。 --- --- 一个房间中只有一个Room实例,保存在RoomInstance全局变量中。 ----@class Room : Object +---@class Room : AbstractRoom ---@field public room fk.Room @ C++层面的Room类实例,别管他就是了,用不着 ---@field public id integer @ 房间的id ---@field private main_co any @ 本房间的主协程 @@ -15,7 +15,6 @@ ---@field public game_finished boolean @ 游戏是否已经结束 ---@field public timeout integer @ 出牌时长上限 ---@field public tag table @ Tag清单,其实跟Player的标记是差不多的东西 ----@field public banners table @ 左上角显示点啥好呢? ---@field public general_pile string[] @ 武将牌堆,这是可用武将名的数组 ---@field public draw_pile integer[] @ 摸牌堆,这是卡牌id的数组 ---@field public discard_pile integer[] @ 弃牌堆,也是卡牌id的数组 @@ -23,14 +22,13 @@ ---@field public void integer[] @ 从游戏中除外区,一样的是卡牌id数组 ---@field public card_place table @ 每个卡牌的id对应的区域,一张表 ---@field public owner_map table @ 每个卡牌id对应的主人,表的值是那个玩家的id,可能是nil ----@field public status_skills Skill[] @ 这个房间中含有的状态技列表 ---@field public settings table @ 房间的额外设置,差不多是json对象 ---@field public logic GameLogic @ 这个房间使用的游戏逻辑,可能根据游戏模式而变动 ---@field public request_queue table ---@field public request_self table ---@field public skill_costs table @ 存放skill.cost_data用 ---@field public card_marks table @ 存放card.mark之用 -local Room = class("Room") +local Room = AbstractRoom:subclass("Room") -- load classes used by the game GameEvent = require "server.gameevent" @@ -67,18 +65,14 @@ dofile "lua/server/ai/init.lua" --- 构造函数。别去构造 ---@param _room fk.Room function Room:initialize(_room) + AbstractRoom.initialize(self) self.room = _room self.id = _room:getId() - self.players = {} - self.alive_players = {} - self.observers = {} - self.current = nil self.game_started = false self.game_finished = false self.timeout = _room:getTimeout() self.tag = {} - self.banners = {} self.general_pile = {} self.draw_pile = {} self.discard_pile = {} @@ -86,16 +80,8 @@ function Room:initialize(_room) self.void = {} self.card_place = {} self.owner_map = {} - self.status_skills = {} - for class, skills in pairs(Fk.global_status_skill) do - self.status_skills[class] = {table.unpack(skills)} - end self.request_queue = {} self.request_self = {} - self.skill_costs = {} - self.card_marks = {} - self.filtered_cards = {} - self.printed_cards = {} self.settings = json.decode(self.room:settings()) self.disabled_packs = self.settings.disabledPack @@ -254,9 +240,11 @@ function Room:run() end local mode = Fk.game_modes[self.settings.gameMode] - self.logic = (mode.logic and mode.logic() or GameLogic):new(self) - if mode.rule then self.logic:addTriggerSkill(mode.rule) end - self.logic:run() + local logic = (mode.logic and mode.logic() or GameLogic):new(self) + self.logic = logic + if mode.rule then logic:addTriggerSkill(mode.rule) end + -- GameEvent(GameEvent.Game):exec() + logic:start() end ------------------------------------------------------------------------ @@ -566,15 +554,10 @@ function Room:removeTag(tag_name) end function Room:setBanner(name, value) - if value == 0 then value = nil end - self.banners[name] = value + AbstractRoom.setBanner(self, name, value) self:doBroadcastNotify("SetBanner", json.encode{ name, value }) end -function Room:getBanner(name) - return self.banners[name] -end - ---@return boolean local function execGameEvent(type, ...) local event = GameEvent:new(type, ...) @@ -704,6 +687,29 @@ function Room:doBroadcastNotify(command, jsonData, players) end end +---@param room Room +local function surrenderCheck(room) + if not room.hasSurrendered then return end + local player = table.find(room.players, function(p) + return p.surrendered + end) + if not player then + room.hasSurrendered = false + return + end + room:broadcastProperty(player, "surrendered") + local mode = Fk.game_modes[room.settings.gameMode] + local winner = Pcall(mode.getWinner, mode, player) + if winner ~= "" then + room:gameOver(winner) + end + + -- 以防万一 + player.surrendered = false + room:broadcastProperty(player, "surrendered") + room.hasSurrendered = false +end + --- 向某个玩家发起一次Request。 ---@param player ServerPlayer @ 发出这个请求的目标玩家 ---@param command string @ 请求的类型 @@ -720,6 +726,7 @@ function Room:doRequest(player, command, jsonData, wait) local ret = player:waitForReply(self.timeout) player.serverplayer:setBusy(false) player.serverplayer:setThinking(false) + surrenderCheck(self) return ret end end @@ -748,6 +755,8 @@ function Room:doBroadcastRequest(command, players, jsonData) p.serverplayer:setBusy(false) p.serverplayer:setThinking(false) end + + surrenderCheck(self) end --- 向多名玩家发出竞争请求。 @@ -815,6 +824,7 @@ function Room:doRaceRequest(command, players, jsonData) p.serverplayer:setThinking(false) end + surrenderCheck(self) return ret end @@ -870,11 +880,11 @@ function Room:notifyMoveCards(players, card_moves, forceVisible) if not (move.moveVisible or forceVisible or containArea(move.toArea, move.to and p.isBuddy and p:isBuddy(move.to))) then for _, info in ipairs(move.moveInfo) do if not containArea(info.fromArea, move.from == p.id) then - info.cardId = -1 + info.cardId = -1 + end end end end - end p:doNotify("MoveCards", json.encode(arg)) end end @@ -1033,7 +1043,7 @@ function Room:notifySkillInvoked(player, skill_name, skill_type) self:doAnimate("InvokeUltSkill", { name = skill_name, player = player.id, - deputy = player.deputyGeneral and player.deputyGeneral ~= "" and table.contains(Fk.generals[player.deputyGeneral]:getSkillNameList(), skill_name), + deputy = player.deputyGeneral and player.deputyGeneral ~= "" and table.contains(Fk.generals[player.deputyGeneral]:getSkillNameList(true), skill_name), }) self:delay(2000) end @@ -1193,7 +1203,7 @@ function Room:askForDiscard(player, minNum, maxNum, includeEquip, skillName, can toDiscard = ret.cards else if cancelable then return {} end - toDiscard = table.random(canDiscards, minNum) + toDiscard = table.random(canDiscards, minNum) ---@type integer[] end if not skipDiscard then @@ -1270,7 +1280,7 @@ function Room:askForCard(player, minNum, maxNum, includeEquip, skillName, cancel pattern = pattern, expand_pile = expand_pile, } - local prompt = prompt or ("#AskForCard:::" .. maxNum .. ":" .. minNum) + prompt = prompt or ("#AskForCard:::" .. maxNum .. ":" .. minNum) local _, ret = self:askForUseActiveSkill(player, "choose_cards_skill", prompt, cancelable, data, no_indicate) if ret then chosenCards = ret.cards @@ -1383,6 +1393,94 @@ function Room:askForChooseCardsAndPlayers(player, minCardNum, maxCardNum, target end end +--- 询问将卡牌分配给任意角色。 +---@param player ServerPlayer @ 要询问的玩家 +---@param cards? integer[] @ 要分配的卡牌。默认拥有的所有牌 +---@param targets? ServerPlayer[] @ 可以获得卡牌的角色。默认所有存活角色 +---@param skillName? string @ 技能名,影响焦点信息。默认为“分配” +---@param minNum? integer @ 最少交出的卡牌数,默认0 +---@param maxNum? integer @ 最多交出的卡牌数,默认所有牌 +---@param prompt? string @ 询问提示信息 +---@param expand_pile? string @ 可选私人牌堆名称,如要分配你武将牌上的牌请填写 +---@param skipMove? boolean @ 是否跳过移动。默认不跳过 +---@param single_max? integer|table @ 限制每人能获得的最大牌数。输入整数或(以角色id为键以整数为值)的表 +---@return table @ 返回一个表,键为角色id,值为分配给其的牌id数组 +function Room:askForYiji(player, cards, targets, skillName, minNum, maxNum, prompt, expand_pile, skipMove, single_max) + targets = targets or self.alive_players + cards = cards or player:getCardIds("he") + local _cards = table.simpleClone(cards) + targets = table.map(targets, Util.IdMapper) + self:sortPlayersByAction(targets) + skillName = skillName or "distribution_select_skill" + minNum = minNum or 0 + maxNum = maxNum or #cards + local list = {} + for _, pid in ipairs(targets) do + list[pid] = {} + end + local toStr = function(int) return string.format("%d", int) end + local residueMap = {} + if type(single_max) == "table" then + for pid, v in pairs(single_max) do + residueMap[toStr(pid)] = v + end + end + local residue_sum = 0 + local residue_num = type(single_max) == "number" and single_max or 9999 + for _, pid in ipairs(targets) do + residueMap[toStr(pid)] = residueMap[toStr(pid)] or residue_num + residue_sum = residue_sum + residueMap[toStr(pid)] + end + minNum = math.min(minNum, #_cards, residue_sum) + local data = { + cards = _cards, + max_num = maxNum, + targets = targets, + residued_list = residueMap, + expand_pile = expand_pile + } + p(json.encode(residueMap)) + + while maxNum > 0 and #_cards > 0 do + data.max_num = maxNum + prompt = prompt or ("#AskForDistribution:::"..minNum..":"..maxNum) + local success, dat = self:askForUseActiveSkill(player, "distribution_select_skill", prompt, minNum == 0, data, true) + if success and dat then + local to = dat.targets[1] + local give_cards = dat.cards + for _, id in ipairs(give_cards) do + table.insert(list[to], id) + table.removeOne(_cards, id) + self:setCardMark(Fk:getCardById(id), "@DistributionTo", Fk:translate(self:getPlayerById(to).general)) + end + minNum = math.max(0, minNum - #give_cards) + maxNum = maxNum - #give_cards + residueMap[toStr(to)] = residueMap[toStr(to)] - #give_cards + else + break + end + end + + for _, id in ipairs(cards) do + self:setCardMark(Fk:getCardById(id), "@DistributionTo", 0) + end + for _, pid in ipairs(targets) do + if minNum == 0 or #_cards == 0 then break end + local num = math.min(residueMap[toStr(pid)] or 0, minNum, #_cards) + if num > 0 then + for i = num, 1, -1 do + local c = table.remove(_cards, i) + table.insert(list[pid], c) + minNum = minNum - 1 + end + end + end + if not skipMove then + self:doYiji(self, list, player.id, skillName) + end + + return list +end --- 抽个武将 --- --- 同getNCards,抽出来就没有了,所以记得放回去。 @@ -1410,17 +1508,22 @@ end --- 把武将牌塞回去(……) ---@param g string[] @ 武将名数组 ----@param position? string @位置,top/bottom,默认bottom +---@param position? string @位置,top/bottom/random,默认random ---@return boolean @ 是否成功 function Room:returnToGeneralPile(g, position) - position = position or "bottom" - assert(position == "top" or position == "bottom") + position = position or "random" + assert(position == "top" or position == "bottom" or position == "random") if position == "bottom" then table.insertTable(self.general_pile, g) elseif position == "top" then while #g > 0 do table.insert(self.general_pile, 1, table.remove(g)) end + elseif position == "random" then + while #g > 0 do + table.insert(self.general_pile, math.random(1, #self.general_pile), + table.remove(g)) + end end return true @@ -1430,6 +1533,7 @@ end ---@param name string @ 武将name,如找不到则查找truename,再找不到则返回nil ---@return string? @ 抽出的武将名 function Room:findGeneral(name) + if not Fk.generals[name] then return nil end for i, g in ipairs(self.general_pile) do if g == name or Fk.generals[g].trueName == Fk.generals[name].trueName then return table.remove(self.general_pile, i) @@ -1446,13 +1550,13 @@ function Room:findGenerals(func, n) n = n or 1 local ret = {} local index = 1 - repeat + while #ret < n and index <= #self.general_pile do if func(self.general_pile[index]) then table.insert(ret, table.remove(self.general_pile, index)) else index = index + 1 end - until index >= #self.general_pile or #ret >= n + end return ret end @@ -1724,6 +1828,26 @@ function Room:askForSkillInvoke(player, skill_name, data, prompt) return invoked end +-- 获取使用牌的合法额外目标(【借刀杀人】等带副目标的卡牌除外) +---@param data CardUseStruct @ 使用事件的data +---@param bypass_distances boolean? @ 是否无距离关系的限制 +---@param use_AimGroup boolean? @ 某些场合需要使用AimGroup,by smart Ho-spair +---@return integer[] @ 返回满足条件的player的id列表 +function Room:getUseExtraTargets(data, bypass_distances, use_AimGroup) + if not (data.card.type == Card.TypeBasic or data.card:isCommonTrick()) then return {} end + if data.card.skill:getMinTargetNum() > 1 then return {} end --stupid collateral + local tos = {} + local current_targets = use_AimGroup and AimGroup:getAllTargets(data.tos) or TargetGroup:getRealTargets(data.tos) + for _, p in ipairs(self.alive_players) do + if not table.contains(current_targets, p.id) and not self:getPlayerById(data.from):isProhibited(p, data.card) then + if data.card.skill:modTargetFilter(p.id, {}, data.from, data.card, not bypass_distances) then + table.insert(tos, p.id) + end + end + end + return tos +end + --为使用牌增减目标 ---@param player ServerPlayer @ 执行的玩家 ---@param targets ServerPlayer[] @ 可选的目标范围 @@ -1925,6 +2049,7 @@ function Room:handleUseCardReply(player, data) use.card = c self:useSkill(player, skill, Util.DummyFunc) + use.attachedSkillAndUser = { skillName = skill.name, user = player.id } local rejectSkillName = skill:beforeUse(player, use) if type(rejectSkillName) == "string" then @@ -1976,36 +2101,26 @@ end ---@param event_data? CardEffectEvent @ 事件信息 ---@return CardUseStruct? @ 返回关于本次使用牌的数据,以便后续处理 function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extra_data, event_data) + pattern = pattern or card_name if event_data and (event_data.disresponsive or table.contains(event_data.disresponsiveList or Util.DummyTable, player.id)) then return nil end - if event_data and event_data.prohibitedCardNames and card_name then - local splitedCardNames = card_name:split(",") - splitedCardNames = table.filter(splitedCardNames, function(name) - return not table.contains(event_data.prohibitedCardNames, name) - end) - - if #splitedCardNames == 0 then - return nil + if event_data and event_data.prohibitedCardNames then + local exp = Exppattern:Parse(pattern) + for _, matcher in ipairs(exp.matchers) do + matcher.name = table.filter(matcher.name, function(name) + return not table.contains(event_data.prohibitedCardNames, name) + end) + if #matcher.name == 0 then return nil end end - - card_name = table.concat(splitedCardNames, ",") + pattern = tostring(exp) end - if extra_data then - if extra_data.bypass_distances then - player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 1) -- FIXME: 缺少直接传入无限制的手段 - end - if extra_data.bypass_times == nil or extra_data.bypass_times then - player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 1) -- FIXME: 缺少直接传入无限制的手段 - end - end local command = "AskForUseCard" self:notifyMoveFocus(player, card_name) cancelable = (cancelable == nil) and true or cancelable extra_data = extra_data or Util.DummyTable - pattern = pattern or card_name prompt = prompt or "" local askForUseCardData = { @@ -2018,8 +2133,6 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr self.logic:trigger(fk.AskForCardUse, player, askForUseCardData) if askForUseCardData.result and type(askForUseCardData.result) == 'table' then - player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 0) -- FIXME: 缺少直接传入无限制的手段 - player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 0) -- FIXME: 缺少直接传入无限制的手段 return askForUseCardData.result else local useResult @@ -2034,8 +2147,6 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr Fk.currentResponsePattern = nil if result ~= "" then - player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 0) -- FIXME: 缺少直接传入无限制的手段 - player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 0) -- FIXME: 缺少直接传入无限制的手段 useResult = self:handleUseCardReply(player, result) if type(useResult) == "string" and useResult ~= "" then @@ -2043,12 +2154,8 @@ function Room:askForUseCard(player, card_name, pattern, prompt, cancelable, extr end end until type(useResult) ~= "string" - player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 0) -- FIXME: 缺少直接传入无限制的手段 - player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 0) -- FIXME: 缺少直接传入无限制的手段 return useResult end - player.room:setPlayerMark(player, MarkEnum.BypassDistancesLimit .. "-tmp", 0) -- FIXME: 缺少直接传入无限制的手段 - player.room:setPlayerMark(player, MarkEnum.BypassTimesLimit .. "-tmp", 0) -- FIXME: 缺少直接传入无限制的手段 return nil end @@ -2212,6 +2319,34 @@ function Room:closeAG(player) else self:doBroadcastNotify("CloseAG", "") end end +-- TODO: 重构request机制,不然这个还得手动拿client_reply +---@param players ServerPlayer[] +---@param focus string +---@param game_type string +---@param data_table table @ 对应每个player +function Room:askForMiniGame(players, focus, game_type, data_table) + local command = "MiniGame" + local game = Fk.mini_games[game_type] + if #players == 0 or not game then return end + for _, p in ipairs(players) do + local data = data_table[p.id] + p.mini_game_data = { type = game_type, data = data } + p.request_data = json.encode(p.mini_game_data) + p.default_reply = game.default_choice and json.encode(game.default_choice(p, data)) or "" + end + + self:notifyMoveFocus(players, focus) + self:doBroadcastRequest(command, players) + + for _, p in ipairs(players) do + p.mini_game_data = nil + if not p.reply_ready then + p.client_reply = p.default_reply + p.reply_ready = true + end + end +end + -- Show a qml dialog and return qml's ClientInstance.replyToServer -- Do anything you like through this function @@ -2427,6 +2562,7 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators) firstTarget = firstTarget, additionalDamage = cardUseEvent.additionalDamage, additionalRecover = cardUseEvent.additionalRecover, + additionalEffect = cardUseEvent.additionalEffect, extra_data = cardUseEvent.extra_data, } @@ -2454,6 +2590,7 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators) aimStruct.targetGroup = cardUseEvent.tos aimStruct.nullifiedTargets = cardUseEvent.nullifiedTargets or {} aimStruct.firstTarget = firstTarget + aimStruct.additionalEffect = cardUseEvent.additionalEffect aimStruct.extra_data = cardUseEvent.extra_data end @@ -2471,6 +2608,7 @@ local onAim = function(room, cardUseEvent, aimEventCollaborators) cardUseEvent.from = aimStruct.from cardUseEvent.tos = aimEventTargetGroup cardUseEvent.nullifiedTargets = aimStruct.nullifiedTargets + cardUseEvent.additionalEffect = aimStruct.additionalEffect cardUseEvent.extra_data = aimStruct.extra_data if #AimGroup:getAllTargets(aimStruct.tos) == 0 then @@ -2601,7 +2739,7 @@ function Room:doCardUseEffect(cardUseEvent) return end - ---@type CardEffectEvent + ---@class CardEffectEvent local cardEffectEvent = { from = cardUseEvent.from, tos = cardUseEvent.tos, @@ -2631,57 +2769,72 @@ function Room:doCardUseEffect(cardUseEvent) return end - -- Else: do effect to all targets - local collaboratorsIndex = {} - for _, toId in ipairs(TargetGroup:getRealTargets(cardUseEvent.tos)) do - if not table.contains(cardUseEvent.nullifiedTargets, toId) and self:getPlayerById(toId):isAlive() then - if aimEventCollaborators[toId] then - cardEffectEvent.to = toId - collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1 - local curAimEvent = aimEventCollaborators[toId][collaboratorsIndex[toId]] + for i = 1, (cardUseEvent.additionalEffect or 0) + 1 do + if #TargetGroup:getRealTargets(cardUseEvent.tos) > 0 and cardUseEvent.card.skill.onAction then + cardUseEvent.card.skill:onAction(self, cardUseEvent) + cardEffectEvent.extra_data = cardUseEvent.extra_data + end - cardEffectEvent.subTargets = curAimEvent.subTargets - cardEffectEvent.additionalDamage = curAimEvent.additionalDamage - cardEffectEvent.additionalRecover = curAimEvent.additionalRecover + -- Else: do effect to all targets + local collaboratorsIndex = {} + for _, toId in ipairs(TargetGroup:getRealTargets(cardUseEvent.tos)) do + if not table.contains(cardUseEvent.nullifiedTargets, toId) and self:getPlayerById(toId):isAlive() then + if aimEventCollaborators[toId] then + cardEffectEvent.to = toId + collaboratorsIndex[toId] = collaboratorsIndex[toId] or 1 + local curAimEvent = aimEventCollaborators[toId][collaboratorsIndex[toId]] - if curAimEvent.disresponsiveList then - cardEffectEvent.disresponsiveList = cardEffectEvent.disresponsiveList or {} + cardEffectEvent.subTargets = curAimEvent.subTargets + cardEffectEvent.additionalDamage = curAimEvent.additionalDamage + cardEffectEvent.additionalRecover = curAimEvent.additionalRecover - for _, disresponsivePlayer in ipairs(curAimEvent.disresponsiveList) do - if not table.contains(cardEffectEvent.disresponsiveList, disresponsivePlayer) then - table.insert(cardEffectEvent.disresponsiveList, disresponsivePlayer) + if curAimEvent.disresponsiveList then + cardEffectEvent.disresponsiveList = cardEffectEvent.disresponsiveList or {} + + for _, disresponsivePlayer in ipairs(curAimEvent.disresponsiveList) do + if not table.contains(cardEffectEvent.disresponsiveList, disresponsivePlayer) then + table.insert(cardEffectEvent.disresponsiveList, disresponsivePlayer) + end end end - end - if curAimEvent.unoffsetableList then - cardEffectEvent.unoffsetableList = cardEffectEvent.unoffsetableList or {} + if curAimEvent.unoffsetableList then + cardEffectEvent.unoffsetableList = cardEffectEvent.unoffsetableList or {} - for _, unoffsetablePlayer in ipairs(curAimEvent.unoffsetableList) do - if not table.contains(cardEffectEvent.unoffsetableList, unoffsetablePlayer) then - table.insert(cardEffectEvent.unoffsetableList, unoffsetablePlayer) + for _, unoffsetablePlayer in ipairs(curAimEvent.unoffsetableList) do + if not table.contains(cardEffectEvent.unoffsetableList, unoffsetablePlayer) then + table.insert(cardEffectEvent.unoffsetableList, unoffsetablePlayer) + end end end - end - cardEffectEvent.disresponsive = curAimEvent.disresponsive - cardEffectEvent.unoffsetable = curAimEvent.unoffsetable - cardEffectEvent.fixedResponseTimes = curAimEvent.fixedResponseTimes - cardEffectEvent.fixedAddTimesResponsors = curAimEvent.fixedAddTimesResponsors + cardEffectEvent.disresponsive = curAimEvent.disresponsive + cardEffectEvent.unoffsetable = curAimEvent.unoffsetable + cardEffectEvent.fixedResponseTimes = curAimEvent.fixedResponseTimes + cardEffectEvent.fixedAddTimesResponsors = curAimEvent.fixedAddTimesResponsors - collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1 + collaboratorsIndex[toId] = collaboratorsIndex[toId] + 1 - local curCardEffectEvent = table.simpleClone(cardEffectEvent) - self:doCardEffect(curCardEffectEvent) + local curCardEffectEvent = table.simpleClone(cardEffectEvent) + self:doCardEffect(curCardEffectEvent) - if curCardEffectEvent.cardsResponded then - cardUseEvent.cardsResponded = cardUseEvent.cardsResponded or {} - for _, card in ipairs(curCardEffectEvent.cardsResponded) do - table.insertIfNeed(cardUseEvent.cardsResponded, card) + if curCardEffectEvent.cardsResponded then + cardUseEvent.cardsResponded = cardUseEvent.cardsResponded or {} + for _, card in ipairs(curCardEffectEvent.cardsResponded) do + table.insertIfNeed(cardUseEvent.cardsResponded, card) + end + end + + if type(curCardEffectEvent.nullifiedTargets) == 'table' then + table.insertTableIfNeed(cardUseEvent.nullifiedTargets, curCardEffectEvent.nullifiedTargets) end end end end + + if #TargetGroup:getRealTargets(cardUseEvent.tos) > 0 and cardUseEvent.card.skill.onAction then + cardUseEvent.card.skill:onAction(self, cardUseEvent, true) + end end end @@ -2713,7 +2866,11 @@ function Room:handleCardEffect(event, cardEffectEvent) local to = self:getPlayerById(cardEffectEvent.to) local prompt = "" if cardEffectEvent.from then - prompt = "#slash-jink:" .. cardEffectEvent.from .. "::" .. 1 + if loopTimes == 1 then + prompt = "#slash-jink:" .. cardEffectEvent.from + else + prompt = "#slash-jink-multi:" .. cardEffectEvent.from .. "::" .. i .. ":" .. loopTimes + end end local use = self:askForUseCard( @@ -2744,27 +2901,13 @@ function Room:handleCardEffect(event, cardEffectEvent) then local players = {} Fk.currentResponsePattern = "nullification" + local cardCloned = Fk:cloneCard("nullification") for _, p in ipairs(self.alive_players) do - local cards = p:getHandlyIds() - for _, cid in ipairs(cards) do - if - Fk:getCardById(cid).trueName == "nullification" and - not ( - table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or - table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id) - ) - then - table.insert(players, p) - break - end - end - if not table.contains(players, p) then - Self = p -- for enabledAtResponse - for _, s in ipairs(table.connect(p.player_skills, p._fake_skills)) do + if not p:prohibitUse(cardCloned) then + local cards = p:getHandlyIds() + for _, cid in ipairs(cards) do if - s.pattern and - Exppattern:Parse("nullification"):matchExp(s.pattern) and - not (s.enabledAtResponse and not s:enabledAtResponse(p)) and + Fk:getCardById(cid).trueName == "nullification" and not ( table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id) @@ -2774,6 +2917,23 @@ function Room:handleCardEffect(event, cardEffectEvent) break end end + if not table.contains(players, p) then + Self = p -- for enabledAtResponse + for _, s in ipairs(table.connect(p.player_skills, p._fake_skills)) do + if + s.pattern and + Exppattern:Parse("nullification"):matchExp(s.pattern) and + not (s.enabledAtResponse and not s:enabledAtResponse(p)) and + not ( + table.contains(cardEffectEvent.disresponsiveList or Util.DummyTable, p.id) or + table.contains(cardEffectEvent.unoffsetableList or Util.DummyTable, p.id) + ) + then + table.insert(players, p) + break + end + end + end end end @@ -2860,13 +3020,16 @@ end --- 让一名玩家获得一张牌 ---@param player integer|ServerPlayer @ 要拿牌的玩家 ----@param cid integer|Card @ 要拿到的卡牌 +---@param cid integer|Card|integer[] @ 要拿到的卡牌 ---@param unhide? boolean @ 是否明着拿 ---@param reason? CardMoveReason @ 卡牌移动的原因 -function Room:obtainCard(player, cid, unhide, reason) +---@param proposer? integer @ 移动操作者的id +function Room:obtainCard(player, cid, unhide, reason, proposer) if type(cid) ~= "number" then - assert(cid and cid:isInstanceOf(Card)) - cid = cid:isVirtual() and cid.subcards or {cid.id} + assert(cid and type(cid) == "table") + if cid:isInstanceOf(Card) then + cid = cid:isVirtual() and cid.subcards or {cid.id} + end else cid = {cid} end @@ -2882,7 +3045,7 @@ function Room:obtainCard(player, cid, unhide, reason) to = player, toArea = Card.PlayerHand, moveReason = reason or fk.ReasonJustMove, - proposer = player, + proposer = proposer or player, moveVisible = unhide or false, }) end @@ -2934,7 +3097,6 @@ function Room:moveCardTo(card, to_place, target, reason, skill_name, special_nam reason = reason or fk.ReasonJustMove skill_name = skill_name or "" special_name = special_name or "" - proposer = proposer or nil local ids = Card:getIdList(card) local to @@ -2970,6 +3132,96 @@ function Room:moveCardTo(card, to_place, target, reason, skill_name, special_nam self:moveCards(table.unpack(movesSplitedByOwner)) end +--- 将一些卡牌同时分配给一些角色。 +---@param room Room @ 房间 +---@param list table @ 分配牌和角色的数据表,键为角色id,值为分配给其的牌id数组 +---@param proposer? integer @ 操作者的id。默认为空 +---@param skillName? string @ 技能名。默认为“分配” +---@return table @ 返回成功分配的卡牌 +function Room:doYiji(room, list, proposer, skillName) + skillName = skillName or "distribution_skill" + local moveInfos = {} + local move_ids = {} + for to, cards in pairs(list) do + local toP = room:getPlayerById(to) + local handcards = toP:getCardIds("h") + cards = table.filter(cards, function (id) return not table.contains(handcards, id) end) + if #cards > 0 then + table.insertTable(move_ids, cards) + local moveMap = {} + local noFrom = {} + for _, id in ipairs(cards) do + local from = room.owner_map[id] + if from then + moveMap[from] = moveMap[from] or {} + table.insert(moveMap[from], id) + else + table.insert(noFrom, id) + end + end + for from, _cards in pairs(moveMap) do + table.insert(moveInfos, { + ids = _cards, + moveInfo = table.map(_cards, function(id) + return {cardId = id, fromArea = room:getCardArea(id), fromSpecialName = room:getPlayerById(from):getPileNameOfId(id)} + end), + from = from, + to = to, + toArea = Card.PlayerHand, + moveReason = fk.ReasonGive, + proposer = proposer, + skillName = skillName, + }) + end + if #noFrom > 0 then + table.insert(moveInfos, { + ids = noFrom, + to = to, + toArea = Card.PlayerHand, + moveReason = fk.ReasonGive, + proposer = proposer, + skillName = skillName, + }) + end + end + end + if #moveInfos > 0 then + room:moveCards(table.unpack(moveInfos)) + end + return move_ids +end + +--- 将一张牌移动至某角色的装备区,若不合法则置入弃牌堆。目前没做相同副类别装备同时置入的适配(甘露神典韦) +---@param target ServerPlayer @ 接受牌的角色 +---@param cards integer|integer[] @ 移动的牌 +---@param skillName? string @ 技能名 +---@param convert? boolean @ 是否可以替换装备(默认可以) +---@param proposer? ServerPlayer @ 操作者 +function Room:moveCardIntoEquip(target, cards, skillName, convert, proposer) + convert = (convert == nil) and true or convert + skillName = skillName or "" + cards = type(cards) == "table" and cards or {cards} + local moves = {} + for _, cardId in ipairs(cards) do + local card = Fk:getCardById(cardId) + local fromId = self.owner_map[cardId] + local proposerId = proposer and proposer.id or nil + if target:canMoveCardIntoEquip(cardId, convert) then + if target:hasEmptyEquipSlot(card.sub_type) then + table.insert(moves,{ids = {cardId}, from = fromId, to = target.id, toArea = Card.PlayerEquip, moveReason = fk.ReasonPut,skillName = skillName,proposer = proposerId}) + else + local existingEquip = target:getEquipments(card.sub_type) + local throw = #existingEquip == 1 and existingEquip[1] or + self:askForCardChosen(proposer or target, target, {card_data = { {Util.convertSubtypeAndEquipSlot(card.sub_type),existingEquip} } }, "replaceEquip","#replaceEquip") + table.insert(moves,{ids = {throw}, from = target.id, toArea = Card.DiscardPile, moveReason = fk.ReasonPutIntoDiscardPile, skillName = skillName,proposer = proposerId}) + table.insert(moves,{ids = {cardId}, from = fromId, to = target.id, toArea = Card.PlayerEquip, moveReason = fk.ReasonPut,skillName = skillName,proposer = proposerId}) + end + else + table.insert(moves,{ids = {cardId}, from = fromId, toArea = Card.DiscardPile, moveReason = fk.ReasonPutIntoDiscardPile,skillName = skillName}) + end + end + self:moveCards(table.unpack(moves)) +end ------------------------------------------------------------------------ -- 其他游戏事件 ------------------------------------------------------------------------ @@ -3061,6 +3313,7 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no if #skill_names == 0 then return end local losts = {} ---@type boolean[] local triggers = {} ---@type Skill[] + local lost_piles = {} ---@type integer[] for _, skill in ipairs(skill_names) do if string.sub(skill, 1, 1) == "-" then local actual_skill = string.sub(skill, 2, #skill) @@ -3082,12 +3335,17 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no table.insert(losts, true) table.insert(triggers, s) + if s.derived_piles then + for _, pile_name in ipairs(s.derived_piles) do + table.insertTableIfNeed(lost_piles, player:getPile(pile_name)) + end + end end end else local sk = Fk.skills[skill] if sk and not player:hasSkill(sk, true, true) then - local got_skills = player:addSkill(sk) + local got_skills = player:addSkill(sk, source_skill) for _, s in ipairs(got_skills) do -- TODO: limit skill mark @@ -3118,6 +3376,15 @@ function Room:handleAddLoseSkills(player, skill_names, source_skill, sendlog, no self.logic:trigger(event, player, triggers[i]) end end + + if #lost_piles > 0 then + self:moveCards({ + ids = lost_piles, + from = player.id, + toArea = Card.DiscardPile, + moveReason = fk.ReasonPutIntoDiscardPile, + }) + end end -- 判定 @@ -3160,12 +3427,6 @@ function Room:retrial(card, player, judge, skillName, exchange) local rebyre = judge.retrial_by_response judge.retrial_by_response = player - local move2 = {} ---@type CardsMoveInfo - move2.ids = { oldJudge:getEffectiveId() } - move2.toArea = exchange and Card.PlayerHand or Card.DiscardPile - move2.moveReason = fk.ReasonJustMove - move2.to = exchange and player.id or nil - self:sendLog{ type = "#ChangedJudge", from = player.id, @@ -3174,12 +3435,22 @@ function Room:retrial(card, player, judge, skillName, exchange) arg = skillName, } - self:moveCards(move2) Fk:filterCard(judge.card.id, judge.who, judge) + + exchange = exchange and not player.dead + + local move2 = {} ---@type CardsMoveInfo + move2.ids = { oldJudge:getEffectiveId() } + move2.toArea = exchange and Card.PlayerHand or Card.DiscardPile + move2.moveReason = exchange and fk.ReasonJustMove or fk.ReasonJudge + move2.to = exchange and player.id or nil + move2.skillName = skillName + + self:moveCards(move2) end --- 弃置一名角色的牌。 ----@param card_ids integer[] @ 被弃掉的牌 +---@param card_ids integer[]|integer @ 被弃掉的牌 ---@param skillName? string @ 技能名 ---@param who ServerPlayer @ 被弃牌的人 ---@param thrower? ServerPlayer @ 弃别人牌的人 @@ -3303,6 +3574,8 @@ function Room:shuffleDrawPile() self.discard_pile = {} table.shuffle(self.draw_pile) + self:doBroadcastNotify("UpdateDrawPile", #self.draw_pile) + self.logic:trigger(fk.AfterDrawPileShuffle, nil, {}) end @@ -3369,7 +3642,13 @@ end function Room:gameOver(winner) if not self.game_started then return end - self.logic:trigger(fk.GameFinished, nil, winner) + if table.contains( + { "running", "normal" }, + coroutine.status(self.main_co) + ) then + self.logic:trigger(fk.GameFinished, nil, winner) + end + self.game_started = false self.game_finished = true @@ -3528,6 +3807,10 @@ function Room:printCard(name, suit, number) return cd end +--- 刷新使命技状态 +---@param player ServerPlayer +---@param skillName Suit +---@param failed? boolean function Room:updateQuestSkillState(player, skillName, failed) assert(Fk.skills[skillName].frequency == Skill.Quest) @@ -3541,6 +3824,9 @@ function Room:updateQuestSkillState(player, skillName, failed) }) end +--- 废除区域 +---@param player ServerPlayer +---@param playerSlots string | string[] function Room:abortPlayerArea(player, playerSlots) assert(type(playerSlots) == "string" or type(playerSlots) == "table") @@ -3593,6 +3879,9 @@ function Room:abortPlayerArea(player, playerSlots) self.logic:trigger(fk.AreaAborted, player, { slots = slotsSealed }) end +--- 恢复区域 +---@param player ServerPlayer +---@param playerSlots string | string[] function Room:resumePlayerArea(player, playerSlots) assert(type(playerSlots) == "string" or type(playerSlots) == "table") @@ -3616,6 +3905,9 @@ function Room:resumePlayerArea(player, playerSlots) end end +--- 设置休整 +---@param player ServerPlayer +---@param roundNum integer function Room:setPlayerRest(player, roundNum) player.rest = roundNum self:broadcastProperty(player, "rest") diff --git a/lua/server/scheduler.lua b/lua/server/scheduler.lua index d2c04e77..2c1a16dd 100644 --- a/lua/server/scheduler.lua +++ b/lua/server/scheduler.lua @@ -93,6 +93,12 @@ local function mainLoop() if over then -- verbose('[#] %s is finished, removing ...', tostring(room)) + for _, e in ipairs(room.logic.game_event_stack.t) do + coroutine.close(e._co) + end + for _, e in ipairs(room.logic.cleaner_stack.t) do + coroutine.close(e._co) + end room.logic = nil runningRooms[room.id] = nil else diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua index 2a86b2f0..e76e88fe 100644 --- a/lua/server/serverplayer.lua +++ b/lua/server/serverplayer.lua @@ -5,6 +5,7 @@ ---@field public room Room ---@field public next ServerPlayer ---@field public request_data string +---@field public mini_game_data any ---@field public client_reply string ---@field public default_reply string ---@field public reply_ready boolean @@ -214,7 +215,7 @@ function ServerPlayer:marshal(player, observe) if self.dead then room:notifyProperty(player, self, "dead") - room:notifyProperty(player, self, "role") + room:notifyProperty(player, self, self.rest > 0 and "rest" or "role") else room:notifyProperty(player, self, "seat") room:notifyProperty(player, self, "phase") @@ -321,6 +322,16 @@ function ServerPlayer:reconnect() local room = self.room self.serverplayer:setState(fk.Player_Online) + self:doNotify("Setup", json.encode{ + self.id, + self._splayer:getScreenName(), + self._splayer:getAvatar(), + }) + self:doNotify("AddTotalGameTime", json.encode { + self.id, + self._splayer:getTotalGameTime(), + }) + self:doNotify("EnterLobby", "") self:doNotify("EnterRoom", json.encode{ #room.players, room.timeout, room.settings, @@ -619,15 +630,18 @@ function ServerPlayer:endPlayPhase() -- TODO: send log end +--- 获得一个额外回合 ---@param delay? boolean -function ServerPlayer:gainAnExtraTurn(delay) +---@param skillName? string +function ServerPlayer:gainAnExtraTurn(delay, skillName) local room = self.room delay = (delay == nil) and true or delay + skillName = (skillName == nil) and room.logic:getCurrentSkillName() or skillName if delay then local logic = room.logic local turn = logic:getCurrentEvent():findParent(GameEvent.Turn, true) if turn then - turn:prependExitFunc(function() self:gainAnExtraTurn(false) end) + turn:prependExitFunc(function() self:gainAnExtraTurn(false, skillName) end) return end end @@ -642,7 +656,6 @@ function ServerPlayer:gainAnExtraTurn(delay) self.tag["_extra_turn_count"] = self.tag["_extra_turn_count"] or {} local ex_tag = self.tag["_extra_turn_count"] - local skillName = room.logic:getCurrentSkillName() table.insert(ex_tag, skillName) GameEvent(GameEvent.Turn, self):exec() @@ -822,7 +835,7 @@ end -- Hegemony func ----@param skill Skill +---@param skill Skill | string function ServerPlayer:addFakeSkill(skill) assert(type(skill) == "string" or skill:isInstanceOf(Skill)) if type(skill) == "string" then @@ -843,7 +856,7 @@ function ServerPlayer:addFakeSkill(skill) self:doNotify("AddSkill", json.encode{ self.id, skill.name, true }) end ----@param skill Skill +---@param skill Skill | string function ServerPlayer:loseFakeSkill(skill) assert(type(skill) == "string" or skill:isInstanceOf(Skill)) if type(skill) == "string" then @@ -862,6 +875,7 @@ function ServerPlayer:loseFakeSkill(skill) self:doNotify("LoseSkill", json.encode{ self.id, skill.name, true }) end +---@param skill Skill | string function ServerPlayer:isFakeSkill(skill) if type(skill) == "string" then skill = Fk.skills[skill] end assert(skill:isInstanceOf(Skill)) @@ -912,7 +926,7 @@ function ServerPlayer:revealGeneral(isDeputy, no_trigger) end local general = Fk.generals[generalName] or Fk.generals["blank_shibing"] - for _, s in ipairs(general:getSkillNameList()) do + for _, s in ipairs(general:getSkillNameList(true)) do local skill = Fk.skills[s] self:loseFakeSkill(skill) end @@ -920,7 +934,7 @@ function ServerPlayer:revealGeneral(isDeputy, no_trigger) local ret = true if not ((isDeputy and self.general ~= "anjiang") or (not isDeputy and self.deputyGeneral ~= "anjiang")) then local other = Fk.generals[self:getMark(isDeputy and "__heg_general" or "__heg_deputy")] or Fk.generals["blank_shibing"] - for _, sname in ipairs(other:getSkillNameList()) do + for _, sname in ipairs(other:getSkillNameList(true)) do local s = Fk.skills[sname] if s.frequency == Skill.Compulsory and s.relate_to_place ~= (isDeputy and "m" or "d") then ret = false @@ -1006,7 +1020,7 @@ function ServerPlayer:revealBySkillName(skill_name) if main then if table.contains(Fk.generals[self:getMark("__heg_general")] - :getSkillNameList(), skill_name) then + :getSkillNameList(true), skill_name) then self:revealGeneral(false) return end @@ -1014,7 +1028,7 @@ function ServerPlayer:revealBySkillName(skill_name) if deputy then if table.contains(Fk.generals[self:getMark("__heg_deputy")] - :getSkillNameList(), skill_name) then + :getSkillNameList(true), skill_name) then self:revealGeneral(true) return end @@ -1038,18 +1052,8 @@ function ServerPlayer:hideGeneral(isDeputy) end local general = Fk.generals[generalName] - local skills = general.skills local place = isDeputy and "m" or "d" - for _, s in ipairs(skills) do - room:handleAddLoseSkills(self, "-" .. s.name, nil, false, true) - if s.relate_to_place ~= place then - if s.frequency == Skill.Compulsory then - self:addFakeSkill("reveal_skill") - end - self:addFakeSkill(s) - end - end - for _, sname in ipairs(general.other_skills) do + for _, sname in ipairs(general:getSkillNameList()) do room:handleAddLoseSkills(self, "-" .. sname, nil, false, true) local s = Fk.skills[sname] if s.relate_to_place ~= place then diff --git a/lua/server/system_enum.lua b/lua/server/system_enum.lua index 9f0b0b88..d77857ef 100644 --- a/lua/server/system_enum.lua +++ b/lua/server/system_enum.lua @@ -50,6 +50,10 @@ ---@field public num integer @ 失去体力的数值 ---@field public skillName string @ 导致这次失去的技能名 +--- 描述跟体力上限变化有关的数据 +---@class MaxHpChangedData +---@field public num integer @ 体力上限变化量,可能是正数或者负数 + ---@alias DamageType integer fk.NormalDamage = 1 @@ -110,6 +114,7 @@ fk.IceDamage = 4 ---@field public cardsResponded? Card[] ---@field public prohibitedCardNames? string[] ---@field public damageDealt? table +---@field public additionalEffect? integer ---@class AimStruct ---@field public from integer @@ -126,9 +131,10 @@ fk.IceDamage = 4 ---@field public unoffsetableList? boolean ---@field public additionalResponseTimes? table|integer ---@field public fixedAddTimesResponsors? integer[] +---@field public additionalEffect? integer ---@class CardEffectEvent ----@field public from integer +---@field public from? integer ---@field public to integer ---@field public subTargets? integer[] ---@field public tos TargetGroup @@ -181,8 +187,34 @@ fk.IceDamage = 4 ---@field public pattern string ---@field public result Card ----@alias CardMoveReason integer +---@class PindianStruct +---@field public from ServerPlayer +---@field public tos ServerPlayer[] +---@field public fromCard Card +---@field public results table +---@field public reason string +---@class LogMessage +---@field public type string @ log主体 +---@field public from? integer @ 要替换%from的玩家的id +---@field public to? integer[] @ 要替换%to的玩家id列表 +---@field public card? integer[] @ 要替换%card的卡牌id列表 +---@field public arg? any @ 要替换%arg的内容 +---@field public arg2? any @ 要替换%arg2的内容 +---@field public arg3? any @ 要替换%arg3的内容 +---@field public toast? boolean @ 是否顺手把消息发送一条相同的toast + +---@class SkillUseStruct +---@field public skill Skill +---@field public willUse boolean + +---@class DrawCardStruct +---@field public who ServerPlayer +---@field public num number +---@field public skillName string +---@field public fromPlace "top"|"bottom" + +---@alias CardMoveReason integer fk.ReasonJustMove = 1 fk.ReasonDraw = 2 fk.ReasonDiscard = 3 @@ -195,29 +227,3 @@ fk.ReasonUse = 9 fk.ReasonResonpse = 10 fk.ReasonJudge = 11 fk.ReasonRecast = 12 - ----@class PindianStruct ----@field public from ServerPlayer ----@field public tos ServerPlayer[] ----@field public fromCard Card ----@field public results table ----@field public reason string - ----@class LogMessage ----@field public type string ----@field public from? integer ----@field public to? integer[] ----@field public card? integer[] ----@field public arg? any ----@field public arg2? any ----@field public arg3? any - ----@class SkillUseStruct ----@field public skill Skill ----@field public willUse boolean - ----@class DrawCardStruct ----@field public who ServerPlayer ----@field public num number ----@field public skillName string ----@field public fromPlace "top"|"bottom" diff --git a/packages/maneuvering/i18n/en_US.lua b/packages/maneuvering/i18n/en_US.lua index 0f1c9cf7..1186e44e 100644 --- a/packages/maneuvering/i18n/en_US.lua +++ b/packages/maneuvering/i18n/en_US.lua @@ -36,6 +36,7 @@ return { ["guding_blade"] = "Ancient Scimitar", [":guding_blade"] = "Ancient Scimitar (equip card, weapon)
ATK range: 2
Weapon skill: When your used Slash is about to cause DMG, if the target player has no hand cards: the DMG is increased by +1.", + ["#guding_blade_skill"] = "Ancient Scimitar", ["fan"] = "Fan", [":fan"] = "Fan (equip card, weapon)
ATK range: 4
Weapon skill: You can use any basic Slash as Fire Slash.", @@ -43,9 +44,11 @@ return { ["vine"] = "Vine", [":vine"] = "Vine (equip card, armor)
Armor skill: Savage Assault, Archery Attack and basic Slash have no effect on you. When you are about to suffer Fire DMG, the DMG is increased by +1.", + ["#vine_skill"] = "Vine", ["silver_lion"] = "Sliver Lion", [":silver_lion"] = "Sliver Lion (equip card, armor)
Armor skill: When you are about to suffer DMG: that DMG is reduced to 1. When you lose this card in your equipment area: you heal 1 HP.", + ["#silver_lion_skill"] = "Sliver Lion", ["hualiu"] = "Hua Liu", [":hualiu"] = "Hua Liu (equip card, horse)
Horse skill: The distance from other players to you is increased by +1.", diff --git a/packages/maneuvering/init.lua b/packages/maneuvering/init.lua index 583f677c..0a95e07c 100644 --- a/packages/maneuvering/init.lua +++ b/packages/maneuvering/init.lua @@ -109,13 +109,13 @@ local analepticSkill = fk.CreateActiveSkill{ prompt = "#analeptic_skill", max_turn_use_time = 1, mod_target_filter = function(self, to_select, _, _, card, _) - return self:withinTimesLimit(Fk:currentRoom():getPlayerById(to_select), Player.HistoryTurn, card, "analeptic", Fk:currentRoom():getPlayerById(to_select)) and - not table.find(Fk:currentRoom().alive_players, function(p) - return p.dying - end) + return not table.find(Fk:currentRoom().alive_players, function(p) + return p.dying + end) end, - can_use = function(self, player, card) - return self:withinTimesLimit(player, Player.HistoryTurn, card, "analeptic", player) + can_use = function(self, player, card, extra_data) + return ((extra_data and (extra_data.bypass_times or extra_data.analepticRecover)) or + self:withinTimesLimit(player, Player.HistoryTurn, card, "analeptic", player)) end, on_use = function(_, _, use) if not use.tos or #TargetGroup:getRealTargets(use.tos) == 0 then @@ -146,7 +146,7 @@ local analepticEffect = fk.CreateTriggerSkill{ name = "analeptic_effect", global = true, priority = 0, -- game rule - events = { fk.PreCardUse, fk.EventPhaseStart }, + events = { fk.PreCardUse, fk.AfterTurnEnd }, can_trigger = function(_, event, target, player, data) if target ~= player then return false @@ -155,7 +155,7 @@ local analepticEffect = fk.CreateTriggerSkill{ if event == fk.PreCardUse then return data.card.trueName == "slash" and player.drank > 0 else - return target.phase == Player.NotActive + return true end end, on_trigger = function(_, event, _, player, data) @@ -291,8 +291,9 @@ local supplyShortageSkill = fk.CreateActiveSkill{ local from = Fk:currentRoom():getPlayerById(user) return from ~= player and not (distance_limited and not self:withinDistanceLimit(from, false, card, player)) end, - target_filter = function(self, to_select, selected, _, card) - return #selected == 0 and self:modTargetFilter(to_select, selected, Self.id, card, true) + target_filter = function(self, to_select, selected, _, card, extra_data) + local count_distances = not (extra_data and extra_data.bypass_distances) + return #selected == 0 and self:modTargetFilter(to_select, selected, Self.id, card, count_distances) end, target_num = 1, on_effect = function(self, room, effect) @@ -359,9 +360,19 @@ local fanSkill = fk.CreateTriggerSkill{ return target == player and player:hasSkill(self) and data.card.name == "slash" end, on_use = function(_, _, _, _, data) - local card = Fk:cloneCard("fire__slash") + local card = Fk:cloneCard("fire__slash", data.card.suit, data.card.number) + for k, v in pairs(data.card) do + if card[k] == nil then + card[k] = v + end + end + if data.card:isVirtual() then + card.subcards = data.card.subcards + else + card.id = data.card.id + end + card.skillNames = data.card.skillNames card.skillName = "fan" - card:addSubcard(data.card) data.card = card end, } @@ -518,16 +529,19 @@ Fk:loadTranslationTable{ ["guding_blade"] = "古锭刀", [":guding_blade"] = "装备牌·武器
攻击范围:2
武器技能:锁定技。每当你使用【杀】对目标角色造成伤害时,若该角色没有手牌,此伤害+1。", + ["#guding_blade_skill"] = "古锭刀", ["fan"] = "朱雀羽扇", - [":fan"] = "装备牌·武器
攻击范围:4
武器技能:你可以将一张普通【杀】当火【杀】使用。", + [":fan"] = "装备牌·武器
攻击范围:4
武器技能:当你声明使用普【杀】后,你可以将此【杀】改为火【杀】。", ["#fan_skill"] = "朱雀羽扇", ["vine"] = "藤甲", [":vine"] = "装备牌·防具
防具技能:锁定技。【南蛮入侵】、【万箭齐发】和普通【杀】对你无效。每当你受到火焰伤害时,此伤害+1。", + ["#vine_skill"] = "藤甲", ["silver_lion"] = "白银狮子", [":silver_lion"] = "装备牌·防具
防具技能:锁定技。每当你受到伤害时,若此伤害大于1点,防止多余的伤害。每当你失去装备区里的【白银狮子】后,你回复1点体力。", + ["#silver_lion_skill"] = "白银狮子", ["hualiu"] = "骅骝", [":hualiu"] = "装备牌·坐骑
坐骑技能:其他角色与你的距离+1。", diff --git a/packages/standard/ai/init.lua b/packages/standard/ai/init.lua index 04a7ab47..e25c8612 100644 --- a/packages/standard/ai/init.lua +++ b/packages/standard/ai/init.lua @@ -1,266 +1,80 @@ require "packages.standard.ai.aux_skills" ---[[ -fk.ai_use_play["rende"] = function(self, skill) - for _, p in ipairs(self.friends_noself) do - if p.kingdom == "shu" and #self.player:getCardIds("h") >= self.player.hp then - self.use_id = {} - for _, cid in ipairs(self.player:getCardIds("h")) do - if #self.use_id < #self.player:getCardIds("h") / 2 then - table.insert(self.use_id, cid) - end - end - self.use_tos = { p.id } - return - end - end - for _, p in ipairs(self.friends_noself) do - if #self.player:getCardIds("h") >= self.player.hp then - self.use_id = {} - for _, cid in ipairs(self.player:getCardIds("h")) do - if #self.use_id < #self.player:getCardIds("h") / 2 then - table.insert(self.use_id, cid) - end - end - self.use_tos = { p.id } - return - end - end -end - -fk.ai_card["jijiang"] = { priority = 10 } - -fk.ai_use_play["lijian"] = function(self, skill) - local c = Fk:cloneCard("duel") - c.skillName = "lijian" - local cards = table.map( - self.player:getCardIds("he"), - function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, p in ipairs(self.enemies) do - for _, pt in ipairs(self.enemies) do - if p.gender == General.Male and pt.gender == General.Male and p.id ~= pt.id - and c.skill:targetFilter(pt.id, {}, p.id, c) then - self.use_id = { cards[1].id } - self.use_tos = { pt.id, p.id } - break - end - end - end - for _, p in ipairs(self.friends_noself) do - for _, pt in ipairs(self.enemies) do - if p.gender == General.Male and pt.gender == General.Male and p.id ~= pt.id - and c.skill:targetFilter(pt.id, {}, p.id, c) then - self.use_id = { cards[1].id } - self.use_tos = { pt.id, p.id } - break - end - end - end -end - -fk.ai_card["lijian"] = { priority = 2 } - -fk.ai_use_play["zhiheng"] = function(self, skill) - local card_ids = {} - local cards = table.map( - self.player:getCardIds("he"), - function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, h in ipairs(cards) do - if #card_ids < #cards / 2 then - table.insert(card_ids, h.id) - end - end - if #card_ids > 0 then - self.use_id = card_ids - end -end - -fk.ai_use_play["kurou"] = function(self, skill) - if #self:getActives("peach") + self.player.hp > 1 then - local slash = Fk:cloneCard("slash") - if slash.skill:canUse(self.player, slash) and not self.player:prohibitUse(slash) then - fk.ai_use_play.slash(self, slash) - if self.use_id then - self.use_id = {} - self.use_tos = {} - end - end - end -end - -fk.ai_use_play["fanjian"] = function(self, skill) - for _, p in ipairs(self.enemies) do - if #self.player:getCardIds("h") > 0 then - self.use_id = {} - table.insert(self.use_tos, p.id) - break - end - end -end - -fk.ai_use_play["jieyin"] = function(self, skill) - local cards = table.map( - self.player:getCardIds("h"), - function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, p in ipairs(self.friends_noself) do - if #cards > 1 and p.gender == General.Male and p:isWounded() then - self.use_id = { cards[1].id, cards[2].id } - table.insert(self.use_tos, p.id) - break - end - end -end - -fk.ai_use_play["qingnang"] = function(self, skill) - local cards = table.map( - self.player:getCardIds("h"), - function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, p in ipairs(self.friends) do - if #cards > 0 and p:isWounded() then - self.use_id = { cards[1].id } - table.insert(self.use_tos, p.id) - break - end - end -end +-- 魏国 fk.ai_skill_invoke["jianxiong"] = true +-- TODO: hujia +-- TODO: guicai 关于如何界定判定的好坏 需要向AI中单独说明 -fk.ai_card["hujia"] = { priority = 10 } +fk.ai_skill_invoke["fankui"] = function(self) + local room = self.room + local logic = room.logic -fk.ai_response_card["#hujia-ask"] = function(self, pattern, prompt, cancelable, data) - local to = self.room:getPlayerById(tonumber(prompt:split(":")[2])) - if to and self:isFriend(to) and (self:isWeak(to) or #self:getActives(pattern)>1) then - self:setUseId(pattern) - end + -- 询问反馈时,处于on_cost环节,当前事件必是damage且有from + local event = logic:getCurrentEvent() + local dmg = event.data[1] + return self:isEnemy(dmg.from) end -fk.ai_response_card["#jijiang-ask"] = fk.ai_response_card["#hujia-ask"] +fk.ai_skill_invoke["ganglie"] = fk.ai_skill_invoke["fankui"] -fk.ai_skill_invoke["fankui"] = function(self, data, prompt) - local damage = self:eventData("Damage") - return damage and damage.from and not self:isFriend(damage.from) -end +-- TODO: tuxi -fk.ai_response_card["#guicai-ask"] = function(self, pattern, prompt, cancelable, data) - local cards = table.map(self.player:getHandlyIds(true), function(id) - return Fk:getCardById(id) - end) - local id = self:getRetrialCardId(cards) - if id then - self.use_id = id - end -end - -fk.ai_skill_invoke["ganglie"] = function(self, data, prompt) - local damage = self:eventData("Damage") - return damage and damage.from and not self:isFriend(damage.from) -end - -fk.ai_judge["ganglie"] = { ".|.|heart", false } - -fk.ai_skill_invoke["luoyi"] = function(self, data, prompt) - for _, p in ipairs(self.enemies) do - if #self:getActives("slash") > 0 and not self:isWeak() then - return true - end - end +fk.ai_skill_invoke["luoyi"] = function(self) + return false end fk.ai_skill_invoke["tiandu"] = true -fk.ai_skill_invoke["yiji"] = true +-- TODO: yiji fk.ai_skill_invoke["luoshen"] = true -fk.ai_skill_invoke["guanxing"] = true +-- TODO: qingguo -fk.ai_skill_invoke["tieqi"] = function(self, data, prompt) - local use = self:eventData("UseCard") - for _, p in ipairs(TargetGroup:getRealTargets(use.tos)) do - p = self.room:getPlayerById(p) - if self:isEnemy(p) then - return true - end - end +-- 蜀国 +-- TODO: rende +-- TODO: jijiang +-- TODO: wusheng +-- TODO: guanxing +-- TODO: longdan + +fk.ai_skill_invoke["tieqi"] = function(self) + local room = self.room + local logic = room.logic + + -- 询问反馈时,处于on_cost环节,当前事件必是damage且有from + local event = logic:getCurrentEvent() + local use = event.data[1] ---@type CardUseStruct + return table.find(use.tos, function(t) + return self:isEnemy(room:getPlayerById(t[1])) + end) end fk.ai_skill_invoke["jizhi"] = true +-- 吴国 +-- TODO: zhiheng +-- TODO: qixi + fk.ai_skill_invoke["keji"] = true +-- TODO: kurou + fk.ai_skill_invoke["yingzi"] = true -fk.ai_skill_invoke["lianying"] = true +-- TODO: fanjian +-- TODO: guose +-- TODO: liuli +fk.ai_skill_invoke["lianying"] = true fk.ai_skill_invoke["xiaoji"] = true +-- TODO: jieyin + +-- 群雄 +-- TODO: qingnang +-- TODO: jijiu +-- TODO: wushuang +-- TODO: lijian fk.ai_skill_invoke["biyue"] = true - -fk.ai_choose_players["tuxi"] = function(self, targets, min_num, num, cancelable) - for _, pid in ipairs(targets) do - local p = self.room:getPlayerById(pid) - if self:isEnemy(p) and #self.use_tos < num then - table.insert(self.use_tos, pid) - end - end -end - -fk.ai_active_skill["yiji_active"] = function(self, prompt, cancelable, data) - for _, p in ipairs(self.friends_noself) do - for c, cid in ipairs(self.player.tag["yiji_ids"]) do - c = Fk:getCardById(cid) - if c:getMark("yiji") > 0 and c.skill:canUse(p, c) then - self.use_tos = { p.id } - self.use_id = json.encode { - skill = "yiji_active", - subcards = { cid } - } - return - end - end - end -end - -fk.ai_choose_players["liuli"] = function(self, targets, min_num, num, cancelable) - local cards = table.map( - self.player:getCardIds("he"), - function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, pid in ipairs(targets) do - local p = self.room:getPlayerById(pid) - if self:isEnemy(p) and #self.use_tos < num and #cards > 0 then - table.insert(self.use_tos, pid) - self.use_id = { cards[1].id } - return - end - end - for _, pid in ipairs(targets) do - local p = self.room:getPlayerById(pid) - if not self:isFriend(p) and #self.use_tos < num and #cards > 0 then - table.insert(self.use_tos, pid) - self.use_id = { cards[1].id } - return - end - end -end ---]] diff --git a/packages/standard/audio/skill/fastchat_f17.mp3 b/packages/standard/audio/skill/fastchat_f17.mp3 new file mode 100644 index 00000000..08d7299f Binary files /dev/null and b/packages/standard/audio/skill/fastchat_f17.mp3 differ diff --git a/packages/standard/audio/skill/fastchat_f18.mp3 b/packages/standard/audio/skill/fastchat_f18.mp3 new file mode 100644 index 00000000..0d551803 Binary files /dev/null and b/packages/standard/audio/skill/fastchat_f18.mp3 differ diff --git a/packages/standard/audio/skill/fastchat_f19.mp3 b/packages/standard/audio/skill/fastchat_f19.mp3 new file mode 100644 index 00000000..e30bcc4e Binary files /dev/null and b/packages/standard/audio/skill/fastchat_f19.mp3 differ diff --git a/packages/standard/audio/skill/fastchat_f20.mp3 b/packages/standard/audio/skill/fastchat_f20.mp3 new file mode 100644 index 00000000..b3079cca Binary files /dev/null and b/packages/standard/audio/skill/fastchat_f20.mp3 differ diff --git a/packages/standard/audio/skill/fastchat_f21.mp3 b/packages/standard/audio/skill/fastchat_f21.mp3 new file mode 100644 index 00000000..51aec32d Binary files /dev/null and b/packages/standard/audio/skill/fastchat_f21.mp3 differ diff --git a/packages/standard/audio/skill/fastchat_f22.mp3 b/packages/standard/audio/skill/fastchat_f22.mp3 new file mode 100644 index 00000000..bca170de Binary files /dev/null and b/packages/standard/audio/skill/fastchat_f22.mp3 differ diff --git a/packages/standard/audio/skill/fastchat_f23.mp3 b/packages/standard/audio/skill/fastchat_f23.mp3 new file mode 100644 index 00000000..748cb3d3 Binary files /dev/null and b/packages/standard/audio/skill/fastchat_f23.mp3 differ diff --git a/packages/standard/audio/skill/fastchat_m17.mp3 b/packages/standard/audio/skill/fastchat_m17.mp3 new file mode 100644 index 00000000..1b2457d6 Binary files /dev/null and b/packages/standard/audio/skill/fastchat_m17.mp3 differ diff --git a/packages/standard/audio/skill/fastchat_m18.mp3 b/packages/standard/audio/skill/fastchat_m18.mp3 new file mode 100644 index 00000000..3c0e4373 Binary files /dev/null and b/packages/standard/audio/skill/fastchat_m18.mp3 differ diff --git a/packages/standard/audio/skill/fastchat_m19.mp3 b/packages/standard/audio/skill/fastchat_m19.mp3 new file mode 100644 index 00000000..332201f2 Binary files /dev/null and b/packages/standard/audio/skill/fastchat_m19.mp3 differ diff --git a/packages/standard/audio/skill/fastchat_m20.mp3 b/packages/standard/audio/skill/fastchat_m20.mp3 new file mode 100644 index 00000000..1c7a7cb7 Binary files /dev/null and b/packages/standard/audio/skill/fastchat_m20.mp3 differ diff --git a/packages/standard/audio/skill/fastchat_m21.mp3 b/packages/standard/audio/skill/fastchat_m21.mp3 new file mode 100644 index 00000000..6de5e680 Binary files /dev/null and b/packages/standard/audio/skill/fastchat_m21.mp3 differ diff --git a/packages/standard/audio/skill/fastchat_m22.mp3 b/packages/standard/audio/skill/fastchat_m22.mp3 new file mode 100644 index 00000000..c4f5ade8 Binary files /dev/null and b/packages/standard/audio/skill/fastchat_m22.mp3 differ diff --git a/packages/standard/audio/skill/fastchat_m23.mp3 b/packages/standard/audio/skill/fastchat_m23.mp3 new file mode 100644 index 00000000..65cbd5cd Binary files /dev/null and b/packages/standard/audio/skill/fastchat_m23.mp3 differ diff --git a/packages/standard/aux_skills.lua b/packages/standard/aux_skills.lua index 310a0b0c..dcbca2a4 100644 --- a/packages/standard/aux_skills.lua +++ b/packages/standard/aux_skills.lua @@ -78,8 +78,8 @@ local chooseCardsSkill = fk.CreateActiveSkill{ local choosePlayersSkill = fk.CreateActiveSkill{ name = "choose_players_skill", - card_filter = function(self, to_select) - return self.pattern ~= "" and Exppattern:Parse(self.pattern):match(Fk:getCardById(to_select)) + card_filter = function(self, to_select, selected) + return self.pattern ~= "" and Exppattern:Parse(self.pattern):match(Fk:getCardById(to_select)) and #selected == 0 end, target_filter = function(self, to_select, selected, cards) if self.pattern ~= "" and #cards == 0 then return end @@ -119,24 +119,49 @@ local exChooseSkill = fk.CreateActiveSkill{ return table.contains(self.targets, to_select) end end, - min_target_num = function(self) return self.min_target_num end, - max_target_num = function(self) return self.max_target_num end, - min_card_num = function(self) return self.min_card_num end, - max_card_num = function(self) return self.max_card_num end, } local maxCardsSkill = fk.CreateMaxCardsSkill{ name = "max_cards_skill", global = true, correct_func = function(self, player) + local function getMark(markname) + local v = 0 + for mark, value in pairs(player.mark) do + if mark == markname then + v = v + value + elseif mark:startsWith(markname .. "-") then + for _, suffix in ipairs(MarkEnum.TempMarkSuffix) do + if mark:find(suffix, 1, true) then + v = v + value + break + end + end + end + end + return v + end return - player:getMark(MarkEnum.AddMaxCards) + - player:getMark(MarkEnum.AddMaxCardsInTurn) - - player:getMark(MarkEnum.MinusMaxCards) - - player:getMark(MarkEnum.MinusMaxCardsInTurn) + getMark(MarkEnum.AddMaxCards) - + getMark(MarkEnum.MinusMaxCards) end, } +local distributionSelectSkill = fk.CreateActiveSkill{ + name = "distribution_select_skill", + mute = true, + min_card_num = 1, + card_filter = function(self, to_select, selected) + return #selected < self.max_num and table.contains(self.cards, to_select) + end, + target_num = 1, + target_filter = function(self, to_select, selected, selected_cards) + return #selected == 0 and #selected_cards > 0 and table.contains(self.targets, to_select) + and #selected_cards <= (self.residued_list[string.format("%d", to_select)] or 0) + end, +} + + local choosePlayersToMoveCardInBoardSkill = fk.CreateActiveSkill{ name = "choose_players_to_move_card_in_board", target_num = 2, @@ -166,15 +191,32 @@ local uncompulsoryInvalidity = fk.CreateInvaliditySkill { name = "uncompulsory_invalidity", global = true, invalidity_func = function(self, from, skill) + ---@param object Card|Player + ---@param markname string + ---@param suffixes string[] + ---@return boolean + local function hasMark(object, markname, suffixes) + if not object then return false end + for mark, _ in pairs(object.mark) do + if mark == markname then return true end + if mark:startsWith(markname .. "-") then + for _, suffix in ipairs(suffixes) do + if mark:find(suffix, 1, true) then return true end + end + end + end + return false + end return (skill.frequency ~= Skill.Compulsory and skill.frequency ~= Skill.Wake) and not (skill:isEquipmentSkill() or skill.name:endsWith("&")) and - ( - from:getMark(MarkEnum.UncompulsoryInvalidity) ~= 0 or - table.find(MarkEnum.TempMarkSuffix, function(s) - return from:getMark(MarkEnum.UncompulsoryInvalidity .. s) ~= 0 - end) - ) + hasMark(from, MarkEnum.UncompulsoryInvalidity, MarkEnum.TempMarkSuffix) + -- ( + -- from:getMark(MarkEnum.UncompulsoryInvalidity) ~= 0 or + -- table.find(MarkEnum.TempMarkSuffix, function(s) + -- return from:getMark(MarkEnum.UncompulsoryInvalidity .. s) ~= 0 + -- end) + -- ) end } @@ -186,29 +228,37 @@ local revealProhibited = fk.CreateInvaliditySkill { if type(from:getMark(MarkEnum.RevealProhibited)) == "table" then generals = from:getMark(MarkEnum.RevealProhibited) end - for _, m in ipairs(table.map(MarkEnum.TempMarkSuffix, function(s) - return from:getMark(MarkEnum.RevealProhibited .. s) - end)) do - if type(m) == "table" then - for _, g in ipairs(m) do - table.insertIfNeed(generals, g) + + for mark, value in pairs(from.mark) do + if mark:startsWith(MarkEnum.RevealProhibited .. "-") and type(value) == "table" then + for _, suffix in ipairs(MarkEnum.TempMarkSuffix) do + if mark:find(suffix, 1, true) then + for _, g in ipairs(value) do + table.insertIfNeed(generals, g) + end + end end end end + -- for _, m in ipairs(table.map(MarkEnum.TempMarkSuffix, function(s) + -- return from:getMark(MarkEnum.RevealProhibited .. s) + -- end)) do + -- if type(m) == "table" then + -- for _, g in ipairs(m) do + -- table.insertIfNeed(generals, g) + -- end + -- end + -- end if #generals == 0 then return false end - if type(from._fake_skills) == "table" and not table.contains(from._fake_skills, skill) then return false end local sname = skill.name for _, g in ipairs(generals) do - if g == "m" then - if from.general ~= "anjiang" then return false end - else - if from.deputyGeneral ~= "anjiang" then return false end - end - local generalName = g == "m" and from:getMark("__heg_general") or from:getMark("__heg_deputy") - local general = Fk.generals[generalName] - if table.contains(general:getSkillNameList(), sname) then - return true + if (g == "m" and from.general == "anjiang") or (g == "d" and from.deputyGeneral == "anjiang") then + local generalName = g == "m" and from:getMark("__heg_general") or from:getMark("__heg_deputy") + local general = Fk.generals[generalName] + if table.contains(general:getSkillNameList(true), sname) then + return true + end end end return false @@ -223,7 +273,7 @@ local revealSkill = fk.CreateActiveSkill{ local choiceList = {} if (Self.general == "anjiang" and not Self:prohibitReveal()) then local general = Fk.generals[Self:getMark("__heg_general")] - for _, sname in ipairs(general:getSkillNameList()) do + for _, sname in ipairs(general:getSkillNameList(true)) do local s = Fk.skills[sname] if s.frequency == Skill.Compulsory and s.relate_to_place ~= "m" then table.insert(choiceList, "revealMain") @@ -233,7 +283,7 @@ local revealSkill = fk.CreateActiveSkill{ end if (Self.deputyGeneral == "anjiang" and not Self:prohibitReveal(true)) then local general = Fk.generals[Self:getMark("__heg_deputy")] - for _, sname in ipairs(general:getSkillNameList()) do + for _, sname in ipairs(general:getSkillNameList(true)) do local s = Fk.skills[sname] if s.frequency == Skill.Compulsory and s.relate_to_place ~= "d" then table.insert(choiceList, "revealDeputy") @@ -254,6 +304,30 @@ local revealSkill = fk.CreateActiveSkill{ elseif choice == "revealMain" then player:revealGeneral(false) elseif choice == "revealDeputy" then player:revealGeneral(true) end end, + can_use = function(self, player) + local choiceList = {} + if (player.general == "anjiang" and not player:prohibitReveal()) then + local general = Fk.generals[player:getMark("__heg_general")] + for _, sname in ipairs(general:getSkillNameList(true)) do + local s = Fk.skills[sname] + if s.frequency == Skill.Compulsory and s.relate_to_place ~= "m" then + table.insert(choiceList, "revealMain") + break + end + end + end + if (player.deputyGeneral == "anjiang" and not player:prohibitReveal(true)) then + local general = Fk.generals[player:getMark("__heg_deputy")] + for _, sname in ipairs(general:getSkillNameList(true)) do + local s = Fk.skills[sname] + if s.frequency == Skill.Compulsory and s.relate_to_place ~= "d" then + table.insert(choiceList, "revealDeputy") + break + end + end + end + return #choiceList > 0 + end } AuxSkills = { @@ -261,6 +335,7 @@ AuxSkills = { chooseCardsSkill, choosePlayersSkill, exChooseSkill, + distributionSelectSkill, maxCardsSkill, choosePlayersToMoveCardInBoardSkill, uncompulsoryInvalidity, diff --git a/packages/standard/game_rule.lua b/packages/standard/game_rule.lua index 72bb0ec8..e893d2b4 100644 --- a/packages/standard/game_rule.lua +++ b/packages/standard/game_rule.lua @@ -39,7 +39,7 @@ GameRule = fk.CreateTriggerSkill{ switch(event, { [fk.AskForPeaches] = function() local dyingPlayer = room:getPlayerById(data.who) - while dyingPlayer.hp < 1 do + while not (player.dead or dyingPlayer.dead) and dyingPlayer.hp < 1 do local cardNames = {"peach"} local prompt = "#AskForPeaches:" .. dyingPlayer.id .. "::" .. tostring(1 - dyingPlayer.hp) if player == dyingPlayer then @@ -53,7 +53,7 @@ GameRule = fk.CreateTriggerSkill{ end) if #cardNames == 0 then return end - local peach_use = room:askForUseCard(player, "peach", table.concat(cardNames, ",") , prompt) + local peach_use = room:askForUseCard(player, "peach", table.concat(cardNames, ",") , prompt, true, {analepticRecover = true}) if not peach_use then break end peach_use.tos = { {dyingPlayer.id} } if peach_use.card.trueName == "analeptic" then diff --git a/packages/standard/i18n/en_US.lua b/packages/standard/i18n/en_US.lua index 948ef599..173de1fb 100644 --- a/packages/standard/i18n/en_US.lua +++ b/packages/standard/i18n/en_US.lua @@ -192,6 +192,8 @@ Fk:loadTranslationTable({ ["discard_skill"] = "Discard", ["choose_cards_skill"] = "Choose card", ["choose_players_skill"] = "Choose players", + ["ex__choose_skill"] = "Choose", + ["distribution_select_skill"] = "Distribute", ["choose_players_to_move_card_in_board"] = "Choose players", ["reveal_skill"] = "Reveal", ["#reveal_skill"] = "Choose a character to reveal", diff --git a/packages/standard/i18n/zh_CN.lua b/packages/standard/i18n/zh_CN.lua index ee58a4a7..acfd0c31 100644 --- a/packages/standard/i18n/zh_CN.lua +++ b/packages/standard/i18n/zh_CN.lua @@ -8,6 +8,8 @@ Fk:loadTranslationTable{ ["qun"] = "群", ["caocao"] = "曹操", + ["#caocao"] = "魏武帝", + ["illustrator:caocao"] = "KayaK", ["~caocao"] = "霸业未成!未成啊!", ["$jianxiong1"] = "宁教我负天下人,休教天下人负我!", ["$jianxiong2"] = "吾好梦中杀人!", @@ -20,6 +22,8 @@ Fk:loadTranslationTable{ ["#hujia-ask"] = "护驾:你可打出一张闪,视为 %src 使用或打出", ["simayi"] = "司马懿", + ["#simayi"] = "狼顾之鬼", + ["illustrator:simayi"] = "KayaK", ["~simayi"] = "难道真是天意难违?", ["$guicai1"] = "天命?哈哈哈哈……", ["$guicai2"] = "吾乃天命之子!", @@ -32,6 +36,8 @@ Fk:loadTranslationTable{ [":fankui"] = "当你受到伤害后,你可以获得伤害来源的一张牌。", ["xiahoudun"] = "夏侯惇", + ["#xiahoudun"] = "独眼的罗刹", + ["illustrator:xiahoudun"] = "KayaK", ["~xiahoudun"] = "两边都看不见了……", ["$ganglie1"] = "鼠辈,竟敢伤我!", ["$ganglie2"] = "以彼之道,还施彼身!", @@ -39,6 +45,8 @@ Fk:loadTranslationTable{ [":ganglie"] = "当你受到伤害后,你可以进行判定:若结果不为红桃,则伤害来源选择一项:弃置两张手牌,或受到1点伤害。", ["zhangliao"] = "张辽", + ["#zhangliao"] = "前将军", + ["illustrator:zhangliao"] = "KayaK", ["~zhangliao"] = "真的没想到……", ["$tuxi1"] = "哼,没想到吧!", ["$tuxi2"] = "拿来吧!", @@ -47,6 +55,8 @@ Fk:loadTranslationTable{ ["#tuxi-ask"] = "是否发动“突袭”,改为获得1-2名角色各一张手牌?", ["xuchu"] = "许褚", + ["#xuchu"] = "虎痴", + ["illustrator:xuchu"] = "KayaK", ["~xuchu"] = "冷,好冷啊……", ["$luoyi1"] = "脱!", ["$luoyi2"] = "谁来与我大战三百回合?", @@ -54,6 +64,8 @@ Fk:loadTranslationTable{ [":luoyi"] = "摸牌阶段,你可以少摸一张牌,然后本回合你使用【杀】或【决斗】对目标角色造成伤害时,此伤害+1。", ["guojia"] = "郭嘉", + ["#guojia"] = "早终的先知", + ["illustrator:guojia"] = "KayaK", ["~guojia"] = "咳,咳……", ["$tiandu1"] = "就这样吧。", ["$tiandu2"] = "哦?", @@ -67,6 +79,8 @@ Fk:loadTranslationTable{ ["#yiji-give"] = "遗计:你可以将这些牌分配给任意角色,点“取消”自己保留", ["zhenji"] = "甄姬", + ["#zhenji"] = "薄幸的美人", + ["illustrator:zhenji"] = "KayaK", ["~zhenji"] = "悼良会之永绝兮,哀一逝而异乡。", ["$luoshen1"] = "髣髴兮若轻云之蔽月。", ["$luoshen2"] = "飘飖兮若流风之回雪。", @@ -79,6 +93,8 @@ Fk:loadTranslationTable{ [":qingguo"] = "你可以将一张黑色手牌当【闪】使用或打出。", ["liubei"] = "刘备", + ["#liubei"] = "乱世的枭雄", + ["illustrator:liubei"] = "KayaK", ["~liubei"] = "这就是桃园吗?", ["$rende1"] = "以德服人。", ["$rende2"] = "唯贤唯德,能服于人。", @@ -91,6 +107,8 @@ Fk:loadTranslationTable{ ["#jijiang-ask"] = "激将:你可打出一张杀,视为 %src 使用或打出", ["guanyu"] = "关羽", + ["#guanyu"] = "美髯公", + ["illustrator:guanyu"] = "KayaK", ["~guanyu"] = "什么?此地名叫麦城?", ["$wusheng1"] = "关羽在此,尔等受死!", ["$wusheng2"] = "看尔乃插标卖首!", @@ -98,6 +116,8 @@ Fk:loadTranslationTable{ [":wusheng"] = "你可以将一张红色牌当【杀】使用或打出。", ["zhangfei"] = "张飞", + ["#zhangfei"] = "万夫不当", + ["illustrator:zhangfei"] = "KayaK", ["~zhangfei"] = "实在是,杀不动了……", ["$paoxiao1"] = "啊~~~!", ["$paoxiao2"] = "燕人张飞在此!", @@ -105,6 +125,8 @@ Fk:loadTranslationTable{ [":paoxiao"] = "锁定技,出牌阶段,你使用【杀】无次数限制。", ["zhugeliang"] = "诸葛亮", + ["#zhugeliang"] = "迟暮的丞相", + ["illustrator:zhugeliang"] = "KayaK", ["~zhugeliang"] = "将星陨落,天命难违。", ["$guanxing1"] = "观今夜天象,知天下大事。", ["$guanxing2"] = "知天易,逆天难。", @@ -116,6 +138,8 @@ Fk:loadTranslationTable{ [":kongcheng"] = "锁定技,若你没有手牌,你不能被选择为【杀】或【决斗】的目标。", ["zhaoyun"] = "赵云", + ["#zhaoyun"] = "少年将军", + ["illustrator:zhaoyun"] = "KayaK", ["~zhaoyun"] = "这,就是失败的滋味吗?", ["$longdan1"] = "能进能退乃真正法器!", ["$longdan2"] = "吾乃常山赵子龙也!", @@ -123,6 +147,8 @@ Fk:loadTranslationTable{ [":longdan"] = "你可以将一张【杀】当【闪】使用或打出,或将一张【闪】当普通【杀】使用或打出。", ["machao"] = "马超", + ["#machao"] = "一骑当千", + ["illustrator:machao"] = "KayaK", ["~machao"] = "(马蹄远去声)", ["mashu"] = "马术", [":mashu"] = "锁定技,你与其他角色的距离-1。", @@ -132,6 +158,8 @@ Fk:loadTranslationTable{ [":tieqi"] = "当你指定【杀】的目标后,你可以进行判定:若结果为红色,该角色不能使用【闪】响应此【杀】。", ["huangyueying"] = "黄月英", + ["#huangyueying"] = "归隐的杰女", + ["illustrator:huangyueying"] = "KayaK", ["~huangyueying"] = "亮……", ["$jizhi1"] = "哼哼~", ["$jizhi2"] = "哼~", @@ -141,6 +169,8 @@ Fk:loadTranslationTable{ [":qicai"] = "锁定技,你使用锦囊牌无距离限制。", ["sunquan"] = "孙权", + ["#sunquan"] = "年轻的贤君", + ["illustrator:sunquan"] = "KayaK", ["~sunquan"] = "父亲,大哥,仲谋愧矣……", ["$zhiheng1"] = "容我三思。", ["$zhiheng2"] = "且慢。", @@ -152,6 +182,8 @@ Fk:loadTranslationTable{ [":jiuyuan"] = "主公技,其他吴势力角色使用【桃】令你回复体力时,回复值+1。", ["ganning"] = "甘宁", + ["#ganning"] = "锦帆游侠", + ["illustrator:ganning"] = "KayaK", ["~ganning"] = "二十年后,又是一条好汉……", ["$qixi1"] = "接招吧!", ["$qixi2"] = "你的牌太多啦!", @@ -159,6 +191,8 @@ Fk:loadTranslationTable{ [":qixi"] = "你可以将一张黑色牌当【过河拆桥】使用。", ["lvmeng"] = "吕蒙", + ["#lvmeng"] = "白衣渡江", + ["illustrator:lvmeng"] = "KayaK", ["~lvmeng"] = "被看穿了吗……", ["$keji1"] = "不是不报,时候未到!", ["$keji2"] = "留得青山在,不怕没柴烧!", @@ -166,6 +200,8 @@ Fk:loadTranslationTable{ [":keji"] = "若你未于出牌阶段内使用或打出【杀】,你可以跳过弃牌阶段。", ["huanggai"] = "黄盖", + ["#huanggai"] = "轻身为国", + ["illustrator:huanggai"] = "KayaK", ["~huanggai"] = "失血……过多了……", ["$kurou1"] = "请鞭笞我吧,公瑾!", ["$kurou2"] = "赴汤蹈火,在所不辞!", @@ -173,6 +209,8 @@ Fk:loadTranslationTable{ [":kurou"] = "出牌阶段,你可以失去1点体力然后摸两张牌。", ["zhouyu"] = "周瑜", + ["#zhouyu"] = "大都督", + ["illustrator:zhouyu"] = "KayaK", ["~zhouyu"] = "既生瑜,何生……", ["$yingzi1"] = "哈哈哈哈……", ["$yingzi2"] = "汝等看好了!", @@ -184,6 +222,8 @@ Fk:loadTranslationTable{ [":fanjian"] = "阶段技。你可以令一名其他角色选择一种花色,然后正面朝上获得你的一张手牌。若此牌花色与该角色所选花色不同,你对其造成1点伤害。", ["daqiao"] = "大乔", + ["#daqiao"] = "矜持之花", + ["illustrator:daqiao"] = "KayaK", ["~daqiao"] = "伯符,我去了……", ["$guose1"] = "请休息吧。", ["$guose2"] = "你累了。", @@ -196,6 +236,8 @@ Fk:loadTranslationTable{ ["#liuli-target"] = "流离:你可以弃置一张牌,将【杀】的目标转移给一名其他角色", ["luxun"] = "陆逊", + ["#luxun"] = "儒生雄才", + ["illustrator:luxun"] = "KayaK", ["~luxun"] = "我还是太年轻了……", ["$qianxun1"] = "儒生脱尘,不为贪逸淫乐之事。", ["$qianxun2"] = "谦谦君子,不饮盗泉之水。", @@ -207,6 +249,8 @@ Fk:loadTranslationTable{ [":lianying"] = "当你失去最后的手牌后,你可以摸一张牌。", ["sunshangxiang"] = "孙尚香", + ["#sunshangxiang"] = "弓腰姬", + ["illustrator:sunshangxiang"] = "KayaK", ["~sunshangxiang"] = "不,还不可以死……", ["$xiaoji1"] = "哼!", ["$xiaoji2"] = "看我的厉害!", @@ -218,6 +262,8 @@ Fk:loadTranslationTable{ [":jieyin"] = "出牌阶段限一次,你可以弃置两张手牌并选择一名已受伤的男性角色:若如此做,你和该角色各回复1点体力。", ["huatuo"] = "华佗", + ["#huatuo"] = "神医", + ["illustrator:huatuo"] = "KayaK", ["~huatuo"] = "医者……不能自医啊……", ["$qingnang1"] = "早睡早起,方能养生。", ["$qingnang2"] = "越老越要补啊。", @@ -229,6 +275,8 @@ Fk:loadTranslationTable{ [":jijiu"] = "你的回合外,你可以将一张红色牌当【桃】使用。", ["lvbu"] = "吕布", + ["#lvbu"] = "武的化身", + ["illustrator:lvbu"] = "KayaK", ["~lvbu"] = "不可能……!", ["$wushuang1"] = "谁能挡我!", ["$wushuang2"] = "神挡杀神,佛挡杀佛!", @@ -236,11 +284,13 @@ Fk:loadTranslationTable{ [":wushuang"] = "锁定技,当你使用【杀】指定目标后,其使用【闪】抵消此【杀】的方式改为需连续使用两张【闪】;当你使用【决斗】指定目标后,或当你成为【决斗】的目标后,你令其打出【杀】响应此【决斗】的方式改为需连续打出两张【杀】。", ["diaochan"] = "貂蝉", + ["#diaochan"] = "绝世的舞姬", + ["illustrator:diaochan"] = "KayaK", ["~diaochan"] = "父亲大人,对不起……", ["$lijian1"] = "嗯呵呵~~呵呵~~", ["$lijian2"] = "夫君,你要替妾身作主啊……", ["lijian"] = "离间", - [":lijian"] = "出牌阶段限一次,你可以弃置一张牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被无懈可击的决斗。", + [":lijian"] = "出牌阶段限一次,你可以弃置一张牌并选择两名其他男性角色,后选择的角色视为对先选择的角色使用了一张不能被【无懈可击】的【决斗】。", ["$biyue1"] = "失礼了~", ["$biyue2"] = "羡慕吧~", ["biyue"] = "闭月", @@ -265,6 +315,13 @@ Fk:loadTranslationTable{ ["$fastchat_m14"] = "哥们,给力点行吗?", ["$fastchat_m15"] = "哥哥,交个朋友吧。", ["$fastchat_m16"] = "妹子,交个朋友吧。", + ["$fastchat_m17"] = "我从未见过如此厚颜无耻之人!", + ["$fastchat_m18"] = "你随便杀,闪不了算我输。", + ["$fastchat_m19"] = "这波,不亏。", + ["$fastchat_m20"] = "请收下我的膝盖。", + ["$fastchat_m21"] = "你咋不上天呢?", + ["$fastchat_m22"] = "放开我的队友,冲我来。", + ["$fastchat_m23"] = "见证奇迹的时刻到了。", ["$fastchat_f1"] = "能不能快一点啊,兵贵神速啊。", ["$fastchat_f2"] = "主公,别开枪,自己人!", ["$fastchat_f3"] = "小内再不跳,后面还怎么玩啊?", @@ -281,6 +338,13 @@ Fk:loadTranslationTable{ ["$fastchat_f14"] = "哥们,给力点行吗?", ["$fastchat_f15"] = "哥,交个朋友吧。", ["$fastchat_f16"] = "妹子,交个朋友吧。", + ["$fastchat_f17"] = "我从未见过如此厚颜无耻之人!", + ["$fastchat_f18"] = "你随便杀,闪不了算我输。", + ["$fastchat_f19"] = "这波,不亏。", + ["$fastchat_f20"] = "请收下我的膝盖。", + ["$fastchat_f21"] = "你咋不上天呢?", + ["$fastchat_f22"] = "放开我的队友,冲我来。", + ["$fastchat_f23"] = "见证奇迹的时刻到了。", ["aaa_role_mode"] = "身份模式", [":aaa_role_mode"] = [========================================[ @@ -455,6 +519,8 @@ Fk:loadTranslationTable{ ["discard_skill"] = "弃牌", ["choose_cards_skill"] = "选牌", ["choose_players_skill"] = "选择角色", + ["ex__choose_skill"] = "选择", + ["distribution_select_skill"] = "分配", ["choose_players_to_move_card_in_board"] = "选择角色", ["reveal_skill"] = "亮将", ["#reveal_skill"] = "选择一个武将亮将(点击左侧选择框展开)", diff --git a/packages/standard/init.lua b/packages/standard/init.lua index a1fbb3fb..2b5fbaba 100644 --- a/packages/standard/init.lua +++ b/packages/standard/init.lua @@ -225,6 +225,21 @@ local tiandu = fk.CreateTriggerSkill{ player.room:obtainCard(player.id, data.card, true, fk.ReasonJustMove) end, } +local yiji_active = fk.CreateActiveSkill{ + name = "yiji_active", + expand_pile = function(self) + return type(Self:getMark("yiji_cards")) == "table" and Self:getMark("yiji_cards") or {} + end, + min_card_num = 1, + target_num = 1, + card_filter = function(self, to_select, selected, targets) + local ids = Self:getMark("yiji_cards") + return type(ids) == "table" and table.contains(ids, to_select) + end, + target_filter = function(self, to_select, selected, selected_cards) + return #selected == 0 and to_select ~= Self.id + end, +} local yiji = fk.CreateTriggerSkill{ name = "yiji", anim_type = "masochism", @@ -246,77 +261,32 @@ local yiji = fk.CreateTriggerSkill{ on_use = function(self, event, target, player, data) local room = player.room local ids = room:getNCards(2) - local fakemove = { - toArea = Card.PlayerHand, - to = player.id, - moveInfo = table.map(ids, function(id) return {cardId = id, fromArea = Card.Void} end), - moveReason = fk.ReasonJustMove, - } - room:notifyMoveCards({player}, {fakemove}) - for _, id in ipairs(ids) do - room:setCardMark(Fk:getCardById(id), "yiji", 1) - end - player.tag["yiji_ids"] = ids --存储遗技卡牌表 - while table.find(ids, function(id) return Fk:getCardById(id):getMark("yiji") > 0 end) do - if not room:askForUseActiveSkill(player, "yiji_active", "#yiji-give", true) then - for _, id in ipairs(ids) do - room:setCardMark(Fk:getCardById(id), "yiji", 0) + while true do + room:setPlayerMark(player, "yiji_cards", ids) + local _, ret = room:askForUseActiveSkill(player, "yiji_active", "#yiji-give", true, nil, true) + room:setPlayerMark(player, "yiji_cards", 0) + if ret then + for _, id in ipairs(ret.cards) do + table.removeOne(ids, id) end - ids = table.filter(ids, function(id) return room:getCardArea(id) ~= Card.PlayerHand end) - fakemove = { - from = player.id, - toArea = Card.Void, - moveInfo = table.map(ids, function(id) return {cardId = id, fromArea = Card.PlayerHand} end), - moveReason = fk.ReasonGive, - } - room:notifyMoveCards({player}, {fakemove}) - room:moveCards({ - fromArea = Card.Void, - ids = ids, - to = player.id, - toArea = Card.PlayerHand, - moveReason = fk.ReasonGive, - skillName = self.name, - }) + room:moveCardTo(ret.cards, Card.PlayerHand, room:getPlayerById(ret.targets[1]), fk.ReasonGive, self.name, nil, false, player.id) + if #ids == 0 then break end + if player.dead then + room:moveCards({ + ids = ids, + toArea = Card.DiscardPile, + moveReason = fk.ReasonJustMove, + skillName = self.name, + }) + break + end + else + room:moveCardTo(ids, Player.Hand, player, fk.ReasonGive, self.name, nil, false, player.id) + break end end end, } -local yiji_active = fk.CreateActiveSkill{ - name = "yiji_active", - mute = true, - min_card_num = 1, - target_num = 1, - card_filter = function(self, to_select, selected, targets) - return Fk:getCardById(to_select):getMark("yiji") > 0 - end, - target_filter = function(self, to_select, selected, selected_cards) - return #selected == 0 - end, - on_use = function(self, room, effect) - local player = room:getPlayerById(effect.from) - local target = room:getPlayerById(effect.tos[1]) - room:doIndicate(player.id, {target.id}) - for _, id in ipairs(effect.cards) do - room:setCardMark(Fk:getCardById(id), "yiji", 0) - end - local fakemove = { - from = player.id, - toArea = Card.Void, - moveInfo = table.map(effect.cards, function(id) return {cardId = id, fromArea = Card.PlayerHand} end), - moveReason = fk.ReasonGive, - } - room:notifyMoveCards({player}, {fakemove}) - room:moveCards({ - fromArea = Card.Void, - ids = effect.cards, - to = target.id, - toArea = Card.PlayerHand, - moveReason = fk.ReasonGive, - skillName = self.name, - }) - end, -} local guojia = General:new(extension, "guojia", "wei", 3) Fk:addSkill(yiji_active) guojia:addSkill(tiandu) @@ -1162,8 +1132,11 @@ local role_getlogic = function() lord_general = lord_generals lord_generals = {lord_general} end - - generals = table.filter(generals, function(g) return not table.contains(lord_generals, g) end) + generals = table.filter(generals, function(g) + return not table.find(lord_generals, function(lg) + return Fk.generals[lg].trueName == Fk.generals[g].trueName + end) + end) room:returnToGeneralPile(generals) room:setPlayerGeneral(lord, lord_general, true) @@ -1172,6 +1145,56 @@ local role_getlogic = function() room:broadcastProperty(lord, "kingdom") room:setDeputyGeneral(lord, deputy) room:broadcastProperty(lord, "deputyGeneral") + + -- 显示技能 + local canAttachSkill = function(player, skillName) + local skill = Fk.skills[skillName] + if not skill then + fk.qCritical("Skill: "..skillName.." doesn't exist!") + return false + end + if skill.lordSkill and (player.role ~= "lord" or #room.players < 5) then + return false + end + + if #skill.attachedKingdom > 0 and not table.contains(skill.attachedKingdom, player.kingdom) then + return false + end + + return true + end + + local lord_skills = {} + for _, s in ipairs(Fk.generals[lord.general].skills) do + if canAttachSkill(lord, s.name) then + table.insertIfNeed(lord_skills, s.name) + end + end + for _, sname in ipairs(Fk.generals[lord.general].other_skills) do + if canAttachSkill(lord, sname) then + table.insertIfNeed(lord_skills, sname) + end + end + + local deputyGeneral = Fk.generals[lord.deputyGeneral] + if deputyGeneral then + for _, s in ipairs(deputyGeneral.skills) do + if canAttachSkill(lord, s.name) then + table.insertIfNeed(lord_skills, s.name) + end + end + for _, sname in ipairs(deputyGeneral.other_skills) do + if canAttachSkill(lord, sname) then + table.insertIfNeed(lord_skills, sname) + end + end + end + for _, skill in ipairs(lord_skills) do + room:doBroadcastNotify("AddSkill", json.encode{ + lord.id, + skill + }) + end end local nonlord = room:getOtherPlayers(lord, true) @@ -1196,13 +1219,18 @@ local role_getlogic = function() room:setPlayerGeneral(p, general, true, true) room:setDeputyGeneral(p, deputy) else + table.insertTableIfNeed(selected, p.default_reply) room:setPlayerGeneral(p, p.default_reply[1], true, true) room:setDeputyGeneral(p, p.default_reply[2]) end p.default_reply = "" end - generals = table.filter(generals, function(g) return not table.contains(selected, g) end) + generals = table.filter(generals, function(g) + return not table.find(selected, function(lg) + return Fk.generals[lg].trueName == Fk.generals[g].trueName + end) + end) room:returnToGeneralPile(generals) room:askForChooseKingdom(nonlord) diff --git a/packages/standard_cards/ai/init.lua b/packages/standard_cards/ai/init.lua index 4fb90903..9c94733a 100644 --- a/packages/standard_cards/ai/init.lua +++ b/packages/standard_cards/ai/init.lua @@ -1,16 +1,50 @@ +-- TODO: 合法性的方便函数 +-- TODO: 关于如何选择多个目标 +-- TODO: 关于装备牌 + -- 基本牌:杀,闪,桃 -fk.ai_use_card["slash"] = function(self, pattern, prompt, cancelable, extra_data) - local slashes = self:getCards("slash", "use", extra_data) +---@param from ServerPlayer +---@param to ServerPlayer +---@param card Card +local function tgtValidator(from, to, card) + return not from:prohibitUse(card) and + not from:isProhibited(to, card) and + true -- feasible +end + +local function justUse(self, card_name, extra_data) + local slashes = self:getCards(card_name, "use", extra_data) + if #slashes == 0 then return nil end + + return self:buildUseReply(slashes[1].id) +end + +---@param self SmartAI +---@param card_name string +local function useToEnemy(self, card_name, extra_data) + local slashes = self:getCards(card_name, "use", extra_data) if #slashes == 0 then return nil end -- TODO: 目标合法性 local targets = {} - if self.enemies[1] then table.insert(targets, self.enemies[1].id) end + if self.enemies[1] then + table.insert(targets, self.enemies[1].id) + else + return nil + end return self:buildUseReply(slashes[1].id, targets) end +fk.ai_use_card["slash"] = function(self, pattern, prompt, cancelable, extra_data) + return useToEnemy(self, "slash", extra_data) +end + +fk.ai_use_card["jink"] = function(self, pattern, prompt, cancelable, extra_data) + return justUse(self, "jink", extra_data) +end + fk.ai_use_card["peach"] = function(self, _, _, _, extra_data) local cards = self:getCards("peach", "use", extra_data) if #cards == 0 then return nil end @@ -32,443 +66,22 @@ fk.ai_use_card["#AskForPeaches"] = function(self) return nil end ---[[ -fk.ai_card.slash = { - intention = 100, -- 身份值 - value = 4, -- 卡牌价值 - priority = 2.5 -- 使用优先值 -} -fk.ai_card.peach = { - intention = -150, - value = 10, - priority = 0.5 -} -fk.ai_card.dismantlement = { - intention = function(self, card, from) - if #self.player.player_cards[Player.Judge] < 1 then - return 80 - elseif self.ai_role[from.id] == "neutral" then - return 30 - end - end, - value = 3.5, - priority = 10.5 -} -fk.ai_card.snatch = { - intention = function(self, card, from) - if #self.player.player_cards[Player.Judge] < 1 then - return 80 - elseif self.ai_role[from.id] == "neutral" then - return 30 - end - end, - value = 4.5, - priority = 10.4 -} -fk.ai_card.duel = { - intention = 120, - value = 4.5, - priority = 3.5 -} -fk.ai_card.collateral = { - intention = 20, - value = 3, - priority = 4.5 -} -fk.ai_card.ex_nihilo = { - intention = -200, - value = 8, - priority = 10 -} -fk.ai_card.savage_assault = { - intention = 20, - value = 2, - priority = 4 -} -fk.ai_card.archery_attack = { - intention = 30, - value = 2, - priority = 3 -} -fk.ai_card.god_salvation = { - intention = function(self, card, from) - if self.player.hp ~= self.player.maxHp then - return -45 - end - end, - value = 1.5, - priority = 4 -} -fk.ai_card.amazing_grace = { - intention = -30, - value = 2, - priority = 2 -} -fk.ai_card.indulgence = { - intention = 150, - value = -1, - priority = 2 -} - -local function slashEeffect(slash, to) - for _, s in ipairs(to:getAllSkills()) do - if s.name == "#vine_skill" then - if slash.name == "slash" then - return - end - end - if s.name == "#nioh_shield_skill" then - if slash.color == Card.Black then - return - end - end - end - return true +fk.ai_use_card["dismantlement"] = function(self, pattern, prompt, cancelable, extra_data) + return useToEnemy(self, "dismantlement", extra_data) end -fk.ai_use_play["slash"] = function(self, card) - self:sort(self.enemies) - for _, p in ipairs(self.enemies) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) and slashEeffect(card, p) and self:objectiveLevel(p) > 2 then - self.use_id = card.id - table.insert(self.use_tos, p.id) - end - end +fk.ai_use_card["snatch"] = function(self, pattern, prompt, cancelable, extra_data) + return useToEnemy(self, "snatch", extra_data) end -fk.ai_use_card["#slash-jink"] = function(self, pattern, prompt, cancelable, extra_data) - local act = self:getActives(pattern) - if tonumber(prompt:split(":")[4]) > #act then - return - end - local cards = - table.map( - self.player:getCardIds("&he"), - function(id) - return Fk:getCardById(id) - end - ) - self:sortValue(cards) - for _, sth in ipairs(act) do - if sth:isInstanceOf(Card) then - self.use_id = sth.id - break - else - local selected = {} - for _, c in ipairs(cards) do - if sth.cardFilter(sth, c.id, selected) then - table.insert(selected, c.id) - end - end - local tc = sth.viewAs(sth, selected) - if tc and tc:matchPattern(pattern) then - self.use_id = - json.encode { - skill = sth.name, - subcards = selected - } - break - end - end - end +fk.ai_use_card["duel"] = function(self, pattern, prompt, cancelable, extra_data) + return useToEnemy(self, "duel", extra_data) end -fk.ai_use_card["#slash-jinks"] = fk.ai_use_card["#slash-jink"] - -fk.ai_use_play["snatch"] = function(self, card) - for _, p in ipairs(self.friends_noself) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) and #p:getCardIds("j") > 0 then - self.use_id = card.id - table.insert(self.use_tos, p.id) - end - end - self:sort(self.enemies) - for _, p in ipairs(self.enemies) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) and #p:getCardIds("he") > 0 then - self.use_id = card.id - table.insert(self.use_tos, p.id) - end - end +fk.ai_use_card["ex_nihilo"] = function(self, pattern, prompt, cancelable, extra_data) + return justUse(self, "ex_nihilo", extra_data) end -fk.ai_nullification.snatch = function(self, card, to, from, positive) - if positive then - if self:isFriend(to) and not self:isFriend(from) and self.ai_role[from.id] ~= "neutral" then - if #self.avail_cards > 1 or self:isWeak(to) or to.id == self.player.id then - self.use_id = self.avail_cards[1] - end - end - else - if self:isEnemy(to) and self:isEnemy(from) then - if #self.avail_cards > 1 or self:isWeak(to) then - self.use_id = self.avail_cards[1] - end - end - end +fk.ai_use_card["indulgence"] = function(self, pattern, prompt, cancelable, extra_data) + return useToEnemy(self, "indulgence", extra_data) end - -fk.ai_use_play["dismantlement"] = function(self, card) - for _, p in ipairs(self.friends_noself) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) and #p:getCardIds("j") > 0 then - self.use_id = card.id - table.insert(self.use_tos, p.id) - end - end - self:sort(self.enemies) - for _, p in ipairs(self.enemies) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) and #p:getCardIds("he") > 0 then - self.use_id = card.id - table.insert(self.use_tos, p.id) - end - end -end - -fk.ai_nullification.dismantlement = function(self, card, to, from, positive) - if positive then - if self:isFriend(to) and not self:isFriend(from) and self.ai_role[from.id] ~= "neutral" then - if #self.avail_cards > 1 or self:isWeak(to) or to.id == self.player.id then - self.use_id = self.avail_cards[1] - end - end - else - if self:isEnemy(to) and self:isEnemy(from) then - if #self.avail_cards > 1 or self:isWeak(to) then - self.use_id = self.avail_cards[1] - end - end - end -end - -fk.ai_use_play["indulgence"] = function(self, card) - self:sort(self.enemies, nil, true) - for _, p in ipairs(self.enemies) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) then - self.use_id = card.id - table.insert(self.use_tos, p.id) - end - end -end - -fk.ai_nullification.indulgence = function(self, card, to, from, positive) - if positive then - if self:isFriend(to) then - if #self.avail_cards > 1 or self:isWeak(to) or to.id == self.player.id then - self.use_id = self.avail_cards[1] - end - end - else - if self:isEnemy(to) then - if #self.avail_cards > 1 or self:isWeak(to) then - self.use_id = self.avail_cards[1] - end - end - end -end - -fk.ai_use_play["collateral"] = function(self, card) - local max = (card.skill:getMaxTargetNum(self.player, card) - 1) * 2 - self:sort(self.enemies) - for _, p in ipairs(self.enemies) do - if #self.use_tos < max and card.skill:targetFilter(p.id, {}, {}, card) then - for _, pt in ipairs(self.enemies) do - if p ~= pt and p:inMyAttackRange(pt) then - table.insert(self.use_tos, p.id) - table.insert(self.use_tos, pt.id) - self.use_id = card.id - break - end - end - end - end - for _, p in ipairs(self.friends_noself) do - if #self.use_tos < max and card.skill:targetFilter(p.id, {}, {}, card) then - for _, pt in ipairs(self.enemies) do - if p ~= pt and p:inMyAttackRange(pt) then - table.insert(self.use_tos, p.id) - table.insert(self.use_tos, pt.id) - self.use_id = card.id - break - end - end - end - end -end - -fk.ai_nullification.collateral = function(self, card, to, from, positive) - if positive then - if self:isFriend(to) and self:isEnemy(from) then - if #self.avail_cards > 1 or self:isWeak(to) or to.id == self.player.id then - self.use_id = self.avail_cards[1] - end - end - end -end - -fk.ai_nullification.ex_nihilo = function(self, card, to, from, positive) - if positive then - if self:isEnemy(to) then - if #self.avail_cards > 1 or self:isWeak(to) then - self.use_id = self.avail_cards[1] - end - end - else - if self:isFriend(to) then - if #self.avail_cards > 1 or self:isWeak(to) or to.id == self.player.id then - self.use_id = self.avail_cards[1] - end - end - end -end - -fk.ai_nullification.savage_assault = function(self, card, to, from, positive) - if positive then - if self:isFriend(to) then - if #self.avail_cards > 1 or self:isWeak(to) or to.id == self.player.id then - self.use_id = self.avail_cards[1] - end - end - else - if self:isEnemy(to) then - if #self.avail_cards > 1 or self:isWeak(to) then - self.use_id = self.avail_cards[1] - end - end - end -end - -fk.ai_nullification.archery_attack = function(self, card, to, from, positive) - if positive then - if self:isFriend(to) then - if #self.avail_cards > 1 or self:isWeak(to) or to.id == self.player.id then - self.use_id = self.avail_cards[1] - end - end - else - if self:isEnemy(to) then - if #self.avail_cards > 1 or self:isWeak(to) then - self.use_id = self.avail_cards[1] - end - end - end -end - -fk.ai_nullification.god_salvation = function(self, card, to, from, positive) - if positive then - if self:isEnemy(to) and to.hp ~= to.maxHp then - if #self.avail_cards > 1 or self:isWeak(to) then - self.use_id = self.avail_cards[1] - end - end - else - if self:isFriend(to) and to.hp ~= to.maxHp then - if #self.avail_cards > 1 or self:isWeak(to) or to.id == self.player.id then - self.use_id = self.avail_cards[1] - end - end - end -end - -fk.ai_use_play["god_salvation"] = function(self, card) - local can = 0 - for _, p in ipairs(self.enemies) do - if p:isWounded() - then - can = can - 1 - if self:isWeak(p) - then - can = can - 1 - end - end - end - for _, p in ipairs(self.friends) do - if p:isWounded() - then - can = can + 1 - if self:isWeak(p) - then - can = can + 1 - end - end - end - self.use_id = can > 0 and card.id -end - -fk.ai_use_play["amazing_grace"] = function(self, card) - self.use_id = #self.player:getCardIds("&h") <= self.player.hp and card.id -end - -fk.ai_use_play["ex_nihilo"] = function(self, card) - self.use_id = card.id -end - -fk.ai_use_play["lightning"] = function(self, card) - self.use_id = #self.enemies > #self.friends and card.id -end - -fk.ai_use_play["peach"] = function(self, card) - if self.command == "PlayCard" then - self.use_id = self.player.hp ~= self.player.maxHp and self.player.hp < #self.player:getCardIds("h") and card.id - else - for _, p in ipairs(self.friends) do - if p.dying then - self.use_id = card.id - self.use_tos = { p.id } - break - end - end - end -end - -fk.ai_use_play["duel"] = function(self, card) - self:sort(self.enemies) - for _, p in ipairs(self.enemies) do - if card.skill:targetFilter(p.id, self.use_tos, {}, card) then - self.use_id = card.id - table.insert(self.use_tos, p.id) - end - end -end - -fk.ai_skill_invoke["#ice_sword_skill"] = function(self) - local damage = self:eventData("Damage") - return self:isFriend(damage.to) or not self:isWeak(damage.to) and #damage.to:getCardIds("e") > 1 -end - -fk.ai_skill_invoke["#double_swords_skill"] = function(self) - local use = self:eventData("UseCard") - for _, p in ipairs(TargetGroup:getRealTargets(use.tos)) do - if not self:isFriend(p) and self.room:getPlayerById(p).gender ~= self.player.gender then - return true - end - end -end - -fk.ai_discard["#double_swords_skill"] = function(self, min_num, num, include_equip, cancelable, pattern, prompt) - local use = self:eventData("UseCard") - return self:isEnemy(use.from) and { self.player:getCardIds("h")[1] } -end - -fk.ai_discard["#axe_skill"] = function(self, min_num, num, include_equip, cancelable, pattern, prompt) - local ids = {} - local effect = self:eventData("CardEffect") - for _, cid in ipairs(self.player:getCardIds("he")) do - if Fk:getCardById(cid):matchPattern(pattern) then - table.insert(ids, cid) - end - if #ids >= min_num and self:isEnemy(effect.to) - and (self:isWeak(effect.to) or #self.player:getCardIds("he") > 3) then - return ids - end - end -end - -fk.ai_skill_invoke["#kylin_bow_skill"] = function(self) - local damage = self:eventData("Damage") - return not self:isFriend(damage.to) -end - -fk.ai_skill_invoke["#eight_diagram_skill"] = function(self) - local effect = self:eventData("CardEffect") - return effect and self:isFriend(effect.to) -end ---]] diff --git a/packages/standard_cards/i18n/en_US.lua b/packages/standard_cards/i18n/en_US.lua index b5fcde3a..0c0f9d6c 100644 --- a/packages/standard_cards/i18n/en_US.lua +++ b/packages/standard_cards/i18n/en_US.lua @@ -54,7 +54,8 @@ Fk:loadTranslationTable({ ["slash"] = "Slash", [":slash"] = "Slash (basic card)
Phase: Action phase
Target: Another player within your ATK range
Effect: Deal 1 DMG to the targets.
Note: You can only use 1 Slash per action phase.", - ["#slash-jink"] = "%src used Slash to you, please use %arg Dodge(s)", + ["#slash-jink"] = "%src used Slash to you, please use a Dodge", + ["#slash-jink-multi"] = "%src used Slash to you, please use a Dodge( %arg th, %arg2 total )", ["#slash_skill"] = "Choose 1 player within your ATK range, deal 1 DMG to him", ["#slash_skill_multi"] = "Choose up to %arg players within your ATK range. Deal 1 DMG to them", @@ -119,6 +120,7 @@ Fk:loadTranslationTable({ ["crossbow"] = "Crossbow", [":crossbow"] = "Crossbow (equip card, weapon)
ATK range: 1
Weapon skill: You can use any amount of Slash in your action phase.", + ["#crossbow_skill"] = "Crossbow", ["qinggang_sword"] = "Qinggang Sword", [":qinggang_sword"] = "Qinggang Sword (equip card, weapon)
ATK range: 2
Weapon skill: Your Slash ignores the target's armor.", @@ -151,6 +153,7 @@ Fk:loadTranslationTable({ ["halberd"] = "Halberd", [":halberd"] = "Halberd (equip card, weapon)
ATK range: 4
Weapon skill: When you are about to use Slash which is your last hand card, you can target up to +2 extra targets.", + ["#halberd_skill"] = "Halberd", ["kylin_bow"] = "Kylin Bow", [":kylin_bow"] = "Kylin Bow (equip card, weapon)
ATK range: 5
Weapon skill: When your used Slash is about to cause DMG, you can discard 1 of his equipped horse.", @@ -162,6 +165,7 @@ Fk:loadTranslationTable({ ["nioh_shield"] = "Nioh Shield", [":nioh_shield"] = "Nioh Shield (equip card, armor)
Armor skill: Black Slash has no effect on you.", + ["#nioh_shield_skill"] = "Nioh Shield", ["dilu"] = "Di Lu", [":dilu"] = "Di Lu (equip card, horse)
Horse skill: The distance from other players to you is increased by +1.", diff --git a/packages/standard_cards/i18n/zh_CN.lua b/packages/standard_cards/i18n/zh_CN.lua index f1aad0d6..66f7700f 100644 --- a/packages/standard_cards/i18n/zh_CN.lua +++ b/packages/standard_cards/i18n/zh_CN.lua @@ -54,7 +54,8 @@ Fk:loadTranslationTable{ ["slash"] = "杀", [":slash"] = "基本牌
时机:出牌阶段
目标:攻击范围内的一名角色
效果:对目标角色造成1点伤害。", - ["#slash-jink"] = "%src 对你使用了杀,请使用 %arg 张闪", + ["#slash-jink"] = "%src 对你使用了杀,请使用一张闪", + ["#slash-jink-multi"] = "%src 对你使用了杀,请使用一张闪(此为第 %arg 张,共需 %arg2 张)", ["#slash_skill"] = "选择攻击范围内的一名角色,对其造成1点伤害", ["#slash_skill_multi"] = "选择攻击范围内的至多%arg名角色,对这些角色各造成1点伤害", @@ -119,6 +120,7 @@ Fk:loadTranslationTable{ ["crossbow"] = "诸葛连弩", [":crossbow"] = "装备牌·武器
攻击范围:1
武器技能:锁定技,你于出牌阶段内使用【杀】无次数限制。", + ["#crossbow_skill"] = "诸葛连弩", ["qinggang_sword"] = "青釭剑", [":qinggang_sword"] = "装备牌·武器
攻击范围:2
武器技能:锁定技,你的【杀】无视目标角色的防具。", @@ -151,6 +153,7 @@ Fk:loadTranslationTable{ ["halberd"] = "方天画戟", [":halberd"] = "装备牌·武器
攻击范围:4
武器技能:锁定技,你使用最后的手牌【杀】可以额外选择至多两名目标。", + ["#halberd_skill"] = "方天画戟", ["kylin_bow"] = "麒麟弓", [":kylin_bow"] = "装备牌·武器
攻击范围:5
武器技能:当你使用【杀】对目标角色造成伤害时,你可以弃置其装备区里的一张坐骑牌。", @@ -162,6 +165,7 @@ Fk:loadTranslationTable{ ["nioh_shield"] = "仁王盾", [":nioh_shield"] = "装备牌·防具
防具技能:锁定技,黑色【杀】对你无效。", + ["#nioh_shield_skill"] = "仁王盾", ["dilu"] = "的卢", [":dilu"] = "装备牌·坐骑
坐骑技能:其他角色与你的距离+1。", diff --git a/packages/standard_cards/init.lua b/packages/standard_cards/init.lua index 26b674b0..9374e9a6 100644 --- a/packages/standard_cards/init.lua +++ b/packages/standard_cards/init.lua @@ -20,8 +20,8 @@ local slashSkill = fk.CreateActiveSkill{ end, max_phase_use_time = 1, target_num = 1, - can_use = function(self, player, card) - return + can_use = function(self, player, card, extra_data) + return (extra_data and extra_data.bypass_times) or player.phase ~= Player.Play or table.find(Fk:currentRoom().alive_players, function(p) return self:withinTimesLimit(player, Player.HistoryPhase, card, "slash", p) end) @@ -31,11 +31,17 @@ local slashSkill = fk.CreateActiveSkill{ local from = Fk:currentRoom():getPlayerById(user) return from ~= player and not (distance_limited and not self:withinDistanceLimit(from, true, card, player)) end, - target_filter = function(self, to_select, selected, _, card) + target_filter = function(self, to_select, selected, _, card, extra_data) + local count_distances = not (extra_data and extra_data.bypass_distances) if #selected < self:getMaxTargetNum(Self, card) then local player = Fk:currentRoom():getPlayerById(to_select) - return self:modTargetFilter(to_select, selected, Self.id, card, true) and - (#selected > 0 or self:withinTimesLimit(Self, Player.HistoryPhase, card, "slash", player)) + return self:modTargetFilter(to_select, selected, Self.id, card, count_distances) and + ( + #selected > 0 or + Self.phase ~= Player.Play or + (extra_data and extra_data.bypass_times) or + self:withinTimesLimit(Self, Player.HistoryPhase, card, "slash", player) + ) end end, on_effect = function(self, room, effect) @@ -111,6 +117,7 @@ local jink = fk.CreateBasicCard{ suit = Card.Heart, number = 2, skill = jinkSkill, + is_passive = true, } extension:addCards({ @@ -229,9 +236,10 @@ local snatchSkill = fk.CreateActiveSkill{ local from = Fk:currentRoom():getPlayerById(user) return from ~= player and not (player:isAllNude() or (distance_limited and not self:withinDistanceLimit(from, false, card, player))) end, - target_filter = function(self, to_select, selected, _, card) + target_filter = function(self, to_select, selected, _, card, extra_data) + local count_distances = not (extra_data and extra_data.bypass_distances) if #selected < self:getMaxTargetNum(Self, card) then - return self:modTargetFilter(to_select, selected, Self.id, card, true) + return self:modTargetFilter(to_select, selected, Self.id, card, count_distances) end end, target_num = 1, @@ -350,18 +358,22 @@ local collateralSkill = fk.CreateActiveSkill{ prompt = "#collateral_skill", mod_target_filter = function(self, to_select, selected, user, card, distance_limited) local player = Fk:currentRoom():getPlayerById(to_select) - return user ~= to_select and player:getEquipment(Card.SubtypeWeapon) + if #selected == 0 then + return user ~= to_select and player:getEquipment(Card.SubtypeWeapon) and not player:prohibitUse(Fk:cloneCard("slash")) + elseif #selected == 1 then + local target = Fk:currentRoom():getPlayerById(to_select) + local from = Fk:currentRoom():getPlayerById(selected[1]) + return from:inMyAttackRange(target) and not from:isProhibited(player, Fk:cloneCard("slash")) + end end, target_filter = function(self, to_select, selected, _, card) if #selected >= (self:getMaxTargetNum(Self, card) - 1) * 2 then return false--修改借刀的目标选择 elseif #selected % 2 == 0 then - return self:modTargetFilter(to_select, selected, Self.id, card) + return self:modTargetFilter(to_select, {}, Self.id, card) else - local player = Fk:currentRoom():getPlayerById(to_select) - local from = Fk:currentRoom():getPlayerById(selected[#selected]) - return self:modTargetFilter(selected[#selected], selected, Self.id, card) - and from:inMyAttackRange(player) and not from:isProhibited(player, Fk:cloneCard("slash")) + return self:modTargetFilter(selected[#selected], {}, Self.id, card) + and self:modTargetFilter(to_select, {selected[#selected]}, Self.id, card) end end, target_num = 2, @@ -380,17 +392,26 @@ local collateralSkill = fk.CreateActiveSkill{ end, on_effect = function(self, room, effect) local to = room:getPlayerById(effect.to) - if to.dead or not to:getEquipment(Card.SubtypeWeapon) then return end + if to.dead then return end local prompt = "#collateral-slash:"..effect.from..":"..effect.subTargets[1] if #effect.subTargets > 1 then prompt = nil end - local use = room:askForUseCard(to, "slash", nil, prompt, nil, { must_targets = effect.subTargets }, effect) + local extra_data = { + must_targets = effect.subTargets, + bypass_times = true, + } + local use = room:askForUseCard(to, "slash", nil, prompt, nil, extra_data, effect) if use then use.extraUse = true room:useCard(use) else - room:moveCardTo(to:getEquipment(Card.SubtypeWeapon), Card.PlayerHand, room:getPlayerById(effect.from), fk.ReasonGive, "collateral", nil, true, to.id) + local from = room:getPlayerById(effect.from) + if from.dead then return end + local weapons = to:getEquipments(Card.SubtypeWeapon) + if #weapons > 0 then + room:moveCardTo(weapons, Card.PlayerHand, from, fk.ReasonGive, "collateral", nil, true, to.id) + end end end } @@ -453,6 +474,7 @@ local nullification = fk.CreateTrickCard{ suit = Card.Spade, number = 11, skill = nullificationSkill, + is_passive = true, } extension:addCards({ @@ -593,36 +615,9 @@ local amazingGraceSkill = fk.CreateActiveSkill{ can_use = Util.GlobalCanUse, on_use = Util.GlobalOnUse, mod_target_filter = Util.TrueFunc, - on_effect = function(self, room, effect) - local to = room:getPlayerById(effect.to) - if not (effect.extra_data and effect.extra_data.AGFilled) then - return - end - - local chosen = room:askForAG(to, effect.extra_data.AGFilled, false, self.name) - room:takeAG(to, chosen, room.players) - room:obtainCard(effect.to, chosen, true, fk.ReasonPrey) - table.removeOne(effect.extra_data.AGFilled, chosen) - end -} - -local amazingGraceAction = fk.CreateTriggerSkill{ - name = "amazing_grace_action", - global = true, - priority = { [fk.BeforeCardUseEffect] = 0, [fk.CardUseFinished] = 10 }, -- game rule - events = { fk.BeforeCardUseEffect, fk.CardUseFinished }, - can_trigger = function(self, event, target, player, data) - local frameFilled = data.extra_data and data.extra_data.AGFilled - if event == fk.BeforeCardUseEffect then - return data.card.trueName == 'amazing_grace' and not frameFilled - else - return frameFilled - end - end, - on_trigger = function(self, event, target, player, data) - local room = player.room - if event == fk.BeforeCardUseEffect then - local toDisplay = room:getNCards(#TargetGroup:getRealTargets(data.tos)) + on_action = function(self, room, use, finished) + if not finished then + local toDisplay = room:getNCards(#TargetGroup:getRealTargets(use.tos)) room:moveCards({ ids = toDisplay, toArea = Card.Processing, @@ -633,15 +628,15 @@ local amazingGraceAction = fk.CreateTriggerSkill{ room:fillAG(p, toDisplay) end) - data.extra_data = data.extra_data or {} - data.extra_data.AGFilled = toDisplay + use.extra_data = use.extra_data or {} + use.extra_data.AGFilled = toDisplay else - table.forEach(room.players, function(p) - room:closeAG(p) - end) + if use.extra_data and use.extra_data.AGFilled then + table.forEach(room.players, function(p) + room:closeAG(p) + end) - if data.extra_data and data.extra_data.AGFilled then - local toDiscard = table.filter(data.extra_data.AGFilled, function(id) + local toDiscard = table.filter(use.extra_data.AGFilled, function(id) return room:getCardArea(id) == Card.Processing end) @@ -654,11 +649,21 @@ local amazingGraceAction = fk.CreateTriggerSkill{ end end - data.extra_data.AGFilled = nil + use.extra_data.AGFilled = nil end end, + on_effect = function(self, room, effect) + local to = room:getPlayerById(effect.to) + if not (effect.extra_data and effect.extra_data.AGFilled) then + return + end + + local chosen = room:askForAG(to, effect.extra_data.AGFilled, false, self.name) + room:takeAG(to, chosen, room.players) + room:obtainCard(effect.to, chosen, true, fk.ReasonPrey) + table.removeOne(effect.extra_data.AGFilled, chosen) + end } -Fk:addSkill(amazingGraceAction) local amazingGrace = fk.CreateTrickCard{ name = "amazing_grace", @@ -996,7 +1001,7 @@ local spearSkill = fk.CreateViewAsSkill{ pattern = "slash", card_filter = function(self, to_select, selected) if #selected == 2 then return false end - return Fk:currentRoom():getCardArea(to_select) ~= Player.Equip + return table.contains(Self:getHandlyIds(true), to_select) end, view_as = function(self, cards) if #cards ~= 2 then diff --git a/packages/test/init.lua b/packages/test/init.lua index 10c163fc..802b1535 100644 --- a/packages/test/init.lua +++ b/packages/test/init.lua @@ -80,6 +80,7 @@ local control = fk.CreateActiveSkill{ -- ok = {10, 2}, -- }) -- room:swapSeat(from, to) + -- p(room:askForYiji(from, from:getCardIds(Player.Hand), table.map(effect.tos, Util.Id2PlayerMapper), self.name, 2, 10, nil, false, nil, false, 3, true)) for _, pid in ipairs(effect.tos) do local to = room:getPlayerById(pid) -- p(room:askForPoxi(from, "test", { @@ -88,6 +89,10 @@ local control = fk.CreateActiveSkill{ -- }, from.hp, false)) -- room:setPlayerMark(from, "@$a", {1,2,3}) -- room:setPlayerMark(from, "@$b", {'slash','duel','axe'}) + --room:askForMiniGame({from}, "test", "test", { [from.id] = {"Helloworld"} }) + --print(from.client_reply) + -- p(Fk.generals[to.general]:getSkillNameList()) + -- p(Fk.generals[to.general]:getSkillNameList(true)) if to:getMark("mouxushengcontrolled") == 0 then room:addPlayerMark(to, "mouxushengcontrolled") from:control(to) @@ -126,6 +131,13 @@ local control = fk.CreateActiveSkill{ end, } --[[ +Fk:addMiniGame{ + name = "test", + qml_path = "packages/test/qml/TestMini", + update_func = function(player, data) + player:doNotify("UpdateMiniGame", json.encode(data)) + end +} Fk:addPoxiMethod{ name = "test", card_filter = function(to_select, selected, data, extra_data) @@ -197,19 +209,10 @@ local test_vs = fk.CreateViewAsSkill{ } local test_trig = fk.CreateTriggerSkill{ name = "test_trig", - events = {fk.EventPhaseEnd}, - can_trigger = function(self, event, target, player, data) - return target == player and player:hasSkill(self.name) and player.phase == Player.Discard - end, - on_cost = function(self, event, target, player, data) - local cards = player.room:askForDiscard(player, 1, 1, false, self.name, true, nil, "#test_trig-ask", true) - if #cards > 0 then - self.cost_data = cards - return true - end - end, + events = {fk.BeforeHpChanged}, + on_cost = Util.TrueFunc, on_use = function(self, event, target, player, data) - player.room:throwCard(self.cost_data, self.name, player, player) + data.num = data.num - 1 end, } local damage_maker = fk.CreateActiveSkill{ @@ -234,7 +237,7 @@ local damage_maker = fk.CreateActiveSkill{ } end, on_use = function(self, room, effect) local from = room:getPlayerById(effect.from) - local victim = #effect.tos > 0 and room:getPlayerById(effect.tos[1]) + local victim = room:getPlayerById(effect.tos[1]) local target = #effect.tos > 1 and room:getPlayerById(effect.tos[2]) local choice = self.interaction.data local number @@ -243,7 +246,7 @@ local damage_maker = fk.CreateActiveSkill{ for i = 1, 99 do table.insert(choices, tostring(i)) end - number = tonumber(room:askForChoice(from, choices, self.name, nil)) + number = tonumber(room:askForChoice(from, choices, self.name, nil)) ---@type integer end if target then from = target end if choice == "heal_hp" then @@ -305,9 +308,6 @@ local change_hero = fk.CreateActiveSkill{ local choice = self.interaction.data local generals = room:getNGenerals(8) local general = room:askForGeneral(from, generals, 1) - if general == nil then - general = table.random(generals) - end table.removeOne(generals, general) room:changeHero(target, general, false, choice == "deputyGeneral", true) room:returnToGeneralPile(generals) @@ -319,7 +319,7 @@ local test_zhenggong = fk.CreateTriggerSkill{ frequency = Skill.Compulsory, anim_type = "negative", can_trigger = function(self, event, target, player, data) - return player:hasSkill(self.name) and player.room:getTag("RoundCount") == 1 + return player:hasSkill(self) and player.room:getTag("RoundCount") == 1 end, on_use = function(self, event, target, player, data) player:gainAnExtraTurn() @@ -348,8 +348,8 @@ test2.hidden = true test2:addSkill("rende") test2:addSkill(cheat) test2:addSkill(control) ---test2:addSkill(test_vs) ---test2:addSkill(test_trig) +-- test2:addSkill(test_vs) +-- test2:addSkill(test_trig) test2:addSkill(damage_maker) test2:addSkill(test_zhenggong) test2:addSkill(change_hero) @@ -379,7 +379,6 @@ Fk:loadTranslationTable{ ["$cheat"] = "喝啊!", -- ["@@test_cheat-phase"] = "苦肉", -- ["@@test_cheat-inhand"] = "连营", - --["#test_trig-ask"] = "你可弃置一张手牌", ["control"] = "控制", [":control"] = "出牌阶段,你可以控制/解除控制若干名其他角色。", ["$control"] = "战将临阵,斩关刈城!", @@ -403,6 +402,11 @@ Fk:loadTranslationTable{ ["$change_hero"] = "敌军色厉内荏,可筑假城以退敌!", ["~mouxusheng"] = "来世,愿再为我江东之臣……", + + ["heal_hp"] = "回复体力", + ["lose_max_hp"] = "减体力上限", + ["heal_max_hp"] = "加体力上限", + ["revive"] = "复活", } return { extension } diff --git a/packages/test/qml/TestDialog.qml b/packages/test/qml/TestDialog.qml index be4ad3e6..e377da40 100644 --- a/packages/test/qml/TestDialog.qml +++ b/packages/test/qml/TestDialog.qml @@ -16,7 +16,7 @@ ColumnLayout { Layout.fillWidth: true Layout.preferredHeight: childrenRect.height + 4 - text: Backend.translate(extra_data.name) + text: luatr(extra_data.name) } PathView { diff --git a/packages/test/qml/TestMini.qml b/packages/test/qml/TestMini.qml new file mode 100644 index 00000000..84a532ca --- /dev/null +++ b/packages/test/qml/TestMini.qml @@ -0,0 +1,47 @@ +// 割圆的例子 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Fk.RoomElement + +GraphicsBox { + id: root + height: 200; width: 300 +ColumnLayout { + Text { + id: txt + color: "white" + } + + Button { + text: "Btn 1" + onClicked: { + ClientInstance.notifyServer("PushRequest", "updatemini,B1") + } + } + + Button { + text: "Btn 2" + onClicked: { + ClientInstance.notifyServer("PushRequest", "updatemini,B2") + } + } + + Button { + text: "Reply" + onClicked: { + close(); + roomScene.state = "notactive"; + ClientInstance.replyToServer("", JSON.stringify("Hello")); + } + } +} + + function loadData(data) { + txt.text = data[0] + } + + function updateData(data) { + txt.text = JSON.stringify(data) + " updated" + } +} diff --git a/src/core/packman.cpp b/src/core/packman.cpp index a98d7ea8..7f3430f0 100644 --- a/src/core/packman.cpp +++ b/src/core/packman.cpp @@ -169,7 +169,11 @@ void PackMan::updatePack(const QString &pack) { int error; error = status(pack); if (error != 0) { - qCritical("packages/%s: Workspace is dirty, or some error occured.", pack.toLatin1().constData()); +#ifndef FK_SERVER_ONLY + if (Backend != nullptr) { + Backend->showToast(tr("packages/%1: some error occured.").arg(pack)); + } +#endif return; } error = pull(pack); @@ -187,7 +191,11 @@ void PackMan::upgradePack(const QString &pack) { return; error = status(pack); if (error != 0) { - qCritical("Workspace is dirty, or some error occured."); +#ifndef FK_SERVER_ONLY + if (Backend != nullptr) { + Backend->showToast(tr("packages/%1: some error occured.").arg(pack)); + } +#endif return; } error = pull(pack); @@ -219,6 +227,12 @@ QString PackMan::listPackages() { const git_error *e = git_error_last(); \ qCritical("Error %d/%d: %s\n", error, e->klass, e->message) +#define GIT_CHK_CLEAN \ + if (error < 0) { \ + GIT_FAIL; \ + goto clean; \ + } + static int transfer_progress_cb(const git_indexer_progress *stats, void *payload) { (void)payload; @@ -290,37 +304,22 @@ int PackMan::pull(const QString &name) { opt2.checkout_strategy = GIT_CHECKOUT_FORCE; error = git_repository_open(&repo, path); - if (error < 0) { - GIT_FAIL; - goto clean; - } + GIT_CHK_CLEAN; // first git fetch origin error = git_remote_lookup(&remote, repo, "origin"); - if (error < 0) { - GIT_FAIL; - goto clean; - } + GIT_CHK_CLEAN; error = git_remote_fetch(remote, NULL, &opt, NULL); - if (error < 0) { - GIT_FAIL; - goto clean; - } + GIT_CHK_CLEAN; // then git checkout FETCH_HEAD error = git_repository_set_head(repo, "FETCH_HEAD"); - if (error < 0) { - GIT_FAIL; - goto clean; - } + GIT_CHK_CLEAN; error = git_checkout_head(repo, &opt2); - if (error < 0) { - GIT_FAIL; - goto clean; - } else { - if (Backend == nullptr) - printf("\n"); - } + GIT_CHK_CLEAN; + + if (Backend == nullptr) + printf("\n"); clean: git_remote_free(remote); @@ -337,25 +336,13 @@ int PackMan::checkout(const QString &name, const QString &hash) { auto path = QString("packages/%1").arg(name).toUtf8(); auto sha = hash.toLatin1(); error = git_repository_open(&repo, path); - if (error < 0) { - GIT_FAIL; - goto clean; - } + GIT_CHK_CLEAN; error = git_oid_fromstr(&oid, sha); - if (error < 0) { - GIT_FAIL; - goto clean; - } + GIT_CHK_CLEAN; error = git_repository_set_head_detached(repo, &oid); - if (error < 0) { - GIT_FAIL; - goto clean; - } + GIT_CHK_CLEAN; error = git_checkout_head(repo, &opt); - if (error < 0) { - GIT_FAIL; - goto clean; - } + GIT_CHK_CLEAN; clean: git_repository_free(repo); @@ -369,20 +356,11 @@ int PackMan::checkout_branch(const QString &name, const QString &branch) { git_object *obj = NULL; auto path = QString("packages/%1").arg(name).toUtf8(); error = git_repository_open(&repo, path); - if (error < 0) { - GIT_FAIL; - goto clean; - } + GIT_CHK_CLEAN; error = git_revparse_single(&obj, repo, branch.toUtf8()); - if (error < 0) { - GIT_FAIL; - goto clean; - } + GIT_CHK_CLEAN; error = git_checkout_tree(repo, obj, NULL); - if (error < 0) { - GIT_FAIL; - goto clean; - } + GIT_CHK_CLEAN; clean: git_object_free(obj); @@ -393,26 +371,24 @@ clean: int PackMan::status(const QString &name) { git_repository *repo = NULL; int error; - git_status_list *status_list; + git_status_list *status_list = NULL; size_t i, maxi; const git_status_entry *s; auto path = QString("packages/%1").arg(name).toUtf8(); error = git_repository_open(&repo, path); - if (error < 0) { - GIT_FAIL; - goto clean; - } + GIT_CHK_CLEAN; error = git_status_list_new(&status_list, repo, NULL); - if (error < 0) { - GIT_FAIL; - goto clean; - } + GIT_CHK_CLEAN; maxi = git_status_list_entrycount(status_list); for (i = 0; i < maxi; ++i) { char *istatus = NULL; s = git_status_byindex(status_list, i); - if (s->status != GIT_STATUS_CURRENT && s->status != GIT_STATUS_IGNORED) + if (s->status != GIT_STATUS_CURRENT && s->status != GIT_STATUS_IGNORED) { + git_status_list_free(status_list); + git_repository_free(repo); + qCritical("Workspace is dirty."); return 1; + } } clean: @@ -425,28 +401,25 @@ QString PackMan::head(const QString &name) { git_repository *repo = NULL; int error; git_object *obj = NULL; + const git_oid *oid; + char buf[42] = {0}; auto path = QString("packages/%1").arg(name).toUtf8(); error = git_repository_open(&repo, path); - if (error < 0) { - GIT_FAIL; - git_object_free(obj); - git_repository_free(repo); - return QString(); - } + GIT_CHK_CLEAN; error = git_revparse_single(&obj, repo, "HEAD"); - if (error < 0) { - GIT_FAIL; - git_object_free(obj); - git_repository_free(repo); - return QString(); - } + GIT_CHK_CLEAN; - const git_oid *oid = git_object_id(obj); - char buf[42]; + oid = git_object_id(obj); git_oid_tostr(buf, 41, oid); git_object_free(obj); git_repository_free(repo); return QString(buf); + +clean: + git_object_free(obj); + git_repository_free(repo); + return QString(); } #undef GIT_FAIL +#undef GIT_CHK_CLEAN diff --git a/src/server/server.cpp b/src/server/server.cpp index e36880ac..9a62d90e 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -517,7 +517,7 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name, void Server::onRoomAbandoned() { Room *room = qobject_cast(sender()); - room->gameOver(); + // room->gameOver(); // Lua会出手 rooms.remove(room->getId()); updateOnlineInfo(); // 按理说这时候就可以删除了,但是这里肯定比Lua先检测到。 diff --git a/src/swig/qt.i b/src/swig/qt.i index 366c6560..523a10b3 100644 --- a/src/swig/qt.i +++ b/src/swig/qt.i @@ -46,7 +46,7 @@ public: static int GetMicroSecond(lua_State *L) { struct timeval tv; gettimeofday(&tv, nullptr); - long long microsecond = tv.tv_sec * 1000000 + tv.tv_usec; + long long microsecond = (long long)tv.tv_sec * 1000000 + tv.tv_usec; lua_pushnumber(L, microsecond); return 1; } diff --git a/src/ui/qmlbackend.cpp b/src/ui/qmlbackend.cpp index b4d86d66..e9ba2361 100644 --- a/src/ui/qmlbackend.cpp +++ b/src/ui/qmlbackend.cpp @@ -225,6 +225,29 @@ QString QmlBackend::callLuaFunction(const QString &func_name, return QString(result); } +QString QmlBackend::evalLuaExp(const QString &lua) { + if (!ClientInstance) return "{}"; + + lua_State *L = ClientInstance->getLuaState(); + int err; + err = luaL_loadstring(L, lua.toUtf8().constData()); + if (err != LUA_OK) { + qCritical() << lua_tostring(L, -1); + lua_pop(L, 1); + return ""; + } + err = lua_pcall(L, 0, 1, 0); + const char *result = luaL_tolstring(L, -1, NULL); + if (err) { + qCritical() << result; + lua_pop(L, 1); + return ""; + } + lua_pop(L, 1); + + return QString(result); +} + QString QmlBackend::pubEncrypt(const QString &key, const QString &data) { // 在用公钥加密口令时,也随机生成AES密钥/IV,并随着口令一起加密 // AES密钥和IV都是固定16字节的,所以可以放在开头 diff --git a/src/ui/qmlbackend.h b/src/ui/qmlbackend.h index 9c167248..2f582910 100644 --- a/src/ui/qmlbackend.h +++ b/src/ui/qmlbackend.h @@ -40,6 +40,7 @@ public: Q_INVOKABLE QString translate(const QString &src); Q_INVOKABLE QString callLuaFunction(const QString &func_name, QVariantList params); + Q_INVOKABLE QString evalLuaExp(const QString &lua); Q_INVOKABLE QString pubEncrypt(const QString &key, const QString &data); Q_INVOKABLE QString loadConf();