forked from OpenWF/SpaceNinjaServer
Compare commits
12 Commits
webui-refr
...
main
Author | SHA1 | Date | |
---|---|---|---|
c0a0463a68 | |||
2307a40833 | |||
304af514e2 | |||
ddf3cd49b5 | |||
41e3f0136f | |||
c0ca9d9398 | |||
0f6b55beed | |||
f8550e9afe | |||
b53c4d9125 | |||
922b65cfab | |||
2f642df20a | |||
62314e89c7 |
@ -7,5 +7,6 @@ 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"]
|
||||||
|
22
src/controllers/api/apartmentController.ts
Normal file
22
src/controllers/api/apartmentController.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||||
|
import { getPersonalRooms } from "@/src/services/personalRoomsService";
|
||||||
|
import { RequestHandler } from "express";
|
||||||
|
|
||||||
|
export const apartmentController: RequestHandler = async (req, res) => {
|
||||||
|
const accountId = await getAccountIdForRequest(req);
|
||||||
|
const personalRooms = await getPersonalRooms(accountId, "Apartment");
|
||||||
|
const response: IApartmentResponse = {};
|
||||||
|
if (req.query.backdrop !== undefined) {
|
||||||
|
response.NewBackdropItem = personalRooms.Apartment.VideoWallBackdrop = req.query.backdrop as string;
|
||||||
|
}
|
||||||
|
if (req.query.soundscape !== undefined) {
|
||||||
|
response.NewSoundscapeItem = personalRooms.Apartment.Soundscape = req.query.soundscape as string;
|
||||||
|
}
|
||||||
|
await personalRooms.save();
|
||||||
|
res.json(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IApartmentResponse {
|
||||||
|
NewBackdropItem?: string;
|
||||||
|
NewSoundscapeItem?: string;
|
||||||
|
}
|
@ -3,11 +3,13 @@ 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) => {
|
||||||
@ -18,9 +20,20 @@ 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;
|
const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest | IClearObstacleCourseRequest;
|
||||||
|
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));
|
||||||
@ -31,3 +44,8 @@ interface IDestroyDojoDecoRequest {
|
|||||||
ComponentId: string;
|
ComponentId: string;
|
||||||
DecoId: string;
|
DecoId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IClearObstacleCourseRequest {
|
||||||
|
ComponentId: string;
|
||||||
|
Act: "cObst" | "maybesomethingelsewedontknowabout";
|
||||||
|
}
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||||
import { IShipDecorationsRequest } from "@/src/types/personalRoomsTypes";
|
import { IShipDecorationsRequest, IResetShipDecorationsRequest } from "@/src/types/personalRoomsTypes";
|
||||||
import { logger } from "@/src/utils/logger";
|
|
||||||
import { RequestHandler } from "express";
|
import { RequestHandler } from "express";
|
||||||
import { handleSetShipDecorations } from "@/src/services/shipCustomizationsService";
|
import { handleResetShipDecorations, 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 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@ 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);
|
||||||
@ -36,5 +37,8 @@ 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();
|
||||||
};
|
};
|
||||||
|
12
src/index.ts
12
src/index.ts
@ -18,17 +18,23 @@ 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";
|
||||||
|
|
||||||
// Patch JSON.stringify to work flawlessly with Bigints.
|
JSON.stringify = JSONStringify; // 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(() => {
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { logger } from "@/src/utils/logger";
|
import { logError } 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 {
|
||||||
logger.error(`uncaught error while processing ${req.path} request: ${err.message}`);
|
logError(err, `processing ${req.path} request`);
|
||||||
res.status(500).end();
|
res.status(500).end();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -122,7 +122,9 @@ const apartmentSchema = new Schema<IApartmentDatabase>(
|
|||||||
{
|
{
|
||||||
Rooms: [roomSchema],
|
Rooms: [roomSchema],
|
||||||
FavouriteLoadouts: [favouriteLoadoutSchema],
|
FavouriteLoadouts: [favouriteLoadoutSchema],
|
||||||
Gardening: gardeningSchema
|
Gardening: gardeningSchema,
|
||||||
|
VideoWallBackdrop: String,
|
||||||
|
Soundscape: String
|
||||||
},
|
},
|
||||||
{ _id: false }
|
{ _id: false }
|
||||||
);
|
);
|
||||||
|
@ -10,6 +10,7 @@ import { addPendingFriendController } from "@/src/controllers/api/addPendingFrie
|
|||||||
import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
|
import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
|
||||||
import { addToGuildController } from "@/src/controllers/api/addToGuildController";
|
import { addToGuildController } from "@/src/controllers/api/addToGuildController";
|
||||||
import { adoptPetController } from "@/src/controllers/api/adoptPetController";
|
import { adoptPetController } from "@/src/controllers/api/adoptPetController";
|
||||||
|
import { apartmentController } from "@/src/controllers/api/apartmentController";
|
||||||
import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
|
import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
|
||||||
import { archonFusionController } from "@/src/controllers/api/archonFusionController";
|
import { archonFusionController } from "@/src/controllers/api/archonFusionController";
|
||||||
import { artifactsController } from "@/src/controllers/api/artifactsController";
|
import { artifactsController } from "@/src/controllers/api/artifactsController";
|
||||||
@ -168,6 +169,7 @@ const apiRouter = express.Router();
|
|||||||
// get
|
// get
|
||||||
apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController);
|
apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController);
|
||||||
apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController);
|
apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController);
|
||||||
|
apiRouter.get("/apartment.php", apartmentController);
|
||||||
apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementController);
|
apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementController);
|
||||||
apiRouter.get("/changeDojoRoot.php", changeDojoRootController);
|
apiRouter.get("/changeDojoRoot.php", changeDojoRootController);
|
||||||
apiRouter.get("/changeGuildRank.php", changeGuildRankController);
|
apiRouter.get("/changeGuildRank.php", changeGuildRankController);
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
IDojoComponentDatabase,
|
IDojoComponentDatabase,
|
||||||
IDojoContributable,
|
IDojoContributable,
|
||||||
IDojoDecoClient,
|
IDojoDecoClient,
|
||||||
|
IDojoDecoDatabase,
|
||||||
IGuildClient,
|
IGuildClient,
|
||||||
IGuildMemberClient,
|
IGuildMemberClient,
|
||||||
IGuildMemberDatabase,
|
IGuildMemberDatabase,
|
||||||
@ -309,7 +310,7 @@ export const removeDojoRoom = async (
|
|||||||
guild.DojoEnergy -= meta.energy;
|
guild.DojoEnergy -= meta.energy;
|
||||||
}
|
}
|
||||||
moveResourcesToVault(guild, component);
|
moveResourcesToVault(guild, component);
|
||||||
component.Decos?.forEach(deco => moveResourcesToVault(guild, deco));
|
component.Decos?.forEach(deco => refundDojoDeco(guild, component, 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));
|
||||||
@ -344,6 +345,14 @@ 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) {
|
||||||
@ -369,7 +378,7 @@ export const removeDojoDeco = (
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
moveResourcesToVault(guild, deco);
|
moveResourcesToVault(guild, deco); // Refund resources spent on construction
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoContributable): void => {
|
const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoContributable): void => {
|
||||||
|
@ -1283,9 +1283,7 @@ export const addMissionRewards = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let medallionAmount = Math.floor(
|
let medallionAmount = Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1));
|
||||||
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)
|
||||||
@ -1637,7 +1635,10 @@ function getRandomMissionDrops(
|
|||||||
rewardManifests = [
|
rewardManifests = [
|
||||||
"/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriMurmurFinalSteelChestRewards"
|
"/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriMurmurFinalSteelChestRewards"
|
||||||
];
|
];
|
||||||
} else if (RewardInfo.T == 70) {
|
} else if (
|
||||||
|
RewardInfo.T == 70 ||
|
||||||
|
RewardInfo.T == 6 // https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2526
|
||||||
|
) {
|
||||||
// Orowyrm chest, gives 10 Pathos Clamps, or 15 on Steel Path.
|
// Orowyrm chest, gives 10 Pathos Clamps, or 15 on Steel Path.
|
||||||
drops.push({
|
drops.push({
|
||||||
StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
|
StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
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,
|
||||||
@ -154,7 +156,6 @@ 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 {
|
||||||
@ -198,6 +199,51 @@ 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))!;
|
||||||
|
@ -2,9 +2,16 @@ import http from "http";
|
|||||||
import https from "https";
|
import https from "https";
|
||||||
import ws from "ws";
|
import ws from "ws";
|
||||||
import { Account } from "@/src/models/loginModel";
|
import { Account } from "@/src/models/loginModel";
|
||||||
import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "@/src/services/loginService";
|
import {
|
||||||
|
createAccount,
|
||||||
|
createNonce,
|
||||||
|
getUsernameFromEmail,
|
||||||
|
isCorrectPassword,
|
||||||
|
isNameTaken
|
||||||
|
} 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;
|
||||||
@ -88,6 +95,7 @@ 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 });
|
||||||
@ -103,6 +111,7 @@ const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
|
|||||||
}
|
}
|
||||||
} else if (data.auth.isRegister) {
|
} else if (data.auth.isRegister) {
|
||||||
const name = await getUsernameFromEmail(data.auth.email);
|
const name = await getUsernameFromEmail(data.auth.email);
|
||||||
|
if (!(await isNameTaken(name))) {
|
||||||
account = await createAccount({
|
account = await createAccount({
|
||||||
email: data.auth.email,
|
email: data.auth.email,
|
||||||
password: data.auth.password,
|
password: data.auth.password,
|
||||||
@ -112,6 +121,7 @@ const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
|
|||||||
Nonce: createNonce()
|
Nonce: createNonce()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (account) {
|
if (account) {
|
||||||
(ws as IWsCustomData).accountId = account.id;
|
(ws as IWsCustomData).accountId = account.id;
|
||||||
ws.send(
|
ws.send(
|
||||||
@ -146,6 +156,9 @@ const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError(e as Error, `processing websocket message`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -91,12 +91,16 @@ export interface IApartmentClient {
|
|||||||
Gardening: IGardeningClient;
|
Gardening: IGardeningClient;
|
||||||
Rooms: IRoom[];
|
Rooms: IRoom[];
|
||||||
FavouriteLoadouts: IFavouriteLoadout[];
|
FavouriteLoadouts: IFavouriteLoadout[];
|
||||||
|
VideoWallBackdrop?: string;
|
||||||
|
Soundscape?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IApartmentDatabase {
|
export interface IApartmentDatabase {
|
||||||
Gardening: IGardeningDatabase;
|
Gardening: IGardeningDatabase;
|
||||||
Rooms: IRoom[];
|
Rooms: IRoom[];
|
||||||
FavouriteLoadouts: IFavouriteLoadoutDatabase[];
|
FavouriteLoadouts: IFavouriteLoadoutDatabase[];
|
||||||
|
VideoWallBackdrop?: string;
|
||||||
|
Soundscape?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPlacedDecosDatabase {
|
export interface IPlacedDecosDatabase {
|
||||||
@ -150,6 +154,17 @@ 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;
|
||||||
|
@ -108,3 +108,13 @@ 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}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -436,22 +436,22 @@
|
|||||||
<h5 class="card-header" data-loc="general_bulkActions"></h5>
|
<h5 class="card-header" data-loc="general_bulkActions"></h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-2 d-flex flex-wrap gap-2">
|
<div class="mb-2 d-flex flex-wrap gap-2">
|
||||||
<button class="btn btn-primary" onclick="addMissingEquipment(['Suits']);" data-loc="inventory_bulkAddSuits"></button>
|
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['Suits']);" data-loc="inventory_bulkAddSuits"></button>
|
||||||
<button class="btn btn-primary" onclick="addMissingEquipment(['Melee', 'LongGuns', 'Pistols']);" data-loc="inventory_bulkAddWeapons"></button>
|
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['Melee', 'LongGuns', 'Pistols']);" data-loc="inventory_bulkAddWeapons"></button>
|
||||||
<button class="btn btn-primary" onclick="addMissingEquipment(['SpaceSuits']);" data-loc="inventory_bulkAddSpaceSuits"></button>
|
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['SpaceSuits']);" data-loc="inventory_bulkAddSpaceSuits"></button>
|
||||||
<button class="btn btn-primary" onclick="addMissingEquipment(['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkAddSpaceWeapons"></button>
|
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkAddSpaceWeapons"></button>
|
||||||
<button class="btn btn-primary" onclick="addMissingEquipment(['Sentinels']);" data-loc="inventory_bulkAddSentinels"></button>
|
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['Sentinels']);" data-loc="inventory_bulkAddSentinels"></button>
|
||||||
<button class="btn btn-primary" onclick="addMissingEquipment(['SentinelWeapons']);" data-loc="inventory_bulkAddSentinelWeapons"></button>
|
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['SentinelWeapons']);" data-loc="inventory_bulkAddSentinelWeapons"></button>
|
||||||
<button class="btn btn-primary" onclick="addMissingEvolutionProgress();" data-loc="inventory_bulkAddEvolutionProgress"></button>
|
<button class="btn btn-primary" onclick="debounce(addMissingEvolutionProgress);" data-loc="inventory_bulkAddEvolutionProgress"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2 d-flex flex-wrap gap-2">
|
<div class="mb-2 d-flex flex-wrap gap-2">
|
||||||
<button class="btn btn-success" onclick="maxRankAllEquipment(['Suits']);" data-loc="inventory_bulkRankUpSuits"></button>
|
<button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['Suits']);" data-loc="inventory_bulkRankUpSuits"></button>
|
||||||
<button class="btn btn-success" onclick="maxRankAllEquipment(['Melee', 'LongGuns', 'Pistols']);" data-loc="inventory_bulkRankUpWeapons"></button>
|
<button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['Melee', 'LongGuns', 'Pistols']);" data-loc="inventory_bulkRankUpWeapons"></button>
|
||||||
<button class="btn btn-success" onclick="maxRankAllEquipment(['SpaceSuits']);" data-loc="inventory_bulkRankUpSpaceSuits"></button>
|
<button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['SpaceSuits']);" data-loc="inventory_bulkRankUpSpaceSuits"></button>
|
||||||
<button class="btn btn-success" onclick="maxRankAllEquipment(['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkRankUpSpaceWeapons"></button>
|
<button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkRankUpSpaceWeapons"></button>
|
||||||
<button class="btn btn-success" onclick="maxRankAllEquipment(['Sentinels']);" data-loc="inventory_bulkRankUpSentinels"></button>
|
<button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['Sentinels']);" data-loc="inventory_bulkRankUpSentinels"></button>
|
||||||
<button class="btn btn-success" onclick="maxRankAllEquipment(['SentinelWeapons']);" data-loc="inventory_bulkRankUpSentinelWeapons"></button>
|
<button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['SentinelWeapons']);" data-loc="inventory_bulkRankUpSentinelWeapons"></button>
|
||||||
<button class="btn btn-success" onclick="maxRankAllEvolutions();" data-loc="inventory_bulkRankUpEvolutionProgress"></button>
|
<button class="btn btn-success" onclick="debounce(maxRankAllEvolutions);" data-loc="inventory_bulkRankUpEvolutionProgress"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -118,9 +118,16 @@ function doLogin() {
|
|||||||
window.registerSubmit = false;
|
window.registerSubmit = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function revalidateAuthz() {
|
function revalidateAuthz() {
|
||||||
await getWebSocket();
|
return new Promise(resolve => {
|
||||||
// We have a websocket connection, so authz should be good.
|
let interval;
|
||||||
|
interval = setInterval(() => {
|
||||||
|
if (ws_is_open && !auth_pending) {
|
||||||
|
clearInterval(interval);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
@ -865,15 +872,11 @@ function updateInventory() {
|
|||||||
|
|
||||||
const datalistEvolutionProgress = document.querySelectorAll("#datalist-EvolutionProgress option");
|
const datalistEvolutionProgress = document.querySelectorAll("#datalist-EvolutionProgress option");
|
||||||
const formEvolutionProgress = document.querySelector('form[onsubmit*="doAcquireEvolution()"]');
|
const formEvolutionProgress = document.querySelector('form[onsubmit*="doAcquireEvolution()"]');
|
||||||
const giveAllQEvolutionProgress = document.querySelector(
|
|
||||||
'button[onclick*="addMissingEvolutionProgress()"]'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (datalistEvolutionProgress.length === 0) {
|
if (datalistEvolutionProgress.length === 0) {
|
||||||
formEvolutionProgress.classList.add("disabled");
|
formEvolutionProgress.classList.add("disabled");
|
||||||
formEvolutionProgress.querySelector("input").disabled = true;
|
formEvolutionProgress.querySelector("input").disabled = true;
|
||||||
formEvolutionProgress.querySelector("button").disabled = true;
|
formEvolutionProgress.querySelector("button").disabled = true;
|
||||||
giveAllQEvolutionProgress.disabled = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.CrewShipHarnesses?.length) {
|
if (data.CrewShipHarnesses?.length) {
|
||||||
@ -1534,6 +1537,7 @@ $(document).on("input", "input[list]", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function dispatchAddItemsRequestsBatch(requests) {
|
function dispatchAddItemsRequestsBatch(requests) {
|
||||||
|
return new Promise(resolve => {
|
||||||
revalidateAuthz().then(() => {
|
revalidateAuthz().then(() => {
|
||||||
const req = $.post({
|
const req = $.post({
|
||||||
url: "/custom/addItems?" + window.authz,
|
url: "/custom/addItems?" + window.authz,
|
||||||
@ -1542,6 +1546,8 @@ function dispatchAddItemsRequestsBatch(requests) {
|
|||||||
});
|
});
|
||||||
req.done(() => {
|
req.done(() => {
|
||||||
updateInventory();
|
updateInventory();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1562,7 +1568,7 @@ function addMissingEquipment(categories) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (requests.length != 0 && window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) {
|
if (requests.length != 0 && window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) {
|
||||||
dispatchAddItemsRequestsBatch(requests);
|
return dispatchAddItemsRequestsBatch(requests);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1578,7 +1584,7 @@ function addMissingEvolutionProgress() {
|
|||||||
requests.push({ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 });
|
requests.push({ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 });
|
||||||
});
|
});
|
||||||
if (requests.length != 0 && window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) {
|
if (requests.length != 0 && window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) {
|
||||||
setEvolutionProgress(requests);
|
return setEvolutionProgress(requests);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1805,6 +1811,7 @@ function maturePet(oid, revert) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setEvolutionProgress(requests) {
|
function setEvolutionProgress(requests) {
|
||||||
|
return new Promise(resolve => {
|
||||||
revalidateAuthz().then(() => {
|
revalidateAuthz().then(() => {
|
||||||
const req = $.post({
|
const req = $.post({
|
||||||
url: "/custom/setEvolutionProgress?" + window.authz,
|
url: "/custom/setEvolutionProgress?" + window.authz,
|
||||||
@ -1813,6 +1820,8 @@ function setEvolutionProgress(requests) {
|
|||||||
});
|
});
|
||||||
req.done(() => {
|
req.done(() => {
|
||||||
updateInventory();
|
updateInventory();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -2080,6 +2089,10 @@ 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");
|
||||||
@ -2500,9 +2513,17 @@ function formatDatetime(fmt, date) {
|
|||||||
const calls_in_flight = new Set();
|
const calls_in_flight = new Set();
|
||||||
|
|
||||||
async function debounce(func, ...args) {
|
async function debounce(func, ...args) {
|
||||||
calls_in_flight.add(func);
|
if (!func.name) {
|
||||||
|
throw new Error(`cannot debounce anonymous functions`);
|
||||||
|
}
|
||||||
|
const callid = JSON.stringify({ func: func.name, args });
|
||||||
|
if (!calls_in_flight.has(callid)) {
|
||||||
|
calls_in_flight.add(callid);
|
||||||
await func(...args);
|
await func(...args);
|
||||||
calls_in_flight.delete(func);
|
calls_in_flight.delete(callid);
|
||||||
|
} else {
|
||||||
|
console.log("debouncing", callid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doMaxPlexus() {
|
async function doMaxPlexus() {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
// French translation by Vitruvio
|
// French translation by Vitruvio
|
||||||
dict = {
|
dict = {
|
||||||
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_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_addButton: `Ajouter`,
|
general_addButton: `Ajouter`,
|
||||||
general_setButton: `[UNTRANSLATED] Set`,
|
general_setButton: `Définir`,
|
||||||
general_none: `Aucun`,
|
general_none: `Aucun`,
|
||||||
general_bulkActions: `Action groupée`,
|
general_bulkActions: `Action groupée`,
|
||||||
general_loading: `[UNTRANSLATED] Loading...`,
|
general_loading: `Chargement...`,
|
||||||
|
|
||||||
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: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`,
|
code_succRelog: `Succès. Un redémarrage du jeu est nécessaire.`,
|
||||||
code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`,
|
code_nothingToDo: `Succès.`,
|
||||||
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: `[UNTRANSLATED] Successfully changed.`,
|
code_succChange: `Changement effectué.`,
|
||||||
code_requiredInvigorationUpgrade: `[UNTRANSLATED] You must select both an offensive & defensive upgrade.`,
|
code_requiredInvigorationUpgrade: `Augmentation offensive et défensive requises.`,
|
||||||
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: `[UNTRANSLATED] You can set or remove the Valence Bonus from your weapon.`,
|
detailedView_valenceBonusDescription: `Définir le Bonus Valence de l'arme.`,
|
||||||
detailedView_modularPartsLabel: `[UNTRANSLATED] Change Modular Parts`,
|
detailedView_modularPartsLabel: `Changer l'équipement modulaire`,
|
||||||
detailedView_suitInvigorationLabel: `[UNTRANSLATED] Warframe Invigoration`,
|
detailedView_suitInvigorationLabel: `Invigoration de Warframe`,
|
||||||
|
|
||||||
invigorations_offensive_AbilityStrength: `[UNTRANSLATED] +200% Ability Strength`,
|
invigorations_offensive_AbilityStrength: `+200% de puissance de pouvoir`,
|
||||||
invigorations_offensive_AbilityRange: `[UNTRANSLATED] +100% Ability Range`,
|
invigorations_offensive_AbilityRange: `+100% de portée de pouvoir`,
|
||||||
invigorations_offensive_AbilityDuration: `[UNTRANSLATED] +100% Ability Duration`,
|
invigorations_offensive_AbilityDuration: `+100% de durée de pouvoir`,
|
||||||
invigorations_offensive_MeleeDamage: `[UNTRANSLATED] +250% Melee Damage`,
|
invigorations_offensive_MeleeDamage: `+250% de dégâts de mêlée`,
|
||||||
invigorations_offensive_PrimaryDamage: `[UNTRANSLATED] +250% Primary Damage`,
|
invigorations_offensive_PrimaryDamage: `+250% de dégâts d'arme primaire`,
|
||||||
invigorations_offensive_SecondaryDamage: `[UNTRANSLATED] +250% Secondary Damage`,
|
invigorations_offensive_SecondaryDamage: `+250% de dégâts d'arme secondaire`,
|
||||||
invigorations_offensive_PrimaryCritChance: `[UNTRANSLATED] +200% Primary Critical Chance`,
|
invigorations_offensive_PrimaryCritChance: `+200% de chances critique sur arme primaire`,
|
||||||
invigorations_offensive_SecondaryCritChance: `[UNTRANSLATED] +200% Secondary Critical Chance`,
|
invigorations_offensive_SecondaryCritChance: `+200% de chances critique sur arme secondaire`,
|
||||||
invigorations_offensive_MeleeCritChance: `[UNTRANSLATED] +200% Melee Critical Chance`,
|
invigorations_offensive_MeleeCritChance: `+200% de chances critique en mêlée`,
|
||||||
|
|
||||||
invigorations_utility_AbilityEfficiency: `[UNTRANSLATED] +75% Ability Efficiency`,
|
invigorations_utility_AbilityEfficiency: `+75% d'efficacité de pouvoir`,
|
||||||
invigorations_utility_SprintSpeed: `[UNTRANSLATED] +75% Sprint Speed`,
|
invigorations_utility_SprintSpeed: `+75% de vitesse de course`,
|
||||||
invigorations_utility_ParkourVelocity: `[UNTRANSLATED] +75% Parkour Velocity`,
|
invigorations_utility_ParkourVelocity: `+75% de vélocité de parkour`,
|
||||||
invigorations_utility_HealthMax: `[UNTRANSLATED] +1000 Health`,
|
invigorations_utility_HealthMax: `+1000 de vie`,
|
||||||
invigorations_utility_EnergyMax: `[UNTRANSLATED] +200% Energy Max`,
|
invigorations_utility_EnergyMax: `+200% d'énergie max`,
|
||||||
invigorations_utility_StatusImmune: `[UNTRANSLATED] Immune to Status Effects`,
|
invigorations_utility_StatusImmune: `Immunisé contre les effets de statut`,
|
||||||
invigorations_utility_ReloadSpeed: `[UNTRANSLATED] +75% Reload Speed`,
|
invigorations_utility_ReloadSpeed: `+75% de vitesse de rechargement`,
|
||||||
invigorations_utility_HealthRegen: `[UNTRANSLATED] +25 Health Regen/s`,
|
invigorations_utility_HealthRegen: `+25 de vie régénérés/s`,
|
||||||
invigorations_utility_ArmorMax: `[UNTRANSLATED] +1000 Armor`,
|
invigorations_utility_ArmorMax: `+1000 d'armure`,
|
||||||
invigorations_utility_Jumps: `[UNTRANSLATED] +5 Jump Resets`,
|
invigorations_utility_Jumps: `+5 réinitialisations de saut`,
|
||||||
invigorations_utility_EnergyRegen: `[UNTRANSLATED] +2 Energy Regen/s`,
|
invigorations_utility_EnergyRegen: `+2 d'énergie régénérés/s`,
|
||||||
|
|
||||||
invigorations_offensiveLabel: `[UNTRANSLATED] Offensive Upgrade`,
|
invigorations_offensiveLabel: `Amélioration offensive`,
|
||||||
invigorations_defensiveLabel: `[UNTRANSLATED] Defensive Upgrade`,
|
invigorations_defensiveLabel: `Amélioration défensive`,
|
||||||
invigorations_expiryLabel: `[UNTRANSLATED] Upgrades Expiry (optional)`,
|
invigorations_expiryLabel: `Expiration de l'invigoration (optionnel)`,
|
||||||
|
|
||||||
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: `[UNTRANSLATED] Add Maxed`,
|
mods_addMax: `Ajouter les mods niveau max`,
|
||||||
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: `[UNTRANSLATED] Success. Please note that you'll need to enter a dojo/relay or relog for the client to refresh the star chart.`,
|
cheats_unlockAllMissions_ok: `Succès. Une actualisation de l'inventaire est nécessaire.`,
|
||||||
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: `[UNTRANSLATED] Baro Always Available`,
|
cheats_baroAlwaysAvailable: `Baro toujours présent`,
|
||||||
cheats_baroFullyStocked: `[UNTRANSLATED] Baro Fully Stocked`,
|
cheats_baroFullyStocked: `Stock de Baro au max`,
|
||||||
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: `[UNTRANSLATED] Exceptional Relics Always Give Bronze Reward`,
|
cheats_exceptionalRelicsAlwaysGiveBronzeReward: `Les reliques exceptionnelles donnent toujours une récompense en bronze`,
|
||||||
cheats_flawlessRelicsAlwaysGiveSilverReward: `[UNTRANSLATED] Flawless Relics Always Give Silver Reward`,
|
cheats_flawlessRelicsAlwaysGiveSilverReward: `Les reliques parfaites donnent toujours une récompense en argent`,
|
||||||
cheats_radiantRelicsAlwaysGiveGoldReward: `[UNTRANSLATED] Radiant Relics Always Give Gold Reward`,
|
cheats_radiantRelicsAlwaysGiveGoldReward: `Les reliques éclatantes donnent toujours une récompense en or`,
|
||||||
cheats_unlockAllSimarisResearchEntries: `Débloquer toute les recherches chez Simaris`,
|
cheats_unlockAllSimarisResearchEntries: `Débloquer toute les recherches chez Simaris`,
|
||||||
cheats_disableDailyTribute: `[UNTRANSLATED] Disable Daily Tribute`,
|
cheats_disableDailyTribute: `Désactiver la récompense quotidienne de connexion`,
|
||||||
cheats_spoofMasteryRank: `Rang de maîtrise personnalisé (-1 pour désactiver)`,
|
cheats_spoofMasteryRank: `Rang de maîtrise personnalisé (-1 pour désactiver)`,
|
||||||
cheats_relicRewardItemCountMultiplier: `[UNTRANSLATED] Relic Reward Item Count Multiplier`,
|
cheats_relicRewardItemCountMultiplier: `Multiplicateur de récompenses de relique`,
|
||||||
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: `[UNTRANSLATED] Add Missing Subsumed Abilities`,
|
cheats_addMissingSubsumedAbilities: `Ajouter les capacités subsumées manquantes`,
|
||||||
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: `[UNTRANSLATED] Mark Inbox As Read`,
|
cheats_markAllAsRead: `Marquer la boîte de réception comme lue`,
|
||||||
|
|
||||||
worldState: `[UNTRANSLATED] World State`,
|
worldState: `Carte Solaire`,
|
||||||
worldState_creditBoost: `[UNTRANSLATED] Credit Boost`,
|
worldState_creditBoost: `Booster de Crédit`,
|
||||||
worldState_affinityBoost: `[UNTRANSLATED] Affinity Boost`,
|
worldState_affinityBoost: `Booster d'Affinité`,
|
||||||
worldState_resourceBoost: `[UNTRANSLATED] Resource Boost`,
|
worldState_resourceBoost: `Booster de Ressource`,
|
||||||
worldState_starDays: `[UNTRANSLATED] Star Days`,
|
worldState_starDays: `Jours Stellaires`,
|
||||||
worldState_galleonOfGhouls: `[UNTRANSLATED] Galleon of Ghouls`,
|
worldState_galleonOfGhouls: `Galion des Goules`,
|
||||||
disabled: `[UNTRANSLATED] Disabled`,
|
disabled: `Désactivé`,
|
||||||
worldState_we1: `[UNTRANSLATED] Weekend 1`,
|
worldState_we1: `Weekend 1`,
|
||||||
worldState_we2: `[UNTRANSLATED] Weekend 2`,
|
worldState_we2: `Weekend 2`,
|
||||||
worldState_we3: `[UNTRANSLATED] Weekend 3`,
|
worldState_we3: `Weekend 3`,
|
||||||
worldState_eidolonOverride: `[UNTRANSLATED] Eidolon Override`,
|
worldState_eidolonOverride: `Météo Plaines d'Eidolon`,
|
||||||
worldState_day: `[UNTRANSLATED] Day`,
|
worldState_day: `Jour`,
|
||||||
worldState_night: `[UNTRANSLATED] Night`,
|
worldState_night: `Nuit`,
|
||||||
worldState_vallisOverride: `[UNTRANSLATED] Orb Vallis Override`,
|
worldState_vallisOverride: `Météo Vallée Orbis`,
|
||||||
worldState_warm: `[UNTRANSLATED] Warm`,
|
worldState_warm: `Chaud`,
|
||||||
worldState_cold: `[UNTRANSLATED] Cold`,
|
worldState_cold: `Froid`,
|
||||||
worldState_duviriOverride: `[UNTRANSLATED] Duviri Override`,
|
worldState_duviriOverride: `Spirale Duviri`,
|
||||||
worldState_joy: `[UNTRANSLATED] Joy`,
|
worldState_joy: `Joie`,
|
||||||
worldState_anger: `[UNTRANSLATED] Anger`,
|
worldState_anger: `Colère`,
|
||||||
worldState_envy: `[UNTRANSLATED] Envy`,
|
worldState_envy: `Envie `,
|
||||||
worldState_sorrow: `[UNTRANSLATED] Sorrow`,
|
worldState_sorrow: `hagrin`,
|
||||||
worldState_fear: `[UNTRANSLATED] Fear`,
|
worldState_fear: `Peur`,
|
||||||
worldState_nightwaveOverride: `[UNTRANSLATED] Nightwave Override`,
|
worldState_nightwaveOverride: `Saison d'Ondes Nocturnes`,
|
||||||
worldState_RadioLegionIntermission13Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 9`,
|
worldState_RadioLegionIntermission13Syndicate: `Mix de Nora Vol. 9`,
|
||||||
worldState_RadioLegionIntermission12Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 8`,
|
worldState_RadioLegionIntermission12Syndicate: `Mix de Nora Vol. 8`,
|
||||||
worldState_RadioLegionIntermission11Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 7`,
|
worldState_RadioLegionIntermission11Syndicate: `Mix de Nora Vol. 7`,
|
||||||
worldState_RadioLegionIntermission10Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 6`,
|
worldState_RadioLegionIntermission10Syndicate: `Mix de Nora Vol. 6`,
|
||||||
worldState_RadioLegionIntermission9Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 5`,
|
worldState_RadioLegionIntermission9Syndicate: `Mix de Nora Vol. 5`,
|
||||||
worldState_RadioLegionIntermission8Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 4`,
|
worldState_RadioLegionIntermission8Syndicate: `Mix de Nora Vol. 4`,
|
||||||
worldState_RadioLegionIntermission7Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 3`,
|
worldState_RadioLegionIntermission7Syndicate: `Mix de Nora Vol. 3`,
|
||||||
worldState_RadioLegionIntermission6Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 2`,
|
worldState_RadioLegionIntermission6Syndicate: `Mix de Nora Vol. 2`,
|
||||||
worldState_RadioLegionIntermission5Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 1`,
|
worldState_RadioLegionIntermission5Syndicate: `Mix de Nora Vol. 1`,
|
||||||
worldState_RadioLegionIntermission4Syndicate: `[UNTRANSLATED] Nora's Choice`,
|
worldState_RadioLegionIntermission4Syndicate: `La Sélection de Nora`,
|
||||||
worldState_RadioLegionIntermission3Syndicate: `[UNTRANSLATED] Intermission III`,
|
worldState_RadioLegionIntermission3Syndicate: `Intermission III`,
|
||||||
worldState_RadioLegion3Syndicate: `[UNTRANSLATED] Glassmaker`,
|
worldState_RadioLegion3Syndicate: `Les Mystères du Verre`,
|
||||||
worldState_RadioLegionIntermission2Syndicate: `[UNTRANSLATED] Intermission II`,
|
worldState_RadioLegionIntermission2Syndicate: `Intermission II`,
|
||||||
worldState_RadioLegion2Syndicate: `[UNTRANSLATED] The Emissary`,
|
worldState_RadioLegion2Syndicate: `L'Émissaire`,
|
||||||
worldState_RadioLegionIntermissionSyndicate: `[UNTRANSLATED] Intermission I`,
|
worldState_RadioLegionIntermissionSyndicate: `Intermission I`,
|
||||||
worldState_RadioLegionSyndicate: `[UNTRANSLATED] The Wolf of Saturn Six`,
|
worldState_RadioLegionSyndicate: `Le Loup de Saturne Six`,
|
||||||
worldState_fissures: `[UNTRANSLATED] Fissures`,
|
worldState_fissures: `Fissures`,
|
||||||
normal: `[UNTRANSLATED] Normal`,
|
normal: `Normal`,
|
||||||
worldState_allAtOnceNormal: `[UNTRANSLATED] All At Once, Normal`,
|
worldState_allAtOnceNormal: `Toutes, Normal`,
|
||||||
worldState_allAtOnceSteelPath: `[UNTRANSLATED] All At Once, Steel Path`,
|
worldState_allAtOnceSteelPath: `Toutes, Route de l'Acier`,
|
||||||
worldState_theCircuitOverride: `[UNTRANSLATED] The Circuit Override`,
|
worldState_theCircuitOverride: `Remplacement du Circuit`,
|
||||||
worldState_darvoStockMultiplier: `[UNTRANSLATED] Darvo Stock Multiplier`,
|
worldState_darvoStockMultiplier: `Multiplicateur du stock de Darvo`,
|
||||||
worldState_varziaFullyStocked: `[UNTRANSLATED] Varzia Fully Stocked`,
|
worldState_varziaFullyStocked: `Stock de Varzia au max`,
|
||||||
worldState_varziaOverride: `[UNTRANSLATED] Varzia Rotation Override`,
|
worldState_varziaOverride: `Rotation de Varzia`,
|
||||||
|
|
||||||
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: `Echantillons :`,
|
import_samples: `Échantillons :`,
|
||||||
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: `[UNTRANSLATED] +|VAL1| Health on kill with Blast Damage (Max |VAL2| Health)`,
|
upgrade_WarframeHPBoostFromImpact: `+|VAL1| de vie sur élimination avec des dégâts d'explostion (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: `[UNTRANSLATED] +50% Hacking Retry Chance`,
|
upgrade_OnFailHackReset: `+50% de chance de refaire un piratage`,
|
||||||
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: `[UNTRANSLATED] Next ability cast gains +50% Ability Strength on Mercy`,
|
upgrade_OnExecutionDrainPower: `Le prochain pouvoir activé gagne +50% de puissance de pouvoir après une miséricorde`,
|
||||||
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: `[UNTRANSLATED] +50% Mercy Kill Speed`,
|
upgrade_SwiftExecute: `+50% de vitesse de d'éxecution en miséricorde`,
|
||||||
upgrade_OnHackInvis: `[UNTRANSLATED] Invisible for 15 seconds after Hacking`,
|
upgrade_OnHackInvis: `Invisible pendant 15s après un piratage`,
|
||||||
|
|
||||||
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: `[UNTRANSLATED] Dark Theme`,
|
theme_dark: `Thème sombre`,
|
||||||
theme_light: `[UNTRANSLATED] Light Theme`,
|
theme_light: `Thème clair`,
|
||||||
|
|
||||||
prettier_sucks_ass: ``
|
prettier_sucks_ass: ``
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user