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.
215 lines
7.8 KiB
215 lines
7.8 KiB
|
3 weeks ago
|
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;
|
||
|
|
}
|
||
|
|
}
|