#!/usr/bin/env python3
"""
Autonomous Missing Skills VFX Producer for FreeExile (Milestone 1 / High-Fidelity PBR PoT Pipeline).
Synthesizes procedural high-fidelity frames, 8-iteration morphological Dilation Padding,
Tangent-Space Sobel Normal Maps (with proper OpenCV BGR ordering: B=Nz, Blue mean > 128.0),
Power-of-Two 2048x2048 Sprite Sheets, and ASTC 4x4 containers for:
  - All 12 Active Skills (1001-1012)
  - Evasion Skill (1099)
  - All 5 Support Sigils (2001, 2002, 2004, 2005, 2006)
"""

from __future__ import annotations
import json, math, sys
from pathlib import Path
from typing import Dict, Any, List
import cv2, numpy as np

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from tools.asset_pipeline.astc_compressor import compress_png_to_astc
from tools.asset_pipeline.pbr_texture_synthesizer import PBRTextureSynthesizer


def create_cuu_tieu_loi_kiem_frame(step: float, size: int = 128) -> np.ndarray:
    """1001: Cửu Tiêu Lôi Kiếm - Giant white-hot thunder sword + crimson-purple lightning arcs."""
    img, cx = np.zeros((size, size, 4), dtype=np.uint8), size // 2
    slen = int(size * 0.75 * min(1.0, step * 1.5))
    top_y, bot_y = max(8, size // 2 - slen // 2), min(size - 8, size // 2 + slen // 2)
    cv2.line(img, (cx, top_y), (cx, bot_y), (200, 40, 160, int(200 * (1.0 - step * 0.2))), 10)
    cv2.line(img, (cx, top_y), (cx, bot_y), (255, 250, 240, 255), 4)
    np.random.seed(int(step * 1001))
    for _ in range(5):
        sy = np.random.randint(top_y, bot_y)
        ex, ey = cx + np.random.randint(-35, 36), sy + np.random.randint(-15, 16)
        mx, my = (cx + ex) // 2 + np.random.randint(-10, 11), (sy + ey) // 2 + np.random.randint(-10, 11)
        cv2.line(img, (cx, sy), (mx, my), (230, 80, 220, 220), 2)
        cv2.line(img, (mx, my), (ex, ey), (255, 220, 255, 240), 1)
    if step > 0.4:
        rg = int((step - 0.4) * 45)
        cv2.ellipse(img, (cx, bot_y), (rg, rg // 2), 0, 0, 360, (230, 60, 180, int(240 * (1.0 - step))), 2)
    return img

def create_thanh_phong_van_kiem_vu_frame(step: float, size: int = 128) -> np.ndarray:
    """1002: Thanh Phong Vạn Kiếm Vũ - Fan of 8 radiating wind-lightning blade arcs at 120 degrees."""
    img, ox, oy, base_dist = np.zeros((size, size, 4), dtype=np.uint8), size // 2, size - 16, 16 + step * 58
    for i in range(8):
        ang = -math.pi * 0.85 + (i / 7.0) * (math.pi * 0.70)
        px, py = int(ox + base_dist * math.cos(ang)), int(oy + base_dist * 0.75 * math.sin(ang))
        tx, ty = int(ox + (base_dist - 18) * math.cos(ang)), int(oy + (base_dist - 18) * 0.75 * math.sin(ang))
        cv2.line(img, (tx, ty), (px, py), (240, 210, 160, int(230 * (1.0 - step * 0.2))), 3)
        cv2.circle(img, (px, py), 2, (255, 255, 255, 255), -1)
    cv2.ellipse(img, (ox, oy), (int(base_dist), int(base_dist * 0.6)), 0, 190, 350, (210, 170, 120, int(160 * (1.0 - step))), 2)
    return img

def create_u_minh_huyet_doc_cham_frame(step: float, size: int = 128) -> np.ndarray:
    """1003: U Minh Huyết Độc Châm - Barbed bone needle dripping dark emerald and coagulated blood poison."""
    img, cx, cy, travel = np.zeros((size, size, 4), dtype=np.uint8), size // 2, size // 2, int(step * 40)
    for ox, oy in [(-16, -10), (0, 0), (16, 10)]:
        nx, ny = cx + ox + travel, cy + oy + travel // 2
        tx, ty = nx - 22, ny - 11
        cv2.line(img, (tx - 8, ty - 4), (tx, ty), (25, 140, 30, int(150 * (1.0 - step))), 4)
        cv2.line(img, (tx, ty), (nx, ny), (180, 215, 225, 255), 2)
        cv2.circle(img, (nx, ny), 3, (20, 200, 40, 255), -1)
        cv2.circle(img, (nx - 4, ny - 2), 2, (30, 25, 190, 230), -1)
    return img

def create_thien_ma_boc_doc_kinh_frame(step: float, size: int = 128) -> np.ndarray:
    """1004: Thiên Ma Bộc Độc Kình - Expanding demonic poison nova blast + exploding toxic miasma."""
    img, center, rx = np.zeros((size, size, 4), dtype=np.uint8), (size // 2, size // 2 + 8), int(14 + step * 44)
    ry = int(rx * 0.55)
    cv2.ellipse(img, center, (rx, ry), 0, 0, 360, (25, 160, 35, int(220 * (1.0 - step))), 4)
    cv2.ellipse(img, center, (max(4, rx - 10), max(2, ry - 6)), 0, 0, 360, (15, 90, 20, int(160 * (1.0 - step))), -1)
    np.random.seed(int(step * 1004))
    for _ in range(12):
        ang, dist = np.random.uniform(0, math.pi * 2), rx * np.random.uniform(0.6, 1.1)
        bx, by = int(center[0] + dist * math.cos(ang)), int(center[1] + (dist * 0.55) * math.sin(ang))
        if 0 <= bx < size and 0 <= by < size:
            cv2.circle(img, (bx, by), np.random.randint(2, 5), (30, 220, 50, int(230 * (1.0 - step * 0.5))), -1)
    return img

def create_bang_phach_han_quang_tran_frame(step: float, size: int = 128) -> np.ndarray:
    """1005: Băng Phách Hàn Quang Trận - Glacial barbarian rune ring & sharp ice crystal spikes."""
    img, center, rx = np.zeros((size, size, 4), dtype=np.uint8), (size // 2, size // 2 + 10), int(size * 0.40)
    ry = int(rx * 0.52)
    cv2.ellipse(img, center, (rx, ry), 0, 0, 360, (245, 225, 160, int(200 * (0.8 + 0.2 * math.sin(step * math.pi)))), 2)
    cv2.ellipse(img, center, (rx - 12, ry - 6), 0, 0, 360, (230, 190, 100, int(150 * (1.0 - step * 0.3))), 1)
    for ang in [0.0, math.pi * 0.35, math.pi * 0.7, math.pi, math.pi * 1.35, math.pi * 1.7]:
        bx, by, sh = int(center[0] + (rx - 6) * math.cos(ang)), int(center[1] + (ry - 3) * math.sin(ang)), int(12 + step * 28)
        pts = np.array([[bx - 5, by], [bx, max(10, by - sh)], [bx + 5, by]], dtype=np.int32)
        cv2.fillConvexPoly(img, pts, (245, 210, 150, int(220 * min(1.0, step * 1.5))))
        cv2.polylines(img, [pts], True, (255, 255, 240, 255), 1)
    return img

def create_bang_tien_xuyen_tam_frame(step: float, size: int = 128) -> np.ndarray:
    """1006: Băng Tiễn Xuyên Tâm - Aerodynamic sharp frost arrow with crystalline shatter."""
    img, px, py = np.zeros((size, size, 4), dtype=np.uint8), int(18 + step * 76), size // 2
    for i in range(1, 6):
        cv2.circle(img, (px - i * 10, py + int(math.sin(i + step * 4) * 3)), 4 - (i // 2), (240, 200, 140, int(120 * (1.0 - step))), -1)
    pts = np.array([[px - 24, py - 7], [px + 8, py], [px - 24, py + 7], [px - 14, py]], dtype=np.int32)
    cv2.fillConvexPoly(img, pts, (250, 225, 170, 240))
    cv2.polylines(img, [pts], True, (255, 255, 255, 255), 1)
    if step > 0.6:
        np.random.seed(int(step * 1006))
        for _ in range(8):
            cv2.circle(img, (px + np.random.randint(0, 24), py + np.random.randint(-16, 17)), np.random.randint(1, 4), (255, 240, 200, 220), -1)
    return img

def create_fire_whirlwind_frame(step: float, size: int = 128) -> np.ndarray:
    """1007: Liệt Hỏa Toàn Phong Trảm - 360-degree raging flame whirlwind arc with embers."""
    img, center, r, sang = np.zeros((size, size, 4), dtype=np.uint8), (size // 2, size // 2), int(size * 0.38), step * 360.0
    cv2.ellipse(img, center, (r, int(r * 0.6)), 0, sang, sang + 145.0, (15, 75, 215, 220), 8)
    cv2.ellipse(img, center, (r - 4, int((r - 4) * 0.6)), 0, sang + 20, sang + 135.0, (40, 190, 255, 255), 4)
    np.random.seed(int(step * 555))
    for _ in range(10):
        ex, ey = center[0] + int(np.random.uniform(-r, r)), center[1] + int(np.random.uniform(-r * 0.5, r * 0.5))
        if 0 <= ex < size and 0 <= ey < size:
            cv2.circle(img, (ex, ey), int(np.random.uniform(1.5, 3.5)), (30, 160, 245, 240), -1)
    return img

def create_liet_diem_boc_khi_tram_frame(step: float, size: int = 128) -> np.ndarray:
    """1008: Liệt Diễm Bộc Khí Trảm - Downward cleave rifting ground with glowing magma fissures."""
    img, cx, flen = np.zeros((size, size, 4), dtype=np.uint8), size // 2, int(size * 0.8 * min(1.0, step * 1.4))
    sy, ey = max(12, size // 2 - flen // 2), min(size - 12, size // 2 + flen // 2)
    cv2.line(img, (cx, sy), (cx, ey), (20, 60, 230, int(220 * (1.0 - step * 0.2))), 6)
    cv2.line(img, (cx, sy), (cx, ey), (60, 210, 255, 255), 2)
    for fx, fy in [(cx - 14, sy + 15), (cx + 16, sy + 35), (cx - 18, sy + 55), (cx + 14, sy + 70)]:
        if fy < ey:
            cv2.line(img, (cx, fy - 10), (fx, fy), (20, 90, 240, 200), 2)
    np.random.seed(int(step * 1008))
    for _ in range(8):
        cv2.circle(img, (cx + np.random.randint(-18, 19), np.random.randint(sy, ey)), np.random.randint(2, 5), (35, 140, 250, 230), -1)
    return img

def create_ground_slam_frame(step: float, size: int = 128) -> np.ndarray:
    """1009: Kim Cang Trụy Địa Chấn - Massive ground slam crater & golden-earth shockwave."""
    img, center, rx = np.zeros((size, size, 4), dtype=np.uint8), (size // 2, size // 2 + 10), int(12 + step * 44)
    ry = int(rx * 0.52)
    cv2.ellipse(img, center, (rx, ry), 0, 0, 360, (20, 100, 180, int(230 * (1.0 - step))), 3)
    for fx, fy in [(-rx, 0), (rx, 0), (0, -ry), (0, ry), (int(rx * 0.7), int(ry * 0.7)), (int(-rx * 0.7), int(-ry * 0.7))]:
        mx, my = (center[0] + fx) // 2 + int(np.random.uniform(-4, 4)), (center[1] + fy) // 2 + int(np.random.uniform(-2, 2))
        cv2.line(img, center, (mx, my), (25, 60, 110, int(240 * (1.0 - step * 0.4))), 2)
        cv2.line(img, (mx, my), (center[0] + fx, center[1] + fy), (25, 60, 110, int(200 * (1.0 - step))), 2)
    return img

def create_kim_cang_bat_hoai_the_frame(step: float, size: int = 128) -> np.ndarray:
    """1010: Kim Cang Bất Hoại Thể - Ancient golden vajra bell protective dome."""
    img, cx, cy = np.zeros((size, size, 4), dtype=np.uint8), size // 2, size // 2 + 6
    rw, rh, by = int(size * 0.36), int(size * 0.44), cy + 12
    pw = int(rw * (1.0 + 0.15 * math.sin(step * math.pi)))
    cv2.ellipse(img, (cx, by), (pw, int(pw * 0.45)), 0, 0, 360, (40, 180, 230, int(180 * (1.0 - step * 0.3))), 3)
    cv2.ellipse(img, (cx, cy - 4), (rw, rh // 2), 0, 180, 360, (30, 160, 215, 230), 4)
    cv2.line(img, (cx - rw, cy - 4), (cx - rw, by), (30, 160, 215, 230), 4)
    cv2.line(img, (cx + rw, cy - 4), (cx + rw, by), (30, 160, 215, 230), 4)
    cv2.ellipse(img, (cx, by), (rw, int(rw * 0.35)), 0, 0, 360, (50, 200, 245, 255), 3)
    cv2.line(img, (cx - 12, cy - 10), (cx + 12, cy - 10), (80, 230, 255, 240), 2)
    cv2.line(img, (cx, cy - 22), (cx, cy + 2), (80, 230, 255, 240), 2)
    return img

def create_blood_drain_frame(step: float, size: int = 128) -> np.ndarray:
    """1011: Huyết Ma Khấp Huyết Kình - Chaos blood drain AoE (converging blood tendrils)."""
    img, center, r = np.zeros((size, size, 4), dtype=np.uint8), (size // 2, size // 2), int(size * 0.42 * (0.8 + 0.2 * math.sin(step * math.pi)))
    cv2.circle(img, center, r, (15, 10, 65, int(180 * (1.0 - step * 0.3))), 3)
    cv2.ellipse(img, center, (r - 8, r // 2), int(step * 180), 0, 360, (20, 25, 200, 220), 4)
    for i in range(6):
        ang, dist = (i / 6.0) * math.pi * 2 + step * 1.5, r * (1.0 - step * 0.7)
        sx, sy = int(center[0] + dist * math.cos(ang)), int(center[1] + (dist * 0.6) * math.sin(ang))
        cv2.line(img, (sx, sy), center, (10, 15, 180, int(210 * (1.0 - step * 0.4))), 2)
        cv2.circle(img, (sx, sy), 2, (30, 40, 240, 255), -1)
    return img

def create_bone_totem_frame(step: float, size: int = 128) -> np.ndarray:
    """1012: Man Cốt Triệu Hồn Đồ - Rising bone skull totem pillar + necrotic miasma."""
    img, cx, cy, mr = np.zeros((size, size, 4), dtype=np.uint8), size // 2, size // 2, int(20 + step * 25)
    cv2.ellipse(img, (cx, cy + 20), (mr, mr // 2), 0, 0, 360, (30, 50, 20, int(160 * (1.0 - step))), -1)
    ty = max(10, cy + 20 - int(10 + step * 42))
    cv2.rectangle(img, (cx - 7, ty), (cx + 7, cy + 20), (180, 205, 220, 240), -1)
    cv2.circle(img, (cx, ty), 11, (210, 225, 235, 255), -1)
    cv2.circle(img, (cx - 4, ty - 2), 2, (15, 15, 30, 255), -1)
    cv2.circle(img, (cx + 4, ty - 2), 2, (15, 15, 30, 255), -1)
    cv2.line(img, (cx - 8, ty + 4), (cx - 16, ty - 6), (190, 210, 225, 240), 2)
    cv2.line(img, (cx + 8, ty + 4), (cx + 16, ty - 6), (190, 210, 225, 240), 2)
    return img

def create_ghost_trail_frame(step: float, size: int = 128) -> np.ndarray:
    """1099: Huyễn Ảnh Bộ - Translucent crimson shadow afterimage + ground slide scratch."""
    img, cx, cy, am = np.zeros((size, size, 4), dtype=np.uint8), size // 2 - int(step * 30), size // 2, 1.0 - step
    cv2.ellipse(img, (cx + 15, cy), (12, 24), 25, 0, 360, (20, 20, 190, int(160 * am)), -1)
    cv2.circle(img, (cx + 25, cy - 18), 7, (20, 20, 210, int(190 * am)), -1)
    cv2.line(img, (cx - 20, cy + 24), (cx + 35, cy + 24), (25, 30, 45, int(190 * am)), 3)
    return img

def create_multi_projectile_frame(step: float, size: int = 128) -> np.ndarray:
    """Sigil 2001: Phân Quang Đa Hóa Phù - Radiant trio of split daggers fanning at 30 degrees."""
    img, cx, cy, dist = np.zeros((size, size, 4), dtype=np.uint8), size // 2 - 10, size // 2, 12 + step * 42
    for ang in [-0.38, 0.0, 0.38]:
        px, py = int(cx + dist * math.cos(ang)), int(cy + dist * math.sin(ang))
        cv2.line(img, (int(px - 16 * math.cos(ang)), int(py - 16 * math.sin(ang))), (px, py), (40, 160, 230, 220), 3)
        cv2.circle(img, (px, py), 4, (240, 245, 255, 255), -1)
    return img

def create_aoe_expansion_frame(step: float, size: int = 128) -> np.ndarray:
    """Sigil 2002: Bộc Phá Khuếch Đại Phù - 1.5x space-distortion expanding shockwave ring."""
    img, center, ro = np.zeros((size, size, 4), dtype=np.uint8), (size // 2, size // 2 + 8), int(14 + step * 48)
    roy = int(ro * 0.55)
    cv2.ellipse(img, center, (ro, roy), 0, 0, 360, (30, 120, 220, int(230 * (1.0 - step))), 4)
    cv2.ellipse(img, center, (max(4, ro - 8), max(2, roy - 4)), 0, 0, 360, (50, 190, 255, int(180 * (1.0 - step))), 2)
    return img

def create_blood_magic_frame(step: float, size: int = 128) -> np.ndarray:
    """Sigil 2004: Huyết Khí Quy Tông Phù - Arterial crimson vitality aura & pulsing blood veins."""
    img, center, rp = np.zeros((size, size, 4), dtype=np.uint8), (size // 2, size // 2), int(20 + 16 * math.sin(step * math.pi))
    cv2.circle(img, center, rp + 8, (15, 15, 120, int(150 * (1.0 - step * 0.3))), -1)
    for i in range(5):
        ang = (i / 5.0) * math.pi * 2 + step
        ex, ey = int(center[0] + rp * math.cos(ang)), int(center[1] + rp * math.sin(ang))
        cv2.line(img, center, (ex, ey), (25, 30, 220, 240), 3)
        cv2.circle(img, (ex, ey), 3, (40, 50, 255, 255), -1)
    return img

def create_chain_lightning_frame(step: float, size: int = 128) -> np.ndarray:
    """Sigil 2005: Lôi Đình Xâu Chuỗi Phù - Zigzag fractal lightning branches connecting 3 nodes."""
    img = np.zeros((size, size, 4), dtype=np.uint8)
    nodes = [(18, 24), (size // 2, size // 2 - 8), (size - 22, size - 26)]
    np.random.seed(int(step * 2005))
    for n in range(len(nodes) - 1):
        p1, p2 = nodes[n], nodes[n + 1]
        mid = ((p1[0] + p2[0]) // 2 + int(np.random.uniform(-12, 13)), (p1[1] + p2[1]) // 2 + int(np.random.uniform(-12, 13)))
        cv2.line(img, p1, mid, (160, 40, 130, 210), 5)
        cv2.line(img, mid, p2, (160, 40, 130, 210), 5)
        cv2.line(img, p1, mid, (240, 235, 255, 255), 2)
        cv2.line(img, mid, p2, (240, 235, 255, 255), 2)
        cv2.circle(img, p1, 4, (255, 255, 255, 255), -1)
        cv2.circle(img, p2, 4, (255, 255, 255, 255), -1)
    return img

def create_freeze_affliction_frame(step: float, size: int = 128) -> np.ndarray:
    """Sigil 2006: Hàn Khí Cực Lãnh Phù - Sub-zero frost crystallization & frozen needle burst."""
    img, center, rh = np.zeros((size, size, 4), dtype=np.uint8), (size // 2, size // 2), int(14 + step * 34)
    hex_pts = [[int(center[0] + rh * math.cos(i * math.pi / 3.0)), int(center[1] + rh * 0.65 * math.sin(i * math.pi / 3.0))] for i in range(6)]
    pts_arr = np.array(hex_pts, dtype=np.int32)
    cv2.polylines(img, [pts_arr], True, (240, 220, 170, int(220 * (1.0 - step * 0.3))), 2)
    for p in hex_pts:
        cv2.line(img, center, (p[0], p[1]), (250, 235, 200, int(200 * (1.0 - step * 0.2))), 1)
        cv2.circle(img, (p[0], p[1]), 2, (255, 255, 255, 255), -1)
    return img


def apply_dilation_padding(rgba: np.ndarray, iterations: int = 8) -> np.ndarray:
    """Extends RGB into transparent regions while strictly preserving original alpha."""
    return PBRTextureSynthesizer.apply_dilation_padding(rgba, iterations=iterations)


def generate_sobel_normal(diffuse_bgra: np.ndarray, intensity: float = 2.2) -> np.ndarray:
    """Synthesizes Tangent-Space Normal Map using Sobel operators (B=Nz, mean > 128.0)."""
    alpha = diffuse_bgra[:, :, 3]
    normal_bgr = PBRTextureSynthesizer.generate_normal_map(diffuse_bgra[:, :, :3], intensity=intensity, alpha=alpha)
    return np.dstack((normal_bgr, alpha))


def produce_all_skills_atlas() -> Dict[str, Any]:
    """Generates complete Power-of-Two 2048x2048 VFX Atlas with Normal Maps & Manifest."""
    frame_size, cols, rows = 128, 16, 16
    atlas_w, atlas_h = cols * frame_size, rows * frame_size
    diffuse = np.zeros((atlas_h, atlas_w, 4), dtype=np.uint8)

    skills_registry = [
        (1001, "skill_1001_cuu_tieu_loi_kiem", "Cửu Tiêu Lôi Kiếm", create_cuu_tieu_loi_kiem_frame, 0, 0),
        (1002, "skill_1002_thanh_phong_van_kiem_vu", "Thanh Phong Vạn Kiếm Vũ", create_thanh_phong_van_kiem_vu_frame, 0, 8),
        (1003, "skill_1003_u_minh_huyet_doc_cham", "U Minh Huyết Độc Châm", create_u_minh_huyet_doc_cham_frame, 1, 0),
        (1004, "skill_1004_thien_ma_boc_doc_kinh", "Thiên Ma Bộc Độc Kình", create_thien_ma_boc_doc_kinh_frame, 1, 8),
        (1005, "skill_1005_bang_phach_han_quang_tran", "Băng Phách Hàn Quang Trận", create_bang_phach_han_quang_tran_frame, 2, 0),
        (1006, "skill_1006_bang_tien_xuyen_tam", "Băng Tiễn Xuyên Tâm", create_bang_tien_xuyen_tam_frame, 2, 8),
        (1007, "skill_1007_liet_hoa_toan_phong_tram", "Liệt Hỏa Toàn Phong Trảm", create_fire_whirlwind_frame, 3, 0),
        (1008, "skill_1008_liet_diem_boc_khi_tram", "Liệt Diễm Bộc Khí Trảm", create_liet_diem_boc_khi_tram_frame, 3, 8),
        (1009, "skill_1009_kim_cang_truy_dia_chan", "Kim Cang Trụy Địa Chấn", create_ground_slam_frame, 4, 0),
        (1010, "skill_1010_kim_cang_bat_hoai_the", "Kim Cang Bất Hoại Thể", create_kim_cang_bat_hoai_the_frame, 4, 8),
        (1011, "skill_1011_huyet_ma_khap_huyet_kinh", "Huyết Ma Khấp Huyết Kình", create_blood_drain_frame, 5, 0),
        (1012, "skill_1012_man_cot_trieu_hon_do", "Man Cốt Triệu Hồn Đồ", create_bone_totem_frame, 5, 8),
        (1099, "skill_1099_huyen_anh_bo", "Huyễn Ảnh Bộ", create_ghost_trail_frame, 6, 0),
        (2001, "sigil_2001_phan_quang_da_hoa_phu", "Phân Quang Đa Hóa Phù", create_multi_projectile_frame, 6, 8),
        (2002, "sigil_2002_boc_pha_khuech_dai_phu", "Bộc Phá Khuếch Đại Phù", create_aoe_expansion_frame, 7, 0),
        (2004, "sigil_2004_huyet_khi_quy_tong_phu", "Huyết Khí Quy Tông Phù", create_blood_magic_frame, 7, 8),
        (2005, "sigil_2005_loi_dinh_xau_chuoi_phu", "Lôi Đình Xâu Chuỗi Phù", create_chain_lightning_frame, 8, 0),
        (2006, "sigil_2006_han_khi_cuc_lanh_phu", "Hàn Khí Cực Lãnh Phù", create_freeze_affliction_frame, 8, 8),
    ]

    manifest: Dict[str, Any] = {
        "name": "savage_primal_skills_vfx_atlas",
        "textureWidth": atlas_w, "textureHeight": atlas_h,
        "texture_width": atlas_w, "texture_height": atlas_h,
        "frameWidth": frame_size, "frameHeight": frame_size,
        "frame_width": frame_size, "frame_height": frame_size,
        "cols": cols, "defaultPivot": [0.5, 0.90], "pivot": [0.5, 0.90],
        "clips": {}, "frames": {}
    }

    aliases_map = {
        1007: ["fire_whirlwind_slash"], 1009: ["ground_slam_crater"],
        1011: ["blood_drain_aoe"], 1012: ["bone_totem_summon"],
        1099: ["ghost_trail_evasion"], 2001: ["multi_projectile_split"],
        2002: ["aoe_expansion"], 2004: ["blood_magic"],
        2005: ["chain_lightning_arc"], 2006: ["freeze_affliction"],
    }

    for skill_id, clip_key, name_vi, render_func, row, start_col in skills_registry:
        frames_list, frame_keys = [], []
        for c in range(4):
            step = (c + 1) / 4.0
            frame = render_func(step, frame_size)
            col = start_col + c
            x, y = col * frame_size, row * frame_size
            diffuse[y:y+frame_size, x:x+frame_size] = frame
            f_id = f"{clip_key}_{c}"
            frame_keys.append(f_id)
            frame_meta = {
                "index": c, "id": f_id, "name": f_id, "x": x, "y": y,
                "w": frame_size, "h": frame_size, "pivot": [0.5, 0.90],
                "uv": [x / atlas_w, y / atlas_h, (x + frame_size) / atlas_w, (y + frame_size) / atlas_h]
            }
            frames_list.append(frame_meta)
            manifest["frames"][f_id] = frame_meta

        clip_data = {
            "skill_id": skill_id, "name": name_vi, "clip_key": clip_key,
            "frameCount": 4, "fps": 16, "duration_ms": 250, "loop": False,
            "frames": frames_list, "frame_keys": frame_keys
        }
        manifest["clips"][clip_key] = clip_data
        manifest["clips"][str(skill_id)] = clip_data

        if skill_id in aliases_map:
            for alias in aliases_map[skill_id]:
                manifest["clips"][alias] = {**clip_data, "atlas_clip": clip_key}
                for c_idx, f_meta in enumerate(frames_list):
                    manifest["frames"][f"{alias}_{c_idx}"] = f_meta

    print("Applying 8-iteration morphological Dilation Padding...")
    diffuse = apply_dilation_padding(diffuse, iterations=8)
    print("Generating Tangent-Space Sobel Normal Map...")
    normal = generate_sobel_normal(diffuse, intensity=2.2)

    for t in [
        PROJECT_ROOT / "client" / "cocos" / "assets" / "resources" / "vfx",
        PROJECT_ROOT / "client" / "webapp" / "assets" / "vfx",
        PROJECT_ROOT / "assets" / "approved" / "vfx"
    ]:
        t.mkdir(parents=True, exist_ok=True)
        png_diff = t / "savage_primal_skills_vfx_atlas.png"
        png_norm = t / "savage_primal_skills_vfx_atlas_normal.png"
        cv2.imwrite(str(png_diff), diffuse)
        cv2.imwrite(str(png_norm), normal)
        with open(t / "savage_primal_skills_vfx_atlas.json", "w", encoding="utf-8") as f:
            json.dump(manifest, f, indent=2, ensure_ascii=False)
        if compress_png_to_astc is not None:
            compress_png_to_astc(png_diff, t / "savage_primal_skills_vfx_atlas.astc", 4, 4)
            compress_png_to_astc(png_norm, t / "savage_primal_skills_vfx_atlas_normal.astc", 4, 4)

    print(f"[OK] Successfully generated Power-of-Two 2048x2048 Atlas ({len(skills_registry)} sequences).")
    return manifest


if __name__ == "__main__":
    produce_all_skills_atlas()
