You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
176 lines
6.5 KiB
176 lines
6.5 KiB
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<string>();
|
|
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;
|
|
}
|
|
|