import { Node, Prefab, Tween, Vec3, instantiate, tween } from 'cc'; import { GBundle } from '../../game/ConfigRes'; import { HurtScore } from '../../ui/BattleGame/HurtScore'; import { DeviceProfile } from './DeviceProfile'; /** * 伤害飘字:默认 atkMonsterScore;暴击 critHurtScore(数值)+ critHurtScoreZi(字); * base_attr=6(额外伤害倍数)用 rateDamageScore。 * 唐僧城墙扣血可指定 addHPScore。 * 击退等状态字走 MonsterStatusText(battleText),不在此处。 */ export class DamageTextSpawner { /** 怪物伤害飘字相对预制体默认尺寸的显示缩放 */ private static readonly DISPLAY_SCALE = 0.72; /** 暴击字出生点相对怪物水平偏移(随机左/右) */ private static readonly CRIT_ZI_SIDE_DRIFT = 30; private static _activeBurst = 0; /** 离场时自增,作废未完成的爆发回扣 */ private static _burstGen = 0; /** 对局结束:清爆发计数,忽略未到期的回扣 */ static resetBurst(): void { DamageTextSpawner._burstGen++; DamageTextSpawner._activeBurst = 0; } spawn( amount: number, worldPos: Vec3, parent: Node, isCrit: boolean, extraMul: boolean, prefabOverride?: string, ): void { if (!DeviceProfile.enableDamageText) return; if (!parent?.isValid || amount <= 0) return; if (DamageTextSpawner._activeBurst >= DeviceProfile.damageTextBurstMax) return; const useCritPair = !prefabOverride && !extraMul && isCrit; const prefabName = prefabOverride || (extraMul ? 'rateDamageScore' : isCrit ? 'critHurtScore' : 'atkMonsterScore'); this._spawnNumber(amount, worldPos, parent, isCrit, extraMul, prefabName, !!prefabOverride); if (useCritPair) { // this._spawnCritZi(worldPos, parent); } } /** 数值飘字:critHurtScore / atkMonsterScore 等,贝塞尔下落 */ private _spawnNumber( amount: number, worldPos: Vec3, parent: Node, isCrit: boolean, extraMul: boolean, prefabName: string, useFullScale: boolean, ): void { let node: Node | null = null; try { node = gg.res.getNode('prefab/数字/', prefabName, GBundle.BattleGame); } catch { return; } if (!node?.isValid) return; DamageTextSpawner._activeBurst++; const burstGen = DamageTextSpawner._burstGen; node.setParent(parent); node.active = true; node.opacity = 255; const s = useFullScale ? 1 : DamageTextSpawner.DISPLAY_SCALE; node.setScale(s, s, 1); Tween.stopAllByTarget(node); node.worldPosition = worldPos; const hurt = node.getComponent(HurtScore); if (hurt) { // 暴击字由 critHurtScoreZi 单独飘,数值预制体不再开「暴」子节点 hurt.showHurtNum(amount, isCrit, extraMul, { hideBaoBadge: isCrit && !extraMul }); } node.setSiblingIndex(parent.children.length - 1); setTimeout(() => { if (burstGen !== DamageTextSpawner._burstGen) return; DamageTextSpawner._activeBurst = Math.max(0, DamageTextSpawner._activeBurst - 1); }, 900); } /** * 暴击字:头顶上方;动效与原先相同(竖直上飘),仅出生点随机左右偏 ±100。 */ private _spawnCritZi(worldPos: Vec3, parent: Node): void { let node: Node | null = null; try { node = gg.res.getNode('prefab/数字/', 'critHurtScoreZi', GBundle.BattleGame); } catch { return; } if (!node?.isValid) return; node.setParent(parent); node.active = true; node.opacity = 255; const s = DamageTextSpawner.DISPLAY_SCALE; node.setScale(s, s, 1); Tween.stopAllByTarget(node); const side = Math.random() < 0.5 ? -1 : 1; const ziPos = worldPos.clone(); ziPos.x += side * DamageTextSpawner.CRIT_ZI_SIDE_DRIFT; ziPos.y += 50; node.setWorldPosition(ziPos); this._playGitNormalFloat(node, () => { if (node?.isValid) gg.res.putNode(node); }); node.setSiblingIndex(parent.children.length - 1); } /** * git HurtScore 普通伤害飘字:爆发弹出(backOut)→ 回弹 → 短暂停顿 → 上飘淡出。 */ private _playGitNormalFloat(node: Node, onComplete: () => void): void { const startPos = node.position.clone(); const baseScale = node.scale.clone(); node.setPosition(startPos); tween(node) .set({ opacity: 255, scale: new Vec3(baseScale.x * 0.5, baseScale.y * 0.5, 1) }) .to(0.08, { scale: new Vec3(baseScale.x * 1.35, baseScale.y * 1.35, 1) }, { easing: 'backOut' }) .to(0.1, { scale: new Vec3(baseScale.x, baseScale.y, 1) }, { easing: 'quadOut' }) .delay(0.5) .to(0.35, { opacity: 0, position: new Vec3(startPos.x, startPos.y + 50, 0), }, { easing: 'sineOut' }) .call(onComplete) .start(); } /** 秒杀 / 斩杀飘字(killEff 预制体:子节点「秒杀」「斩杀」) */ spawnKillEffect( worldPos: Vec3, parent: Node, kind: 'instant_kill' | 'execute', ): void { if (!DeviceProfile.enableDamageText) return; if (!parent?.isValid) return; const node = this._borrowKillEffNode(); if (!node?.isValid) return; const badgeName = kind === 'instant_kill' ? '秒杀' : '斩杀'; const miaoSha = node.getChildByName('秒杀'); const zhanSha = node.getChildByName('斩杀'); if (miaoSha) miaoSha.active = badgeName === '秒杀'; if (zhanSha) zhanSha.active = badgeName === '斩杀'; node.setParent(parent); node.active = true; node.opacity = 255; const s = DamageTextSpawner.DISPLAY_SCALE; node.setScale(s, s, 1); Tween.stopAllByTarget(node); node.setWorldPosition(worldPos); const startPos = node.position.clone(); const baseScale = node.scale.clone(); tween(node) .set({ opacity: 255, scale: new Vec3(baseScale.x * 0.5, baseScale.y * 0.5, 1) }) .to(0.08, { scale: new Vec3(baseScale.x * 1.35, baseScale.y * 1.35, 1) }, { easing: 'backOut' }) .to(0.1, { scale: new Vec3(baseScale.x, baseScale.y, 1) }, { easing: 'quadOut' }) .delay(0.12) .to(0.35, { opacity: 0, position: new Vec3(startPos.x, startPos.y + 50, 0), }, { easing: 'sineOut' }) .call(() => { gg.res.putNode(node); }) .start(); node.setSiblingIndex(parent.children.length - 1); } private _borrowKillEffNode(): Node | null { const path = 'prefab/'; const name = 'killEff'; const bundle = GBundle.BattleGame; let node: Node | null = null; try { node = gg.res.getNode(path, name, bundle); } catch { node = null; } if (node?.isValid) return node; const prefab = gg.res.getRes(name, path, bundle, Prefab) as Prefab | null; if (!prefab?.isValid) return null; node = instantiate(prefab); node['resKey'] = bundle + path + name; return node; } /** 由 damageType 识别秒杀 / 斩杀飘字种类 */ static resolveKillFloaterKind(damageType?: string): 'instant_kill' | 'execute' | null { if (!damageType) return null; if (damageType.includes('instant_kill')) return 'instant_kill'; if (damageType === 'execute' || damageType.endsWith('_execute')) return 'execute'; return null; } }