feat: credit boosters (+ daily first win) (#2324)
Some checks failed
Build Docker image / docker-amd64 (push) Waiting to run
Build Docker image / docker-arm64 (push) Waiting to run
Build / build (push) Has been cancelled

Daily first win is kinda weird because the client doesn't even seem to acknowledge it.

Also fixed missionCompletionCredits being added to inventory inconsistently (sometimes once, sometimes twice).

Closes #1086
Closes #2322

Reviewed-on: #2324
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
This commit is contained in:
Sainan 2025-06-27 08:20:37 -07:00 committed by Sainan
parent 690b872b5e
commit 4895b4630b
10 changed files with 63 additions and 47 deletions

View File

@ -1,16 +1,12 @@
import { getAccountForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
const checkDailyMissionBonusController: RequestHandler = (_req, res) => { export const checkDailyMissionBonusController: RequestHandler = async (req, res) => {
const data = Buffer.from([ const account = await getAccountForRequest(req);
0x44, 0x61, 0x69, 0x6c, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x6f, 0x6e, 0x75, 0x73, 0x3a, const today = Math.trunc(Date.now() / 86400000) * 86400;
0x31, 0x2d, 0x44, 0x61, 0x69, 0x6c, 0x79, 0x50, 0x56, 0x50, 0x57, 0x69, 0x6e, 0x42, 0x6f, 0x6e, 0x75, 0x73, if (account.DailyFirstWinDate != today) {
0x3a, 0x31, 0x0a res.send("DailyMissionBonus:1-DailyPVPWinBonus:1\n");
]); } else {
res.writeHead(200, { res.send("DailyMissionBonus:0-DailyPVPWinBonus:1\n");
"Content-Type": "text/html", }
"Content-Length": data.length
});
res.end(data);
}; };
export { checkDailyMissionBonusController };

View File

@ -88,7 +88,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
AffiliationMods, AffiliationMods,
SyndicateXPItemReward, SyndicateXPItemReward,
ConquestCompletedMissionsCount ConquestCompletedMissionsCount
} = await addMissionRewards(inventory, missionReport, firstCompletion); } = await addMissionRewards(account, inventory, missionReport, firstCompletion);
if (missionReport.EndOfMatchUpload) { if (missionReport.EndOfMatchUpload) {
inventory.RewardSeed = generateRewardSeed(); inventory.RewardSeed = generateRewardSeed();

View File

@ -26,7 +26,7 @@ export const completeAllMissionsController: RequestHandler = async (req, res) =>
if (mission.Completes == 0) { if (mission.Completes == 0) {
mission.Completes++; mission.Completes++;
if (node.missionReward) { if (node.missionReward) {
addFixedLevelRewards(node.missionReward, inventory, MissionRewards); addFixedLevelRewards(node.missionReward, MissionRewards);
} }
} }
mission.Tier = 1; mission.Tier = 1;

View File

@ -23,9 +23,9 @@ export const setBoosterController: RequestHandler = async (req, res) => {
res.status(400).send("Invalid ItemType provided."); res.status(400).send("Invalid ItemType provided.");
return; return;
} }
const now = Math.floor(Date.now() / 1000); const now = Math.trunc(Date.now() / 1000);
for (const { ItemType, ExpiryDate } of requests) { for (const { ItemType, ExpiryDate } of requests) {
if (ExpiryDate < now) { if (ExpiryDate <= now) {
// remove expired boosters // remove expired boosters
const index = boosters.findIndex(item => item.ItemType === ItemType); const index = boosters.findIndex(item => item.ItemType === ItemType);
if (index !== -1) { if (index !== -1) {

View File

@ -25,7 +25,8 @@ const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
LastLogin: { type: Date, default: 0 }, LastLogin: { type: Date, default: 0 },
LatestEventMessageDate: { type: Date, default: 0 }, LatestEventMessageDate: { type: Date, default: 0 },
LastLoginRewardDate: { type: Number, default: 0 }, LastLoginRewardDate: { type: Number, default: 0 },
LoginDays: { type: Number, default: 1 } LoginDays: { type: Number, default: 1 },
DailyFirstWinDate: { type: Number, default: 0 }
}, },
opts opts
); );

View File

@ -962,6 +962,7 @@ const droptableAliases: Record<string, string> = {
//TODO: return type of partial missioninventoryupdate response //TODO: return type of partial missioninventoryupdate response
export const addMissionRewards = async ( export const addMissionRewards = async (
account: TAccountDocument,
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
{ {
wagerTier: wagerTier, wagerTier: wagerTier,
@ -1009,13 +1010,17 @@ export const addMissionRewards = async (
const fixedLevelRewards = getLevelKeyRewards(levelKeyName); const fixedLevelRewards = getLevelKeyRewards(levelKeyName);
//logger.debug(`fixedLevelRewards ${fixedLevelRewards}`); //logger.debug(`fixedLevelRewards ${fixedLevelRewards}`);
if (fixedLevelRewards.levelKeyRewards) { if (fixedLevelRewards.levelKeyRewards) {
addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, MissionRewards, rewardInfo); missionCompletionCredits += addFixedLevelRewards(
fixedLevelRewards.levelKeyRewards,
MissionRewards,
rewardInfo
);
} }
if (fixedLevelRewards.levelKeyRewards2) { if (fixedLevelRewards.levelKeyRewards2) {
for (const reward of fixedLevelRewards.levelKeyRewards2) { for (const reward of fixedLevelRewards.levelKeyRewards2) {
//quest stage completion credit rewards //quest stage completion credit rewards
if (reward.rewardType == "RT_CREDITS") { if (reward.rewardType == "RT_CREDITS") {
missionCompletionCredits += reward.amount; // will be added to inventory in addCredits missionCompletionCredits += reward.amount;
continue; continue;
} }
MissionRewards.push({ MissionRewards.push({
@ -1044,12 +1049,11 @@ export const addMissionRewards = async (
) { ) {
const levelCreditReward = getLevelCreditRewards(node); const levelCreditReward = getLevelCreditRewards(node);
missionCompletionCredits += levelCreditReward; missionCompletionCredits += levelCreditReward;
inventory.RegularCredits += levelCreditReward;
logger.debug(`levelCreditReward ${levelCreditReward}`); logger.debug(`levelCreditReward ${levelCreditReward}`);
} }
if (node.missionReward) { if (node.missionReward) {
missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards, rewardInfo); missionCompletionCredits += addFixedLevelRewards(node.missionReward, MissionRewards, rewardInfo);
} }
if (rewardInfo.sortieTag == "Mission1") { if (rewardInfo.sortieTag == "Mission1") {
@ -1159,7 +1163,9 @@ export const addMissionRewards = async (
combineInventoryChanges(inventoryChanges, inventoryChange.InventoryChanges); combineInventoryChanges(inventoryChanges, inventoryChange.InventoryChanges);
} }
const credits = addCredits(inventory, { inventory.RegularCredits += missionCompletionCredits;
const credits = await addCredits(account, inventory, {
missionCompletionCredits, missionCompletionCredits,
missionDropCredits: creditDrops ?? 0, missionDropCredits: creditDrops ?? 0,
rngRewardCredits: inventoryChanges.RegularCredits ?? 0 rngRewardCredits: inventoryChanges.RegularCredits ?? 0
@ -1382,48 +1388,61 @@ export const addMissionRewards = async (
}; };
}; };
//creditBonus is not entirely accurate. export const addCredits = async (
//TODO: consider ActiveBoosters account: TAccountDocument,
export const addCredits = (
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
{ {
missionDropCredits, missionDropCredits,
missionCompletionCredits, missionCompletionCredits,
rngRewardCredits rngRewardCredits
}: { missionDropCredits: number; missionCompletionCredits: number; rngRewardCredits: number } }: { missionDropCredits: number; missionCompletionCredits: number; rngRewardCredits: number }
): IMissionCredits => { ): Promise<IMissionCredits> => {
const hasDailyCreditBonus = true;
const totalCredits = missionDropCredits + missionCompletionCredits + rngRewardCredits;
const finalCredits: IMissionCredits = { const finalCredits: IMissionCredits = {
MissionCredits: [missionDropCredits, missionDropCredits], MissionCredits: [missionDropCredits, missionDropCredits],
CreditBonus: [missionCompletionCredits, missionCompletionCredits], CreditsBonus: [missionCompletionCredits, missionCompletionCredits],
TotalCredits: [totalCredits, totalCredits] TotalCredits: [0, 0]
}; };
if (hasDailyCreditBonus) { const today = Math.trunc(Date.now() / 86400000) * 86400;
if (account.DailyFirstWinDate != today) {
account.DailyFirstWinDate = today;
await account.save();
logger.debug(`daily first win, doubling missionCompletionCredits (${missionCompletionCredits})`);
finalCredits.DailyMissionBonus = true;
inventory.RegularCredits += missionCompletionCredits; inventory.RegularCredits += missionCompletionCredits;
finalCredits.CreditBonus[1] *= 2; finalCredits.CreditsBonus[1] *= 2;
finalCredits.MissionCredits[1] *= 2;
finalCredits.TotalCredits[1] *= 2;
} }
if (!hasDailyCreditBonus) { const totalCredits = finalCredits.MissionCredits[1] + finalCredits.CreditsBonus[1] + rngRewardCredits;
return finalCredits; finalCredits.TotalCredits = [totalCredits, totalCredits];
if (config.worldState?.creditBoost) {
inventory.RegularCredits += finalCredits.TotalCredits[1];
finalCredits.TotalCredits[1] += finalCredits.TotalCredits[1];
} }
return { ...finalCredits, DailyMissionBonus: true }; const now = Math.trunc(Date.now() / 1000); // TOVERIFY: Should we maybe subtract mission time as to apply credit boosters that expired during mission?
if ((inventory.Boosters.find(x => x.ItemType == "/Lotus/Types/Boosters/CreditBooster")?.ExpiryDate ?? 0) > now) {
inventory.RegularCredits += finalCredits.TotalCredits[1];
finalCredits.TotalCredits[1] += finalCredits.TotalCredits[1];
}
if ((inventory.Boosters.find(x => x.ItemType == "/Lotus/Types/Boosters/CreditBlessing")?.ExpiryDate ?? 0) > now) {
inventory.RegularCredits += finalCredits.TotalCredits[1];
finalCredits.TotalCredits[1] += finalCredits.TotalCredits[1];
}
return finalCredits;
}; };
export const addFixedLevelRewards = ( export const addFixedLevelRewards = (
rewards: IMissionRewardExternal, rewards: IMissionRewardExternal,
inventory: TInventoryDatabaseDocument,
MissionRewards: IMissionReward[], MissionRewards: IMissionReward[],
rewardInfo?: IRewardInfo rewardInfo?: IRewardInfo
): number => { ): number => {
let missionBonusCredits = 0; let missionBonusCredits = 0;
if (rewards.credits) { if (rewards.credits) {
missionBonusCredits += rewards.credits; missionBonusCredits += rewards.credits;
inventory.RegularCredits += rewards.credits;
} }
if (rewards.items) { if (rewards.items) {
for (const item of rewards.items) { for (const item of rewards.items) {

View File

@ -331,7 +331,7 @@ export const giveKeyChainMissionReward = async (
const fixedLevelRewards = getLevelKeyRewards(missionName); const fixedLevelRewards = getLevelKeyRewards(missionName);
if (fixedLevelRewards.levelKeyRewards) { if (fixedLevelRewards.levelKeyRewards) {
const missionRewards: { StoreItem: string; ItemCount: number }[] = []; const missionRewards: { StoreItem: string; ItemCount: number }[] = [];
addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, missionRewards); inventory.RegularCredits += addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, missionRewards);
for (const reward of missionRewards) { for (const reward of missionRewards) {
await addItem(inventory, fromStoreItem(reward.StoreItem), reward.ItemCount); await addItem(inventory, fromStoreItem(reward.StoreItem), reward.ItemCount);

View File

@ -25,6 +25,7 @@ export interface IDatabaseAccount extends IDatabaseAccountRequiredFields {
LatestEventMessageDate: Date; LatestEventMessageDate: Date;
LastLoginRewardDate: number; LastLoginRewardDate: number;
LoginDays: number; LoginDays: number;
DailyFirstWinDate: number;
} }
// Includes virtual ID // Includes virtual ID

View File

@ -17,9 +17,9 @@ export interface IMissionReward {
} }
export interface IMissionCredits { export interface IMissionCredits {
MissionCredits: number[]; MissionCredits: [number, number];
CreditBonus: number[]; CreditsBonus: [number, number]; // "Credit Reward"; `CreditsBonus[1]` is `CreditsBonus[0] * 2` if DailyMissionBonus
TotalCredits: number[]; TotalCredits: [number, number];
DailyMissionBonus?: boolean; DailyMissionBonus?: boolean;
} }

View File

@ -2289,14 +2289,13 @@ function doAcquireBoosters() {
const ExpiryDate = Date.now() / 1000 + 3 * 24 * 60 * 60; // default 3 days const ExpiryDate = Date.now() / 1000 + 3 * 24 * 60 * 60; // default 3 days
setBooster(uniqueName, ExpiryDate, () => { setBooster(uniqueName, ExpiryDate, () => {
$("#acquire-type-Boosters").val(""); $("#acquire-type-Boosters").val("");
updateInventory();
}); });
} }
function doChangeBoosterExpiry(ItemType, ExpiryDateInput) { function doChangeBoosterExpiry(ItemType, ExpiryDateInput) {
console.log("Changing booster expiry for", ItemType, "to", ExpiryDateInput.value); console.log("Changing booster expiry for", ItemType, "to", ExpiryDateInput.value);
// cast local datetime string to unix timestamp // cast local datetime string to unix timestamp
const ExpiryDate = new Date(ExpiryDateInput.value).getTime() / 1000; const ExpiryDate = Math.trunc(new Date(ExpiryDateInput.value).getTime() / 1000);
if (isNaN(ExpiryDate)) { if (isNaN(ExpiryDate)) {
ExpiryDateInput.addClass("is-invalid").focus(); ExpiryDateInput.addClass("is-invalid").focus();
return false; return false;