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/AttackRangePreviewPresenter.ts

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