diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index afbb95ad..d42dd22f 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -14,7 +14,9 @@ import { addRecipes, occupySlot, combineInventoryChanges, - addKubrowPetPrint + addKubrowPetPrint, + addPowerSuit, + addEquipment } from "@/src/services/inventoryService"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; @@ -22,7 +24,7 @@ import { toOid2 } from "@/src/helpers/inventoryHelpers"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { IRecipe } from "warframe-public-export-plus"; import { config } from "@/src/services/configService"; -import { IEquipmentClient, Status } from "@/src/types/equipmentTypes"; +import { EquipmentFeatures, IEquipmentClient, Status } from "@/src/types/equipmentTypes"; interface IClaimCompletedRecipeRequest { RecipeIds: IOid[]; @@ -124,17 +126,122 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!; addKubrowPetPrint(inventory, pet, InventoryChanges); } else if (recipe.secretIngredientAction != "SIA_UNBRAND") { - InventoryChanges = { - ...InventoryChanges, - ...(await addItem( + if (recipe.resultType == "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") { + // Quite the special case here... + // We don't just get Umbra, but also Skiajati and Umbra Mods. Both items are max rank, potatoed, and with the mods are pre-installed. + // Source: https://wiki.warframe.com/w/The_Sacrifice, https://wiki.warframe.com/w/Excalibur/Umbra, https://wiki.warframe.com/w/Skiajati + + const umbraModA = ( + await addItem( + inventory, + "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModA", + 1, + false, + undefined, + `{"lvl":5}` + ) + ).Upgrades![0]; + const umbraModB = ( + await addItem( + inventory, + "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModB", + 1, + false, + undefined, + `{"lvl":5}` + ) + ).Upgrades![0]; + const umbraModC = ( + await addItem( + inventory, + "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModC", + 1, + false, + undefined, + `{"lvl":5}` + ) + ).Upgrades![0]; + const sacrificeModA = ( + await addItem( + inventory, + "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModA", + 1, + false, + undefined, + `{"lvl":5}` + ) + ).Upgrades![0]; + const sacrificeModB = ( + await addItem( + inventory, + "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModB", + 1, + false, + undefined, + `{"lvl":5}` + ) + ).Upgrades![0]; + InventoryChanges.Upgrades ??= []; + InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB); + + await addPowerSuit( inventory, - recipe.resultType, - recipe.num, - false, - undefined, - pendingRecipe.TargetFingerprint - )) - }; + "/Lotus/Powersuits/Excalibur/ExcaliburUmbra", + { + Configs: [ + { + Upgrades: [ + "", + "", + "", + "", + "", + umbraModA.ItemId.$oid, + umbraModB.ItemId.$oid, + umbraModC.ItemId.$oid + ] + } + ], + XP: 900_000, + Features: EquipmentFeatures.DOUBLE_CAPACITY + }, + InventoryChanges + ); + inventory.XPInfo.push({ + ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra", + XP: 900_000 + }); + + addEquipment( + inventory, + "Melee", + "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana", + { + Configs: [ + { Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid] } + ], + XP: 450_000, + Features: EquipmentFeatures.DOUBLE_CAPACITY + }, + InventoryChanges + ); + inventory.XPInfo.push({ + ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana", + XP: 450_000 + }); + } else { + InventoryChanges = { + ...InventoryChanges, + ...(await addItem( + inventory, + recipe.resultType, + recipe.num, + false, + undefined, + pendingRecipe.TargetFingerprint + )) + }; + } } if ( config.claimingBlueprintRefundsIngredients && diff --git a/src/controllers/api/inventorySlotsController.ts b/src/controllers/api/inventorySlotsController.ts index 3face44e..8a8fce0a 100644 --- a/src/controllers/api/inventorySlotsController.ts +++ b/src/controllers/api/inventorySlotsController.ts @@ -2,7 +2,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService"; import { getInventory, updateCurrency, updateSlots } from "@/src/services/inventoryService"; import { RequestHandler } from "express"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; -import { logger } from "@/src/utils/logger"; +import { exhaustive } from "@/src/utils/ts-utils"; /* loadout slots are additionally purchased slots only @@ -22,13 +22,44 @@ export const inventorySlotsController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const body = JSON.parse(req.body as string) as IInventorySlotsRequest; - if (body.Bin != InventorySlot.SUITS && body.Bin != InventorySlot.PVE_LOADOUTS) { - logger.warn(`unexpected slot purchase of type ${body.Bin}, account may be overcharged`); + let price; + let amount; + switch (body.Bin) { + case InventorySlot.SUITS: + case InventorySlot.MECHSUITS: + case InventorySlot.PVE_LOADOUTS: + case InventorySlot.CREWMEMBERS: + price = 20; + amount = 1; + break; + + case InventorySlot.SPACESUITS: + price = 12; + amount = 1; + break; + + case InventorySlot.WEAPONS: + case InventorySlot.SPACEWEAPONS: + case InventorySlot.SENTINELS: + case InventorySlot.RJ_COMPONENT_AND_ARMAMENTS: + case InventorySlot.AMPS: + price = 12; + amount = 2; + break; + + case InventorySlot.RIVENS: + price = 60; + amount = 3; + break; + + default: + exhaustive(body.Bin); + throw new Error(`unexpected slot purchase of type ${body.Bin as string}`); } const inventory = await getInventory(accountId); - const currencyChanges = updateCurrency(inventory, 20, true); - updateSlots(inventory, body.Bin, 1, 1); + const currencyChanges = updateCurrency(inventory, price, true); + updateSlots(inventory, body.Bin, amount, amount); await inventory.save(); res.json({ InventoryChanges: currencyChanges }); diff --git a/src/controllers/api/playerSkillsController.ts b/src/controllers/api/playerSkillsController.ts index 5c8b301a..05319b35 100644 --- a/src/controllers/api/playerSkillsController.ts +++ b/src/controllers/api/playerSkillsController.ts @@ -1,25 +1,39 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; -import { getInventory } from "@/src/services/inventoryService"; +import { addConsumables, getInventory } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; import { IPlayerSkills } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; export const playerSkillsController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId, "PlayerSkills"); + const inventory = await getInventory(accountId, "PlayerSkills Consumables"); const request = getJSONfromString(String(req.body)); const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]; const cost = (request.Pool == "LPP_DRIFTER" ? drifterCosts[oldRank] : 1 << oldRank) * 1000; inventory.PlayerSkills[request.Pool as keyof IPlayerSkills] -= cost; inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]++; - await inventory.save(); + const inventoryChanges: IInventoryChanges = {}; + if (request.Skill == "LPS_COMMAND" && inventory.PlayerSkills.LPS_COMMAND == 9) { + const consumablesChanges = [ + { + ItemType: "/Lotus/Types/Restoratives/Consumable/CrewmateBall", + ItemCount: 1 + } + ]; + addConsumables(inventory, consumablesChanges); + inventoryChanges.Consumables = consumablesChanges; + } + + await inventory.save(); res.json({ Pool: request.Pool, PoolInc: -cost, Skill: request.Skill, - Rank: oldRank + 1 + Rank: oldRank + 1, + InventoryChanges: inventoryChanges }); }; diff --git a/src/helpers/relicHelper.ts b/src/helpers/relicHelper.ts index 3c663692..5a34d342 100644 --- a/src/helpers/relicHelper.ts +++ b/src/helpers/relicHelper.ts @@ -23,10 +23,16 @@ export const crackRelic = async ( weights = { COMMON: 0, UNCOMMON: 0, RARE: 1, LEGENDARY: 0 }; } logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights); - const reward = getRandomWeightedReward( + let reward = getRandomWeightedReward( ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics weights )!; + if (config.relicRewardItemCountMultiplier !== undefined && (config.relicRewardItemCountMultiplier ?? 1) != 1) { + reward = { + ...reward, + itemCount: reward.itemCount * config.relicRewardItemCountMultiplier + }; + } logger.debug(`relic rolled`, reward); participant.Reward = reward.type; @@ -43,13 +49,7 @@ export const crackRelic = async ( // Give reward combineInventoryChanges( inventoryChanges, - ( - await handleStoreItemAcquisition( - reward.type, - inventory, - reward.itemCount * (config.relicRewardItemCountMultiplier ?? 1) - ) - ).InventoryChanges + (await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount)).InventoryChanges ); return reward; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 7f587a4b..766ee4b1 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -482,11 +482,14 @@ export const addItem = async ( if (quantity != 1) { logger.warn(`adding 1 of ${typeName} ${targetFingerprint} even tho quantity ${quantity} was requested`); } - inventory.Upgrades.push({ - ItemType: typeName, - UpgradeFingerprint: targetFingerprint - }); - return {}; // there's not exactly a common "InventoryChanges" format for these + const upgrade = + inventory.Upgrades[ + inventory.Upgrades.push({ + ItemType: typeName, + UpgradeFingerprint: targetFingerprint + }) - 1 + ]; + return { Upgrades: [upgrade.toJSON()] }; } const changes = [ { @@ -812,7 +815,7 @@ export const addItem = async ( if (!seed) { throw new Error(`Expected crew member to have a seed`); } - seed |= 0x33b81en << 32n; + seed |= BigInt(Math.trunc(inventory.Created.getTime() / 1000) & 0xffffff) << 32n; return { ...addCrewMember(inventory, typeName, seed), ...occupySlot(inventory, InventorySlot.CREWMEMBERS, premiumPurchase) @@ -1106,6 +1109,10 @@ export const addKubrowPet = ( }; } else { dominantTraits = createRandomTraits(kubrowPetName, traitsPool); + if (kubrowPetName == "/Lotus/Types/Game/KubrowPet/ChargerKubrowPetPowerSuit") { + dominantTraits.BodyType = "/Lotus/Types/Game/KubrowPet/BodyTypes/ChargerKubrowPetBodyType"; + dominantTraits.FurPattern = "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternInfested"; + } } const recessiveTraits: ITraits = createRandomTraits( diff --git a/src/services/questService.ts b/src/services/questService.ts index 550bad2f..c34cae6b 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -236,7 +236,7 @@ const handleQuestCompletion = async ( setupKahlSyndicate(inventory); } - // Whispers in the Walls is unlocked once The New + Heart of Deimos are completed. + // Whispers in the Walls is unlocked once The New War + Heart of Deimos are completed. if ( doesQuestCompletionFinishSet(inventory, questKey, [ "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain", diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 3433883f..0b2dc7a5 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -971,25 +971,26 @@ const getCalendarSeason = (week: number): ICalendarSeason => { // Not very faithful, but to avoid the same node coming up back-to-back (which is not valid), I've split these into 2 arrays which we're alternating between. -const voidStormMissionsA = { - VoidT1: ["CrewBattleNode519", "CrewBattleNode518", "CrewBattleNode515", "CrewBattleNode503"], - VoidT2: ["CrewBattleNode501", "CrewBattleNode534", "CrewBattleNode530"], - VoidT3: ["CrewBattleNode521", "CrewBattleNode516"], +const voidStormMissions = { + VoidT1: [ + "CrewBattleNode519", + "CrewBattleNode518", + "CrewBattleNode515", + "CrewBattleNode503", + "CrewBattleNode509", + "CrewBattleNode522", + "CrewBattleNode511", + "CrewBattleNode512" + ], + VoidT2: ["CrewBattleNode501", "CrewBattleNode534", "CrewBattleNode530", "CrewBattleNode535", "CrewBattleNode533"], + VoidT3: ["CrewBattleNode521", "CrewBattleNode516", "CrewBattleNode524", "CrewBattleNode525"], VoidT4: [ "CrewBattleNode555", "CrewBattleNode553", "CrewBattleNode554", "CrewBattleNode539", "CrewBattleNode531", - "CrewBattleNode527" - ] -}; - -const voidStormMissionsB = { - VoidT1: ["CrewBattleNode509", "CrewBattleNode522", "CrewBattleNode511", "CrewBattleNode512"], - VoidT2: ["CrewBattleNode535", "CrewBattleNode533"], - VoidT3: ["CrewBattleNode524", "CrewBattleNode525"], - VoidT4: [ + "CrewBattleNode527", "CrewBattleNode542", "CrewBattleNode538", "CrewBattleNode543", @@ -997,18 +998,21 @@ const voidStormMissionsB = { "CrewBattleNode550", "CrewBattleNode529" ] -}; +} as const; + +const voidStormLookbehind = { + VoidT1: 3, + VoidT2: 1, + VoidT3: 1, + VoidT4: 3 +} as const; const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => { const activation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute; const expiry = activation + 90 * unixTimesInMs.minute; let accum = 0; - const rng = new SRng(new SRng(hour).randomInt(0, 100_000)); - const voidStormMissions = structuredClone(hour & 1 ? voidStormMissionsA : voidStormMissionsB); + const tierIdx = { VoidT1: hour * 2, VoidT2: hour, VoidT3: hour, VoidT4: hour * 2 }; for (const tier of ["VoidT1", "VoidT1", "VoidT2", "VoidT3", "VoidT4", "VoidT4"] as const) { - const idx = rng.randomInt(0, voidStormMissions[tier].length - 1); - const node = voidStormMissions[tier][idx]; - voidStormMissions[tier].splice(idx, 1); arr.push({ _id: { $oid: @@ -1016,7 +1020,12 @@ const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => { "0321e89b" + (accum++).toString().padStart(8, "0") }, - Node: node, + Node: sequentiallyUniqueRandomElement( + voidStormMissions[tier], + tierIdx[tier]++, + voidStormLookbehind[tier], + 2051969264 + )!, Activation: { $date: { $numberLong: activation.toString() } }, Expiry: { $date: { $numberLong: expiry.toString() } }, ActiveMissionTier: tier @@ -1024,75 +1033,124 @@ const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => { } }; -const doesTimeSatsifyConstraints = (timeSecs: number): boolean => { - if (config.worldState?.eidolonOverride) { +interface ITimeConstraint { + //name: string; + isValidTime: (timeSecs: number) => boolean; + getIdealTimeBefore: (timeSecs: number) => number; +} + +const eidolonDayConstraint: ITimeConstraint = { + //name: "eidolon day", + isValidTime: (timeSecs: number): boolean => { 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; - } - } + return !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleNightStart * 1000); + }, + getIdealTimeBefore: (timeSecs: number): number => { + const eidolonEpoch = 1391992660; + const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000); + const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000; + return eidolonCycleStart; } +}; - if (config.worldState?.vallisOverride) { +const eidolonNightConstraint: ITimeConstraint = { + //name: "eidolon night", + isValidTime: (timeSecs: number): boolean => { + const eidolonEpoch = 1391992660; + const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000); + const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000; + const eidolonCycleEnd = eidolonCycleStart + 9000; + const eidolonCycleNightStart = eidolonCycleEnd - 3000; + return ( + timeSecs >= eidolonCycleNightStart && + !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, eidolonCycleEnd * 1000) + ); + }, + getIdealTimeBefore: (timeSecs: number): number => { + const eidolonEpoch = 1391992660; + const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000); + const eidolonCycleStart = eidolonEpoch + eidolonCycle * 9000; + const eidolonCycleEnd = eidolonCycleStart + 9000; + const eidolonCycleNightStart = eidolonCycleEnd - 3000; + if (eidolonCycleNightStart > timeSecs) { + // Night hasn't started yet, but we need to return a time in the past. + return eidolonCycleNightStart - 9000; + } + return eidolonCycleNightStart; + } +}; + +const venusColdConstraint: ITimeConstraint = { + //name: "venus cold", + isValidTime: (timeSecs: number): boolean => { 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 ( + timeSecs >= vallisCycleColdStart && + !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleEnd * 1000) + ); + }, + getIdealTimeBefore: (timeSecs: number): number => { + const vallisEpoch = 1541837628; + const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600); + const vallisCycleStart = vallisEpoch + vallisCycle * 1600; + const vallisCycleColdStart = vallisCycleStart + 400; + if (vallisCycleColdStart > timeSecs) { + // Cold hasn't started yet, but we need to return a time in the past. + return vallisCycleColdStart - 1600; + } + return vallisCycleColdStart; + } +}; + +const venusWarmConstraint: ITimeConstraint = { + //name: "venus warm", + isValidTime: (timeSecs: number): boolean => { + const vallisEpoch = 1541837628; + const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600); + const vallisCycleStart = vallisEpoch + vallisCycle * 1600; + const vallisCycleColdStart = vallisCycleStart + 400; + return !isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, vallisCycleColdStart * 1000); + }, + getIdealTimeBefore: (timeSecs: number): number => { + const vallisEpoch = 1541837628; + const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600); + const vallisCycleStart = vallisEpoch + vallisCycle * 1600; + return vallisCycleStart; + } +}; + +const getIdealTimeSatsifyingConstraints = (constraints: ITimeConstraint[]): number => { + let timeSecs = Math.trunc(Date.now() / 1000); + let allGood; + do { + allGood = true; + for (const constraint of constraints) { + if (!constraint.isValidTime(timeSecs)) { + //logger.debug(`${constraint.name} is not happy with ${timeSecs}`); + const prevTimeSecs = timeSecs; + const suggestion = constraint.getIdealTimeBefore(timeSecs); + timeSecs = suggestion; + do { + timeSecs += 60; + if (timeSecs >= prevTimeSecs || !constraint.isValidTime(timeSecs)) { + timeSecs = suggestion; // Can't find a compromise; just take the suggestion and try to compromise on another constraint. + break; + } + } while (!constraints.every(constraint => constraint.isValidTime(timeSecs))); + allGood = false; + break; } } - } - - if (config.worldState?.duviriOverride) { - const duviriMoods = ["sorrow", "fear", "joy", "anger", "envy"]; - const desiredMood = duviriMoods.indexOf(config.worldState.duviriOverride); - if (desiredMood == -1) { - logger.warn(`ignoring invalid config value for worldState.duviriOverride`, { - value: config.worldState.duviriOverride, - valid_values: duviriMoods - }); - } else { - const moodIndex = Math.trunc(timeSecs / 7200); - const moodStart = moodIndex * 7200; - const moodEnd = moodStart + 7200; - if ( - moodIndex % 5 != desiredMood || - isBeforeNextExpectedWorldStateRefresh(timeSecs * 1000, moodEnd * 1000) - ) { - return false; - } - } - } - - return true; + } while (!allGood); + return timeSecs; }; const getVarziaRotation = (week: number): string => { @@ -1170,10 +1228,38 @@ const getAllVarziaManifests = (): IPrimeVaultTraderOffer[] => { }; export const getWorldState = (buildLabel?: string): IWorldState => { - let timeSecs = Math.round(Date.now() / 1000); - while (!doesTimeSatsifyConstraints(timeSecs)) { - timeSecs -= 60; + const constraints: ITimeConstraint[] = []; + if (config.worldState?.eidolonOverride) { + constraints.push(config.worldState.eidolonOverride == "day" ? eidolonDayConstraint : eidolonNightConstraint); } + if (config.worldState?.vallisOverride) { + constraints.push(config.worldState.vallisOverride == "cold" ? venusColdConstraint : venusWarmConstraint); + } + if (config.worldState?.duviriOverride) { + const duviriMoods = ["sorrow", "fear", "joy", "anger", "envy"]; + const desiredMood = duviriMoods.indexOf(config.worldState.duviriOverride); + if (desiredMood == -1) { + logger.warn(`ignoring invalid config value for worldState.duviriOverride`, { + value: config.worldState.duviriOverride, + valid_values: duviriMoods + }); + } else { + constraints.push({ + //name: `duviri ${config.worldState.duviriOverride}`, + isValidTime: (timeSecs: number): boolean => { + const moodIndex = Math.trunc(timeSecs / 7200); + return moodIndex % 5 == desiredMood; + }, + getIdealTimeBefore: (timeSecs: number): number => { + let moodIndex = Math.trunc(timeSecs / 7200); + moodIndex -= ((moodIndex % 5) - desiredMood + 5) % 5; // while (moodIndex % 5 != desiredMood) --moodIndex; + const moodStart = moodIndex * 7200; + return moodStart; + } + }); + } + } + const timeSecs = getIdealTimeSatsifyingConstraints(constraints); const timeMs = timeSecs * 1000; const day = Math.trunc((timeMs - EPOCH) / 86400000); const week = Math.trunc(day / 7); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index b10d3282..4fa39f93 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -520,7 +520,8 @@ export enum InventorySlot { SENTINELS = "SentinelBin", AMPS = "OperatorAmpBin", RJ_COMPONENT_AND_ARMAMENTS = "CrewShipSalvageBin", - CREWMEMBERS = "CrewMemberBin" + CREWMEMBERS = "CrewMemberBin", + RIVENS = "RandomModBin" } export interface ISlots { diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index 1dcb0914..8b0db0f7 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -8,7 +8,8 @@ import { IRecentVendorPurchaseClient, TEquipmentKey, ICrewMemberClient, - IKubrowPetPrintClient + IKubrowPetPrintClient, + IUpgradeClient } from "@/src/types/inventoryTypes/inventoryTypes"; export enum PurchaseSource { @@ -80,6 +81,7 @@ export type IInventoryChanges = { RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0 CrewMembers?: ICrewMemberClient[]; KubrowPetPrints?: IKubrowPetPrintClient[]; + Upgrades?: IUpgradeClient[]; // TOVERIFY } & Record< Exclude< string, diff --git a/src/utils/ts-utils.ts b/src/utils/ts-utils.ts index 4f456c12..93b43fd4 100644 --- a/src/utils/ts-utils.ts +++ b/src/utils/ts-utils.ts @@ -3,3 +3,5 @@ type Entries = (K extends unknown ? [K, T[K]] : export function getEntriesUnsafe(object: T): Entries { return Object.entries(object) as Entries; } + +export const exhaustive = (_: never): void => {}; diff --git a/static/fixed_responses/allDialogue.json b/static/fixed_responses/allDialogue.json index a4ee727a..4b47303e 100644 --- a/static/fixed_responses/allDialogue.json +++ b/static/fixed_responses/allDialogue.json @@ -135,5 +135,6 @@ "/Lotus/Language/EntratiLab/EntratiGeneral/HumanLoidLoved", "ConquestSetupIntro", "EntratiLabConquestHardModeUnlocked", - "/Lotus/Language/Npcs/KonzuPostNewWar" + "/Lotus/Language/Npcs/KonzuPostNewWar", + "/Lotus/Language/SolarisVenus/EudicoPostNewWar" ] diff --git a/static/fixed_responses/questCompletionRewards.json b/static/fixed_responses/questCompletionRewards.json index 1e584e72..8a04e2aa 100644 --- a/static/fixed_responses/questCompletionRewards.json +++ b/static/fixed_responses/questCompletionRewards.json @@ -16,6 +16,10 @@ { "ItemType": "/Lotus/Types/Keys/1999PrologueQuest/1999PrologueQuestKeyChain", "ItemCount": 1 + }, + { + "ItemType": "/Lotus/Types/Items/EmailItems/TennokaiEmailItem", + "ItemCount": 1 } ] } diff --git a/static/webui/index.html b/static/webui/index.html index 63d4d936..55676cd8 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -457,7 +457,8 @@
-

+

+

@@ -527,8 +528,8 @@
- +
@@ -803,21 +804,21 @@
- +
- +
- +
@@ -830,6 +831,7 @@
+ @@ -942,14 +944,14 @@
- +
- +
diff --git a/static/webui/script.js b/static/webui/script.js index 9649019e..bb557ff3 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -273,6 +273,8 @@ function fetchItemList() { window.itemListPromise = new Promise(resolve => { const req = $.get("/custom/getItemLists?lang=" + window.lang); req.done(async data => { + window.allQuestKeys = data.QuestKeys; + await dictPromise; document.querySelectorAll('[id^="datalist-"]').forEach(datalist => { @@ -879,6 +881,14 @@ function updateInventory() { // Populate quests route document.getElementById("QuestKeys-list").innerHTML = ""; + window.allQuestKeys.forEach(questKey => { + if (!data.QuestKeys.some(x => x.ItemType == questKey.uniqueName)) { + const datalist = document.getElementById("datalist-QuestKeys"); + if (!datalist.querySelector(`option[data-key="${questKey.uniqueName}"]`)) { + readdQuestKey(itemMap, questKey.uniqueName); + } + } + }); data.QuestKeys.forEach(item => { const tr = document.createElement("tr"); tr.setAttribute("data-item-type", item.ItemType); @@ -972,10 +982,7 @@ function updateInventory() { a.href = "#"; a.onclick = function (event) { event.preventDefault(); - const option = document.createElement("option"); - option.setAttribute("data-key", item.ItemType); - option.value = itemMap[item.ItemType]?.name ?? item.ItemType; - document.getElementById("datalist-QuestKeys").appendChild(option); + readdQuestKey(itemMap, item.ItemType); doQuestUpdate("deleteKey", item.ItemType); }; a.title = loc("code_remove"); @@ -1166,14 +1173,15 @@ function updateInventory() { const item = data[category].find(x => x.ItemId.$oid == oid); if (item) { + document.getElementById("detailedView-loading").classList.add("d-none"); + if (item.ItemName) { - $("#detailedView-route h3").text(item.ItemName); + $("#detailedView-title").text(item.ItemName); $("#detailedView-route .text-body-secondary").text( itemMap[item.ItemType]?.name ?? item.ItemType ); } else { - $("#detailedView-route h3").text(itemMap[item.ItemType]?.name ?? item.ItemType); - $("#detailedView-route .text-body-secondary").text(""); + $("#detailedView-title").text(itemMap[item.ItemType]?.name ?? item.ItemType); } if (category == "Suits") { @@ -1954,6 +1962,19 @@ for (const id of uiConfigs) { } } +document.querySelectorAll(".config-form .input-group").forEach(grp => { + const input = grp.querySelector("input"); + const btn = grp.querySelector("button"); + input.oninput = input.onchange = function () { + btn.classList.remove("btn-secondary"); + btn.classList.add("btn-primary"); + }; + btn.onclick = function () { + btn.classList.remove("btn-primary"); + btn.classList.add("btn-secondary"); + }; +}); + function doSaveConfigInt(id) { $.post({ url: "/custom/setConfig?" + window.authz + "&wsid=" + wsid, @@ -2171,7 +2192,9 @@ function doAddMissingMaxRankMods() { // DetailedView Route single.getRoute("#detailedView-route").on("beforeload", function () { - this.element.querySelector("h3").textContent = "Loading..."; + document.getElementById("detailedView-loading").classList.remove("d-none"); + document.getElementById("detailedView-title").textContent = ""; + document.querySelector("#detailedView-route .text-body-secondary").textContent = ""; document.getElementById("archonShards-card").classList.add("d-none"); document.getElementById("valenceBonus-card").classList.add("d-none"); if (window.didInitialInventoryUpdate) { @@ -2254,6 +2277,13 @@ function doAddCurrency(currency) { }); } +function readdQuestKey(itemMap, itemType) { + const option = document.createElement("option"); + option.setAttribute("data-key", itemType); + option.value = itemMap[itemType]?.name ?? itemType; + document.getElementById("datalist-QuestKeys").appendChild(option); +} + function doQuestUpdate(operation, itemType) { revalidateAuthz().then(() => { $.post({ @@ -2764,3 +2794,16 @@ document.querySelectorAll("#sidebar .nav-link").forEach(function (elm) { window.scrollTo(0, 0); }); }); + +async function markAllAsRead() { + await revalidateAuthz(); + const { Inbox } = await fetch("/api/inbox.php?" + window.authz).then(x => x.json()); + let any = false; + for (const msg of Inbox) { + if (!msg.r) { + await fetch("/api/inbox.php?" + window.authz + "&messageId=" + msg.messageId.$oid); + any = true; + } + } + toast(loc(any ? "code_succRelog" : "code_nothingToDo")); +} diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 05eceed6..97fd3511 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -4,6 +4,7 @@ dict = { general_addButton: `Hinzufügen`, general_setButton: `[UNTRANSLATED] Set`, general_bulkActions: `Massenaktionen`, + general_loading: `[UNTRANSLATED] Loading...`, code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`, code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`, @@ -44,6 +45,8 @@ dict = { code_focusUnlocked: `|COUNT| neue Fokus-Schulen freigeschaltet! Ein Inventar-Update wird benötigt, damit die Änderungen im Spiel sichtbar werden. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`, code_addModsConfirm: `Bist du sicher, dass du |COUNT| Mods zu deinem Account hinzufügen möchtest?`, code_succImport: `Erfolgreich importiert.`, + code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`, + code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`, code_gild: `Veredeln`, code_moa: `Moa`, code_zanuka: `Jagdhund`, @@ -199,6 +202,7 @@ dict = { cheats_changeSupportedSyndicate: `Unterstütztes Syndikat`, cheats_changeButton: `Ändern`, cheats_none: `Keines`, + cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`, worldState: `[UNTRANSLATED] World State`, worldState_creditBoost: `[UNTRANSLATED] Credit Boost`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index fa6d3d18..02b316f2 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -3,6 +3,7 @@ dict = { general_addButton: `Add`, general_setButton: `Set`, general_bulkActions: `Bulk Actions`, + general_loading: `Loading...`, code_loginFail: `Login failed. Double-check the email and password.`, code_regFail: `Registration failed. Account already exists?`, @@ -43,6 +44,8 @@ dict = { code_focusUnlocked: `Unlocked |COUNT| new focus schools! An inventory update will be needed for the changes to be reflected in-game. Visiting the navigation should be the easiest way to trigger that.`, code_addModsConfirm: `Are you sure you want to add |COUNT| mods to your account?`, code_succImport: `Successfully imported.`, + code_succRelog: `Done. Please note that you'll need to relog to see a difference in-game.`, + code_nothingToDo: `Done. There was nothing to do.`, code_gild: `Gild`, code_moa: `Moa`, code_zanuka: `Hound`, @@ -198,6 +201,7 @@ dict = { cheats_changeSupportedSyndicate: `Supported syndicate`, cheats_changeButton: `Change`, cheats_none: `None`, + cheats_markAllAsRead: `Mark Inbox As Read`, worldState: `World State`, worldState_creditBoost: `Credit Boost`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index c66adccf..8e1fca1c 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -4,6 +4,7 @@ dict = { general_addButton: `Agregar`, general_setButton: `Establecer`, general_bulkActions: `Acciones masivas`, + general_loading: `[UNTRANSLATED] Loading...`, code_loginFail: `Error al iniciar sesión. Verifica el correo electrónico y la contraseña.`, code_regFail: `Error al registrar la cuenta. ¿Ya existe una cuenta con este correo?`, @@ -44,6 +45,8 @@ dict = { code_focusUnlocked: `¡Desbloqueadas |COUNT| nuevas escuelas de enfoque! Se necesita una actualización del inventario para reflejar los cambios en el juego. Visitar la navegación debería ser la forma más sencilla de activarlo.`, code_addModsConfirm: `¿Estás seguro de que deseas agregar |COUNT| modificadores a tu cuenta?`, code_succImport: `Importación exitosa.`, + code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`, + code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`, code_gild: `Refinar`, code_moa: `Moa`, code_zanuka: `Sabueso`, @@ -199,6 +202,7 @@ dict = { cheats_changeSupportedSyndicate: `Sindicatos disponibles`, cheats_changeButton: `Cambiar`, cheats_none: `Ninguno`, + cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`, worldState: `Estado del mundo`, worldState_creditBoost: `Potenciador de Créditos`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 48b991e9..638f733f 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -4,6 +4,7 @@ dict = { general_addButton: `Ajouter`, general_setButton: `[UNTRANSLATED] Set`, general_bulkActions: `Action groupée`, + general_loading: `[UNTRANSLATED] Loading...`, code_loginFail: `Connexion échouée. Vérifiez le mot de passe.`, code_regFail: `Enregistrement impossible. Compte existant?`, @@ -44,6 +45,8 @@ dict = { code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`, code_addModsConfirm: `Ajouter |COUNT| mods à l'inventaire ?`, code_succImport: `Importé.`, + code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`, + code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`, code_gild: `Polir`, code_moa: `Moa`, code_zanuka: `Molosse`, @@ -199,6 +202,7 @@ dict = { cheats_changeSupportedSyndicate: `Allégeance`, cheats_changeButton: `Changer`, cheats_none: `Aucun`, + cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`, worldState: `[UNTRANSLATED] World State`, worldState_creditBoost: `[UNTRANSLATED] Credit Boost`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 791df3c3..cb7dc75f 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -4,6 +4,7 @@ dict = { general_addButton: `Добавить`, general_setButton: `Установить`, general_bulkActions: `Массовые действия`, + general_loading: `[UNTRANSLATED] Loading...`, code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`, code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`, @@ -44,6 +45,8 @@ dict = { code_focusUnlocked: `Разблокировано |COUNT| новых школ фокуса! Для отображения изменений в игре потребуется обновление инвентаря. Посещение навигации — самый простой способ этого добиться.`, code_addModsConfirm: `Вы уверены, что хотите добавить |COUNT| модов на ваш аккаунт?`, code_succImport: `Успешно импортировано.`, + code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`, + code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`, code_gild: `Улучшить`, code_moa: `МОА`, code_zanuka: `Гончая`, @@ -199,6 +202,7 @@ dict = { cheats_changeSupportedSyndicate: `Поддерживаемый синдикат`, cheats_changeButton: `Изменить`, cheats_none: `Отсутствует`, + cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`, worldState: `[UNTRANSLATED] World State`, worldState_creditBoost: `[UNTRANSLATED] Credit Boost`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 45f4a1ce..b7efc8c5 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -4,6 +4,7 @@ dict = { general_addButton: `添加`, general_setButton: `设置`, general_bulkActions: `批量操作`, + general_loading: `[UNTRANSLATED] Loading...`, code_loginFail: `登录失败.请检查邮箱和密码.`, code_regFail: `注册失败.账号已存在.`, @@ -44,6 +45,8 @@ dict = { code_focusUnlocked: `已解锁|COUNT|个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新.`, code_addModsConfirm: `确定要向账户添加|COUNT|张MOD吗?`, code_succImport: `导入成功。`, + code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`, + code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`, code_gild: `镀金`, code_moa: `恐鸟`, code_zanuka: `猎犬`, @@ -199,6 +202,7 @@ dict = { cheats_changeSupportedSyndicate: `支持的集团`, cheats_changeButton: `更改`, cheats_none: `无`, + cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`, worldState: `世界状态配置`, worldState_creditBoost: `现金加成`,