feat: split skills-v2-defs into category modules and fix export
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 35s

- Split 636-line skills-v2-defs.ts into 9 category files (all under 400 lines)
- Add skills-v2-registry.ts to build SKILLS_V2 flat record from modules
- Fix missing re-export of SKILLS_V2 from skills-v2.ts
- Fix clickMana clamping: remove Math.round to allow fractional values
- Fix golemDuration clamping: remove Math.round to allow fractional values
- Fix guardianConstructs effect: duration uses 'add' mode instead of 'multiply'
- All 70 existing tests pass
This commit is contained in:
2026-05-12 11:28:44 +02:00
parent 70ec32bd4e
commit b0eea7dadd
15 changed files with 1451 additions and 2 deletions
+269
View File
@@ -0,0 +1,269 @@
import type { SkillV2Def, SkillEffect, StatKey, ComputedStats } from './skills-v2-types';
import { SKILLS_V2 } from './skills-v2-defs';
// Re-export individual skills from category modules
export {
manaWell,
manaFlow,
manaOverflow,
manaTap,
manaSurge,
manaSpring,
quickLearner,
focusedMind,
knowledgeRetention,
meditation,
deepTrance,
voidMeditation,
} from './skills-core';
export {
enchanting,
efficientEnchant,
enchantSpeed,
essenceRefining,
disenchanting,
} from './skills-enchant';
export {
arcaneFury,
combatTraining,
precision,
elementalMastery,
attackSpeed,
armorPiercing,
spellDamage,
} from './skills-combat';
export {
golemMastery,
golemEfficiency,
golemLongevity,
} from './skills-golemancy';
export {
invocation,
pactMastery,
guardianLore,
} from './skills-invocation';
export {
effCrafting,
fieldRepair,
} from './skills-crafting';
export {
fireManaCap,
waterManaCap,
airManaCap,
earthManaCap,
lightManaCap,
darkManaCap,
deathManaCap,
metalManaCap,
sandManaCap,
lightningManaCap,
transferenceManaCap,
} from './skills-element-caps';
export {
pactWeaving,
guardianConstructs,
enchantedGolemancy,
} from './skills-hybrid';
export {
researchManaSpells,
researchFireSpells,
researchWaterSpells,
researchAirSpells,
researchEarthSpells,
researchLightSpells,
researchDarkSpells,
researchLifeDeathSpells,
researchAdvancedFire,
researchAdvancedWater,
researchAdvancedAir,
researchAdvancedEarth,
researchAdvancedLight,
researchAdvancedDark,
researchMasterFire,
researchMasterWater,
researchMasterEarth,
researchDamageEffects,
researchCombatEffects,
researchManaEffects,
researchAdvancedManaEffects,
researchUtilityEffects,
researchSpecialEffects,
researchOverpower,
researchMetalSpells,
researchSandSpells,
researchLightningSpells,
researchAdvancedMetal,
researchAdvancedSand,
researchAdvancedLightning,
researchMasterMetal,
researchMasterSand,
researchMasterLightning,
researchTransferenceSpells,
researchAdvancedTransference,
researchMasterTransference,
researchAdvancedFireCap,
researchAdvancedWaterCap,
researchAdvancedAirCap,
researchAdvancedEarthCap,
researchAdvancedLightCap,
researchAdvancedDarkCap,
researchAdvancedDeathCap,
researchMasterFireCap,
researchMasterWaterCap,
researchMasterAirCap,
researchMasterEarthCap,
researchMasterLightCap,
researchMasterDarkCap,
researchMasterDeathCap,
researchMetalCapacity,
researchAdvancedMetalCap,
researchSandCapacity,
researchAdvancedSandCap,
researchLightningCapacity,
researchAdvancedLightningCap,
researchCrystalCapacity,
researchAdvancedCrystalCap,
researchStellarCapacity,
researchAdvancedStellarCap,
researchVoidCapacity,
researchAdvancedVoidCap,
} from './skills-research';
export { SKILLS_V2 };
// Default Base Stats
export const BASE_STATS: ComputedStats = {
maxMana: 100,
manaRegen: 2,
clickMana: 1,
elementCap: 10,
studySpeed: 1,
studyCostMult: 1,
meditationEfficiency: 1,
enchantCapacity: 100,
enchantSpeed: 1,
enchantPower: 1,
disenchantRecovery: 1,
baseDamage: 5,
damageMultiplier: 1,
attackSpeed: 1,
critChance: 0,
critMultiplier: 1.5,
armorPierce: 0,
insightGain: 1,
golemDamage: 1,
golemDuration: 1,
pactMultiplier: 1,
conversionRate: 1,
spellDamage: 1,
guardianDamage: 1,
craftSpeed: 1,
repairSpeed: 1,
fireCap: 0, waterCap: 0, airCap: 0, earthCap: 0,
lightCap: 0, darkCap: 0, deathCap: 0,
metalCap: 0, sandCap: 0, lightningCap: 0,
transferenceCap: 0, crystalCap: 0, stellarCap: 0, voidCap: 0,
elementalDamage: 1,
};
const ELEMENT_CAP_STATS: Record<string, keyof ComputedStats> = {
fire: 'fireCap', water: 'waterCap', air: 'airCap', earth: 'earthCap',
light: 'lightCap', dark: 'darkCap', death: 'deathCap', metal: 'metalCap',
sand: 'sandCap', lightning: 'lightningCap', transference: 'transferenceCap',
crystal: 'crystalCap', stellar: 'stellarCap', void: 'voidCap',
};
/**
* Compute all game stats from skill levels.
*/
export function computeStats(
skills: Record<string, number>,
prestigeUpgrades: Record<string, number> = {}
): ComputedStats {
const stats: ComputedStats = { ...BASE_STATS };
for (const [skillId, level] of Object.entries(skills)) {
if (level <= 0) continue;
const def = SKILLS_V2[skillId];
if (!def) continue;
for (const effect of def.effects) {
const key = effect.stat as keyof ComputedStats;
const currentVal = stats[key] as number;
if (effect.mode === 'add') {
(stats[key] as number) = currentVal + effect.valuePerLevel * level;
} else {
const perLevelMultiplier = 1 + effect.valuePerLevel;
let result = currentVal;
for (let i = 0; i < level; i++) result *= perLevelMultiplier;
(stats[key] as number) = result;
}
}
}
if (prestigeUpgrades.manaWell) stats.maxMana += prestigeUpgrades.manaWell * 500;
if (prestigeUpgrades.manaFlow) stats.manaRegen += prestigeUpgrades.manaFlow * 0.5;
if (prestigeUpgrades.elementalAttune) stats.elementCap += prestigeUpgrades.elementalAttune * 25;
if (prestigeUpgrades.pactBinding) stats.pactMultiplier += prestigeUpgrades.pactBinding * 0.1;
if (prestigeUpgrades.insightAmp) stats.insightGain *= 1 + prestigeUpgrades.insightAmp * 0.25;
let elementCapFromSkills = 0;
for (const [, statKey] of Object.entries(ELEMENT_CAP_STATS)) {
const val = stats[statKey] as number;
if (val > 0) elementCapFromSkills += val;
}
if (elementCapFromSkills > 0) stats.elementCap += elementCapFromSkills;
stats.maxMana = Math.max(1, Math.round(stats.maxMana));
stats.manaRegen = Math.round(stats.manaRegen * 100) / 100;
stats.clickMana = Math.max(1, stats.clickMana);
stats.elementCap = Math.max(10, Math.round(stats.elementCap));
stats.baseDamage = Math.max(1, Math.round(stats.baseDamage));
stats.critChance = Math.min(1, Math.max(0, stats.critChance));
stats.armorPierce = Math.min(1, Math.max(0, stats.armorPierce));
stats.attackSpeed = Math.max(0.1, stats.attackSpeed);
stats.insightGain = Math.max(0, stats.insightGain);
stats.golemDamage = Math.max(0.1, stats.golemDamage);
stats.golemDuration = Math.max(1, stats.golemDuration);
stats.enchantCapacity = Math.max(10, Math.round(stats.enchantCapacity));
stats.conversionRate = Math.max(0, stats.conversionRate);
return stats;
}
/**
* Get the base key for a tiered skill (strips _tN suffix).
*/
export function getBaseSkillId(skillId: string): string {
const match = skillId.match(/^(.+?)_t(\d+)$/);
return match ? match[1] : skillId;
}
/**
* Check if a skill has prerequisites that are met.
*/
export function hasPrerequisites(
skills: Record<string, number>,
prerequisites?: Record<string, number>
): boolean {
if (!prerequisites) return true;
for (const [reqId, reqLevel] of Object.entries(prerequisites)) {
if ((skills[reqId] || 0) < reqLevel) return false;
}
return true;
}
/** Legacy compat wrapper */
export function computeStatsLegacy(state: {
skills: Record<string, number>;
prestigeUpgrades?: Record<string, number>;
}): ComputedStats {
return computeStats(state.skills, state.prestigeUpgrades || {});
}