feat: overhaul mana conversion system to unified regen-deduction model
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m2s

- New files: element-distance.ts, conversion-costs.ts, conversion-rates.ts
- All conversion types (discipline, attunement, pact) use unified formula
- Conversion costs scale exponentially by element tier (10^(d+1) raw, 10*(d+1) per component)
- Costs deducted from regen, not from mana pool
- Auto-pause on insufficient regen with UI warning
- Meditation boosts conversion rates (reduced by distance)
- Attunement levels provide +50% multiplicative bonus per level
- Guardian pacts provide +0.15/hr base rate + invoker level bonus
- Removed convertMana, processConvertAction, craftComposite from manaStore
- Stats tab shows per-element conversion breakdown with formulas
- ManaDisplay shows per-element net regen rates
- All 916 tests pass, all files under 400 lines
This commit is contained in:
2026-06-04 18:12:41 +02:00
parent 94a2b671b9
commit ab3afae2a6
19 changed files with 742 additions and 572 deletions
+31 -63
View File
@@ -1,6 +1,10 @@
// ─── Attunement Definitions ─────────────────────────────────────────────────────
// Attunements are class-like abilities tied to body locations
// Each provides unique capabilities, primary mana types, and skill access
//
// NEW MODEL: Attunements contribute base conversion rates for their primary mana type.
// Levels provide a multiplicative bonus (+50% per level) to conversions involving
// their primary element (as destination or component).
import type { AttunementDef, AttunementSlot } from '../types';
@@ -18,69 +22,62 @@ export const ATTUNEMENT_SLOT_NAMES: Record<AttunementSlot, string> = {
// All attunement definitions
export const ATTUNEMENTS_DEF: Record<string, AttunementDef> = {
// ─── Enchanter (Right Hand) ─────────────────────────────────────────────────
// Unlocks the enchanting system - applying magical effects to equipment
// Primary mana: Transference (used to move/apply enchantments)
enchanter: {
id: 'enchanter',
name: 'Enchanter',
desc: 'Channel transference mana through your right hand to apply magical enchantments to equipment. The art of enchanting allows you to imbue items with spell effects, stat bonuses, and special properties.',
slot: 'rightHand',
icon: '✨',
color: '#1ABC9C', // Teal (transference color)
color: '#1ABC9C',
primaryManaType: 'transference',
rawManaRegen: 0.5,
conversionRate: 0.2, // Converts 0.2 raw mana to transference per hour
unlocked: true, // Starting attunement
conversionRate: 0.2, // Base rate for transference conversion (per hour)
unlocked: true,
capabilities: ['enchanting'],
skillCategories: ['enchant', 'effectResearch'],
},
// ─── Invoker (Chest/Heart) ───────────────────────────────────────────────────
// Enables forming pacts with spire guardians
// No primary mana - instead gains mana types from each pact signed
invoker: {
id: 'invoker',
name: 'Invoker',
desc: 'Open your heart to the guardians of the spire. Form pacts with defeated guardians to gain their elemental affinity and access to their unique powers. Each pact grants access to a new mana type.',
slot: 'chest',
icon: '💜',
color: '#9B59B6', // Purple
primaryManaType: undefined, // Invoker has no primary - gains from pacts
color: '#9B59B6',
primaryManaType: undefined,
rawManaRegen: 0.3,
conversionRate: 0, // No automatic conversion - mana comes from pacts
unlocked: false, // Unlocked through gameplay
conversionRate: 0, // No automatic conversion mana comes from pacts
unlocked: false,
unlockCondition: 'Defeat your first guardian and choose the path of the Invoker',
capabilities: ['pacts', 'guardianPowers', 'elementalMastery'],
skillCategories: ['invocation', 'pact'],
},
// ─── Fabricator (Left Hand) ──────────────────────────────────────────────────
// Crafts earth golems and earthen gear
// Primary mana: Earth
// Later with fire mana -> metal mana, can craft metallic gear and golems
fabricator: {
id: 'fabricator',
name: 'Fabricator',
desc: 'Shape earth and metal through your left hand to craft golems and equipment. Start with earthen constructs, and unlock metalworking when you gain fire mana to create metal mana.',
slot: 'leftHand',
icon: '⚒️',
color: '#F4A261', // Earth color
color: '#F4A261',
primaryManaType: 'earth',
rawManaRegen: 0.4,
conversionRate: 0.25, // Converts 0.25 raw mana to earth per hour
unlocked: false, // Unlocked through gameplay
conversionRate: 0.25, // Base rate for earth conversion (per hour)
unlocked: false,
unlockCondition: 'Prove your worth as a crafter',
capabilities: ['golemCrafting', 'gearCrafting', 'earthShaping'],
skillCategories: ['fabrication', 'golemancy'],
},
};
// Helper function to get attunement by slot
// ─── Helpers ──────────────────────────────────────────────────────────────────
export function getAttunementBySlot(slot: AttunementSlot): AttunementDef | undefined {
return Object.values(ATTUNEMENTS_DEF).find(a => a.slot === slot);
}
// Helper function to get all unlocked attunements for a player
export function getUnlockedAttunements(attunements: Record<string, { active: boolean; level: number; experience: number }>): AttunementDef[] {
return Object.entries(attunements)
.filter(([, state]) => state.active)
@@ -88,71 +85,54 @@ export function getUnlockedAttunements(attunements: Record<string, { active: boo
.filter(Boolean);
}
// Helper function to calculate total raw mana regen from attunements (with level scaling)
/** Total raw mana regen from attunements (with level scaling) */
export function getTotalAttunementRegen(attunements: Record<string, { active: boolean; level: number; experience: number }>): number {
return Object.entries(attunements)
.filter(([, state]) => state.active)
.reduce((total, [id, state]) => {
const def = ATTUNEMENTS_DEF[id];
if (!def) return total;
// Exponential scaling: base * (1.5 ^ (level - 1))
const levelMult = Math.pow(1.5, (state.level || 1) - 1);
return total + def.rawManaRegen * levelMult;
}, 0);
}
// Helper function to calculate total conversion drain from all active attunements (per hour)
export function getTotalAttunementConversionDrain(attunements: Record<string, { active: boolean; level: number; experience: number }>): number {
return Object.entries(attunements)
.filter(([, state]) => state.active)
.reduce((total, [id, state]) => {
const def = ATTUNEMENTS_DEF[id];
if (!def || def.conversionRate <= 0) return total;
// Use the same level scaling as getAttunementConversionRate
const scaledRate = getAttunementConversionRate(id, state.level || 1);
return total + scaledRate;
}, 0);
}
// Get conversion rate with level scaling
/**
* Get the attunement base conversion rate for a specific attunement.
* This is the base rate contribution to the unified conversion system.
*/
export function getAttunementConversionRate(attunementId: string, level: number): number {
const def = ATTUNEMENTS_DEF[attunementId];
if (!def || def.conversionRate <= 0) return 0;
// Exponential scaling: base * (1.5 ^ (level - 1))
return def.conversionRate * Math.pow(1.5, (level || 1) - 1);
}
// XP required for attunement level
/**
* Get the attunement level multiplier for conversions.
* Each level adds +0.5 to the multiplier.
*/
export function getAttunementLevelMultiplier(level: number): number {
return 1 + (level || 1) * 0.5;
}
/** XP required for attunement level */
export function getAttunementXPForLevel(level: number): number {
// New scaling:
// Level 2: 1000 XP
// Level 3: 2500 XP
// Level 4: 5000 XP
// Level 5: 10000 XP
// etc. (each level requires 2x the previous, starting from 1000)
if (level <= 1) return 0;
if (level === 2) return 1000;
// For level 3+: 1000 * 2.5^(level-2), but rounded nicely
return Math.floor(1000 * Math.pow(2, level - 2) * (level >= 3 ? 1.25 : 1));
}
// Calculate XP gained from enchanting based on capacity used
export function calculateEnchantingXP(capacityUsed: number): number {
// 1 XP per 10 capacity used, floored, minimum 1
return Math.max(1, Math.floor(capacityUsed / 10));
}
// Max attunement level
export const MAX_ATTUNEMENT_LEVEL = 10;
// Helper function to get mana types from active attunements and pacts
export function getAttunementManaTypes(
attunements: Record<string, { active: boolean; level: number; experience: number }>,
signedPacts: number[]
): string[] {
const manaTypes: string[] = [];
// Add primary mana types from active attunements
Object.entries(attunements)
.filter(([, state]) => state.active)
.forEach(([id]) => {
@@ -161,30 +141,19 @@ export function getAttunementManaTypes(
manaTypes.push(def.primaryManaType);
}
});
// Invoker gains mana types from signed pacts
if (attunements.invoker?.active && signedPacts.length > 0) {
// Import GUARDIANS would be circular, so this is handled in the store
// For now, just mark that invoker provides pact-based mana
manaTypes.push('pactElements');
}
return [...new Set(manaTypes)]; // Remove duplicates
return [...new Set(manaTypes)];
}
// Get skill categories available to player based on active attunements
export function getAvailableSkillCategories(
attunements: Record<string, { active: boolean; level: number; experience: number }>
): string[] {
const categories = new Set<string>();
// Always available categories
categories.add('mana');
categories.add('study');
categories.add('research');
// categories.add('ascension'); // removed: banned mechanic
// Add categories from active attunements
Object.entries(attunements)
.filter(([, state]) => state.active)
.forEach(([id]) => {
@@ -193,6 +162,5 @@ export function getAvailableSkillCategories(
def.skillCategories.forEach(cat => categories.add(cat));
}
});
return Array.from(categories);
}
+118
View File
@@ -0,0 +1,118 @@
// ─── Conversion Cost Ratios ───────────────────────────────────────────────────
// All conversions produce 1 unit of destination mana.
// Costs are deducted from regen (not from the mana pool).
//
// For a destination element at distance d:
// rawCost = 10^(d+1)
// componentCost = 10 * (d+1) per component
import type { ElementRecipe } from '../types';
export interface ConversionCost {
/** Destination element ID */
element: string;
/** Distance from raw mana */
distance: number;
/** Raw mana cost per 1 unit of destination */
rawCost: number;
/** Component costs: element ID → amount per 1 unit of destination */
componentCosts: Record<string, number>;
}
function computeRawCost(distance: number): number {
return Math.pow(10, distance + 1);
}
function computeComponentCost(distance: number): number {
return 10 * (distance + 1);
}
/** Build a ConversionCost for a base element (distance 1, no components) */
function baseElementCost(element: string): ConversionCost {
return {
element,
distance: 1,
rawCost: computeRawCost(1), // 100
componentCosts: {},
};
}
/** Build a ConversionCost for a composite element (distance 2) */
function compositeElementCost(element: string, components: string[]): ConversionCost {
return {
element,
distance: 2,
rawCost: computeRawCost(2), // 1,000
componentCosts: Object.fromEntries(
components.map(c => [c, computeComponentCost(2)]), // 30 each
),
};
}
/** Build a ConversionCost for an exotic element (distance 3) */
function exoticElementCost(element: string, components: string[]): ConversionCost {
return {
element,
distance: 3,
rawCost: computeRawCost(3), // 10,000
componentCosts: Object.fromEntries(
components.map(c => [c, computeComponentCost(3)]), // 40 each
),
};
}
/** Build a ConversionCost for time (distance 4) */
function timeElementCost(element: string, components: string[]): ConversionCost {
return {
element,
distance: 4,
rawCost: computeRawCost(4), // 100,000
componentCosts: Object.fromEntries(
components.map(c => [c, computeComponentCost(4)]), // 50 each
),
};
}
// ─── Full Cost Table ──────────────────────────────────────────────────────────
export const CONVERSION_COSTS: Record<string, ConversionCost> = {
// Base (distance 1)
fire: baseElementCost('fire'),
water: baseElementCost('water'),
air: baseElementCost('air'),
earth: baseElementCost('earth'),
light: baseElementCost('light'),
dark: baseElementCost('dark'),
death: baseElementCost('death'),
// Utility (distance 1)
transference: baseElementCost('transference'),
// Composite (distance 2)
metal: compositeElementCost('metal', ['fire', 'earth']),
sand: compositeElementCost('sand', ['earth', 'water']),
lightning: compositeElementCost('lightning', ['fire', 'air']),
frost: compositeElementCost('frost', ['air', 'water']),
blackflame: compositeElementCost('blackflame', ['dark', 'fire']),
radiantflames: compositeElementCost('radiantflames', ['light', 'fire']),
miasma: compositeElementCost('miasma', ['air', 'death']),
shadowglass: compositeElementCost('shadowglass', ['earth', 'dark']),
// Exotic (distance 3)
crystal: exoticElementCost('crystal', ['sand', 'light']),
stellar: exoticElementCost('stellar', ['plasma', 'light']),
void: exoticElementCost('void', ['dark', 'death']),
soul: exoticElementCost('soul', ['light', 'dark', 'transference']),
plasma: exoticElementCost('plasma', ['lightning', 'fire', 'transference']),
// Time (distance 4)
time: timeElementCost('time', ['soul', 'sand', 'transference']),
};
/** Get the conversion cost for an element. Returns null if not found. */
export function getConversionCost(element: string): ConversionCost | null {
return CONVERSION_COSTS[element] ?? null;
}
/** Get all source types (raw + components) for a conversion */
export function getConversionSources(element: string): string[] {
const cost = CONVERSION_COSTS[element];
if (!cost) return [];
return ['raw', ...Object.keys(cost.componentCosts)];
}
@@ -1,6 +1,10 @@
// ─── Elemental Conversion Disciplines (Composite + Exotic) ──────────────────────
// Conversion disciplines for composite and exotic mana types.
// All are BASE attunement so they are available to every role once the element is unlocked.
//
// NEW MODEL: Disciplines contribute to conversion_{element} stat bonus.
// The unified conversion-rates.ts calculator handles rate computation.
// No direct mana drain — costs are deducted from regen.
import { DisciplinesAttunementType } from '../../types/disciplines';
import type { DisciplineDefinition } from '../../types/disciplines';
@@ -16,19 +20,11 @@ interface AdvancedConversionConfig {
scalingFactor: number;
drainBase: number;
sourceManaTypes: DisciplineDefinition['manaType'][];
customOnceDescription?: string;
customOnceAmount?: number;
customInfiniteDescription?: string;
customInfiniteAmount?: number;
infiniteThreshold?: number;
}
function createAdvancedConversionDiscipline(cfg: AdvancedConversionConfig): DisciplineDefinition {
const nameLower = cfg.name.toLowerCase();
const onceDesc = cfg.customOnceDescription ?? `+${cfg.conversionRate} ${cfg.name} Conversion/sec`;
const onceAmt = cfg.customOnceAmount ?? cfg.conversionRate;
const infDesc = cfg.customInfiniteDescription ?? `Every 100 XP: +${cfg.conversionRate * 0.5} ${cfg.name} Conversion/sec`;
const infAmt = cfg.customInfiniteAmount ?? cfg.conversionRate * 0.5;
const infThreshold = cfg.infiniteThreshold ?? 400;
return {
@@ -41,7 +37,7 @@ function createAdvancedConversionDiscipline(cfg: AdvancedConversionConfig): Disc
statBonus: {
stat: `conversion_${cfg.manaType}` as DisciplineDefinition['statBonus']['stat'],
baseValue: cfg.conversionRate,
label: `${cfg.name} Conversion/sec`,
label: `${cfg.name} Conversion/hr`,
},
difficultyFactor: cfg.difficultyFactor,
scalingFactor: cfg.scalingFactor,
@@ -55,23 +51,23 @@ function createAdvancedConversionDiscipline(cfg: AdvancedConversionConfig): Disc
type: 'once',
threshold: 150,
value: 0,
description: onceDesc,
bonus: { stat: `conversion_${cfg.manaType}`, amount: onceAmt },
description: `+${cfg.conversionRate} ${cfg.name} Conversion/hr`,
bonus: { stat: `conversion_${cfg.manaType}`, amount: cfg.conversionRate },
},
{
id: `${cfg.id}-inf`,
type: 'infinite',
threshold: infThreshold,
value: 100,
description: infDesc,
bonus: { stat: `conversion_${cfg.manaType}`, amount: infAmt },
description: `Every 100 XP: +${cfg.conversionRate * 0.5} ${cfg.name} Conversion/hr`,
bonus: { stat: `conversion_${cfg.manaType}`, amount: cfg.conversionRate * 0.5 },
},
],
};
}
export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
// ── Composite Elements ─────────────────────────────────────────────────────
// ── Composite Elements (distance 2, rate 0.35/hr) ────────────────────────
createAdvancedConversionDiscipline({
id: 'regen-metal',
name: 'Metal',
@@ -120,6 +116,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
drainBase: 2,
sourceManaTypes: ['raw', 'air', 'water'],
}),
// ── Composite Elements (distance 2, rate 0.30/hr) ────────────────────────
createAdvancedConversionDiscipline({
id: 'regen-blackflame',
name: 'BlackFlame',
@@ -169,7 +166,7 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
sourceManaTypes: ['raw', 'earth', 'dark'],
}),
// ── Exotic Elements ────────────────────────────────────────────────────────
// ── Exotic Elements (distance 3, rate 0.25/hr) ──────────────────────────
createAdvancedConversionDiscipline({
id: 'regen-crystal',
name: 'Crystal',
@@ -183,19 +180,6 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
sourceManaTypes: ['raw', 'sand', 'light'],
infiniteThreshold: 500,
}),
createAdvancedConversionDiscipline({
id: 'regen-stellar',
name: 'Stellar',
manaType: 'stellar',
cost: 20,
description: 'Convert raw mana + plasma mana + light mana into stellar mana over time.',
conversionRate: 0.2,
difficultyFactor: 230,
scalingFactor: 115,
drainBase: 3,
sourceManaTypes: ['raw', 'plasma', 'light'],
infiniteThreshold: 500,
}),
createAdvancedConversionDiscipline({
id: 'regen-void',
name: 'Void',
@@ -209,6 +193,21 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
sourceManaTypes: ['raw', 'dark', 'death'],
infiniteThreshold: 500,
}),
// ── Exotic Elements (distance 3, rate 0.20/hr) ──────────────────────────
createAdvancedConversionDiscipline({
id: 'regen-stellar',
name: 'Stellar',
manaType: 'stellar',
cost: 20,
description: 'Convert raw mana + plasma mana + light mana into stellar mana over time.',
conversionRate: 0.2,
difficultyFactor: 230,
scalingFactor: 115,
drainBase: 3,
sourceManaTypes: ['raw', 'plasma', 'light'],
infiniteThreshold: 500,
}),
createAdvancedConversionDiscipline({
id: 'regen-soul',
name: 'Soul',
@@ -235,6 +234,8 @@ export const elementalRegenAdvancedDisciplines: DisciplineDefinition[] = [
sourceManaTypes: ['raw', 'lightning', 'fire', 'transference'],
infiniteThreshold: 550,
}),
// ── Time (distance 4, rate 0.15/hr) ─────────────────────────────────────
createAdvancedConversionDiscipline({
id: 'regen-time',
name: 'Time',
@@ -1,33 +1,31 @@
// ─── Elemental Conversion Disciplines (Base + Utility) ─────────────────────────
// One discipline per mana type that converts raw mana into that element.
// All are BASE attunement so they are available to every role once the element is unlocked.
//
// NEW MODEL: Disciplines contribute to conversion_{element} stat bonus.
// The unified conversion-rates.ts calculator handles rate computation.
// No direct mana drain — costs are deducted from regen.
import { DisciplinesAttunementType } from '../../types/disciplines';
import type { DisciplineDefinition } from '../../types/disciplines';
const BASE_CONVERSION = 0.5;
const BASE_DRAIN = 1.5;
const BASE_DIFF = 120;
const BASE_SCALE = 60;
interface BaseConversionConfig {
id: string;
name: string;
manaType: string;
cost: number;
/** Base conversion rate (per hour) before XP scaling */
conversionRate?: number;
difficultyFactor?: number;
scalingFactor?: number;
drainBase?: number;
sourceManaTypes?: DisciplineDefinition['manaType'][];
}
function createManaConversionDiscipline(cfg: BaseConversionConfig): DisciplineDefinition {
const rate = cfg.conversionRate ?? BASE_CONVERSION;
const diff = cfg.difficultyFactor ?? BASE_DIFF;
const scale = cfg.scalingFactor ?? BASE_SCALE;
const drain = cfg.drainBase ?? BASE_DRAIN;
const sources = cfg.sourceManaTypes ?? ['raw' as DisciplineDefinition['manaType']];
const rate = cfg.conversionRate ?? 0.5;
const diff = cfg.difficultyFactor ?? 120;
const scale = cfg.scalingFactor ?? 60;
const drain = cfg.drainBase ?? 1.5;
const nameLower = cfg.name.toLowerCase();
return {
@@ -40,13 +38,13 @@ function createManaConversionDiscipline(cfg: BaseConversionConfig): DisciplineDe
statBonus: {
stat: `conversion_${cfg.manaType}` as DisciplineDefinition['statBonus']['stat'],
baseValue: rate,
label: `${cfg.name} Conversion/sec`,
label: `${cfg.name} Conversion/hr`,
},
difficultyFactor: diff,
scalingFactor: scale,
drainBase: drain,
conversionRate: rate,
sourceManaTypes: sources,
sourceManaTypes: ['raw' as DisciplineDefinition['manaType']],
requires: [cfg.manaType],
perks: [
{
@@ -54,7 +52,7 @@ function createManaConversionDiscipline(cfg: BaseConversionConfig): DisciplineDe
type: 'once',
threshold: 100,
value: 0,
description: `+${rate} ${cfg.name} Conversion/sec`,
description: `+${rate} ${cfg.name} Conversion/hr`,
bonus: { stat: `conversion_${cfg.manaType}`, amount: rate },
},
{
@@ -62,24 +60,24 @@ function createManaConversionDiscipline(cfg: BaseConversionConfig): DisciplineDe
type: 'infinite',
threshold: 300,
value: 100,
description: `Every 100 XP: +0.25 ${cfg.name} Conversion/sec`,
bonus: { stat: `conversion_${cfg.manaType}`, amount: 0.25 },
description: `Every 100 XP: +${rate * 0.5} ${cfg.name} Conversion/hr`,
bonus: { stat: `conversion_${cfg.manaType}`, amount: rate * 0.5 },
},
],
};
}
export const elementalRegenDisciplines: DisciplineDefinition[] = [
// ── Base Elements ──────────────────────────────────────────────────────────
createManaConversionDiscipline({ id: 'regen-fire', name: 'Fire', manaType: 'fire', cost: 8 }),
createManaConversionDiscipline({ id: 'regen-water', name: 'Water', manaType: 'water', cost: 8 }),
createManaConversionDiscipline({ id: 'regen-air', name: 'Air', manaType: 'air', cost: 8 }),
createManaConversionDiscipline({ id: 'regen-earth', name: 'Earth', manaType: 'earth', cost: 8 }),
createManaConversionDiscipline({ id: 'regen-light', name: 'Light', manaType: 'light', cost: 8 }),
createManaConversionDiscipline({ id: 'regen-dark', name: 'Dark', manaType: 'dark', cost: 8 }),
createManaConversionDiscipline({ id: 'regen-death', name: 'Death', manaType: 'death', cost: 8 }),
// ── Base Elements (distance 1, rate 0.5/hr) ──────────────────────────────
createManaConversionDiscipline({ id: 'regen-fire', name: 'Fire', manaType: 'fire', cost: 8 }),
createManaConversionDiscipline({ id: 'regen-water', name: 'Water', manaType: 'water', cost: 8 }),
createManaConversionDiscipline({ id: 'regen-air', name: 'Air', manaType: 'air', cost: 8 }),
createManaConversionDiscipline({ id: 'regen-earth', name: 'Earth', manaType: 'earth', cost: 8 }),
createManaConversionDiscipline({ id: 'regen-light', name: 'Light', manaType: 'light', cost: 8 }),
createManaConversionDiscipline({ id: 'regen-dark', name: 'Dark', manaType: 'dark', cost: 8 }),
createManaConversionDiscipline({ id: 'regen-death', name: 'Death', manaType: 'death', cost: 8 }),
// ── Utility Element ────────────────────────────────────────────────────────
// ── Utility Element (distance 1, rate 0.4/hr) ────────────────────────────
createManaConversionDiscipline({
id: 'regen-transference',
name: 'Transference',
@@ -89,6 +87,5 @@ export const elementalRegenDisciplines: DisciplineDefinition[] = [
difficultyFactor: 100,
scalingFactor: 50,
drainBase: 1,
sourceManaTypes: ['raw'],
}),
];