From 099f12a197d53237e0508a76425225a9f0e3d256 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 13 May 2025 20:38:52 -0700 Subject: [PATCH] feat: bounty chemistry bonus (#2070) Re #388 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2070 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/saveDialogueController.ts | 28 +------ src/services/inventoryService.ts | 26 +++++- src/services/missionInventoryUpdateService.ts | 83 ++++++++++++++++++- src/services/rngService.ts | 15 ++++ 4 files changed, 123 insertions(+), 29 deletions(-) diff --git a/src/controllers/api/saveDialogueController.ts b/src/controllers/api/saveDialogueController.ts index 14dc9aa2..171538a7 100644 --- a/src/controllers/api/saveDialogueController.ts +++ b/src/controllers/api/saveDialogueController.ts @@ -1,8 +1,7 @@ -import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { config } from "@/src/services/configService"; -import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService"; +import { addEmailItem, getDialogue, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; +import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { RequestHandler } from "express"; @@ -107,26 +106,3 @@ interface IOtherDialogueInfo { Tag: string; Value: number; } - -const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => { - let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName); - if (!dialogue) { - dialogue = - inventory.DialogueHistory!.Dialogues![ - inventory.DialogueHistory!.Dialogues!.push({ - Rank: 0, - Chemistry: 0, - AvailableDate: new Date(0), - AvailableGiftDate: new Date(0), - RankUpExpiry: new Date(0), - BountyChemExpiry: new Date(0), - QueuedDialogues: [], - Gifts: [], - Booleans: [], - Completed: [], - DialogueName: dialogueName - }) - 1 - ]; - } - return dialogue; -}; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index ae5ded50..bbb68167 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -28,7 +28,8 @@ import { ITraits, ICalendarProgress, INemesisWeaponTargetFingerprint, - INemesisPetTargetFingerprint + INemesisPetTargetFingerprint, + IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; @@ -1906,6 +1907,29 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void => } }; +export const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => { + let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName); + if (!dialogue) { + dialogue = + inventory.DialogueHistory!.Dialogues![ + inventory.DialogueHistory!.Dialogues!.push({ + Rank: 0, + Chemistry: 0, + AvailableDate: new Date(0), + AvailableGiftDate: new Date(0), + RankUpExpiry: new Date(0), + BountyChemExpiry: new Date(0), + QueuedDialogues: [], + Gifts: [], + Booleans: [], + Completed: [], + DialogueName: dialogueName + }) - 1 + ]; + } + return dialogue; +}; + export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICalendarProgress => { const currentSeason = getWorldState().KnownCalendarSeasons[0]; diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index bdaf5fd5..6e90f5e1 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -35,6 +35,7 @@ import { combineInventoryChanges, generateRewardSeed, getCalendarProgress, + getDialogue, giveNemesisPetRecipe, giveNemesisWeaponRecipe, updateCurrency, @@ -63,7 +64,15 @@ import { } from "@/src/helpers/nemesisHelpers"; import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; -import { getLiteSortie, getSortie, idToBountyCycle, idToDay, idToWeek, pushClassicBounties } from "./worldStateService"; +import { + getLiteSortie, + getSortie, + getWorldState, + idToBountyCycle, + idToDay, + idToWeek, + pushClassicBounties +} from "./worldStateService"; import { config } from "./configService"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; import { ISyndicateMissionInfo } from "../types/worldStateTypes"; @@ -1188,8 +1197,9 @@ export const addMissionRewards = async ( } if (rewardInfo.challengeMissionId) { - const [syndicateTag, tierStr] = rewardInfo.challengeMissionId.split("_"); // TODO: third part in HexSyndicate jobs - Chemistry points + const [syndicateTag, tierStr, chemistryStr] = rewardInfo.challengeMissionId.split("_"); const tier = Number(tierStr); + const chemistry = Number(chemistryStr); const isSteelPath = missions?.Tier; if (syndicateTag === "ZarimanSyndicate") { let medallionAmount = tier + 1; @@ -1206,6 +1216,23 @@ export const addMissionRewards = async ( if (isSteelPath) standingAmount *= 1.5; AffiliationMods.push(addStanding(inventory, syndicateTag, standingAmount)); } + if (syndicateTag == "HexSyndicate" && chemistry && tier < 6) { + const seed = getWorldState().SyndicateMissions.find(x => x.Tag == "HexSyndicate")!.Seed; + const { nodes, buddies } = getHexBounties(seed); + const buddy = buddies[tier]; + logger.debug(`Hex seed is ${seed}, giving chemistry for ${buddy}`); + if (missions?.Tag != nodes[tier]) { + logger.warn( + `Uh-oh, tier ${tier} bounty should've been on ${nodes[tier]} but you were just on ${missions?.Tag}` + ); + } + const tomorrowAt0Utc = config.noKimCooldowns + ? Date.now() + : (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000; + const dialogue = getDialogue(inventory, buddy); + dialogue.Chemistry += chemistry; + dialogue.BountyChemExpiry = new Date(tomorrowAt0Utc); + } if (isSteelPath) { await addItem(inventory, "/Lotus/Types/Items/MiscItems/SteelEssence", 1); MissionRewards.push({ @@ -1765,3 +1792,55 @@ const libraryPersonalTargetToAvatar: Record = { "/Lotus/Types/Game/Library/Targets/Research10Target": "/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar" }; + +const node_excluded_buddies: Record = { + SolNode856: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue", + SolNode852: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue", + SolNode851: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue", + SolNode850: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue", + SolNode853: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue", + SolNode854: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue" +}; + +const getHexBounties = (seed: number): { nodes: string[]; buddies: string[] } => { + // We're gonna shuffle these arrays, so they're not truly 'const'. + const nodes: string[] = [ + "SolNode850", + "SolNode851", + "SolNode852", + "SolNode853", + "SolNode854", + "SolNode856", + "SolNode858" + ]; + const excludable_nodes: string[] = ["SolNode851", "SolNode852", "SolNode853", "SolNode854"]; + const buddies: string[] = [ + "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue", + "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue" + ]; + + const rng = new SRng(seed); + rng.shuffleArray(nodes); + rng.shuffleArray(excludable_nodes); + while (nodes.length > buddies.length) { + nodes.splice( + nodes.findIndex(x => x == excludable_nodes[0]), + 1 + ); + excludable_nodes.splice(0, 1); + } + rng.shuffleArray(buddies); + for (let i = 0; i != 6; ++i) { + if (buddies[i] == node_excluded_buddies[nodes[i]]) { + const swapIdx = (i + 1) % buddies.length; + const tmp = buddies[swapIdx]; + buddies[swapIdx] = buddies[i]; + buddies[i] = tmp; + } + } + return { nodes, buddies }; +}; diff --git a/src/services/rngService.ts b/src/services/rngService.ts index 597ae01b..72379ab0 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -115,4 +115,19 @@ export class SRng { randomReward(pool: T[]): T | undefined { return getRewardAtPercentage(pool, this.randomFloat()); } + + churnSeed(its: number): void { + while (its--) { + this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; + } + } + + shuffleArray(arr: T[]): void { + for (let lastIdx = arr.length - 1; lastIdx >= 1; --lastIdx) { + const swapIdx = this.randomInt(0, lastIdx); + const tmp = arr[swapIdx]; + arr[swapIdx] = arr[lastIdx]; + arr[lastIdx] = tmp; + } + } }