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/skill/DragonPathFlyPresenter.ts

475 lines
16 KiB

import {
director,
Director,
game,
Node,
Rect,
Size,
Sprite,
SpriteFrame,
UIOpacity,
UITransform,
Vec3,
} from 'cc';
import { setNodeWorldPos } from './SkillTransformDriver';
/** 龙字处向上盘旋上升 */
const RISE_DURATION = 0.44;
/** 盘旋移向目标上方 */
const TRANSIT_DURATION = 0.48;
/** 俯冲砸敌 */
const STRIKE_DURATION = 0.18;
/** 相对龙字向上盘旋净高度(像素) */
const RISE_HEIGHT_PX = 180;
const HOVER_ABOVE_TARGET_PX = 100;
const RISE_LAPS = 1.1;
/** 略减圈数,降低极限曲率 */
const TRANSIT_LAPS = 0.85;
const RISE_RADIUS_START = 14;
const RISE_RADIUS_END = 46;
const TRANSIT_RADIUS = 52;
const LANE_SPREAD_PX = 44;
const HISTORY_MAX = 320;
const SLICE_SPACING = 6.5;
const SLICE_COUNT = 60;
/** 片宽相对间距放大,急弯露缝减小 */
const SLICE_WIDTH_OVERLAP = 1.3;
const TANGENT_DELTA = 6;
const MIN_HEAD_STEP_PX = 0.25;
/** 片角平滑:约 18 帧追到目标(60fps) */
const ANGLE_SMOOTH = 12;
/** 单帧最大转角(度),急弯顺过来而非硬折 */
const ANGLE_MAX_DEG_PER_SEC = 540;
/** 俯冲贝塞尔沿出口切线伸出长度 */
const STRIKE_TANGENT_CTRL_PX = 70;
const _headPos = new Vec3();
const _posA = new Vec3();
const _posB = new Vec3();
const _hoverCenter = new Vec3();
const _riseEnd = new Vec3();
const _transitCenter = new Vec3();
const _strikeCtrl = new Vec3();
export type DragonFlyComplete = (impactWorld: Readonly<Vec3>) => void;
/**
* 墨龙三阶段:龙字向上盘旋上升 → 移向目标上方盘旋 → 俯冲攻击。
* 阶段切线 C1 衔接 + 片角平滑,避免转向卡顿与切片断裂。
*/
export class DragonPathFlyPresenter {
private readonly _root: Node;
private readonly _slices: Node[] = [];
private readonly _sliceAngles: number[] = [];
private readonly _history: Vec3[] = [];
private readonly _originWorld = new Vec3();
private readonly _targetWorld = new Vec3();
private readonly _strikeFrom = new Vec3();
private readonly _strikeTo = new Vec3();
/** 阶段出口切线(单位向量),供下一阶段 C1 衔接 */
private readonly _exitDir = new Vec3(1, 0, 0);
private _alive = true;
private _phase: 'rise' | 'transit' | 'strike' = 'rise';
private _elapsed = 0;
private _riseStartAngle = 0;
private _transitStartAngle = 0;
private _laneOffsetX = 0;
private _resolveTargetWorld: (() => Vec3 | null) | null = null;
private _onComplete: DragonFlyComplete | null = null;
private constructor(root: Node, slices: Node[]) {
this._root = root;
this._slices.push(...slices);
for (let i = 0; i < slices.length; i++) {
this._sliceAngles.push(slices[i].angle);
}
}
static start(opts: {
flyLayer: Node;
textureTemplate: Node;
originWorld: Readonly<Vec3>;
targetWorld: Readonly<Vec3>;
laneIndex?: number;
/** 俯冲阶段每帧刷新落点(追踪移动中的敌人) */
resolveTargetWorld?: () => Vec3 | null;
onComplete: DragonFlyComplete;
}): DragonPathFlyPresenter | null {
const layer = opts.flyLayer;
const template = opts.textureTemplate;
if (!layer?.isValid || !template?.isValid) {
opts.onComplete(opts.targetWorld);
return null;
}
const srcSprite = template.getComponent(Sprite);
const srcFrame = srcSprite?.spriteFrame;
if (!srcFrame?.texture) {
opts.onComplete(opts.targetWorld);
return null;
}
layer.active = true;
const root = new Node('dragon_fly');
root.parent = layer;
root.layer = layer.layer;
root.addComponent(UITransform);
const slices = _createDragonSlices(root, srcFrame, template.layer);
if (slices.length === 0) {
root.destroy();
opts.onComplete(opts.targetWorld);
return null;
}
const presenter = new DragonPathFlyPresenter(root, slices);
presenter._onComplete = opts.onComplete;
presenter._originWorld.set(opts.originWorld);
presenter._targetWorld.set(opts.targetWorld);
presenter._laneOffsetX = (opts.laneIndex ?? 0) * LANE_SPREAD_PX;
presenter._resolveTargetWorld = opts.resolveTargetWorld ?? null;
presenter._setupPath(opts.originWorld, opts.targetWorld);
presenter._pushHead(opts.originWorld);
presenter._updateBody(1 / 60);
_registerFly(presenter);
return presenter;
}
dispose(): void {
if (!this._alive) return;
this._alive = false;
_unregisterFly(this);
if (this._root?.isValid) this._root.destroy();
this._slices.length = 0;
this._sliceAngles.length = 0;
this._history.length = 0;
this._onComplete = null;
}
private _setupPath(origin: Readonly<Vec3>, target: Readonly<Vec3>): void {
this._syncHoverFromTarget(target);
this._riseStartAngle = -Math.PI * 0.5;
// 初始盘旋角占位;rise→transit 时按出口切线重算
const dx = _hoverCenter.x - origin.x;
const dy = _hoverCenter.y - origin.y;
this._transitStartAngle = Math.atan2(dy, dx);
this._exitDir.set(0, 1, 0);
}
private _syncTargetWorld(fallback?: Readonly<Vec3>): boolean {
const live = this._resolveTargetWorld?.();
if (live) {
this._targetWorld.set(live);
return true;
}
if (fallback) {
this._targetWorld.set(fallback);
return true;
}
return false;
}
private _syncHoverFromTarget(fallback?: Readonly<Vec3>): void {
this._syncTargetWorld(fallback);
_hoverCenter.set(
this._targetWorld.x + this._laneOffsetX,
this._targetWorld.y + HOVER_ABOVE_TARGET_PX,
this._targetWorld.z,
);
}
/** 沿出口切线布置俯冲控制点,避免砸落瞬间掉头 */
private _refreshStrikeCtrl(): void {
const tx = this._exitDir.x;
const ty = this._exitDir.y;
_strikeCtrl.set(
this._strikeFrom.x + tx * STRIKE_TANGENT_CTRL_PX
+ (this._strikeTo.x - this._strikeFrom.x) * 0.25,
this._strikeFrom.y + ty * STRIKE_TANGENT_CTRL_PX
+ (this._strikeTo.y - this._strikeFrom.y) * 0.15,
0,
);
}
/** 从历史头两点刷新出口切线 */
private _captureExitDir(): void {
if (this._history.length < 2) return;
const a = this._history[0];
const b = this._history[1];
const dx = a.x - b.x;
const dy = a.y - b.y;
const len = Math.sqrt(dx * dx + dy * dy);
if (len < 1e-4) return;
this._exitDir.set(dx / len, dy / len, 0);
}
/** 每帧推进飞龙;由模块级 afterUpdate 调用(避免 Set/私有方法在抖音压缩后丢引用) */
stepFly(dt: number): void {
if (!this._alive || !this._root?.isValid) {
this.dispose();
return;
}
if (this._phase === 'rise') {
this._elapsed += dt;
const rawT = Math.min(1, this._elapsed / RISE_DURATION);
const ease = 1 - Math.pow(1 - rawT, 2.2);
const angle = this._riseStartAngle + ease * Math.PI * 2 * RISE_LAPS;
const radius = RISE_RADIUS_START + (RISE_RADIUS_END - RISE_RADIUS_START) * ease;
const origin = this._originWorld;
const riseY = origin.y + ease * RISE_HEIGHT_PX;
_headPos.set(
origin.x + Math.cos(angle) * radius + this._laneOffsetX * ease * 0.2,
riseY + Math.sin(angle) * radius * 0.15,
origin.z,
);
this._pushHead(_headPos);
this._updateBody(dt);
if (rawT >= 1) {
this._captureExitDir();
_riseEnd.set(_headPos);
_riseEnd.y = Math.max(_riseEnd.y, origin.y + RISE_HEIGHT_PX);
// 半径从 0 长大:初始扩张方向对齐出口切线,位置/切线连续
this._transitStartAngle = Math.atan2(this._exitDir.y, this._exitDir.x);
this._phase = 'transit';
this._elapsed = 0;
}
return;
}
if (this._phase === 'transit') {
this._elapsed += dt;
const rawT = Math.min(1, this._elapsed / TRANSIT_DURATION);
const ease = rawT * rawT * (3 - 2 * rawT);
this._syncHoverFromTarget();
_transitCenter.set(
_riseEnd.x + (_hoverCenter.x - _riseEnd.x) * ease,
_riseEnd.y + (_hoverCenter.y - _riseEnd.y) * ease,
_hoverCenter.z,
);
// 半径从 0 平滑长大,消除 rise 结束瞬间外甩
const radiusGrow = ease * ease * (3 - 2 * ease);
const angle = this._transitStartAngle + ease * Math.PI * 2 * TRANSIT_LAPS;
const radius = TRANSIT_RADIUS * radiusGrow * (0.85 + 0.15 * Math.sin(ease * Math.PI));
_headPos.set(
_transitCenter.x + Math.cos(angle) * radius,
_transitCenter.y + Math.sin(angle) * radius * 0.65,
_transitCenter.z,
);
this._pushHead(_headPos);
this._updateBody(dt);
if (rawT >= 1) {
this._captureExitDir();
this._strikeFrom.set(_headPos);
this._syncTargetWorld();
this._strikeTo.set(this._targetWorld);
this._refreshStrikeCtrl();
this._phase = 'strike';
this._elapsed = 0;
}
return;
}
this._elapsed += dt;
const rawT = Math.min(1, this._elapsed / STRIKE_DURATION);
const t = rawT * rawT * rawT;
this._syncTargetWorld();
this._strikeTo.set(this._targetWorld);
// 追踪目标时保持出口切线控制点,避免每帧乱跳
this._refreshStrikeCtrl();
this._sampleBezier(this._strikeFrom, _strikeCtrl, this._strikeTo, t, _headPos);
this._pushHead(_headPos);
this._updateBody(dt);
if (rawT >= 1) {
const cb = this._onComplete;
const impact = this._targetWorld.clone();
this.dispose();
cb?.(impact);
}
}
private _updateBody(dt: number): void {
const sliceCount = this._slices.length;
const maxStep = ANGLE_MAX_DEG_PER_SEC * Math.max(dt, 1 / 120);
const smoothK = 1 - Math.exp(-ANGLE_SMOOTH * Math.max(dt, 1 / 120));
for (let i = 0; i < sliceCount; i++) {
const slice = this._slices[i];
if (!slice?.isValid) continue;
const dist = i * SLICE_SPACING;
if (!this._sampleHistory(dist, _posA)) continue;
setNodeWorldPos(slice, _posA);
const t = sliceCount > 1 ? i / (sliceCount - 1) : 0;
const scale = 1.14 - t * 0.28;
slice.setScale(scale, scale, 1);
const uo = slice.getComponent(UIOpacity);
if (uo) uo.opacity = Math.round(255 - t * 180);
let targetDeg = this._sliceAngles[i] ?? slice.angle;
if (this._sampleHistory(dist + TANGENT_DELTA, _posB)) {
const dx = _posA.x - _posB.x;
const dy = _posA.y - _posB.y;
if (dx * dx + dy * dy > 1e-6) {
targetDeg = Math.atan2(dy, dx) * 180 / Math.PI;
}
}
const cur = this._sliceAngles[i] ?? slice.angle;
let delta = _shortestAngleDelta(cur, targetDeg);
delta = Math.max(-maxStep, Math.min(maxStep, delta * smoothK));
const next = cur + delta;
this._sliceAngles[i] = next;
slice.angle = next;
}
}
private _sampleBezier(
from: Readonly<Vec3>,
ctrl: Readonly<Vec3>,
to: Readonly<Vec3>,
t: number,
out: Vec3,
): void {
const u = 1 - t;
const uu = u * u;
const tt = t * t;
const ut2 = 2 * u * t;
out.x = uu * from.x + ut2 * ctrl.x + tt * to.x;
out.y = uu * from.y + ut2 * ctrl.y + tt * to.y;
out.z = uu * from.z + ut2 * ctrl.z + tt * to.z;
}
private _pushHead(pos: Readonly<Vec3>): void {
if (this._history.length > 0) {
const last = this._history[0];
const dx = pos.x - last.x;
const dy = pos.y - last.y;
if (dx * dx + dy * dy < MIN_HEAD_STEP_PX * MIN_HEAD_STEP_PX) return;
}
this._history.unshift(pos.clone());
if (this._history.length > HISTORY_MAX) {
this._history.length = HISTORY_MAX;
}
}
private _sampleHistory(dist: number, out: Vec3): boolean {
const hist = this._history;
if (hist.length === 0) return false;
if (dist <= 0) {
out.set(hist[0]);
return true;
}
let acc = 0;
for (let i = 0; i < hist.length - 1; i++) {
const a = hist[i];
const b = hist[i + 1];
const dx = b.x - a.x;
const dy = b.y - a.y;
const segLen = Math.sqrt(dx * dx + dy * dy);
if (acc + segLen >= dist) {
const local = segLen > 1e-4 ? (dist - acc) / segLen : 0;
out.x = a.x + dx * local;
out.y = a.y + dy * local;
out.z = a.z;
return true;
}
acc += segLen;
}
out.set(hist[hist.length - 1]);
return true;
}
}
/** 与 SkillCollisionHit / SkillTransformDriver 一致:Array + 具名 afterUpdate,兼容抖音打包 */
const _activeFlies: DragonPathFlyPresenter[] = [];
let _flyHooked = false;
function _dragonFlyAfterUpdate(): void {
if (_activeFlies.length === 0) return;
const battle = gg.game?.CurentBattle as { TimeScale?: number } | null | undefined;
const scale = battle?.TimeScale ?? 1;
if (scale <= 0) return;
const dt = (game.deltaTime || 1 / 60) * scale;
for (let i = _activeFlies.length - 1; i >= 0; i--) {
_activeFlies[i].stepFly(dt);
}
}
function _ensureFlyHook(): void {
if (_flyHooked) return;
_flyHooked = true;
director.on(Director.EVENT_AFTER_UPDATE, _dragonFlyAfterUpdate);
}
function _registerFly(p: DragonPathFlyPresenter): void {
_ensureFlyHook();
if (_activeFlies.indexOf(p) < 0) {
_activeFlies.push(p);
}
}
function _unregisterFly(p: DragonPathFlyPresenter): void {
const idx = _activeFlies.indexOf(p);
if (idx >= 0) _activeFlies.splice(idx, 1);
}
/** 最短角差(度),结果落在 (-180, 180] */
function _shortestAngleDelta(fromDeg: number, toDeg: number): number {
let d = (toDeg - fromDeg) % 360;
if (d > 180) d -= 360;
if (d <= -180) d += 360;
return d;
}
function _createDragonSlices(
root: Node,
srcFrame: SpriteFrame,
layer: number,
): Node[] {
const tex = srcFrame.texture;
if (!tex) return [];
const rect = srcFrame.rect;
const origW = srcFrame.originalSize.width || rect.width;
const origH = srcFrame.originalSize.height || rect.height;
const texSliceW = origW / SLICE_COUNT;
// 显示宽度 ≥ 间距×重叠,盖住急弯缝隙
const displayW = Math.max(texSliceW, SLICE_SPACING * SLICE_WIDTH_OVERLAP);
const slices: Node[] = [];
for (let i = SLICE_COUNT - 1; i >= 0; i--) {
const node = new Node(`dragon_slice_${i}`);
node.parent = root;
node.layer = layer;
const ut = node.addComponent(UITransform);
ut.setContentSize(displayW, origH);
ut.setAnchorPoint(0.5, 0.5);
const sp = node.addComponent(Sprite);
const sf = new SpriteFrame();
sf.texture = tex;
const x = rect.x + rect.width - (i + 1) * (rect.width / SLICE_COUNT);
sf.rect = new Rect(x, rect.y, rect.width / SLICE_COUNT, rect.height);
sf.originalSize = new Size(displayW, origH);
sf.offset = srcFrame.offset;
sf.rotated = srcFrame.rotated;
sp.spriteFrame = sf;
sp.sizeMode = Sprite.SizeMode.CUSTOM;
const uo = node.addComponent(UIOpacity);
const t = (SLICE_COUNT - 1 - i) / Math.max(1, SLICE_COUNT - 1);
uo.opacity = Math.round(255 - t * 180);
slices.push(node);
}
slices.reverse();
return slices;
}