feat(goals): cetus events (#2598)

Includes `Plague Star` and `Ghoul Purge`.
Translation for webUI taken from game files.
Re #1103

Reviewed-on: OpenWF/SpaceNinjaServer#2598
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
This commit is contained in:
AMelonInsideLemon 2025-08-11 08:08:40 -07:00 committed by Sainan
parent 9129bdb5fc
commit 51c0ddda38
13 changed files with 305 additions and 38 deletions

View File

@ -72,6 +72,8 @@
"resourceBoost": false,
"tennoLiveRelay": false,
"galleonOfGhouls": 0,
"ghoulEmergenceOverride": null,
"plagueStarOverride": null,
"starDaysOverride": null,
"eidolonOverride": "",
"vallisOverride": "",

View File

@ -84,6 +84,8 @@ export interface IConfig {
tennoLiveRelay?: boolean;
baroTennoConRelay?: boolean;
galleonOfGhouls?: number;
ghoulEmergenceOverride?: boolean;
plagueStarOverride?: boolean;
starDaysOverride?: boolean;
eidolonOverride?: string;
vallisOverride?: string;

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 (
}
}
AffiliationMods ??= [];
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 specialCase = [
{ 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 = specialCase.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,72 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
});
}
const firstNovemberWeekday = new Date(Date.UTC(date.getUTCFullYear(), 10, 1)).getUTCDay();
const firstNovemberMondayOffset = (8 - firstNovemberWeekday) % 7;
const plagueStarStart = Date.UTC(date.getUTCFullYear(), 10, firstNovemberMondayOffset + 1, 16);
const plagueStarEnd = Date.UTC(date.getUTCFullYear(), 10, firstNovemberMondayOffset + 15, 16);
const isPlagueStarActive = timeMs >= plagueStarStart && timeMs < plagueStarEnd;
if (config.worldState?.plagueStarOverride ?? isPlagueStarActive) {
worldState.Goals.push({
_id: { $oid: "654a5058c757487cdb11824f" },
Activation: {
$date: {
$numberLong: config.worldState?.plagueStarOverride ? "1699372800000" : plagueStarStart.toString()
}
},
Expiry: {
$date: {
$numberLong: config.worldState?.plagueStarOverride ? "2000000000000" : plagueStarEnd.toString()
}
},
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 +1699,103 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
pushClassicBounties(worldState.SyndicateMissions, bountyCycle);
} while (isBeforeNextExpectedWorldStateRefresh(timeMs, bountyCycleEnd) && ++bountyCycle);
const ghoulsCycleDay = day % 21;
const isGhoulEmergenceActive = ghoulsCycleDay >= 17 && ghoulsCycleDay <= 20; // 4 days for event and 17 days for break
if (config.worldState?.ghoulEmergenceOverride ?? isGhoulEmergenceActive) {
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);
const activeStartDay = day - ghoulsCycleDay + 17;
const activeEndDay = activeStartDay + 5;
const dayWithFraction = (timeMs - EPOCH) / 86400000;
const progress = (dayWithFraction - activeStartDay) / (activeEndDay - activeStartDay);
const healthPct = 1 - Math.min(Math.max(progress, 0), 1);
worldState.Goals.push({
_id: { $oid: "687ebbe6d1d17841c9c59f38" },
Activation: {
$date: {
$numberLong: config.worldState?.ghoulEmergenceOverride
? "1753204900185"
: Date.UTC(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate() + (day - ghoulsCycleDay + 17)
).toString()
}
},
Expiry: {
$date: {
$numberLong: config.worldState?.ghoulEmergenceOverride
? "2000000000000"
: Date.UTC(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate() + (day - ghoulsCycleDay + 21)
).toString()
}
},
HealthPct: config.worldState?.ghoulEmergenceOverride ? 1 : healthPct,
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 - 1) * 9000) & 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

@ -946,6 +946,22 @@
<option value="3" data-loc="worldState_we3"></option>
</select>
</div>
<div class="form-group mt-2">
<label class="form-label" for="worldState.ghoulEmergenceOverride" data-loc="worldState_ghoulEmergence"></label>
<select class="form-control" id="worldState.ghoulEmergenceOverride" data-default="null">
<option value="null" data-loc="normal"></option>
<option value="true" data-loc="enabled"></option>
<option value="false" data-loc="disabled"></option>
</select>
</div>
<div class="form-group mt-2">
<label class="form-label" for="worldState.plagueStarOverride" data-loc="worldState_plagueStar"></label>
<select class="form-control" id="worldState.plagueStarOverride" data-default="null">
<option value="null" data-loc="normal"></option>
<option value="true" data-loc="enabled"></option>
<option value="false" data-loc="disabled"></option>
</select>
</div>
<div class="form-group mt-2">
<label class="form-label" for="worldState.starDaysOverride" data-loc="worldState_starDays"></label>
<select class="form-control" id="worldState.starDaysOverride" data-default="null">

View File

@ -246,6 +246,8 @@ dict = {
worldState_baroTennoConRelay: `Baros TennoCon Relais`,
worldState_starDays: `Sternen-Tage`,
worldState_galleonOfGhouls: `Galeone der Ghule`,
worldState_ghoulEmergence: `Ghul Ausrottung`,
worldState_plagueStar: `Plagenstern`,
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_ghoulEmergence: `Ghoul Purge`,
worldState_plagueStar: `Plague Star`,
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_ghoulEmergence: `Purga de Gules`,
worldState_plagueStar: `Estrella Infestada`,
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_ghoulEmergence: `Purge des Goules`,
worldState_plagueStar: `Fléau Céleste`,
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_ghoulEmergence: `Избавление от гулей`,
worldState_plagueStar: `Чумная звезда`,
enabled: `Включено`,
disabled: `Отключено`,
worldState_we1: `Выходные 1`,

View File

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

View File

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