feat(goals): cetus events #2598

Merged
Sainan merged 5 commits from AMelonInsideLemon/SpaceNinjaServer:goals-cetus into main 2025-08-11 08:08:41 -07:00
13 changed files with 262 additions and 39 deletions
Showing only changes of commit 4606980d7f - Show all commits

View File

@ -81,7 +81,9 @@
"circuitGameModes": null,
"darvoStockMultiplier": 1,
"varziaOverride": "",
"varziaFullyStocked": false
"varziaFullyStocked": false,
"plagueStar": false,
"ghoulEmergence": false
Sainan marked this conversation as resolved Outdated

Still ordered & named awkwardly:

  • Move it before starDaysOverride in config-vanilla.json + configService.ts to match the webui ordering.
  • Rename to plagueStarOverride and ghoulEmergenceOverride to match starDaysOverride.
Still ordered & named awkwardly: - Move it before `starDaysOverride` in config-vanilla.json + configService.ts to match the webui ordering. - Rename to `plagueStarOverride` and `ghoulEmergenceOverride` to match `starDaysOverride`.
},
"dev": {
"keepVendorsExpired": false

View File

@ -94,6 +94,8 @@ export interface IConfig {
darvoStockMultiplier?: number;
varziaOverride?: string;
varziaFullyStocked?: boolean;
plagueStar?: boolean;
ghoulEmergence?: boolean;
Sainan marked this conversation as resolved Outdated

The ordering here seems a bit weird compared to how they are ordered in the WebUI.

Also, from what I understand, these are periodic events, so something similar to Star Days would seem more appropriate.

The ordering here seems a bit weird compared to how they are ordered in the WebUI. Also, from what I understand, these are periodic events, so something similar to Star Days would seem more appropriate.

Plague Star is... awkwardly periodic. In that it kinda happens whenever DE remembers they got the code for it. There is... zero consistency for it, in terms of when it happens, how long it lasts, or when it's supposed to come back. Ghoul Purge and Thermia Fractures are also similarly awkwardly periodic, only they are properly recurring, just nobody has any idea when they'll actually drop until they do, save for a vague "every few weeks".

image.png

Plague Star is... *awkwardly* periodic. In that it kinda happens whenever DE remembers they got the code for it. There is... zero consistency for it, in terms of when it happens, how long it lasts, or when it's supposed to come back. Ghoul Purge and Thermia Fractures are also similarly awkwardly periodic, only they *are* properly recurring, just nobody has any idea *when* they'll actually drop until they do, save for a vague "every few weeks". ![image.png](/attachments/4e7d95d4-2674-45e8-9b9f-c8fbb36da5e1)

From a quick historical analysis, I can see that ghoul purge is active for 3-4 days and inactive for 15-17 days.

From a quick historical analysis, I can see that ghoul purge is active for 3-4 days and inactive for 15-17 days.
};
dev?: {
keepVendorsExpired?: boolean;

View File

@ -76,7 +76,7 @@ import {
} from "@/src/services/worldStateService";
import { config } from "@/src/services/configService";
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { ISyndicateMissionInfo } from "@/src/types/worldStateTypes";
import { IGoal, ISyndicateMissionInfo } from "@/src/types/worldStateTypes";
import { fromOid } from "@/src/helpers/inventoryHelpers";
import { TAccountDocument } from "@/src/services/loginService";
import { ITypeCount } from "@/src/types/commonTypes";
@ -1259,6 +1259,8 @@ export const addMissionRewards = async (
}
}
if (!AffiliationMods) AffiliationMods ??= [];
AMelonInsideLemon marked this conversation as resolved Outdated

if (!AffiliationMods) seems redundant when you do ??= anyway

`if (!AffiliationMods)` seems redundant when you do `??=` anyway
if (rewardInfo.JobStage != undefined && rewardInfo.jobId) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [jobType, unkIndex, hubNode, syndicateMissionId] = rewardInfo.jobId.split("_");
@ -1266,9 +1268,29 @@ export const addMissionRewards = async (
if (syndicateMissionId) {
pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
}
const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
let syndicateEntry: ISyndicateMissionInfo | IGoal | undefined = syndicateMissions.find(
m => m._id.$oid === syndicateMissionId
);
if (
[
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty"
].some(prefix => jobType.startsWith(prefix))
) {
const { Goals } = getWorldState(undefined);
syndicateEntry = Goals.find(m => m._id.$oid === syndicateMissionId);
if (syndicateEntry) syndicateEntry.Tag = syndicateEntry.JobAffiliationTag!;
}
if (syndicateEntry && syndicateEntry.Jobs) {
let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!];
if (
[
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty"
].some(prefix => jobType.startsWith(prefix))
) {
currentJob = syndicateEntry.Jobs.find(j => j.jobType === jobType)!;
}
if (syndicateEntry.Tag === "EntratiSyndicate") {
if (
[
@ -1311,31 +1333,35 @@ export const addMissionRewards = async (
`Giving ${medallionAmount} medallions for the ${rewardInfo.JobStage} stage of the ${rewardInfo.JobTier} tier bounty`
);
} else {
if (rewardInfo.JobTier! >= 0) {
const specialСase = [
{ endings: ["Heists/HeistProfitTakerBountyOne"], stage: 2, amount: 1000 },
{ endings: ["Hunts/AllTeralystsHunt"], stage: 2, amount: 5000 },
{
endings: [
"Hunts/TeralystHunt",
"Heists/HeistProfitTakerBountyTwo",
"Heists/HeistProfitTakerBountyThree",
"Heists/HeistProfitTakerBountyFour",
"Heists/HeistExploiterBountyOne"
],
amount: 1000
}
];
const specialCaseReward = specialСase.find(
rule =>
rule.endings.some(e => jobType.endsWith(e)) &&
(rule.stage === undefined || rewardInfo.JobStage === rule.stage)
);
if (specialCaseReward) {
addStanding(inventory, syndicateEntry.Tag, specialCaseReward.amount, AffiliationMods);
} else {
addStanding(
inventory,
syndicateEntry.Tag,
Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1)),
AffiliationMods
);
} else {
if (jobType.endsWith("Heists/HeistProfitTakerBountyOne") && rewardInfo.JobStage === 2) {
addStanding(inventory, syndicateEntry.Tag, 1000, AffiliationMods);
}
if (jobType.endsWith("Hunts/AllTeralystsHunt") && rewardInfo.JobStage === 2) {
addStanding(inventory, syndicateEntry.Tag, 5000, AffiliationMods);
}
if (
[
"Hunts/TeralystHunt",
"Heists/HeistProfitTakerBountyTwo",
"Heists/HeistProfitTakerBountyThree",
"Heists/HeistProfitTakerBountyFour",
"Heists/HeistExploiterBountyOne"
].some(ending => jobType.endsWith(ending))
) {
addStanding(inventory, syndicateEntry.Tag, 1000, AffiliationMods);
}
}
}
}
@ -1672,7 +1698,19 @@ function getRandomMissionDrops(
if (syndicateMissionId) {
pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
}
const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
let syndicateEntry: ISyndicateMissionInfo | IGoal | undefined = syndicateMissions.find(
m => m._id.$oid === syndicateMissionId
);
if (
[
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty"
].some(prefix => jobType.startsWith(prefix))
) {
const { Goals } = getWorldState(undefined);
syndicateEntry = Goals.find(m => m._id.$oid === syndicateMissionId);
if (syndicateEntry) syndicateEntry.Tag = syndicateEntry.JobAffiliationTag!;
}
if (syndicateEntry && syndicateEntry.Jobs) {
let job = syndicateEntry.Jobs[RewardInfo.JobTier!];
@ -1757,6 +1795,14 @@ function getRandomMissionDrops(
}
}
}
if (
[
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty"
].some(prefix => jobType.startsWith(prefix))
) {
job = syndicateEntry.Jobs.find(j => j.jobType === jobType)!;
}
rewardManifests = [job.rewards];
if (job.xpAmounts.length > 1) {
const curentStage = RewardInfo.JobStage! + 1;

View File

@ -131,6 +131,13 @@ const eidolonNarmerJobs: readonly string[] = [
"/Lotus/Types/Gameplay/Eidolon/Jobs/Narmer/AttritionBountyLib"
];
const eidolonGhoulJobs: readonly string[] = [
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBountyAss",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBountyExt",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBountyHunt",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBountyRes"
];
const venusJobs: readonly string[] = [
"/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobAmbush",
"/Lotus/Types/Gameplay/Venus/Jobs/VenusArtifactJobExcavation",
@ -1556,6 +1563,65 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
});
}
if (config.worldState?.plagueStar) {
worldState.Goals.push({
_id: { $oid: "654a5058c757487cdb11824f" },
Activation: {
$date: {
$numberLong: "1699372800000"
}
},
Expiry: {
$date: {
$numberLong: "2000000000000"
}
},
Tag: "InfestedPlains",
RegionIdx: 2,
Faction: "FC_INFESTATION",
Desc: "/Lotus/Language/InfestedPlainsEvent/InfestedPlainsBountyName",
ToolTip: "/Lotus/Language/InfestedPlainsEvent/InfestedPlainsBountyDesc",
Icon: "/Lotus/Materials/Emblems/PlagueStarEventBadge_e.png",
JobAffiliationTag: "EventSyndicate",
Jobs: [
{
jobType: "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty",
rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/PlagueStarTableRewards",
minEnemyLevel: 15,
maxEnemyLevel: 25,
xpAmounts: [50, 300, 100, 575]
},
{
jobType: "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBountyAdvanced",
rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/PlagueStarTableRewards",
minEnemyLevel: 55,
maxEnemyLevel: 65,
xpAmounts: [200, 1000, 300, 1700],
requiredItems: [
"/Lotus/StoreItems/Types/Items/Eidolon/InfestedEventIngredient",
"/Lotus/StoreItems/Types/Items/Eidolon/InfestedEventClanIngredient"
],
useRequiredItemsAsMiscItemFee: true
},
{
jobType: "/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBountySteelPath",
rewards: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/PlagueStarTableSteelPathRewards",
minEnemyLevel: 100,
maxEnemyLevel: 110,
xpAmounts: [200, 1100, 400, 2100],
masteryReq: 10,
requiredItems: [
"/Lotus/StoreItems/Types/Items/Eidolon/InfestedEventIngredient",
"/Lotus/StoreItems/Types/Items/Eidolon/InfestedEventClanIngredient"
],
useRequiredItemsAsMiscItemFee: true
}
],
Transmission: "/Lotus/Sounds/Dialog/PlainsMeteorLeadUp/LeadUp/DLeadUp0021Lotus",
InstructionalItem: "/Lotus/Types/StoreItems/Packages/PlagueStarEventStoreItem"
});
}
// Nightwave Challenges
const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel);
if (nightwaveSyndicateTag) {
@ -1626,6 +1692,74 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
pushClassicBounties(worldState.SyndicateMissions, bountyCycle);
} while (isBeforeNextExpectedWorldStateRefresh(timeMs, bountyCycleEnd) && ++bountyCycle);
if (config.worldState?.ghoulEmergence) {
const ghoulPool = [...eidolonGhoulJobs];
const pastGhoulPool = [...eidolonGhoulJobs];
const seed = new SRng(bountyCycle).randomInt(0, 100_000);
const pastSeed = new SRng(bountyCycle - 1).randomInt(0, 100_000);
const rng = new SRng(seed);
const pastRng = new SRng(pastSeed);
worldState.Goals.push({
_id: { $oid: "687ebbe6d1d17841c9c59f38" },
Activation: { $date: { $numberLong: "1753204900185" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
HealthPct: 1,
VictimNode: "SolNode228",
Regions: [2],
Success: 0,
Desc: "/Lotus/Language/GameModes/RecurringGhoulAlert",
ToolTip: "/Lotus/Language/GameModes/RecurringGhoulAlertDesc",
Icon: "/Lotus/Interface/Icons/Categories/IconGhouls256.png",
Tag: "GhoulEmergence",
JobAffiliationTag: "CetusSyndicate",
JobCurrentVersion: {
$oid: ((bountyCycle * 9000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008"
},
Jobs: [
{
jobType: rng.randomElementPop(ghoulPool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/GhoulBountyTableARewards`,
masteryReq: 1,
minEnemyLevel: 15,
maxEnemyLevel: 25,
xpAmounts: [270, 270, 270, 400] // not faithful
},
{
jobType: rng.randomElementPop(ghoulPool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/GhoulBountyTableBRewards`,
masteryReq: 3,
minEnemyLevel: 40,
maxEnemyLevel: 50,
xpAmounts: [480, 480, 480, 710] // not faithful
}
],
JobPreviousVersion: {
$oid: (((bountyCycle - 9000) / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008"
},
PreviousJobs: [
{
jobType: pastRng.randomElementPop(pastGhoulPool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/GhoulBountyTableARewards`,
masteryReq: 1,
minEnemyLevel: 15,
maxEnemyLevel: 25,
xpAmounts: [270, 270, 270, 400] // not faithful
},
{
jobType: pastRng.randomElementPop(pastGhoulPool),
rewards: `/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/GhoulBountyTableBRewards`,
masteryReq: 3,
minEnemyLevel: 40,
maxEnemyLevel: 50,
xpAmounts: [480, 480, 480, 710] // not faithful
}
]
});
}
if (config.worldState?.creditBoost) {
worldState.GlobalUpgrades.push({
_id: { $oid: "5b23106f283a555109666672" },

View File

@ -37,19 +37,46 @@ export interface IGoal {
_id: IOid;
Activation: IMongoDate;
Expiry: IMongoDate;
Count: number;
Goal: number;
Success: number;
Personal: boolean;
Count?: number;
Goal?: number;
HealthPct?: number;
Success?: number;
Personal?: boolean;
Bounty?: boolean;
Faction?: string;
ClampNodeScores?: boolean;
Desc: string;
ToolTip?: string;
Transmission?: string;
InstructionalItem?: string;
Icon: string;
Tag: string;
Node: string;
Node?: string;
VictimNode?: string;
RegionIdx?: number;
Regions?: number[];
MissionKeyName?: string;
Reward?: IMissionReward;
JobAffiliationTag?: string;
Jobs?: ISyndicateJob[];
PreviousJobs?: ISyndicateJob[];
JobCurrentVersion?: IOid;
JobPreviousVersion?: IOid;
}
export interface ISyndicateJob {
jobType?: string;
rewards: string;
masteryReq?: number;
minEnemyLevel: number;
maxEnemyLevel: number;
xpAmounts: number[];
endless?: boolean;
locationTag?: string;
isVault?: boolean;
requiredItems?: string[];
useRequiredItemsAsMiscItemFee?: boolean;
}
export interface ISyndicateMissionInfo {
@ -59,17 +86,7 @@ export interface ISyndicateMissionInfo {
Tag: string;
Seed: number;
Nodes: string[];
Jobs?: {
jobType?: string;
rewards: string;
masteryReq: number;
minEnemyLevel: number;
maxEnemyLevel: number;
xpAmounts: number[];
endless?: boolean;
locationTag?: string;
isVault?: boolean;
}[];
Jobs?: ISyndicateJob[];
}
export interface IGlobalUpgrade {

View File

@ -937,6 +937,14 @@
<input class="form-check-input" type="checkbox" id="worldState.varziaFullyStocked" />
<label class="form-check-label" for="worldState.varziaFullyStocked" data-loc="worldState_varziaFullyStocked"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="worldState.plagueStar" />
<label class="form-check-label" for="worldState.plagueStar" data-loc="worldState_plagueStar"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="worldState.ghoulEmergence" />
<label class="form-check-label" for="worldState.ghoulEmergence" data-loc="worldState_ghoulEmergence"></label>
</div>
<div class="form-group mt-2">
<label class="form-label" for="worldState.galleonOfGhouls" data-loc="worldState_galleonOfGhouls"></label>
<select class="form-control" id="worldState.galleonOfGhouls" data-default="0">

View File

@ -246,6 +246,8 @@ dict = {
worldState_baroTennoConRelay: `Baros TennoCon Relais`,
worldState_starDays: `Sternen-Tage`,
worldState_galleonOfGhouls: `Galeone der Ghule`,
worldState_plagueStar: `Plagenstern`,
worldState_ghoulEmergence: `Ghul Ausrottung`,
enabled: `Aktiviert`,
disabled: `Deaktiviert`,
worldState_we1: `Wochenende 1`,

View File

@ -245,6 +245,8 @@ dict = {
worldState_baroTennoConRelay: `Baro's TennoCon Relay`,
worldState_starDays: `Star Days`,
worldState_galleonOfGhouls: `Galleon of Ghouls`,
worldState_plagueStar: `Plague Star`,
worldState_ghoulEmergence: `Ghoul Purge`,
enabled: `Enabled`,
disabled: `Disabled`,
worldState_we1: `Weekend 1`,

View File

@ -246,6 +246,8 @@ dict = {
worldState_baroTennoConRelay: `Repetidor de Baro de la TennoCon`,
worldState_starDays: `Días estelares`,
worldState_galleonOfGhouls: `Galeón de Gules`,
worldState_plagueStar: `Estrella Infestada`,
worldState_ghoulEmergence: `Purga de Gules`,
enabled: `Activado`,
disabled: `Desactivado`,
worldState_we1: `Semana 1`,

View File

@ -246,6 +246,8 @@ dict = {
worldState_baroTennoConRelay: `[UNTRANSLATED] Baro's TennoCon Relay`,
worldState_starDays: `Jours Stellaires`,
worldState_galleonOfGhouls: `Galion des Goules`,
worldState_plagueStar: `Fléau Céleste`,
worldState_ghoulEmergence: `Purge des Goules`,
enabled: `[UNTRANSLATED] Enabled`,
disabled: `Désactivé`,
worldState_we1: `Weekend 1`,

View File

@ -246,6 +246,8 @@ dict = {
worldState_baroTennoConRelay: `Реле Баро TennoCon`,
worldState_starDays: `Звёздные дни`,
worldState_galleonOfGhouls: `Галеон Гулей`,
worldState_plagueStar: `Чумная звезда`,
worldState_ghoulEmergence: `Избавление от гулей`,
enabled: `Включено`,
disabled: `Отключено`,
worldState_we1: `Выходные 1`,

View File

@ -246,6 +246,8 @@ dict = {
worldState_baroTennoConRelay: `Реле Баро TennoCon`,
worldState_starDays: `Зоряні дні`,
worldState_galleonOfGhouls: `Гульський Галеон`,
worldState_plagueStar: `Морова зірка`,
worldState_ghoulEmergence: `Зачищення від гулів`,
enabled: `Увімкнено`,
disabled: `Вимкнено`,
worldState_we1: `Вихідні 1`,

View File

@ -246,6 +246,8 @@ dict = {
worldState_baroTennoConRelay: `Baro的TennoCon中继站`,
worldState_starDays: `活动:星日`,
worldState_galleonOfGhouls: `战术警报:尸鬼的帆船战舰`,
worldState_plagueStar: `瘟疫之星`,
worldState_ghoulEmergence: `尸鬼净化`,
enabled: `启用`,
disabled: `关闭/取消配置`,
worldState_we1: `活动阶段:第一周`,