From 85b403e45292ea377e871e703c1f1e604adc345a Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Fri, 24 Oct 2025 06:29:13 +0200 Subject: [PATCH 1/6] feat: nightcap syndicate Closes #2928 Closes #2931 --- src/controllers/api/feedPrinceController.ts | 39 ++++++ .../api/researchMushroomController.ts | 117 ++++++++++++++++++ src/models/inventoryModels/inventoryModel.ts | 24 +++- src/routes/api.ts | 4 + src/types/inventoryTypes/inventoryTypes.ts | 11 ++ 5 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 src/controllers/api/feedPrinceController.ts create mode 100644 src/controllers/api/researchMushroomController.ts diff --git a/src/controllers/api/feedPrinceController.ts b/src/controllers/api/feedPrinceController.ts new file mode 100644 index 00000000..89c6fce6 --- /dev/null +++ b/src/controllers/api/feedPrinceController.ts @@ -0,0 +1,39 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { addMiscItem, getInventory } from "../../services/inventoryService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { logger } from "../../utils/logger.ts"; + +export const feedPrinceController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "MiscItems NokkoColony"); + const payload = getJSONfromString(String(req.body)); + + switch (payload.Mode) { + case "r": { + const InventoryChanges = {}; + addMiscItem(inventory, "/Lotus/Types/Items/MiscItems/MushroomFood", payload.Amount * -1, InventoryChanges); + inventory.NokkoColony ??= { + FeedLevel: 0, + JournalEntries: [] + }; + inventory.NokkoColony.FeedLevel += payload.Amount; + await inventory.save(); + res.json({ + FeedSucceeded: true, + FeedLevel: inventory.NokkoColony.FeedLevel, + InventoryChanges + }); + break; + } + + default: + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + throw new Error(`unknown feedPrince mode: ${payload.Mode}`); + } +}; + +interface IFeedPrinceRequest { + Mode: string; // r + Amount: number; +} diff --git a/src/controllers/api/researchMushroomController.ts b/src/controllers/api/researchMushroomController.ts new file mode 100644 index 00000000..727fe234 --- /dev/null +++ b/src/controllers/api/researchMushroomController.ts @@ -0,0 +1,117 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { addMiscItem, getInventory } from "../../services/inventoryService.ts"; +import { getJSONfromString } from "../../helpers/stringHelpers.ts"; +import { logger } from "../../utils/logger.ts"; +import type { IJournalEntry } from "../../types/inventoryTypes/inventoryTypes.ts"; +import type { IAffiliationMods } from "../../types/purchaseTypes.ts"; + +export const researchMushroomController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "MiscItems NokkoColony Affiliations"); + const payload = getJSONfromString(String(req.body)); + switch (payload.Mode) { + case "r": { + const InventoryChanges = {}; + const AffiliationMods: IAffiliationMods[] = []; + + addMiscItem(inventory, payload.MushroomItem, payload.Amount * -1, InventoryChanges); + if (payload.Convert) { + addMiscItem(inventory, "/Lotus/Types/Items/MiscItems/MushroomFood", payload.Amount, InventoryChanges); + } + + inventory.NokkoColony ??= { + FeedLevel: 0, + JournalEntries: [] + }; + + let journalEntry = inventory.NokkoColony.JournalEntries.find(x => x.EntryType == payload.MushroomItem); + if (!journalEntry) { + journalEntry = { EntryType: payload.MushroomItem, Progress: 0 }; + inventory.NokkoColony.JournalEntries.push(journalEntry); + } + + let syndicate = inventory.Affiliations.find(x => x.Tag == "NightcapJournalSyndicate"); + if (!syndicate) { + syndicate = { Tag: "NightcapJournalSyndicate", Title: 0, Standing: 0 }; + inventory.Affiliations.push(syndicate); + } + const completedBefore = inventory.NokkoColony.JournalEntries.filter( + entry => getJournalRank(entry) === 3 + ).length; + const PrevRank = syndicateTitleThresholds.reduce( + (rank, threshold, i) => (completedBefore >= threshold ? i : rank), + 0 + ); + + if (getJournalRank(journalEntry) < 3) journalEntry.Progress += payload.Amount; + + const completedAfter = inventory.NokkoColony.JournalEntries.filter( + entry => getJournalRank(entry) === 3 + ).length; + const NewRank = syndicateTitleThresholds.reduce( + (rank, threshold, i) => (completedAfter >= threshold ? i : rank), + 0 + ); + + if (NewRank > (syndicate.Title ?? 0)) { + syndicate.Title = NewRank; + AffiliationMods.push({ Tag: "NightcapJournalSyndicate", Title: NewRank }); + } + + await inventory.save(); + res.json({ + PrevRank, + NewRank, + Progress: journalEntry.Progress, + InventoryChanges, + AffiliationMods + }); + break; + } + + default: + logger.debug(`data provided to ${req.path}: ${String(req.body)}`); + throw new Error(`unknown researchMushroom mode: ${payload.Mode}`); + } +}; + +interface IResearchMushroom { + Mode: string; // r + MushroomItem: string; + Amount: number; + Convert: boolean; +} + +const journalEntriesRank: Record = { + "/Lotus/Types/Items/MushroomJournal/PlainMushroomJournalItem": 1, + "/Lotus/Types/Items/MushroomJournal/GasMushroomJournalItem": 4, + "/Lotus/Types/Items/MushroomJournal/ToxinMushroomJournalItem": 3, + "/Lotus/Types/Items/MushroomJournal/ViralMushroomJournalItem": 4, + "/Lotus/Types/Items/MushroomJournal/MagneticMushroomJournalItem": 4, + "/Lotus/Types/Items/MushroomJournal/ElectricMushroomJournalItem": 3, + "/Lotus/Types/Items/MushroomJournal/TauMushroomJournalItem": 5, + "/Lotus/Types/Items/MushroomJournal/SlashMushroomJournalItem": 3, + "/Lotus/Types/Items/MushroomJournal/BlastMushroomJournalItem": 4, + "/Lotus/Types/Items/MushroomJournal/ImpactMushroomJournalItem": 3, + "/Lotus/Types/Items/MushroomJournal/ColdMushroomJournalItem": 3, + "/Lotus/Types/Items/MushroomJournal/CorrosiveMushroomJournalItem": 4, + "/Lotus/Types/Items/MushroomJournal/PunctureMushroomJournalItem": 3, + "/Lotus/Types/Items/MushroomJournal/HeatMushroomJournalItem": 3, + "/Lotus/Types/Items/MushroomJournal/RadiationMushroomJournalItem": 4, + "/Lotus/Types/Items/MushroomJournal/VoidMushroomJournalItem": 5 +}; + +const syndicateTitleThresholds = [0, 1, 2, 6, 12, 16]; + +const getJournalRank = (journalEntry: IJournalEntry): number => { + const k = journalEntriesRank[journalEntry.EntryType]; + if (!k) return 0; + + const thresholds = [k * 1, k * 3, k * 6]; + + if (journalEntry.Progress >= thresholds[2]) return 3; + if (journalEntry.Progress >= thresholds[1]) return 2; + if (journalEntry.Progress >= thresholds[0]) return 1; + return 0; +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index dcb92b89..909639cc 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -88,7 +88,9 @@ import type { IGoalProgressDatabase, IGoalProgressClient, IKubrowPetPrintClient, - IKubrowPetPrintDatabase + IKubrowPetPrintDatabase, + INokkoColony, + IJournalEntry } from "../../types/inventoryTypes/inventoryTypes.ts"; import { equipmentKeys } from "../../types/inventoryTypes/inventoryTypes.ts"; import type { IOid, ITypeCount } from "../../types/commonTypes.ts"; @@ -1424,6 +1426,22 @@ const hubNpcCustomizationSchema = new Schema( { _id: false } ); +const journalEntrySchema = new Schema( + { + EntryType: String, + Progress: Number + }, + { _id: false } +); + +const nokkoColonySchema = new Schema( + { + FeedLevel: Number, + JournalEntries: [journalEntrySchema] + }, + { _id: false } +); + const inventorySchema = new Schema( { accountOwnerId: Schema.Types.ObjectId, @@ -1840,7 +1858,9 @@ const inventorySchema = new Schema( ClaimedJunctionChallengeRewards: { type: [String], default: undefined }, - SpecialItemRewardAttenuation: { type: [rewardAttenutationSchema], default: undefined } + SpecialItemRewardAttenuation: { type: [rewardAttenutationSchema], default: undefined }, + + NokkoColony: { type: nokkoColonySchema, default: undefined } }, { timestamps: { createdAt: "Created", updatedAt: false } } ); diff --git a/src/routes/api.ts b/src/routes/api.ts index 59bbc83b..5502a0c8 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -50,6 +50,7 @@ import { dronesController } from "../controllers/api/dronesController.ts"; import { endlessXpController } from "../controllers/api/endlessXpController.ts"; import { entratiLabConquestModeController } from "../controllers/api/entratiLabConquestModeController.ts"; import { evolveWeaponController } from "../controllers/api/evolveWeaponController.ts"; +import { feedPrinceController } from "../controllers/api/feedPrinceController.ts"; import { findSessionsController } from "../controllers/api/findSessionsController.ts"; import { fishmongerController } from "../controllers/api/fishmongerController.ts"; import { focusController } from "../controllers/api/focusController.ts"; @@ -116,6 +117,7 @@ import { removeFromGuildController } from "../controllers/api/removeFromGuildCon import { removeIgnoredUserController } from "../controllers/api/removeIgnoredUserController.ts"; import { renamePetController } from "../controllers/api/renamePetController.ts"; import { rerollRandomModController } from "../controllers/api/rerollRandomModController.ts"; +import { researchMushroomController } from "../controllers/api/researchMushroomController.ts"; import { resetQuestProgressController } from "../controllers/api/resetQuestProgressController.ts"; import { retrievePetFromStasisController } from "../controllers/api/retrievePetFromStasisController.ts"; import { saveDialogueController } from "../controllers/api/saveDialogueController.ts"; @@ -271,6 +273,7 @@ apiRouter.post("/drones.php", dronesController); apiRouter.post("/endlessXp.php", endlessXpController); apiRouter.post("/entratiLabConquestMode.php", entratiLabConquestModeController); apiRouter.post("/evolveWeapon.php", evolveWeaponController); +apiRouter.post("/feedPrince.php", feedPrinceController); apiRouter.post("/findSessions.php", findSessionsController); apiRouter.post("/fishmonger.php", fishmongerController); apiRouter.post("/focus.php", focusController); @@ -318,6 +321,7 @@ apiRouter.post("/removeFromGuild.php", removeFromGuildController); apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController); apiRouter.post("/renamePet.php", renamePetController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController); +apiRouter.post("/researchMushroom.php", researchMushroomController); apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController); apiRouter.post("/saveDialogue.php", saveDialogueController); apiRouter.post("/saveLoadout.php", saveLoadoutController); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index d4c96e66..cc44c061 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -436,6 +436,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Ship?: IOrbiterClient; // U22 and below, response only ClaimedJunctionChallengeRewards?: string[]; // U39 SpecialItemRewardAttenuation?: IRewardAttenuation[]; // Baro's Void Surplus + NokkoColony?: INokkoColony; // Field Guide } export interface IAffiliation { @@ -1220,3 +1221,13 @@ export interface IHubNpcCustomization { Pattern: string; Tag: string; } + +export interface IJournalEntry { + EntryType: string; + Progress: number; +} + +export interface INokkoColony { + FeedLevel: number; + JournalEntries: IJournalEntry[]; +} -- 2.47.2 From 632a9fab6eb5bc7bd858e3c30eeb965f245b5fe4 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Fri, 24 Oct 2025 06:38:35 +0200 Subject: [PATCH 2/6] exclude nightcap from Supported syndicate cheat --- static/webui/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/script.js b/static/webui/script.js index 697631cd..f1a899be 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -559,7 +559,7 @@ function fetchItemList() { }); } else if (type == "Syndicates") { items.forEach(item => { - if (item.uniqueName === "ConclaveSyndicate") { + if (["ConclaveSyndicate", "NightcapJournalSyndicate"].includes(item.uniqueName)) { return; } if (item.uniqueName.startsWith("RadioLegion")) { -- 2.47.2 From 244c6df4f039a659739a3f164f98bb5179dac54f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 24 Oct 2025 08:58:28 +0200 Subject: [PATCH 3/6] fix completing a node intro completely reordering NodeIntrosCompleted --- src/services/inventoryService.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 17cdf370..269e5faf 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1400,6 +1400,12 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr const inventoryChanges: IInventoryChanges = {}; for (const node of data.NodeIntrosCompleted) { + if (inventory.NodeIntrosCompleted.indexOf(node) != -1) { + continue; + } + inventory.NodeIntrosCompleted.push(node); + logger.debug(`completed dialogue/cutscene for ${node}`); + if (node == "TC2025") { inventoryChanges.ShipDecorations = [ { @@ -1422,14 +1428,6 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/ClearedFiveLoopsEmailItem", inventoryChanges); } } - - // Combine the two arrays into one. - data.NodeIntrosCompleted = inventory.NodeIntrosCompleted.concat(data.NodeIntrosCompleted); - - // Remove duplicate entries. - const nodes = [...new Set(data.NodeIntrosCompleted)]; - - inventory.NodeIntrosCompleted = nodes; await inventory.save(); return { -- 2.47.2 From 6da3e3034877eddc5d0e981e990d1de57dda27af Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 24 Oct 2025 09:01:42 +0200 Subject: [PATCH 4/6] give cheeky sprodling items for completing all trips --- src/services/inventoryService.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 269e5faf..d5dd1549 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1391,7 +1391,10 @@ export const addStanding = ( // TODO: AffiliationMods support (Nightwave). export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise => { - const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems ShipDecorations"); + const inventory = await getInventory( + accountId, + "NodeIntrosCompleted MiscItems ShipDecorations FlavourItems WeaponSkins" + ); // Make it an array for easier parsing. if (typeof data.NodeIntrosCompleted === "string") { @@ -1426,6 +1429,13 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/BeatCaliberChicksEmailItem", inventoryChanges); } else if (node == "ClearedFiveLoops") { await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/ClearedFiveLoopsEmailItem", inventoryChanges); + } else if (node == "NokkoVisions_AllCompleted") { + addCustomization( + inventory, + "/Lotus/Types/StoreItems/AvatarImages/Warframes/NokkoBabySecretGlyph", + inventoryChanges + ); + addSkin(inventory, "/Lotus/Upgrades/Skins/Clan/NokkoBabySecretEmblemItem", inventoryChanges); } } await inventory.save(); -- 2.47.2 From 6fd7967b4fe2cf47eecdd0d99a7dcc137750c78e Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:57:24 +0200 Subject: [PATCH 5/6] check if previous vision is completed --- src/controllers/api/feedPrinceController.ts | 34 +++++++++++++++------ 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/controllers/api/feedPrinceController.ts b/src/controllers/api/feedPrinceController.ts index 89c6fce6..67fd07db 100644 --- a/src/controllers/api/feedPrinceController.ts +++ b/src/controllers/api/feedPrinceController.ts @@ -6,24 +6,40 @@ import { logger } from "../../utils/logger.ts"; export const feedPrinceController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); - const inventory = await getInventory(accountId, "MiscItems NokkoColony"); + const inventory = await getInventory(accountId, "MiscItems NokkoColony NodeIntrosCompleted"); const payload = getJSONfromString(String(req.body)); switch (payload.Mode) { case "r": { - const InventoryChanges = {}; - addMiscItem(inventory, "/Lotus/Types/Items/MiscItems/MushroomFood", payload.Amount * -1, InventoryChanges); inventory.NokkoColony ??= { FeedLevel: 0, JournalEntries: [] }; + const InventoryChanges = {}; inventory.NokkoColony.FeedLevel += payload.Amount; - await inventory.save(); - res.json({ - FeedSucceeded: true, - FeedLevel: inventory.NokkoColony.FeedLevel, - InventoryChanges - }); + if ( + (!inventory.NodeIntrosCompleted.includes("CompletedVision1") && inventory.NokkoColony.FeedLevel > 20) || + (!inventory.NodeIntrosCompleted.includes("CompletedVision2") && inventory.NokkoColony.FeedLevel > 60) + ) { + res.json({ + FeedSucceeded: false, + FeedLevel: inventory.NokkoColony.FeedLevel - payload.Amount, + InventoryChanges + }); + } else { + addMiscItem( + inventory, + "/Lotus/Types/Items/MiscItems/MushroomFood", + payload.Amount * -1, + InventoryChanges + ); + await inventory.save(); + res.json({ + FeedSucceeded: true, + FeedLevel: inventory.NokkoColony.FeedLevel, + InventoryChanges + }); + } break; } -- 2.47.2 From dd09bcdb4557f0e708e8fd26af222b4111ba5792 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:03:10 +0200 Subject: [PATCH 6/6] add IFeedPrinceResponse type --- src/controllers/api/feedPrinceController.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/controllers/api/feedPrinceController.ts b/src/controllers/api/feedPrinceController.ts index 67fd07db..8c6bde17 100644 --- a/src/controllers/api/feedPrinceController.ts +++ b/src/controllers/api/feedPrinceController.ts @@ -3,6 +3,7 @@ import { getAccountIdForRequest } from "../../services/loginService.ts"; import { addMiscItem, getInventory } from "../../services/inventoryService.ts"; import { getJSONfromString } from "../../helpers/stringHelpers.ts"; import { logger } from "../../utils/logger.ts"; +import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; export const feedPrinceController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -15,7 +16,7 @@ export const feedPrinceController: RequestHandler = async (req, res) => { FeedLevel: 0, JournalEntries: [] }; - const InventoryChanges = {}; + const InventoryChanges: IInventoryChanges = {}; inventory.NokkoColony.FeedLevel += payload.Amount; if ( (!inventory.NodeIntrosCompleted.includes("CompletedVision1") && inventory.NokkoColony.FeedLevel > 20) || @@ -25,7 +26,7 @@ export const feedPrinceController: RequestHandler = async (req, res) => { FeedSucceeded: false, FeedLevel: inventory.NokkoColony.FeedLevel - payload.Amount, InventoryChanges - }); + } satisfies IFeedPrinceResponse); } else { addMiscItem( inventory, @@ -38,7 +39,7 @@ export const feedPrinceController: RequestHandler = async (req, res) => { FeedSucceeded: true, FeedLevel: inventory.NokkoColony.FeedLevel, InventoryChanges - }); + } satisfies IFeedPrinceResponse); } break; } @@ -53,3 +54,9 @@ interface IFeedPrinceRequest { Mode: string; // r Amount: number; } + +interface IFeedPrinceResponse { + FeedSucceeded: boolean; + FeedLevel: number; + InventoryChanges: IInventoryChanges; +} -- 2.47.2