diff --git a/README.md b/README.md index e1ef957a..e8d2b419 100644 --- a/README.md +++ b/README.md @@ -14,4 +14,22 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi - `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. - `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`. -- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE. +- `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift. +- `worldState.vallisOverride` can be set to `warm` or `cold` to lock the temperature on Orb Vallis. +- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values: + - `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9 + - `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8 + - `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7 + - `RadioLegionIntermission10Syndicate` for Nora's Mix Vol. 6 + - `RadioLegionIntermission9Syndicate` for Nora's Mix Vol. 5 + - `RadioLegionIntermission8Syndicate` for Nora's Mix Vol. 4 + - `RadioLegionIntermission7Syndicate` for Nora's Mix Vol. 3 + - `RadioLegionIntermission6Syndicate` for Nora's Mix Vol. 2 + - `RadioLegionIntermission5Syndicate` for Nora's Mix Vol. 1 + - `RadioLegionIntermission4Syndicate` for Nora's Choice + - `RadioLegionIntermission3Syndicate` for Intermission III + - `RadioLegion3Syndicate` for Glassmaker + - `RadioLegionIntermission2Syndicate` for Intermission II + - `RadioLegion2Syndicate` for The Emissary + - `RadioLegionIntermissionSyndicate` for Intermission I + - `RadioLegionSyndicate` for The Wolf of Saturn Six diff --git a/UPDATE AND START SERVER.bat b/UPDATE AND START SERVER.bat index 983b8f8d..7f0bd170 100644 --- a/UPDATE AND START SERVER.bat +++ b/UPDATE AND START SERVER.bat @@ -3,7 +3,7 @@ echo Updating SpaceNinjaServer... git fetch --prune git stash -git reset --hard origin/main +git checkout -f origin/main if exist static\data\0\ ( echo Updating stripped assets... diff --git a/UPDATE AND START SERVER.sh b/UPDATE AND START SERVER.sh new file mode 100755 index 00000000..e57c8061 --- /dev/null +++ b/UPDATE AND START SERVER.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +echo "Updating SpaceNinjaServer..." +git fetch --prune +git stash +git checkout -f origin/main + +if [ -d "static/data/0/" ]; then + echo "Updating stripped assets..." + cd static/data/0/ + git pull + cd ../../../ +fi + +echo "Updating dependencies..." +npm i --omit=dev + +npm run build +if [ $? -eq 0 ]; then + npm run start + echo "SpaceNinjaServer seems to have crashed." +fi + diff --git a/config.json.example b/config.json.example index d9585902..b9ebaab0 100644 --- a/config.json.example +++ b/config.json.example @@ -55,6 +55,8 @@ "affinityBoost": false, "resourceBoost": false, "starDays": true, - "lockTime": 0 + "eidolonOverride": "", + "vallisOverride": "", + "nightwaveOverride": "" } } diff --git a/package-lock.json b/package-lock.json index 59407f15..dc4968db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.64", + "warframe-public-export-plus": "^0.5.66", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" @@ -3814,9 +3814,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.64", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.64.tgz", - "integrity": "sha512-JyHRtYumfwQ1Iog2unzlBWfQHJlZER+iUISquyFFv0Qqtv2QsNzFv2AbV7sCaqgDcE8tw6e5/YqGgfI0m403/g==" + "version": "0.5.66", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.66.tgz", + "integrity": "sha512-AU7XQA96OfYrLm2RioCwDjjdI3IrsmUiqebXyE+bpM0iST+4x/NHu8LTRT4Oygfo/2OBtDYhib7G6re0EeAe5g==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 3eaff1b9..535dfa30 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "morgan": "^1.10.0", "ncp": "^2.0.0", "typescript": "^5.5", - "warframe-public-export-plus": "^0.5.64", + "warframe-public-export-plus": "^0.5.66", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" diff --git a/src/controllers/api/adoptPetController.ts b/src/controllers/api/adoptPetController.ts new file mode 100644 index 00000000..3cd340da --- /dev/null +++ b/src/controllers/api/adoptPetController.ts @@ -0,0 +1,27 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const adoptPetController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "KubrowPets"); + const data = getJSONfromString(String(req.body)); + const details = inventory.KubrowPets.id(data.petId)!.Details!; + details.Name = data.name; + await inventory.save(); + res.json({ + petId: data.petId, + newName: data.name + } satisfies IAdoptPetResponse); +}; + +interface IAdoptPetRequest { + petId: string; + name: string; +} + +interface IAdoptPetResponse { + petId: string; + newName: string; +} diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index e519d170..071a6c9c 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -17,7 +17,7 @@ import { } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; -import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; +import { InventorySlot, IPendingRecipeDatabase, Status } from "@/src/types/inventoryTypes/inventoryTypes"; import { toOid2 } from "@/src/helpers/inventoryHelpers"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { IRecipe } from "warframe-public-export-plus"; @@ -105,7 +105,21 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = ...updateCurrency(inventory, cost, true) }; } - if (recipe.secretIngredientAction != "SIA_UNBRAND") { + + if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") { + const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!; + if (pet.Details!.HatchDate!.getTime() > Date.now()) { + pet.Details!.HatchDate = new Date(); + } + let canSetActive = true; + for (const pet of inventory.KubrowPets) { + if (pet.Details!.Status == Status.StatusAvailable) { + canSetActive = false; + break; + } + } + pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis; + } else if (recipe.secretIngredientAction != "SIA_UNBRAND") { InventoryChanges = { ...InventoryChanges, ...(await addItem( @@ -118,7 +132,10 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = )) }; } - if (config.claimingBlueprintRefundsIngredients) { + if ( + config.claimingBlueprintRefundsIngredients && + recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg + ) { await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe); } await inventory.save(); diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 97da2c65..df04e9d4 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -310,7 +310,7 @@ export const getInventoryResponse = async ( // Fix nemesis for older versions if ( inventoryResponse.Nemesis && - version_compare(getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild, buildLabel) < 0 + version_compare(buildLabel, getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild) < 0 ) { inventoryResponse.Nemesis = undefined; } diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts index 9c305103..8da8fe4d 100644 --- a/src/controllers/api/nemesisController.ts +++ b/src/controllers/api/nemesisController.ts @@ -1,12 +1,18 @@ import { version_compare } from "@/src/helpers/inventoryHelpers"; import { consumeModCharge, + decodeNemesisGuess, encodeNemesisGuess, getInfNodes, getKnifeUpgrade, getNemesisManifest, getNemesisPasscode, getNemesisPasscodeModTypes, + GUESS_CORRECT, + GUESS_INCORRECT, + GUESS_NEUTRAL, + GUESS_NONE, + GUESS_WILDCARD, IKnifeResponse } from "@/src/helpers/nemesisHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers"; @@ -82,7 +88,7 @@ export const nemesisController: RequestHandler = async (req, res) => { } } else { for (let i = 0; i != 3; ++i) { - if (body.guess[i] == passcode[i]) { + if (body.guess[i] == passcode[i] || body.guess[i] == GUESS_WILDCARD) { ++guessResult; } } @@ -97,18 +103,29 @@ export const nemesisController: RequestHandler = async (req, res) => { if (inventory.Nemesis!.Faction == "FC_INFESTATION") { const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf]; const passcode = getNemesisPasscode(inventory.Nemesis!)[0]; - - // Add to GuessHistory - const result1 = passcode == guess[0] ? 0 : 1; - const result2 = passcode == guess[1] ? 0 : 1; - const result3 = passcode == guess[2] ? 0 : 1; + const result1 = passcode == guess[0] ? GUESS_CORRECT : GUESS_INCORRECT; + const result2 = passcode == guess[1] ? GUESS_CORRECT : GUESS_INCORRECT; + const result3 = passcode == guess[2] ? GUESS_CORRECT : GUESS_INCORRECT; inventory.Nemesis!.GuessHistory.push( - encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3) + encodeNemesisGuess([ + { + symbol: guess[0], + result: result1 + }, + { + symbol: guess[1], + result: result2 + }, + { + symbol: guess[2], + result: result3 + } + ]) ); // Increase antivirus if correct antivirus mod is installed const response: IKnifeResponse = {}; - if (result1 == 0 || result2 == 0 || result3 == 0) { + if (result1 == GUESS_CORRECT || result2 == GUESS_CORRECT || result3 == GUESS_CORRECT) { let antivirusGain = 5; const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid); @@ -149,18 +166,48 @@ export const nemesisController: RequestHandler = async (req, res) => { await inventory.save(); res.json(response); } else { - const passcode = getNemesisPasscode(inventory.Nemesis!); - if (passcode[body.position] != body.guess) { - res.end(); - } else { - inventory.Nemesis!.Rank += 1; - inventory.Nemesis!.InfNodes = getInfNodes( - getNemesisManifest(inventory.Nemesis!.manifest), - inventory.Nemesis!.Rank + // For first guess, create a new entry. + if (body.position == 0) { + inventory.Nemesis!.GuessHistory.push( + encodeNemesisGuess([ + { + symbol: GUESS_NONE, + result: GUESS_NEUTRAL + }, + { + symbol: GUESS_NONE, + result: GUESS_NEUTRAL + }, + { + symbol: GUESS_NONE, + result: GUESS_NEUTRAL + } + ]) ); - await inventory.save(); - res.json({ RankIncrease: 1 }); } + + // Evaluate guess + const correct = + body.guess == GUESS_WILDCARD || getNemesisPasscode(inventory.Nemesis!)[body.position] == body.guess; + + // Update entry + const guess = decodeNemesisGuess( + inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] + ); + guess[body.position].symbol = body.guess; + guess[body.position].result = correct ? GUESS_CORRECT : GUESS_INCORRECT; + inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] = encodeNemesisGuess(guess); + + // Increase rank if incorrect + let RankIncrease: number | undefined; + if (!correct) { + RankIncrease = 1; + const manifest = getNemesisManifest(inventory.Nemesis!.manifest); + inventory.Nemesis!.Rank = Math.min(inventory.Nemesis!.Rank + 1, manifest.systemIndexes.length - 1); + inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank); + } + await inventory.save(); + res.json({ RankIncrease }); } } else if ((req.query.mode as string) == "rs") { // report spawn; POST but no application data in body @@ -170,11 +217,14 @@ export const nemesisController: RequestHandler = async (req, res) => { res.json({ LastEnc: inventory.Nemesis!.LastEnc }); } else if ((req.query.mode as string) == "s") { const inventory = await getInventory(account._id.toString(), "Nemesis"); + if (inventory.Nemesis) { + logger.warn(`overwriting an existing nemesis as a new one is being requested`); + } const body = getJSONfromString(String(req.body)); body.target.fp = BigInt(body.target.fp); const manifest = getNemesisManifest(body.target.manifest); - if (account.BuildLabel && version_compare(manifest.minBuild, account.BuildLabel) < 0) { + if (account.BuildLabel && version_compare(account.BuildLabel, manifest.minBuild) < 0) { logger.warn( `client on version ${account.BuildLabel} provided nemesis manifest ${body.target.manifest} which was expected to require ${manifest.minBuild} or above. please file a bug report.` ); @@ -185,13 +235,15 @@ export const nemesisController: RequestHandler = async (req, res) => { const weapons: readonly string[] = manifest.weapons; const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1); weaponIdx = initialWeaponIdx; - do { - const weapon = weapons[weaponIdx]; - if (body.target.DisallowedWeapons.indexOf(weapon) == -1) { - break; - } - weaponIdx = (weaponIdx + 1) % weapons.length; - } while (weaponIdx != initialWeaponIdx); + if (body.target.DisallowedWeapons) { + do { + const weapon = weapons[weaponIdx]; + if (body.target.DisallowedWeapons.indexOf(weapon) == -1) { + break; + } + weaponIdx = (weaponIdx + 1) % weapons.length; + } while (weaponIdx != initialWeaponIdx); + } } inventory.Nemesis = { @@ -212,10 +264,10 @@ export const nemesisController: RequestHandler = async (req, res) => { GuessHistory: [], Hints: [], HintProgress: 0, - Weakened: body.target.Weakened, + Weakened: false, PrevOwners: 0, HenchmenKilled: 0, - SecondInCommand: body.target.SecondInCommand, + SecondInCommand: false, MissionCount: 0, LastEnc: 0 }; @@ -276,7 +328,7 @@ interface INemesisStartRequest { KillingSuit: string; killingDamageType: number; ShoulderHelmet: string; - DisallowedWeapons: string[]; + DisallowedWeapons?: string[]; WeaponIdx: number; AgentIdx: number; BirthNode: string; diff --git a/src/controllers/api/renamePetController.ts b/src/controllers/api/renamePetController.ts new file mode 100644 index 00000000..6672d064 --- /dev/null +++ b/src/controllers/api/renamePetController.ts @@ -0,0 +1,23 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const renamePetController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "KubrowPets PremiumCredits PremiumCreditsFree"); + const data = getJSONfromString(String(req.body)); + const details = inventory.KubrowPets.id(data.petId)!.Details!; + details.Name = data.name; + const currencyChanges = updateCurrency(inventory, 15, true); + await inventory.save(); + res.json({ + ...data, + inventoryChanges: currencyChanges + }); +}; + +interface IRenamePetRequest { + petId: string; + name: string; +} diff --git a/src/controllers/api/startRecipeController.ts b/src/controllers/api/startRecipeController.ts index 78adf1fc..8f68fc28 100644 --- a/src/controllers/api/startRecipeController.ts +++ b/src/controllers/api/startRecipeController.ts @@ -3,12 +3,14 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { logger } from "@/src/utils/logger"; import { RequestHandler } from "express"; import { getRecipe } from "@/src/services/itemDataService"; -import { addItem, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { addItem, addKubrowPet, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { Types } from "mongoose"; import { InventorySlot, ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes"; -import { toOid } from "@/src/helpers/inventoryHelpers"; +import { fromOid, toOid } from "@/src/helpers/inventoryHelpers"; import { ExportWeapons } from "warframe-public-export-plus"; +import { getRandomElement } from "@/src/services/rngService"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; interface IStartRecipeRequest { RecipeName: string; @@ -42,24 +44,35 @@ export const startRecipeController: RequestHandler = async (req, res) => { for (let i = 0; i != recipe.ingredients.length; ++i) { if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") { - const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory; - if (category != "LongGuns" && category != "Pistols" && category != "Melee") { - throw new Error(`unexpected equipment ingredient type: ${category}`); + if (recipe.ingredients[i].ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") { + const index = inventory.KubrowPetEggs!.findIndex(x => x._id.equals(startRecipeRequest.Ids[i])); + if (index != -1) { + inventory.KubrowPetEggs!.splice(index, 1); + } + } else { + const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory; + if (category != "LongGuns" && category != "Pistols" && category != "Melee") { + throw new Error(`unexpected equipment ingredient type: ${category}`); + } + const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i])); + if (equipmentIndex == -1) { + throw new Error(`could not find equipment item to use for recipe`); + } + pr[category] ??= []; + pr[category].push(inventory[category][equipmentIndex]); + inventory[category].splice(equipmentIndex, 1); + freeUpSlot(inventory, InventorySlot.WEAPONS); } - const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i])); - if (equipmentIndex == -1) { - throw new Error(`could not find equipment item to use for recipe`); - } - pr[category] ??= []; - pr[category].push(inventory[category][equipmentIndex]); - inventory[category].splice(equipmentIndex, 1); - freeUpSlot(inventory, InventorySlot.WEAPONS); } else { await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1); } } - if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { + let inventoryChanges: IInventoryChanges | undefined; + if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") { + inventoryChanges = addKubrowPet(inventory, getRandomElement(recipe.secretIngredients!)!.ItemType); + pr.KubrowPet = new Types.ObjectId(fromOid(inventoryChanges.KubrowPets![0].ItemId)); + } else if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { const spectreLoadout: ISpectreLoadout = { ItemType: recipe.resultType, Suits: "", @@ -116,5 +129,5 @@ export const startRecipeController: RequestHandler = async (req, res) => { await inventory.save(); - res.json({ RecipeId: toOid(pr._id) }); + res.json({ RecipeId: toOid(pr._id), InventoryChanges: inventoryChanges }); }; diff --git a/src/controllers/api/syndicateSacrificeController.ts b/src/controllers/api/syndicateSacrificeController.ts index 8b1a9c7a..021608b3 100644 --- a/src/controllers/api/syndicateSacrificeController.ts +++ b/src/controllers/api/syndicateSacrificeController.ts @@ -3,7 +3,7 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; -import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { addMiscItem, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { toStoreItem } from "@/src/services/itemDataService"; import { logger } from "@/src/utils/logger"; @@ -18,80 +18,83 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: data.AffiliationTag, Standing: 0 }) - 1]; } - const level = data.SacrificeLevel - (syndicate.Title ?? 0); + const oldLevel = syndicate.Title ?? 0; + const levelIncrease = data.SacrificeLevel - oldLevel; + if (levelIncrease < 1) { + throw new Error(`syndicate sacrifice needs an increase of at least 1`); + } + if (levelIncrease > 1 && !data.AllowMultiple) { + throw new Error(`desired syndicate level is an increase of ${levelIncrease}, max. allowed increase is 1`); + } + const res: ISyndicateSacrificeResponse = { AffiliationTag: data.AffiliationTag, InventoryChanges: {}, Level: data.SacrificeLevel, - LevelIncrease: level <= 0 ? 1 : level, + LevelIncrease: levelIncrease, NewEpisodeReward: false }; + // Process sacrifices and rewards for every level we're reaching const manifest = ExportSyndicates[data.AffiliationTag]; - let sacrifice: ISyndicateSacrifice | undefined; - let reward: string | undefined; - if (data.SacrificeLevel == 0) { - sacrifice = manifest.initiationSacrifice; - reward = manifest.initiationReward; - syndicate.Initiated = true; - } else { - sacrifice = manifest.titles?.find(x => x.level == data.SacrificeLevel)?.sacrifice; - } - - if (sacrifice) { - res.InventoryChanges = { ...updateCurrency(inventory, sacrifice.credits, false) }; - - const miscItemChanges = sacrifice.items.map(x => ({ - ItemType: x.ItemType, - ItemCount: x.ItemCount * -1 - })); - addMiscItems(inventory, miscItemChanges); - res.InventoryChanges.MiscItems = miscItemChanges; - } - - syndicate.Title ??= 0; - syndicate.Title += 1; - - if (reward) { - combineInventoryChanges( - res.InventoryChanges, - (await handleStoreItemAcquisition(reward, inventory)).InventoryChanges - ); - } - - // Quacks like a nightwave syndicate? - if (manifest.dailyChallenges) { - const title = manifest.titles!.find(x => x.level == syndicate.Title); - if (title) { - res.NewEpisodeReward = true; - let rewardType: string; - let rewardCount: number; - if (title.storeItemReward) { - rewardType = title.storeItemReward; - rewardCount = 1; - } else { - rewardType = toStoreItem(title.reward!.ItemType); - rewardCount = title.reward!.ItemCount; + for (let level = oldLevel + 1; level <= data.SacrificeLevel; ++level) { + let sacrifice: ISyndicateSacrifice | undefined; + if (level == 0) { + sacrifice = manifest.initiationSacrifice; + if (manifest.initiationReward) { + combineInventoryChanges( + res.InventoryChanges, + (await handleStoreItemAcquisition(manifest.initiationReward, inventory)).InventoryChanges + ); } - const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount)) - .InventoryChanges; - if (Object.keys(rewardInventoryChanges).length == 0) { - logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`); - const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType; - rewardInventoryChanges.MiscItems = [{ ItemType: nightwaveCredsItemType, ItemCount: 50 }]; - addMiscItems(inventory, rewardInventoryChanges.MiscItems); - } - combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges); + syndicate.Initiated = true; + } else { + sacrifice = manifest.titles?.find(x => x.level == level)?.sacrifice; } - } else { - if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) { - syndicate.FreeFavorsEarned ??= []; - if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) { - syndicate.FreeFavorsEarned.push(syndicate.Title); + + if (sacrifice) { + updateCurrency(inventory, sacrifice.credits, false, res.InventoryChanges); + + for (const item of sacrifice.items) { + addMiscItem(inventory, item.ItemType, item.ItemCount * -1, res.InventoryChanges); + } + } + + // Quacks like a nightwave syndicate? + if (manifest.dailyChallenges) { + const title = manifest.titles!.find(x => x.level == level); + if (title) { + res.NewEpisodeReward = true; + let rewardType: string; + let rewardCount: number; + if (title.storeItemReward) { + rewardType = title.storeItemReward; + rewardCount = 1; + } else { + rewardType = toStoreItem(title.reward!.ItemType); + rewardCount = title.reward!.ItemCount; + } + const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount)) + .InventoryChanges; + if (Object.keys(rewardInventoryChanges).length == 0) { + logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`); + const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType; + addMiscItem(inventory, nightwaveCredsItemType, 50, rewardInventoryChanges); + } + combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges); + } + } else { + if (level > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == level)) { + syndicate.FreeFavorsEarned ??= []; + if (!syndicate.FreeFavorsEarned.includes(level)) { + syndicate.FreeFavorsEarned.push(level); + } } } } + // Commit + syndicate.Title = data.SacrificeLevel; await inventory.save(); response.json(res); diff --git a/src/controllers/api/trainingResultController.ts b/src/controllers/api/trainingResultController.ts index f44d7dae..0e436386 100644 --- a/src/controllers/api/trainingResultController.ts +++ b/src/controllers/api/trainingResultController.ts @@ -35,6 +35,17 @@ const trainingResultController: RequestHandler = async (req, res): Promise inventory.PlayerLevel += 1; inventory.TradesRemaining += 1; + if (inventory.PlayerLevel == 2) { + await createMessage(accountId, [ + { + sndr: "/Lotus/Language/Game/Maroo", + msg: "/Lotus/Language/Clan/MarooClanSearchDesc", + sub: "/Lotus/Language/Clan/MarooClanSearchTitle", + icon: "/Lotus/Interface/Icons/Npcs/Maroo.png" + } + ]); + } + await createMessage(accountId, [ { sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index 716104a5..4f03ee94 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -25,6 +25,7 @@ import allIncarnons from "@/static/fixed_responses/allIncarnonList.json"; interface ListedItem { uniqueName: string; name: string; + subtype?: string; fusionLimit?: number; exalted?: string[]; badReason?: "starter" | "frivolous" | "notraw"; @@ -175,7 +176,8 @@ const getItemListsController: RequestHandler = (req, response) => { ) { res.miscitems.push({ uniqueName: uniqueName, - name: name + name: name, + subtype: "Resource" }); } } @@ -193,7 +195,8 @@ const getItemListsController: RequestHandler = (req, response) => { for (const [uniqueName, item] of Object.entries(ExportGear)) { res.miscitems.push({ uniqueName: uniqueName, - name: getString(item.name, lang) + name: getString(item.name, lang), + subtype: "Gear" }); } const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang); diff --git a/src/helpers/nemesisHelpers.ts b/src/helpers/nemesisHelpers.ts index 9d6a7a2b..8d0d8527 100644 --- a/src/helpers/nemesisHelpers.ts +++ b/src/helpers/nemesisHelpers.ts @@ -237,7 +237,7 @@ export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFacti return passcode; }; -const reqiuemMods: readonly string[] = [ +const requiemMods: readonly string[] = [ "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod", @@ -263,29 +263,51 @@ export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNeme const passcode = getNemesisPasscode(nemesis); return nemesis.Faction == "FC_INFESTATION" ? passcode.map(i => antivirusMods[i]) - : passcode.map(i => reqiuemMods[i]); + : passcode.map(i => requiemMods[i]); }; -export const encodeNemesisGuess = ( - symbol1: number, - result1: number, - symbol2: number, - result2: number, - symbol3: number, - result3: number -): number => { +// Symbols; 0-7 are the normal requiem mods. +export const GUESS_NONE = 8; +export const GUESS_WILDCARD = 9; + +// Results; there are 3, 4, 5 as well which are more muted versions but unused afaik. +export const GUESS_NEUTRAL = 0; +export const GUESS_INCORRECT = 1; +export const GUESS_CORRECT = 2; + +interface NemesisPositionGuess { + symbol: number; + result: number; +} + +export type NemesisGuess = [NemesisPositionGuess, NemesisPositionGuess, NemesisPositionGuess]; + +export const encodeNemesisGuess = (guess: NemesisGuess): number => { return ( - (symbol1 & 0xf) | - ((result1 & 3) << 12) | - ((symbol2 << 4) & 0xff) | - ((result2 << 14) & 0xffff) | - ((symbol3 & 0xf) << 8) | - ((result3 & 3) << 16) + (guess[0].symbol & 0xf) | + ((guess[0].result & 3) << 12) | + ((guess[1].symbol << 4) & 0xff) | + ((guess[1].result << 14) & 0xffff) | + ((guess[2].symbol & 0xf) << 8) | + ((guess[2].result & 3) << 16) ); }; -export const decodeNemesisGuess = (val: number): number[] => { - return [val & 0xf, (val >> 12) & 3, (val & 0xff) >> 4, (val & 0xffff) >> 14, (val >> 8) & 0xf, (val >> 16) & 3]; +export const decodeNemesisGuess = (val: number): NemesisGuess => { + return [ + { + symbol: val & 0xf, + result: (val >> 12) & 3 + }, + { + symbol: (val & 0xff) >> 4, + result: (val & 0xffff) >> 14 + }, + { + symbol: (val >> 8) & 0xf, + result: (val >> 16) & 3 + } + ]; }; export interface IKnifeResponse { diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 27caf691..2e4008dd 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -1097,7 +1097,8 @@ const pendingRecipeSchema = new Schema( LongGuns: { type: [EquipmentSchema], default: undefined }, Pistols: { type: [EquipmentSchema], default: undefined }, Melee: { type: [EquipmentSchema], default: undefined }, - SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined } + SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined }, + KubrowPet: { type: Schema.Types.ObjectId, default: undefined } }, { id: false } ); @@ -1115,6 +1116,7 @@ pendingRecipeSchema.set("toJSON", { delete returnedObject.Pistols; delete returnedObject.Melees; delete returnedObject.SuitToUnbrand; + delete returnedObject.KubrowPet; (returnedObject as IPendingRecipeClient).CompletionDate = { $date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() } }; diff --git a/src/routes/api.ts b/src/routes/api.ts index 82b42842..a12efbd0 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -9,6 +9,7 @@ import { addIgnoredUserController } from "@/src/controllers/api/addIgnoredUserCo import { addPendingFriendController } from "@/src/controllers/api/addPendingFriendController"; import { addToAllianceController } from "@/src/controllers/api/addToAllianceController"; import { addToGuildController } from "@/src/controllers/api/addToGuildController"; +import { adoptPetController } from "@/src/controllers/api/adoptPetController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; import { archonFusionController } from "@/src/controllers/api/archonFusionController"; import { artifactsController } from "@/src/controllers/api/artifactsController"; @@ -107,6 +108,7 @@ import { removeFriendGetController, removeFriendPostController } from "@/src/con import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController"; import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController"; +import { renamePetController } from "@/src/controllers/api/renamePetController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController"; import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; @@ -225,6 +227,7 @@ apiRouter.post("/addIgnoredUser.php", addIgnoredUserController); apiRouter.post("/addPendingFriend.php", addPendingFriendController); apiRouter.post("/addToAlliance.php", addToAllianceController); apiRouter.post("/addToGuild.php", addToGuildController); +apiRouter.post("/adoptPet.php", adoptPetController); apiRouter.post("/arcaneCommon.php", arcaneCommonController); apiRouter.post("/archonFusion.php", archonFusionController); apiRouter.post("/artifacts.php", artifactsController); @@ -294,6 +297,7 @@ apiRouter.post("/releasePet.php", releasePetController); apiRouter.post("/removeFriend.php", removeFriendPostController); apiRouter.post("/removeFromGuild.php", removeFromGuildController); apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController); +apiRouter.post("/renamePet.php", renamePetController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController); apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController); apiRouter.post("/saveDialogue.php", saveDialogueController); diff --git a/src/services/configService.ts b/src/services/configService.ts index 6a42481b..dc8a78bc 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -61,7 +61,9 @@ interface IConfig { affinityBoost?: boolean; resourceBoost?: boolean; starDays?: boolean; - lockTime?: number; + eidolonOverride?: string; + vallisOverride?: string; + nightwaveOverride?: string; }; } diff --git a/src/services/configWatcherService.ts b/src/services/configWatcherService.ts index 46c236ea..77c56d9a 100644 --- a/src/services/configWatcherService.ts +++ b/src/services/configWatcherService.ts @@ -27,9 +27,21 @@ fs.watchFile(configPath, () => { }); export const validateConfig = (): void => { - if (typeof config.administratorNames == "string") { - logger.info(`Updating config.json to make administratorNames an array.`); - config.administratorNames = [config.administratorNames]; + let modified = false; + if (config.administratorNames) { + if (!Array.isArray(config.administratorNames)) { + config.administratorNames = [config.administratorNames]; + modified = true; + } + for (let i = 0; i != config.administratorNames.length; ++i) { + if (typeof config.administratorNames[i] != "string") { + config.administratorNames[i] = String(config.administratorNames[i]); + modified = true; + } + } + } + if (modified) { + logger.info(`Updating config.json to fix some issues with it.`); void saveConfig(); } }; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 4ccd9c5c..5194f771 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -86,6 +86,7 @@ import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; import { getNightwaveSyndicateTag, getWorldState } from "./worldStateService"; import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers"; import { TAccountDocument } from "./loginService"; +import { unixTimesInMs } from "../constants/timeConstants"; export const createInventory = async ( accountOwnerId: Types.ObjectId, @@ -722,6 +723,10 @@ export const addItem = async ( } break; + case "Boons": + // Can purchase /Lotus/Upgrades/Boons/DuviriVendorBoonItem from Acrithis, doesn't need to be added to inventory. + return {}; + case "Stickers": { const entry = inventory.RawUpgrades.find(x => x.ItemType == typeName); @@ -776,7 +781,9 @@ export const addItem = async ( typeName.substr(1).split("/")[3] == "CatbrowPet" || typeName.substr(1).split("/")[3] == "KubrowPet" ) { - return addKubrowPet(inventory, typeName, undefined, premiumPurchase); + if (typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") { + return addKubrowPet(inventory, typeName, undefined, premiumPurchase); + } } else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) { if (!seed) { throw new Error(`Expected crew member to have a seed`); @@ -791,6 +798,12 @@ export const addItem = async ( } break; } + case "Items": { + if (typeName.substr(1).split("/")[3] == "Emotes") { + return addCustomization(inventory, typeName); + } + break; + } case "NeutralCreatures": { if (inventory.Horses.length != 0) { logger.warn("refusing to add Horse because account already has one"); @@ -1015,12 +1028,13 @@ export const addSpaceSuit = ( export const addKubrowPet = ( inventory: TInventoryDatabaseDocument, kubrowPetName: string, - details: IKubrowPetDetailsDatabase | undefined, - premiumPurchase: boolean, + details?: IKubrowPetDetailsDatabase, + premiumPurchase: boolean = false, inventoryChanges: IInventoryChanges = {} ): IInventoryChanges => { combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase)); + // TODO: When incubating, this should only be given when claiming the recipe. const kubrowPet = ExportSentinels[kubrowPetName] as ISentinel | undefined; const exalted = kubrowPet?.exalted ?? []; for (const specialItem of exalted) { @@ -1069,11 +1083,11 @@ export const addKubrowPet = ( details = { Name: "", - IsPuppy: false, + IsPuppy: !premiumPurchase, HasCollar: true, - PrintsRemaining: 2, - Status: Status.StatusStasis, - HatchDate: new Date(Math.trunc(Date.now() / 86400000) * 86400000), + PrintsRemaining: 3, + Status: premiumPurchase ? Status.StatusStasis : Status.StatusIncubating, + HatchDate: premiumPurchase ? new Date() : new Date(Date.now() + 10 * unixTimesInMs.hour), // On live, this seems to be somewhat randomised so that the pet hatches 9~11 hours after start. IsMale: !!getRandomInt(0, 1), Size: getRandomInt(70, 100) / 100, DominantTraits: traits, @@ -1511,7 +1525,8 @@ export const applyClientEquipmentUpdates = ( gearArray.forEach(({ ItemId, XP, InfestationDate }) => { const item = category.id(fromOid(ItemId)); if (!item) { - throw new Error(`No item with id ${fromOid(ItemId)} in ${categoryName}`); + logger.warn(`Skipping unknown ${categoryName} item: id ${fromOid(ItemId)} not found`); + return; } if (XP) { diff --git a/src/services/itemDataService.ts b/src/services/itemDataService.ts index 3b3e997c..0e862283 100644 --- a/src/services/itemDataService.ts +++ b/src/services/itemDataService.ts @@ -1,5 +1,4 @@ import { IKeyChainRequest } from "@/src/types/requestTypes"; -import { getIndexAfter } from "@/src/helpers/stringHelpers"; import { dict_de, dict_en, @@ -53,20 +52,32 @@ export const getRecipeByResult = (resultType: string): IRecipe | undefined => { return Object.values(ExportRecipes).find(x => x.resultType == resultType); }; -export const getItemCategoryByUniqueName = (uniqueName: string): string => { - //Lotus/Types/Items/MiscItems/PolymerBundle - - let splitWord = "Items/"; - if (!uniqueName.includes("/Items/")) { - splitWord = "/Types/"; +export const getItemCategoryByUniqueName = (uniqueName: string): string | undefined => { + if (uniqueName in ExportCustoms) { + return ExportCustoms[uniqueName].productCategory; } - - const index = getIndexAfter(uniqueName, splitWord); - if (index === -1) { - throw new Error(`error parsing item category ${uniqueName}`); + if (uniqueName in ExportDrones) { + return "Drones"; } - const category = uniqueName.substring(index).split("/")[0]; - return category; + if (uniqueName in ExportKeys) { + return "LevelKeys"; + } + if (uniqueName in ExportGear) { + return "Consumables"; + } + if (uniqueName in ExportResources) { + return ExportResources[uniqueName].productCategory; + } + if (uniqueName in ExportSentinels) { + return ExportSentinels[uniqueName].productCategory; + } + if (uniqueName in ExportWarframes) { + return ExportWarframes[uniqueName].productCategory; + } + if (uniqueName in ExportWeapons) { + return ExportWeapons[uniqueName].productCategory; + } + return undefined; }; export const getItemName = (uniqueName: string): string | undefined => { @@ -222,7 +233,7 @@ export const isStoreItem = (type: string): boolean => { }; export const toStoreItem = (type: string): string => { - if (type.startsWith("/Lotus/Types/StoreItems/Boosters/")) { + if (type.startsWith("/Lotus/Types/Boosters/")) { const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == type); if (boosterEntry) { return boosterEntry[0]; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 908be883..21aff3b1 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -326,8 +326,8 @@ export const addMissionInventoryUpdates = async ( break; } case "PlayerSkillGains": { - inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE; - inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER; + inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE ?? 0; + inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER ?? 0; break; } case "CustomMarkers": { @@ -1182,14 +1182,12 @@ export const addMissionRewards = async ( if (nodeIndex !== -1) inventory.Nemesis.InfNodes.splice(nodeIndex, 1); if (inventory.Nemesis.InfNodes.length <= 0) { + const manifest = getNemesisManifest(inventory.Nemesis.manifest); if (inventory.Nemesis.Faction != "FC_INFESTATION") { - inventory.Nemesis.Rank = Math.min(inventory.Nemesis.Rank + 1, 4); + inventory.Nemesis.Rank = Math.min(inventory.Nemesis.Rank + 1, manifest.systemIndexes.length - 1); inventoryChanges.Nemesis.Rank = inventory.Nemesis.Rank; } - inventory.Nemesis.InfNodes = getInfNodes( - getNemesisManifest(inventory.Nemesis.manifest), - inventory.Nemesis.Rank - ); + inventory.Nemesis.InfNodes = getInfNodes(manifest, inventory.Nemesis.Rank); } if (inventory.Nemesis.Faction == "FC_INFESTATION") { @@ -1207,7 +1205,9 @@ export const addMissionRewards = async ( // eslint-disable-next-line @typescript-eslint/no-unused-vars const [jobType, unkIndex, hubNode, syndicateMissionId, locationTag] = rewardInfo.jobId.split("_"); const syndicateMissions: ISyndicateMissionInfo[] = []; - pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + if (syndicateMissionId) { + pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + } const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId); if (syndicateEntry && syndicateEntry.Jobs) { let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!]; @@ -1556,7 +1556,9 @@ function getRandomMissionDrops( let isEndlessJob = false; if (syndicateMissionId) { const syndicateMissions: ISyndicateMissionInfo[] = []; - pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + if (syndicateMissionId) { + pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId)); + } const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId); if (syndicateEntry && syndicateEntry.Jobs) { let job = syndicateEntry.Jobs[RewardInfo.JobTier!]; diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index 89154147..0282c562 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -303,7 +303,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani } const cycleStart = cycleOffset + cycleIndex * cycleDuration; for (const rawItem of offersToAdd) { - const durationHoursRange = toRange(rawItem.durationHours); + const durationHoursRange = toRange(rawItem.durationHours ?? cycleDuration); const expiry = cycleStart + rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour; diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 7d91e027..e2041907 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -19,6 +19,7 @@ import { IWorldState } from "../types/worldStateTypes"; import { version_compare } from "../helpers/inventoryHelpers"; +import { logger } from "../utils/logger"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -166,8 +167,8 @@ const microplanetEndlessJobs = [ const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 -const isBeforeNextExpectedWorldStateRefresh = (date: number): boolean => { - return Date.now() + 300_000 > date; +const isBeforeNextExpectedWorldStateRefresh = (nowMs: number, thenMs: number): boolean => { + return nowMs + 300_000 > thenMs; }; const getSortieTime = (day: number): number => { @@ -348,6 +349,7 @@ interface IRotatingSeasonChallengePools { daily: string[]; weekly: string[]; hardWeekly: string[]; + hasWeeklyPermanent: boolean; } const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => { @@ -359,7 +361,12 @@ const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallenge x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") && !x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent") ), - hardWeekly: syndicate.weeklyChallenges!.filter(x => x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")) + hardWeekly: syndicate.weeklyChallenges!.filter(x => + x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/") + ), + hasWeeklyPermanent: !!syndicate.weeklyChallenges!.find(x => + x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent") + ) }; }; @@ -416,26 +423,34 @@ const pushWeeklyActs = ( activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 0)); activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 1)); - activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 2)); - activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 3)); - activeChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" - }); - activeChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" - }); - activeChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" - }); + if (pools.hasWeeklyPermanent) { + activeChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + }); + activeChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + }); + activeChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + }); + activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 2)); + activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 3)); + } else { + activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 2)); + activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 3)); + activeChallenges.push(getSeasonWeeklyChallenge(pools, week, 4)); + activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 5)); + activeChallenges.push(getSeasonWeeklyHardChallenge(pools, week, 6)); + } }; export const pushClassicBounties = (syndicateMissions: ISyndicateMissionInfo[], bountyCycle: number): void => { @@ -924,15 +939,70 @@ const getCalendarSeason = (week: number): ICalendarSeason => { }; }; +const doesTimeSatsifyConstraints = (timeSecs: number): boolean => { + if (config.worldState?.eidolonOverride) { + const eidolonEpoch = 1391992660; + const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000); + const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000; + const eidolonCycleEnd = eidolonCycleStart + 9000; + const eidolonCycleNightStart = eidolonCycleEnd - 3000; + if (config.worldState.eidolonOverride == "day") { + if ( + //timeSecs < eidolonCycleStart || + isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleNightStart * 1000) + ) { + return false; + } + } else { + if ( + timeSecs < eidolonCycleNightStart || + isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleEnd * 1000) + ) { + return false; + } + } + } + + if (config.worldState?.vallisOverride) { + const vallisEpoch = 1541837628; + const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600); + const vallisCycleStart = vallisEpoch + vallisCycle * 1600; + const vallisCycleEnd = vallisCycleStart + 1600; + const vallisCycleColdStart = vallisCycleStart + 400; + if (config.worldState.vallisOverride == "cold") { + if ( + timeSecs < vallisCycleColdStart || + isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleEnd * 1000) + ) { + return false; + } + } else { + if ( + //timeSecs < vallisCycleStart || + isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleColdStart * 1000) + ) { + return false; + } + } + } + + return true; +}; + export const getWorldState = (buildLabel?: string): IWorldState => { - const day = Math.trunc((Date.now() - EPOCH) / 86400000); + let timeSecs = Math.round(Date.now() / 1000); + while (!doesTimeSatsifyConstraints(timeSecs)) { + timeSecs -= 60; + } + const timeMs = timeSecs * 1000; + const day = Math.trunc((timeMs - EPOCH) / 86400000); const week = Math.trunc(day / 7); const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; const worldState: IWorldState = { BuildLabel: typeof buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel, - Time: config.worldState?.lockTime || Math.round(Date.now() / 1000), + Time: timeSecs, Goals: [], Alerts: [], Sorties: [], @@ -986,11 +1056,11 @@ export const getWorldState = (buildLabel?: string): IWorldState => { worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 2)); worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 1)); worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day - 0)); - if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) { + if (isBeforeNextExpectedWorldStateRefresh(timeMs, EPOCH + (day + 1) * 86400000)) { worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1)); } pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week); - if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) { pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week + 1); } } @@ -999,7 +1069,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new SRng(week).randomInt(0, 0xff_ffff); // Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation - let bountyCycle = Math.trunc(Date.now() / 9000000); + let bountyCycle = Math.trunc(timeSecs / 9000); let bountyCycleEnd: number | undefined; do { const bountyCycleStart = bountyCycle * 9000000; @@ -1030,7 +1100,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }); pushClassicBounties(worldState.SyndicateMissions, bountyCycle); - } while (isBeforeNextExpectedWorldStateRefresh(bountyCycleEnd) && ++bountyCycle); + } while (isBeforeNextExpectedWorldStateRefresh(timeMs, bountyCycleEnd) && ++bountyCycle); if (config.worldState?.creditBoost) { worldState.GlobalUpgrades.push({ @@ -1073,15 +1143,15 @@ export const getWorldState = (buildLabel?: string): IWorldState => { { const rollover = getSortieTime(day); - if (Date.now() < rollover) { + if (timeMs < rollover) { worldState.Sorties.push(getSortie(day - 1)); } - if (isBeforeNextExpectedWorldStateRefresh(rollover)) { + if (isBeforeNextExpectedWorldStateRefresh(timeMs, rollover)) { worldState.Sorties.push(getSortie(day)); } // The client does not seem to respect activation for classic syndicate missions, so only pushing current ones. - const sdy = Date.now() >= rollover ? day : day - 1; + const sdy = timeMs >= rollover ? day : day - 1; const rng = new SRng(sdy); pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa48049", "ArbitersSyndicate"); pushSyndicateMissions(worldState, sdy, rng.randomInt(0, 100_000), "ba6f84724fa4804a", "CephalonSudaSyndicate"); @@ -1093,7 +1163,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { // Archon Hunt cycling every week worldState.LiteSorties.push(getLiteSortie(week)); - if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) { worldState.LiteSorties.push(getLiteSortie(week + 1)); } @@ -1130,12 +1200,12 @@ export const getWorldState = (buildLabel?: string): IWorldState => { // 1999 Calendar Season cycling every week + YearIteration every 4 weeks worldState.KnownCalendarSeasons.push(getCalendarSeason(week)); - if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) { worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1)); } // Sentient Anomaly cycling every 30 minutes - const halfHour = Math.trunc(Date.now() / (unixTimesInMs.hour / 2)); + const halfHour = Math.trunc(timeMs / (unixTimesInMs.hour / 2)); const tmp = { cavabegin: "1690761600", PurchasePlatformLockEnabled: true, @@ -1258,6 +1328,15 @@ export const isArchwingMission = (node: IRegion): boolean => { }; export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string | undefined => { + if (config.worldState?.nightwaveOverride) { + if (config.worldState.nightwaveOverride in nightwaveTagToSeason) { + return config.worldState.nightwaveOverride; + } + logger.warn(`ignoring invalid config value for worldState.nightwaveOverride`, { + value: config.worldState.nightwaveOverride, + valid_values: Object.keys(nightwaveTagToSeason) + }); + } if (!buildLabel || version_compare(buildLabel, "2025.05.20.10.18") >= 0) { return "RadioLegionIntermission13Syndicate"; } @@ -1268,6 +1347,20 @@ export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string }; const nightwaveTagToSeason: Record = { - RadioLegionIntermission13Syndicate: 15, - RadioLegionIntermission12Syndicate: 14 + RadioLegionIntermission13Syndicate: 15, // Nora's Mix Vol. 9 + RadioLegionIntermission12Syndicate: 14, // Nora's Mix Vol. 8 + RadioLegionIntermission11Syndicate: 13, // Nora's Mix Vol. 7 + RadioLegionIntermission10Syndicate: 12, // Nora's Mix Vol. 6 + RadioLegionIntermission9Syndicate: 11, // Nora's Mix Vol. 5 + RadioLegionIntermission8Syndicate: 10, // Nora's Mix Vol. 4 + RadioLegionIntermission7Syndicate: 9, // Nora's Mix Vol. 3 + RadioLegionIntermission6Syndicate: 8, // Nora's Mix Vol. 2 + RadioLegionIntermission5Syndicate: 7, // Nora's Mix Vol. 1 + RadioLegionIntermission4Syndicate: 6, // Nora's Choice + RadioLegionIntermission3Syndicate: 5, // Intermission III + RadioLegion3Syndicate: 4, // Glassmaker + RadioLegionIntermission2Syndicate: 3, // Intermission II + RadioLegion2Syndicate: 2, // The Emissary + RadioLegionIntermissionSyndicate: 1, // Intermission I + RadioLegionSyndicate: 0 // The Wolf of Saturn Six }; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 32cac658..997b50f4 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -765,7 +765,8 @@ export interface IKubrowPetDetailsClient extends Omit { + extends Omit< + IPendingRecipeDatabase, + "CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand" | "KubrowPet" + > { CompletionDate: IMongoDate; } diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 6fdd0c09..a6b3f1c1 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -96,7 +96,7 @@ export type IMissionInventoryUpdateRequest = { FpsSamples: number; EvolutionProgress?: IEvolutionProgress[]; FocusXpIncreases?: number[]; - PlayerSkillGains: IPlayerSkills; + PlayerSkillGains: Partial; CustomMarkers?: ICustomMarkers[]; LoreFragmentScans?: ILoreFragmentScan[]; VoidTearParticipantsCurrWave?: { diff --git a/static/webui/script.js b/static/webui/script.js index d8d30a41..f4abcb9f 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -312,7 +312,7 @@ function fetchItemList() { document.getElementById("changeSyndicate").appendChild(option); }); } else { - const nameSet = new Set(); + const nameToItems = {}; items.forEach(item => { item.name = item.name.replace(/<.+>/g, "").trim(); if ("badReason" in item) { @@ -322,6 +322,11 @@ function fetchItemList() { item.name += " " + loc("code_badItem"); } } + nameToItems[item.name] ??= []; + nameToItems[item.name].push(item); + }); + + items.forEach(item => { if (type == "ModularParts") { const supportedModularParts = [ "LWPT_HB_DECK", @@ -360,15 +365,26 @@ function fetchItemList() { .appendChild(option); } } else if (item.badReason != "notraw") { - if (nameSet.has(item.name)) { - //console.log(`Not adding ${item.uniqueName} to datalist for ${type} due to duplicate display name: ${item.name}`); - } else { - nameSet.add(item.name); - + const ambiguous = nameToItems[item.name].length > 1; + let canDisambiguate = true; + if (ambiguous) { + for (const i2 of nameToItems[item.name]) { + if (!i2.subtype) { + canDisambiguate = false; + break; + } + } + } + if (!ambiguous || canDisambiguate || nameToItems[item.name][0] == item) { const option = document.createElement("option"); option.setAttribute("data-key", item.uniqueName); option.value = item.name; + if (ambiguous && canDisambiguate) { + option.value += " (" + item.subtype + ")"; + } document.getElementById("datalist-" + type).appendChild(option); + } else { + //console.log(`Not adding ${item.uniqueName} to datalist for ${type} due to duplicate display name: ${item.name}`); } } itemMap[item.uniqueName] = { ...item, type }; @@ -476,7 +492,7 @@ function updateInventory() { } let anyExaltedMissingXP = false; - if (item.XP >= maxXP && "exalted" in itemMap[item.ItemType]) { + if (item.XP >= maxXP && item.ItemType in itemMap && "exalted" in itemMap[item.ItemType]) { for (const exaltedType of itemMap[item.ItemType].exalted) { const exaltedItem = data.SpecialItems.find(x => x.ItemType == exaltedType); if (exaltedItem) { diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 5b38f847..d46ce77d 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -1,4 +1,4 @@ -// Chinese translation by meb154 +// Chinese translation by meb154 & bishan178 dict = { general_inventoryUpdateNote: `注意:此处所做的更改只有在游戏同步仓库后才会生效。您可以通过访问星图来触发仓库更新。`, general_addButton: `添加`, @@ -18,7 +18,7 @@ dict = { code_kDrive: `K式悬浮板`, code_legendaryCore: `传奇核心`, code_traumaticPeculiar: `创伤怪奇`, - code_starter: `|MOD| (有瑕疵的)`, + code_starter: `|MOD|(有瑕疵的)`, code_badItem: `(Imposter)`, code_maxRank: `满级`, code_rename: `重命名`, @@ -28,7 +28,7 @@ dict = { code_succRankUp: `[UNTRANSLATED] Successfully ranked up.`, code_noEquipmentToRankUp: `没有可升级的装备。`, code_succAdded: `已成功添加。`, - code_succRemoved: `[UNTRANSLATED] Successfully removed.`, + code_succRemoved: `已成功移除。`, code_buffsNumber: `增益数量`, code_cursesNumber: `负面数量`, code_rerollsNumber: `洗卡次数`, @@ -39,27 +39,27 @@ dict = { code_count: `数量`, code_focusAllUnlocked: `所有专精学派均已解锁。`, code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`, - code_addModsConfirm: `确定要向账户添加 |COUNT| 张MOD吗?`, + code_addModsConfirm: `确定要向账户添加 |COUNT| 张MOD吗?`, code_succImport: `导入成功。`, code_gild: `镀金`, code_moa: `恐鸟`, code_zanuka: `猎犬`, - code_stage: `[UNTRANSLATED] Stage`, - code_complete: `[UNTRANSLATED] Complete`, - code_nextStage: `[UNTRANSLATED] Next stage`, - code_prevStage: `[UNTRANSLATED] Previous stage`, - code_reset: `[UNTRANSLATED] Reset`, - code_setInactive: `[UNTRANSLATED] Make the quest inactive`, - code_completed: `[UNTRANSLATED] Completed`, - code_active: `[UNTRANSLATED] Active`, + code_stage: `阶段`, + code_complete: `完成`, + code_nextStage: `下一阶段`, + code_prevStage: `上一阶段`, + code_reset: `重置`, + code_setInactive: `使任务处于未激活状态`, + code_completed: `已完成`, + code_active: `正在执行`, code_pigment: `颜料`, - code_mature: `[UNTRANSLATED] Mature for combat`, - code_unmature: `[UNTRANSLATED] Regress genetic aging`, + code_mature: `成长并战备`, + code_unmature: `逆转衰老基因`, login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)。`, login_emailLabel: `电子邮箱`, login_passwordLabel: `密码`, login_loginButton: `登录`, - login_registerButton: `[UNTRANSLATED] Register`, + login_registerButton: `注册账号`, navbar_logout: `退出登录`, navbar_renameAccount: `重命名账户`, navbar_deleteAccount: `删除账户`, @@ -82,22 +82,22 @@ dict = { inventory_operatorAmps: `增幅器`, inventory_hoverboards: `K式悬浮板`, inventory_moaPets: `恐鸟`, - inventory_kubrowPets: `[UNTRANSLATED] Beasts`, - inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`, + inventory_kubrowPets: `动物同伴`, + inventory_evolutionProgress: `灵化之源进度`, inventory_bulkAddSuits: `添加缺失战甲`, inventory_bulkAddWeapons: `添加缺失武器`, inventory_bulkAddSpaceSuits: `添加缺失Archwing`, inventory_bulkAddSpaceWeapons: `添加缺失Archwing武器`, inventory_bulkAddSentinels: `添加缺失守护`, inventory_bulkAddSentinelWeapons: `添加缺失守护武器`, - inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`, + inventory_bulkAddEvolutionProgress: `添加缺失的灵化之源`, inventory_bulkRankUpSuits: `所有战甲升满级`, inventory_bulkRankUpWeapons: `所有武器升满级`, inventory_bulkRankUpSpaceSuits: `所有Archwing升满级`, inventory_bulkRankUpSpaceWeapons: `所有Archwing武器升满级`, inventory_bulkRankUpSentinels: `所有守护升满级`, inventory_bulkRankUpSentinelWeapons: `所有守护武器升满级`, - inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`, + inventory_bulkRankUpEvolutionProgress: `所有灵化之源最大等级`, quests_list: `任务`, quests_completeAll: `完成所有任务`, @@ -111,15 +111,15 @@ dict = { currency_owned: `当前拥有 |COUNT|。`, powersuit_archonShardsLabel: `执刑官源力石槽位`, powersuit_archonShardsDescription: `您可以使用这些无限插槽应用各种强化效果`, - powersuit_archonShardsDescription2: `[UNTRANSLATED] Note that each archon shard takes some time to be applied when loading in.`, + powersuit_archonShardsDescription2: `请注意, 在加载时, 每个执政官源力石都需要一定的时间来生效。`, mods_addRiven: `添加裂罅MOD`, mods_fingerprint: `印记`, mods_fingerprintHelp: `需要印记相关的帮助?`, mods_rivens: `裂罅MOD`, mods_mods: `Mods`, - mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`, - mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`, - mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`, + mods_addMissingUnrankedMods: `添加所有缺失的Mods`, + mods_removeUnranked: `删除所有未升级的Mods`, + mods_addMissingMaxRankMods: `添加所有缺失的满级Mods`, cheats_administratorRequirement: `您必须是管理员才能使用此功能。要成为管理员,请将 |DISPLAYNAME| 添加到 config.json 的 administratorNames 中。`, cheats_server: `服务器`, cheats_skipTutorial: `跳过教程`, @@ -131,43 +131,43 @@ dict = { cheats_infiniteEndo: `无限内融核心`, cheats_infiniteRegalAya: `无限御品阿耶`, cheats_infiniteHelminthMaterials: `无限Helminth材料`, - cheats_claimingBlueprintRefundsIngredients: `[UNTRANSLATED] Claiming Blueprint Refunds Ingredients`, - cheats_dontSubtractVoidTraces: `[UNTRANSLATED] Don't Subtract Void Traces`, - cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`, + cheats_claimingBlueprintRefundsIngredients: `取消蓝图制造时返还材料`, + cheats_dontSubtractVoidTraces: `虚空光体无消耗`, + cheats_dontSubtractConsumables: `消耗物品使用时无损耗`, cheats_unlockAllShipFeatures: `解锁所有飞船功能`, cheats_unlockAllShipDecorations: `解锁所有飞船装饰`, cheats_unlockAllFlavourItems: `解锁所有装饰物品`, cheats_unlockAllSkins: `解锁所有外观`, cheats_unlockAllCapturaScenes: `解锁所有Captura场景`, - cheats_unlockAllDecoRecipes: `[UNTRANSLATED] Unlock All Dojo Deco Recipes`, + cheats_unlockAllDecoRecipes: `解锁所有道场配方`, cheats_universalPolarityEverywhere: `全局万用极性`, cheats_unlockDoubleCapacityPotatoesEverywhere: `全物品自带Orokin反应堆`, cheats_unlockExilusEverywhere: `全物品自带适配器`, cheats_unlockArcanesEverywhere: `全物品自带赋能适配器`, cheats_noDailyStandingLimits: `无每日声望限制`, - cheats_noDailyFocusLimit: `[UNTRANSLATED] No Daily Focus Limits`, - cheats_noArgonCrystalDecay: `[UNTRANSLATED] No Argon Crystal Decay`, - cheats_noMasteryRankUpCooldown: `[UNTRANSLATED] No Mastery Rank Up Cooldown`, - cheats_noVendorPurchaseLimits: `[UNTRANSLATED] No Vendor Purchase Limits`, - cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`, - cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`, - cheats_syndicateMissionsRepeatable: `[UNTRANSLATED] Syndicate Missions Repeatable`, - cheats_instantFinishRivenChallenge: `[UNTRANSLATED] Instant Finish Riven Challenge`, + cheats_noDailyFocusLimit: `指挥官专精无每日获取上限`, + cheats_noArgonCrystalDecay: `氩结晶无衰变`, + cheats_noMasteryRankUpCooldown: `段位考核无冷却时间`, + cheats_noVendorPurchaseLimits: `商城或商人无购买限制`, + cheats_noDeathMarks: `无死亡标记(不会被 Stalker/Grustrag 三霸/Zanuka 猎人等标记)`, + cheats_noKimCooldowns: `无 KIM 冷却时间`, + cheats_syndicateMissionsRepeatable: `集团任务可重复`, + cheats_instantFinishRivenChallenge: `立即完成裂罅挑战`, cheats_instantResourceExtractorDrones: `即时资源采集无人机`, - cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`, - cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`, + cheats_noResourceExtractorDronesDamage: `资源提取器不会损毁`, + cheats_skipClanKeyCrafting: `跳过氏族钥匙制作, 进入道场无需氏族钥匙`, cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`, - cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`, + cheats_noDojoDecoBuildStage: `道场装饰建造立即完成`, cheats_fastDojoRoomDestruction: `快速拆除道场房间`, cheats_noDojoResearchCosts: `无视道场研究消耗`, cheats_noDojoResearchTime: `无视道场研究时间`, cheats_fastClanAscension: `快速升级氏族`, - cheats_spoofMasteryRank: `伪造精通段位(-1为禁用)`, + cheats_spoofMasteryRank: `伪造精通段位(-1为禁用)`, cheats_saveSettings: `保存设置`, cheats_account: `账户`, cheats_unlockAllFocusSchools: `解锁所有专精学派`, cheats_helminthUnlockAll: `完全升级Helminth`, - cheats_intrinsicsUnlockAll: `[UNTRANSLATED] Max Rank All Intrinsics`, + cheats_intrinsicsUnlockAll: `所有内源之力最大等级`, cheats_changeSupportedSyndicate: `支持的集团`, cheats_changeButton: `更改`, cheats_none: `无`,