Compare commits

...

7 Commits

Author SHA1 Message Date
541ec3d702 feat: claiming of tennolive relay's secret (#2569)
Reviewed-on: OpenWF/SpaceNinjaServer#2569
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:51:07 -07:00
0a28eab65d feat: worldState.tennoLiveRelay config (#2568)
Re #2531

Reviewed-on: OpenWF/SpaceNinjaServer#2568
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:50:43 -07:00
8e639a16bd feat: initial protovyre/evolving cosmetics (#2566)
Basic handling of sending the challenge rewards to the inbox upon completion.

Re #2485. Still missing handling for the Protovyre armor pieces which require killing sentients.

Reviewed-on: OpenWF/SpaceNinjaServer#2566
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:50:23 -07:00
522924a823 chore: remove empty ModularParts arrays from equipment (#2565)
Reviewed-on: OpenWF/SpaceNinjaServer#2565
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:49:55 -07:00
48e3f324e2 chore: log when worldState time is behind real time + make sure client knows fissures are active (#2562)
Reviewed-on: OpenWF/SpaceNinjaServer#2562
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:49:34 -07:00
8f77c722cb chore(webui): update German translation (#2564)
Reviewed-on: OpenWF/SpaceNinjaServer#2564
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-29 07:34:04 -07:00
e7287933b5 chore(webui): update Chinese translation (#2563)
Reviewed-on: OpenWF/SpaceNinjaServer#2563
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-29 07:33:57 -07:00
14 changed files with 162 additions and 25 deletions

View File

@ -70,6 +70,7 @@
"creditBoost": false,
"affinityBoost": false,
"resourceBoost": false,
"tennoLiveRelay": false,
"starDays": true,
"galleonOfGhouls": 0,
"eidolonOverride": "",

View File

@ -10,6 +10,7 @@ import { logger } from "@/src/utils/logger";
export const updateChallengeProgressController: RequestHandler = async (req, res) => {
const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body));
const account = await getAccountForRequest(req);
logger.debug(`challenge report:`, challenges);
const inventory = await getInventory(
account._id.toString(),
@ -17,7 +18,7 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
);
let affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress) {
affiliationMods = addChallenges(
affiliationMods = await addChallenges(
account,
inventory,
challenges.ChallengeProgress,

View File

@ -81,6 +81,7 @@ export interface IConfig {
creditBoost?: boolean;
affinityBoost?: boolean;
resourceBoost?: boolean;
tennoLiveRelay?: boolean;
starDays?: boolean;
galleonOfGhouls?: number;
eidolonOverride?: string;

View File

@ -25,7 +25,8 @@ import {
INemesisWeaponTargetFingerprint,
INemesisPetTargetFingerprint,
IDialogueDatabase,
IKubrowPetPrintClient
IKubrowPetPrintClient,
equipmentKeys
} from "@/src/types/inventoryTypes/inventoryTypes";
import { IGenericUpdate, IUpdateNodeIntrosResponse } from "@/src/types/genericUpdate";
import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "@/src/types/requestTypes";
@ -1341,7 +1342,7 @@ export const addStanding = (
// TODO: AffiliationMods support (Nightwave).
export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise<IUpdateNodeIntrosResponse> => {
const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems");
const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems ShipDecorations");
// Make it an array for easier parsing.
if (typeof data.NodeIntrosCompleted === "string") {
@ -1350,7 +1351,15 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr
const inventoryChanges: IInventoryChanges = {};
for (const node of data.NodeIntrosCompleted) {
if (node == "KayaFirstVisitPack") {
if (node == "TC2025") {
inventoryChanges.ShipDecorations = [
{
ItemType: "/Lotus/Types/Items/ShipDecos/TauGrineerLancerBobbleHead",
ItemCount: 1
}
];
addShipDecorations(inventory, inventoryChanges.ShipDecorations);
} else if (node == "KayaFirstVisitPack") {
inventoryChanges.MiscItems = [
{
ItemType: "/Lotus/Types/Items/MiscItems/1999FixedStickersPack",
@ -1903,25 +1912,87 @@ export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr:
});
};
export const addChallenges = (
const challengeRewardsInboxMessages: Record<string, IMessageCreationTemplate> = {
SentEvoEphemeraRankOne: {
sub: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockAName",
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockADesc",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
att: ["/Lotus/Upgrades/Skins/Effects/NarmerEvolvingEphemeraB"]
},
SentEvoEphemeraRankTwo: {
sub: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockBName",
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockBDesc",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
att: ["/Lotus/Upgrades/Skins/Effects/NarmerEvolvingEphemeraC"]
},
SentEvoSyandanaRankOne: {
sub: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockAName",
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockADesc",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
att: ["/Lotus/Upgrades/Skins/Scarves/NarmerEvolvingSyandanaBCape"]
},
SentEvoSyandanaRankTwo: {
sub: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockBName",
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockBDesc",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
att: ["/Lotus/Upgrades/Skins/Scarves/NarmerEvolvingSyandanaCCape"]
},
SentEvoSekharaRankOne: {
sub: "/Lotus/Language/Inbox/EvolvingSekharaUnlockAName",
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/EvolvingSekharaUnlockADesc",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
att: ["/Lotus/Upgrades/Skins/Clan/ZarimanEvolvingSekharaBadgeItemB"]
},
SentEvoSekharaRankTwo: {
sub: "/Lotus/Language/Inbox/EvolvingSekharaUnlockBName",
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/EvolvingSekharaUnlockBDesc",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
att: ["/Lotus/Upgrades/Skins/Clan/ZarimanEvolvingSekharaBadgeItemC"]
}
};
export const addChallenges = async (
account: TAccountDocument,
inventory: TInventoryDatabaseDocument,
ChallengeProgress: IChallengeProgress[],
SeasonChallengeCompletions: ISeasonChallenge[] | undefined
): IAffiliationMods[] => {
ChallengeProgress.forEach(({ Name, Progress }) => {
const itemIndex = inventory.ChallengeProgress.findIndex(i => i.Name === Name);
if (itemIndex !== -1) {
inventory.ChallengeProgress[itemIndex].Progress = Progress;
): Promise<IAffiliationMods[]> => {
for (const { Name, Progress, Completed } of ChallengeProgress) {
let dbChallenge = inventory.ChallengeProgress.find(x => x.Name == Name);
if (dbChallenge) {
dbChallenge.Progress = Progress;
} else {
inventory.ChallengeProgress.push({ Name, Progress });
dbChallenge = { Name, Progress };
inventory.ChallengeProgress.push(dbChallenge);
}
if (Name.startsWith("Calendar")) {
addString(getCalendarProgress(inventory).SeasonProgress.ActivatedChallenges, Name);
}
});
if ((Completed?.length ?? 0) > (dbChallenge.Completed?.length ?? 0)) {
dbChallenge.Completed ??= [];
for (const completion of Completed!) {
if (dbChallenge.Completed.indexOf(completion) == -1) {
if (completion == "challengeRewards") {
if (Name in challengeRewardsInboxMessages) {
await createMessage(account._id, [challengeRewardsInboxMessages[Name]]);
dbChallenge.Completed.push(completion);
// Would love to somehow let the client know about inbox or inventory changes, but there doesn't seem to anything for updateChallengeProgress.
continue;
}
}
logger.warn(`ignoring unknown challenge completion`, { challenge: Name, completion });
}
}
}
}
const affiliationMods: IAffiliationMods[] = [];
if (SeasonChallengeCompletions) {
@ -2117,6 +2188,21 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void =>
inventory.LotusCustomization.syancol = {};
}
}
{
let numFixed = 0;
for (const equipmentKey of equipmentKeys) {
for (const item of inventory[equipmentKey]) {
if (item.ModularParts?.length === 0) {
item.ModularParts = undefined;
++numFixed;
}
}
}
if (numFixed != 0) {
logger.debug(`removed ModularParts from ${numFixed} non-modular items`);
}
}
};
export const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => {

View File

@ -292,7 +292,7 @@ export const addMissionInventoryUpdates = async (
addRecipes(inventory, value);
break;
case "ChallengeProgress":
addChallenges(account, inventory, value, inventoryUpdates.SeasonChallengeCompletions);
await addChallenges(account, inventory, value, inventoryUpdates.SeasonChallengeCompletions);
break;
case "FusionTreasures":
addFusionTreasures(inventory, value);

View File

@ -1038,13 +1038,13 @@ const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => {
};
interface ITimeConstraint {
//name: string;
name: string;
isValidTime: (timeSecs: number) => boolean;
getIdealTimeBefore: (timeSecs: number) => number;
}
const eidolonDayConstraint: ITimeConstraint = {
//name: "eidolon day",
name: "eidolon day",
isValidTime: (timeSecs: number): boolean => {
const eidolonEpoch = 1391992660;
const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000);
@ -1062,7 +1062,7 @@ const eidolonDayConstraint: ITimeConstraint = {
};
const eidolonNightConstraint: ITimeConstraint = {
//name: "eidolon night",
name: "eidolon night",
isValidTime: (timeSecs: number): boolean => {
const eidolonEpoch = 1391992660;
const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000);
@ -1089,7 +1089,7 @@ const eidolonNightConstraint: ITimeConstraint = {
};
const venusColdConstraint: ITimeConstraint = {
//name: "venus cold",
name: "venus cold",
isValidTime: (timeSecs: number): boolean => {
const vallisEpoch = 1541837628;
const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
@ -1115,7 +1115,7 @@ const venusColdConstraint: ITimeConstraint = {
};
const venusWarmConstraint: ITimeConstraint = {
//name: "venus warm",
name: "venus warm",
isValidTime: (timeSecs: number): boolean => {
const vallisEpoch = 1541837628;
const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600);
@ -1321,7 +1321,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
});
} else {
constraints.push({
//name: `duviri ${config.worldState.duviriOverride}`,
name: `duviri ${config.worldState.duviriOverride}`,
isValidTime: (timeSecs: number): boolean => {
const moodIndex = Math.trunc(timeSecs / 7200);
return moodIndex % 5 == desiredMood;
@ -1336,6 +1336,14 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
}
}
const timeSecs = getIdealTimeSatsifyingConstraints(constraints);
if (constraints.length != 0) {
const delta = Math.trunc(Date.now() / 1000) - timeSecs;
if (delta > 1) {
logger.debug(
`reported time is ${delta} seconds behind real time to satisfy selected constraints (${constraints.map(x => x.name).join(", ")})`
);
}
}
const timeMs = timeSecs * 1000;
const day = Math.trunc((timeMs - EPOCH) / 86400000);
const week = Math.trunc(day / 7);
@ -1367,6 +1375,32 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
worldState.PVPChallengeInstances = [];
}
if (config.worldState?.tennoLiveRelay) {
worldState.Goals.push({
_id: {
$oid: "687bf9400000000000000000"
},
Activation: {
$date: {
$numberLong: "1752955200000"
}
},
Expiry: {
$date: {
$numberLong: "2000000000000"
}
},
Count: 0,
Goal: 0,
Success: 0,
Personal: true,
Desc: "/Lotus/Language/Locations/RelayStationTennoConB",
ToolTip: "/Lotus/Language/Locations/RelayStationTennoConDescB",
Icon: "/Lotus/Interface/Icons/Categories/IconTennoLive.png",
Tag: "TennoConRelayB",
Node: "TennoConBHUB6"
});
}
if (config.worldState?.starDays) {
worldState.Goals.push({
_id: { $oid: "67a4dcce2a198564d62e1647" },
@ -1823,7 +1857,10 @@ export const populateFissures = async (worldState: IWorldState): Promise<void> =
_id: toOid(fissure._id),
Region: meta.systemIndex + 1,
Seed: 1337,
Activation: toMongoDate(fissure.Activation),
Activation:
fissure.Activation.getTime() < Date.now() // Activation is in the past?
? { $date: { $numberLong: "1000000000000" } } // Let the client know 'explicitly' to avoid interference from time constraints.
: toMongoDate(fissure.Activation),
Expiry: toMongoDate(fissure.Expiry),
Node: fissure.Node,
MissionType: eMissionType[meta.missionIndex].tag,

View File

@ -74,6 +74,7 @@ export type IInventoryChanges = {
InfestedFoundry?: IInfestedFoundryClient;
Drones?: IDroneClient[];
MiscItems?: IMiscItem[];
ShipDecorations?: ITypeCount[];
EmailItems?: ITypeCount[];
CrewShipRawSalvage?: ITypeCount[];
Nemesis?: Partial<INemesisClient>;

View File

@ -925,6 +925,10 @@
<input class="form-check-input" type="checkbox" id="worldState.resourceBoost" />
<label class="form-check-label" for="worldState.resourceBoost" data-loc="worldState_resourceBoost"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="worldState.tennoLiveRelay" />
<label class="form-check-label" for="worldState.tennoLiveRelay" data-loc="worldState_tennoLiveRelay"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="worldState.starDays" />
<label class="form-check-label" for="worldState.starDays" data-loc="worldState_starDays"></label>

View File

@ -156,8 +156,8 @@ dict = {
invigorations_defensiveLabel: `Defensives Upgrade`,
invigorations_expiryLabel: `Upgrades Ablaufdatum (optional)`,
abilityOverride_label: `[UNTRANSLATED] Ability Override`,
abilityOverride_onSlot: `[UNTRANSLATED] on slot`,
abilityOverride_label: `Fähigkeitsüberschreibung`,
abilityOverride_onSlot: `auf Slot`,
mods_addRiven: `Riven hinzufügen`,
mods_fingerprint: `Fingerabdruck`,
@ -242,6 +242,7 @@ dict = {
worldState_creditBoost: `Event Booster: Credit`,
worldState_affinityBoost: `Event Booster: Erfahrung`,
worldState_resourceBoost: `Event Booster: Ressourcen`,
worldState_tennoLiveRelay: `[UNTRANSLATED] TennoLive Relay`,
worldState_starDays: `Sternen-Tage`,
worldState_galleonOfGhouls: `Galeone der Ghule`,
disabled: `Deaktiviert`,

View File

@ -241,6 +241,7 @@ dict = {
worldState_creditBoost: `Credit Boost`,
worldState_affinityBoost: `Affinity Boost`,
worldState_resourceBoost: `Resource Boost`,
worldState_tennoLiveRelay: `TennoLive Relay`,
worldState_starDays: `Star Days`,
worldState_galleonOfGhouls: `Galleon of Ghouls`,
disabled: `Disabled`,

View File

@ -242,6 +242,7 @@ dict = {
worldState_creditBoost: `Potenciador de Créditos`,
worldState_affinityBoost: `Potenciador de Afinidad`,
worldState_resourceBoost: `Potenciador de Recursos`,
worldState_tennoLiveRelay: `[UNTRANSLATED] TennoLive Relay`,
worldState_starDays: `Días estelares`,
worldState_galleonOfGhouls: `Galeón de Gules`,
disabled: `Desactivado`,

View File

@ -242,6 +242,7 @@ dict = {
worldState_creditBoost: `Booster de Crédit`,
worldState_affinityBoost: `Booster d'Affinité`,
worldState_resourceBoost: `Booster de Ressource`,
worldState_tennoLiveRelay: `[UNTRANSLATED] TennoLive Relay`,
worldState_starDays: `Jours Stellaires`,
worldState_galleonOfGhouls: `Galion des Goules`,
disabled: `Désactivé`,

View File

@ -242,6 +242,7 @@ dict = {
worldState_creditBoost: `[UNTRANSLATED] Credit Boost`,
worldState_affinityBoost: `[UNTRANSLATED] Affinity Boost`,
worldState_resourceBoost: `[UNTRANSLATED] Resource Boost`,
worldState_tennoLiveRelay: `[UNTRANSLATED] TennoLive Relay`,
worldState_starDays: `[UNTRANSLATED] Star Days`,
worldState_galleonOfGhouls: `[UNTRANSLATED] Galleon of Ghouls`,
disabled: `[UNTRANSLATED] Disabled`,

View File

@ -156,8 +156,8 @@ dict = {
invigorations_defensiveLabel: `功能型属性`,
invigorations_expiryLabel: `活化时效(可选)`,
abilityOverride_label: `[UNTRANSLATED] Ability Override`,
abilityOverride_onSlot: `[UNTRANSLATED] on slot`,
abilityOverride_label: `技能替换`,
abilityOverride_onSlot: `槽位`,
mods_addRiven: `添加裂罅MOD`,
mods_fingerprint: `印记`,
@ -242,6 +242,7 @@ dict = {
worldState_creditBoost: `现金加成`,
worldState_affinityBoost: `经验加成`,
worldState_resourceBoost: `资源加成`,
worldState_tennoLiveRelay: `[UNTRANSLATED] TennoLive Relay`,
worldState_starDays: `活动:星日`,
worldState_galleonOfGhouls: `战术警报:尸鬼的帆船战舰`,
disabled: `关闭/取消配置`,