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.

2311 lines
86 KiB

2 weeks ago
import { _decorator, BlockInputEvents, Component, isValid, Node, Prefab, Sorting2D, sp, Tween, tween, UIOpacity, UITransform, v3, Vec2, Vec3, type EventTouch } from 'cc';
3 weeks ago
import type { EntityId } from 'db://assets/mx/module/ecs';
import type { ComposedUnit } from 'db://assets/mx/module/merge';
2 weeks ago
import { isElementGlyphType, Weapon2TileType } from 'db://assets/mx/module/merge';
3 weeks ago
import { GEvent } from 'db://assets/mx/module/event/GEvent';
import { GBundle, GPath } from '../../game/ConfigRes';
import { BATTLE_HERO_BUFF_PATH, BATTLE_HERO_NUM_PATH } from '../res/BattleHeroResCollector';
2 weeks ago
import { TableNames } from '../../game/ConfigTableData';
3 weeks ago
import { BattleMapSpec } from '../map/BattleMapSpec';
2 weeks ago
import { PEACH_TREE_WEAPON2_ID } from '../bag/BagPrepConfig';
3 weeks ago
import { WeaponComposeConfigProvider } from '../bag/WeaponComposeConfigProvider';
import { textIconName } from '../config/BattleHeroTable';
import type { DeploySync } from '../runtime/DeploySync';
import type { TileEntitySnapshot, TileLocation, TowerRuntimeSnapshot } from '../combat/TowerQueryTypes';
import type { BattleBagSession } from '../bag/BattleBagSession';
import { TowerQueryService } from '../combat/TowerQueryService';
2 weeks ago
import { findEntityIdByGridIndex } from '../runtime/DeployEntityQuery';
3 weeks ago
import { Skill } from '../skill/Skill';
import { BattleSpinePlayback } from '../presentation/BattleSpinePlayback';
import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch';
2 weeks ago
import { HERO_GLYPH_RESOURCES, isHeroActorWeapon1 } from '../heros/HeroActorRes';
2 weeks ago
import { aiPlaceCampForPlayer } from '../ai/runtime/AiCampArea';
3 weeks ago
import { HeroActorHost } from '../heros/HeroActorHost';
2 weeks ago
import { HeroActor } from '../heros/HeroActor';
3 weeks ago
import { ClientErrorReporter } from '../../../mx/tools/ClientErrorReporter';
2 weeks ago
import { applyGlyphSpineSkin, glyphSpineHasSkin } from './GlyphSpineSkin';
3 weeks ago
import 'db://assets/mx/extension/NodeDragExt';
export type {
TileEntitySnapshot,
TileLocation,
TileBindRecord,
TowerConfigSnapshot,
TowerCombatStats,
TowerRuntimeSnapshot,
} from '../combat/TowerQueryTypes';
export { TowerQueryService } from '../combat/TowerQueryService';
/** @deprecated 使用 TileEntitySnapshot */
export type TextGridEntityData = TileEntitySnapshot;
/** @deprecated 使用 TileLocation */
export type TextGridLocation = TileLocation;
/** @deprecated 使用 TileBindRecord */
export type TextGridTileRecord = import('../combat/TowerQueryTypes').TileBindRecord;
const { ccclass } = _decorator;
2 weeks ago
/** 字块 buff 特效 Sorting2D 层(与 textGrid.prefab canUpEff 一致) */
3 weeks ago
const TEXT_GRID_BUFF_FX_SORT_LAYER = 3;
2 weeks ago
/** 字形 skin Sorting2D order(与 textGrid.prefab 内升级特效一致) */
3 weeks ago
const TEXT_GRID_GLYPH_SORT_ORDER = 3;
/** 字块下方 buff 特效 order(须低于字形) */
const TEXT_GRID_BUFF_FX_UNDER_ORDER = 2;
/** 字块上方 buff 特效 order(须高于 canUpEff=4) */
const TEXT_GRID_BUFF_FX_OVER_ORDER = 5;
/** 主动 buff 特效挂层 */
export type TextGridBattleBuffFxLayer = 'over' | 'under';
/** 字块脚下阴影节点 */
const SHADOW_NODE = 'ebg';
2 weeks ago
/** ebg 未旋转时的局部坐标 */
3 weeks ago
interface RuinDecorRest {
/** 节点 */
n: Node;
/** 局部 x */
x: number;
/** 局部 y */
y: number;
/** 局部 z */
z: number;
}
/** 字块显示颜色(hex,不含 #) */
export enum TextColor {
Black = '000000',
Yellow = 'F8D874',
Pink = 'FF69B4',
}
/** 字块视图/UI 状态 */
export interface TextGridViewState {
nodeActive: boolean;
draggable: boolean;
skinReady: boolean;
displayText: string;
textColorHex: string | null;
}
/** 地图格语义(仅 location=map) */
export interface TextGridMapCellState {
gridIndex: number;
row: number;
col: number;
isCanWalk: boolean;
isCanPutText: boolean;
isCanPutGrid: boolean;
isCanBeDig: boolean;
isCanRemove: boolean;
isBestPlace: boolean;
isFreePlace: boolean;
}
/** Skill 表现组件状态 */
export interface TextGridSkillState {
present: boolean;
isActive: boolean;
}
/** getData():战斗层实体 + 视图 + 实时 runtime */
export interface TextGridData extends TileEntitySnapshot {
view: TextGridViewState;
mapCell: TextGridMapCellState | null;
skill: TextGridSkillState | null;
/** 场上实时(AttributeSystem;未部署为 null) */
runtime: TowerRuntimeSnapshot | null;
}
/**
* 背包/地图字块视图(textGrid 预制体)。
2 weeks ago
* 五行字:按 Hero.number 从 battleHero/prefab/num 出池,挂到 skin,并切金/木/水/火/土皮。
2 weeks ago
* 地图激活后按当前 Hero.herotype 挂 weaponType*。
3 weeks ago
*/
@ccclass('TextGrid')
export class TextGrid extends Component {
private static readonly CLICK_MOVE_THRESHOLD_SQ = 10 * 10;
weapon2Id = 0;
weapon1Id = 0;
level = 1;
location: TileLocation | null = null;
private _skinNode: Node | null = null;
/** 当前 Text.icon 皮肤名 */
private _skinIconName: string | null = null;
/** 字块脚下阴影(ebg) */
private _shadowNode: Node | null = null;
private _dragSurface: Node | null = null;
private _originParent: Node | null = null;
private _originPos = { x: 0, y: 0, z: 0 };
2 weeks ago
/** 当前五行数字皮节点(battleHero/prefab/num,挂在 skin 下) */
2 weeks ago
private _numberNode: Node | null = null;
2 weeks ago
/** 当前数字节点已套的五行皮名(金/木/水/火/土) */
private _elementSkinApplied = '';
3 weeks ago
private _clickStart: Vec2 | null = null;
private _entity: TileEntitySnapshot | null = null;
private _dragFollow = true;
/** 地图字块点击(查看攻击范围等) */
private _mapClickHandler: ((grid: TextGrid) => void) | null = null;
/** 拖拽悬停选中(绿色) */
private _isSelectStatus = false;
/** 拖拽悬停不可放置(红色,textGrid/redSelect) */
private _isRedSelectStatus = false;
selectNode: Node | null = null;
redSelectNode: Node | null = null;
/** 牛魔王损毁:字形倒下 */
private _ruin = false;
/** 字形倒下 tween 进行中 */
private _ruinFalling = false;
/** 正在倒下的字形节点(回收后须作废 tween) */
private _ruinFallNode: Node | null = null;
2 weeks ago
/** ebg 未旋转时的局部坐标 */
3 weeks ago
private readonly _decorRest: RuinDecorRest[] = [];
2 weeks ago
/** 可升级提示箭头(battleGame/prefab/canUpEff,按需出池) */
3 weeks ago
private _canUpEff: Node | null = null;
private _canUpEffBaseX = 0;
private _canUpEffBaseY = 0;
2 weeks ago
/** 入场拖尾 trail1(按需出池) */
private _trail1: Node | null = null;
/** 入场拖尾 trail2(按需出池) */
private _trail2: Node | null = null;
/** 入场拖尾 trail3(按需出池) */
private _trail3: Node | null = null;
3 weeks ago
/** 拖动时可合成目标:每秒抖动一次 */
private _mergeShakeOn = false;
private _mergeShakeBase = { x: 0, y: 0, z: 0 };
/** 正面 buff 特效节点(怪_攻击buff) */
private _positiveBuffFx: Node | null = null;
/** 未组合组合字「睡觉」特效 */
private _sleepFx: Node | null = null;
/** 混乱(debuff_8)特效 */
private _confusionFx: Node | null = null;
/** attachBattleBuffFx 挂载的节点(施法幻魂镜等) */
private readonly _attachedBattleBuffFx: Node[] = [];
/**
2 weeks ago
* AI 无头地图轻量模式:只刷等级/散字皮,禁止 ensure/destroy HeroActor
3 weeks ago
*(避免与 DeploySync 双管并在物理步进中改节点导致 Box2D 崩)。
*/
private _aiMapLite = false;
2 weeks ago
/**
* 武器本地 X:索引 = 显示等级 - 1。
* 数字:1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192
*/
wPosX = [0, 0, 0, 0, -18, 18, 18, -24, -24, -24, -30, -30, -30, -12];
3 weeks ago
protected onLoad(): void {
this._skinNode = this.node.getChildByName('skin');
this._shadowNode = this.node.getChildByName(SHADOW_NODE);
this.selectNode = this.node.getChildByName('select');
this.redSelectNode = this.node.getChildByName('redSelect');
this._updateSelect();
}
/**
* 挂载主动 buff 特效到字块根节点并校正层级。
2 weeks ago
* over:置于字形之上(Sorting2D + sibling 置顶);under:置于 skin 之下。
* 多格组合字时锚点对齐占格几何中心。
3 weeks ago
*/
attachBattleBuffFx(fx: Node, layer: TextGridBattleBuffFxLayer): void {
if (!fx?.isValid || !this.node?.isValid) return;
fx.parent = this.node;
const anchor = this._resolveComposeSpineOffset();
fx.setPosition(anchor.x, anchor.y, 0);
fx.layer = this.node.layer;
fx.active = true;
this._ensureGlyphSkinSorting();
2 weeks ago
const ref = this.node.getChildByName('skin');
3 weeks ago
if (layer === 'under') {
if (ref?.isValid) {
fx.setSiblingIndex(ref.getSiblingIndex());
} else {
fx.setSiblingIndex(0);
}
this._applyBattleBuffFxSorting(fx, TEXT_GRID_BUFF_FX_UNDER_ORDER);
return;
}
fx.setSiblingIndex(this.node.children.length - 1);
this._applyBattleBuffFxSorting(fx, TEXT_GRID_BUFF_FX_OVER_ORDER);
this._ensureDragSurface();
this._attachedBattleBuffFx.push(fx);
}
/**
* 回收单个 attachBattleBuffFx 节点(Presenter 中断施法时调用)。
* @param fx 特效节点
*/
releaseAttachedBattleBuffFx(fx: Node): void {
const idx = this._attachedBattleBuffFx.indexOf(fx);
if (idx >= 0) this._attachedBattleBuffFx.splice(idx, 1);
if (!fx?.isValid) return;
Tween.stopAllByTarget(fx);
if (fx['resKey']) gg.res.putNode(fx);
else fx.destroy();
}
/**
* 按节点名回收字块上的主动 buff 特效(含未登记的残留鬼手)。
* @param name 预制体节点名
*/
releaseAttachedBattleBuffFxByName(name: string): void {
if (!name) return;
const tracked = this._attachedBattleBuffFx.slice();
for (let i = 0; i < tracked.length; i++) {
const fx = tracked[i];
if (fx?.name === name) this.releaseAttachedBattleBuffFx(fx);
}
const root = this.node;
if (!root?.isValid) return;
const children = root.children;
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (child?.isValid && child.name === name) this.releaseAttachedBattleBuffFx(child);
}
}
/** 回收字块上由 attachBattleBuffFx 挂入的特效(回池/重建前) */
clearAttachedBattleBuffFx(): void {
while (this._attachedBattleBuffFx.length > 0) {
const fx = this._attachedBattleBuffFx[0];
this.releaseAttachedBattleBuffFx(fx);
}
}
/** 主动 buff 特效锚点世界坐标(多格组合时为占格几何中心) */
resolveBattleBuffFxWorldPosition(): Vec3 {
const anchor = this._resolveComposeSpineOffset();
const ut = this.node.getComponent(UITransform);
if (ut) {
return ut.convertToWorldSpaceAR(v3(anchor.x, anchor.y, 0));
}
const p = this.node.worldPosition;
return v3(p.x + anchor.x, p.y + anchor.y, p.z);
}
/** 字形 skin 补 Sorting2D,与 buff 特效 order 对齐(地图格原先无 Sorting2D 导致下层盖字) */
private _ensureGlyphSkinSorting(): void {
const skin = this._skinNode?.isValid ? this._skinNode : this.node.getChildByName('skin');
if (!skin?.isValid) return;
let sorting = skin.getComponent(Sorting2D);
if (!sorting) sorting = skin.addComponent(Sorting2D);
sorting.sortingLayer = TEXT_GRID_BUFF_FX_SORT_LAYER;
sorting.sortingOrder = TEXT_GRID_GLYPH_SORT_ORDER;
}
/** 主动 buff 特效 Spine Sorting2D(地图格 skin 无 Sorting2D,仅 sibling 不足) */
private _applyBattleBuffFxSorting(fx: Node, order: number): void {
for (const sk of fx.getComponentsInChildren(sp.Skeleton)) {
if (!sk?.node?.isValid) continue;
sk.node.layer = fx.layer;
let sorting = sk.node.getComponent(Sorting2D);
if (!sorting) sorting = sk.node.addComponent(Sorting2D);
sorting.sortingLayer = TEXT_GRID_BUFF_FX_SORT_LAYER;
sorting.sortingOrder = order;
}
for (const sort of fx.getComponents(Sorting2D)) {
sort.sortingLayer = TEXT_GRID_BUFF_FX_SORT_LAYER;
sort.sortingOrder = order;
}
}
/** 显示正面 buff 特效(prefab/怪_攻击buff),挂在字块根节点 */
showPositiveBuffFx(): void {
if (!this.node?.isValid) return;
if (this._positiveBuffFx?.isValid) return;
let node: Node | null = null;
try {
node = gg.res.getNode('prefab/', '怪_攻击buff', GBundle.BattleGameMonster);
} catch (e) {
console.log('[TextGrid] 怪_攻击buff 预制体加载失败', String(e));
return;
}
if (!node?.isValid) return;
node.parent = this.node;
node.setPosition(0, 0, 0);
node.active = true;
node.setSiblingIndex(this.node.children.length - 1);
this._positiveBuffFx = node;
for (const sk of node.getComponentsInChildren(sp.Skeleton)) {
sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME);
const anim = (sk as sp.Skeleton & { defaultAnimation?: string }).defaultAnimation || 'animation';
sk.setAnimation(0, anim, true);
}
this._ensureDragSurface();
}
clearPositiveBuffFx(): void {
if (this._positiveBuffFx?.isValid) {
gg.res.putNode(this._positiveBuffFx);
}
this._positiveBuffFx = null;
}
/**
* 未组合成英雄的组合字:显示「睡觉」特效;组合/离场时回收。
*/
setSleepFx(visible: boolean): void {
if (!visible) {
this.clearSleepFx();
return;
}
if (!this.node?.isValid) return;
if (this._sleepFx?.isValid) {
this._sleepFx.active = true;
this._sleepFx.setSiblingIndex(this.node.children.length - 1);
this._ensureDragSurface();
return;
}
let node: Node | null = null;
try {
node = gg.res.getNode('prefab/', '睡觉', GBundle.BattleGameMonster);
} catch {
return;
}
if (!node?.isValid) return;
node.parent = this.node;
node.setPosition(0, 0, 0);
node.active = true;
node.setSiblingIndex(this.node.children.length - 1);
this._sleepFx = node;
for (const sk of node.getComponentsInChildren(sp.Skeleton)) {
sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME);
const anim = (sk as sp.Skeleton & { defaultAnimation?: string }).defaultAnimation || 'animation';
sk.setAnimation(0, anim, true);
}
this._ensureDragSurface();
}
clearSleepFx(): void {
if (this._sleepFx?.isValid) {
gg.res.putNode(this._sleepFx);
}
this._sleepFx = null;
}
/**
* 白无常 / 顺风耳混乱:格子上挂「混乱效果」。
* @param visible 是否显示
*/
setConfusionFx(visible: boolean): void {
if (!visible) {
this.clearConfusionFx();
return;
}
if (!this.node?.isValid) return;
if (this._confusionFx?.isValid) {
this._confusionFx.active = true;
this._confusionFx.setSiblingIndex(this.node.children.length - 1);
this._ensureDragSurface();
return;
}
let node: Node | null = null;
try {
node = gg.res.getNode(GPath.HeroSkillPrefab(), '混乱效果', GBundle.BattleHero);
} catch (err) {
console.log('[TextGrid] 混乱效果 getNode 失败', String(err));
return;
}
if (!node?.isValid) {
console.log('[TextGrid] 混乱效果预制体未加载');
return;
}
node.parent = this.node;
node.setPosition(0, BattleMapSpec.CELL_SIZE * 0.25, 0);
node.active = true;
node.layer = this.node.layer;
node.setSiblingIndex(this.node.children.length - 1);
this._confusionFx = node;
const sk = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton);
if (!sk?.isValid) {
console.log('[TextGrid] 混乱效果缺少 Skeleton');
this.clearConfusionFx();
return;
}
const anim = String((sk as sp.Skeleton & { defaultAnimation?: string }).defaultAnimation || 'animation').trim();
sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME);
if (!BattleSpinePlayback.play(sk, anim, { loop: true })) {
console.log('[TextGrid] 混乱效果无动画', anim);
this.clearConfusionFx();
return;
}
this._ensureDragSurface();
}
/** 回收混乱特效 */
clearConfusionFx(): void {
const node = this._confusionFx;
this._confusionFx = null;
if (!node?.isValid) return;
const sk = node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton);
BattleSpinePlayback.clearPendingComplete(sk);
BattleSpinePlayback.resetTracks(sk);
gg.res.putNode(node);
}
2 weeks ago
/** 多格组合:特效锚点偏到占格几何中心(水平偏 X,竖直偏 Y) */
3 weeks ago
private _resolveComposeSpineOffset(): { x: number; y: number } {
const composition = this._getComposition();
if (!composition || composition.gridIndices.length < 2) {
return { x: 0, y: 0 };
}
const loc = this.location;
if (loc?.kind !== 'map') return { x: 0, y: 0 };
const map = gg.game.CurentBattle?.Map;
const hostUt = this.node.getComponent(UITransform);
if (!map || !hostUt) return { x: 0, y: 0 };
let sx = 0;
let sy = 0;
let n = 0;
for (const idx of composition.gridIndices) {
const mg = map.getMapGridByIndex(idx);
if (!mg?.node?.isValid) continue;
const p = mg.node.worldPosition;
sx += p.x;
sy += p.y;
n++;
}
if (n === 0) return { x: 0, y: 0 };
const centerLocal = hostUt.convertToNodeSpaceAR(new Vec3(sx / n, sy / n, 0));
if (composition.direction === 'horizontal') {
return { x: centerLocal.x, y: 0 };
}
if (composition.direction === 'vertical') {
return { x: 0, y: centerLocal.y };
}
return { x: centerLocal.x, y: centerLocal.y };
}
/**
2 weeks ago
* 可合成/可升级提示:显示时出池并上下浮动;隐藏时回池。
2 weeks ago
* 蟠桃树不可合成,强制不显示。
3 weeks ago
*/
setCanUpgradeHint(visible: boolean): void {
2 weeks ago
if (!visible || this.isPeachTreeTile()) {
2 weeks ago
this._releaseCanUpEff();
3 weeks ago
return;
}
2 weeks ago
const n = this._ensureCanUpEff();
if (!n) return;
Tween.stopAllByTarget(n);
3 weeks ago
n.active = true;
const x = this._canUpEffBaseX;
const y = this._canUpEffBaseY;
2 weeks ago
n.setPosition(x, y, 0);
3 weeks ago
tween(n)
.repeatForever(
tween(n)
2 weeks ago
.to(0.45, { position: v3(x, y + 10, 0) }, { easing: 'sineInOut' })
.to(0.45, { position: v3(x, y, 0) }, { easing: 'sineInOut' }),
3 weeks ago
)
.start();
}
/**
2 weeks ago
* 拖动时可合成对应目标:每秒左右抖动一下(动 skin 节点,避免与背包 Layout 抢根节点坐标)。
3 weeks ago
*/
setMergeTargetShake(enabled: boolean): void {
const n = this._presentationRoot();
if (!n?.isValid) return;
Tween.stopAllByTarget(n);
if (this._mergeShakeOn) {
n.setPosition(this._mergeShakeBase.x, this._mergeShakeBase.y, this._mergeShakeBase.z);
}
this._mergeShakeOn = enabled;
if (!enabled) return;
const p = n.position;
this._mergeShakeBase = { x: p.x, y: p.y, z: p.z };
const { x, y, z } = this._mergeShakeBase;
const amp = 6;
tween(n)
.repeatForever(
tween(n)
.to(0.05, { position: v3(x + amp, y, z) })
.to(0.05, { position: v3(x - amp, y, z) })
.to(0.05, { position: v3(x + amp * 0.5, y, z) })
.to(0.05, { position: v3(x, y, z) })
.delay(0.8),
)
.start();
}
/** 拖拽悬停目标时显示 select 节点(绿色) */
get isSelectStatus(): boolean {
return this._isSelectStatus;
}
set isSelectStatus(value: boolean) {
this._isSelectStatus = value;
if (value) this._isRedSelectStatus = false;
this._updateSelect();
}
/** 拖拽悬停不可放置格时显示 redSelect 节点(红色) */
get isRedSelectStatus(): boolean {
return this._isRedSelectStatus;
}
set isRedSelectStatus(value: boolean) {
this._isRedSelectStatus = value;
if (value) this._isSelectStatus = false;
this._updateSelect();
}
protected onEnable(): void {
this._bindClickSurface();
}
protected onDisable(): void {
this._unbindClickSurface();
}
get dragNode(): Node {
this._ensureDragSurface();
return this._dragSurface!;
}
/** 停止根节点 tween 并还原 scale/angle(入场 squash 动画被打断时会残留) */
resetNodeTransform(): void {
const node = this.node;
if (!node?.isValid) return;
Tween.stopAllByTarget(node);
node.setScale(1, 1, 1);
node.angle = 0;
if (this._cellRuined()) this._holdRuinXform();
}
/** 根节点归零到父格/槽中心(合成升级、setData 时用;勿在 restoreOrigin 之后调用) */
resetLocalPosition(): void {
const node = this.node;
if (!node?.isValid) return;
Tween.stopAllByTarget(node);
node.setPosition(0, 0, 0);
}
2 weeks ago
/** 还原 skin 节点位移 */
3 weeks ago
resetSkinPresentation(): void {
this.setMergeTargetShake(false);
const n = this._presentationRoot();
if (!n?.isValid) return;
Tween.stopAllByTarget(n);
n.setPosition(0, 0, 0);
n.setScale(1, 1, 1);
n.angle = 0;
if (this._cellRuined()) this._holdRuinXform();
}
2 weeks ago
/** 当前地图格是否敌方放置区(本机看对面 CampAreaID,与出怪口无关) */
3 weeks ago
private _isEnemyCampMapCell(): boolean {
const loc = this.location;
if (loc?.kind !== 'map') return false;
const grid = gg.game.CurentBattle?.Map?.getMapGridByIndex(loc.gridIndex);
2 weeks ago
const campAreaId = (grid?.CampAreaID ?? 0) | 0;
const localId = gg.game.CurentBattle?.player?.playerId ?? 0;
const enemyPlayerId = localId === 0 ? 1 : 0;
return campAreaId === aiPlaceCampForPlayer(enemyPlayerId);
3 weeks ago
}
/** 绑定文字实体完整数据(来自 TowerQueryService.buildTileEntity) */
setData(data: TileEntitySnapshot): void {
const prevLoc = this.location;
this._invalidateHeroActorRetry();
this.resetNodeTransform();
this.resetSkinPresentation();
this._clearBattleCardVisual();
3 weeks ago
this._entity = data;
this.weapon2Id = Number(data.tile.id);
this.weapon1Id = Number(data.weapon1Id) || 0;
this.level = data.level;
this.location = data.location;
// 仅当场上真实占格节点收回背包时才拆英雄;对象池幽灵节点带旧 location 不得误伤
if (prevLoc?.kind === 'map' && data.location.kind === 'bag' && this._isLiveMapOccupant(prevLoc.gridIndex)) {
HeroActorHost.instance.destroyAtGrid(prevLoc.gridIndex);
}
const cardKind = Math.floor(Number(data.battleCardKind) || 0);
if (cardKind > 0) {
this._hideNumberNodes();
this._ensureSkinNode();
this._skinIconName = null;
this._applyBattleCardVisual(cardKind === 1 ? '倍率卡' : '属性卡');
this._finishSkinPresentation();
this._updateLevelLabel();
this.hideEntranceTrail();
this._ensureDragSurface();
this._forwardChildTouchToSurface();
return;
}
2 weeks ago
if (!this._isElementNumberTile()) {
2 weeks ago
this._hideNumberNodes();
3 weeks ago
this._ensureSkinNode();
const iconName = textIconName(data.tile);
this._skinIconName = null;
this._applySkinIcon(iconName);
1 week ago
} else {
// 五行数字不用 Text.icon;清掉池化残留,避免走英雄字形分支盖住数字子节点
this._skinIconName = null;
3 weeks ago
}
this._finishSkinPresentation();
this._updateLevelLabel();
this.hideEntranceTrail();
this._ensureDragSurface();
this._forwardChildTouchToSurface();
}
getEntityData(): TileEntitySnapshot | null {
if (!this.location && !this._entity) return null;
if (this._aiMapLite && this._entity) return this._entity;
return this._queryEntity();
}
setLocation(loc: TileLocation | null): void {
this.location = loc;
}
/** 完整快照:实体走战斗层查询,视图层只补 UI 字段 */
getData(deploySync?: DeploySync | null): TextGridData {
const entity = this._aiMapLite && this._entity
? this._entity
: this._queryEntity();
const runtime = entity.location.kind === 'map' && !this._aiMapLite
? TowerQueryService.instance.getRuntimeByGridIndex(entity.location.gridIndex, undefined, deploySync ?? null)
: null;
return {
...entity,
view: this._buildViewState(),
mapCell: this._buildMapCellState(),
skill: this._buildSkillState(),
runtime,
};
}
setMapClickHandler(handler: ((grid: TextGrid) => void) | null): void {
this._mapClickHandler = handler;
}
onClickText(e: EventTouch): void {
const data = this.getData();
const range = data.runtime?.stats.range ?? data.tower?.stats.range;
if (this.location?.kind === 'map' || this.location?.kind === 'bag') {
if (this._mapClickHandler) {
e.propagationStopped = true;
this._mapClickHandler(this);
} else {
console.log('[TextGrid] mapClickHandler 未绑定', this.weapon2Id, this.location);
}
}
}
getSkinNode(): Node | null {
return this._skinNode?.isValid ? this._skinNode : null;
}
rememberOrigin(): void {
this._originParent = this.node.parent;
const p = this.node.position;
this._originPos = { x: p.x, y: p.y, z: p.z };
}
restoreOrigin(): void {
if (this._originParent?.isValid) {
this.node.parent = this._originParent;
}
this.node.setPosition(this._originPos.x, this._originPos.y, this._originPos.z);
this.resetNodeTransform();
}
2 weeks ago
/** 入场落地后恢复可见性:五行数字皮优先,其余字按 HeroActor 态同步 */
3 weeks ago
syncPresentationVisibility(): void {
if (!this.isValid) return;
2 weeks ago
if (this._isElementNumberTile()) {
this._updateLevelLabel();
}
3 weeks ago
this._syncSkillActiveState();
}
/**
* 拖动/交换落地后恢复字形可见。
* putNode 会把子树 Skeleton.enabled 关掉;地图原先跳过,交换后只剩 ebg 阴影和 lv。
2 weeks ago
* 已成将格只对账藏/显皮,不把格子皮强行打开(避免叠在 heroTemplate 上)。
2 weeks ago
* 根节点回到 rememberOrigin 位置(地图字在专用层上本地非 0)。
3 weeks ago
* @returns 是否曾发现异常并修复
*/
ensureBagGlyphVisible(): boolean {
if (!this.isValid || !this.node?.isValid) return false;
let repaired = false;
if (!this.node.active) {
this.node.active = true;
repaired = true;
}
const uo = this.node.getComponent(UIOpacity);
if (uo?.isValid && uo.opacity < 255) {
uo.opacity = 255;
repaired = true;
}
this.node.setScale(1, 1, 1);
2 weeks ago
// 地图字挂 ui_textGrids 时本地非 (0,0);归零会把字形和武器甩到层原点
this.restoreOrigin();
3 weeks ago
this.resetSkinPresentation();
2 weeks ago
if (this._isElementNumberTile()) {
1 week ago
// 背包必须强制露数字;飞行藏英雄期间 _shouldHideGridSkin 仍可能为 true
const forceShow = this.location?.kind === 'bag' || !this._shouldHideGridSkin();
if (this._repairElementNumberGlyph(forceShow, true)) repaired = true;
2 weeks ago
this._syncShadow();
2 weeks ago
} else if (this._isHeroActorComposeCell()) {
// 成将只对账:覆盖层已挂上则藏皮。禁止强行开皮叠在 heroTemplate 上。
3 weeks ago
this._syncSkillActiveState();
} else {
this._ensureSkinNode();
const skin = this._skinNode;
if (skin?.isValid && !skin.active) {
skin.active = true;
repaired = true;
}
const sk = this._getSkinSkeleton();
if (sk?.isValid) {
if (!sk.enabled) {
sk.enabled = true;
repaired = true;
}
sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME);
if (this._skinIconName) {
this._applySkinSpineIcon(this._skinIconName);
}
}
this._syncShadow();
}
if (repaired) {
console.log(
'[TextGrid] ensureBagGlyphVisible 修复字形',
this.weapon2Id,
this.location,
);
}
return repaired;
}
/**
2 weeks ago
* AI 无头地图:轻量表现(等级标 + 散字皮),不碰 HeroActor 生命周期。
3 weeks ago
* @param on 是否开启
*/
setAiMapLite(on: boolean): void {
this._aiMapLite = !!on;
}
/** 松手飞入目标格前:组合/英雄皮可能被 HeroActor 隐藏,先恢复可见再播弧线 */
preparePlaceFlightVisual(): void {
2 weeks ago
if (this._isElementNumberTile()) {
1 week ago
// 飞行中 HeroActor 透明隐藏,须强制露数字,否则落地背包只剩阴影
// 飞行途中勿 _syncNumberSkill,避免又挂回 HeroActor 把字皮藏掉
this._repairElementNumberGlyph(true, false);
2 weeks ago
return;
}
3 weeks ago
if (this._skinNode?.isValid) this._skinNode.active = true;
}
1 week ago
/** 落子/交换落地后:按 HeroActor 状态收起格子皮;收回背包再强制露数字 */
3 weeks ago
finishPlaceFlightVisual(): void {
this._syncSkillActiveState();
1 week ago
if (this.location?.kind === 'bag' && this._isElementNumberTile()) {
this.ensureBagGlyphVisible();
}
3 weeks ago
}
/**
* 回节点池前:停入场/拖拽残留,清选中与透明度。
* 否则下次取出会带着旋转角、绿色 select 框进背包。
*/
prepareForPool(): void {
if (!this.isValid) return;
2 weeks ago
this.releaseNumberSkill();
3 weeks ago
this.clearPositiveBuffFx();
this.clearSleepFx();
this.clearConfusionFx();
this.clearAttachedBattleBuffFx();
this.resetNodeTransform();
this.resetSkinPresentation();
this._skinIconName = null;
2 weeks ago
this._hideNumberNodes();
3 weeks ago
this.hideEntranceTrail();
this.isSelectStatus = false;
this.isRedSelectStatus = false;
this.setCanUpgradeHint(false);
this.setRuinStyle(false, true);
this.enableDrag(false);
this.setMapClickHandler(null);
this.setDragFollow(true);
this.setAiMapLite(false);
this.location = null;
this._entity = null;
this.weapon2Id = 0;
this.weapon1Id = 0;
this.level = 1;
this.node.name = 'textGrid';
const uo = this.node.getComponent(UIOpacity);
if (uo) uo.opacity = 255;
this.node.setPosition(0, 0, 0);
}
enableDrag(enabled: boolean): void {
if (!this.isValid || !this.node?.isValid) return;
this._ensureDragSurface();
const surface = this._dragSurface!;
surface.off(Node.DragEvent.DRAG_MOVE, this._syncDragMove, this);
surface.off(Node.DragEvent.DRAG_END, this._resetDragSurface, this);
if (enabled) {
surface.on(Node.DragEvent.DRAG_MOVE, this._syncDragMove, this);
surface.on(Node.DragEvent.DRAG_END, this._resetDragSurface, this);
}
surface.draggable = enabled;
const canClick = this.location?.kind === 'map' || this.location?.kind === 'bag';
const block = surface.getComponent(BlockInputEvents);
if (block) {
// 禁拖拽时仍保留点击(查看攻击范围 / 武器详情)
block.enabled = enabled || canClick;
}
if (canClick) {
this._bindClickSurface();
}
}
setDragFollow(enabled: boolean): void {
this._dragFollow = enabled;
if (!enabled) this._resetDragSurface();
}
onDestroy(): void {
2 weeks ago
this.releaseNumberSkill();
3 weeks ago
this.clearPositiveBuffFx();
this.clearSleepFx();
this.clearConfusionFx();
this.clearAttachedBattleBuffFx();
2 weeks ago
this._releaseCanUpEff();
this._releaseTrails();
3 weeks ago
if (this._mergeShakeOn) {
this.setMergeTargetShake(false);
}
this.resetSkinPresentation();
2 weeks ago
this._hideNumberNodes();
3 weeks ago
if (this._dragSurface?.isValid) {
this._dragSurface.removeDragEvent?.();
this._dragSurface.destroy();
}
this._dragSurface = null;
this._skinIconName = null;
this.node.removeDragEvent?.();
}
private _queryEntity(): TileEntitySnapshot {
const location = this.location ?? this._entity?.location;
if (!location) return this._entity!;
const composition = this._findComposition() ?? this._entity?.composition ?? null;
const level = composition?.level ?? this.level;
return TowerQueryService.instance.buildTileEntity(
location,
{
weapon2Id: this.weapon2Id,
weapon1Id: this.weapon1Id > 0 ? this.weapon1Id : undefined,
level,
source: this._entity?.source ?? undefined,
},
{ composition, player: gg.game.localPlayer },
) ?? this._entity!;
}
private _findComposition(): ComposedUnit | null {
const loc = this.location;
if (loc?.kind !== 'map') return null;
const local = gg.game.localPlayer?.compositions.find(c => c.gridIndices.includes(loc.gridIndex));
if (local) return local;
const remote = gg.game.CurentBattle?.remotePlayer?.compositions.find(
c => c.gridIndices.includes(loc.gridIndex),
);
return remote ?? null;
}
/** 优先用 MergeSession 当前组合;快照仅在 live 尚未覆盖本格时回落 */
private _getComposition(): ComposedUnit | null {
const live = this._findComposition();
if (live) return live;
const loc = this.location;
const fromEntity = this._entity?.composition;
if (fromEntity && loc?.kind === 'map' && this._compHasGrid(fromEntity, loc.gridIndex)) {
return fromEntity;
}
return null;
}
/** 组合是否仍占该格 */
private _compHasGrid(composition: ComposedUnit, gridIndex: number): boolean {
const idx = composition.gridIndices;
for (let i = 0; i < idx.length; i++) {
if (idx[i] === gridIndex) return true;
}
return false;
}
private _buildViewState(): TextGridViewState {
2 weeks ago
const skinReady = !!this._getSkinSkeleton();
3 weeks ago
return {
nodeActive: this.node.active,
draggable: this._dragSurface?.draggable ?? false,
skinReady,
displayText: '',
textColorHex: null,
};
}
private _buildMapCellState(): TextGridMapCellState | null {
const loc = this.location;
if (loc?.kind !== 'map') return null;
const grid = gg.game.CurentBattle?.Map?.getMapGridByIndex(loc.gridIndex);
if (!grid) return null;
return {
gridIndex: loc.gridIndex,
row: grid.Row,
col: grid.Col,
isCanWalk: grid.isCanWalk,
isCanPutText: grid.isCanPutText,
isCanPutGrid: grid.isCanPutGrid,
isCanBeDig: grid.isCanBeDig,
isCanRemove: grid.isCanRemove,
isBestPlace: grid.isBestPlace,
isFreePlace: grid.isFreePlace,
};
}
private _buildSkillState(): TextGridSkillState | null {
const loc = this.location;
if (loc?.kind === 'map') {
const entityId = TowerQueryService.instance.getRuntimeByGridIndex(loc.gridIndex)?.entityId;
if (entityId != null) {
const actor = HeroActorHost.instance.getActor(entityId);
if (actor?.isValid) {
return { present: true, isActive: actor.isActive };
}
}
}
const skin = this._skinNode?.isValid ? this._skinNode : null;
if (!skin) return null;
const skill = skin.getComponent(Skill) ?? skin.getComponentInChildren(Skill);
return skill ? { present: true, isActive: skill.isActive } : { present: false, isActive: false };
}
private _bindClickSurface(): void {
this._ensureDragSurface();
const surface = this._dragSurface;
if (!surface?.isValid) return;
this._unbindClickSurface();
surface.on(Node.EventType.TOUCH_START, this._onSurfaceTouchStart, this);
surface.on(Node.EventType.TOUCH_END, this._onSurfaceTouchEnd, this);
surface.on(Node.EventType.TOUCH_CANCEL, this._onSurfaceTouchCancel, this);
}
private _unbindClickSurface(): void {
const surface = this._dragSurface;
if (!surface?.isValid) return;
surface.off(Node.EventType.TOUCH_START, this._onSurfaceTouchStart, this);
surface.off(Node.EventType.TOUCH_END, this._onSurfaceTouchEnd, this);
surface.off(Node.EventType.TOUCH_CANCEL, this._onSurfaceTouchCancel, this);
}
private _onSurfaceTouchStart = (e: EventTouch): void => {
if (!this._clickStart) this._clickStart = new Vec2();
this._clickStart.set(e.getUILocation());
};
private _onSurfaceTouchEnd = (e: EventTouch): void => {
if (!this._clickStart) return;
const end = e.getUILocation();
const dx = end.x - this._clickStart.x;
const dy = end.y - this._clickStart.y;
this._clickStart = null;
if (dx * dx + dy * dy > TextGrid.CLICK_MOVE_THRESHOLD_SQ) return;
const surface = this._dragSurface as (Node & { _dragging?: boolean }) | null;
if (surface?._dragging) return;
this.onClickText(e);
};
private _onSurfaceTouchCancel = (): void => {
this._clickStart = null;
};
private _syncDragMove = (): void => {
if (!this._dragFollow) {
this._resetDragSurface();
return;
}
const surface = this._dragSurface;
if (!surface?.isValid) return;
const lp = surface.position;
if (lp.x === 0 && lp.y === 0) return;
const np = this.node.position;
this.node.setPosition(np.x + lp.x, np.y + lp.y, np.z + lp.z);
surface.setPosition(0, 0, 0);
};
private _resetDragSurface = (): void => {
this._dragSurface?.setPosition(0, 0, 0);
};
/** 拖拽悬停 select / redSelect */
private _updateSelect(): void {
if (this.selectNode && isValid(this.selectNode)) {
this.selectNode.active = this._isSelectStatus;
}
if (this.redSelectNode && isValid(this.redSelectNode)) {
this.redSelectNode.active = this._isRedSelectStatus;
}
}
/**
2 weeks ago
* 牛魔王损毁:只倒本格字形(HeroActor / skin),格子根不转。
* ebg 保持世界旋转与世界坐标不变。
3 weeks ago
* 是否倒下跟当前格/槽字块 ruined,禁止拧到另一座同名英雄。
* @param on 是否处于损毁失效
* @param fromPool 回池强制清本格节点;字块仍 ruined 时不得扶正场上 HeroActor
*/
setRuinStyle(on: boolean, fromPool = false): void {
if (!this.isValid) return;
this._snapAngle(this.node, 0);
if (fromPool && !on) {
this._ruin = false;
this._clearRuinXform(true);
return;
}
const tileRuined = this._cellRuined();
const next = tileRuined || !!on;
if (!on && tileRuined) {
if (this._ruin) this._holdRuinXform();
else {
this._ruin = true;
this._playRuinFall();
}
return;
}
if (this._ruin === next) {
if (next) this._holdRuinXform();
else this._clearLeftoverRuinAngle();
return;
}
this._ruin = next;
if (this._ruin) this._playRuinFall();
else this._clearRuinXform(false);
}
/**
2 weeks ago
* 损毁期间每帧钉死本格字形 -90°,并把 ebg 钉回世界角与世界坐标。
3 weeks ago
*/
lateUpdate(): void {
if (!this._ruin && !this._cellRuined()) return;
this._holdRuinXform();
}
2 weeks ago
/** 可见字形:只取本格 HeroActor / 格子 skin */
3 weeks ago
private _ruinTarget(): Node | null {
const loc = this.location;
if (loc?.kind === 'map') {
const n = HeroActorHost.instance.getDisplayAtGrid(loc.gridIndex);
if (n?.isValid) return n;
}
const skin = this._presentationRoot();
return skin?.isValid ? skin : null;
}
/**
* 钉死节点角度。
* @param n 节点
* @param angle 角度
*/
private _snapAngle(n: Node | null, angle: number): void {
if (!n?.isValid) return;
Tween.stopAllByTarget(n);
n.angle = angle;
}
/**
* 当前格/槽字块是否仍损毁(交换后以数据为准,避免拧到新占格的英雄皮)。
* 敌方 AI 字读对手 bag,禁止误用本机格子。
*/
private _cellRuined(): boolean {
const loc = this.location;
const bag = this._ownerBagSession();
if (!loc || !bag) return this._ruin;
if (loc.kind === 'map') return !!bag.merge.map.getTile(loc.gridIndex)?.ruined;
return !!bag.merge.bag.getSlot(loc.slotIndex)?.ruined;
}
/**
* 字块所属玩家的 bag(敌方读 remotePlayer)。
*/
private _ownerBagSession(): BattleBagSession | null {
const battle = gg.game.CurentBattle;
if (!battle) return null;
if (this._aiMapLite || this._isEnemyCampMapCell()) {
return battle.remotePlayer?.bagSession ?? null;
}
return battle.player?.bagSession ?? null;
}
/** 已倒下时保持字形 -90°;格子根保持 0;底框/阴影世界角与世界坐标保持不变 */
private _holdRuinXform(): void {
this._snapAngle(this.node, 0);
if (!this._cellRuined()) return;
const n = this._ruinTarget();
if (this._ruinFalling && !this._isRuinFallLive(n)) {
this._ruinFalling = false;
this._ruinFallNode = null;
}
if (!this._ruinFalling && n?.isValid && n.angle !== -90) this._snapAngle(n, -90);
this._lockRuinDecorUpright();
}
/**
* 倒下 tween 是否仍在 0°~-90° 之间。
* @param n 当前本格字形
*/
private _isRuinFallLive(n: Node | null): boolean {
const fall = this._ruinFallNode;
if (!fall?.isValid || fall !== n) return false;
return Math.abs(fall.angle) > 1 && Math.abs(fall.angle + 90) > 1;
}
/** 字形倒下 */
private _playRuinFall(): void {
this._snapAngle(this.node, 0);
const n = this._ruinTarget();
if (!n?.isValid) return;
Tween.stopAllByTarget(n);
this._ruinFalling = true;
this._ruinFallNode = n;
tween(n).to(0.2, { angle: -90 }).call(this._onRuinFallEnd, this).start();
this._lockRuinDecorUpright();
}
/** 倒下 tween 结束:钉死字形 -90° */
private _onRuinFallEnd(): void {
this._ruinFalling = false;
this._ruinFallNode = null;
if (!this.isValid || !this._ruin) return;
const n = this._ruinTarget();
if (n?.isValid) n.angle = -90;
this._lockRuinDecorUpright();
}
/**
* 字形回正,格子根与底框/阴影也回正。
* @param fromPool 回池:字块仍 ruined 时不得扶正场上 HeroActor
*/
private _clearRuinXform(fromPool = false): void {
this._ruinFalling = false;
this._ruinFallNode = null;
this._snapAngle(this.node, 0);
this._snapAngle(this._presentationRoot(), 0);
const skipHero = fromPool && this._cellRuined();
if (!skipHero && !this._cellRuined()) this._snapAngle(this._heroDisplayOnThisCell(), 0);
this._resetRuinDecorLocal(skipHero);
}
/**
2 weeks ago
* ebg 相对父节点反向旋转,并钉在父节点未旋转时的世界坐标上。
3 weeks ago
*/
private _lockRuinDecorUpright(): void {
this._lockDecorOn(this._ruinTarget());
this._lockDecorOn(this.node);
}
/**
2 weeks ago
* 锁定 host 下 ebg。
3 weeks ago
* @param host 倒下的字形根或格子根
*/
private _lockDecorOn(host: Node | null): void {
if (!host?.isValid) return;
this._applyUprightWorld(host.getChildByName(SHADOW_NODE));
}
/**
* 局部角 = -父角;世界坐标 = 父世界坐标 + 未旋转局部偏移。
2 weeks ago
* @param n ebg
3 weeks ago
*/
private _applyUprightWorld(n: Node | null): void {
if (!n?.isValid) return;
const parent = n.parent;
if (!parent?.isValid) {
n.angle = 0;
return;
}
const rest = this._ensureDecorRest(n);
n.angle = -parent.angle;
const wp = parent.worldPosition;
const ws = parent.worldScale;
n.setWorldPosition(wp.x + rest.x * ws.x, wp.y + rest.y * ws.y, wp.z + rest.z);
}
/**
2 weeks ago
* 记下阴影未倒下时的局部坐标。
* @param n ebg
3 weeks ago
*/
private _ensureDecorRest(n: Node): RuinDecorRest {
const list = this._decorRest;
for (let i = 0; i < list.length; i++) {
if (list[i].n === n) return list[i];
}
const p = n.position;
const rest: RuinDecorRest = { n, x: p.x, y: p.y, z: p.z };
const parent = n.parent;
if (parent?.isValid && Math.abs(parent.angle) > 1 && Math.abs(n.angle + parent.angle) < 1) {
const rad = parent.angle * Math.PI / 180;
const c = Math.cos(rad);
const s = Math.sin(rad);
rest.x = p.x * c - p.y * s;
rest.y = p.x * s + p.y * c;
}
list.push(rest);
return rest;
}
/**
* 解除损毁时把底框/阴影局部角与坐标清回未倒下。
* @param skipHero 回池且字块仍 ruined:不要动场上 HeroActor 的 ebg
*/
private _resetRuinDecorLocal(skipHero = false): void {
this._resetDecorOn(this._presentationRoot());
this._resetDecorOn(this.node);
if (!skipHero) this._resetDecorOn(this._heroDisplayOnThisCell());
}
/**
* 本格 HeroActor 可见字形。
*/
private _heroDisplayOnThisCell(): Node | null {
const loc = this.location;
if (loc?.kind !== 'map') return null;
return HeroActorHost.instance.getDisplayAtGrid(loc.gridIndex);
}
/**
2 weeks ago
* 回正 host 下 ebg。
3 weeks ago
* @param host 字形根或格子根
*/
private _resetDecorOn(host: Node | null): void {
if (!host?.isValid) return;
this._restoreDecor(host.getChildByName(SHADOW_NODE));
}
/**
* 恢复单节点休息位姿并丢掉缓存。
2 weeks ago
* @param n ebg
3 weeks ago
*/
private _restoreDecor(n: Node | null): void {
if (!n?.isValid) return;
const list = this._decorRest;
for (let i = list.length - 1; i >= 0; i--) {
if (list[i].n !== n) continue;
n.setPosition(list[i].x, list[i].y, list[i].z);
list.splice(i, 1);
break;
}
this._snapAngle(n, 0);
}
/**
* 未损毁时清掉残留 -90°(交换后英雄皮可能被旧格误拧)。
* 本格字块仍 ruined 则不许扶正。
*/
private _clearLeftoverRuinAngle(): void {
if (this._cellRuined()) {
this._holdRuinXform();
return;
}
this._snapIfFallen(this._ruinTarget());
this._snapIfFallen(this._presentationRoot());
this._snapIfFallen(this._heroDisplayOnThisCell());
this._resetRuinDecorLocal(false);
}
/**
* 仅当节点处于损毁角时回正,避免每帧打断其它 tween。
* @param n 节点
*/
private _snapIfFallen(n: Node | null): void {
if (!n?.isValid) return;
if (Math.abs(n.angle + 90) > 1) return;
this._snapAngle(n, 0);
}
private _ensureSkinNode(): void {
if (!this._skinNode?.isValid) {
this._skinNode = this.node.getChildByName('skin');
}
}
2 weeks ago
/** 当前字块是否为蟠桃树(Text.id=102;供玩法分支,表现走 skin) */
3 weeks ago
isPeachTreeTile(): boolean {
return this.weapon2Id === PEACH_TREE_WEAPON2_ID;
}
2 weeks ago
/** 抖动/复位用的表现根:五行数字皮,其余用 skin */
3 weeks ago
private _presentationRoot(): Node {
2 weeks ago
if (this._isElementNumberTile() && this._numberNode?.isValid) return this._numberNode;
3 weeks ago
if (this._skinNode?.isValid) return this._skinNode;
return this.node;
}
2 weeks ago
/** 阴影:英雄皮已挂上时隐藏;单字上场等英雄预制体期间仍显示 */
3 weeks ago
private _syncShadow(): void {
if (!this._shadowNode?.isValid) {
this._shadowNode = this.node.getChildByName(SHADOW_NODE);
}
const node = this._shadowNode;
if (!node?.isValid) return;
2 weeks ago
node.active = !this._shouldHideSkinForHeroActor();
3 weeks ago
}
private _getSkinSkeleton(): sp.Skeleton | null {
return this._skinNode?.getComponent(sp.Skeleton) ?? null;
}
/**
2 weeks ago
* 出字入场拖尾:普通字 / 五行字用 trail1。
3 weeks ago
* 仅入场飞行时打开,落地后须 hideEntranceTrail。
*/
showEntranceTrail(): void {
const type = (this._entity?.tileType as Weapon2TileType | undefined)
?? WeaponComposeConfigProvider.instance.getWeapon2Type(this.weapon2Id) as Weapon2TileType;
this._applyTrailByTileType(type);
}
hideEntranceTrail(): void {
2 weeks ago
this._releaseTrails();
3 weeks ago
}
2 weeks ago
/** 入场拖尾:文字 trail1 */
3 weeks ago
private _applyTrailByTileType(type: Weapon2TileType): void {
2 weeks ago
this._releaseTrails();
2 weeks ago
const use1 = type === Weapon2TileType.Single || isElementGlyphType(type);
2 weeks ago
if (!use1) return;
const trail1 = this._ensureTrail(1);
if (trail1) trail1.active = true;
}
/**
* 从 battleGame/prefab 出池并挂到字块根。
* @param name 预制体名
*/
private _takeGridDecor(name: string): Node | null {
let node: Node | null = null;
try {
node = gg.res.getNode(GPath.Prefab, name, GBundle.BattleGame);
} catch (e) {
console.error('[TextGrid] 装饰预制体加载失败', name, String(e));
return null;
}
if (!node?.isValid) {
console.error('[TextGrid] 装饰预制体缺失', name);
return null;
}
node.parent = this.node;
node.layer = this.node.layer;
this._forwardChildTouchToSurface();
this._ensureDragSurface();
return node;
}
/** 装饰节点回池 */
private _putGridDecor(node: Node | null): void {
if (!node?.isValid) return;
Tween.stopAllByTarget(node);
if (node['resKey']) gg.res.putNode(node);
else node.destroy();
}
/** 可升级箭头出池 */
private _ensureCanUpEff(): Node | null {
if (this._canUpEff?.isValid) return this._canUpEff;
const node = this._takeGridDecor('canUpEff');
if (!node) return null;
this._applyCanUpEffPrefabPose(node);
this._canUpEff = node;
return node;
}
/** 按预制体记录箭头局部坐标(getNode 会清零 position) */
private _applyCanUpEffPrefabPose(node: Node): void {
const prefab = gg.res.getRes('canUpEff', GPath.Prefab, GBundle.BattleGame, Prefab) as Prefab | null;
const data = prefab?.data;
if (!data?.isValid) {
console.error('[TextGrid] canUpEff 预制体位姿读不到');
this._canUpEffBaseX = 0;
this._canUpEffBaseY = 0;
return;
}
this._canUpEffBaseX = data.position.x;
this._canUpEffBaseY = data.position.y;
node.setPosition(this._canUpEffBaseX, this._canUpEffBaseY, 0);
}
/** 可升级箭头回池 */
private _releaseCanUpEff(): void {
const n = this._canUpEff;
this._canUpEff = null;
this._putGridDecor(n);
const leftover = this.node?.getChildByName('canUpEff');
if (leftover?.isValid && leftover !== n) this._putGridDecor(leftover);
}
/**
* 入场拖尾出池。
* @param index 1/2/3
*/
private _ensureTrail(index: 1 | 2 | 3): Node | null {
const held = this._trailByIndex(index);
if (held?.isValid) return held;
const name = `trail${index}`;
const node = this._takeGridDecor(name);
if (!node) return null;
node.setPosition(0, 0, 0);
this._setTrailByIndex(index, node);
return node;
}
/**
* 当前持有的拖尾节点。
* @param index 1/2/3
*/
private _trailByIndex(index: 1 | 2 | 3): Node | null {
if (index === 1) return this._trail1;
if (index === 2) return this._trail2;
return this._trail3;
}
/**
* 记录拖尾节点。
* @param index 1/2/3
* @param node 拖尾节点
*/
private _setTrailByIndex(index: 1 | 2 | 3, node: Node | null): void {
if (index === 1) this._trail1 = node;
else if (index === 2) this._trail2 = node;
else this._trail3 = node;
}
/** 入场拖尾全部回池 */
private _releaseTrails(): void {
this._putGridDecor(this._trail1);
this._putGridDecor(this._trail2);
this._putGridDecor(this._trail3);
this._trail1 = null;
this._trail2 = null;
this._trail3 = null;
if (!this.node?.isValid) return;
this._putGridDecor(this.node.getChildByName('trail1'));
this._putGridDecor(this.node.getChildByName('trail2'));
this._putGridDecor(this.node.getChildByName('trail3'));
2 weeks ago
}
/**
* 五行字:Text.type 为 1–5(金木水火土)。
2 weeks ago
* 与旧 Weapon2 类型枚举编号重叠,但语义不同。
2 weeks ago
*/
private _isElementNumberTile(): boolean {
const type = Math.floor(Number(this._entity?.tile?.type));
return type >= 1 && type <= 5;
}
/** 子节点名是否为纯数字(对应 Hero.number) */
private _isNumberFaceName(name: string): boolean {
if (!name) return false;
let n = 0;
for (let i = 0; i < name.length; i++) {
const c = name.charCodeAt(i);
if (c < 48 || c > 57) return false;
n = n * 10 + (c - 48);
}
return n > 0;
}
/** Hero.herotype → 数字 Spine 皮肤名 */
private _elementSkinName(herotype: number): string {
switch (herotype) {
case 1: return '金';
case 2: return '木';
case 3: return '水';
case 4: return '火';
case 5: return '土';
default: return '';
}
}
/** 按 herotype 与显示等级查 Hero。显示等级含段位加值与合成升级,不是 Text.level */
private _heroByTypeLevel(herotype: number, level: number): ITableHero | null {
const heroes = gg.data.table.getTableList<ITableHero>(TableNames.Hero) ?? [];
for (let i = 0; i < heroes.length; i++) {
const hero = heroes[i];
if (Math.floor(Number(hero.herotype)) !== herotype) continue;
if (Math.floor(Number(hero.level)) !== level) continue;
return hero;
}
return null;
}
2 weeks ago
/** 当前数字节点回池 */
private _putNumberNode(node: Node | null): void {
if (!node?.isValid) return;
Tween.stopAllByTarget(node);
node.off(Node.EventType.TOUCH_START, this._relayTouchToDragSurface, this);
node.off(Node.EventType.TOUCH_MOVE, this._relayTouchToDragSurface, this);
node.off(Node.EventType.TOUCH_END, this._relayTouchToDragSurface, this);
node.off(Node.EventType.TOUCH_CANCEL, this._relayTouchToDragSurface, this);
if (node['resKey']) gg.res.putNode(node);
else node.destroy();
}
/**
* 从对象池取出数字预制体。
* @param num Hero.number
*/
private _takeNumberNode(num: number): Node | null {
const name = String(num);
let node: Node | null = null;
try {
node = gg.res.getNode(BATTLE_HERO_NUM_PATH, name, GBundle.BattleHero);
} catch (e) {
console.error('[TextGrid] 数字预制体加载失败', name, String(e));
return null;
}
if (!node?.isValid) {
console.error('[TextGrid] 数字预制体缺失', name);
return null;
}
return node;
}
/**
* 把数字预制体挂到 skin。
* @param node 出池节点
*/
private _attachNumberNode(node: Node): boolean {
this._ensureSkinNode();
const skin = this._skinNode;
if (!skin?.isValid) {
console.error('[TextGrid] skin 节点缺失,无法挂数字', node.name);
this._putNumberNode(node);
return false;
}
skin.active = true;
1 week ago
// 五行数字走子节点 Spine;关掉 skin 自带 Skeleton,避免盖住数字或抢 Sorting2D
const skinSk = skin.getComponent(sp.Skeleton);
if (skinSk?.isValid) skinSk.enabled = false;
2 weeks ago
node.parent = skin;
node.layer = this.node.layer;
node.setPosition(0, 0, 0);
this._ensureNumberGlyphSorting(node);
this._forwardChildTouchToSurface();
return true;
}
1 week ago
/**
* 强制恢复五行数字可见(拖回背包 / 飞行露字)。
* @param show 是否显示数字皮
* @param syncSkill 是否对账 HeroActor(飞行露字阶段传 false)
* @returns 是否曾修复
*/
private _repairElementNumberGlyph(show: boolean, syncSkill: boolean): boolean {
let repaired = false;
this._ensureSkinNode();
const skin = this._skinNode;
if (skin?.isValid && !skin.active) {
skin.active = true;
repaired = true;
}
const before = this._numberNode;
this._applyNumberGlyph(show);
if (show && this._numberNode?.isValid) {
if (skin?.isValid && this._numberNode.parent !== skin) {
if (!this._attachNumberNode(this._numberNode)) {
this._numberNode = null;
this._elementSkinApplied = '';
console.warn('[TextGrid] 数字节点无法挂回 skin', this.weapon2Id, this.location);
} else {
repaired = true;
}
}
if (this._numberNode?.isValid) {
if (!this._numberNode.active) {
this._numberNode.active = true;
repaired = true;
}
const sk = this._numberNode.getComponent(sp.Skeleton);
if (sk?.isValid) {
if (!sk.enabled) {
sk.enabled = true;
repaired = true;
}
sk.paused = false;
sk.timeScale = 1;
sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME);
if (this._elementSkinApplied) {
this._playElementSkin(this._numberNode, this._elementSkinApplied);
}
}
// 数字就绪后再关 skin 自带 Skeleton,避免修失败只剩 ebg
const skinSk = skin?.getComponent(sp.Skeleton);
if (skinSk?.isValid && skinSk.enabled) {
skinSk.enabled = false;
repaired = true;
}
}
}
if (show && !this._numberNode?.isValid) {
console.warn('[TextGrid] 数字皮修复失败,回退 skin Skeleton', this.weapon2Id, this.location);
const skinSk = skin?.getComponent(sp.Skeleton);
if (skinSk?.isValid) skinSk.enabled = true;
}
if (before !== this._numberNode) repaired = true;
if (syncSkill) this._syncNumberSkill();
return repaired;
}
2 weeks ago
/** 数字 Spine 与字形 Sorting2D 对齐 */
private _ensureNumberGlyphSorting(node: Node): void {
let sorting = node.getComponent(Sorting2D);
if (!sorting) sorting = node.addComponent(Sorting2D);
sorting.sortingLayer = TEXT_GRID_BUFF_FX_SORT_LAYER;
sorting.sortingOrder = TEXT_GRID_GLYPH_SORT_ORDER;
}
/** host 下残留数字节点回池 */
private _recycleNumberChildren(host: Node | null): void {
if (!host?.isValid) return;
const children = host.children;
for (let i = children.length - 1; i >= 0; i--) {
2 weeks ago
const child = children[i];
2 weeks ago
if (!child?.isValid || child === this._numberNode) continue;
if (!this._isNumberFaceName(child.name)) continue;
this._putNumberNode(child);
3 weeks ago
}
2 weeks ago
}
/** 挂载倍率卡/属性卡预制体(battleHero/prefab/buff) */
private _applyBattleCardVisual(prefabName: string): void {
this._clearBattleCardVisual();
let node: Node | null = null;
try {
node = gg.res.getNode(BATTLE_HERO_BUFF_PATH, prefabName, GBundle.BattleHero);
} catch (e) {
console.log('[TextGrid] 产卡预制体加载异常', prefabName, String(e));
return;
}
if (!node?.isValid) {
console.log('[TextGrid] 产卡预制体缺失', prefabName);
return;
}
node.name = '_battleCardFx';
node.parent = this.node;
node.setPosition(0, 0, 0);
node.active = true;
}
/** 回收产卡预制体 */
private _clearBattleCardVisual(): void {
const child = this.node.getChildByName('_battleCardFx');
if (!child?.isValid) return;
gg.res.putNode(child);
}
2 weeks ago
/** 关掉当前数字皮并归还对象池 */
private _hideNumberNodes(): void {
const held = this._numberNode;
2 weeks ago
this._numberNode = null;
2 weeks ago
this._elementSkinApplied = '';
2 weeks ago
this._putNumberNode(held);
this._recycleNumberChildren(this._skinNode);
this._recycleNumberChildren(this.node);
2 weeks ago
}
/** 旧 skin 与当前数字皮一起显隐 */
private _setGlyphFaceActive(active: boolean): void {
if (this._skinNode?.isValid) this._skinNode.active = active;
if (!this._numberNode?.isValid) return;
this._numberNode.active = active;
if (!active) return;
const sk = this._numberNode.getComponent(sp.Skeleton);
if (sk?.isValid) sk.enabled = true;
}
/**
2 weeks ago
* 按 Hero.number 出池数字预制体并切五行皮。
2 weeks ago
* @param show 是否可见(成将盖住时仍选中节点但保持关闭)
2 weeks ago
*/
private _applyNumberGlyph(show: boolean): void {
const tile = this._entity?.tile;
if (!tile) {
this._hideNumberNodes();
return;
}
const herotype = Math.floor(Number(tile.type));
3 weeks ago
const level = this._resolveDisplayLevel();
2 weeks ago
const hero = this._heroByTypeLevel(herotype, level);
if (!hero) {
console.log('[TextGrid] 无英雄编号', this.weapon2Id, 'type=', herotype, 'level=', level);
this._hideNumberNodes();
3 weeks ago
return;
}
2 weeks ago
const num = Math.floor(Number(hero.number));
if (!(num > 0)) {
console.log('[TextGrid] Hero.number 无效', hero.id, hero.number);
this._hideNumberNodes();
3 weeks ago
return;
}
2 weeks ago
const skinName = this._elementSkinName(herotype);
if (!skinName) {
console.log('[TextGrid] 五行皮肤未定义', herotype, 'hero=', hero.id);
this._hideNumberNodes();
return;
}
2 weeks ago
const faceName = String(num);
if (this._numberNode?.isValid && this._numberNode.name === faceName && this._elementSkinApplied === skinName) {
this._ensureSkinNode();
1 week ago
if (this._skinNode?.isValid) {
this._skinNode.active = true;
if (this._numberNode.parent !== this._skinNode) {
if (!this._attachNumberNode(this._numberNode)) return;
} else {
const skinSk = this._skinNode.getComponent(sp.Skeleton);
if (skinSk?.isValid) skinSk.enabled = false;
}
}
2 weeks ago
if (this._numberNode.active !== show) this._numberNode.active = show;
1 week ago
if (show) {
const sk = this._numberNode.getComponent(sp.Skeleton);
if (sk?.isValid) {
sk.enabled = true;
sk.paused = false;
sk.timeScale = 1;
this._playElementSkin(this._numberNode, skinName);
}
}
2 weeks ago
this._syncElementVisualChannel();
return;
}
2 weeks ago
this._hideNumberNodes();
2 weeks ago
const node = this._takeNumberNode(num);
if (!node) return;
if (!this._attachNumberNode(node)) return;
2 weeks ago
this._numberNode = node;
node.active = true;
this._playElementSkin(node, skinName);
2 weeks ago
this._elementSkinApplied = skinName;
2 weeks ago
node.active = show;
2 weeks ago
this._syncElementVisualChannel();
}
/**
* 武器本地 X(wPosX[等级-1])。
*/
weaponLocalX(): number {
const arr = this.wPosX;
if (!arr?.length) return 0;
const idx = Math.max(0, Math.min(Math.floor(this.level) - 1, arr.length - 1));
return Number(arr[idx]) || 0;
}
/** 把数字 Spine / 武器 X 同步到 HeroActor 视觉通道 */
private _syncElementVisualChannel(actor?: HeroActor | null): void {
const a = actor ?? this.getComponent(HeroActor);
if (!a) return;
const sk = this._numberNode?.isValid ? this._numberNode.getComponent(sp.Skeleton) : null;
a.visualChannel.setGlyphAnim(sk?.isValid ? sk : null);
a.visualChannel.setWeaponLocalX(this.weaponLocalX());
3 weeks ago
}
2 weeks ago
/** 数字节点切五行皮并播待机 */
private _playElementSkin(node: Node, skinName: string): void {
const sk = node.getComponent(sp.Skeleton);
if (!sk?.isValid) {
console.log('[TextGrid] 数字节点缺少 Skeleton', node.name, this.weapon2Id);
return;
}
const sd = sk.skeletonData;
if (!sd?.isValid) {
2 weeks ago
sk.enabled = false;
console.error('[TextGrid] 数字节点 skeletonData 无效', node.name, this.weapon2Id);
2 weeks ago
return;
}
2 weeks ago
sk.enabled = true;
sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME);
2 weeks ago
ensureSpineSkeletonDataRuntime(sd);
if (!glyphSpineHasSkin(sk, skinName)) {
console.log('[TextGrid] 数字皮不存在', skinName, node.name, this.weapon2Id);
return;
}
sk.setSkin(skinName);
sk.setSlotsToSetupPose();
const runtime = sd.getRuntimeData?.(true);
if (runtime?.findAnimation?.('待机')) {
sk.setAnimation(0, '待机', true);
}
}
2 weeks ago
/** 按当前显示等级刷新五行数字皮 */
2 weeks ago
private _updateLevelLabel(): void {
if (!this._isElementNumberTile()) {
this._hideNumberNodes();
2 weeks ago
this._syncNumberSkill();
2 weeks ago
return;
}
1 week ago
// 背包始终显示数字;地图上仅在未被 HeroActor 盖住时显示
const show = this.location?.kind === 'bag' || !this._shouldHideGridSkin();
this._applyNumberGlyph(show);
2 weeks ago
this._syncNumberSkill();
3 weeks ago
}
/** 显示等级:以 setData 写入的 level 为准(与 tile / 组合数据同步) */
private _resolveDisplayLevel(): number {
return this.level;
}
2 weeks ago
/** 地图字块等级变更后刷新数字皮(免全量 setData) */
3 weeks ago
applyMapLevel(level: number): void {
if (!this.isValid) return;
const lv = Math.max(1, Math.floor(level) || 1);
this.level = lv;
if (this._entity) {
const comp = this._entity.composition
? { ...this._entity.composition, level: lv }
: null;
this._entity = { ...this._entity, level: lv, composition: comp };
}
this._updateLevelLabel();
this._syncSkillActiveState();
}
private _ensureDragSurface(): void {
const { width, height } = this._resolveDragSurfaceSize();
if (this._dragSurface?.isValid) {
this._dragSurface.getComponent(UITransform)?.setContentSize(width, height);
this._dragSurface.setSiblingIndex(this.node.children.length - 1);
return;
}
const surface = new Node('__dragSurface');
surface.parent = this.node;
surface.setPosition(0, 0, 0);
surface.addComponent(UITransform).setContentSize(width, height);
if (!surface.getComponent(BlockInputEvents)) surface.addComponent(BlockInputEvents);
surface.setSiblingIndex(this.node.children.length - 1);
this._dragSurface = surface;
this._bindClickSurface();
}
private _resolveDragSurfaceSize(): { width: number; height: number } {
const cell = BattleMapSpec.CELL_SIZE;
const nodeUt = this.node.getComponent(UITransform);
if (nodeUt && nodeUt.contentSize.width > 0 && nodeUt.contentSize.width <= cell * 2) {
return { width: nodeUt.contentSize.width, height: nodeUt.contentSize.height };
}
return { width: cell, height: cell };
}
2 weeks ago
/** 子节点触摸转发给拖拽面 */
private _relayTouchToDragSurface(e: EventTouch): void {
3 weeks ago
const surface = this._dragSurface;
2 weeks ago
if (!surface?.isValid) return;
e.preventSwallow = true;
surface.emit(e.type, e);
}
/**
* 绑定触摸转发。
* @param node 子节点
*/
private _bindTouchRelay(node: Node): void {
node.off(Node.EventType.TOUCH_START, this._relayTouchToDragSurface, this);
node.off(Node.EventType.TOUCH_MOVE, this._relayTouchToDragSurface, this);
node.off(Node.EventType.TOUCH_END, this._relayTouchToDragSurface, this);
node.off(Node.EventType.TOUCH_CANCEL, this._relayTouchToDragSurface, this);
node.on(Node.EventType.TOUCH_START, this._relayTouchToDragSurface, this);
node.on(Node.EventType.TOUCH_MOVE, this._relayTouchToDragSurface, this);
node.on(Node.EventType.TOUCH_END, this._relayTouchToDragSurface, this);
node.on(Node.EventType.TOUCH_CANCEL, this._relayTouchToDragSurface, this);
}
private _forwardChildTouchToSurface(): void {
if (!this._dragSurface) return;
const children = this.node.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (!child?.isValid || child === this._dragSurface) continue;
this._bindTouchRelay(child);
}
const skin = this._skinNode;
if (!skin?.isValid) return;
const nested = skin.children;
for (let i = 0; i < nested.length; i++) {
const child = nested[i];
if (!child?.isValid) continue;
this._bindTouchRelay(child);
3 weeks ago
}
}
2 weeks ago
/** 按 Text.icon 刷新 skin 节点 Spine */
3 weeks ago
private _applySkinIcon(iconName: string): void {
const name = this._normalizeIconName(iconName);
if (!name) return;
this._ensureSkinNode();
const skinNode = this._skinNode;
if (!skinNode?.isValid) {
console.error('[TextGrid] skin 节点缺失', name);
return;
}
if (this._skinIconName === name) {
if (!this._shouldHideGridSkin()) {
skinNode.active = true;
}
this.resetSkinPresentation();
const sk = this._getSkinSkeleton();
if (sk) sk.enabled = true;
this._applySkinSpineIcon(name);
return;
}
this._skinIconName = name;
if (!this._shouldHideGridSkin()) {
skinNode.active = true;
}
const sk = this._getSkinSkeleton();
if (sk) sk.enabled = true;
this.resetSkinPresentation();
this._applySkinSpineIcon(name);
this._ensureDragSurface();
this._forwardChildTouchToSurface();
}
/** skin 节点 Spine 切 Text.icon 并播待机 */
private _applySkinSpineIcon(iconName: string): void {
const sk = this._getSkinSkeleton();
if (!sk?.isValid) {
console.log('[TextGrid] skin 节点缺少 sp.Skeleton', iconName, this.weapon2Id);
return;
}
sk.enabled = true;
// 池化复用后强制 REALTIME,避免 SHARED/PRIVATE 缓存与换肤叠出空网格
sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME);
if (!applyGlyphSpineSkin(sk, iconName)) {
console.log('[TextGrid] Spine 皮肤未能应用', iconName, this.weapon2Id);
}
if (sk.skeletonData) ensureSpineSkeletonDataRuntime(sk.skeletonData);
const runtime = sk.skeletonData?.getRuntimeData?.(true);
if (runtime?.findAnimation?.('待机')) {
sk.setAnimation(0, '待机', true);
}
}
private _normalizeIconName(icon: unknown): string | null {
if (typeof icon === 'string') {
const name = icon.trim();
return name || null;
}
return null;
}
/** 同步英雄表现:隐藏格子皮 / 显示 HeroActor */
syncHeroPresentation(): void {
this._finishSkinPresentation();
}
private _finishSkinPresentation(): void {
this._heroActorRetry = 0;
this._syncSkillActiveState();
GEvent.Ins.emit(GEvent.UpdateBestPlace);
}
/** 本格属于已成将的 HeroActor 组合(不论英雄皮是否已挂上) */
private _isHeroActorComposeCell(): boolean {
const composition = this._getComposition();
if (composition == null || !isHeroActorWeapon1(composition.weapon1Id)) return false;
const loc = this.location;
if (loc?.kind !== 'map') return false;
return this._compHasGrid(composition, loc.gridIndex);
}
/** 本格已编入 HeroActor 且英雄字形已挂上:藏格子皮。预制体失败/异步未到时仍显示原单字 */
private _shouldHideSkinForHeroActor(): boolean {
if (!this._isHeroActorComposeCell()) return false;
1 week ago
const loc = this.location;
if (loc?.kind !== 'map') return false;
// 飞行透明隐藏中须露字皮,否则拖回背包只剩 ebg
if (HeroActorHost.instance.isFlightHiddenAtGrid(loc.gridIndex)) return false;
3 weeks ago
const composition = this._getComposition();
return composition != null && this._isHeroDisplayOnThisCell(composition);
}
2 weeks ago
/** 格子皮应藏:HeroActor 已盖住当前格 */
3 weeks ago
private _shouldHideGridSkin(): boolean {
2 weeks ago
return this._shouldHideSkinForHeroActor();
3 weeks ago
}
/** 英雄皮是否盖在当前地图格(交换拆开后必须为 false) */
private _isHeroDisplayOnThisCell(composition: ComposedUnit): boolean {
const loc = this.location;
if (loc?.kind !== 'map') return false;
return HeroActorHost.instance.isDisplayVisibleAtGrid(composition, loc.gridIndex);
}
2 weeks ago
/** 地图字块已组合为攻击单位 → 激活;拆开或未组合 → 关闭;五行字切换 Spine 皮肤 */
3 weeks ago
private _syncSkillActiveState(): void {
this._syncShadow();
2 weeks ago
this._syncNumberSkill();
3 weeks ago
const composition = this._getComposition();
const active = this._isMapComposedAttackUnit();
2 weeks ago
// 五行数字与玩家同一套 Skill 通道;轻量路径只给非数字(避免对账重刷待机皮)
if (this._aiMapLite && !this._isElementNumberTile()) {
3 weeks ago
this._syncAiMapLitePresentation(composition, active);
return;
}
// 正面 buff 特效由 CocosTowerPositiveBuffPresenter 按 AttributeComp 同步,勿在此清
const heroActor = composition != null && isHeroActorWeapon1(composition.weapon1Id);
const heroGlyph = this._skinIconName != null && HERO_GLYPH_RESOURCES.has(this._skinIconName);
if (heroActor || heroGlyph) {
this._syncHeroActorState(heroActor ? composition : null, active);
return;
}
const skin = this._skinNode?.isValid ? this._skinNode : null;
2 weeks ago
const skill = skin?.getComponent(Skill) ?? skin?.getComponentInChildren(Skill) ?? null;
3 weeks ago
if (!skill) {
2 weeks ago
this._setGlyphFaceActive(!this._shouldHideSkinForHeroActor());
3 weeks ago
return;
}
const w2Type = WeaponComposeConfigProvider.instance.getWeapon2Type(this.weapon2Id) as Weapon2TileType;
const entityType = this._entity?.tileType as Weapon2TileType | undefined;
2 weeks ago
const useGlyphSkin = isElementGlyphType(w2Type) || isElementGlyphType(Number(entityType));
3 weeks ago
2 weeks ago
// 激活态未变:升级/重建只刷等级皮,勿 abort(嫦娥领域等)
3 weeks ago
if (skill.isActive === active) {
2 weeks ago
if (useGlyphSkin && active) {
3 weeks ago
skill.applyComposeTextSkin(true, this._resolveDisplayLevel());
} else if (!active) {
this.resetSkinPresentation();
// 背包/未上场:强制待机(预制体 defaultAnimation 可能是「攻击」)
2 weeks ago
if (useGlyphSkin) {
3 weeks ago
skill.applyComposeTextSkin(false, this._resolveDisplayLevel());
}
skill.onIdle();
}
return;
}
const activating = active && !skill.isActive;
const deactivating = !active && skill.isActive;
// 仅失活时打断;首次激活时 PresentationCue 可能同帧已开始,abort 会 cancel 掉首刀
if (deactivating) {
skill.abortPresentation();
this.resetSkinPresentation();
}
skill.onSetActiveAtk(active);
2 weeks ago
if (useGlyphSkin) {
3 weeks ago
skill.applyComposeTextSkin(active, this._resolveDisplayLevel());
}
if (deactivating || !active) {
skill.onIdle();
} else if (!activating || !skill.isPresentationBusy()) {
skill.onIdle();
}
}
/** HeroActor 对账重试(DeploySync 可能晚于 TextGrid.setData) */
private _heroActorRetry = 0;
2 weeks ago
/** 递增作废未触发的 HeroActor 重试回调(换字后勿再开 skin) */
3 weeks ago
private _heroActorRetryToken = 0;
/**
2 weeks ago
* AI 地图轻量对账:HeroActor 格只藏/显格子皮;散字只套 icon。
3 weeks ago
* @param composition 合成体
* @param active 是否已组成攻击单位
*/
private _syncAiMapLitePresentation(composition: ComposedUnit | null, active: boolean): void {
const heroActor = composition != null && isHeroActorWeapon1(composition.weapon1Id);
if (heroActor) {
if (this._skinNode?.isValid) this._skinNode.active = !this._shouldHideSkinForHeroActor();
this._updateLevelLabel();
return;
}
if (this._skinNode?.isValid) this._skinNode.active = true;
if (this._skinIconName) this._applySkinSpineIcon(this._skinIconName);
this._updateLevelLabel();
}
/** setData 换字时作废挂起的 HeroActor 对账 */
private _invalidateHeroActorRetry(): void {
this._heroActorRetryToken++;
this._heroActorRetry = 0;
}
private _syncHeroActorState(composition: ComposedUnit | null, active: boolean): void {
const loc = this.location;
if (active && composition) {
const map = gg.game.CurentBattle?.Map;
let entityId: number | null = null;
if (map) {
entityId = loc?.kind === 'map'
? TowerQueryService.instance.getRuntimeByGridIndex(loc.gridIndex)?.entityId ?? null
: null;
const enemyCamp = this._isEnemyCampMapCell();
HeroActorHost.instance.ensureVisual(composition, map, entityId, enemyCamp);
}
const useHeroActor = isHeroActorWeapon1(composition.weapon1Id);
const displayReady = this._isHeroDisplayOnThisCell(composition);
2 weeks ago
this._setGlyphFaceActive(!this._shouldHideSkinForHeroActor());
3 weeks ago
// 皮可见 ≠ 已挂 Skill:entityId 晚到时必须继续对账,否则有皮无武器、不攻击
const skillReady = !useHeroActor
|| loc?.kind !== 'map'
|| (entityId != null && !!HeroActorHost.instance.getActor(entityId));
if (!displayReady || !skillReady) {
const delay = this._heroActorRetry < 12 ? 0 : (this._heroActorRetry < 48 ? 0.08 : 0.25);
if (this._heroActorRetry < 140) {
this._heroActorRetry++;
const token = this._heroActorRetryToken;
this.scheduleOnce(() => {
if (!this.isValid || token !== this._heroActorRetryToken) return;
this._syncHeroActorState(composition, active);
}, delay);
} else if (useHeroActor) {
ClientErrorReporter.report(
'TextGrid_HeroActorNotReady',
`w1=${composition.weapon1Id}|display=${displayReady}|entity=${entityId}`,
);
}
} else {
this._heroActorRetry = 0;
}
this._syncShadow();
return;
}
this._heroActorRetry = 0;
2 weeks ago
this._setGlyphFaceActive(true);
this.resetSkinPresentation();
this._resetGlyphSkinToBagIdle();
3 weeks ago
// 仅当地图格曾属于已激活组合时才拆 HeroActor;散落组合字(如单独「空」)勿误伤邻格
if (loc?.kind === 'map' && this._shouldDestroyHeroAtDeactivate(loc.gridIndex)) {
HeroActorHost.instance.destroyAtGrid(loc.gridIndex);
}
this._syncShadow();
}
/** 本节点是否仍挂在该地图格上(对象池幽灵 / 飞行节点不算) */
private _isLiveMapOccupant(gridIndex: number): boolean {
const map = gg.game.CurentBattle?.Map;
const parent = map?.getMapGridByIndex?.(gridIndex)?.node;
return !!parent?.isValid && this.node?.parent === parent;
}
/**
* 失活时是否应销毁该格 HeroActor。
* 组合仍在场时邻格快照未带上 composition 不得拆整座 Actor。
*/
private _shouldDestroyHeroAtDeactivate(gridIndex: number): boolean {
if (!this._isLiveMapOccupant(gridIndex)) return false;
if (!HeroActorHost.instance.hasActorAtGrid(gridIndex)) return false;
if (this._findComposition()) return false;
return true;
}
/** 收回背包:恢复 Text.icon 待机皮 */
private _resetGlyphSkinToBagIdle(): void {
if (!this._skinIconName) return;
this._applySkinSpineIcon(this._skinIconName);
}
2 weeks ago
/**
* 地图五行数字挂 HeroActor:普攻走 Skill 表,武器播 Skill.animation。
* @param heroId Hero.id
* @param entityId 塔实体
*/
ensureNumberSkill(heroId: number, entityId: EntityId): Skill | null {
if (!this._isElementNumberTile() || this.location?.kind !== 'map') return null;
const hid = Math.floor(Number(heroId) || 0);
if (!(hid > 0)) return null;
let actor = this.getComponent(HeroActor);
if (!actor) actor = this.addComponent(HeroActor);
actor.setHeroId(hid);
actor.bindEntity(entityId);
2 weeks ago
this._syncElementVisualChannel(actor);
2 weeks ago
if (!actor.isActive) {
actor.isActive = true;
actor.onUpdateActive(true);
2 weeks ago
} else if (!actor.isPresentationBusy() && !actor.visualChannel.isCastOrRestHold) {
2 weeks ago
actor.onIdle();
}
return actor;
}
/** 收回背包或换非五行字时拆掉数字 HeroActor */
releaseNumberSkill(): void {
const actor = this.getComponent(HeroActor);
if (!actor) return;
actor.isActive = false;
actor.onUpdateActive(false);
actor.visualChannel.release();
actor.destroy();
}
/** 地图激活数字:对账 HeroActor 与武器 */
private _syncNumberSkill(): void {
const onMap = this._isElementNumberTile() && this.location?.kind === 'map';
if (!onMap) {
this.releaseNumberSkill();
return;
}
const herotype = Math.floor(Number(this._entity?.tile?.type));
const hero = this._heroByTypeLevel(herotype, this._resolveDisplayLevel());
if (!hero) {
this.releaseNumberSkill();
return;
}
const loc = this.location;
2 weeks ago
if (loc?.kind !== 'map') return;
const entityId = this._resolveMapEntityId();
2 weeks ago
if (entityId == null) return;
this.ensureNumberSkill(hero.id, entityId);
}
2 weeks ago
/**
* 地图格对应塔实体:己方走本机,AI 轻量字走对手。
*/
private _resolveMapEntityId(): EntityId | null {
const loc = this.location;
if (loc?.kind !== 'map') return null;
const battle = gg.game.CurentBattle;
if (!battle?.session || !battle.Map) return null;
const player = this._aiMapLite || this._isEnemyCampMapCell()
? battle.remotePlayer
: battle.player;
if (!player) return null;
return findEntityIdByGridIndex(loc.gridIndex, battle.session, battle.Map, player);
}
3 weeks ago
/** 当前字块在地图上且属于有效组合(含单格 Weapon1) */
private _isMapComposedAttackUnit(): boolean {
const loc = this.location;
if (loc?.kind !== 'map') return false;
return this._getComposition() != null;
}
}