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.
295 lines
11 KiB
295 lines
11 KiB
import { Color, find, Graphics, Node, Sprite, Tween, tween, UIOpacity, UITransform, Vec3 } from 'cc';
|
|
|
|
/** 千里眼攻击范围扩散插值时长(秒) */
|
|
const ATTACK_RANGE_BUFF_TWEEN_SEC = 0.5;
|
|
/** 千里眼扩散完成后停留再隐藏(秒) */
|
|
const ATTACK_RANGE_BUFF_HOLD_SEC = 0.5;
|
|
|
|
/** 拖拽/选中时 ui_range 攻击范围预显示(半径=配表像素) */
|
|
export class AttackRangePreviewPresenter {
|
|
/** ui_range 节点 */
|
|
private _root: Node | null = null;
|
|
/** BattleGame 根,圈始终挂在这下面 */
|
|
private _host: Node | null = null;
|
|
/** 世界坐标 scratch */
|
|
private _worldScratch = new Vec3();
|
|
/** 圆心本地坐标 */
|
|
private _centerLocal = new Vec3();
|
|
/** 格 A 本地坐标 */
|
|
private _cellALocal = new Vec3();
|
|
/** 格 B 本地坐标 */
|
|
private _cellBLocal = new Vec3();
|
|
/** Graphics 描边 */
|
|
private _stroke = new Color(207, 123, 48, 255);
|
|
/** Graphics 填充 */
|
|
private _fill = new Color(248, 251, 204, 90);
|
|
/** 插值 tween 代数,用于取消过期回调 */
|
|
private _radiusTweenGen = 0;
|
|
/** 插值进度 0~1 */
|
|
private _radiusTweenScratch = { t: 0 };
|
|
/** 当前插值参数缓存 */
|
|
private _radiusTweenArgs: {
|
|
centerWorld: Readonly<Vec3>;
|
|
fromRadiusPx: number;
|
|
toRadiusPx: number;
|
|
tableCellPx: number;
|
|
cellWorldA: Readonly<Vec3> | null;
|
|
cellWorldB: Readonly<Vec3> | null;
|
|
host: Node | null;
|
|
} | null = null;
|
|
|
|
/**
|
|
* @param root 预制体 ui_range;无效时在 host 下查找或重建
|
|
* @param host BattleGame 根节点,圈必须挂在这里才能盖在地图上
|
|
*/
|
|
bind(root: Node | null, host: Node | null): void {
|
|
this._host = host?.isValid ? host : null;
|
|
let n = root?.isValid ? root : null;
|
|
if (!n && host?.isValid) {
|
|
n = find('ui_range', host);
|
|
}
|
|
if (!n?.isValid && host?.isValid) {
|
|
console.log('[AttackRangePreview] ui_range 已丢失,重建节点');
|
|
n = this._createRangeNode(host);
|
|
}
|
|
if (n?.isValid && host?.isValid && n.parent !== host) {
|
|
n.parent = host;
|
|
}
|
|
if (n?.isValid && host?.isValid) {
|
|
n.layer = host.layer;
|
|
n.setSiblingIndex(host.children.length - 1);
|
|
}
|
|
this._root = n?.isValid ? n : null;
|
|
this._removeRootGraphics();
|
|
if (!this._root) {
|
|
console.log('[AttackRangePreview] bind 失败,没有可用的范围节点');
|
|
}
|
|
this.hide();
|
|
}
|
|
|
|
/** 解除节点绑定 */
|
|
unbind(): void {
|
|
this.hide();
|
|
this._root = null;
|
|
this._host = null;
|
|
}
|
|
|
|
/**
|
|
* 在目标世界坐标显示攻击范围圆。
|
|
* @param centerWorld 圆心世界坐标
|
|
* @param radiusPx 配表射程(像素)
|
|
* @param tableCellPx MapManager.mapGridSize
|
|
* @param cellWorldA 相邻格世界坐标,可空
|
|
* @param cellWorldB 另一格世界坐标,可空
|
|
* @param host BattleGame 根;热更后 bind 未再跑时仍能挂回节点
|
|
*/
|
|
showAtWorldCenter(centerWorld: Readonly<Vec3>, radiusPx: number, tableCellPx: number, cellWorldA?: Readonly<Vec3> | null, cellWorldB?: Readonly<Vec3> | null, host?: Node | null): void {
|
|
if (host?.isValid) this._host = host;
|
|
this._ensureRoot();
|
|
if (!this._root?.isValid || !(radiusPx > 0)) {
|
|
console.log('[AttackRangePreview] 无法显示', !!this._root?.isValid, radiusPx);
|
|
this.hide();
|
|
return;
|
|
}
|
|
|
|
const parent = this._root.parent;
|
|
const parentUt = parent?.getComponent(UITransform);
|
|
if (!parentUt) {
|
|
console.log('[AttackRangePreview] 父节点缺少 UITransform');
|
|
return;
|
|
}
|
|
|
|
parentUt.convertToNodeSpaceAR(this._worldScratch.set(centerWorld.x, centerWorld.y, centerWorld.z), this._centerLocal);
|
|
this._root.setPosition(this._centerLocal);
|
|
this._root.setScale(1, 1, 1);
|
|
this._root.setSiblingIndex(parent.children.length - 1);
|
|
|
|
let diameter = radiusPx * 2;
|
|
if (tableCellPx > 0 && cellWorldA && cellWorldB) {
|
|
parentUt.convertToNodeSpaceAR(this._worldScratch.set(cellWorldA.x, cellWorldA.y, cellWorldA.z), this._cellALocal);
|
|
parentUt.convertToNodeSpaceAR(this._worldScratch.set(cellWorldB.x, cellWorldB.y, cellWorldB.z), this._cellBLocal);
|
|
const cellLocal = Vec3.distance(this._cellALocal, this._cellBLocal);
|
|
if (cellLocal > 0) {
|
|
diameter = radiusPx * 2 * (cellLocal / tableCellPx);
|
|
}
|
|
}
|
|
|
|
const ut = this._root.getComponent(UITransform) ?? this._root.addComponent(UITransform);
|
|
ut.setAnchorPoint(0.5, 0.5);
|
|
const sprite = this._root.getComponent(Sprite);
|
|
if (sprite) {
|
|
sprite.sizeMode = Sprite.SizeMode.CUSTOM;
|
|
sprite.trim = true;
|
|
sprite.enabled = !!sprite.spriteFrame;
|
|
}
|
|
const opacity = this._root.getComponent(UIOpacity) ?? this._root.addComponent(UIOpacity);
|
|
opacity.opacity = 255;
|
|
|
|
this._root.active = true;
|
|
ut.setContentSize(diameter, diameter);
|
|
sprite?.markForUpdateRenderData();
|
|
this._strokeRing(diameter * 0.5, !sprite?.spriteFrame);
|
|
}
|
|
|
|
/**
|
|
* 千里眼施放:0.5s 内从当前射程插值到加成后,停留 0.5s 再隐藏。
|
|
* @param centerWorld 圆心世界坐标
|
|
* @param fromRadiusPx 加成前射程(像素)
|
|
* @param toRadiusPx 加成后射程(像素)
|
|
* @param tableCellPx MapManager.mapGridSize
|
|
* @param cellWorldA 相邻格世界坐标,可空
|
|
* @param cellWorldB 另一格世界坐标,可空
|
|
* @param host BattleGame 根
|
|
*/
|
|
animateRadiusAtWorldCenter(
|
|
centerWorld: Readonly<Vec3>,
|
|
fromRadiusPx: number,
|
|
toRadiusPx: number,
|
|
tableCellPx: number,
|
|
cellWorldA?: Readonly<Vec3> | null,
|
|
cellWorldB?: Readonly<Vec3> | null,
|
|
host?: Node | null,
|
|
): void {
|
|
if (!(fromRadiusPx > 0) || !(toRadiusPx > 0)) {
|
|
console.log('[AttackRangePreview] 千里眼范围插值参数非法', fromRadiusPx, toRadiusPx);
|
|
this.hide();
|
|
return;
|
|
}
|
|
this._cancelRadiusTween();
|
|
this._radiusTweenArgs = {
|
|
centerWorld,
|
|
fromRadiusPx,
|
|
toRadiusPx,
|
|
tableCellPx,
|
|
cellWorldA: cellWorldA ?? null,
|
|
cellWorldB: cellWorldB ?? null,
|
|
host: host?.isValid ? host : null,
|
|
};
|
|
const gen = ++this._radiusTweenGen;
|
|
this._radiusTweenScratch.t = 0;
|
|
this.showAtWorldCenter(centerWorld, fromRadiusPx, tableCellPx, cellWorldA, cellWorldB, host);
|
|
tween(this._radiusTweenScratch)
|
|
.to(ATTACK_RANGE_BUFF_TWEEN_SEC, { t: 1 }, { onUpdate: this._onRadiusTweenUpdate.bind(this) })
|
|
.delay(ATTACK_RANGE_BUFF_HOLD_SEC)
|
|
.call(this._onRadiusTweenComplete.bind(this, gen))
|
|
.start();
|
|
}
|
|
|
|
/** 隐藏攻击范围圆 */
|
|
hide(): void {
|
|
this._cancelRadiusTween();
|
|
if (!this._root?.isValid) return;
|
|
const gfxNode = this._root.getChildByName('gfxRing');
|
|
if (gfxNode?.isValid) gfxNode.active = false;
|
|
const g = gfxNode?.getComponent(Graphics);
|
|
if (g) {
|
|
g.clear();
|
|
g.enabled = false;
|
|
}
|
|
this._root.active = false;
|
|
}
|
|
|
|
/**
|
|
* 无贴图时用 Graphics 描圈;有贴图则关掉 Graphics,避免叠两层。
|
|
* @param radiusLocal 父节点本地半径
|
|
* @param useGraphics 是否描圈
|
|
*/
|
|
private _strokeRing(radiusLocal: number, useGraphics: boolean): void {
|
|
let gfxNode = this._root!.getChildByName('gfxRing');
|
|
if (!useGraphics) {
|
|
if (gfxNode?.isValid) gfxNode.active = false;
|
|
return;
|
|
}
|
|
if (!gfxNode?.isValid) {
|
|
gfxNode = new Node('gfxRing');
|
|
gfxNode.layer = this._root!.layer;
|
|
gfxNode.parent = this._root!;
|
|
const ut = gfxNode.addComponent(UITransform);
|
|
ut.setAnchorPoint(0.5, 0.5);
|
|
}
|
|
gfxNode.active = true;
|
|
const g = gfxNode.getComponent(Graphics) ?? gfxNode.addComponent(Graphics);
|
|
g.enabled = true;
|
|
g.clear();
|
|
g.lineWidth = 4;
|
|
g.strokeColor = this._stroke;
|
|
g.fillColor = this._fill;
|
|
g.circle(0, 0, radiusLocal);
|
|
g.fill();
|
|
g.stroke();
|
|
}
|
|
|
|
/**
|
|
* 上一轮误把 ui_range 挂到地图上被一起销毁时,补一个可画圈的节点。
|
|
* @param host BattleGame 根
|
|
*/
|
|
private _createRangeNode(host: Node): Node {
|
|
const n = new Node('ui_range');
|
|
n.layer = host.layer;
|
|
const ut = n.addComponent(UITransform);
|
|
ut.setAnchorPoint(0.5, 0.5);
|
|
n.parent = host;
|
|
return n;
|
|
}
|
|
|
|
/** 插值帧更新:按 t 刷新圆半径 */
|
|
private _onRadiusTweenUpdate(): void {
|
|
const args = this._radiusTweenArgs;
|
|
if (!args) return;
|
|
const radiusPx = args.fromRadiusPx + (args.toRadiusPx - args.fromRadiusPx) * this._radiusTweenScratch.t;
|
|
this.showAtWorldCenter(
|
|
args.centerWorld,
|
|
radiusPx,
|
|
args.tableCellPx,
|
|
args.cellWorldA,
|
|
args.cellWorldB,
|
|
args.host,
|
|
);
|
|
}
|
|
|
|
/** 扩散并停留结束:隐藏范围圈 */
|
|
private _onRadiusTweenComplete(gen: number): void {
|
|
if (gen !== this._radiusTweenGen) return;
|
|
this._radiusTweenArgs = null;
|
|
this.hide();
|
|
}
|
|
|
|
/** 停止进行中的范围插值 */
|
|
private _cancelRadiusTween(): void {
|
|
this._radiusTweenGen++;
|
|
Tween.stopAllByTarget(this._radiusTweenScratch);
|
|
this._radiusTweenArgs = null;
|
|
}
|
|
|
|
/** 热更/地图销毁后补回节点,并挂回 BattleGame */
|
|
private _ensureRoot(): void {
|
|
const host = this._host;
|
|
if (this._root?.isValid) {
|
|
if (host?.isValid && this._root.parent !== host) {
|
|
this._root.parent = host;
|
|
this._root.layer = host.layer;
|
|
}
|
|
this._removeRootGraphics();
|
|
return;
|
|
}
|
|
if (!host?.isValid) return;
|
|
let n = find('ui_range', host);
|
|
if (!n?.isValid) {
|
|
console.log('[AttackRangePreview] ui_range 已丢失,重建节点');
|
|
n = this._createRangeNode(host);
|
|
}
|
|
this._root = n;
|
|
if (n.parent !== host) n.parent = host;
|
|
n.layer = host.layer;
|
|
n.setSiblingIndex(host.children.length - 1);
|
|
this._removeRootGraphics();
|
|
}
|
|
|
|
/** ui_range 已有 Sprite,引擎禁止同节点再挂 Graphics */
|
|
private _removeRootGraphics(): void {
|
|
if (!this._root?.isValid) return;
|
|
if (!this._root.getComponent(Sprite)) return;
|
|
const g = this._root.getComponent(Graphics);
|
|
if (g) g.destroy();
|
|
}
|
|
}
|
|
|