Compare commits
12 Commits
c7034b5e6d
...
cf68201ce5
Author | SHA1 | Date | |
---|---|---|---|
cf68201ce5 | |||
5d81e03e1a | |||
3165d9f459 | |||
28d7ca8ca0 | |||
3f0a2bec48 | |||
d28437b658 | |||
a6d2c8b18a | |||
0c884576bd | |||
380f0662a4 | |||
bd83738168 | |||
fa68a1357d | |||
43f3917b09 |
@ -1,14 +1,12 @@
|
||||
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
|
||||
import { generateRewardSeed } from "@/src/services/inventoryService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const getNewRewardSeedController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
|
||||
const rewardSeed = generateRewardSeed();
|
||||
logger.debug(`generated new reward seed: ${rewardSeed}`);
|
||||
await Inventory.updateOne(
|
||||
{
|
||||
accountOwnerId: accountId
|
||||
|
20
src/controllers/api/giveShipDecoAndLoreFragmentController.ts
Normal file
20
src/controllers/api/giveShipDecoAndLoreFragmentController.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { addLoreFragmentScans, addShipDecorations, getInventory } from "@/src/services/inventoryService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { ILoreFragmentScan, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const giveShipDecoAndLoreFragmentController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const inventory = await getInventory(accountId, "LoreFragmentScans ShipDecorations");
|
||||
const data = getJSONfromString<IGiveShipDecoAndLoreFragmentRequest>(String(req.body));
|
||||
addLoreFragmentScans(inventory, data.LoreFragmentScans);
|
||||
addShipDecorations(inventory, data.ShipDecorations);
|
||||
await inventory.save();
|
||||
res.end();
|
||||
};
|
||||
|
||||
interface IGiveShipDecoAndLoreFragmentRequest {
|
||||
LoreFragmentScans: ILoreFragmentScan[];
|
||||
ShipDecorations: ITypeCount[];
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
|
||||
import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
|
||||
import { RequestHandler } from "express";
|
||||
import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService";
|
||||
|
||||
@ -7,5 +7,17 @@ export const setPlacedDecoInfoController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest;
|
||||
await handleSetPlacedDecoInfo(accountId, payload);
|
||||
res.end();
|
||||
res.json({
|
||||
DecoId: payload.DecoId,
|
||||
IsPicture: true,
|
||||
PictureFrameInfo: payload.PictureFrameInfo,
|
||||
BootLocation: payload.BootLocation
|
||||
} satisfies ISetPlacedDecoInfoResponse);
|
||||
};
|
||||
|
||||
interface ISetPlacedDecoInfoResponse {
|
||||
DecoId: string;
|
||||
IsPicture: boolean;
|
||||
PictureFrameInfo?: IPictureFrameInfo;
|
||||
BootLocation?: string;
|
||||
}
|
||||
|
@ -25,7 +25,13 @@ export const upgradesController: RequestHandler = async (req, res) => {
|
||||
operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker"
|
||||
) {
|
||||
updateCurrency(inventory, 10, true);
|
||||
} else if (operation.OperationType != "UOT_ABILITY_OVERRIDE") {
|
||||
} else if (
|
||||
operation.OperationType != "UOT_SWAP_POLARITY" &&
|
||||
operation.OperationType != "UOT_ABILITY_OVERRIDE"
|
||||
) {
|
||||
if (!operation.UpgradeRequirement) {
|
||||
throw new Error(`${operation.OperationType} operation should be free?`);
|
||||
}
|
||||
addMiscItems(inventory, [
|
||||
{
|
||||
ItemType: operation.UpgradeRequirement,
|
||||
|
@ -1213,7 +1213,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
|
||||
accountOwnerId: Schema.Types.ObjectId,
|
||||
SubscribedToEmails: { type: Number, default: 0 },
|
||||
SubscribedToEmailsPersonalized: { type: Number, default: 0 },
|
||||
RewardSeed: Number,
|
||||
RewardSeed: BigInt,
|
||||
|
||||
//Credit
|
||||
RegularCredits: { type: Number, default: 0 },
|
||||
|
@ -62,6 +62,7 @@ import { gildWeaponController } from "@/src/controllers/api/gildWeaponController
|
||||
import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
|
||||
import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController";
|
||||
import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKey";
|
||||
import { giveShipDecoAndLoreFragmentController } from "@/src/controllers/api/giveShipDecoAndLoreFragmentController";
|
||||
import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController";
|
||||
import { guildTechController } from "@/src/controllers/api/guildTechController";
|
||||
import { hostSessionController } from "@/src/controllers/api/hostSessionController";
|
||||
@ -239,6 +240,7 @@ apiRouter.post("/gildWeapon.php", gildWeaponController);
|
||||
apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController);
|
||||
apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController);
|
||||
apiRouter.post("/giveQuestKeyReward.php", giveQuestKeyRewardController);
|
||||
apiRouter.post("/giveShipDecoAndLoreFragment.php", giveShipDecoAndLoreFragmentController);
|
||||
apiRouter.post("/giveStartingGear.php", giveStartingGearController);
|
||||
apiRouter.post("/guildTech.php", guildTechController);
|
||||
apiRouter.post("/hostSession.php", hostSessionController);
|
||||
|
@ -21,7 +21,8 @@ import {
|
||||
ICalendarProgress,
|
||||
IDroneClient,
|
||||
IUpgradeClient,
|
||||
TPartialStartingGear
|
||||
TPartialStartingGear,
|
||||
ILoreFragmentScan
|
||||
} from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate";
|
||||
import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes";
|
||||
@ -1350,6 +1351,17 @@ export const addFocusXpIncreases = (inventory: TInventoryDatabaseDocument, focus
|
||||
inventory.DailyFocus -= focusXpPlus.reduce((a, b) => a + b, 0);
|
||||
};
|
||||
|
||||
export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr: ILoreFragmentScan[]): void => {
|
||||
arr.forEach(clientFragment => {
|
||||
const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType);
|
||||
if (fragment) {
|
||||
fragment.Progress += clientFragment.Progress;
|
||||
} else {
|
||||
inventory.LoreFragmentScans.push(clientFragment);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const addChallenges = (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
ChallengeProgress: IChallengeProgress[],
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
} from "warframe-public-export-plus";
|
||||
import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes";
|
||||
import { logger } from "@/src/utils/logger";
|
||||
import { IRngResult, getRandomElement, getRandomReward } from "@/src/services/rngService";
|
||||
import { IRngResult, SRng, getRandomElement, getRandomReward } from "@/src/services/rngService";
|
||||
import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
|
||||
import {
|
||||
addBooster,
|
||||
@ -23,6 +23,7 @@ import {
|
||||
addGearExpByCategory,
|
||||
addItem,
|
||||
addLevelKeys,
|
||||
addLoreFragmentScans,
|
||||
addMiscItems,
|
||||
addMissionComplete,
|
||||
addMods,
|
||||
@ -30,6 +31,7 @@ import {
|
||||
addShipDecorations,
|
||||
addStanding,
|
||||
combineInventoryChanges,
|
||||
generateRewardSeed,
|
||||
updateCurrency,
|
||||
updateSyndicate
|
||||
} from "@/src/services/inventoryService";
|
||||
@ -53,7 +55,22 @@ import { Loadout } from "../models/inventoryModels/loadoutModel";
|
||||
import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes";
|
||||
import { getWorldState } from "./worldStateService";
|
||||
|
||||
const getRotations = (rotationCount: number, tierOverride: number | undefined): number[] => {
|
||||
const getRotations = (rewardInfo: IRewardInfo, tierOverride: number | undefined): number[] => {
|
||||
// For Spy missions, e.g. 3 vaults cracked = A, B, C
|
||||
if (rewardInfo.VaultsCracked) {
|
||||
const rotations: number[] = [];
|
||||
for (let i = 0; i != rewardInfo.VaultsCracked; ++i) {
|
||||
rotations.push(i);
|
||||
}
|
||||
return rotations;
|
||||
}
|
||||
|
||||
// For Rescue missions
|
||||
if (rewardInfo.rewardTier) {
|
||||
return [rewardInfo.rewardTier];
|
||||
}
|
||||
|
||||
const rotationCount = rewardInfo.rewardQualifications?.length || 0;
|
||||
if (rotationCount === 0) return [0];
|
||||
|
||||
const rotationPattern =
|
||||
@ -69,7 +86,12 @@ const getRotations = (rotationCount: number, tierOverride: number | undefined):
|
||||
return rotatedValues;
|
||||
};
|
||||
|
||||
const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => {
|
||||
const getRandomRewardByChance = (pool: IReward[], rng?: SRng): IRngResult | undefined => {
|
||||
if (rng) {
|
||||
const res = rng.randomReward(pool as IRngResult[]);
|
||||
rng.randomFloat(); // something related to rewards multiplier
|
||||
return res;
|
||||
}
|
||||
return getRandomReward(pool as IRngResult[]);
|
||||
};
|
||||
|
||||
@ -291,14 +313,7 @@ export const addMissionInventoryUpdates = async (
|
||||
break;
|
||||
}
|
||||
case "LoreFragmentScans":
|
||||
value.forEach(clientFragment => {
|
||||
const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType);
|
||||
if (fragment) {
|
||||
fragment.Progress += clientFragment.Progress;
|
||||
} else {
|
||||
inventory.LoreFragmentScans.push(clientFragment);
|
||||
}
|
||||
});
|
||||
addLoreFragmentScans(inventory, value);
|
||||
break;
|
||||
case "LibraryScans":
|
||||
value.forEach(scan => {
|
||||
@ -554,6 +569,11 @@ export const addMissionRewards = async (
|
||||
return { MissionRewards: [] };
|
||||
}
|
||||
|
||||
if (rewardInfo.rewardSeed) {
|
||||
// We're using a reward seed, so give the client a new one in the response. On live, missionInventoryUpdate seems to always provide a fresh one in the response.
|
||||
inventory.RewardSeed = generateRewardSeed();
|
||||
}
|
||||
|
||||
//TODO: check double reward merging
|
||||
const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo, wagerTier);
|
||||
logger.debug("random mission drops:", MissionRewards);
|
||||
@ -567,7 +587,7 @@ export const addMissionRewards = async (
|
||||
const fixedLevelRewards = getLevelKeyRewards(levelKeyName);
|
||||
//logger.debug(`fixedLevelRewards ${fixedLevelRewards}`);
|
||||
if (fixedLevelRewards.levelKeyRewards) {
|
||||
addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, MissionRewards);
|
||||
addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, inventory, MissionRewards, rewardInfo);
|
||||
}
|
||||
if (fixedLevelRewards.levelKeyRewards2) {
|
||||
for (const reward of fixedLevelRewards.levelKeyRewards2) {
|
||||
@ -592,7 +612,14 @@ export const addMissionRewards = async (
|
||||
const node = ExportRegions[missions.Tag];
|
||||
|
||||
//node based credit rewards for mission completion
|
||||
if (node.missionIndex !== 28) {
|
||||
if (
|
||||
node.missionIndex != 23 && // junction
|
||||
node.missionIndex != 28 && // open world
|
||||
missions.Tag != "SolNode761" && // the index
|
||||
missions.Tag != "SolNode762" && // the index
|
||||
missions.Tag != "SolNode763" && // the index
|
||||
missions.Tag != "CrewBattleNode556" // free flight
|
||||
) {
|
||||
const levelCreditReward = getLevelCreditRewards(node);
|
||||
missionCompletionCredits += levelCreditReward;
|
||||
inventory.RegularCredits += levelCreditReward;
|
||||
@ -600,7 +627,7 @@ export const addMissionRewards = async (
|
||||
}
|
||||
|
||||
if (node.missionReward) {
|
||||
missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards);
|
||||
missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards, rewardInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@ -843,7 +870,8 @@ export const addCredits = (
|
||||
export const addFixedLevelRewards = (
|
||||
rewards: IMissionRewardExternal,
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
MissionRewards: IMissionReward[]
|
||||
MissionRewards: IMissionReward[],
|
||||
rewardInfo?: IRewardInfo
|
||||
): number => {
|
||||
let missionBonusCredits = 0;
|
||||
if (rewards.credits) {
|
||||
@ -873,13 +901,24 @@ export const addFixedLevelRewards = (
|
||||
}
|
||||
if (rewards.droptable) {
|
||||
if (rewards.droptable in ExportRewards) {
|
||||
logger.debug(`rolling ${rewards.droptable} for level key rewards`);
|
||||
const reward = getRandomRewardByChance(ExportRewards[rewards.droptable][0]);
|
||||
if (reward) {
|
||||
MissionRewards.push({
|
||||
StoreItem: reward.type,
|
||||
ItemCount: reward.itemCount
|
||||
});
|
||||
const rotations: number[] = [];
|
||||
// This makes it so that /Lotus/Types/Keys/ProteaQuest/ProteaQuestMissionFour gives all 3 Xoris parts
|
||||
if (rewardInfo?.VaultsCracked) {
|
||||
for (let i = 0; i != rewardInfo.VaultsCracked; ++i) {
|
||||
rotations.push(i);
|
||||
}
|
||||
} else {
|
||||
rotations.push(0);
|
||||
}
|
||||
logger.debug(`rolling ${rewards.droptable} for level key rewards`, { rotations });
|
||||
for (const tier of rotations) {
|
||||
const reward = getRandomRewardByChance(ExportRewards[rewards.droptable][tier]);
|
||||
if (reward) {
|
||||
MissionRewards.push({
|
||||
StoreItem: reward.type,
|
||||
ItemCount: reward.itemCount
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.error(`unknown droptable ${rewards.droptable}`);
|
||||
@ -898,6 +937,12 @@ function getLevelCreditRewards(node: IRegion): number {
|
||||
|
||||
function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | undefined): IMissionReward[] {
|
||||
const drops: IMissionReward[] = [];
|
||||
if (RewardInfo.periodicMissionTag?.startsWith("HardDaily")) {
|
||||
drops.push({
|
||||
StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/SteelEssence",
|
||||
ItemCount: 5
|
||||
});
|
||||
}
|
||||
if (RewardInfo.node in ExportRegions) {
|
||||
const region = ExportRegions[RewardInfo.node];
|
||||
let rewardManifests: string[] =
|
||||
@ -922,7 +967,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u
|
||||
|
||||
if (syndicateEntry.Tag === "EntratiSyndicate") {
|
||||
const vault = syndicateEntry.Jobs.find(j => j.locationTag === locationTag);
|
||||
if (vault) job = vault;
|
||||
if (vault && locationTag) job = vault;
|
||||
// if (
|
||||
// [
|
||||
// "DeimosRuinsExterminateBounty",
|
||||
@ -997,8 +1042,11 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u
|
||||
(RewardInfo.JobStage === job.xpAmounts.length - 1 || job.isVault) &&
|
||||
!isEndlessJob
|
||||
) {
|
||||
rewardManifests.push(job.rewards);
|
||||
rotations.push(ExportRewards[job.rewards].length - 1);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (ExportRewards[job.rewards]) {
|
||||
rewardManifests.push(job.rewards);
|
||||
rotations.push(ExportRewards[job.rewards].length - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1041,29 +1089,28 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u
|
||||
} else {
|
||||
logger.error(`Unknown syndicate or tier: ${RewardInfo.challengeMissionId}`);
|
||||
}
|
||||
} else if (RewardInfo.VaultsCracked) {
|
||||
// For Spy missions, e.g. 3 vaults cracked = A, B, C
|
||||
for (let i = 0; i != RewardInfo.VaultsCracked; ++i) {
|
||||
rotations.push(i);
|
||||
}
|
||||
} else {
|
||||
const rotationCount = RewardInfo.rewardQualifications?.length || 0;
|
||||
rotations = getRotations(rotationCount, tierOverride);
|
||||
rotations = getRotations(RewardInfo, tierOverride);
|
||||
}
|
||||
if (rewardManifests.length != 0) {
|
||||
logger.debug(`generating random mission rewards`, { rewardManifests, rotations });
|
||||
}
|
||||
rewardManifests
|
||||
.map(name => ExportRewards[name])
|
||||
.forEach(table => {
|
||||
for (const rotation of rotations) {
|
||||
const rotationRewards = table[rotation];
|
||||
const drop = getRandomRewardByChance(rotationRewards);
|
||||
if (drop) {
|
||||
drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount });
|
||||
}
|
||||
const rng = new SRng(BigInt(RewardInfo.rewardSeed ?? generateRewardSeed()) ^ 0xffffffffffffffffn);
|
||||
rewardManifests.forEach(name => {
|
||||
const table = ExportRewards[name];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!table) {
|
||||
logger.error(`unknown droptable: ${name}`);
|
||||
return;
|
||||
}
|
||||
for (const rotation of rotations) {
|
||||
const rotationRewards = table[rotation];
|
||||
const drop = getRandomRewardByChance(rotationRewards, rng);
|
||||
if (drop) {
|
||||
drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (region.cacheRewardManifest && RewardInfo.EnemyCachesFound) {
|
||||
const deck = ExportRewards[region.cacheRewardManifest];
|
||||
|
@ -31,7 +31,7 @@ const getRewardAtPercentage = <T extends { probability: number }>(pool: T[], per
|
||||
return item;
|
||||
}
|
||||
}
|
||||
throw new Error("What the fuck?");
|
||||
return pool[pool.length - 1];
|
||||
};
|
||||
|
||||
export const getRandomReward = <T extends { probability: number }>(pool: T[]): T | undefined => {
|
||||
@ -142,4 +142,8 @@ export class SRng {
|
||||
this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn;
|
||||
return (Number(this.state >> 38n) & 0xffffff) * 0.000000059604645;
|
||||
}
|
||||
|
||||
randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
|
||||
return getRewardAtPercentage(pool, this.randomFloat());
|
||||
}
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
|
||||
Mailbox?: IMailboxClient;
|
||||
SubscribedToEmails: number;
|
||||
Created: IMongoDate;
|
||||
RewardSeed: number;
|
||||
RewardSeed: number | bigint;
|
||||
RegularCredits: number;
|
||||
PremiumCredits: number;
|
||||
PremiumCreditsFree: number;
|
||||
|
@ -130,7 +130,7 @@ export type IMissionInventoryUpdateRequest = {
|
||||
export interface IRewardInfo {
|
||||
node: string;
|
||||
VaultsCracked?: number; // for Spy missions
|
||||
rewardTier?: number;
|
||||
rewardTier?: number; // for Rescue missions
|
||||
nightmareMode?: boolean;
|
||||
useVaultManifest?: boolean;
|
||||
EnemyCachesFound?: number;
|
||||
@ -141,7 +141,7 @@ export interface IRewardInfo {
|
||||
EOM_AFK?: number;
|
||||
rewardQualifications?: string; // did a Survival for 5 minutes and this was "1"
|
||||
PurgatoryRewardQualifications?: string;
|
||||
rewardSeed?: number;
|
||||
rewardSeed?: number | bigint;
|
||||
periodicMissionTag?: string;
|
||||
|
||||
// for bounties, only EOM_AFK and node are given from above, plus:
|
||||
|
@ -139,7 +139,7 @@ dict = {
|
||||
cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`,
|
||||
cheats_noKimCooldowns: `Keine Wartezeit bei KIM`,
|
||||
cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`,
|
||||
cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`,
|
||||
cheats_noResourceExtractorDronesDamage: `Kein Schaden für Ressourcen-Extraktor-Drohnen`,
|
||||
cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`,
|
||||
cheats_noDojoDecoBuildStage: `Kein Dojo-Deko-Bauvorgang`,
|
||||
cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`,
|
||||
|
Loading…
x
Reference in New Issue
Block a user