From 14a4a09c678c50794af5732c0a8ef3931d6af595 Mon Sep 17 00:00:00 2001 From: angelTomo9 <144371630+angelTomo9@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:00:39 +0200 Subject: [PATCH 1/2] feat(enchanting): add arcane disenchanting and mystic essence extraction forge engine --- api/src/lib/enchantmentDisenchantForge.ts | 95 +++++++++++++++++++ .../tests/enchantmentDisenchantForge.test.ts | 48 ++++++++++ 2 files changed, 143 insertions(+) create mode 100644 api/src/lib/enchantmentDisenchantForge.ts create mode 100644 api/src/tests/enchantmentDisenchantForge.test.ts diff --git a/api/src/lib/enchantmentDisenchantForge.ts b/api/src/lib/enchantmentDisenchantForge.ts new file mode 100644 index 00000000..9ce9a989 --- /dev/null +++ b/api/src/lib/enchantmentDisenchantForge.ts @@ -0,0 +1,95 @@ +/** + * Arcane Disenchanting & Mystic Essence Extraction Forge Engine for OpenAO MMORPG. + * Simulates the breakdown of enchanted weapons and armor into crafting essences, + * scaling yields by item rarity tiers, enchanter skill proficiency, and critical extraction rolls. + */ + +export type ItemRarityTier = "COMMON" | "MAGIC" | "RARE" | "EPIC" | "LEGENDARY"; +export type EssenceType = "ARCANE_DUST" | "LESSER_MYSTIC_ESSENCE" | "GREATER_MYSTIC_ESSENCE" | "RADIANT_SHARD"; + +export interface DisenchantYield { + essence: EssenceType; + quantity: number; + isCriticalBonus?: boolean; +} + +export interface DisenchantableItem { + itemId: string; + itemTemplateId: string; + rarity: ItemRarityTier; + enchantmentPower: number; // e.g. +1 to +10 +} + +export interface DisenchantResult { + success: boolean; + yields: DisenchantYield[]; + totalDustEquivalent: number; + wasCriticalExtraction: boolean; + reason?: string; +} + +export const RARITY_BASE_YIELDS: Record = { + COMMON: { dust: 2, essenceQty: 0 }, + MAGIC: { dust: 6, essence: "LESSER_MYSTIC_ESSENCE", essenceQty: 1 }, + RARE: { dust: 15, essence: "GREATER_MYSTIC_ESSENCE", essenceQty: 1 }, + EPIC: { dust: 35, essence: "GREATER_MYSTIC_ESSENCE", essenceQty: 3 }, + LEGENDARY: { dust: 80, essence: "RADIANT_SHARD", essenceQty: 2 }, +}; + +export class EnchantmentDisenchantForgeEngine { + /** + * Disenchants an item into arcane components with skill scaling and critical bonus rolls. + */ + public static disenchantItem( + item: DisenchantableItem, + playerEnchantingSkill: number, + rng: () => number = Math.random + ): DisenchantResult { + const base = RARITY_BASE_YIELDS[item.rarity]; + if (!base) { + return { + success: false, + yields: [], + totalDustEquivalent: 0, + wasCriticalExtraction: false, + reason: "Invalid item rarity.", + }; + } + + const skill = Math.min(100, Math.max(1, playerEnchantingSkill)); + + // Skill scaling factor: 1.0 at skill 1 up to 1.50 at skill 100 + const skillFactor = 1.0 + (skill / 100) * 0.50; + + // Critical Extraction Chance: 5% base + 1% per 10 skill levels (max 15%) + const criticalChance = 0.05 + skill / 1000; + const wasCriticalExtraction = rng() < criticalChance; + + const critMultiplier = wasCriticalExtraction ? 2 : 1; + const dustQty = Math.floor(base.dust * skillFactor * critMultiplier); + + const yields: DisenchantYield[] = [ + { + essence: "ARCANE_DUST", + quantity: dustQty, + isCriticalBonus: wasCriticalExtraction, + }, + ]; + + if (base.essence && base.essenceQty > 0) { + const essenceQty = Math.max(1, Math.floor(base.essenceQty * (wasCriticalExtraction ? 2 : 1))); + yields.push({ + essence: base.essence, + quantity: essenceQty, + isCriticalBonus: wasCriticalExtraction, + }); + } + + return { + success: true, + yields, + totalDustEquivalent: dustQty, + wasCriticalExtraction, + }; + } +} \ No newline at end of file diff --git a/api/src/tests/enchantmentDisenchantForge.test.ts b/api/src/tests/enchantmentDisenchantForge.test.ts new file mode 100644 index 00000000..4e649787 --- /dev/null +++ b/api/src/tests/enchantmentDisenchantForge.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from "vitest"; +import { + EnchantmentDisenchantForgeEngine, + DisenchantableItem, +} from "../lib/enchantmentDisenchantForge.js"; + +describe("EnchantmentDisenchantForgeEngine Essence Extraction & Skill Scaling", () => { + it("disenchants RARE item with high skill efficiency", () => { + const rareSword: DisenchantableItem = { + itemId: "item_sword_99", + itemTemplateId: "glowing_broadsword", + rarity: "RARE", + enchantmentPower: 3, + }; + + // Skill 100 -> skill factor 1.50 -> 15 base * 1.50 = 22 Arcane Dust + const result = EnchantmentDisenchantForgeEngine.disenchantItem(rareSword, 100, () => 0.99); // No crit + expect(result.success).toBe(true); + expect(result.wasCriticalExtraction).toBe(false); + + const dust = result.yields.find((y) => y.essence === "ARCANE_DUST"); + expect(dust?.quantity).toBe(22); + + const essence = result.yields.find((y) => y.essence === "GREATER_MYSTIC_ESSENCE"); + expect(essence?.quantity).toBe(1); + }); + + it("doubles dust and essence on critical extraction roll", () => { + const epicStaff: DisenchantableItem = { + itemId: "item_staff_01", + itemTemplateId: "archmage_staff", + rarity: "EPIC", + enchantmentPower: 5, + }; + + // Base 35 dust, 3 greater essences -> Crit roll (rng = 0.01) + const result = EnchantmentDisenchantForgeEngine.disenchantItem(epicStaff, 50, () => 0.01); + expect(result.success).toBe(true); + expect(result.wasCriticalExtraction).toBe(true); + + const dust = result.yields.find((y) => y.essence === "ARCANE_DUST"); + expect(dust?.isCriticalBonus).toBe(true); + expect(dust?.quantity).toBeGreaterThan(70); + + const essence = result.yields.find((y) => y.essence === "GREATER_MYSTIC_ESSENCE"); + expect(essence?.quantity).toBe(6); // 3 * 2 + }); +}); \ No newline at end of file From d7c70c075689ffc833cb2a0c7421cf3339f79fa7 Mon Sep 17 00:00:00 2001 From: angelTomo9 <144371630+angelTomo9@users.noreply.github.com> Date: Thu, 27 Aug 2026 21:00:43 +0200 Subject: [PATCH 2/2] fix(enchanting): factor enchantmentPower into yields, compute totalDustEquivalent, and guard against NaN skill --- api/src/lib/enchantmentDisenchantForge.ts | 32 ++++++++++++--- .../tests/enchantmentDisenchantForge.test.ts | 41 ++++++++----------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/api/src/lib/enchantmentDisenchantForge.ts b/api/src/lib/enchantmentDisenchantForge.ts index 9ce9a989..205e0a99 100644 --- a/api/src/lib/enchantmentDisenchantForge.ts +++ b/api/src/lib/enchantmentDisenchantForge.ts @@ -1,7 +1,8 @@ /** * Arcane Disenchanting & Mystic Essence Extraction Forge Engine for OpenAO MMORPG. * Simulates the breakdown of enchanted weapons and armor into crafting essences, - * scaling yields by item rarity tiers, enchanter skill proficiency, and critical extraction rolls. + * scaling yields by item rarity tiers, item enchantment power (+1..+10), enchanter skill proficiency, + * and critical extraction rolls. */ export type ItemRarityTier = "COMMON" | "MAGIC" | "RARE" | "EPIC" | "LEGENDARY"; @@ -17,17 +18,25 @@ export interface DisenchantableItem { itemId: string; itemTemplateId: string; rarity: ItemRarityTier; - enchantmentPower: number; // e.g. +1 to +10 + enchantmentPower?: number; // e.g. +1 to +10 } export interface DisenchantResult { success: boolean; yields: DisenchantYield[]; + totalArcaneDust: number; totalDustEquivalent: number; wasCriticalExtraction: boolean; reason?: string; } +export const ESSENCE_DUST_VALUES: Record = { + ARCANE_DUST: 1, + LESSER_MYSTIC_ESSENCE: 5, + GREATER_MYSTIC_ESSENCE: 15, + RADIANT_SHARD: 40, +}; + export const RARITY_BASE_YIELDS: Record = { COMMON: { dust: 2, essenceQty: 0 }, MAGIC: { dust: 6, essence: "LESSER_MYSTIC_ESSENCE", essenceQty: 1 }, @@ -38,7 +47,7 @@ export const RARITY_BASE_YIELDS: Record 0) { const essenceQty = Math.max(1, Math.floor(base.essenceQty * (wasCriticalExtraction ? 2 : 1))); yields.push({ @@ -83,12 +101,14 @@ export class EnchantmentDisenchantForgeEngine { quantity: essenceQty, isCriticalBonus: wasCriticalExtraction, }); + totalEquivalent += essenceQty * ESSENCE_DUST_VALUES[base.essence]; } return { success: true, yields, - totalDustEquivalent: dustQty, + totalArcaneDust: dustQty, + totalDustEquivalent: totalEquivalent, wasCriticalExtraction, }; } diff --git a/api/src/tests/enchantmentDisenchantForge.test.ts b/api/src/tests/enchantmentDisenchantForge.test.ts index 4e649787..5175f16b 100644 --- a/api/src/tests/enchantmentDisenchantForge.test.ts +++ b/api/src/tests/enchantmentDisenchantForge.test.ts @@ -4,45 +4,36 @@ import { DisenchantableItem, } from "../lib/enchantmentDisenchantForge.js"; -describe("EnchantmentDisenchantForgeEngine Essence Extraction & Skill Scaling", () => { - it("disenchants RARE item with high skill efficiency", () => { +describe("EnchantmentDisenchantForgeEngine Power, Skill Scaling & Total Valuation", () => { + it("disenchants RARE item with enchantment power scaling and calculates total dust equivalent", () => { const rareSword: DisenchantableItem = { itemId: "item_sword_99", itemTemplateId: "glowing_broadsword", rarity: "RARE", - enchantmentPower: 3, + enchantmentPower: 3, // +30% power }; - // Skill 100 -> skill factor 1.50 -> 15 base * 1.50 = 22 Arcane Dust - const result = EnchantmentDisenchantForgeEngine.disenchantItem(rareSword, 100, () => 0.99); // No crit + // Skill 100 -> skill factor 1.50, power 1.30 -> 15 * 1.30 * 1.50 = 29 Arcane Dust + 1 Greater Essence (15 dust eq) = 44 total + const result = EnchantmentDisenchantForgeEngine.disenchantItem(rareSword, 100, () => 0.99); expect(result.success).toBe(true); expect(result.wasCriticalExtraction).toBe(false); const dust = result.yields.find((y) => y.essence === "ARCANE_DUST"); - expect(dust?.quantity).toBe(22); - - const essence = result.yields.find((y) => y.essence === "GREATER_MYSTIC_ESSENCE"); - expect(essence?.quantity).toBe(1); + expect(dust?.quantity).toBe(29); + expect(result.totalArcaneDust).toBe(29); + expect(result.totalDustEquivalent).toBe(44); // 29 + 15 }); - it("doubles dust and essence on critical extraction roll", () => { - const epicStaff: DisenchantableItem = { - itemId: "item_staff_01", - itemTemplateId: "archmage_staff", - rarity: "EPIC", - enchantmentPower: 5, + it("guards safely against NaN player skill", () => { + const item: DisenchantableItem = { + itemId: "item_1", + itemTemplateId: "iron_dagger", + rarity: "COMMON", }; - // Base 35 dust, 3 greater essences -> Crit roll (rng = 0.01) - const result = EnchantmentDisenchantForgeEngine.disenchantItem(epicStaff, 50, () => 0.01); + const result = EnchantmentDisenchantForgeEngine.disenchantItem(item, NaN as any); expect(result.success).toBe(true); - expect(result.wasCriticalExtraction).toBe(true); - - const dust = result.yields.find((y) => y.essence === "ARCANE_DUST"); - expect(dust?.isCriticalBonus).toBe(true); - expect(dust?.quantity).toBeGreaterThan(70); - - const essence = result.yields.find((y) => y.essence === "GREATER_MYSTIC_ESSENCE"); - expect(essence?.quantity).toBe(6); // 3 * 2 + expect(Number.isFinite(result.totalArcaneDust)).toBe(true); + expect(result.totalArcaneDust).toBe(2); }); }); \ No newline at end of file