From 8c2ab6b952c223caabf902e9f4ec063632d9b0d2 Mon Sep 17 00:00:00 2001 From: angelTomo9 <144371630+angelTomo9@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:41:04 +0200 Subject: [PATCH] feat(leathercraft): add ancient runic leather falcon hood bench, plumage beak chape, and raptor blind engine --- .../lib/ancientRunicLeatherFalconHoodBench.ts | 261 ++++++++++++++++++ ...ancientRunicLeatherFalconHoodBench.test.ts | 154 +++++++++++ 2 files changed, 415 insertions(+) create mode 100644 api/src/lib/ancientRunicLeatherFalconHoodBench.ts create mode 100644 api/src/tests/ancientRunicLeatherFalconHoodBench.test.ts diff --git a/api/src/lib/ancientRunicLeatherFalconHoodBench.ts b/api/src/lib/ancientRunicLeatherFalconHoodBench.ts new file mode 100644 index 00000000..813c4217 --- /dev/null +++ b/api/src/lib/ancientRunicLeatherFalconHoodBench.ts @@ -0,0 +1,261 @@ +import crypto from "node:crypto"; + +/** + * Ancient Runic Leather Falcon Hood Bench, Plumage Beak Chape & Raptor Blind Engine for OpenAO MMORPG. + * Simulates falconry hood stitching benches and raptor blind shaping blocks (Oak Falcon Hood Bench, Runic Ironwood Falconer Rig, Celestial Void Seraphic Skyhunter Sanctum), + * raw tanned shadowhawk plumage leather blanks and gilded brass beak chape sets (Tanned Shadowhawk Plumage Leather Blank, Gilded Brass Beak Chape Set, Celestial Void Starlight Skyhunter Leather), + * novice calming blind hoods and seraphic horizon-piercing apex blind recipes (Novice Calming Blind Hood, Master Falconer Plumed Chape Hood, Celestial Void Seraphic Horizon-Piercing Apex Blind), + * independent plumage calming soothing & sensory dampening ratings (scaled across catalog baselines ~14% to 100%), calibrated clamped raptor scout vision range bonus and clamped raptor agitated stress 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 falcon hood bench maintenance. + */ + +export type FalconHoodBenchType = "OAK_FALCON_HOOD_BENCH" | "RUNIC_IRONWOOD_FALCONER_RIG" | "CELESTIAL_VOID_SERAPHIC_SKYHUNTER_SANCTUM"; +export type RawLeatherFalconHoodType = "TANNED_SHADOWHAWK_PLUMAGE_LEATHER_BLANK" | "GILDED_BRASS_BEAK_CHAPE_SET" | "CELESTIAL_VOID_STARLIGHT_SKYHUNTER_LEATHER"; +export type FalconHoodRecipeType = "NOVICE_CALMING_BLIND_HOOD" | "MASTER_FALCONER_PLUMED_CHAPE_HOOD" | "CELESTIAL_VOID_SERAPHIC_HORIZON_PIERCING_APEX_BLIND"; + +export interface FalconHoodBenchData { + benchType: FalconHoodBenchType; + maxDurability: number; + leathercraftPower: number; + baseSuccessRatePercent: number; // 0 to 100 + calmingBonusPercent: number; +} + +export interface FalconHoodRecipeData { + recipeType: FalconHoodRecipeType; + requiredLeatherType: RawLeatherFalconHoodType; + requiredLeatherCount: number; + baseRaptorScoutVisionRangeBonusPercent: number; + baseRaptorAgitatedStressMitigationPercent: number; +} + +export interface ActiveFalconHoodBench { + benchId: string; + leatherworkerPlayerId: string; + benchType: FalconHoodBenchType; + currentDurability: number; + maxDurability: number; + isFunctional: boolean; +} + +export interface CraftedFalconHood { + hoodId: string; + recipeType: FalconHoodRecipeType; + finalRaptorScoutVisionRangeBonusPercent: number; + finalRaptorAgitatedStressMitigationPercent: number; + plumageCalmingPercent: number; // Scaled rating (clamped 0 to 100%, with catalog bench baselines ~14% to 100%) + consumedLeatherCount: number; + consumedLeatherType: RawLeatherFalconHoodType; + remainingProvidedLeathers: RawLeatherFalconHoodType[]; + craftedEpochMs: number; +} + +export const FALCON_HOOD_BENCH_CATALOG: Record = { + OAK_FALCON_HOOD_BENCH: { benchType: "OAK_FALCON_HOOD_BENCH", maxDurability: 75, leathercraftPower: 25, baseSuccessRatePercent: 85, calmingBonusPercent: 10 }, + RUNIC_IRONWOOD_FALCONER_RIG: { benchType: "RUNIC_IRONWOOD_FALCONER_RIG", maxDurability: 170, leathercraftPower: 65, baseSuccessRatePercent: 92, calmingBonusPercent: 20 }, + CELESTIAL_VOID_SERAPHIC_SKYHUNTER_SANCTUM: { benchType: "CELESTIAL_VOID_SERAPHIC_SKYHUNTER_SANCTUM", maxDurability: 310, leathercraftPower: 120, baseSuccessRatePercent: 99, calmingBonusPercent: 35 }, +}; + +export const FALCON_HOOD_RECIPE_CATALOG: Record = { + NOVICE_CALMING_BLIND_HOOD: { recipeType: "NOVICE_CALMING_BLIND_HOOD", requiredLeatherType: "TANNED_SHADOWHAWK_PLUMAGE_LEATHER_BLANK", requiredLeatherCount: 2, baseRaptorScoutVisionRangeBonusPercent: 20, baseRaptorAgitatedStressMitigationPercent: 10 }, + MASTER_FALCONER_PLUMED_CHAPE_HOOD: { recipeType: "MASTER_FALCONER_PLUMED_CHAPE_HOOD", requiredLeatherType: "GILDED_BRASS_BEAK_CHAPE_SET", requiredLeatherCount: 2, baseRaptorScoutVisionRangeBonusPercent: 45, baseRaptorAgitatedStressMitigationPercent: 25 }, + CELESTIAL_VOID_SERAPHIC_HORIZON_PIERCING_APEX_BLIND: { recipeType: "CELESTIAL_VOID_SERAPHIC_HORIZON_PIERCING_APEX_BLIND", requiredLeatherType: "CELESTIAL_VOID_STARLIGHT_SKYHUNTER_LEATHER", requiredLeatherCount: 2, baseRaptorScoutVisionRangeBonusPercent: 80, baseRaptorAgitatedStressMitigationPercent: 60 }, +}; + +export class AncientRunicLeatherFalconHoodBenchEngine { + 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(FALCON_HOOD_BENCH_CATALOG).map(b => b.leathercraftPower), 1), + maxBonus: Math.max(...Object.values(FALCON_HOOD_BENCH_CATALOG).map(b => b.calmingBonusPercent), 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 falcon hood stitching bench or falconer rig. + */ + public static constructBench( + leatherworkerPlayerId: string, + benchType: FalconHoodBenchType + ): ActiveFalconHoodBench { + const data = FALCON_HOOD_BENCH_CATALOG[benchType]; + if (!data) { + throw new Error(`Unsupported falcon hood 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 rivets shadowhawk plumage blanks and beak chapes into falcon hoods. + * Returns an updated clone of `bench` leaving the input instance immutable. + */ + public static craftHood( + bench: ActiveFalconHoodBench, + recipeType: FalconHoodRecipeType, + providedLeathers: RawLeatherFalconHoodType[], + craftRoll?: number, + calmingRoll?: number, + currentEpochMs = Date.now() + ): { success: boolean; hood?: CraftedFalconHood; updatedBench?: ActiveFalconHoodBench; remainingDurability: number; remainingProvidedLeathers: RawLeatherFalconHoodType[]; 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: `Falcon hood bench is warped or lacks durability (requires ${this.DURABILITY_COST_PER_CRAFT}).`, + }; + } + + const benchData = FALCON_HOOD_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 = FALCON_HOOD_RECIPE_CATALOG[recipeType]; + if (!recipe) { + return { success: false, updatedBench: { ...bench }, remainingDurability: bench.currentDurability, remainingProvidedLeathers: fallbackLeathers, reason: `Unknown falcon hood 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 falcon leather/chapes: 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: `Beak chape misaligned: brass riveting punch crimped plumage eye blind, rolled ${rollPercent.toFixed(1)}, needed <= ${benchData.baseSuccessRatePercent}.`, + }; + } + + // Calculate independent plumage calming score dynamically using cached catalog maxima & authoritative catalog values (clamped 0% to 100%, scaling across catalog baselines) + const { maxPower, maxBonus } = this.CATALOG_MAXIMA; + const safeCalmingRoll = typeof calmingRoll === "number" && Number.isFinite(calmingRoll) ? Math.max(0, Math.min(1, calmingRoll)) : this.generateSecureRoll(); + const powerRatio = Math.min(1.0, benchData.leathercraftPower / maxPower); + const bonusPoints = (benchData.calmingBonusPercent / maxBonus) * 20; + const calmingScore = Math.max(0, Math.min(100, Math.round( + (safeCalmingRoll * 40) + (powerRatio * 40) + bonusPoints + ))); + const qualityMultiplier = 0.8 + ((calmingScore / 100) * 0.4); // 0.8 to 1.2x + + const finalVisionBonus = Math.max(0, Math.min(100, Math.round(recipe.baseRaptorScoutVisionRangeBonusPercent * qualityMultiplier))); + const finalStressMitigate = Math.max(0, Math.min(100, Math.round(recipe.baseRaptorAgitatedStressMitigationPercent * qualityMultiplier))); + + const uuid = this.generateSecureId(); + + const hood: CraftedFalconHood = { + hoodId: `hood_${recipeType.toLowerCase()}_${uuid}`, + recipeType, + finalRaptorScoutVisionRangeBonusPercent: finalVisionBonus, + finalRaptorAgitatedStressMitigationPercent: finalStressMitigate, + plumageCalmingPercent: calmingScore, + consumedLeatherCount: recipe.requiredLeatherCount, + consumedLeatherType: recipe.requiredLeatherType, + remainingProvidedLeathers: remaining, + craftedEpochMs: currentEpochMs, + }; + + return { + success: true, + hood, + updatedBench, + remainingDurability: updatedBench.currentDurability, + remainingProvidedLeathers: remaining, + }; + } + + /** + * Cleans plumage dust and maintains falcon hood bench. + * Returns an updated clone of `bench` leaving the input instance immutable. + */ + public static maintainBench( + bench: ActiveFalconHoodBench, + repairAmount = 50 + ): { success: boolean; updatedBench?: ActiveFalconHoodBench; 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, + }; + } +} \ No newline at end of file diff --git a/api/src/tests/ancientRunicLeatherFalconHoodBench.test.ts b/api/src/tests/ancientRunicLeatherFalconHoodBench.test.ts new file mode 100644 index 00000000..f7301be2 --- /dev/null +++ b/api/src/tests/ancientRunicLeatherFalconHoodBench.test.ts @@ -0,0 +1,154 @@ +import { describe, it, expect } from "vitest"; +import { AncientRunicLeatherFalconHoodBenchEngine } from "../lib/ancientRunicLeatherFalconHoodBench"; +import type { ActiveFalconHoodBench } from "../lib/ancientRunicLeatherFalconHoodBench"; + +describe("AncientRunicLeatherFalconHoodBenchEngine Falcon Benches & Raptor Hoods", () => { + it("crafts Celestial Void Seraphic Horizon-Piercing Apex Blind in Skyhunter Sanctum achieving 100% calming and returns spliced leathers", () => { + const bench = AncientRunicLeatherFalconHoodBenchEngine.constructBench("leather_01", "CELESTIAL_VOID_SERAPHIC_SKYHUNTER_SANCTUM"); + expect(bench.benchType).toBe("CELESTIAL_VOID_SERAPHIC_SKYHUNTER_SANCTUM"); + expect(bench.currentDurability).toBe(310); + + const initialLeathers = [ + "CELESTIAL_VOID_STARLIGHT_SKYHUNTER_LEATHER", + "CELESTIAL_VOID_STARLIGHT_SKYHUNTER_LEATHER", + "CELESTIAL_VOID_STARLIGHT_SKYHUNTER_LEATHER" + ] as any[]; + + const craftRes = AncientRunicLeatherFalconHoodBenchEngine.craftHood( + bench, + "CELESTIAL_VOID_SERAPHIC_HORIZON_PIERCING_APEX_BLIND", + initialLeathers, + 0.1, // Success roll + 1.0, // Calming roll 1.0 -> 40 + 40 + 20 = 100% + 100000 + ); + + expect(craftRes.success).toBe(true); + expect(craftRes.hood?.recipeType).toBe("CELESTIAL_VOID_SERAPHIC_HORIZON_PIERCING_APEX_BLIND"); + expect(craftRes.hood?.plumageCalmingPercent).toBe(100); + expect(craftRes.hood?.finalRaptorScoutVisionRangeBonusPercent).toBe(96); // 80 * 1.20 = 96% + expect(craftRes.hood?.finalRaptorAgitatedStressMitigationPercent).toBe(72); // 60 * 1.20 = 72% + expect(craftRes.hood?.consumedLeatherCount).toBe(2); + expect(craftRes.hood?.consumedLeatherType).toBe("CELESTIAL_VOID_STARLIGHT_SKYHUNTER_LEATHER"); + expect(craftRes.hood?.remainingProvidedLeathers.length).toBe(1); + expect(craftRes.remainingDurability).toBe(300); // 310 - 10 + }); + + it("verifies mid-range calming roll and sub-100% quality scaling on Oak falcon hood bench", () => { + const bench = AncientRunicLeatherFalconHoodBenchEngine.constructBench("leather_mid", "OAK_FALCON_HOOD_BENCH"); + // powerRatio = 25/120 = 0.20833, bonusPoints = (10/35)*20 = 5.714 + // safeCalmingRoll = 0.5 -> 0.5 * 40 = 20 + // calmingScore = Math.round(20 + 8.333 + 5.714) = 34 + // qualityMultiplier = 0.8 + (34/100)*0.4 = 0.8 + 0.136 = 0.936 + // finalVisionBonus = Math.round(20 * 0.936) = 19 + // finalStressMitigate = Math.round(10 * 0.936) = 9 + const craftRes = AncientRunicLeatherFalconHoodBenchEngine.craftHood( + bench, + "NOVICE_CALMING_BLIND_HOOD", + ["TANNED_SHADOWHAWK_PLUMAGE_LEATHER_BLANK", "TANNED_SHADOWHAWK_PLUMAGE_LEATHER_BLANK"], + 0.1, + 0.5 + ); + + expect(craftRes.success).toBe(true); + expect(craftRes.hood?.plumageCalmingPercent).toBe(34); + expect(craftRes.hood?.finalRaptorScoutVisionRangeBonusPercent).toBe(19); + expect(craftRes.hood?.finalRaptorAgitatedStressMitigationPercent).toBe(9); + }); + + it("handles bench becoming non-functional after successful craft when durability falls below threshold", () => { + const bench = AncientRunicLeatherFalconHoodBenchEngine.constructBench("leather_wear", "OAK_FALCON_HOOD_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 = AncientRunicLeatherFalconHoodBenchEngine.craftHood( + bench, + "NOVICE_CALMING_BLIND_HOOD", + ["TANNED_SHADOWHAWK_PLUMAGE_LEATHER_BLANK", "TANNED_SHADOWHAWK_PLUMAGE_LEATHER_BLANK"], + 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 = AncientRunicLeatherFalconHoodBenchEngine.craftHood( + res1.updatedBench!, + "NOVICE_CALMING_BLIND_HOOD", + ["TANNED_SHADOWHAWK_PLUMAGE_LEATHER_BLANK", "TANNED_SHADOWHAWK_PLUMAGE_LEATHER_BLANK"] + ); + 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 = AncientRunicLeatherFalconHoodBenchEngine.constructBench("leather_02", "OAK_FALCON_HOOD_BENCH"); + + const failRes = AncientRunicLeatherFalconHoodBenchEngine.craftHood( + bench, + "MASTER_FALCONER_PLUMED_CHAPE_HOOD", + ["GILDED_BRASS_BEAK_CHAPE_SET"] + ); + + expect(failRes.success).toBe(false); + expect(failRes.reason).toContain("Insufficient falcon leather/chapes"); + expect(failRes.remainingProvidedLeathers.length).toBe(1); + expect(bench.currentDurability).toBe(75); + }); + + it("handles beak chape misaligned failure roll consuming durability and leathers", () => { + const bench = AncientRunicLeatherFalconHoodBenchEngine.constructBench("leather_03", "OAK_FALCON_HOOD_BENCH"); // 85% success + + const fail = AncientRunicLeatherFalconHoodBenchEngine.craftHood( + bench, + "NOVICE_CALMING_BLIND_HOOD", + ["TANNED_SHADOWHAWK_PLUMAGE_LEATHER_BLANK", "TANNED_SHADOWHAWK_PLUMAGE_LEATHER_BLANK", "TANNED_SHADOWHAWK_PLUMAGE_LEATHER_BLANK"], + 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(65); // 75 - 10 + }); + + it("gates isFunctional in maintainBench based on DURABILITY_COST_PER_CRAFT threshold and returns clone", () => { + const bench = AncientRunicLeatherFalconHoodBenchEngine.constructBench("leather_04", "OAK_FALCON_HOOD_BENCH"); + bench.currentDurability = 0; + bench.isFunctional = false; + + // Maintain 5 (below 10 required) -> isFunctional remains false + const repLow = AncientRunicLeatherFalconHoodBenchEngine.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 = AncientRunicLeatherFalconHoodBenchEngine.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(() => AncientRunicLeatherFalconHoodBenchEngine.constructBench("l", "PLASTIC_BENCH" as any)).toThrow( + "Unsupported falcon hood bench type" + ); + + const invalidBench: ActiveFalconHoodBench = { + benchId: "bad", + leatherworkerPlayerId: "p", + benchType: "BENCH" as any, + currentDurability: 50, + maxDurability: 50, + isFunctional: true, + }; + + expect(AncientRunicLeatherFalconHoodBenchEngine.craftHood(invalidBench, "NOVICE_CALMING_BLIND_HOOD", ["TANNED_SHADOWHAWK_PLUMAGE_LEATHER_BLANK", "TANNED_SHADOWHAWK_PLUMAGE_LEATHER_BLANK"]).success).toBe(false); + expect(AncientRunicLeatherFalconHoodBenchEngine.craftHood(null as any, "NOVICE_CALMING_BLIND_HOOD", []).success).toBe(false); + expect(AncientRunicLeatherFalconHoodBenchEngine.maintainBench(null as any).success).toBe(false); + }); +}); \ No newline at end of file