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.
 
 
 
 
AdLike2048/assets/script/battle/view/GlyphSpineSkin.ts

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;
}