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; fromRadiusPx: number; toRadiusPx: number; tableCellPx: number; cellWorldA: Readonly | null; cellWorldB: Readonly | 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, radiusPx: number, tableCellPx: number, cellWorldA?: Readonly | null, cellWorldB?: Readonly | 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, fromRadiusPx: number, toRadiusPx: number, tableCellPx: number, cellWorldA?: Readonly | null, cellWorldB?: Readonly | 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(); } }