Compare commits

..

9 Commits

Author SHA1 Message Date
7aaa81277f use inventory projection in contributeToVaultController
Some checks failed
Build / build (20) (pull_request) Failing after 48s
Build / build (18) (pull_request) Failing after 39s
Build / build (22) (pull_request) Failing after 22s
Build / build (18) (push) Failing after 45s
Build / build (22) (push) Failing after 22s
Build / build (20) (push) Failing after 36s
2025-03-31 13:18:18 +02:00
653091bbae prettier 2025-03-31 13:18:00 +02:00
8659928dca chore: run save operatons in parallel where possible 2025-03-31 13:18:00 +02:00
054abee62c chore: use inventory projection in sellController (#1399)
All checks were successful
Build / build (22) (push) Successful in 45s
Build / build (18) (push) Successful in 1m16s
Build / build (20) (push) Successful in 1m19s
Build Docker image / docker (push) Successful in 1m25s
Yeah, it's not pretty but it's a good amount faster.

Reviewed-on: #1399
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:16:09 -07:00
04d39ed973 chore: use SubdocumentArray.id in some more places (#1400)
Some checks failed
Build Docker image / docker (push) Waiting to run
Build / build (18) (push) Has been cancelled
Build / build (20) (push) Has been cancelled
Build / build (22) (push) Has been cancelled
Reviewed-on: #1400
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:15:32 -07:00
b0f0b61d49 fix: allow completion of unknown nodes (#1395)
Some checks failed
Build / build (18) (push) Has been cancelled
Build / build (20) (push) Has been cancelled
Build / build (22) (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
Reviewed-on: #1395
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:15:00 -07:00
23f8901505 fix: reduce platinum cost of rushing recipes based on progress (#1393)
Some checks failed
Build Docker image / docker (push) Waiting to run
Build / build (18) (push) Has been cancelled
Build / build (20) (push) Has been cancelled
Build / build (22) (push) Has been cancelled
Reviewed-on: #1393
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:14:35 -07:00
d3d966a503 feat: grustrag bolt (#1392)
Some checks failed
Build Docker image / docker (push) Waiting to run
Build / build (18) (push) Has been cancelled
Build / build (20) (push) Has been cancelled
Build / build (22) (push) Has been cancelled
Reviewed-on: #1392
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:14:20 -07:00
48598c145f feat: guild ads (#1390)
Some checks failed
Build / build (22) (push) Waiting to run
Build / build (18) (push) Has been cancelled
Build / build (20) (push) Has been cancelled
Build Docker image / docker (push) Has been cancelled
Reviewed-on: #1390
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:14:00 -07:00
27 changed files with 329 additions and 78 deletions

View File

@ -0,0 +1,20 @@
import { GuildAd } from "@/src/models/guildModel";
import { getGuildForRequestEx, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const cancelGuildAdvertisementController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId");
const guild = await getGuildForRequestEx(req, inventory);
if (!(await hasGuildPermission(guild, accountId, GuildPermission.Advertiser))) {
res.status(400).end();
return;
}
await GuildAd.deleteOne({ GuildId: guild._id });
res.end();
};

View File

@ -99,6 +99,11 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
}
} else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
inventory.BrandedSuits!.splice(
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1
);
}
let InventoryChanges = {};
@ -111,15 +116,23 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
]);
}
if (req.query.rush) {
const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
const start = end - recipe.buildTime;
const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
const progress = secondsElapsed / recipe.buildTime;
logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
const cost = Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5)));
InventoryChanges = {
...InventoryChanges,
...updateCurrency(inventory, recipe.skipBuildTimePrice, true)
...updateCurrency(inventory, cost, true)
};
}
if (recipe.secretIngredientAction != "SIA_UNBRAND") {
InventoryChanges = {
...InventoryChanges,
...(await addItem(inventory, recipe.resultType, recipe.num, false))
};
}
InventoryChanges = {
...InventoryChanges,
...(await addItem(inventory, recipe.resultType, recipe.num, false))
};
await inventory.save();
res.json({ InventoryChanges });
}

View File

@ -1,6 +1,7 @@
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import {
addGuildMemberMiscItemContribution,
getDojoClient,
getGuildForRequestEx,
hasAccessToDojo,
@ -141,8 +142,7 @@ const processContribution = (
ItemCount: ingredientContribution.ItemCount * -1
});
guildMember.MiscItemsContributed ??= [];
guildMember.MiscItemsContributed.push(ingredientContribution);
addGuildMemberMiscItemContribution(guildMember, ingredientContribution);
}
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;

View File

@ -1,4 +1,9 @@
import { GuildMember } from "@/src/models/guildModel";
import {
addGuildMemberMiscItemContribution,
addVaultMiscItems,
getGuildForRequestEx
} from "@/src/services/guildService";
import { addVaultMiscItems, getGuildForRequestEx } from "@/src/services/guildService";
import {
addFusionTreasures,
@ -33,14 +38,8 @@ export const contributeToVaultController: RequestHandler = async (req, res) => {
if (request.MiscItems.length) {
addVaultMiscItems(guild, request.MiscItems);
guildMember.MiscItemsContributed ??= [];
for (const item of request.MiscItems) {
const miscItemContribution = guildMember.MiscItemsContributed.find(x => x.ItemType == item.ItemType);
if (miscItemContribution) {
miscItemContribution.ItemCount += item.ItemCount;
} else {
guildMember.MiscItemsContributed.push(item);
}
addGuildMemberMiscItemContribution(guildMember, item);
addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
}

View File

@ -17,7 +17,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => {
recipe.ingredients.map(x => ({ ItemType: x.ItemType, ItemCount: x.ItemCount * -1 }))
);
const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!;
const item = inventory[payload.Category].id(req.query.ItemId as string)!;
item.Features ??= 0;
item.Features |= EquipmentFeatures.INCARNON_GENESIS;
@ -39,7 +39,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => {
}
]);
const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!;
const item = inventory[payload.Category].id(req.query.ItemId as string)!;
item.Features! &= ~EquipmentFeatures.INCARNON_GENESIS;
} else {
throw new Error(`unexpected evolve weapon action: ${payload.Action}`);

View File

@ -18,17 +18,15 @@ export const focusController: RequestHandler = async (req, res) => {
case FocusOperation.InstallLens: {
const request = JSON.parse(String(req.body)) as ILensInstallRequest;
const inventory = await getInventory(accountId);
for (const item of inventory[request.Category]) {
if (item._id.toString() == request.WeaponId) {
item.FocusLens = request.LensType;
addMiscItems(inventory, [
{
ItemType: request.LensType,
ItemCount: -1
} satisfies IMiscItem
]);
break;
}
const item = inventory[request.Category].id(request.WeaponId);
if (item) {
item.FocusLens = request.LensType;
addMiscItems(inventory, [
{
ItemType: request.LensType,
ItemCount: -1
} satisfies IMiscItem
]);
}
await inventory.save();
res.json({

View File

@ -1,5 +1,6 @@
import { RequestHandler } from "express";
import {
addGuildMemberMiscItemContribution,
getGuildForRequestEx,
getGuildVault,
hasAccessToDojo,
@ -146,8 +147,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
ItemCount: miscItem.ItemCount * -1
});
guildMember.MiscItemsContributed ??= [];
guildMember.MiscItemsContributed.push(miscItem);
addGuildMemberMiscItemContribution(guildMember, miscItem);
}
}
addMiscItems(inventory, miscItemChanges);

View File

@ -28,7 +28,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
// shard installation
const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(accountId);
const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!;
const suit = inventory.Suits.id(request.SuitId.$oid)!;
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
}
@ -56,7 +56,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
// shard removal
const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
const inventory = await getInventory(accountId);
const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!;
const suit = inventory.Suits.id(request.SuitId.$oid)!;
const miscItemChanges: IMiscItem[] = [];
if (suit.ArchonCrystalUpgrades![request.Slot].Color) {

View File

@ -12,9 +12,7 @@ export const nameWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const body = getJSONfromString<INameWeaponRequest>(String(req.body));
const item = inventory[req.query.Category as string as TEquipmentKey].find(
item => item._id.toString() == (req.query.ItemId as string)
)!;
const item = inventory[req.query.Category as string as TEquipmentKey].id(req.query.ItemId as string)!;
if (body.ItemName != "") {
item.ItemName = body.ItemName;
} else {

View File

@ -0,0 +1,75 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { GuildAd, GuildMember } from "@/src/models/guildModel";
import {
addGuildMemberMiscItemContribution,
addVaultMiscItems,
getGuildForRequestEx,
getVaultMiscItemCount,
hasGuildPermissionEx
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getVendorManifestByTypeName, preprocessVendorManifest } from "@/src/services/serversideVendorsService";
import { GuildPermission } from "@/src/types/guildTypes";
import { IPurchaseParams } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
export const postGuildAdvertisementController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId MiscItems");
const guild = await getGuildForRequestEx(req, inventory);
const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }))!;
if (!hasGuildPermissionEx(guild, guildMember, GuildPermission.Advertiser)) {
res.status(400).end();
return;
}
const payload = getJSONfromString<IPostGuildAdvertisementRequest>(String(req.body));
// Handle resource cost
const vendor = preprocessVendorManifest(
getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest")!
);
const offer = vendor.VendorInfo.ItemManifest.find(x => x.StoreItem == payload.PurchaseParams.StoreItem)!;
if (getVaultMiscItemCount(guild, offer.ItemPrices![0].ItemType) >= offer.ItemPrices![0].ItemCount) {
addVaultMiscItems(guild, [
{
ItemType: offer.ItemPrices![0].ItemType,
ItemCount: offer.ItemPrices![0].ItemCount * -1
}
]);
} else {
const miscItem = inventory.MiscItems.find(x => x.ItemType == offer.ItemPrices![0].ItemType);
if (!miscItem || miscItem.ItemCount < offer.ItemPrices![0].ItemCount) {
res.status(400).json("Insufficient funds");
return;
}
miscItem.ItemCount -= offer.ItemPrices![0].ItemCount;
addGuildMemberMiscItemContribution(guildMember, offer.ItemPrices![0]);
await guildMember.save();
await inventory.save();
}
// Create or update ad
await GuildAd.findOneAndUpdate(
{ GuildId: guild._id },
{
Emblem: guild.Emblem,
Expiry: new Date(Date.now() + 12 * 3600 * 1000),
Features: payload.Features,
GuildName: guild.Name,
MemberCount: await GuildMember.countDocuments({ guildId: guild._id, status: 0 }),
RecruitMsg: payload.RecruitMsg,
Tier: guild.Tier
},
{ upsert: true }
);
res.end();
};
interface IPostGuildAdvertisementRequest {
Features: number;
RecruitMsg: string;
Languages: string[];
PurchaseParams: IPurchaseParams;
}

View File

@ -13,7 +13,42 @@ import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
export const sellController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as ISellRequest;
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const requiredFields = new Set();
if (payload.SellCurrency == "SC_RegularCredits") {
requiredFields.add("RegularCredits");
} else if (payload.SellCurrency == "SC_FusionPoints") {
requiredFields.add("FusionPoints");
} else {
requiredFields.add("MiscItems");
}
for (const key of Object.keys(payload.Items)) {
requiredFields.add(key);
}
if (requiredFields.has("Upgrades")) {
requiredFields.add("RawUpgrades");
}
if (payload.Items.Suits) {
requiredFields.add(InventorySlot.SUITS);
}
if (payload.Items.LongGuns || payload.Items.Pistols || payload.Items.Melee) {
requiredFields.add(InventorySlot.WEAPONS);
}
if (payload.Items.SpaceSuits) {
requiredFields.add(InventorySlot.SPACESUITS);
}
if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) {
requiredFields.add(InventorySlot.SPACEWEAPONS);
}
if (payload.Items.Sentinels || payload.Items.SentinelWeapons) {
requiredFields.add(InventorySlot.SENTINELS);
}
if (payload.Items.OperatorAmps) {
requiredFields.add(InventorySlot.AMPS);
}
if (payload.Items.Hoverboards) {
requiredFields.add(InventorySlot.SPACESUITS);
}
const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
// Give currency
if (payload.SellCurrency == "SC_RegularCredits") {

View File

@ -6,12 +6,10 @@ import { WeaponTypeInternal } from "@/src/services/itemDataService";
export const setWeaponSkillTreeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const inventory = await getInventory(accountId, req.query.Category as string);
const payload = getJSONfromString<ISetWeaponSkillTreeRequest>(String(req.body));
const item = inventory[req.query.Category as WeaponTypeInternal].find(
item => item._id.toString() == (req.query.ItemId as string)
)!;
const item = inventory[req.query.Category as WeaponTypeInternal].id(req.query.ItemId as string)!;
item.SkillTree = payload.SkillTree;
await inventory.save();

View File

@ -111,6 +111,8 @@ export const startRecipeController: RequestHandler = async (req, res) => {
inventory.PendingSpectreLoadouts.push(spectreLoadout);
logger.debug("pending spectre loadout", spectreLoadout);
}
} else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
pr.SuitToUnbrand = new Types.ObjectId(startRecipeRequest.Ids[recipe.ingredients.length + 0]);
}
await inventory.save();

View File

@ -5,7 +5,7 @@ import { getInventory } from "@/src/services/inventoryService";
export const popArchonCrystalUpgradeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const suit = inventory.Suits.find(suit => suit._id.toString() == (req.query.oid as string));
const suit = inventory.Suits.id(req.query.oid as string);
if (suit && suit.ArchonCrystalUpgrades) {
suit.ArchonCrystalUpgrades = suit.ArchonCrystalUpgrades.filter(
x => x.UpgradeType != (req.query.type as string)

View File

@ -5,7 +5,7 @@ import { getInventory } from "@/src/services/inventoryService";
export const pushArchonCrystalUpgradeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const suit = inventory.Suits.find(suit => suit._id.toString() == (req.query.oid as string));
const suit = inventory.Suits.id(req.query.oid as string);
if (suit) {
suit.ArchonCrystalUpgrades ??= [];
const count = (req.query.count as number | undefined) ?? 1;

View File

@ -0,0 +1,26 @@
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { GuildAd } from "@/src/models/guildModel";
import { IGuildAdInfoClient } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const getGuildAdsController: RequestHandler = async (req, res) => {
const ads = await GuildAd.find(req.query.tier ? { Tier: req.query.tier } : {});
const guildAdInfos: IGuildAdInfoClient[] = [];
for (const ad of ads) {
guildAdInfos.push({
_id: toOid(ad.GuildId),
CrossPlatformEnabled: true,
Emblem: ad.Emblem,
Expiry: toMongoDate(ad.Expiry),
Features: ad.Features,
GuildName: ad.GuildName,
MemberCount: ad.MemberCount,
OriginalPlatform: 0,
RecruitMsg: ad.RecruitMsg,
Tier: ad.Tier
});
}
res.json({
GuildAdInfos: guildAdInfos
});
};

View File

@ -10,7 +10,8 @@ import {
IGuildLogRoomChange,
IGuildLogEntryRoster,
IGuildLogEntryContributable,
IDojoLeaderboardEntry
IDojoLeaderboardEntry,
IGuildAdDatabase
} from "@/src/types/guildTypes";
import { Document, Model, model, Schema, Types } from "mongoose";
import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel";
@ -165,6 +166,7 @@ const guildSchema = new Schema<IGuildDatabase>(
Ranks: { type: [guildRankSchema], default: defaultRanks },
TradeTax: { type: Number, default: 0 },
Tier: { type: Number, default: 1 },
Emblem: { type: Boolean },
DojoComponents: { type: [dojoComponentSchema], default: [] },
DojoCapacity: { type: Number, default: 100 },
DojoEnergy: { type: Number, default: 5 },
@ -225,3 +227,19 @@ const guildMemberSchema = new Schema<IGuildMemberDatabase>({
guildMemberSchema.index({ accountId: 1, guildId: 1 }, { unique: true });
export const GuildMember = model<IGuildMemberDatabase>("GuildMember", guildMemberSchema);
const guildAdSchema = new Schema<IGuildAdDatabase>({
GuildId: { type: Schema.Types.ObjectId, required: true },
Emblem: Boolean,
Expiry: { type: Date, required: true },
Features: { type: Number, required: true },
GuildName: { type: String, required: true },
MemberCount: { type: Number, required: true },
RecruitMsg: { type: String, required: true },
Tier: { type: Number, required: true }
});
guildAdSchema.index({ GuildId: 1 }, { unique: true });
guildAdSchema.index({ Expiry: 1 }, { expireAfterSeconds: 0 });
export const GuildAd = model<IGuildAdDatabase>("GuildAd", guildAdSchema);

View File

@ -903,7 +903,8 @@ const pendingRecipeSchema = new Schema<IPendingRecipeDatabase>(
CompletionDate: Date,
LongGuns: { type: [EquipmentSchema], default: undefined },
Pistols: { type: [EquipmentSchema], default: undefined },
Melee: { type: [EquipmentSchema], default: undefined }
Melee: { type: [EquipmentSchema], default: undefined },
SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined }
},
{ id: false }
);
@ -920,6 +921,7 @@ pendingRecipeSchema.set("toJSON", {
delete returnedObject.LongGuns;
delete returnedObject.Pistols;
delete returnedObject.Melees;
delete returnedObject.SuitToUnbrand;
(returnedObject as IPendingRecipeClient).CompletionDate = {
$date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() }
};
@ -1484,7 +1486,9 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
EchoesHexConquestHardModeStatus: { type: Number, default: undefined },
EchoesHexConquestCacheScoreMission: { type: Number, default: undefined },
EchoesHexConquestActiveFrameVariants: { type: [String], default: undefined },
EchoesHexConquestActiveStickers: { type: [String], default: undefined }
EchoesHexConquestActiveStickers: { type: [String], default: undefined },
BrandedSuits: { type: [Schema.Types.ObjectId], default: undefined }
},
{ timestamps: { createdAt: "Created", updatedAt: false } }
);
@ -1516,6 +1520,9 @@ inventorySchema.set("toJSON", {
if (inventoryDatabase.EntratiVaultCountResetDate) {
inventoryResponse.EntratiVaultCountResetDate = toMongoDate(inventoryDatabase.EntratiVaultCountResetDate);
}
if (inventoryDatabase.BrandedSuits) {
inventoryResponse.BrandedSuits = inventoryDatabase.BrandedSuits.map(toOid);
}
}
});

View File

@ -9,6 +9,7 @@ import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonContro
import { archonFusionController } from "@/src/controllers/api/archonFusionController";
import { artifactsController } from "@/src/controllers/api/artifactsController";
import { artifactTransmutationController } from "@/src/controllers/api/artifactTransmutationController";
import { cancelGuildAdvertisementController } from "@/src/controllers/api/cancelGuildAdvertisementController";
import { changeDojoRootController } from "@/src/controllers/api/changeDojoRootController";
import { changeGuildRankController } from "@/src/controllers/api/changeGuildRankController";
import { checkDailyMissionBonusController } from "@/src/controllers/api/checkDailyMissionBonusController";
@ -80,6 +81,7 @@ import { nameWeaponController } from "@/src/controllers/api/nameWeaponController
import { nemesisController } from "@/src/controllers/api/nemesisController";
import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController";
import { playerSkillsController } from "@/src/controllers/api/playerSkillsController";
import { postGuildAdvertisementController } from "@/src/controllers/api/postGuildAdvertisementController";
import { projectionManagerController } from "@/src/controllers/api/projectionManagerController";
import { purchaseController } from "@/src/controllers/api/purchaseController";
import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController";
@ -131,6 +133,7 @@ const apiRouter = express.Router();
// get
apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController);
apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController);
apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementController);
apiRouter.get("/changeGuildRank.php", changeGuildRankController);
apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController);
apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController);
@ -228,6 +231,7 @@ apiRouter.post("/nameWeapon.php", nameWeaponController);
apiRouter.post("/nemesis.php", nemesisController);
apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController);
apiRouter.post("/playerSkills.php", playerSkillsController);
apiRouter.post("/postGuildAdvertisement.php", postGuildAdvertisementController);
apiRouter.post("/projectionManager.php", projectionManagerController);
apiRouter.post("/purchase.php", purchaseController);
apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController);

View File

@ -1,11 +1,13 @@
import express from "express";
import { aggregateSessionsController } from "@/src/controllers/dynamic/aggregateSessionsController";
import { getGuildAdsController } from "@/src/controllers/dynamic/getGuildAdsController";
import { getProfileViewingDataController } from "@/src/controllers/dynamic/getProfileViewingDataController";
import { worldStateController } from "@/src/controllers/dynamic/worldStateController";
const dynamicController = express.Router();
dynamicController.get("/aggregateSessions.php", aggregateSessionsController);
dynamicController.get("/getGuildAds.php", getGuildAdsController);
dynamicController.get("/getProfileViewingData.php", getProfileViewingDataController);
dynamicController.get("/worldState.php", worldStateController);

View File

@ -1,7 +1,7 @@
import { Request } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addRecipes, getInventory } from "@/src/services/inventoryService";
import { Guild, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import {
GuildPermission,
@ -298,6 +298,10 @@ const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoCon
}
};
export const getVaultMiscItemCount = (guild: TGuildDatabaseDocument, itemType: string): number => {
return guild.VaultMiscItems?.find(x => x.ItemType == itemType)?.ItemCount ?? 0;
};
export const addVaultMiscItems = (guild: TGuildDatabaseDocument, miscItems: ITypeCount[]): void => {
guild.VaultMiscItems ??= [];
for (const miscItem of miscItems) {
@ -310,6 +314,16 @@ export const addVaultMiscItems = (guild: TGuildDatabaseDocument, miscItems: ITyp
}
};
export const addGuildMemberMiscItemContribution = (guildMember: IGuildMemberDatabase, item: ITypeCount): void => {
guildMember.MiscItemsContributed ??= [];
const miscItemContribution = guildMember.MiscItemsContributed.find(x => x.ItemType == item.ItemType);
if (miscItemContribution) {
miscItemContribution.ItemCount += item.ItemCount;
} else {
guildMember.MiscItemsContributed.push(item);
}
};
export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => {
if (build.guildXpValue) {
guild.ClaimedXP ??= [];
@ -513,4 +527,6 @@ export const deleteGuild = async (guildId: Types.ObjectId): Promise<void> => {
contextInfo: guildId.toString(),
acceptAction: "GUILD_INVITE"
});
await GuildAd.deleteOne({ GuildId: guildId });
};

View File

@ -24,7 +24,6 @@ import {
ExportGear,
ExportKeys,
ExportRecipes,
ExportRegions,
ExportResources,
ExportSentinels,
ExportWarframes,
@ -34,7 +33,6 @@ import {
IMissionReward,
IPowersuit,
IRecipe,
IRegion,
TReward
} from "warframe-public-export-plus";
import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json";
@ -192,16 +190,6 @@ export const getLevelKeyRewards = (
};
};
export const getNode = (nodeName: string): IRegion => {
const node = ExportRegions[nodeName];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!node) {
throw new Error(`Node ${nodeName} not found`);
}
return node;
};
export const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => {
if (questKey in questCompletionItems) {
return questCompletionItems[questKey as keyof typeof questCompletionItems];

View File

@ -4,6 +4,7 @@ import {
ExportRegions,
ExportRewards,
IMissionReward as IMissionRewardExternal,
IRegion,
IReward
} from "warframe-public-export-plus";
import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes";
@ -30,9 +31,9 @@ import {
updateSyndicate
} from "@/src/services/inventoryService";
import { updateQuestKey } from "@/src/services/questService";
import { HydratedDocument } from "mongoose";
import { HydratedDocument, Types } from "mongoose";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getLevelKeyRewards, getNode, toStoreItem } from "@/src/services/itemDataService";
import { getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService";
import { InventoryDocumentProps, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { getEntriesUnsafe } from "@/src/utils/ts-utils";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
@ -45,6 +46,7 @@ import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePe
import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json";
import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json";
import { getInfNodes } from "@/src/helpers/nemesisHelpers";
import { Loadout } from "../models/inventoryModels/loadoutModel";
const getRotations = (rotationCount: number): number[] => {
if (rotationCount === 0) return [0];
@ -89,6 +91,31 @@ export const addMissionInventoryUpdates = async (
if (inventoryUpdates.RewardInfo && inventoryUpdates.RewardInfo.NemesisAbandonedRewards) {
inventory.NemesisAbandonedRewards = inventoryUpdates.RewardInfo.NemesisAbandonedRewards;
}
if (
inventoryUpdates.MissionFailed &&
inventoryUpdates.MissionStatus == "GS_FAILURE" &&
inventoryUpdates.EndOfMatchUpload &&
inventoryUpdates.ObjectiveReached
) {
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!;
const config = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!;
const SuitId = new Types.ObjectId(config.s!.ItemId.$oid);
inventory.BrandedSuits ??= [];
if (!inventory.BrandedSuits.find(x => x.equals(SuitId))) {
inventory.BrandedSuits.push(SuitId);
await createMessage(inventory.accountOwnerId.toString(), [
{
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
msg: "/Lotus/Language/G1Quests/BrandedMessage",
sub: "/Lotus/Language/G1Quests/BrandedTitle",
att: ["/Lotus/Types/Recipes/Components/BrandRemovalBlueprint"],
highPriority: true // I cannot find any content of this within the last 10 years so I can only assume that highPriority is set (it certainly would make sense), but I just don't know for sure that it is so on live.
}
]);
}
}
for (const [key, value] of getEntriesUnsafe(inventoryUpdates)) {
if (value === undefined) {
logger.error(`Inventory update key ${key} has no value `);
@ -443,15 +470,15 @@ export const addMissionRewards = async (
}
}
if (
missions &&
missions.Tag != "" // https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1013
) {
const node = getNode(missions.Tag);
// ignoring tags not in ExportRegions, because it can just be garbage:
// - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1013
// - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1365
if (missions && missions.Tag in ExportRegions) {
const node = ExportRegions[missions.Tag];
//node based credit rewards for mission completion
if (node.missionIndex !== 28) {
const levelCreditReward = getLevelCreditRewards(missions.Tag);
const levelCreditReward = getLevelCreditRewards(node);
missionCompletionCredits += levelCreditReward;
inventory.RegularCredits += levelCreditReward;
logger.debug(`levelCreditReward ${levelCreditReward}`);
@ -634,8 +661,8 @@ export const addFixedLevelRewards = (
return missionBonusCredits;
};
function getLevelCreditRewards(nodeName: string): number {
const minEnemyLevel = getNode(nodeName).minEnemyLevel;
function getLevelCreditRewards(node: IRegion): number {
const minEnemyLevel = node.minEnemyLevel;
return 1000 + (minEnemyLevel - 1) * 100;

View File

@ -84,9 +84,7 @@ export const handleInventoryItemConfigChange = async (
continue;
}
const oldLoadoutConfig = loadout[loadoutSlot].find(
loadout => loadout._id.toString() === loadoutId
);
const oldLoadoutConfig = loadout[loadoutSlot].id(loadoutId);
const { ItemId, ...loadoutConfigItemIdRemoved } = loadoutConfig;
const loadoutConfigDatabase: ILoadoutConfigDatabase = {

View File

@ -61,19 +61,17 @@ export const handleSetShipDecorations = async (
if (placedDecoration.MoveId) {
//moved within the same room
if (placedDecoration.OldRoom === placedDecoration.Room) {
const existingDecorationIndex = roomToPlaceIn.PlacedDecos.findIndex(
deco => deco._id.toString() === placedDecoration.MoveId
);
const existingDecoration = roomToPlaceIn.PlacedDecos.id(placedDecoration.MoveId);
if (existingDecorationIndex === -1) {
if (!existingDecoration) {
throw new Error("decoration to be moved not found");
}
roomToPlaceIn.PlacedDecos[existingDecorationIndex].Pos = placedDecoration.Pos;
roomToPlaceIn.PlacedDecos[existingDecorationIndex].Rot = placedDecoration.Rot;
existingDecoration.Pos = placedDecoration.Pos;
existingDecoration.Rot = placedDecoration.Rot;
if (placedDecoration.Scale) {
roomToPlaceIn.PlacedDecos[existingDecorationIndex].Scale = placedDecoration.Scale;
existingDecoration.Scale = placedDecoration.Scale;
}
await personalRooms.save();

View File

@ -28,6 +28,7 @@ export interface IGuildDatabase {
Ranks: IGuildRank[];
TradeTax: number;
Tier: number;
Emblem?: boolean;
DojoComponents: IDojoComponentDatabase[];
DojoCapacity: number;
@ -223,3 +224,27 @@ export interface IDojoLeaderboardEntry {
r: number; // rank
n: string; // displayName
}
export interface IGuildAdInfoClient {
_id: IOid; // Guild ID
CrossPlatformEnabled: boolean;
Emblem?: boolean;
Expiry: IMongoDate;
Features: number;
GuildName: string;
MemberCount: number;
OriginalPlatform: number;
RecruitMsg: string;
Tier: number;
}
export interface IGuildAdDatabase {
GuildId: Types.ObjectId;
Emblem?: boolean;
Expiry: Date;
Features: number;
GuildName: string;
MemberCount: number;
RecruitMsg: string;
Tier: number;
}

View File

@ -44,6 +44,7 @@ export interface IInventoryDatabase
| "NextRefill"
| "Nemesis"
| "EntratiVaultCountResetDate"
| "BrandedSuits"
| TEquipmentKey
>,
InventoryDatabaseEquipment {
@ -73,6 +74,7 @@ export interface IInventoryDatabase
NextRefill?: Date;
Nemesis?: INemesisDatabase;
EntratiVaultCountResetDate?: Date;
BrandedSuits?: Types.ObjectId[];
}
export interface IQuestKeyDatabase {
@ -346,6 +348,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
EchoesHexConquestCacheScoreMission?: number;
EchoesHexConquestActiveFrameVariants?: string[];
EchoesHexConquestActiveStickers?: string[];
BrandedSuits?: IOid[];
}
export interface IAffiliation {
@ -857,10 +860,11 @@ export interface IPendingRecipeDatabase {
LongGuns?: IEquipmentDatabase[];
Pistols?: IEquipmentDatabase[];
Melee?: IEquipmentDatabase[];
SuitToUnbrand?: Types.ObjectId;
}
export interface IPendingRecipeClient
extends Omit<IPendingRecipeDatabase, "CompletionDate" | "LongGuns" | "Pistols" | "Melee"> {
extends Omit<IPendingRecipeDatabase, "CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand"> {
CompletionDate: IMongoDate;
}