From 688d7b23d651bc8a8a5abdcc8433fcc652ea2eb1 Mon Sep 17 00:00:00 2001 From: angelTomo9 <144371630+angelTomo9@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:01:37 +0200 Subject: [PATCH] feat(leathercraft): ancient runic leather horse fender bench engine --- .../ancientRunicLeatherHorseFenderBench.ts | 261 ++++++++++++++++++ ...ncientRunicLeatherHorseFenderBench.test.ts | 154 +++++++++++ 2 files changed, 415 insertions(+) create mode 100644 api/src/lib/ancientRunicLeatherHorseFenderBench.ts create mode 100644 api/src/tests/ancientRunicLeatherHorseFenderBench.test.ts diff --git a/api/src/lib/ancientRunicLeatherHorseFenderBench.ts b/api/src/lib/ancientRunicLeatherHorseFenderBench.ts new file mode 100644 index 00000000..3c78ebc9 --- /dev/null +++ b/api/src/lib/ancientRunicLeatherHorseFenderBench.ts @@ -0,0 +1,261 @@ +import crypto from "node:crypto"; + +/** + * Ancient Runic Leather Horse Stirrup Fender Bench, Mithril Slider Rig & Celestial Valkyrie Fender Sanctum Engine for OpenAO MMORPG. + * Simulates stirrup fender stitching benches and leg shielding tension rigs (Elder Fender Bench, Runic Oak Fender Rig, Celestial Void Valkyrie Fender Sanctum), + * raw tanned buffalo fender panels and tempered mithril fender slider sets (Tanned Buffalo Fender Panel, Tempered Mithril Fender Slider Set, Celestial Void Astral Fender Pelt), + * novice reinforced leg fenders and sovereign aerial fender recipes (Novice Reinforced Leg Fender, Warmaster Mithril Slider Fender, Celestial Void Valkyrie Sovereign Fender), + * independent steed leg-friction protection ratings and stirrup sway mitigation ratings (scaled across catalog baselines ~16% to 100%), calibrated clamped leg friction protection bonus and stirrup sway mitigation scaling, + * upfront leather material deduction on all craft attempts, consistent remainingProvidedLeathers return shapes across all paths, immutable bench cloning for safe rollbacks on both craft and maintain operations, cached static catalog maxima, crypto-secure default gameplay rolls strictly in [0, 1), authoritative catalog power ratio without dead instance fields, and horse stirrup fender bench maintenance. + */ + +export type FenderBenchType = "ELDER_FENDER_BENCH" | "RUNIC_OAK_FENDER_RIG" | "CELESTIAL_VOID_VALKYRIE_FENDER_SANCTUM"; +export type RawLeatherFenderType = "TANNED_BUFFALO_FENDER_PANEL" | "TEMPERED_MITHRIL_FENDER_SLIDER_SET" | "CELESTIAL_VOID_ASTRAL_FENDER_PELT"; +export type FenderRecipeType = "NOVICE_REINFORCED_LEG_FENDER" | "WARMASTER_MITHRIL_SLIDER_FENDER" | "CELESTIAL_VOID_VALKYRIE_SOVEREIGN_FENDER"; + +export interface FenderBenchData { + benchType: FenderBenchType; + maxDurability: number; + leathercraftPower: number; + baseSuccessRatePercent: number; // 0 to 100 + legShieldingBonusPercent: number; +} + +export interface FenderRecipeData { + recipeType: FenderRecipeType; + requiredLeatherType: RawLeatherFenderType; + requiredLeatherCount: number; + baseLegFrictionProtectionPercent: number; + baseStirrupSwayMitigationBonusPercent: number; +} + +export interface ActiveFenderBench { + benchId: string; + leatherworkerPlayerId: string; + benchType: FenderBenchType; + currentDurability: number; + maxDurability: number; + isFunctional: boolean; +} + +export interface CraftedHorseFender { + fenderId: string; + recipeType: FenderRecipeType; + finalLegFrictionProtectionPercent: number; + finalStirrupSwayMitigationBonusPercent: number; + legShieldingPercent: number; // Scaled rating (clamped 0 to 100%, with catalog bench baselines ~16% to 100%) + consumedLeatherCount: number; + consumedLeatherType: RawLeatherFenderType; + remainingProvidedLeathers: RawLeatherFenderType[]; + craftedEpochMs: number; +} + +export const FENDER_BENCH_CATALOG: Record = { + ELDER_FENDER_BENCH: { benchType: "ELDER_FENDER_BENCH", maxDurability: 95, leathercraftPower: 30, baseSuccessRatePercent: 87, legShieldingBonusPercent: 14 }, + RUNIC_OAK_FENDER_RIG: { benchType: "RUNIC_OAK_FENDER_RIG", maxDurability: 200, leathercraftPower: 72, baseSuccessRatePercent: 94, legShieldingBonusPercent: 24 }, + CELESTIAL_VOID_VALKYRIE_FENDER_SANCTUM: { benchType: "CELESTIAL_VOID_VALKYRIE_FENDER_SANCTUM", maxDurability: 350, leathercraftPower: 130, baseSuccessRatePercent: 99, legShieldingBonusPercent: 40 }, +}; + +export const FENDER_RECIPE_CATALOG: Record = { + NOVICE_REINFORCED_LEG_FENDER: { recipeType: "NOVICE_REINFORCED_LEG_FENDER", requiredLeatherType: "TANNED_BUFFALO_FENDER_PANEL", requiredLeatherCount: 2, baseLegFrictionProtectionPercent: 24, baseStirrupSwayMitigationBonusPercent: 14 }, + WARMASTER_MITHRIL_SLIDER_FENDER: { recipeType: "WARMASTER_MITHRIL_SLIDER_FENDER", requiredLeatherType: "TEMPERED_MITHRIL_FENDER_SLIDER_SET", requiredLeatherCount: 2, baseLegFrictionProtectionPercent: 50, baseStirrupSwayMitigationBonusPercent: 30 }, + CELESTIAL_VOID_VALKYRIE_SOVEREIGN_FENDER: { recipeType: "CELESTIAL_VOID_VALKYRIE_SOVEREIGN_FENDER", requiredLeatherType: "CELESTIAL_VOID_ASTRAL_FENDER_PELT", requiredLeatherCount: 2, baseLegFrictionProtectionPercent: 84, baseStirrupSwayMitigationBonusPercent: 64 }, +}; + +export class AncientRunicLeatherHorseFenderBenchEngine { + public static readonly DURABILITY_COST_PER_CRAFT = 10; + + /** + * Cached static catalog maxima to prevent runtime array reallocation. + */ + public static readonly CATALOG_MAXIMA = { + maxPower: Math.max(...Object.values(FENDER_BENCH_CATALOG).map(b => b.leathercraftPower), 1), + maxBonus: Math.max(...Object.values(FENDER_BENCH_CATALOG).map(b => b.legShieldingBonusPercent), 1), + }; + + /** + * Generates a crypto-secure UUID or 128-bit hex string using node:crypto. + */ + private static generateSecureId(): string { + if (typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + return crypto.randomBytes(16).toString("hex"); + } + + /** + * Generates a cryptographically secure random float strictly in [0, 1). + */ + public static generateSecureRoll(): number { + if (typeof crypto.randomInt === "function") { + return crypto.randomInt(0, 1000000) / 1000000; + } + return crypto.randomBytes(4).readUInt32LE(0) / 0x100000000; + } + + /** + * Constructs and initializes a horse stirrup fender stitching bench or slider rig. + */ + public static constructBench( + leatherworkerPlayerId: string, + benchType: FenderBenchType + ): ActiveFenderBench { + const data = FENDER_BENCH_CATALOG[benchType]; + if (!data) { + throw new Error(`Unsupported horse stirrup fender bench type: ${String(benchType)}`); + } + + const uuid = this.generateSecureId(); + + return { + benchId: `bench_${benchType.toLowerCase()}_${uuid}`, + leatherworkerPlayerId, + benchType, + currentDurability: data.maxDurability, + maxDurability: data.maxDurability, + isFunctional: true, + }; + } + + /** + * Stitches and tensions reinforced fender panels and tempered mithril fender sliders into horse stirrup fenders. + * Returns an updated clone of `bench` leaving the input instance immutable. + */ + public static craftFender( + bench: ActiveFenderBench, + recipeType: FenderRecipeType, + providedLeathers: RawLeatherFenderType[], + craftRoll?: number, + shieldingRoll?: number, + currentEpochMs = Date.now() + ): { success: boolean; fender?: CraftedHorseFender; updatedBench?: ActiveFenderBench; remainingDurability: number; remainingProvidedLeathers: RawLeatherFenderType[]; reason?: string } { + const fallbackLeathers = Array.isArray(providedLeathers) ? [...providedLeathers] : []; + + if (!bench || !bench.isFunctional || bench.currentDurability < this.DURABILITY_COST_PER_CRAFT) { + return { + success: false, + updatedBench: bench ? { ...bench } : undefined, + remainingDurability: bench?.currentDurability ?? 0, + remainingProvidedLeathers: fallbackLeathers, + reason: `Horse stirrup fender bench is warped or lacks durability (requires ${this.DURABILITY_COST_PER_CRAFT}).`, + }; + } + + const benchData = FENDER_BENCH_CATALOG[bench.benchType]; + if (!benchData) { + return { success: false, updatedBench: { ...bench }, remainingDurability: bench.currentDurability, remainingProvidedLeathers: fallbackLeathers, reason: `Unknown bench model: ${String(bench.benchType)}` }; + } + + const recipe = FENDER_RECIPE_CATALOG[recipeType]; + if (!recipe) { + return { success: false, updatedBench: { ...bench }, remainingDurability: bench.currentDurability, remainingProvidedLeathers: fallbackLeathers, reason: `Unknown horse stirrup fender recipe: ${String(recipeType)}` }; + } + + if (!Array.isArray(providedLeathers)) { + return { success: false, updatedBench: { ...bench }, remainingDurability: bench.currentDurability, remainingProvidedLeathers: [], reason: "Invalid leathers array." }; + } + + // Count matching leather materials + const matchingCount = providedLeathers.filter(l => l === recipe.requiredLeatherType).length; + if (matchingCount < recipe.requiredLeatherCount) { + return { + success: false, + updatedBench: { ...bench }, + remainingDurability: bench.currentDurability, + remainingProvidedLeathers: fallbackLeathers, + reason: `Insufficient fender panels/slider sets: requires ${recipe.requiredLeatherCount}x ${recipe.requiredLeatherType}, provided ${matchingCount}.`, + }; + } + + // Create updated bench clone + const updatedBench = { ...bench }; + + // Deduct durability on clone + updatedBench.currentDurability -= this.DURABILITY_COST_PER_CRAFT; + if (updatedBench.currentDurability < this.DURABILITY_COST_PER_CRAFT) { + updatedBench.currentDurability = Math.max(0, updatedBench.currentDurability); + updatedBench.isFunctional = false; + } + + // Deduct materials upfront on all craft attempts + const remaining = [...providedLeathers]; + let removed = 0; + for (let i = remaining.length - 1; i >= 0 && removed < recipe.requiredLeatherCount; i--) { + if (remaining[i] === recipe.requiredLeatherType) { + remaining.splice(i, 1); + removed++; + } + } + + const safeRoll = typeof craftRoll === "number" && Number.isFinite(craftRoll) ? Math.max(0, Math.min(1, craftRoll)) : this.generateSecureRoll(); + const rollPercent = safeRoll * 100; + + if (rollPercent > benchData.baseSuccessRatePercent) { + return { + success: false, + updatedBench, + remainingDurability: updatedBench.currentDurability, + remainingProvidedLeathers: remaining, + reason: `Fender panel misaligned: mithril slider rail warped under tension rig, rolled ${rollPercent.toFixed(1)}, needed <= ${benchData.baseSuccessRatePercent}.`, + }; + } + + // Calculate independent leg shielding score dynamically using cached catalog maxima & authoritative catalog values (clamped 0% to 100%) + const { maxPower, maxBonus } = this.CATALOG_MAXIMA; + const safeShieldingRoll = typeof shieldingRoll === "number" && Number.isFinite(shieldingRoll) ? Math.max(0, Math.min(1, shieldingRoll)) : this.generateSecureRoll(); + const powerRatio = Math.min(1.0, benchData.leathercraftPower / maxPower); + const bonusPoints = (benchData.legShieldingBonusPercent / maxBonus) * 20; + const shieldingScore = Math.max(0, Math.min(100, Math.round( + (safeShieldingRoll * 40) + (powerRatio * 40) + bonusPoints + ))); + const qualityMultiplier = 0.8 + ((shieldingScore / 100) * 0.4); // 0.8 to 1.2x + + const finalProtection = Math.max(0, Math.min(100, Math.round(recipe.baseLegFrictionProtectionPercent * qualityMultiplier))); + const finalSwayBonus = Math.max(0, Math.min(100, Math.round(recipe.baseStirrupSwayMitigationBonusPercent * qualityMultiplier))); + + const uuid = this.generateSecureId(); + + const fender: CraftedHorseFender = { + fenderId: `fender_${recipeType.toLowerCase()}_${uuid}`, + recipeType, + finalLegFrictionProtectionPercent: finalProtection, + finalStirrupSwayMitigationBonusPercent: finalSwayBonus, + legShieldingPercent: shieldingScore, + consumedLeatherCount: recipe.requiredLeatherCount, + consumedLeatherType: recipe.requiredLeatherType, + remainingProvidedLeathers: remaining, + craftedEpochMs: currentEpochMs, + }; + + return { + success: true, + fender, + updatedBench, + remainingDurability: updatedBench.currentDurability, + remainingProvidedLeathers: remaining, + }; + } + + /** + * Cleans equestrian trail grime and maintains horse stirrup fender bench. + * Returns an updated clone of `bench` leaving the input instance immutable. + */ + public static maintainBench( + bench: ActiveFenderBench, + repairAmount = 50 + ): { success: boolean; updatedBench?: ActiveFenderBench; newDurability: number; isFunctional: boolean } { + if (!bench) return { success: false, newDurability: 0, isFunctional: false }; + + const updatedBench = { ...bench }; + const amt = Number.isFinite(repairAmount) ? Math.max(0, repairAmount) : 50; + updatedBench.currentDurability = Math.min(updatedBench.maxDurability, updatedBench.currentDurability + amt); + updatedBench.isFunctional = updatedBench.currentDurability >= this.DURABILITY_COST_PER_CRAFT; + + return { + success: true, + updatedBench, + newDurability: updatedBench.currentDurability, + isFunctional: updatedBench.isFunctional, + }; + } +} diff --git a/api/src/tests/ancientRunicLeatherHorseFenderBench.test.ts b/api/src/tests/ancientRunicLeatherHorseFenderBench.test.ts new file mode 100644 index 00000000..472e3208 --- /dev/null +++ b/api/src/tests/ancientRunicLeatherHorseFenderBench.test.ts @@ -0,0 +1,154 @@ +import { describe, it, expect } from "vitest"; +import { AncientRunicLeatherHorseFenderBenchEngine } from "../lib/ancientRunicLeatherHorseFenderBench"; +import type { ActiveFenderBench } from "../lib/ancientRunicLeatherHorseFenderBench"; + +describe("AncientRunicLeatherHorseFenderBenchEngine Stirrup Fender Stitching Benches & Rigs", () => { + it("crafts Celestial Void Valkyrie Sovereign Fender in Fender Sanctum achieving 100% shielding and returns spliced pelts", () => { + const bench = AncientRunicLeatherHorseFenderBenchEngine.constructBench("leather_01", "CELESTIAL_VOID_VALKYRIE_FENDER_SANCTUM"); + expect(bench.benchType).toBe("CELESTIAL_VOID_VALKYRIE_FENDER_SANCTUM"); + expect(bench.currentDurability).toBe(350); + + const initialLeathers = [ + "CELESTIAL_VOID_ASTRAL_FENDER_PELT", + "CELESTIAL_VOID_ASTRAL_FENDER_PELT", + "CELESTIAL_VOID_ASTRAL_FENDER_PELT" + ] as any[]; + + const craftRes = AncientRunicLeatherHorseFenderBenchEngine.craftFender( + bench, + "CELESTIAL_VOID_VALKYRIE_SOVEREIGN_FENDER", + initialLeathers, + 0.1, // Success roll + 1.0, // Shielding roll 1.0 -> 40 + 40 + 20 = 100% + 100000 + ); + + expect(craftRes.success).toBe(true); + expect(craftRes.fender?.recipeType).toBe("CELESTIAL_VOID_VALKYRIE_SOVEREIGN_FENDER"); + expect(craftRes.fender?.legShieldingPercent).toBe(100); + expect(craftRes.fender?.finalLegFrictionProtectionPercent).toBe(100); // 84 * 1.20 = 100.8 -> clamped to 100% + expect(craftRes.fender?.finalStirrupSwayMitigationBonusPercent).toBe(77); // 64 * 1.20 = 76.8 -> 77% + expect(craftRes.fender?.consumedLeatherCount).toBe(2); + expect(craftRes.fender?.consumedLeatherType).toBe("CELESTIAL_VOID_ASTRAL_FENDER_PELT"); + expect(craftRes.fender?.remainingProvidedLeathers.length).toBe(1); + expect(craftRes.remainingDurability).toBe(340); // 350 - 10 + }); + + it("verifies mid-range shielding roll and sub-100% quality scaling on Elder fender bench", () => { + const bench = AncientRunicLeatherHorseFenderBenchEngine.constructBench("leather_mid", "ELDER_FENDER_BENCH"); + // powerRatio = 30/130 = 0.23077, bonusPoints = (14/40)*20 = 7.0 + // safeShieldingRoll = 0.5 -> 0.5 * 40 = 20 + // score = Math.round(20 + 9.23077 + 7.0) = 36 + // qualityMultiplier = 0.8 + (36/100)*0.4 = 0.944 + // finalProtection = Math.round(24 * 0.944) = 23 + // finalSwayBonus = Math.round(14 * 0.944) = 13 + const craftRes = AncientRunicLeatherHorseFenderBenchEngine.craftFender( + bench, + "NOVICE_REINFORCED_LEG_FENDER", + ["TANNED_BUFFALO_FENDER_PANEL", "TANNED_BUFFALO_FENDER_PANEL"], + 0.1, + 0.5 + ); + + expect(craftRes.success).toBe(true); + expect(craftRes.fender?.legShieldingPercent).toBe(36); + expect(craftRes.fender?.finalLegFrictionProtectionPercent).toBe(23); + expect(craftRes.fender?.finalStirrupSwayMitigationBonusPercent).toBe(13); + }); + + it("handles bench becoming non-functional after successful craft when durability falls below threshold", () => { + const bench = AncientRunicLeatherHorseFenderBenchEngine.constructBench("leather_wear", "ELDER_FENDER_BENCH"); + bench.currentDurability = 15; + expect(bench.isFunctional).toBe(true); + + // First craft succeeds: 15 - 10 = 5 (< 10), so isFunctional flips to false in updatedBench + const res1 = AncientRunicLeatherHorseFenderBenchEngine.craftFender( + bench, + "NOVICE_REINFORCED_LEG_FENDER", + ["TANNED_BUFFALO_FENDER_PANEL", "TANNED_BUFFALO_FENDER_PANEL"], + 0.1 + ); + expect(res1.success).toBe(true); + expect(res1.remainingDurability).toBe(5); + expect(res1.updatedBench?.isFunctional).toBe(false); + + // Subsequent craft on updated bench is rejected and returns fallback array + const res2 = AncientRunicLeatherHorseFenderBenchEngine.craftFender( + res1.updatedBench!, + "NOVICE_REINFORCED_LEG_FENDER", + ["TANNED_BUFFALO_FENDER_PANEL", "TANNED_BUFFALO_FENDER_PANEL"] + ); + expect(res2.success).toBe(false); + expect(res2.reason).toContain("warped or lacks durability"); + expect(res2.remainingProvidedLeathers.length).toBe(2); + }); + + it("rejects crafting when insufficient leather is provided and returns provided leathers", () => { + const bench = AncientRunicLeatherHorseFenderBenchEngine.constructBench("leather_02", "ELDER_FENDER_BENCH"); + + const failRes = AncientRunicLeatherHorseFenderBenchEngine.craftFender( + bench, + "WARMASTER_MITHRIL_SLIDER_FENDER", + ["TEMPERED_MITHRIL_FENDER_SLIDER_SET"] + ); + + expect(failRes.success).toBe(false); + expect(failRes.reason).toContain("Insufficient fender panels/slider sets"); + expect(failRes.remainingProvidedLeathers.length).toBe(1); + expect(bench.currentDurability).toBe(95); + }); + + it("handles fender panel misaligned failure roll consuming durability and leathers", () => { + const bench = AncientRunicLeatherHorseFenderBenchEngine.constructBench("leather_03", "ELDER_FENDER_BENCH"); // 87% success + + const fail = AncientRunicLeatherHorseFenderBenchEngine.craftFender( + bench, + "NOVICE_REINFORCED_LEG_FENDER", + ["TANNED_BUFFALO_FENDER_PANEL", "TANNED_BUFFALO_FENDER_PANEL", "TANNED_BUFFALO_FENDER_PANEL"], + 0.95 + ); + + expect(fail.success).toBe(false); + expect(fail.reason).toContain("misaligned"); + expect(fail.remainingProvidedLeathers?.length).toBe(1); // 3 - 2 = 1 remaining + expect(fail.remainingDurability).toBe(85); // 95 - 10 + }); + + it("gates isFunctional in maintainBench based on DURABILITY_COST_PER_CRAFT threshold and returns clone", () => { + const bench = AncientRunicLeatherHorseFenderBenchEngine.constructBench("leather_04", "ELDER_FENDER_BENCH"); + bench.currentDurability = 0; + bench.isFunctional = false; + + // Maintain 5 (below 10 required) -> isFunctional remains false + const repLow = AncientRunicLeatherHorseFenderBenchEngine.maintainBench(bench, 5); + expect(repLow.success).toBe(true); + expect(repLow.newDurability).toBe(5); + expect(repLow.isFunctional).toBe(false); + expect(bench.currentDurability).toBe(0); // input unchanged + + // Maintain 10 more on clone -> 15 (>= 10) -> isFunctional becomes true + const repHigh = AncientRunicLeatherHorseFenderBenchEngine.maintainBench(repLow.updatedBench!, 10); + expect(repHigh.success).toBe(true); + expect(repHigh.newDurability).toBe(15); + expect(repHigh.isFunctional).toBe(true); + }); + + it("guards against null inputs and unsupported bench models", () => { + expect(() => AncientRunicLeatherHorseFenderBenchEngine.constructBench("l", "PLASTIC_BENCH" as any)).toThrow( + "Unsupported horse stirrup fender bench type" + ); + + const invalidBench: ActiveFenderBench = { + benchId: "bad", + leatherworkerPlayerId: "p", + benchType: "BENCH" as any, + currentDurability: 50, + maxDurability: 50, + isFunctional: true, + }; + + expect(AncientRunicLeatherHorseFenderBenchEngine.craftFender(invalidBench, "NOVICE_REINFORCED_LEG_FENDER", ["TANNED_BUFFALO_FENDER_PANEL", "TANNED_BUFFALO_FENDER_PANEL"]).success).toBe(false); + expect(AncientRunicLeatherHorseFenderBenchEngine.craftFender(null as any, "NOVICE_REINFORCED_LEG_FENDER", []).success).toBe(false); + expect(AncientRunicLeatherHorseFenderBenchEngine.maintainBench(null as any).success).toBe(false); + }); +});