Compare commits

..

No commits in common. "main" and "webui-refresh-fixes" have entirely different histories.

14 changed files with 183 additions and 294 deletions

View File

@ -7,6 +7,5 @@ WORKDIR /app
RUN npm i --omit=dev RUN npm i --omit=dev
RUN npm run build RUN npm run build
RUN date '+%d %B %Y' > BUILD_DATE
ENTRYPOINT ["/app/docker-entrypoint.sh"] ENTRYPOINT ["/app/docker-entrypoint.sh"]

View File

@ -3,13 +3,11 @@ import {
getGuildForRequestEx, getGuildForRequestEx,
hasAccessToDojo, hasAccessToDojo,
hasGuildPermission, hasGuildPermission,
refundDojoDeco,
removeDojoDeco removeDojoDeco
} from "@/src/services/guildService"; } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes"; import { GuildPermission } from "@/src/types/guildTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const destroyDojoDecoController: RequestHandler = async (req, res) => { export const destroyDojoDecoController: RequestHandler = async (req, res) => {
@ -20,20 +18,9 @@ export const destroyDojoDecoController: RequestHandler = async (req, res) => {
res.json({ DojoRequestStatus: -1 }); res.json({ DojoRequestStatus: -1 });
return; return;
} }
const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest | IClearObstacleCourseRequest; const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest;
if ("DecoType" in request) {
removeDojoDeco(guild, request.ComponentId, request.DecoId); removeDojoDeco(guild, request.ComponentId, request.DecoId);
} else if (request.Act == "cObst") {
const component = guild.DojoComponents.id(request.ComponentId)!;
if (component.Decos) {
for (const deco of component.Decos) {
refundDojoDeco(guild, component, deco);
}
component.Decos.splice(0, component.Decos.length);
}
} else {
logger.error(`unhandled destroyDojoDeco request`, request);
}
await guild.save(); await guild.save();
res.json(await getDojoClient(guild, 0, request.ComponentId)); res.json(await getDojoClient(guild, 0, request.ComponentId));
@ -44,8 +31,3 @@ interface IDestroyDojoDecoRequest {
ComponentId: string; ComponentId: string;
DecoId: string; DecoId: string;
} }
interface IClearObstacleCourseRequest {
ComponentId: string;
Act: "cObst" | "maybesomethingelsewedontknowabout";
}

View File

@ -1,17 +1,20 @@
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IShipDecorationsRequest, IResetShipDecorationsRequest } from "@/src/types/personalRoomsTypes"; import { IShipDecorationsRequest } from "@/src/types/personalRoomsTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { handleResetShipDecorations, handleSetShipDecorations } from "@/src/services/shipCustomizationsService"; import { handleSetShipDecorations } from "@/src/services/shipCustomizationsService";
export const shipDecorationsController: RequestHandler = async (req, res) => { export const shipDecorationsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
if (req.query.reset == "1") {
const request = JSON.parse(req.body as string) as IResetShipDecorationsRequest;
const response = await handleResetShipDecorations(accountId, request);
res.send(response);
} else {
const shipDecorationsRequest = JSON.parse(req.body as string) as IShipDecorationsRequest; const shipDecorationsRequest = JSON.parse(req.body as string) as IShipDecorationsRequest;
try {
const placedDecoration = await handleSetShipDecorations(accountId, shipDecorationsRequest); const placedDecoration = await handleSetShipDecorations(accountId, shipDecorationsRequest);
res.send(placedDecoration); res.send(placedDecoration);
} catch (error: unknown) {
if (error instanceof Error) {
logger.error(`error in shipDecorationsController: ${error.message}`);
res.status(400).json({ error: error.message });
}
} }
}; };

View File

@ -11,7 +11,6 @@ import { GuildMember } from "@/src/models/guildModel";
import { Leaderboard } from "@/src/models/leaderboardModel"; import { Leaderboard } from "@/src/models/leaderboardModel";
import { deleteGuild } from "@/src/services/guildService"; import { deleteGuild } from "@/src/services/guildService";
import { Friendship } from "@/src/models/friendModel"; import { Friendship } from "@/src/models/friendModel";
import { sendWsBroadcastTo } from "@/src/services/wsService";
export const deleteAccountController: RequestHandler = async (req, res) => { export const deleteAccountController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -37,8 +36,5 @@ export const deleteAccountController: RequestHandler = async (req, res) => {
Ship.deleteMany({ ShipOwnerId: accountId }), Ship.deleteMany({ ShipOwnerId: accountId }),
Stats.deleteOne({ accountOwnerId: accountId }) Stats.deleteOne({ accountOwnerId: accountId })
]); ]);
sendWsBroadcastTo(accountId, { logged_out: true });
res.end(); res.end();
}; };

View File

@ -18,23 +18,17 @@ logger.info("Starting up...");
// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP. // Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP.
import mongoose from "mongoose"; import mongoose from "mongoose";
import path from "path";
import { JSONStringify } from "json-with-bigint"; import { JSONStringify } from "json-with-bigint";
import { startWebServer } from "@/src/services/webService"; import { startWebServer } from "@/src/services/webService";
import { validateConfig } from "@/src/services/configWatcherService"; import { validateConfig } from "@/src/services/configWatcherService";
import { updateWorldStateCollections } from "@/src/services/worldStateService"; import { updateWorldStateCollections } from "@/src/services/worldStateService";
import { repoDir } from "@/src/helpers/pathHelper";
JSON.stringify = JSONStringify; // Patch JSON.stringify to work flawlessly with Bigints. // Patch JSON.stringify to work flawlessly with Bigints.
JSON.stringify = JSONStringify;
validateConfig(); validateConfig();
fs.readFile(path.join(repoDir, "BUILD_DATE"), "utf-8", (err, data) => {
if (!err) {
logger.info(`Docker image was built on ${data.trim()}`);
}
});
mongoose mongoose
.connect(config.mongodbUrl) .connect(config.mongodbUrl)
.then(() => { .then(() => {

View File

@ -1,11 +1,16 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { logError } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
export const errorHandler = (err: Error, req: Request, res: Response, _next: NextFunction): void => { export const errorHandler = (err: Error, req: Request, res: Response, _next: NextFunction): void => {
if (err.message == "Invalid accountId-nonce pair") { if (err.message == "Invalid accountId-nonce pair") {
res.status(400).send("Log-in expired"); res.status(400).send("Log-in expired");
} else if (err.stack) {
const stackArr = err.stack.split("\n");
stackArr[0] += ` while processing ${req.path} request`;
logger.error(stackArr.join("\n"));
res.status(500).end();
} else { } else {
logError(err, `processing ${req.path} request`); logger.error(`uncaught error while processing ${req.path} request: ${err.message}`);
res.status(500).end(); res.status(500).end();
} }
}; };

View File

@ -13,7 +13,6 @@ import {
IDojoComponentDatabase, IDojoComponentDatabase,
IDojoContributable, IDojoContributable,
IDojoDecoClient, IDojoDecoClient,
IDojoDecoDatabase,
IGuildClient, IGuildClient,
IGuildMemberClient, IGuildMemberClient,
IGuildMemberDatabase, IGuildMemberDatabase,
@ -310,7 +309,7 @@ export const removeDojoRoom = async (
guild.DojoEnergy -= meta.energy; guild.DojoEnergy -= meta.energy;
} }
moveResourcesToVault(guild, component); moveResourcesToVault(guild, component);
component.Decos?.forEach(deco => refundDojoDeco(guild, component, deco)); component.Decos?.forEach(deco => moveResourcesToVault(guild, deco));
if (guild.RoomChanges) { if (guild.RoomChanges) {
const index = guild.RoomChanges.findIndex(x => x.componentId.equals(component._id)); const index = guild.RoomChanges.findIndex(x => x.componentId.equals(component._id));
@ -345,14 +344,6 @@ export const removeDojoDeco = (
component.Decos!.findIndex(x => x._id.equals(decoId)), component.Decos!.findIndex(x => x._id.equals(decoId)),
1 1
)[0]; )[0];
refundDojoDeco(guild, component, deco);
};
export const refundDojoDeco = (
guild: TGuildDatabaseDocument,
component: IDojoComponentDatabase,
deco: IDojoDecoDatabase
): void => {
const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type); const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type);
if (meta) { if (meta) {
if (meta.capacityCost) { if (meta.capacityCost) {
@ -378,7 +369,7 @@ export const refundDojoDeco = (
]); ]);
} }
} }
moveResourcesToVault(guild, deco); // Refund resources spent on construction moveResourcesToVault(guild, deco);
}; };
const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoContributable): void => { const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoContributable): void => {

View File

@ -1283,7 +1283,9 @@ export const addMissionRewards = async (
} }
} }
} }
let medallionAmount = Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1)); let medallionAmount = Math.floor(
Math.min(rewardInfo.JobStage, currentJob.xpAmounts.length - 1) / (rewardInfo.Q ? 0.8 : 1)
);
if ( if (
["DeimosEndlessAreaDefenseBounty", "DeimosEndlessExcavateBounty", "DeimosEndlessPurifyBounty"].some( ["DeimosEndlessAreaDefenseBounty", "DeimosEndlessExcavateBounty", "DeimosEndlessPurifyBounty"].some(
ending => jobType.endsWith(ending) ending => jobType.endsWith(ending)

View File

@ -1,8 +1,6 @@
import { getPersonalRooms } from "@/src/services/personalRoomsService"; import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { getShip } from "@/src/services/shipService"; import { getShip } from "@/src/services/shipService";
import { import {
IResetShipDecorationsRequest,
IResetShipDecorationsResponse,
ISetPlacedDecoInfoRequest, ISetPlacedDecoInfoRequest,
ISetShipCustomizationsRequest, ISetShipCustomizationsRequest,
IShipDecorationsRequest, IShipDecorationsRequest,
@ -156,6 +154,7 @@ export const handleSetShipDecorations = async (
if (!config.unlockAllShipDecorations) { if (!config.unlockAllShipDecorations) {
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == placedDecoration.Type)![0];
if (placedDecoration.Sockets !== undefined) { if (placedDecoration.Sockets !== undefined) {
addFusionTreasures(inventory, [{ ItemType: itemType, Sockets: placedDecoration.Sockets, ItemCount: -1 }]); addFusionTreasures(inventory, [{ ItemType: itemType, Sockets: placedDecoration.Sockets, ItemCount: -1 }]);
} else { } else {
@ -199,51 +198,6 @@ const getRoomsForBootLocation = (
return personalRooms.Ship.Rooms; return personalRooms.Ship.Rooms;
}; };
export const handleResetShipDecorations = async (
accountId: string,
request: IResetShipDecorationsRequest
): Promise<IResetShipDecorationsResponse> => {
const [personalRooms, inventory] = await Promise.all([getPersonalRooms(accountId), getInventory(accountId)]);
const room = getRoomsForBootLocation(personalRooms, request).find(room => room.Name === request.Room);
if (!room) {
throw new Error(`unknown room: ${request.Room}`);
}
for (const deco of room.PlacedDecos) {
const entry = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type);
if (!entry) {
throw new Error(`unknown deco type: ${deco.Type}`);
}
const [itemType, meta] = entry;
if (meta.capacityCost === undefined) {
throw new Error(`unknown deco type: ${deco.Type}`);
}
// refund item
if (!config.unlockAllShipDecorations) {
if (deco.Sockets !== undefined) {
addFusionTreasures(inventory, [{ ItemType: itemType, Sockets: deco.Sockets, ItemCount: 1 }]);
} else {
addShipDecorations(inventory, [{ ItemType: itemType, ItemCount: 1 }]);
}
}
// refund capacity
room.MaxCapacity += meta.capacityCost;
}
// empty room
room.PlacedDecos.splice(0, room.PlacedDecos.length);
await Promise.all([personalRooms.save(), inventory.save()]);
return {
ResetRoom: request.Room,
ClaimedDecos: [], // Not sure what this is for; the client already implies that the decos were returned to inventory.
NewCapacity: room.MaxCapacity
};
};
export const handleSetPlacedDecoInfo = async (accountId: string, req: ISetPlacedDecoInfoRequest): Promise<void> => { export const handleSetPlacedDecoInfo = async (accountId: string, req: ISetPlacedDecoInfoRequest): Promise<void> => {
if (req.GuildId && req.ComponentId) { if (req.GuildId && req.ComponentId) {
const guild = (await Guild.findById(req.GuildId))!; const guild = (await Guild.findById(req.GuildId))!;

View File

@ -5,7 +5,6 @@ import { Account } from "@/src/models/loginModel";
import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "@/src/services/loginService"; import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "@/src/services/loginService";
import { IDatabaseAccountJson } from "@/src/types/loginTypes"; import { IDatabaseAccountJson } from "@/src/types/loginTypes";
import { HydratedDocument } from "mongoose"; import { HydratedDocument } from "mongoose";
import { logError } from "@/src/utils/logger";
let wsServer: ws.Server | undefined; let wsServer: ws.Server | undefined;
let wssServer: ws.Server | undefined; let wssServer: ws.Server | undefined;
@ -89,7 +88,6 @@ const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
ws.on("message", async msg => { ws.on("message", async msg => {
try {
const data = JSON.parse(String(msg)) as IWsMsgFromClient; const data = JSON.parse(String(msg)) as IWsMsgFromClient;
if (data.auth) { if (data.auth) {
let account: IDatabaseAccountJson | null = await Account.findOne({ email: data.auth.email }); let account: IDatabaseAccountJson | null = await Account.findOne({ email: data.auth.email });
@ -148,9 +146,6 @@ const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
} }
); );
} }
} catch (e) {
logError(e as Error, `processing websocket message`);
}
}); });
}; };

View File

@ -150,17 +150,6 @@ export interface IShipDecorationsResponse {
NewRoom?: string; NewRoom?: string;
} }
export interface IResetShipDecorationsRequest {
Room: string;
BootLocation?: TBootLocation;
}
export interface IResetShipDecorationsResponse {
ResetRoom: string;
ClaimedDecos: [];
NewCapacity: number;
}
export interface ISetPlacedDecoInfoRequest { export interface ISetPlacedDecoInfoRequest {
DecoType: string; DecoType: string;
DecoId: string; DecoId: string;

View File

@ -108,13 +108,3 @@ errorLog.on("new", filename => logger.info(`Using error log file: ${filename}`))
combinedLog.on("new", filename => logger.info(`Using combined log file: ${filename}`)); combinedLog.on("new", filename => logger.info(`Using combined log file: ${filename}`));
errorLog.on("rotate", filename => logger.info(`Rotated error log file: ${filename}`)); errorLog.on("rotate", filename => logger.info(`Rotated error log file: ${filename}`));
combinedLog.on("rotate", filename => logger.info(`Rotated combined log file: ${filename}`)); combinedLog.on("rotate", filename => logger.info(`Rotated combined log file: ${filename}`));
export const logError = (err: Error, context: string): void => {
if (err.stack) {
const stackArr = err.stack.split("\n");
stackArr[0] += ` while ${context}`;
logger.error(stackArr.join("\n"));
} else {
logger.error(`uncaught error while ${context}: ${err.message}`);
}
};

View File

@ -118,16 +118,9 @@ function doLogin() {
window.registerSubmit = false; window.registerSubmit = false;
} }
function revalidateAuthz() { async function revalidateAuthz() {
return new Promise(resolve => { await getWebSocket();
let interval; // We have a websocket connection, so authz should be good.
interval = setInterval(() => {
if (ws_is_open && !auth_pending) {
clearInterval(interval);
resolve();
}
}, 10);
});
} }
function logout() { function logout() {
@ -2087,10 +2080,6 @@ single.getRoute("/webui/cheats").on("beforeload", function () {
}) })
.fail(res => { .fail(res => {
if (res.responseText == "Log-in expired") { if (res.responseText == "Log-in expired") {
if (ws_is_open && !auth_pending) {
console.warn("Credentials invalidated but the server didn't let us know");
sendAuth();
}
revalidateAuthz().then(() => { revalidateAuthz().then(() => {
if (single.getCurrentPath() == "/webui/cheats") { if (single.getCurrentPath() == "/webui/cheats") {
single.loadRoute("/webui/cheats"); single.loadRoute("/webui/cheats");

View File

@ -1,11 +1,11 @@
// French translation by Vitruvio // French translation by Vitruvio
dict = { dict = {
general_inventoryUpdateNote: `Note : Pour voir les changements en jeu, l'inventaire doit être actualisé. Cela se fait en tapant /sync dans le tchat, en visitant un dojo/relais ou en se reconnectant.`, general_inventoryUpdateNote: `[UNTRANSLATED] Note: To see changes in-game, you need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging.`,
general_addButton: `Ajouter`, general_addButton: `Ajouter`,
general_setButton: `Définir`, general_setButton: `[UNTRANSLATED] Set`,
general_none: `Aucun`, general_none: `Aucun`,
general_bulkActions: `Action groupée`, general_bulkActions: `Action groupée`,
general_loading: `Chargement...`, general_loading: `[UNTRANSLATED] Loading...`,
code_loginFail: `Connexion échouée. Vérifiez le mot de passe.`, code_loginFail: `Connexion échouée. Vérifiez le mot de passe.`,
code_regFail: `Enregistrement impossible. Compte existant?`, code_regFail: `Enregistrement impossible. Compte existant?`,
@ -46,8 +46,8 @@ dict = {
code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`, code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`,
code_addModsConfirm: `Ajouter |COUNT| mods à l'inventaire ?`, code_addModsConfirm: `Ajouter |COUNT| mods à l'inventaire ?`,
code_succImport: `Importé.`, code_succImport: `Importé.`,
code_succRelog: `Succès. Un redémarrage du jeu est nécessaire.`, code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`,
code_nothingToDo: `Succès.`, code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`,
code_gild: `Polir`, code_gild: `Polir`,
code_moa: `Moa`, code_moa: `Moa`,
code_zanuka: `Molosse`, code_zanuka: `Molosse`,
@ -62,8 +62,8 @@ dict = {
code_pigment: `Pigment`, code_pigment: `Pigment`,
code_mature: `Maturer pour le combat`, code_mature: `Maturer pour le combat`,
code_unmature: `Régrésser l'âge génétique`, code_unmature: `Régrésser l'âge génétique`,
code_succChange: `Changement effectué.`, code_succChange: `[UNTRANSLATED] Successfully changed.`,
code_requiredInvigorationUpgrade: `Augmentation offensive et défensive requises.`, code_requiredInvigorationUpgrade: `[UNTRANSLATED] You must select both an offensive & defensive upgrade.`,
login_description: `Connexion avec les informations de connexion OpenWF.`, login_description: `Connexion avec les informations de connexion OpenWF.`,
login_emailLabel: `Email`, login_emailLabel: `Email`,
login_passwordLabel: `Mot de passe`, login_passwordLabel: `Mot de passe`,
@ -125,42 +125,42 @@ dict = {
detailedView_archonShardsDescription: `Slots illimités pour appliquer plusieurs améliorations`, detailedView_archonShardsDescription: `Slots illimités pour appliquer plusieurs améliorations`,
detailedView_archonShardsDescription2: `Un délai sera présent entre l'application des éclats et le chargement en jeu.`, detailedView_archonShardsDescription2: `Un délai sera présent entre l'application des éclats et le chargement en jeu.`,
detailedView_valenceBonusLabel: `Bonus de Valence`, detailedView_valenceBonusLabel: `Bonus de Valence`,
detailedView_valenceBonusDescription: `Définir le Bonus Valence de l'arme.`, detailedView_valenceBonusDescription: `[UNTRANSLATED] You can set or remove the Valence Bonus from your weapon.`,
detailedView_modularPartsLabel: `Changer l'équipement modulaire`, detailedView_modularPartsLabel: `[UNTRANSLATED] Change Modular Parts`,
detailedView_suitInvigorationLabel: `Invigoration de Warframe`, detailedView_suitInvigorationLabel: `[UNTRANSLATED] Warframe Invigoration`,
invigorations_offensive_AbilityStrength: `+200% de puissance de pouvoir`, invigorations_offensive_AbilityStrength: `[UNTRANSLATED] +200% Ability Strength`,
invigorations_offensive_AbilityRange: `+100% de portée de pouvoir`, invigorations_offensive_AbilityRange: `[UNTRANSLATED] +100% Ability Range`,
invigorations_offensive_AbilityDuration: `+100% de durée de pouvoir`, invigorations_offensive_AbilityDuration: `[UNTRANSLATED] +100% Ability Duration`,
invigorations_offensive_MeleeDamage: `+250% de dégâts de mêlée`, invigorations_offensive_MeleeDamage: `[UNTRANSLATED] +250% Melee Damage`,
invigorations_offensive_PrimaryDamage: `+250% de dégâts d'arme primaire`, invigorations_offensive_PrimaryDamage: `[UNTRANSLATED] +250% Primary Damage`,
invigorations_offensive_SecondaryDamage: `+250% de dégâts d'arme secondaire`, invigorations_offensive_SecondaryDamage: `[UNTRANSLATED] +250% Secondary Damage`,
invigorations_offensive_PrimaryCritChance: `+200% de chances critique sur arme primaire`, invigorations_offensive_PrimaryCritChance: `[UNTRANSLATED] +200% Primary Critical Chance`,
invigorations_offensive_SecondaryCritChance: `+200% de chances critique sur arme secondaire`, invigorations_offensive_SecondaryCritChance: `[UNTRANSLATED] +200% Secondary Critical Chance`,
invigorations_offensive_MeleeCritChance: `+200% de chances critique en mêlée`, invigorations_offensive_MeleeCritChance: `[UNTRANSLATED] +200% Melee Critical Chance`,
invigorations_utility_AbilityEfficiency: `+75% d'efficacité de pouvoir`, invigorations_utility_AbilityEfficiency: `[UNTRANSLATED] +75% Ability Efficiency`,
invigorations_utility_SprintSpeed: `+75% de vitesse de course`, invigorations_utility_SprintSpeed: `[UNTRANSLATED] +75% Sprint Speed`,
invigorations_utility_ParkourVelocity: `+75% de vélocité de parkour`, invigorations_utility_ParkourVelocity: `[UNTRANSLATED] +75% Parkour Velocity`,
invigorations_utility_HealthMax: `+1000 de vie`, invigorations_utility_HealthMax: `[UNTRANSLATED] +1000 Health`,
invigorations_utility_EnergyMax: `+200% d'énergie max`, invigorations_utility_EnergyMax: `[UNTRANSLATED] +200% Energy Max`,
invigorations_utility_StatusImmune: `Immunisé contre les effets de statut`, invigorations_utility_StatusImmune: `[UNTRANSLATED] Immune to Status Effects`,
invigorations_utility_ReloadSpeed: `+75% de vitesse de rechargement`, invigorations_utility_ReloadSpeed: `[UNTRANSLATED] +75% Reload Speed`,
invigorations_utility_HealthRegen: `+25 de vie régénérés/s`, invigorations_utility_HealthRegen: `[UNTRANSLATED] +25 Health Regen/s`,
invigorations_utility_ArmorMax: `+1000 d'armure`, invigorations_utility_ArmorMax: `[UNTRANSLATED] +1000 Armor`,
invigorations_utility_Jumps: `+5 réinitialisations de saut`, invigorations_utility_Jumps: `[UNTRANSLATED] +5 Jump Resets`,
invigorations_utility_EnergyRegen: `+2 d'énergie régénérés/s`, invigorations_utility_EnergyRegen: `[UNTRANSLATED] +2 Energy Regen/s`,
invigorations_offensiveLabel: `Amélioration offensive`, invigorations_offensiveLabel: `[UNTRANSLATED] Offensive Upgrade`,
invigorations_defensiveLabel: `Amélioration défensive`, invigorations_defensiveLabel: `[UNTRANSLATED] Defensive Upgrade`,
invigorations_expiryLabel: `Expiration de l'invigoration (optionnel)`, invigorations_expiryLabel: `[UNTRANSLATED] Upgrades Expiry (optional)`,
mods_addRiven: `Ajouter un riven`, mods_addRiven: `Ajouter un riven`,
mods_fingerprint: `Empreinte`, mods_fingerprint: `Empreinte`,
mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`, mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`,
mods_rivens: `Rivens`, mods_rivens: `Rivens`,
mods_mods: `Mods`, mods_mods: `Mods`,
mods_addMax: `Ajouter les mods niveau max`, mods_addMax: `[UNTRANSLATED] Add Maxed`,
mods_addMissingUnrankedMods: `Ajouter les mods sans rang manquants`, mods_addMissingUnrankedMods: `Ajouter les mods sans rang manquants`,
mods_removeUnranked: `Retirer les mods sans rang`, mods_removeUnranked: `Retirer les mods sans rang`,
mods_addMissingMaxRankMods: `Ajouter les mods niveau max manquants`, mods_addMissingMaxRankMods: `Ajouter les mods niveau max manquants`,
@ -170,7 +170,7 @@ dict = {
cheats_skipAllDialogue: `Passer les dialogues`, cheats_skipAllDialogue: `Passer les dialogues`,
cheats_unlockAllScans: `Débloquer tous les scans`, cheats_unlockAllScans: `Débloquer tous les scans`,
cheats_unlockAllMissions: `Débloquer toutes les missions`, cheats_unlockAllMissions: `Débloquer toutes les missions`,
cheats_unlockAllMissions_ok: `Succès. Une actualisation de l'inventaire est nécessaire.`, cheats_unlockAllMissions_ok: `[UNTRANSLATED] Success. Please note that you'll need to enter a dojo/relay or relog for the client to refresh the star chart.`,
cheats_infiniteCredits: `Crédits infinis`, cheats_infiniteCredits: `Crédits infinis`,
cheats_infinitePlatinum: `Platinum infini`, cheats_infinitePlatinum: `Platinum infini`,
cheats_infiniteEndo: `Endo infini`, cheats_infiniteEndo: `Endo infini`,
@ -201,8 +201,8 @@ dict = {
cheats_noDeathMarks: `Aucune marque d'assassin`, cheats_noDeathMarks: `Aucune marque d'assassin`,
cheats_noKimCooldowns: `Aucun cooldown sur le KIM`, cheats_noKimCooldowns: `Aucun cooldown sur le KIM`,
cheats_fullyStockedVendors: `Les vendeurs ont un stock à 100%`, cheats_fullyStockedVendors: `Les vendeurs ont un stock à 100%`,
cheats_baroAlwaysAvailable: `Baro toujours présent`, cheats_baroAlwaysAvailable: `[UNTRANSLATED] Baro Always Available`,
cheats_baroFullyStocked: `Stock de Baro au max`, cheats_baroFullyStocked: `[UNTRANSLATED] Baro Fully Stocked`,
cheats_syndicateMissionsRepeatable: `Mission syndicat répétables`, cheats_syndicateMissionsRepeatable: `Mission syndicat répétables`,
cheats_unlockAllProfitTakerStages: `Débloquer toutes les étapes du Preneur de Profit`, cheats_unlockAllProfitTakerStages: `Débloquer toutes les étapes du Preneur de Profit`,
cheats_instantFinishRivenChallenge: `Débloquer le challenge Riven instantanément`, cheats_instantFinishRivenChallenge: `Débloquer le challenge Riven instantanément`,
@ -216,75 +216,75 @@ dict = {
cheats_noDojoResearchTime: `Aucun temps de recherche (Dojo)`, cheats_noDojoResearchTime: `Aucun temps de recherche (Dojo)`,
cheats_fastClanAscension: `Ascension de clan rapide`, cheats_fastClanAscension: `Ascension de clan rapide`,
cheats_missionsCanGiveAllRelics: `Les missions donnent toutes les reliques`, cheats_missionsCanGiveAllRelics: `Les missions donnent toutes les reliques`,
cheats_exceptionalRelicsAlwaysGiveBronzeReward: `Les reliques exceptionnelles donnent toujours une récompense en bronze`, cheats_exceptionalRelicsAlwaysGiveBronzeReward: `[UNTRANSLATED] Exceptional Relics Always Give Bronze Reward`,
cheats_flawlessRelicsAlwaysGiveSilverReward: `Les reliques parfaites donnent toujours une récompense en argent`, cheats_flawlessRelicsAlwaysGiveSilverReward: `[UNTRANSLATED] Flawless Relics Always Give Silver Reward`,
cheats_radiantRelicsAlwaysGiveGoldReward: `Les reliques éclatantes donnent toujours une récompense en or`, cheats_radiantRelicsAlwaysGiveGoldReward: `[UNTRANSLATED] Radiant Relics Always Give Gold Reward`,
cheats_unlockAllSimarisResearchEntries: `Débloquer toute les recherches chez Simaris`, cheats_unlockAllSimarisResearchEntries: `Débloquer toute les recherches chez Simaris`,
cheats_disableDailyTribute: `Désactiver la récompense quotidienne de connexion`, cheats_disableDailyTribute: `[UNTRANSLATED] Disable Daily Tribute`,
cheats_spoofMasteryRank: `Rang de maîtrise personnalisé (-1 pour désactiver)`, cheats_spoofMasteryRank: `Rang de maîtrise personnalisé (-1 pour désactiver)`,
cheats_relicRewardItemCountMultiplier: `Multiplicateur de récompenses de relique`, cheats_relicRewardItemCountMultiplier: `[UNTRANSLATED] Relic Reward Item Count Multiplier`,
cheats_nightwaveStandingMultiplier: `Multiplicateur de réputation d'Ondes Nocturnes`, cheats_nightwaveStandingMultiplier: `Multiplicateur de réputation d'Ondes Nocturnes`,
cheats_save: `Sauvegarder`, cheats_save: `Sauvegarder`,
cheats_account: `Compte`, cheats_account: `Compte`,
cheats_unlockAllFocusSchools: `Débloquer toutes les écoles de focus`, cheats_unlockAllFocusSchools: `Débloquer toutes les écoles de focus`,
cheats_helminthUnlockAll: `Helminth niveau max`, cheats_helminthUnlockAll: `Helminth niveau max`,
cheats_addMissingSubsumedAbilities: `Ajouter les capacités subsumées manquantes`, cheats_addMissingSubsumedAbilities: `[UNTRANSLATED] Add Missing Subsumed Abilities`,
cheats_intrinsicsUnlockAll: `Inhérences niveau max`, cheats_intrinsicsUnlockAll: `Inhérences niveau max`,
cheats_changeSupportedSyndicate: `Allégeance`, cheats_changeSupportedSyndicate: `Allégeance`,
cheats_changeButton: `Changer`, cheats_changeButton: `Changer`,
cheats_markAllAsRead: `Marquer la boîte de réception comme lue`, cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`,
worldState: `Carte Solaire`, worldState: `[UNTRANSLATED] World State`,
worldState_creditBoost: `Booster de Crédit`, worldState_creditBoost: `[UNTRANSLATED] Credit Boost`,
worldState_affinityBoost: `Booster d'Affinité`, worldState_affinityBoost: `[UNTRANSLATED] Affinity Boost`,
worldState_resourceBoost: `Booster de Ressource`, worldState_resourceBoost: `[UNTRANSLATED] Resource Boost`,
worldState_starDays: `Jours Stellaires`, worldState_starDays: `[UNTRANSLATED] Star Days`,
worldState_galleonOfGhouls: `Galion des Goules`, worldState_galleonOfGhouls: `[UNTRANSLATED] Galleon of Ghouls`,
disabled: `Désactivé`, disabled: `[UNTRANSLATED] Disabled`,
worldState_we1: `Weekend 1`, worldState_we1: `[UNTRANSLATED] Weekend 1`,
worldState_we2: `Weekend 2`, worldState_we2: `[UNTRANSLATED] Weekend 2`,
worldState_we3: `Weekend 3`, worldState_we3: `[UNTRANSLATED] Weekend 3`,
worldState_eidolonOverride: `Météo Plaines d'Eidolon`, worldState_eidolonOverride: `[UNTRANSLATED] Eidolon Override`,
worldState_day: `Jour`, worldState_day: `[UNTRANSLATED] Day`,
worldState_night: `Nuit`, worldState_night: `[UNTRANSLATED] Night`,
worldState_vallisOverride: `Météo Vallée Orbis`, worldState_vallisOverride: `[UNTRANSLATED] Orb Vallis Override`,
worldState_warm: `Chaud`, worldState_warm: `[UNTRANSLATED] Warm`,
worldState_cold: `Froid`, worldState_cold: `[UNTRANSLATED] Cold`,
worldState_duviriOverride: `Spirale Duviri`, worldState_duviriOverride: `[UNTRANSLATED] Duviri Override`,
worldState_joy: `Joie`, worldState_joy: `[UNTRANSLATED] Joy`,
worldState_anger: `Colère`, worldState_anger: `[UNTRANSLATED] Anger`,
worldState_envy: `Envie `, worldState_envy: `[UNTRANSLATED] Envy`,
worldState_sorrow: `hagrin`, worldState_sorrow: `[UNTRANSLATED] Sorrow`,
worldState_fear: `Peur`, worldState_fear: `[UNTRANSLATED] Fear`,
worldState_nightwaveOverride: `Saison d'Ondes Nocturnes`, worldState_nightwaveOverride: `[UNTRANSLATED] Nightwave Override`,
worldState_RadioLegionIntermission13Syndicate: `Mix de Nora Vol. 9`, worldState_RadioLegionIntermission13Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 9`,
worldState_RadioLegionIntermission12Syndicate: `Mix de Nora Vol. 8`, worldState_RadioLegionIntermission12Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 8`,
worldState_RadioLegionIntermission11Syndicate: `Mix de Nora Vol. 7`, worldState_RadioLegionIntermission11Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 7`,
worldState_RadioLegionIntermission10Syndicate: `Mix de Nora Vol. 6`, worldState_RadioLegionIntermission10Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 6`,
worldState_RadioLegionIntermission9Syndicate: `Mix de Nora Vol. 5`, worldState_RadioLegionIntermission9Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 5`,
worldState_RadioLegionIntermission8Syndicate: `Mix de Nora Vol. 4`, worldState_RadioLegionIntermission8Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 4`,
worldState_RadioLegionIntermission7Syndicate: `Mix de Nora Vol. 3`, worldState_RadioLegionIntermission7Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 3`,
worldState_RadioLegionIntermission6Syndicate: `Mix de Nora Vol. 2`, worldState_RadioLegionIntermission6Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 2`,
worldState_RadioLegionIntermission5Syndicate: `Mix de Nora Vol. 1`, worldState_RadioLegionIntermission5Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 1`,
worldState_RadioLegionIntermission4Syndicate: `La Sélection de Nora`, worldState_RadioLegionIntermission4Syndicate: `[UNTRANSLATED] Nora's Choice`,
worldState_RadioLegionIntermission3Syndicate: `Intermission III`, worldState_RadioLegionIntermission3Syndicate: `[UNTRANSLATED] Intermission III`,
worldState_RadioLegion3Syndicate: `Les Mystères du Verre`, worldState_RadioLegion3Syndicate: `[UNTRANSLATED] Glassmaker`,
worldState_RadioLegionIntermission2Syndicate: `Intermission II`, worldState_RadioLegionIntermission2Syndicate: `[UNTRANSLATED] Intermission II`,
worldState_RadioLegion2Syndicate: `L'Émissaire`, worldState_RadioLegion2Syndicate: `[UNTRANSLATED] The Emissary`,
worldState_RadioLegionIntermissionSyndicate: `Intermission I`, worldState_RadioLegionIntermissionSyndicate: `[UNTRANSLATED] Intermission I`,
worldState_RadioLegionSyndicate: `Le Loup de Saturne Six`, worldState_RadioLegionSyndicate: `[UNTRANSLATED] The Wolf of Saturn Six`,
worldState_fissures: `Fissures`, worldState_fissures: `[UNTRANSLATED] Fissures`,
normal: `Normal`, normal: `[UNTRANSLATED] Normal`,
worldState_allAtOnceNormal: `Toutes, Normal`, worldState_allAtOnceNormal: `[UNTRANSLATED] All At Once, Normal`,
worldState_allAtOnceSteelPath: `Toutes, Route de l'Acier`, worldState_allAtOnceSteelPath: `[UNTRANSLATED] All At Once, Steel Path`,
worldState_theCircuitOverride: `Remplacement du Circuit`, worldState_theCircuitOverride: `[UNTRANSLATED] The Circuit Override`,
worldState_darvoStockMultiplier: `Multiplicateur du stock de Darvo`, worldState_darvoStockMultiplier: `[UNTRANSLATED] Darvo Stock Multiplier`,
worldState_varziaFullyStocked: `Stock de Varzia au max`, worldState_varziaFullyStocked: `[UNTRANSLATED] Varzia Fully Stocked`,
worldState_varziaOverride: `Rotation de Varzia`, worldState_varziaOverride: `[UNTRANSLATED] Varzia Rotation 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`,
import_samples: `Échantillons :`, import_samples: `Echantillons :`,
import_samples_maxFocus: `Toutes les écoles de focus au rang max`, import_samples_maxFocus: `Toutes les écoles de focus au rang max`,
upgrade_Equilibrium: `Ramasser de la santé donne +|VAL|% d'énergie supplémentaire. Ramasser de l'énergie donne +|VAL|% de santé supplémentaire.`, upgrade_Equilibrium: `Ramasser de la santé donne +|VAL|% d'énergie supplémentaire. Ramasser de l'énergie donne +|VAL|% de santé supplémentaire.`,
@ -305,7 +305,7 @@ dict = {
upgrade_WarframeGlobeEffectEnergy: `+|VAL|% d'efficacité d'orbe d'énergie`, upgrade_WarframeGlobeEffectEnergy: `+|VAL|% d'efficacité d'orbe d'énergie`,
upgrade_WarframeGlobeEffectHealth: `+|VAL|% d'efficacité d'orbe de santé`, upgrade_WarframeGlobeEffectHealth: `+|VAL|% d'efficacité d'orbe de santé`,
upgrade_WarframeHealthMax: `+|VAL| de santé`, upgrade_WarframeHealthMax: `+|VAL| de santé`,
upgrade_WarframeHPBoostFromImpact: `+|VAL1| de vie sur élimination avec des dégâts d'explostion (Max |VAL2| Health)`, upgrade_WarframeHPBoostFromImpact: `[UNTRANSLATED] +|VAL1| Health on kill with Blast Damage (Max |VAL2| Health)`,
upgrade_WarframeParkourVelocity: `+|VAL|% de vélocité de parkour`, upgrade_WarframeParkourVelocity: `+|VAL|% de vélocité de parkour`,
upgrade_WarframeRadiationDamageBoost: `+|VAL|% de dégâts de pouvoir sur les ennemis affectés par du statut radiation`, upgrade_WarframeRadiationDamageBoost: `+|VAL|% de dégâts de pouvoir sur les ennemis affectés par du statut radiation`,
upgrade_WarframeHealthRegen: `+|VAL| régénération de santé/s`, upgrade_WarframeHealthRegen: `+|VAL| régénération de santé/s`,
@ -324,7 +324,7 @@ dict = {
upgrade_OnExecutionAmmo: `100% de rechargement des armes primaires et secondaires sur une une miséricorde`, upgrade_OnExecutionAmmo: `100% de rechargement des armes primaires et secondaires sur une une miséricorde`,
upgrade_OnExecutionHealthDrop: `100% de chance de drop une orbe de santé sur une miséricorde`, upgrade_OnExecutionHealthDrop: `100% de chance de drop une orbe de santé sur une miséricorde`,
upgrade_OnExecutionEnergyDrop: `50% de chance de drop une orbe d'énergie sur une miséricorde`, upgrade_OnExecutionEnergyDrop: `50% de chance de drop une orbe d'énergie sur une miséricorde`,
upgrade_OnFailHackReset: `+50% de chance de refaire un piratage`, upgrade_OnFailHackReset: `[UNTRANSLATED] +50% Hacking Retry Chance`,
upgrade_DamageReductionOnHack: `75% de réduction de dégâts pendant un piratage`, upgrade_DamageReductionOnHack: `75% de réduction de dégâts pendant un piratage`,
upgrade_OnExecutionReviveCompanion: `Les miséricordes réduisent le temps de récupération du compagnon de 15s`, upgrade_OnExecutionReviveCompanion: `Les miséricordes réduisent le temps de récupération du compagnon de 15s`,
upgrade_OnExecutionParkourSpeed: `+60% de vitesse de parkour pendant 15s après une miséricorde`, upgrade_OnExecutionParkourSpeed: `+60% de vitesse de parkour pendant 15s après une miséricorde`,
@ -333,10 +333,10 @@ dict = {
upgrade_OnExecutionTerrify: `Les ennemis dans un rayon de 15m ont 50% de chance de s'enfuir après une miséricorde`, upgrade_OnExecutionTerrify: `Les ennemis dans un rayon de 15m ont 50% de chance de s'enfuir après une miséricorde`,
upgrade_OnHackLockers: `5 casiers s'ouvrent dans un rayon de 20m après un piratage`, upgrade_OnHackLockers: `5 casiers s'ouvrent dans un rayon de 20m après un piratage`,
upgrade_OnExecutionBlind: `Les ennemis sont aveuglés dans un rayon de 18 après une miséricorde`, upgrade_OnExecutionBlind: `Les ennemis sont aveuglés dans un rayon de 18 après une miséricorde`,
upgrade_OnExecutionDrainPower: `Le prochain pouvoir activé gagne +50% de puissance de pouvoir après une miséricorde`, upgrade_OnExecutionDrainPower: `[UNTRANSLATED] Next ability cast gains +50% Ability Strength on Mercy`,
upgrade_OnHackSprintSpeed: `+75% de vitesse de course pendant 15s après un piratage`, upgrade_OnHackSprintSpeed: `+75% de vitesse de course pendant 15s après un piratage`,
upgrade_SwiftExecute: `+50% de vitesse de d'éxecution en miséricorde`, upgrade_SwiftExecute: `[UNTRANSLATED] +50% Mercy Kill Speed`,
upgrade_OnHackInvis: `Invisible pendant 15s après un piratage`, upgrade_OnHackInvis: `[UNTRANSLATED] Invisible for 15 seconds after Hacking`,
damageType_Electricity: `Électrique`, damageType_Electricity: `Électrique`,
damageType_Fire: `Feu`, damageType_Fire: `Feu`,
@ -346,8 +346,8 @@ dict = {
damageType_Poison: `Poison`, damageType_Poison: `Poison`,
damageType_Radiation: `Radiations`, damageType_Radiation: `Radiations`,
theme_dark: `Thème sombre`, theme_dark: `[UNTRANSLATED] Dark Theme`,
theme_light: `Thème clair`, theme_light: `[UNTRANSLATED] Light Theme`,
prettier_sucks_ass: `` prettier_sucks_ass: ``
}; };