feat: overhaul mana conversion system to unified regen-deduction model
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m2s
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
}),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user