From 0b423f8a788e7eed334cbc177e26311afadde220 Mon Sep 17 00:00:00 2001 From: angelTomo9 <144371630+angelTomo9@users.noreply.github.com> Date: Mon, 31 Aug 2026 05:31:00 +0200 Subject: [PATCH] feat(glaziery): add ancient runic glass stained chancel window, lead came assembly, and holy light engine --- .../ancientRunicGlassStainedChancelWindow.ts | 247 ++++++++++++++++++ ...ientRunicGlassStainedChancelWindow.test.ts | 134 ++++++++++ 2 files changed, 381 insertions(+) create mode 100644 api/src/lib/ancientRunicGlassStainedChancelWindow.ts create mode 100644 api/src/tests/ancientRunicGlassStainedChancelWindow.test.ts diff --git a/api/src/lib/ancientRunicGlassStainedChancelWindow.ts b/api/src/lib/ancientRunicGlassStainedChancelWindow.ts new file mode 100644 index 00000000..e94d93c1 --- /dev/null +++ b/api/src/lib/ancientRunicGlassStainedChancelWindow.ts @@ -0,0 +1,247 @@ +import crypto from "node:crypto"; + +/** + * Ancient Runic Glass Stained Chancel Window, Lead Came Assembly & Holy Light Refraction Engine for OpenAO MMORPG. + * Simulates stained window glazier benches and lead came easels (Cedar Window Assembly Bench, Runic Lead Came Easel, Celestial Void Chancel Sanctum), + * stained glass rondels and flashed colored glass panes (Cathedral Blue Rondel, Ruby Red Flashed Glass Pane, Celestial Void Holy Light Plate), + * chancel window and cathedral rose window recipes (Saint Benedict Protective Window, Sunburst Holy Blessing Chancel, Celestial Void Seraphic Rose Window), + * independent holy sanctuary warding ratings (0% to 100%), clamped holy warding and clamped health regen aura scaling, + * upfront glass material deduction on all craft attempts, consistent remainingProvidedGlass return shapes across all paths, cached static catalog maxima, authoritative catalog power ratio, and assembly easel maintenance. + */ + +export type WindowAssemblyBenchType = "CEDAR_WINDOW_ASSEMBLY_BENCH" | "RUNIC_LEAD_CAME_EASEL" | "CELESTIAL_VOID_CHANCEL_SANCTUM"; +export type RawStainedGlassType = "CATHEDRAL_BLUE_RONDEL" | "RUBY_RED_FLASHED_GLASS_PANE" | "CELESTIAL_VOID_HOLY_LIGHT_PLATE"; +export type ChancelWindowRecipeType = "SAINT_BENEDICT_PROTECTIVE_WINDOW" | "SUNBURST_HOLY_BLESSING_CHANCEL" | "CELESTIAL_VOID_SERAPHIC_ROSE_WINDOW"; + +export interface WindowAssemblyBenchData { + benchType: WindowAssemblyBenchType; + maxDurability: number; + glazieryPower: number; + baseSuccessRatePercent: number; // 0 to 100 + sanctityBonusPercent: number; +} + +export interface ChancelWindowRecipeData { + recipeType: ChancelWindowRecipeType; + requiredGlassType: RawStainedGlassType; + requiredGlassCount: number; + baseHolyWardingPercent: number; + baseHealthRegenAuraPercent: number; +} + +export interface ActiveWindowAssemblyBench { + benchId: string; + glazierPlayerId: string; + benchType: WindowAssemblyBenchType; + currentDurability: number; + maxDurability: number; + glazieryPower: number; + isFunctional: boolean; +} + +export interface CraftedChancelWindow { + windowId: string; + recipeType: ChancelWindowRecipeType; + finalHolyWardingPercent: number; + finalHealthRegenAuraPercent: number; + sanctuarySanctityPercent: number; // 0 to 100 + consumedGlassCount: number; + consumedGlassType: RawStainedGlassType; + remainingProvidedGlass: RawStainedGlassType[]; + craftedEpochMs: number; +} + +export const CHANCEL_BENCH_CATALOG: Record = { + CEDAR_WINDOW_ASSEMBLY_BENCH: { benchType: "CEDAR_WINDOW_ASSEMBLY_BENCH", maxDurability: 75, glazieryPower: 25, baseSuccessRatePercent: 85, sanctityBonusPercent: 10 }, + RUNIC_LEAD_CAME_EASEL: { benchType: "RUNIC_LEAD_CAME_EASEL", maxDurability: 170, glazieryPower: 65, baseSuccessRatePercent: 92, sanctityBonusPercent: 20 }, + CELESTIAL_VOID_CHANCEL_SANCTUM: { benchType: "CELESTIAL_VOID_CHANCEL_SANCTUM", maxDurability: 310, glazieryPower: 120, baseSuccessRatePercent: 99, sanctityBonusPercent: 35 }, +}; + +export const CHANCEL_RECIPE_CATALOG: Record = { + SAINT_BENEDICT_PROTECTIVE_WINDOW: { recipeType: "SAINT_BENEDICT_PROTECTIVE_WINDOW", requiredGlassType: "CATHEDRAL_BLUE_RONDEL", requiredGlassCount: 2, baseHolyWardingPercent: 20, baseHealthRegenAuraPercent: 10 }, + SUNBURST_HOLY_BLESSING_CHANCEL: { recipeType: "SUNBURST_HOLY_BLESSING_CHANCEL", requiredGlassType: "RUBY_RED_FLASHED_GLASS_PANE", requiredGlassCount: 2, baseHolyWardingPercent: 45, baseHealthRegenAuraPercent: 25 }, + CELESTIAL_VOID_SERAPHIC_ROSE_WINDOW: { recipeType: "CELESTIAL_VOID_SERAPHIC_ROSE_WINDOW", requiredGlassType: "CELESTIAL_VOID_HOLY_LIGHT_PLATE", requiredGlassCount: 2, baseHolyWardingPercent: 85, baseHealthRegenAuraPercent: 60 }, +}; + +export class AncientRunicGlassStainedChancelWindowEngine { + 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(CHANCEL_BENCH_CATALOG).map(b => b.glazieryPower), 1), + maxBonus: Math.max(...Object.values(CHANCEL_BENCH_CATALOG).map(b => b.sanctityBonusPercent), 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"); + } + + /** + * Constructs and initializes a stained glass window assembly bench or lead came easel. + */ + public static constructBench( + glazierPlayerId: string, + benchType: WindowAssemblyBenchType + ): ActiveWindowAssemblyBench { + const data = CHANCEL_BENCH_CATALOG[benchType]; + if (!data) { + throw new Error(`Unsupported window assembly bench type: ${String(benchType)}`); + } + + const uuid = this.generateSecureId(); + + return { + benchId: `bench_${benchType.toLowerCase()}_${uuid}`, + glazierPlayerId, + benchType, + currentDurability: data.maxDurability, + maxDurability: data.maxDurability, + glazieryPower: data.glazieryPower, + isFunctional: true, + }; + } + + /** + * Assembles and solders stained glass rondels into cathedral chancel windows and rose windows. + * Note: Mutates the passed `bench` in place and returns it as `updatedBench` for caller ergonomics. + */ + public static assembleWindow( + bench: ActiveWindowAssemblyBench, + recipeType: ChancelWindowRecipeType, + providedGlass: RawStainedGlassType[], + craftRoll = Math.random(), + sanctityRoll = Math.random(), + currentEpochMs = Date.now() + ): { success: boolean; window?: CraftedChancelWindow; updatedBench?: ActiveWindowAssemblyBench; remainingDurability: number; remainingProvidedGlass: RawStainedGlassType[]; reason?: string } { + const fallbackGlass = Array.isArray(providedGlass) ? [...providedGlass] : []; + + if (!bench || !bench.isFunctional || bench.currentDurability < this.DURABILITY_COST_PER_CRAFT) { + return { + success: false, + updatedBench: bench, + remainingDurability: bench?.currentDurability ?? 0, + remainingProvidedGlass: fallbackGlass, + reason: `Window assembly bench is rickety or lacks durability (requires ${this.DURABILITY_COST_PER_CRAFT}).`, + }; + } + + const benchData = CHANCEL_BENCH_CATALOG[bench.benchType]; + if (!benchData) { + return { success: false, updatedBench: bench, remainingDurability: bench.currentDurability, remainingProvidedGlass: fallbackGlass, reason: `Unknown bench model: ${String(bench.benchType)}` }; + } + + const recipe = CHANCEL_RECIPE_CATALOG[recipeType]; + if (!recipe) { + return { success: false, updatedBench: bench, remainingDurability: bench.currentDurability, remainingProvidedGlass: fallbackGlass, reason: `Unknown chancel recipe: ${String(recipeType)}` }; + } + + if (!Array.isArray(providedGlass)) { + return { success: false, updatedBench: bench, remainingDurability: bench.currentDurability, remainingProvidedGlass: [], reason: "Invalid glass array." }; + } + + // Count matching glass panes + const matchingCount = providedGlass.filter(g => g === recipe.requiredGlassType).length; + if (matchingCount < recipe.requiredGlassCount) { + return { + success: false, + updatedBench: bench, + remainingDurability: bench.currentDurability, + remainingProvidedGlass: fallbackGlass, + reason: `Insufficient glass pane: requires ${recipe.requiredGlassCount}x ${recipe.requiredGlassType}, provided ${matchingCount}.`, + }; + } + + // Deduct durability in place + bench.currentDurability -= this.DURABILITY_COST_PER_CRAFT; + if (bench.currentDurability < this.DURABILITY_COST_PER_CRAFT) { + bench.currentDurability = Math.max(0, bench.currentDurability); + bench.isFunctional = false; + } + + // Deduct materials upfront on all craft attempts + const remaining = [...providedGlass]; + let removed = 0; + for (let i = remaining.length - 1; i >= 0 && removed < recipe.requiredGlassCount; i--) { + if (remaining[i] === recipe.requiredGlassType) { + remaining.splice(i, 1); + removed++; + } + } + + const safeRoll = Number.isFinite(craftRoll) ? Math.max(0, Math.min(1, craftRoll)) : Math.random(); + const rollPercent = safeRoll * 100; + + if (rollPercent > benchData.baseSuccessRatePercent) { + return { + success: false, + updatedBench: bench, + remainingDurability: bench.currentDurability, + remainingProvidedGlass: remaining, + reason: `Lead came buckled: solder seam warped under iron heat, rolled ${rollPercent.toFixed(1)}, needed <= ${benchData.baseSuccessRatePercent}.`, + }; + } + + // Calculate independent sanctuary sanctity score (0% to 100%) dynamically using cached catalog maxima & authoritative catalog values + const { maxPower, maxBonus } = this.CATALOG_MAXIMA; + const safeSanctityRoll = Number.isFinite(sanctityRoll) ? Math.max(0, Math.min(1, sanctityRoll)) : Math.random(); + const powerRatio = Math.min(1.0, benchData.glazieryPower / maxPower); + const bonusPoints = (benchData.sanctityBonusPercent / maxBonus) * 20; + const sanctityScore = Math.max(0, Math.min(100, Math.round( + (safeSanctityRoll * 40) + (powerRatio * 40) + bonusPoints + ))); + const qualityMultiplier = 0.8 + ((sanctityScore / 100) * 0.4); // 0.8 to 1.2x + + const finalWarding = Math.max(0, Math.min(100, Math.round(recipe.baseHolyWardingPercent * qualityMultiplier))); + const finalRegen = Math.max(0, Math.min(100, Math.round(recipe.baseHealthRegenAuraPercent * qualityMultiplier))); + + const uuid = this.generateSecureId(); + + const chancelWindow: CraftedChancelWindow = { + windowId: `window_${recipeType.toLowerCase()}_${uuid}`, + recipeType, + finalHolyWardingPercent: finalWarding, + finalHealthRegenAuraPercent: finalRegen, + sanctuarySanctityPercent: sanctityScore, + consumedGlassCount: recipe.requiredGlassCount, + consumedGlassType: recipe.requiredGlassType, + remainingProvidedGlass: remaining, + craftedEpochMs: currentEpochMs, + }; + + return { + success: true, + window: chancelWindow, + updatedBench: bench, + remainingDurability: bench.currentDurability, + remainingProvidedGlass: remaining, + }; + } + + /** + * Re-leads came tracks and maintains window assembly bench. + */ + public static maintainBench( + bench: ActiveWindowAssemblyBench, + repairAmount = 50 + ): { success: boolean; newDurability: number; isFunctional: boolean } { + if (!bench) return { success: false, newDurability: 0, isFunctional: false }; + + const amt = Number.isFinite(repairAmount) ? Math.max(0, repairAmount) : 50; + bench.currentDurability = Math.min(bench.maxDurability, bench.currentDurability + amt); + bench.isFunctional = bench.currentDurability >= this.DURABILITY_COST_PER_CRAFT; + + return { + success: true, + newDurability: bench.currentDurability, + isFunctional: bench.isFunctional, + }; + } +} \ No newline at end of file diff --git a/api/src/tests/ancientRunicGlassStainedChancelWindow.test.ts b/api/src/tests/ancientRunicGlassStainedChancelWindow.test.ts new file mode 100644 index 00000000..35be1a6a --- /dev/null +++ b/api/src/tests/ancientRunicGlassStainedChancelWindow.test.ts @@ -0,0 +1,134 @@ +import { describe, it, expect } from "vitest"; +import { + AncientRunicGlassStainedChancelWindowEngine, + ActiveWindowAssemblyBench, +} from "../lib/ancientRunicGlassStainedChancelWindow.js"; + +describe("AncientRunicGlassStainedChancelWindowEngine Window Benches & Rose Windows", () => { + it("assembles Celestial Void Seraphic Rose Window in Chancel Sanctum achieving 100% sanctity and returns spliced plates", () => { + const bench = AncientRunicGlassStainedChancelWindowEngine.constructBench("glazier_01", "CELESTIAL_VOID_CHANCEL_SANCTUM"); + expect(bench.benchType).toBe("CELESTIAL_VOID_CHANCEL_SANCTUM"); + expect(bench.currentDurability).toBe(310); + + const initialPlates = [ + "CELESTIAL_VOID_HOLY_LIGHT_PLATE", + "CELESTIAL_VOID_HOLY_LIGHT_PLATE", + "CELESTIAL_VOID_HOLY_LIGHT_PLATE" + ] as any[]; + + const craftRes = AncientRunicGlassStainedChancelWindowEngine.assembleWindow( + bench, + "CELESTIAL_VOID_SERAPHIC_ROSE_WINDOW", + initialPlates, + 0.1, // Success roll + 1.0, // Sanctity roll 1.0 -> 40 + 40 + 20 = 100% + 100000 + ); + + expect(craftRes.success).toBe(true); + expect(craftRes.window?.recipeType).toBe("CELESTIAL_VOID_SERAPHIC_ROSE_WINDOW"); + expect(craftRes.window?.sanctuarySanctityPercent).toBe(100); + expect(craftRes.window?.finalHolyWardingPercent).toBe(100); // 85 * 1.20 = 102 -> clamped to 100% + expect(craftRes.window?.finalHealthRegenAuraPercent).toBe(72); // 60 * 1.20 = 72% + expect(craftRes.window?.consumedGlassCount).toBe(2); + expect(craftRes.window?.consumedGlassType).toBe("CELESTIAL_VOID_HOLY_LIGHT_PLATE"); + expect(craftRes.window?.remainingProvidedGlass.length).toBe(1); + expect(craftRes.remainingDurability).toBe(300); // 310 - 10 + }); + + it("handles bench becoming non-functional after successful craft when durability falls below threshold", () => { + const bench = AncientRunicGlassStainedChancelWindowEngine.constructBench("glazier_wear", "CEDAR_WINDOW_ASSEMBLY_BENCH"); + bench.currentDurability = 15; + expect(bench.isFunctional).toBe(true); + + // First craft succeeds: 15 - 10 = 5 (< 10), so isFunctional flips to false + const res1 = AncientRunicGlassStainedChancelWindowEngine.assembleWindow( + bench, + "SAINT_BENEDICT_PROTECTIVE_WINDOW", + ["CATHEDRAL_BLUE_RONDEL", "CATHEDRAL_BLUE_RONDEL"], + 0.1 + ); + expect(res1.success).toBe(true); + expect(res1.remainingDurability).toBe(5); + expect(bench.isFunctional).toBe(false); + + // Subsequent craft is rejected and returns fallback array + const res2 = AncientRunicGlassStainedChancelWindowEngine.assembleWindow( + bench, + "SAINT_BENEDICT_PROTECTIVE_WINDOW", + ["CATHEDRAL_BLUE_RONDEL", "CATHEDRAL_BLUE_RONDEL"] + ); + expect(res2.success).toBe(false); + expect(res2.reason).toContain("rickety or lacks durability"); + expect(res2.remainingProvidedGlass.length).toBe(2); + }); + + it("rejects crafting when insufficient glass is provided and returns provided glass", () => { + const bench = AncientRunicGlassStainedChancelWindowEngine.constructBench("glazier_02", "CEDAR_WINDOW_ASSEMBLY_BENCH"); + + const failRes = AncientRunicGlassStainedChancelWindowEngine.assembleWindow( + bench, + "SUNBURST_HOLY_BLESSING_CHANCEL", + ["RUBY_RED_FLASHED_GLASS_PANE"] + ); + + expect(failRes.success).toBe(false); + expect(failRes.reason).toContain("Insufficient glass pane"); + expect(failRes.remainingProvidedGlass.length).toBe(1); + expect(bench.currentDurability).toBe(75); + }); + + it("handles lead came buckled failure roll consuming durability and glass panes", () => { + const bench = AncientRunicGlassStainedChancelWindowEngine.constructBench("glazier_03", "CEDAR_WINDOW_ASSEMBLY_BENCH"); // 85% success + + const fail = AncientRunicGlassStainedChancelWindowEngine.assembleWindow( + bench, + "SAINT_BENEDICT_PROTECTIVE_WINDOW", + ["CATHEDRAL_BLUE_RONDEL", "CATHEDRAL_BLUE_RONDEL", "CATHEDRAL_BLUE_RONDEL"], + 0.95 + ); + + expect(fail.success).toBe(false); + expect(fail.reason).toContain("buckled"); + expect(fail.remainingProvidedGlass?.length).toBe(1); // 3 - 2 = 1 remaining + expect(bench.currentDurability).toBe(65); // 75 - 10 + }); + + it("gates isFunctional in maintainBench based on DURABILITY_COST_PER_CRAFT threshold", () => { + const bench = AncientRunicGlassStainedChancelWindowEngine.constructBench("glazier_04", "CEDAR_WINDOW_ASSEMBLY_BENCH"); + bench.currentDurability = 0; + bench.isFunctional = false; + + // Maintain 5 (below 10 required) -> isFunctional remains false + const repLow = AncientRunicGlassStainedChancelWindowEngine.maintainBench(bench, 5); + expect(repLow.success).toBe(true); + expect(repLow.newDurability).toBe(5); + expect(repLow.isFunctional).toBe(false); + + // Maintain 10 more -> 15 (>= 10) -> isFunctional becomes true + const repHigh = AncientRunicGlassStainedChancelWindowEngine.maintainBench(bench, 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(() => AncientRunicGlassStainedChancelWindowEngine.constructBench("g", "PLASTIC_BENCH" as any)).toThrow( + "Unsupported window assembly bench type" + ); + + const invalidBench: ActiveWindowAssemblyBench = { + benchId: "bad", + glazierPlayerId: "p", + benchType: "BENCH" as any, + currentDurability: 50, + maxDurability: 50, + glazieryPower: 10, + isFunctional: true, + }; + + expect(AncientRunicGlassStainedChancelWindowEngine.assembleWindow(invalidBench, "SAINT_BENEDICT_PROTECTIVE_WINDOW", ["CATHEDRAL_BLUE_RONDEL", "CATHEDRAL_BLUE_RONDEL"]).success).toBe(false); + expect(AncientRunicGlassStainedChancelWindowEngine.assembleWindow(null as any, "SAINT_BENEDICT_PROTECTIVE_WINDOW", []).success).toBe(false); + expect(AncientRunicGlassStainedChancelWindowEngine.maintainBench(null as any).success).toBe(false); + }); +}); \ No newline at end of file