From 67c513ffd382cb093a53967949d99a8c6faf6690 Mon Sep 17 00:00:00 2001
From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Date: Tue, 11 Nov 2025 03:42:35 +0100
Subject: [PATCH 1/2] feat(webui): echoes of umbra
sequel to #2177
---
.../custom/setUmbraEchoesController.ts | 22 +++++++
src/routes/custom.ts | 2 +
static/webui/index.html | 16 +++++
static/webui/script.js | 62 +++++++++++++------
static/webui/translations/de.js | 4 ++
static/webui/translations/en.js | 4 ++
static/webui/translations/es.js | 4 ++
static/webui/translations/fr.js | 4 ++
static/webui/translations/ru.js | 4 ++
static/webui/translations/uk.js | 4 ++
static/webui/translations/zh.js | 4 ++
11 files changed, 112 insertions(+), 18 deletions(-)
create mode 100644 src/controllers/custom/setUmbraEchoesController.ts
diff --git a/src/controllers/custom/setUmbraEchoesController.ts b/src/controllers/custom/setUmbraEchoesController.ts
new file mode 100644
index 00000000..290ea764
--- /dev/null
+++ b/src/controllers/custom/setUmbraEchoesController.ts
@@ -0,0 +1,22 @@
+import { getAccountIdForRequest } from "../../services/loginService.ts";
+import { getInventory } from "../../services/inventoryService.ts";
+import type { RequestHandler } from "express";
+import { broadcastInventoryUpdate } from "../../services/wsService.ts";
+
+export const setUmbraEchoesController: RequestHandler = async (req, res) => {
+ const accountId = await getAccountIdForRequest(req);
+ const request = req.body as ISetUmbraEchoesRequest;
+ const inventory = await getInventory(accountId, "Suits");
+ const suit = inventory.Suits.id(request.oid);
+ if (suit) {
+ suit.UmbraDate = request.UmbraDate ? new Date(request.UmbraDate) : undefined;
+ await inventory.save();
+ broadcastInventoryUpdate(req);
+ }
+ res.end();
+};
+
+interface ISetUmbraEchoesRequest {
+ oid: string;
+ UmbraDate: number;
+}
diff --git a/src/routes/custom.ts b/src/routes/custom.ts
index 1a6fca1f..3978a41f 100644
--- a/src/routes/custom.ts
+++ b/src/routes/custom.ts
@@ -46,6 +46,7 @@ import { updateFingerprintController } from "../controllers/custom/updateFingerp
import { unlockLevelCapController } from "../controllers/custom/unlockLevelCapController.ts";
import { changeModularPartsController } from "../controllers/custom/changeModularPartsController.ts";
import { setInvigorationController } from "../controllers/custom/setInvigorationController.ts";
+import { setUmbraEchoesController } from "../controllers/custom/setUmbraEchoesController.ts";
import { setAccountCheatController } from "../controllers/custom/setAccountCheatController.ts";
import { setGuildCheatController } from "../controllers/custom/setGuildCheatController.ts";
@@ -97,6 +98,7 @@ customRouter.post("/updateFingerprint", updateFingerprintController);
customRouter.post("/unlockLevelCap", unlockLevelCapController);
customRouter.post("/changeModularParts", changeModularPartsController);
customRouter.post("/setInvigoration", setInvigorationController);
+customRouter.post("/setUmbraEchoes", setUmbraEchoesController);
customRouter.post("/setAccountCheat", setAccountCheatController);
customRouter.post("/setGuildCheat", setGuildCheatController);
diff --git a/static/webui/index.html b/static/webui/index.html
index 7c052786..6f356c97 100644
--- a/static/webui/index.html
+++ b/static/webui/index.html
@@ -822,6 +822,22 @@
+
diff --git a/static/webui/script.js b/static/webui/script.js
index ace99d86..7605d7e7 100644
--- a/static/webui/script.js
+++ b/static/webui/script.js
@@ -159,7 +159,7 @@ function doLogout() {
function renameAccount(taken_name) {
const newname = window.prompt(
(taken_name ? loc("code_changeNameRetry").split("|NAME|").join(taken_name) + " " : "") +
- loc("code_changeNameConfirm")
+ loc("code_changeNameConfirm")
);
if (newname) {
revalidateAuthz().then(() => {
@@ -1652,6 +1652,12 @@ function updateInventory() {
formatDatetime("%Y-%m-%d %H:%M", Number(item.UpgradesExpiry?.$date.$numberLong)) || "";
}
+ if (item.ItemType != "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") {
+ document.getElementById("umbraEchoes-card").classList.remove("d-none");
+ document.getElementById("umbraEchoes-expiry").value =
+ formatDatetime("%Y-%m-%d %H:%M", Number(item.UmbraDate?.$date.$numberLong)) || "";
+ }
+
{
document.getElementById("loadout-card").classList.remove("d-none");
const maxModConfigNum = Math.min(2 + (item.ModSlotPurchases ?? 0), 5);
@@ -2151,13 +2157,13 @@ function changeGuildRank(guildId, targetId, rankChange) {
revalidateAuthz().then(() => {
const req = $.get(
"/api/changeGuildRank.php?" +
- window.authz +
- "&guildId=" +
- guildId +
- "&targetId=" +
- targetId +
- "&rankChange=" +
- rankChange
+ window.authz +
+ "&guildId=" +
+ guildId +
+ "&targetId=" +
+ targetId +
+ "&rankChange=" +
+ rankChange
);
req.done(() => {
updateInventory();
@@ -2752,9 +2758,9 @@ function maxRankAllEquipment(categories) {
data[category].forEach(item => {
const maxXP =
category === "Suits" ||
- category === "SpaceSuits" ||
- category === "Sentinels" ||
- category === "Hoverboards"
+ category === "SpaceSuits" ||
+ category === "Sentinels" ||
+ category === "Hoverboards"
? 1_600_000
: 800_000;
@@ -3519,6 +3525,7 @@ single.getRoute("#detailedView-route").on("beforeload", function () {
document.getElementById("loadout-card").classList.add("d-none");
document.getElementById("archonShards-card").classList.add("d-none");
document.getElementById("edit-suit-invigorations-card").classList.add("d-none");
+ document.getElementById("umbraEchoes-card").classList.add("d-none");
document.getElementById("modularParts-card").classList.add("d-none");
document.getElementById("modularParts-form").innerHTML = "";
document.getElementById("valenceBonus-card").classList.add("d-none");
@@ -3571,13 +3578,13 @@ function doPushArchonCrystalUpgrade() {
revalidateAuthz().then(() => {
$.get(
"/custom/pushArchonCrystalUpgrade?" +
- window.authz +
- "&oid=" +
- urlParams.get("itemId") +
- "&type=" +
- uniqueName +
- "&count=" +
- $("#archon-crystal-add-count").val()
+ window.authz +
+ "&oid=" +
+ urlParams.get("itemId") +
+ "&type=" +
+ uniqueName +
+ "&count=" +
+ $("#archon-crystal-add-count").val()
).done(function () {
$("[list='datalist-archonCrystalUpgrades']").val("");
updateInventory();
@@ -4257,6 +4264,25 @@ function setInvigoration(data) {
});
}
+function submitUmbraEchoes(event) {
+ event.preventDefault();
+ const expiry = document.getElementById("umbraEchoes-expiry").value;
+ setUmbraEchoes({
+ UmbraDate: expiry ? new Date(expiry).getTime() : Date.now() + 1 * 24 * 60 * 60 * 1000
+ });
+}
+
+function setUmbraEchoes(data) {
+ const oid = new URLSearchParams(window.location.search).get("itemId");
+ $.post({
+ url: "/custom/setUmbraEchoes?" + window.authz,
+ contentType: "application/json",
+ data: JSON.stringify({ oid, ...data })
+ }).done(function () {
+ updateInventory();
+ });
+}
+
function handleAbilityOverride(event, configIndex) {
event.preventDefault();
const urlParams = new URLSearchParams(window.location.search);
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js
index 2ce7e4c9..37fb291c 100644
--- a/static/webui/translations/de.js
+++ b/static/webui/translations/de.js
@@ -163,6 +163,7 @@ dict = {
detailedView_invigorationLabel: `Kräftigung`,
detailedView_loadoutLabel: `Loadouts`,
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
+ detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
invigorations_offensive_AbilityStrength: `+200% Fähigkeitsstärke`,
invigorations_offensive_AbilityRange: `+100% Fähigkeitsreichweite`,
@@ -193,6 +194,9 @@ dict = {
abilityOverride_label: `Fähigkeitsüberschreibung`,
abilityOverride_onSlot: `auf Slot`,
+ detailedView_umbraEchoesDescription: `Wird ein Warframe mit dieser Flüssigkeit injiziert, kann er selbstständig an der Seite des Operators kämpfen.`,
+ detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
+
mods_addRiven: `Riven hinzufügen`,
mods_fingerprint: `Fingerabdruck`,
mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`,
diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js
index dcb8267a..47ef0d1d 100644
--- a/static/webui/translations/en.js
+++ b/static/webui/translations/en.js
@@ -162,6 +162,7 @@ dict = {
detailedView_invigorationLabel: `Invigoration`,
detailedView_loadoutLabel: `Loadouts`,
detailedView_equipmentFeaturesLabel: `Equipment Features`,
+ detailedView_umbraEchoesLabel: `Echoes Of Umbra`,
invigorations_offensive_AbilityStrength: `+200% Ability Strength`,
invigorations_offensive_AbilityRange: `+100% Ability Range`,
@@ -192,6 +193,9 @@ dict = {
abilityOverride_label: `Ability Override`,
abilityOverride_onSlot: `on slot`,
+ detailedView_umbraEchoesDescription: `Injecting this fluid into a Warframe will imbue it with the ability to fight autonomously alongside the Operator.`,
+ detailedView_umbraEchoesExpiryLabel: `Echo Expiry (optional)`,
+
mods_addRiven: `Add Riven`,
mods_fingerprint: `Fingerprint`,
mods_fingerprintHelp: `Need help with the fingerprint?`,
diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js
index 836b7c4c..e8109a60 100644
--- a/static/webui/translations/es.js
+++ b/static/webui/translations/es.js
@@ -163,6 +163,7 @@ dict = {
detailedView_invigorationLabel: `Fortalecimiento`,
detailedView_loadoutLabel: `Equipamientos`,
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
+ detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
invigorations_offensive_AbilityStrength: `+200% Fuerza de Habilidad`,
invigorations_offensive_AbilityRange: `+100% Alcance de Habilidad`,
@@ -193,6 +194,9 @@ dict = {
abilityOverride_label: `Intercambio de Habilidad`,
abilityOverride_onSlot: `en el espacio`,
+ detailedView_umbraEchoesDescription: `Inyectar este fluido en un warframe lo imbuirá con la capacidad para luchar autónomamente junto a su operador.`,
+ detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
+
mods_addRiven: `Agregar Agrietado`,
mods_fingerprint: `Huella digital`,
mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`,
diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js
index 7d0119eb..606d68f8 100644
--- a/static/webui/translations/fr.js
+++ b/static/webui/translations/fr.js
@@ -163,6 +163,7 @@ dict = {
detailedView_invigorationLabel: `Dynamisation`,
detailedView_loadoutLabel: `Équipements`,
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
+ detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
invigorations_offensive_AbilityStrength: `+200% de puissance de pouvoir`,
invigorations_offensive_AbilityRange: `+100% de portée de pouvoir`,
@@ -193,6 +194,9 @@ dict = {
abilityOverride_label: `Remplacement de pouvoir`,
abilityOverride_onSlot: `Sur l'emplacement`,
+ detailedView_umbraEchoesDescription: `L'injection de ce fluide dans une Warframe lui donnera la possibilité de se battre de manière autonome aux côtés de l'Opérateur.`,
+ detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
+
mods_addRiven: `Ajouter un riven`,
mods_fingerprint: `Empreinte`,
mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`,
diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js
index 567d4736..727d8d3f 100644
--- a/static/webui/translations/ru.js
+++ b/static/webui/translations/ru.js
@@ -163,6 +163,7 @@ dict = {
detailedView_invigorationLabel: `Воодушевление`,
detailedView_loadoutLabel: `Конфигурации`,
detailedView_equipmentFeaturesLabel: `Модификаторы снаряжения`,
+ detailedView_umbraEchoesLabel: `Эхо Умбры`,
invigorations_offensive_AbilityStrength: `+200% к силе способностей.`,
invigorations_offensive_AbilityRange: `+100% к зоне поражения способностей.`,
@@ -193,6 +194,9 @@ dict = {
abilityOverride_label: `Переопределение способности`,
abilityOverride_onSlot: `в ячейке`,
+ detailedView_umbraEchoesDescription: `Введение этой жидкости в варфрейм позволит ему автономно сражаться бок о бок с оператором.`,
+ detailedView_umbraEchoesExpiryLabel: `Срок действия Эха (необязательно)`,
+
mods_addRiven: `Добавить мод Разлома`,
mods_fingerprint: `Отпечаток`,
mods_fingerprintHelp: `Нужна помощь с отпечатком?`,
diff --git a/static/webui/translations/uk.js b/static/webui/translations/uk.js
index 64e99c88..49fa93b1 100644
--- a/static/webui/translations/uk.js
+++ b/static/webui/translations/uk.js
@@ -163,6 +163,7 @@ dict = {
detailedView_invigorationLabel: `Зміцнення`,
detailedView_loadoutLabel: `Конфігурації`,
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
+ detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
invigorations_offensive_AbilityStrength: `+200% до потужності здібностей.`,
invigorations_offensive_AbilityRange: `+100% до досяжності здібностей.`,
@@ -193,6 +194,9 @@ dict = {
abilityOverride_label: `Перевизначення здібностей`,
abilityOverride_onSlot: `у комірці`,
+ detailedView_umbraEchoesDescription: `Рідина, яка після введення дозволяє ворфрейму використовувати єдність і битися самостійно пліч-о-пліч з оператором.`,
+ detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
+
mods_addRiven: `Добавити модифікатор Розколу`,
mods_fingerprint: `Відбиток`,
mods_fingerprintHelp: `Потрібна допомога з відбитком?`,
diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js
index 8d36c529..a5df62fc 100644
--- a/static/webui/translations/zh.js
+++ b/static/webui/translations/zh.js
@@ -163,6 +163,7 @@ dict = {
detailedView_invigorationLabel: `活化`,
detailedView_loadoutLabel: `配置`,
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
+ detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
invigorations_offensive_AbilityStrength: `+200%技能强度`,
invigorations_offensive_AbilityRange: `+100%技能范围`,
@@ -193,6 +194,9 @@ dict = {
abilityOverride_label: `技能替换`,
abilityOverride_onSlot: `槽位`,
+ detailedView_umbraEchoesDescription: `将这种液体注入战甲内,使其具有与指挥官并肩作战的能力。`,
+ detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
+
mods_addRiven: `添加裂罅MOD`,
mods_fingerprint: `印记`,
mods_fingerprintHelp: `需要印记相关的帮助?`,
--
2.49.1
From 1c4d466d420aa90ba05e1f8f1506824e47dad445 Mon Sep 17 00:00:00 2001
From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Date: Tue, 11 Nov 2025 03:43:21 +0100
Subject: [PATCH 2/2] prettier
---
static/webui/script.js | 36 ++++++++++++++++++------------------
1 file changed, 18 insertions(+), 18 deletions(-)
diff --git a/static/webui/script.js b/static/webui/script.js
index 7605d7e7..7eabcebe 100644
--- a/static/webui/script.js
+++ b/static/webui/script.js
@@ -159,7 +159,7 @@ function doLogout() {
function renameAccount(taken_name) {
const newname = window.prompt(
(taken_name ? loc("code_changeNameRetry").split("|NAME|").join(taken_name) + " " : "") +
- loc("code_changeNameConfirm")
+ loc("code_changeNameConfirm")
);
if (newname) {
revalidateAuthz().then(() => {
@@ -2157,13 +2157,13 @@ function changeGuildRank(guildId, targetId, rankChange) {
revalidateAuthz().then(() => {
const req = $.get(
"/api/changeGuildRank.php?" +
- window.authz +
- "&guildId=" +
- guildId +
- "&targetId=" +
- targetId +
- "&rankChange=" +
- rankChange
+ window.authz +
+ "&guildId=" +
+ guildId +
+ "&targetId=" +
+ targetId +
+ "&rankChange=" +
+ rankChange
);
req.done(() => {
updateInventory();
@@ -2758,9 +2758,9 @@ function maxRankAllEquipment(categories) {
data[category].forEach(item => {
const maxXP =
category === "Suits" ||
- category === "SpaceSuits" ||
- category === "Sentinels" ||
- category === "Hoverboards"
+ category === "SpaceSuits" ||
+ category === "Sentinels" ||
+ category === "Hoverboards"
? 1_600_000
: 800_000;
@@ -3578,13 +3578,13 @@ function doPushArchonCrystalUpgrade() {
revalidateAuthz().then(() => {
$.get(
"/custom/pushArchonCrystalUpgrade?" +
- window.authz +
- "&oid=" +
- urlParams.get("itemId") +
- "&type=" +
- uniqueName +
- "&count=" +
- $("#archon-crystal-add-count").val()
+ window.authz +
+ "&oid=" +
+ urlParams.get("itemId") +
+ "&type=" +
+ uniqueName +
+ "&count=" +
+ $("#archon-crystal-add-count").val()
).done(function () {
$("[list='datalist-archonCrystalUpgrades']").val("");
updateInventory();
--
2.49.1