feat: nightcap syndicate #2934

Merged
Sainan merged 6 commits from AMelonInsideLemon/SpaceNinjaServer:nightcap-syndicate into main 2025-10-25 00:26:37 -07:00
7 changed files with 234 additions and 12 deletions

View File

@ -0,0 +1,62 @@
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 { IInventoryChanges } from "../../types/purchaseTypes.ts";
export const feedPrinceController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "MiscItems NokkoColony NodeIntrosCompleted");
const payload = getJSONfromString<IFeedPrinceRequest>(String(req.body));
switch (payload.Mode) {
case "r": {
inventory.NokkoColony ??= {
FeedLevel: 0,
JournalEntries: []
};
const InventoryChanges: IInventoryChanges = {};
inventory.NokkoColony.FeedLevel += payload.Amount;
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
} satisfies IFeedPrinceResponse);
} else {
addMiscItem(
inventory,
"/Lotus/Types/Items/MiscItems/MushroomFood",
payload.Amount * -1,
InventoryChanges
);
await inventory.save();
res.json({
FeedSucceeded: true,
FeedLevel: inventory.NokkoColony.FeedLevel,
InventoryChanges
} satisfies IFeedPrinceResponse);
}
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;
}
interface IFeedPrinceResponse {
FeedSucceeded: boolean;
FeedLevel: number;
InventoryChanges: IInventoryChanges;
}

View File

@ -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<IResearchMushroom>(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<string, number> = {
"/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;
};

View File

@ -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<IHubNpcCustomization>(
{ _id: false }
);
const journalEntrySchema = new Schema<IJournalEntry>(
{
EntryType: String,
Progress: Number
},
{ _id: false }
);
const nokkoColonySchema = new Schema<INokkoColony>(
{
FeedLevel: Number,
JournalEntries: [journalEntrySchema]
},
{ _id: false }
);
const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
{
accountOwnerId: Schema.Types.ObjectId,
@ -1840,7 +1858,9 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
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 } }
);

View File

@ -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);

View File

@ -1391,7 +1391,10 @@ export const addStanding = (
// TODO: AffiliationMods support (Nightwave).
export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise<IUpdateNodeIntrosResponse> => {
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") {
@ -1400,6 +1403,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 = [
{
@ -1420,16 +1429,15 @@ 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);
}
}
// 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 {

View File

@ -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[];
}

View File

@ -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")) {