XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

SpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 1 Star 0
返回提交历史

XFEstudio/SpaceNinjaServer

fix(webui): cleanup state tracking (#3072)

In general, all options should always be available to avoid needless complexity and confusion. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3072 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>

4a04e635
Sainan <63328889+Sainan@users.noreply.github.com>
提交于

代码差异

12 个文件 +71 -125
Modified src/controllers/custom/addItemsController.ts +7 -3
@@ -10,9 +10,13 @@ export const addItemsController: RequestHandler = async (req, res) => {
10 10 for (const request of requests) {
11 11 await addItem(inventory, request.ItemType, request.ItemCount, true, undefined, request.Fingerprint, true);
12 12 }
13 await inventory.save();
14 res.end();
15 broadcastInventoryUpdate(req);
13 if (inventory.isModified()) {
14 await inventory.save();
15 res.json(true);
16 broadcastInventoryUpdate(req);
17 } else {
18 res.json(false);
19 }
16 20 };
17 21
18 22 interface IAddItemRequest {
Modified src/controllers/custom/manageQuestsController.ts +8 -4
@@ -51,7 +51,7 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
51 51 break;
52 52 }
53 53 case "giveAll": {
54 allQuestKeys.forEach(questKey => addQuestKey(inventory, { ItemType: questKey }));
54 allQuestKeys.forEach(questKey => addQuestKey(inventory, { ItemType: questKey }, true));
55 55 break;
56 56 }
57 57 case "deleteKey": {
@@ -169,7 +169,11 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
169 169 break;
170 170 }
171 171
172 await inventory.save();
173 res.status(200).end();
174 broadcastInventoryUpdate(req);
172 if (inventory.isModified()) {
173 await inventory.save();
174 res.json(true);
175 broadcastInventoryUpdate(req);
176 } else {
177 res.json(false);
178 }
175 179 };
Modified src/services/questService.ts +5 -2
@@ -124,10 +124,13 @@ export const resetQuestKeyToStage = (
124 124
125 125 export const addQuestKey = (
126 126 inventory: TInventoryDatabaseDocument,
127 questKey: IQuestKeyDatabase
127 questKey: IQuestKeyDatabase,
128 silentFailure?: boolean
128 129 ): IQuestKeyClient | undefined => {
129 130 if (inventory.QuestKeys.some(q => q.ItemType === questKey.ItemType)) {
130 logger.warn(`Quest key ${questKey.ItemType} already exists. It will not be added`);
131 if (!silentFailure) {
132 logger.warn(`Quest key ${questKey.ItemType} already exists. It will not be added`);
133 }
131 134 return;
132 135 }
133 136
Modified static/webui/index.html +3 -2
@@ -71,7 +71,7 @@
71 71 <li class="nav-item">
72 72 <a class="nav-link" href="/webui/import" data-bs-dismiss="offcanvas" data-bs-target="#sidebar" data-loc="navbar_import"></a>
73 73 </li>
74 <li class="nav-item d-none" id="nav-guildView">
74 <li class="nav-item" id="nav-guildView">
75 75 <a class="nav-link" href="/webui/guildView" data-bs-dismiss="offcanvas" data-bs-target="#sidebar" data-loc="navbar_guildView"></a>
76 76 </li>
77 77 </ul>
@@ -573,7 +573,8 @@
573 573 </div>
574 574 </div>
575 575 <div id="guild-route" data-route="/webui/guildView" data-title="Guild | OpenWF WebUI">
576 <h3 id="guildView-loading" class="mb-0" data-loc="general_loading"></h3>
576 <h3 id="guildView-loading" data-loc="general_loading"></h3>
577 <h3 id="guildView-na" class="d-none" data-loc="guildView_na"></h3>
577 578 <h3 id="guildView-title" class="mb-0"></h3>
578 579 <p id="guildView-tier" class="text-body-secondary mb-0"></p>
579 580 <p id="guildView-class" class="text-body-secondary mb-0"></p>
Modified static/webui/script.js +41 -114
@@ -61,6 +61,8 @@ function openWebSocket() {
61 61 if (single.getCurrentPath() == "/webui/") {
62 62 single.loadRoute("/webui/inventory");
63 63 }
64 $("#email").val("");
65 $("#password").val("");
64 66 $(".displayname").text(data.DisplayName);
65 67 window.accountId = data.id;
66 68 window.authz = "accountId=" + data.id + "&nonce=" + data.Nonce + "&wsid=" + wsid;
@@ -268,9 +270,6 @@ function setLanguage(lang) {
268 270 // Not in prelogin state?
269 271 fetchItemList();
270 272 updateInventory();
271 if (single.getCurrentPath().startsWith("/webui/guildView")) {
272 updateInventory();
273 }
274 273 }
275 274 }
276 275
@@ -316,8 +315,6 @@ function fetchItemList() {
316 315 window.itemListPromise = new Promise(resolve => {
317 316 const req = $.get("/custom/getItemLists?lang=" + window.lang);
318 317 req.done(async data => {
319 window.allQuestKeys = data.QuestKeys;
320
321 318 await dictPromise;
322 319
323 320 document.querySelectorAll('[id^="datalist-"]').forEach(datalist => {
@@ -760,12 +757,7 @@ function updateInventory() {
760 757 req.done(data => {
761 758 window.itemListPromise.then(itemMap => {
762 759 window.didInitialInventoryUpdate = true;
763 if (data.GuildId) {
764 window.guildId = data.GuildId.$oid;
765 document.getElementById("nav-guildView").classList.remove("d-none");
766 } else {
767 document.getElementById("nav-guildView").classList.add("d-none");
768 }
760 window.guildId = data?.GuildId?.$oid;
769 761
770 762 const modularWeapons = [
771 763 "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary",
@@ -1003,11 +995,6 @@ function updateInventory() {
1003 995
1004 996 document.getElementById("EvolutionProgress-list").innerHTML = "";
1005 997 data.EvolutionProgress?.forEach(item => {
1006 const datalist = document.getElementById("datalist-EvolutionProgress");
1007 const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`);
1008 if (optionToRemove) {
1009 datalist.removeChild(optionToRemove);
1010 }
1011 998 const tr = document.createElement("tr");
1012 999 tr.setAttribute("data-item-type", item.ItemType);
1013 1000 {
@@ -1134,15 +1121,6 @@ function updateInventory() {
1134 1121
1135 1122 document.getElementById("FlavourItems-list").innerHTML = "";
1136 1123 data.FlavourItems.forEach(item => {
1137 const datalist = document.getElementById("datalist-FlavourItems");
1138 if (!data.FlavourItems.some(x => x.ItemType == item.ItemType)) {
1139 if (!datalist.querySelector(`option[data-key="${item.ItemType}"]`)) {
1140 reAddToItemList(itemMap, "FlavourItems", item.ItemType);
1141 }
1142 }
1143 const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`);
1144 optionToRemove?.remove();
1145
1146 1124 const tr = document.createElement("tr");
1147 1125 tr.setAttribute("data-item-type", item.ItemType);
1148 1126 {
@@ -1159,7 +1137,6 @@ function updateInventory() {
1159 1137 a.href = "#";
1160 1138 a.onclick = function (event) {
1161 1139 event.preventDefault();
1162 reAddToItemList(itemMap, "FlavourItems", item.ItemType);
1163 1140 removeCustomization(item.ItemType);
1164 1141 };
1165 1142 a.title = loc("code_remove");
@@ -1175,11 +1152,6 @@ function updateInventory() {
1175 1152 document.getElementById("WeaponSkins-list").innerHTML = "";
1176 1153 data.WeaponSkins.forEach(item => {
1177 1154 if (item.ItemId.$oid.startsWith("ca70ca70ca70ca70")) return;
1178 const datalist = document.getElementById("datalist-WeaponSkins");
1179 const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`);
1180 if (optionToRemove) {
1181 datalist.removeChild(optionToRemove);
1182 }
1183 1155 const tr = document.createElement("tr");
1184 1156 {
1185 1157 const td = document.createElement("td");
@@ -1196,7 +1168,6 @@ function updateInventory() {
1196 1168 a.onclick = function (event) {
1197 1169 event.preventDefault();
1198 1170 document.getElementById("WeaponSkins-list").removeChild(tr);
1199 reAddToItemList(itemMap, "WeaponSkins", item.ItemType);
1200 1171 disposeOfGear("WeaponSkins", item.ItemId.$oid);
1201 1172 };
1202 1173 a.title = loc("code_remove");
@@ -1210,15 +1181,6 @@ function updateInventory() {
1210 1181 document.getElementById("WeaponSkins-list").appendChild(tr);
1211 1182 });
1212 1183
1213 const datalistEvolutionProgress = document.querySelectorAll("#datalist-EvolutionProgress option");
1214 const formEvolutionProgress = document.querySelector('form[onsubmit*="doAcquireEvolution()"]');
1215
1216 if (datalistEvolutionProgress.length === 0) {
1217 formEvolutionProgress.classList.add("disabled");
1218 formEvolutionProgress.querySelector("input").disabled = true;
1219 formEvolutionProgress.querySelector("button").disabled = true;
1220 }
1221
1222 1184 if (data.CrewShipHarnesses?.length) {
1223 1185 window.plexus = {
1224 1186 id: data.CrewShipHarnesses[0].ItemId.$oid,
@@ -1262,26 +1224,12 @@ function updateInventory() {
1262 1224
1263 1225 // Populate quests route
1264 1226 document.getElementById("QuestKeys-list").innerHTML = "";
1265 window.allQuestKeys.forEach(questKey => {
1266 if (!data.QuestKeys.some(x => x.ItemType == questKey.uniqueName)) {
1267 const datalist = document.getElementById("datalist-QuestKeys");
1268 if (!datalist.querySelector(`option[data-key="${questKey.uniqueName}"]`)) {
1269 reAddToItemList(itemMap, "QuestKeys", questKey.uniqueName);
1270 }
1271 }
1272 });
1273 1227 data.QuestKeys.forEach(item => {
1274 1228 const tr = document.createElement("tr");
1275 1229 tr.setAttribute("data-item-type", item.ItemType);
1276 1230 const run = item.Progress[0]?.c ?? 0;
1277 1231 const stage = run == 0 ? item.Progress.length : item.Progress.map(p => p.c ?? 0).lastIndexOf(run);
1278 1232
1279 const datalist = document.getElementById("datalist-QuestKeys");
1280 const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`);
1281 if (optionToRemove) {
1282 datalist.removeChild(optionToRemove);
1283 }
1284
1285 1233 {
1286 1234 const td = document.createElement("td");
1287 1235 td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType;
@@ -1368,7 +1316,6 @@ function updateInventory() {
1368 1316 a.href = "#";
1369 1317 a.onclick = function (event) {
1370 1318 event.preventDefault();
1371 reAddToItemList(itemMap, "QuestKeys", item.ItemType);
1372 1319 debounce(doQuestUpdate, "deleteKey", item.ItemType);
1373 1320 };
1374 1321 a.title = loc("code_remove");
@@ -1380,22 +1327,6 @@ function updateInventory() {
1380 1327 document.getElementById("QuestKeys-list").appendChild(tr);
1381 1328 });
1382 1329
1383 const datalistQuestKeys = document.querySelectorAll("#datalist-QuestKeys option");
1384 const formQuestKeys = document.querySelector("form[onsubmit*=\"doAcquireEquipment('QuestKeys')\"]");
1385 const giveAllQuestButton = document.querySelector("button[onclick*=\"doBulkQuestUpdate('giveAll')\"]");
1386
1387 if (datalistQuestKeys.length === 0) {
1388 formQuestKeys.classList.add("disabled");
1389 formQuestKeys.querySelector("input").disabled = true;
1390 formQuestKeys.querySelector("button").disabled = true;
1391 giveAllQuestButton.disabled = true;
1392 } else {
1393 formQuestKeys.classList.remove("disabled");
1394 formQuestKeys.querySelector("input").disabled = false;
1395 formQuestKeys.querySelector("button").disabled = false;
1396 giveAllQuestButton.disabled = false;
1397 }
1398
1399 1330 // Populate mods route
1400 1331 document.getElementById("riven-list").innerHTML = "";
1401 1332 document.getElementById("mods-list").innerHTML = "";
@@ -1854,6 +1785,7 @@ function updateInventory() {
1854 1785 guildReq.done(guildData => {
1855 1786 window.itemListPromise.then(itemMap => {
1856 1787 document.getElementById("guildView-loading").classList.add("d-none");
1788 $("#guild-route > .row").removeClass("d-none");
1857 1789
1858 1790 document.getElementById("guildView-title").textContent = guildData.Name;
1859 1791 document.getElementById("guildView-tier").textContent = loc("guildView_tierDisplay")
@@ -1920,11 +1852,6 @@ function updateInventory() {
1920 1852 document.getElementById("TechProjects-list").innerHTML = "";
1921 1853 guildData.TechProjects ??= [];
1922 1854 guildData.TechProjects.forEach(item => {
1923 const datalist = document.getElementById("datalist-TechProjects");
1924 const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`);
1925 if (optionToRemove) {
1926 datalist.removeChild(optionToRemove);
1927 }
1928 1855 const tr = document.createElement("tr");
1929 1856 tr.setAttribute("data-item-type", item.ItemType);
1930 1857 {
@@ -1975,7 +1902,6 @@ function updateInventory() {
1975 1902 a.href = "#";
1976 1903 a.onclick = function (event) {
1977 1904 event.preventDefault();
1978 reAddToItemList(itemMap, "TechProjects", item.ItemType);
1979 1905 debounce(removeGuildTechProject, item.ItemType);
1980 1906 };
1981 1907 a.title = loc("code_remove");
@@ -1992,16 +1918,6 @@ function updateInventory() {
1992 1918 ["VaultDecoRecipes", "VaultMiscItems", "VaultShipDecorations"].forEach(vaultKey => {
1993 1919 document.getElementById(vaultKey + "-list").innerHTML = "";
1994 1920 (guildData[vaultKey] ??= []).forEach(item => {
1995 if (vaultKey == "VaultDecoRecipes") {
1996 const datalist = document.getElementById("datalist-VaultDecoRecipes");
1997 const optionToRemove = datalist.querySelector(
1998 `option[data-key="${item.ItemType}"]`
1999 );
2000 if (optionToRemove) {
2001 datalist.removeChild(optionToRemove);
2002 }
2003 }
2004
2005 1921 const tr = document.createElement("tr");
2006 1922 tr.setAttribute("data-item-type", item.ItemType);
2007 1923 {
@@ -2025,9 +1941,6 @@ function updateInventory() {
2025 1941 a.title = loc("code_remove");
2026 1942 a.onclick = e => {
2027 1943 e.preventDefault();
2028 if (vaultKey == "VaultDecoRecipes") {
2029 reAddToItemList(itemMap, vaultKey, item.ItemType);
2030 }
2031 1944 removeVaultItem(vaultKey, item.ItemType, item.ItemCount * -1);
2032 1945 };
2033 1946 a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>`;
@@ -2169,7 +2082,8 @@ function updateInventory() {
2169 2082 });
2170 2083
2171 2084 guildReq.fail(() => {
2172 single.loadRoute("/webui/inventory");
2085 document.getElementById("guildView-loading").classList.add("d-none");
2086 document.getElementById("guildView-na").classList.remove("d-none");
2173 2087 });
2174 2088 }
2175 2089
@@ -2252,9 +2166,13 @@ function doAcquireEquipment(category) {
2252 2166 }
2253 2167 ])
2254 2168 });
2255 req.done(() => {
2169 req.done(didAnything => {
2256 2170 document.getElementById("acquire-type-" + category).value = "";
2257 updateInventory();
2171 if (didAnything) {
2172 updateInventory();
2173 } else {
2174 toast(loc("code_nothingToDo"));
2175 }
2258 2176 });
2259 2177 });
2260 2178 }
@@ -2419,8 +2337,11 @@ function doAcquireEvolution() {
2419 2337 $("#acquire-type-EvolutionProgress").addClass("is-invalid").focus();
2420 2338 return;
2421 2339 }
2422
2423 setEvolutionProgress([{ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 }]);
2340 if (!document.querySelector("#EvolutionProgress-list [data-item-type='" + uniqueName + "']")) {
2341 setEvolutionProgress([{ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 }]);
2342 } else {
2343 toast(loc("code_nothingToDo"));
2344 }
2424 2345 }
2425 2346
2426 2347 $(document).on("input", "input", function () {
@@ -2747,10 +2668,14 @@ function addMissingEvolutionProgress() {
2747 2668 const requests = [];
2748 2669 document.querySelectorAll("#datalist-EvolutionProgress option").forEach(elm => {
2749 2670 const uniqueName = elm.getAttribute("data-key");
2750 requests.push({ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 });
2671 if (!document.querySelector("#EvolutionProgress-list [data-item-type='" + uniqueName + "']")) {
2672 requests.push({ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 });
2673 }
2751 2674 });
2752 if (requests.length != 0 && window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) {
2753 return setEvolutionProgress(requests);
2675 if (requests.length == 0) {
2676 toast(loc("code_nothingToDo"));
2677 } else if (window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) {
2678 setEvolutionProgress(requests);
2754 2679 }
2755 2680 }
2756 2681
@@ -2770,7 +2695,9 @@ function maxRankAllEvolutions() {
2770 2695 });
2771 2696
2772 2697 if (Object.keys(requests).length > 0) {
2773 return setEvolutionProgress(requests);
2698 return setEvolutionProgress(requests).then(() => {
2699 toast(loc("code_succRankUp"));
2700 });
2774 2701 }
2775 2702
2776 2703 toast(loc("code_noEquipmentToRankUp"));
@@ -3572,6 +3499,7 @@ single.getRoute("#detailedView-route").on("beforeload", function () {
3572 3499
3573 3500 single.getRoute("#guild-route").on("beforeload", function () {
3574 3501 document.getElementById("guildView-loading").classList.remove("d-none");
3502 document.getElementById("guildView-na").classList.add("d-none");
3575 3503 document.getElementById("guildView-title").textContent = "";
3576 3504 document.getElementById("guildView-tier").textContent = "";
3577 3505 document.getElementById("guildView-class").textContent = "";
@@ -3597,6 +3525,7 @@ single.getRoute("#guild-route").on("beforeload", function () {
3597 3525 document.querySelectorAll("#guild-actions button").forEach(btn => {
3598 3526 btn.disabled = true;
3599 3527 });
3528 $("#guild-route > .row").addClass("d-none");
3600 3529 if (window.didInitialInventoryUpdate) {
3601 3530 updateInventory();
3602 3531 }
@@ -3686,23 +3615,17 @@ function doAddCurrency(currency) {
3686 3615 });
3687 3616 }
3688 3617
3689 function reAddToItemList(itemMap, datalist, itemType) {
3690 const item = itemMap[itemType];
3691 if (!item?.alwaysAvailable) {
3692 const option = document.createElement("option");
3693 option.setAttribute("data-key", itemType);
3694 option.value = item?.name ?? itemType;
3695 document.getElementById("datalist-" + datalist).appendChild(option);
3696 }
3697 }
3698
3699 3618 function doQuestUpdate(operation, itemType) {
3700 3619 revalidateAuthz().then(() => {
3701 3620 $.post({
3702 3621 url: "/custom/manageQuests?" + window.authz + "&operation=" + operation + "&itemType=" + itemType,
3703 3622 contentType: "application/json"
3704 }).then(function () {
3705 updateInventory();
3623 }).then(function (didAnything) {
3624 if (didAnything) {
3625 updateInventory();
3626 } else {
3627 toast(loc("code_nothingToDo"));
3628 }
3706 3629 });
3707 3630 });
3708 3631 }
@@ -3712,8 +3635,12 @@ function doBulkQuestUpdate(operation) {
3712 3635 $.post({
3713 3636 url: "/custom/manageQuests?" + window.authz + "&operation=" + operation,
3714 3637 contentType: "application/json"
3715 }).then(function () {
3716 updateInventory();
3638 }).then(function (didAnything) {
3639 if (didAnything) {
3640 updateInventory();
3641 } else {
3642 toast(loc("code_nothingToDo"));
3643 }
3717 3644 });
3718 3645 });
3719 3646 }
Modified static/webui/translations/de.js +1 -0
@@ -439,6 +439,7 @@ dict = {
439 439 theme_dark: `Dunkles Design`,
440 440 theme_light: `Helles Design`,
441 441
442 guildView_na: `[UNTRANSLATED] You're not in a clan.`,
442 443 guildView_cheats: `Clan-Cheats`,
443 444 guildView_techProjects: `Forschung`,
444 445 guildView_vaultDecoRecipes: `Dojo-Deko-Baupläne`,
Modified static/webui/translations/en.js +1 -0
@@ -438,6 +438,7 @@ dict = {
438 438 theme_dark: `Dark Theme`,
439 439 theme_light: `Light Theme`,
440 440
441 guildView_na: `You're not in a clan.`,
441 442 guildView_cheats: `Clan Cheats`,
442 443 guildView_techProjects: `Research`,
443 444 guildView_vaultDecoRecipes: `Dojo Deco Recipes`,
Modified static/webui/translations/es.js +1 -0
@@ -439,6 +439,7 @@ dict = {
439 439 theme_dark: `Tema Oscuro`,
440 440 theme_light: `Tema Claro`,
441 441
442 guildView_na: `[UNTRANSLATED] You're not in a clan.`,
442 443 guildView_cheats: `Trucos de Clan`,
443 444 guildView_techProjects: `Investigación`,
444 445 guildView_vaultDecoRecipes: `Planos de Decoración de Dojo`,
Modified static/webui/translations/fr.js +1 -0
@@ -439,6 +439,7 @@ dict = {
439 439 theme_dark: `Thème sombre`,
440 440 theme_light: `Thème clair`,
441 441
442 guildView_na: `[UNTRANSLATED] You're not in a clan.`,
442 443 guildView_cheats: `Clan`,
443 444 guildView_techProjects: `Recherche`,
444 445 guildView_vaultDecoRecipes: `Schémas de décorations de dojo`,
Modified static/webui/translations/ru.js +1 -0
@@ -439,6 +439,7 @@ dict = {
439 439 theme_dark: `Темная тема`,
440 440 theme_light: `Светлая тема`,
441 441
442 guildView_na: `[UNTRANSLATED] You're not in a clan.`,
442 443 guildView_cheats: `Читы Клана`,
443 444 guildView_techProjects: `Исследовения`,
444 445 guildView_vaultDecoRecipes: `Рецепты декораций Додзё`,
Modified static/webui/translations/uk.js +1 -0
@@ -439,6 +439,7 @@ dict = {
439 439 theme_dark: `Темна тема`,
440 440 theme_light: `Світла тема`,
441 441
442 guildView_na: `[UNTRANSLATED] You're not in a clan.`,
442 443 guildView_cheats: `Кланові чити`,
443 444 guildView_techProjects: `Дослідження`,
444 445 guildView_vaultDecoRecipes: `Рецепти оздоблень Доджьо`,
Modified static/webui/translations/zh.js +1 -0
@@ -439,6 +439,7 @@ dict = {
439 439 theme_dark: `暗色主题`,
440 440 theme_light: `亮色主题`,
441 441
442 guildView_na: `[UNTRANSLATED] You're not in a clan.`,
442 443 guildView_cheats: `[UNTRANSLATED] Clan Cheats`,
443 444 guildView_techProjects: `研究`,
444 445 guildView_vaultDecoRecipes: `[UNTRANSLATED] Dojo Deco Recipes`,