import { sp } from 'cc'; import { createMembershipSet, membershipAdd, membershipFromArray, membershipDedupeArray, type MembershipSet, type ReadonlyMembershipSet } from '../../../mx/tools/MembershipSet'; import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch'; /** 字形 Spine 待机皮(未激活 / 默认) */ export const GLYPH_SKIN_IDLE = '普通字'; /** @deprecated 字形皮首位为阵营;保留常量避免旧引用崩 */ export const GLYPH_SKIN_QUALITY = 1; /** 己方阵营皮肤首位 */ export const GLYPH_SKIN_CAMP_ALLY = 1; /** 敌方阵营皮肤首位 */ export const GLYPH_SKIN_CAMP_ENEMY = 2; export const GLYPH_ACTIVE_SKIN_BUCKETS = [3, 4, 5] as const; export function spineSkinName(raw: unknown): string { if (typeof raw === 'string') return raw; if (raw && typeof (raw as { str?: string }).str === 'string') return (raw as { str: string }).str; return ''; } /** 查皮肤:runtime + skeletonJson 双路径(资源纹理未就绪时 runtime 可能为空) */ export function glyphSpineHasSkin(anim: sp.Skeleton, skinName: string): boolean { const sd = anim.skeletonData; if (!sd || !skinName) return false; ensureSpineSkeletonDataRuntime(sd); try { const runtime = sd.getRuntimeData?.(true); if (runtime?.findSkin?.(skinName)) return true; const skins = (runtime as { skins?: ReadonlyArray<{ name?: unknown }> } | null)?.skins; if (skins?.some((s) => spineSkinName(s?.name) === skinName)) return true; } catch { // ignore } const raw = sd as unknown as { skeletonJson?: { skins?: unknown }; _skeletonJson?: { skins?: unknown } }; const skins = raw.skeletonJson?.skins ?? raw._skeletonJson?.skins; if (Array.isArray(skins)) { return skins.some((s) => spineSkinName((s as { name?: unknown })?.name) === skinName); } if (skins && typeof skins === 'object') { return Object.prototype.hasOwnProperty.call(skins, skinName); } return false; } export function withSkinNameVariants(name: string): string[] { if (!name) return []; const alt = name.includes('_') ? name.replace(/_/g, '-') : name.replace(/-/g, '_'); return alt !== name ? [name, alt] : [name]; } /** 武器 Spine 候选皮肤名(配表名 + _/- 变体) */ export function weaponSpineSkinCandidates(tableSkin: string): string[] { return withSkinNameVariants(spineSkinName(tableSkin).trim()); } /** * 按 Weapon.skin 切武器 Spine 皮肤;成功返回实际皮肤名。 * @param anim 目标 Skeleton * @param tableSkin Weapon.skin * @param logTag 告警前缀 */ export function applyWeaponSpineSkin(anim: sp.Skeleton, tableSkin: string, logTag = '[WeaponSpineSkin]'): string { if (!anim?.isValid || !anim.skeletonData) return ''; ensureSpineSkeletonDataRuntime(anim.skeletonData); const candidates = weaponSpineSkinCandidates(tableSkin); if (!candidates.length) { console.log(logTag, 'Weapon.skin 为空'); return ''; } for (let i = 0; i < candidates.length; i++) { const name = candidates[i]; if (!glyphSpineHasSkin(anim, name)) continue; try { anim.setSkin(name); anim.setSlotsToSetupPose(); return name; } catch (e) { console.log(logTag, 'setSkin 失败', name, String(e)); } } const defaultFromComp = spineSkinName((anim as unknown as { defaultSkin?: unknown }).defaultSkin).trim(); if (defaultFromComp && defaultFromComp !== 'default' && glyphSpineHasSkin(anim, defaultFromComp)) { try { anim.setSkin(defaultFromComp); anim.setSlotsToSetupPose(); console.log(logTag, 'Weapon.skin 回退 defaultSkin', tableSkin, '->', defaultFromComp); return defaultFromComp; } catch (e) { console.log(logTag, 'defaultSkin setSkin 失败', defaultFromComp, String(e)); } } console.log(logTag, '无可用武器皮肤', tableSkin, candidates); return ''; } /** 激活皮档位:1–3 级共用 *_3,4 级 *_4,5 级 *_5 */ export function bucketActiveGlyphSkinLevel(level: number): number { const lv = Math.max(1, Math.floor(level)); if (lv <= 3) return 3; if (lv === 4) return 4; return 5; } /** * 字形激活皮名:`皮肤字{camp}_{bucket}`。 * @param level 合成等级 * @param camp 阵营:1 己方 / 2 敌方 */ export function resolveActiveGlyphSkin(level: number, camp = GLYPH_SKIN_CAMP_ALLY): string { const bucket = bucketActiveGlyphSkinLevel(level); const c = camp === GLYPH_SKIN_CAMP_ENEMY ? GLYPH_SKIN_CAMP_ENEMY : GLYPH_SKIN_CAMP_ALLY; return `皮肤字${c}_${bucket}`; } /** 按候选顺序 setSkin;preferred 为首选,extra 追加在 普通字 等兜底之前 */ export function applyGlyphSpineSkin( anim: sp.Skeleton, preferred: string, extraCandidates: readonly string[] = [], ): boolean { if (!anim?.isValid) return false; const sd = anim.skeletonData; if (!sd?.isValid) return false; ensureSpineSkeletonDataRuntime(sd); const defaultFromComp = spineSkinName((anim as unknown as { defaultSkin?: unknown }).defaultSkin); const ordered: string[] = []; const push = (name: string) => { if (!name) return; for (const v of withSkinNameVariants(name)) { if (!ordered.includes(v)) ordered.push(v); } }; push(preferred); for (const name of extraCandidates) push(name); if (defaultFromComp) push(defaultFromComp); if (preferred !== GLYPH_SKIN_IDLE) push(GLYPH_SKIN_IDLE); if (preferred !== GLYPH_SKIN_IDLE) { const m = /^皮肤字(\d+)_/.exec(preferred); const camp = m ? Number(m[1]) : GLYPH_SKIN_CAMP_ALLY; for (const b of GLYPH_ACTIVE_SKIN_BUCKETS) { push(`皮肤字${camp}_${b}`); } } const tried = createMembershipSet(); for (const name of ordered) { if (tried.has(name)) continue; membershipAdd(tried, name); if (!glyphSpineHasSkin(anim, name)) continue; try { anim.setSkin(name); anim.setSlotsToSetupPose(); return true; } catch { // 真机偶发 setSkin 失败,继续尝试 } } for (const name of ordered) { if (tried.has(name)) continue; membershipAdd(tried, name); try { anim.setSkin(name); anim.setSlotsToSetupPose(); return true; } catch { // ignore } } return false; }