feat(webui): the circuit override (#2335)
All checks were successful
Build Docker image / docker-arm64 (push) Successful in 58s
Build / build (push) Successful in 49s
Build Docker image / docker-amd64 (push) Successful in 1m11s

Re #2312

Reviewed-on: #2335
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
This commit is contained in:
Sainan 2025-06-27 20:28:44 -07:00 committed by Sainan
parent 3d21813a79
commit a9359bd989
10 changed files with 109 additions and 11 deletions

View File

@ -35,4 +35,4 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
- `RadioLegionIntermissionSyndicate` for Intermission I - `RadioLegionIntermissionSyndicate` for Intermission I
- `RadioLegionSyndicate` for The Wolf of Saturn Six - `RadioLegionSyndicate` for The Wolf of Saturn Six
- `allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively. - `allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively.
- `worldState.circuitGameModes` can be provided with an array of valid game modes (`Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, `Alchemy`) - `worldState.circuitGameModes` can be set to an array of game modes which will override the otherwise-random pattern in The Circuit. Valid element values are `Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, and `Alchemy`.

View File

@ -55,6 +55,7 @@ interface ItemLists {
EvolutionProgress: ListedItem[]; EvolutionProgress: ListedItem[];
mods: ListedItem[]; mods: ListedItem[];
Boosters: ListedItem[]; Boosters: ListedItem[];
//circuitGameModes: ListedItem[];
} }
const relicQualitySuffixes: Record<TRelicQuality, string> = { const relicQualitySuffixes: Record<TRelicQuality, string> = {
@ -64,6 +65,10 @@ const relicQualitySuffixes: Record<TRelicQuality, string> = {
VPQ_PLATINUM: " [Exceptional]" VPQ_PLATINUM: " [Exceptional]"
}; };
/*const toTitleCase = (str: string): string => {
return str.replace(/[^\s-]+/g, word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase());
};*/
const getItemListsController: RequestHandler = (req, response) => { const getItemListsController: RequestHandler = (req, response) => {
const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en"); const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en");
const res: ItemLists = { const res: ItemLists = {
@ -87,6 +92,36 @@ const getItemListsController: RequestHandler = (req, response) => {
EvolutionProgress: [], EvolutionProgress: [],
mods: [], mods: [],
Boosters: [] Boosters: []
/*circuitGameModes: [
{
uniqueName: "Survival",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Survival", lang))
},
{
uniqueName: "VoidFlood",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Corruption", lang))
},
{
uniqueName: "Excavation",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Excavation", lang))
},
{
uniqueName: "Defense",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Defense", lang))
},
{
uniqueName: "Exterminate",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Exterminate", lang))
},
{
uniqueName: "Assassination",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Assassination", lang))
},
{
uniqueName: "Alchemy",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Alchemy", lang))
}
]*/
}; };
for (const [uniqueName, item] of Object.entries(ExportWarframes)) { for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
res[item.productCategory].push({ res[item.productCategory].push({

View File

@ -768,14 +768,14 @@
<input class="form-check-input" type="checkbox" id="unlockAllSimarisResearchEntries" /> <input class="form-check-input" type="checkbox" id="unlockAllSimarisResearchEntries" />
<label class="form-check-label" for="unlockAllSimarisResearchEntries" data-loc="cheats_unlockAllSimarisResearchEntries"></label> <label class="form-check-label" for="unlockAllSimarisResearchEntries" data-loc="cheats_unlockAllSimarisResearchEntries"></label>
</div> </div>
<form class="form-group mt-2" onsubmit="doSaveConfig('spoofMasteryRank'); return false;"> <form class="form-group mt-2" onsubmit="doSaveConfigInt('spoofMasteryRank'); return false;">
<label class="form-label" for="spoofMasteryRank" data-loc="cheats_spoofMasteryRank"></label> <label class="form-label" for="spoofMasteryRank" data-loc="cheats_spoofMasteryRank"></label>
<div class="input-group"> <div class="input-group">
<input class="form-control" id="spoofMasteryRank" type="number" min="-1" max="65535" /> <input class="form-control" id="spoofMasteryRank" type="number" min="-1" max="65535" />
<button class="btn btn-primary" type="submit" data-loc="cheats_save"></button> <button class="btn btn-primary" type="submit" data-loc="cheats_save"></button>
</div> </div>
</form> </form>
<form class="form-group mt-2" onsubmit="doSaveConfig('nightwaveStandingMultiplier'); return false;"> <form class="form-group mt-2" onsubmit="doSaveConfigInt('nightwaveStandingMultiplier'); return false;">
<label class="form-label" for="nightwaveStandingMultiplier" data-loc="cheats_nightwaveStandingMultiplier"></label> <label class="form-label" for="nightwaveStandingMultiplier" data-loc="cheats_nightwaveStandingMultiplier"></label>
<div class="input-group"> <div class="input-group">
<input class="form-control" id="nightwaveStandingMultiplier" type="number" min="1" max="1000000" value="1" /> <input class="form-control" id="nightwaveStandingMultiplier" type="number" min="1" max="1000000" value="1" />
@ -892,6 +892,13 @@
<option value="hard" data-loc="worldState_allAtOnceSteelPath"></option> <option value="hard" data-loc="worldState_allAtOnceSteelPath"></option>
</select> </select>
</div> </div>
<form class="form-group mt-2" onsubmit="doSaveConfigStringArray('worldState.circuitGameModes'); return false;">
<label class="form-label" for="worldState.circuitGameModes" data-loc="worldState_theCircuitOverride"></label>
<div class="input-group">
<input id="worldState.circuitGameModes" type="text" class="form-control tags-input" list="datalist-circuitGameModes" />
<button class="btn btn-primary" type="submit" data-loc="cheats_save"></button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
@ -923,10 +930,7 @@
<datalist id="datalist-KubrowPets"></datalist> <datalist id="datalist-KubrowPets"></datalist>
<datalist id="datalist-QuestKeys"></datalist> <datalist id="datalist-QuestKeys"></datalist>
<datalist id="datalist-miscitems"></datalist> <datalist id="datalist-miscitems"></datalist>
<datalist id="datalist-mods"> <datalist id="datalist-mods"></datalist>
<option data-key="/Lotus/Upgrades/Mods/Fusers/LegendaryModFuser" value="Legendary Core"></option>
<option data-key="/Lotus/Upgrades/CosmeticEnhancers/Peculiars/CyoteMod" value="Traumatic Peculiar"></option>
</datalist>
<datalist id="datalist-archonCrystalUpgrades"></datalist> <datalist id="datalist-archonCrystalUpgrades"></datalist>
<datalist id="datalist-OperatorAmps"></datalist> <datalist id="datalist-OperatorAmps"></datalist>
<datalist id="datalist-EvolutionProgress"></datalist> <datalist id="datalist-EvolutionProgress"></datalist>
@ -958,6 +962,15 @@
<datalist id="datalist-ModularParts-KUBROW_ANTIGEN"></datalist> <datalist id="datalist-ModularParts-KUBROW_ANTIGEN"></datalist>
<datalist id="datalist-ModularParts-KUBROW_MUTAGEN"></datalist> <datalist id="datalist-ModularParts-KUBROW_MUTAGEN"></datalist>
<datalist id="datalist-Boosters"></datalist> <datalist id="datalist-Boosters"></datalist>
<datalist id="datalist-circuitGameModes">
<option>Survival</option>
<option>VoidFlood</option>
<option>Excavation</option>
<option>Defense</option>
<option>Exterminate</option>
<option>Assassination</option>
<option>Alchemy</option>
</datalist>
<script src="/webui/libs/jquery-3.6.0.min.js"></script> <script src="/webui/libs/jquery-3.6.0.min.js"></script>
<script src="/webui/libs/whirlpool-js.min.js"></script> <script src="/webui/libs/whirlpool-js.min.js"></script>
<script src="/webui/libs/single.js"></script> <script src="/webui/libs/single.js"></script>

View File

@ -1882,12 +1882,27 @@ for (const id of uiConfigs) {
} }
} }
function doSaveConfig(id) { function doSaveConfigInt(id) {
const elm = document.getElementById(id);
$.post({ $.post({
url: "/custom/setConfig?" + window.authz + "&wsid=" + wsid, url: "/custom/setConfig?" + window.authz + "&wsid=" + wsid,
contentType: "application/json", contentType: "application/json",
data: JSON.stringify({ [id]: parseInt(elm.value) }) data: JSON.stringify({
[id]: parseInt(document.getElementById(id).value)
})
});
}
function doSaveConfigStringArray(id) {
$.post({
url: "/custom/setConfig?" + window.authz + "&wsid=" + wsid,
contentType: "application/json",
data: JSON.stringify({
[id]: document
.getElementById(id)
.getAttribute("data-tags-value")
.split(", ")
.filter(x => x)
})
}); });
} }
@ -1914,7 +1929,12 @@ single.getRoute("/webui/cheats").on("beforeload", function () {
if (x.type == "checkbox") { if (x.type == "checkbox") {
x.checked = value; x.checked = value;
} else if (x.type == "number") { } else if (x.type == "number") {
x.setAttribute("value", `${value}`); x.setAttribute("value", value);
} else if (x.classList.contains("tags-input")) {
x.value = value.join(", ");
x.oninput();
} else {
x.value = value;
} }
} }
}); });
@ -2597,3 +2617,27 @@ const importSamples = {
function setImportSample(key) { function setImportSample(key) {
$("#import-inventory").val(JSON.stringify(importSamples[key], null, 2)); $("#import-inventory").val(JSON.stringify(importSamples[key], null, 2));
} }
document.querySelectorAll(".tags-input").forEach(input => {
const datalist = document.getElementById(input.getAttribute("list"));
const options = [...datalist.querySelectorAll("option")].map(x => x.textContent);
input.oninput = function () {
const value = [];
for (const tag of this.value.split(",")) {
const index = options.map(x => x.toLowerCase()).indexOf(tag.trim().toLowerCase());
if (index != -1) {
value.push(options[index]);
}
}
this.setAttribute("data-tags-value", value.join(", "));
datalist.innerHTML = "";
for (const option of options) {
const elm = document.createElement("option");
elm.textContent = [...value, option, ""].join(", ");
datalist.appendChild(elm);
}
};
input.oninput();
});

View File

@ -230,6 +230,7 @@ dict = {
normal: `[UNTRANSLATED] Normal`, normal: `[UNTRANSLATED] Normal`,
worldState_allAtOnceNormal: `[UNTRANSLATED] All At Once, Normal`, worldState_allAtOnceNormal: `[UNTRANSLATED] All At Once, Normal`,
worldState_allAtOnceSteelPath: `[UNTRANSLATED] All At Once, Steel Path`, worldState_allAtOnceSteelPath: `[UNTRANSLATED] All At Once, Steel Path`,
worldState_theCircuitOverride: `[UNTRANSLATED] The Circuit Override`,
import_importNote: `Du kannst hier eine vollständige oder teilweise Inventarantwort (Client-Darstellung) einfügen. Alle Felder, die vom Importer unterstützt werden, <b>werden in deinem Account überschrieben</b>.`, import_importNote: `Du kannst hier eine vollständige oder teilweise Inventarantwort (Client-Darstellung) einfügen. Alle Felder, die vom Importer unterstützt werden, <b>werden in deinem Account überschrieben</b>.`,
import_submit: `Absenden`, import_submit: `Absenden`,

View File

@ -229,6 +229,7 @@ dict = {
normal: `Normal`, normal: `Normal`,
worldState_allAtOnceNormal: `All At Once, Normal`, worldState_allAtOnceNormal: `All At Once, Normal`,
worldState_allAtOnceSteelPath: `All At Once, Steel Path`, worldState_allAtOnceSteelPath: `All At Once, Steel Path`,
worldState_theCircuitOverride: `The Circuit Override`,
import_importNote: `You can provide a full or partial inventory response (client respresentation) here. All fields that are supported by the importer <b>will be overwritten</b> in your account.`, import_importNote: `You can provide a full or partial inventory response (client respresentation) here. All fields that are supported by the importer <b>will be overwritten</b> in your account.`,
import_submit: `Submit`, import_submit: `Submit`,

View File

@ -230,6 +230,7 @@ dict = {
normal: `[UNTRANSLATED] Normal`, normal: `[UNTRANSLATED] Normal`,
worldState_allAtOnceNormal: `[UNTRANSLATED] All At Once, Normal`, worldState_allAtOnceNormal: `[UNTRANSLATED] All At Once, Normal`,
worldState_allAtOnceSteelPath: `[UNTRANSLATED] All At Once, Steel Path`, worldState_allAtOnceSteelPath: `[UNTRANSLATED] All At Once, Steel Path`,
worldState_theCircuitOverride: `[UNTRANSLATED] The Circuit Override`,
import_importNote: `Puedes proporcionar una respuesta de inventario completa o parcial (representación del cliente) aquí. Todos los campos compatibles con el importador <b>serán sobrescritos</b> en tu cuenta.`, import_importNote: `Puedes proporcionar una respuesta de inventario completa o parcial (representación del cliente) aquí. Todos los campos compatibles con el importador <b>serán sobrescritos</b> en tu cuenta.`,
import_submit: `Enviar`, import_submit: `Enviar`,

View File

@ -230,6 +230,7 @@ dict = {
normal: `[UNTRANSLATED] Normal`, normal: `[UNTRANSLATED] Normal`,
worldState_allAtOnceNormal: `[UNTRANSLATED] All At Once, Normal`, worldState_allAtOnceNormal: `[UNTRANSLATED] All At Once, Normal`,
worldState_allAtOnceSteelPath: `[UNTRANSLATED] All At Once, Steel Path`, worldState_allAtOnceSteelPath: `[UNTRANSLATED] All At Once, Steel Path`,
worldState_theCircuitOverride: `[UNTRANSLATED] The Circuit Override`,
import_importNote: `Import manuel. Toutes les modifcations supportées par l'inventaire <b>écraseront celles présentes dans la base de données</b>.`, import_importNote: `Import manuel. Toutes les modifcations supportées par l'inventaire <b>écraseront celles présentes dans la base de données</b>.`,
import_submit: `Soumettre`, import_submit: `Soumettre`,

View File

@ -230,6 +230,7 @@ dict = {
normal: `[UNTRANSLATED] Normal`, normal: `[UNTRANSLATED] Normal`,
worldState_allAtOnceNormal: `[UNTRANSLATED] All At Once, Normal`, worldState_allAtOnceNormal: `[UNTRANSLATED] All At Once, Normal`,
worldState_allAtOnceSteelPath: `[UNTRANSLATED] All At Once, Steel Path`, worldState_allAtOnceSteelPath: `[UNTRANSLATED] All At Once, Steel Path`,
worldState_theCircuitOverride: `[UNTRANSLATED] The Circuit Override`,
import_importNote: `Вы можете загрузить полный или частичный ответ инвентаря (клиентское представление) здесь. Все поддерживаемые поля <b>будут перезаписаны</b> в вашем аккаунте.`, import_importNote: `Вы можете загрузить полный или частичный ответ инвентаря (клиентское представление) здесь. Все поддерживаемые поля <b>будут перезаписаны</b> в вашем аккаунте.`,
import_submit: `Отправить`, import_submit: `Отправить`,

View File

@ -230,6 +230,7 @@ dict = {
normal: `正常`, normal: `正常`,
worldState_allAtOnceNormal: `全部开启(普通)`, worldState_allAtOnceNormal: `全部开启(普通)`,
worldState_allAtOnceSteelPath: `全部开启(钢铁之路)`, worldState_allAtOnceSteelPath: `全部开启(钢铁之路)`,
worldState_theCircuitOverride: `[UNTRANSLATED] The Circuit Override`,
import_importNote: `您可以在此处提供完整或部分库存响应(客户端表示)。支持的所有字段<b>将被覆盖</b>到您的账户中。`, import_importNote: `您可以在此处提供完整或部分库存响应(客户端表示)。支持的所有字段<b>将被覆盖</b>到您的账户中。`,
import_submit: `提交`, import_submit: `提交`,