import { _decorator, assetManager, Color, Component, instantiate, isValid, Node, Sprite, SpriteFrame, tween, Tween } from 'cc'; import { GBundle } from '../../../game/ConfigRes'; import { hideGridFallSpine, hideGridOpenSpine, hideGridUpgradeSpine, playGridFallSpineDeferred, playGridOpenSpineDeferred, playGridUpgradeSpineDeferred, } from '../../view/GridUpgradeSpineEffect'; import { DEV, EDITOR } from 'cc/env'; const { ccclass, property, executeInEditMode } = _decorator; /** 拖拽引导:可走格被阻挡时的红色 tint */ const BLOCKED_WALK_TINT = new Color(255, 96, 96, 255); /** 铲开挖:格子 dig 遮罩 fallback 渐隐时长(秒;正常走 Spine timeScale 推算) */ const OPEN_GRID_FILL_DURATION_SEC = 1.2; @ccclass('MapGrid') @executeInEditMode(true) export class MapGrid extends Component { @property({ displayName: '是否可以通行' }) get isCanWalk(): boolean { return this._isCanWalk; } set isCanWalk(value: boolean) { this._isCanWalk = value; this.updateGrid(); } @property private _isCanWalk: boolean = true; @property({ displayName: '是否可放置文字' }) get isCanPutText(): boolean { return this._isCanPutText; } set isCanPutText(value: boolean) { this._isCanPutText = value; this.updateGrid(); } @property private _isCanPutText: boolean = false; @property({ displayName: '是否可摆放格子' }) get isCanPutGrid(): boolean { return this._isCanPutGrid; } set isCanPutGrid(value: boolean) { this._isCanPutGrid = value; this.updateGrid(); } @property private _isCanPutGrid: boolean = true; /**是否可被开挖 */ @property({ displayName: '是否可被开挖' }) get isCanBeDig(): boolean { return this._isCanBeDig; } set isCanBeDig(value: boolean) { this._isCanBeDig = value; this.updateGrid(); } @property private _isCanBeDig: boolean = false; get isCanRemove(): boolean { return this._isCanRemove; } set isCanRemove(value: boolean) { this._isCanRemove = value; this.updateGrid(); } @property private _isCanRemove: boolean = false; @property({ displayName: '是否可以被雷击' }) get isCanBeThunder(): boolean { return this._isCanBeThunder; } set isCanBeThunder(value: boolean) { this._isCanBeThunder = value; this.updateGrid(); } @property private _isCanBeThunder: boolean = false; //@property({ displayName: '是否最佳摆放点' }) get isBestPlace(): boolean { return this._isBestPlace; } set isBestPlace(value: boolean) { this._isBestPlace = value; this.updateGrid(); } @property private _isBestPlace = false; /** 是否是免费推荐点 */ //@property({ displayName: '是否免费推荐点' }) get isFreePlace(): boolean { return this._isFreePlace; } set isFreePlace(value: boolean) { this._isFreePlace = value; this.updateGrid(); } @property private _isFreePlace = false; /** 阵营区域ID * 该ID可用于多种战斗模式,策划会根据战斗模式需求配置对应的地图和关卡数据,比如: * 1.普通闯关模式和部分副本模式只需要一个阵营,所用到的地图就只会配置1一个阵营ID * 2.PVP、PVE副本和其他特殊副本会配置多个阵营ID。 * 3.正常情况下,普通模式和PVE模式1代表己方,2代表敌方。 */ @property({ displayName: '阵营区域ID' }) get CampAreaID(): number { return this._CampAreaID; } set CampAreaID(value: number) { this._CampAreaID = value; this.updateGrid(); } @property private _CampAreaID = 1; /** * 玩家主区域 ID(与阵营区域 ID 独立)。 * 阵营格可跨区域;本字段只表示该格属于哪边主区域。 * 当前仅万箭齐发用:只选己方区域内的敌方阵营通行格。 */ @property({ displayName: '区域ID' }) get AreaID(): number { return this._AreaID; } set AreaID(value: number) { this._AreaID = value; this.updateGrid(); } @property private _AreaID = 1; /**怪物移速倍速 */ @property({ displayName: '怪物移速倍速' }) get MonsterSpeed(): number { return this._MonsterSpeed; } set MonsterSpeed(value: number) { this._MonsterSpeed = value; this.updateGrid(); } @property private _MonsterSpeed = 1; /** 动态推荐点已占用(由 MapManager.updateRecommendPoints 设置) */ ShowGuidePos = false; /** 蜘蛛精蛛网占用(禁止 type3 放置) */ hasSpiderWeb = false; @property({ type: SpriteFrame }) walkSprite: SpriteFrame = null; @property({ type: SpriteFrame }) putSprite: SpriteFrame = null; @property({ type: SpriteFrame }) digSprite: SpriteFrame = null; /**是否是引导状态(拖拽时可落子格:仅控制 addIcon 绿色加号) */ get isGuideStatus(): boolean { return this._isGuideStatus; } set isGuideStatus(value: boolean) { this._isGuideStatus = value; if (value) this._isBlockedGuideStatus = false; this.updateAddIcon(); } private _isGuideStatus: boolean = false; /** 拖拽引导:不可落子但相关的可走格(仅 type3 红底,不显示加号) */ get isBlockedGuideStatus(): boolean { return this._isBlockedGuideStatus; } set isBlockedGuideStatus(value: boolean) { this._isBlockedGuideStatus = value; if (value) this._isGuideStatus = false; this.updateAddIcon(); } private _isBlockedGuideStatus = false; /** * 运行时显示可走格底图(与编辑器 isCanWalk + walkSprite 一致)。 * 拖拽时由 MapDragGuideHelper 仅对 type3 可落子可走格打开。 */ get showWalkSprite(): boolean { return this._showWalkSprite; } set showWalkSprite(value: boolean) { if (this._showWalkSprite === value) return; this._showWalkSprite = value; if (value) this._showBlockedWalkSprite = false; this.updateGrid(); } private _showWalkSprite = false; /** 拖拽引导:type3 不可落子的可走格(walkSprite 红色 tint) */ get showBlockedWalkSprite(): boolean { return this._showBlockedWalkSprite; } set showBlockedWalkSprite(value: boolean) { if (this._showBlockedWalkSprite === value) return; this._showBlockedWalkSprite = value; if (value) this._showWalkSprite = false; this.updateGrid(); } private _showBlockedWalkSprite = false; /** 拖拽悬停选中(仅当前目标格显示 select 节点) */ get isSelectStatus(): boolean { return this._isSelectStatus; } set isSelectStatus(value: boolean) { this._isSelectStatus = value; if (value) this._isRedSelectStatus = false; this.updateSelect(); } private _isSelectStatus = false; /** 拖拽悬停不可放置格(显示 redSelect) */ get isRedSelectStatus(): boolean { return this._isRedSelectStatus; } set isRedSelectStatus(value: boolean) { this._isRedSelectStatus = value; if (value) this._isSelectStatus = false; this.updateSelect(); } private _isRedSelectStatus = false; /** 行 */ Row = 0; /** 列 */ Col = 0; /** 网格索引 */ Index = 0; statusSprite: Sprite = null; /** 开挖遮罩(digSprite + fillRange,盖在 statusSprite 之上) */ iconNode: Node | null = null; iconSprite: Sprite | null = null; addIcon: Node = null; addIconSprite: Sprite | null = null; private _addIconGreenFrame: SpriteFrame | null = null; selectNode: Node = null; redSelectNode: Node | null = null; /** openSpine 是否已播完 */ private _openSpineDone = false; /** fillRange 渐隐是否已播完 */ private _openFillDone = false; /** dig 遮罩 fill 渐隐是否进行中(rebuild 可能停 tween 却不清遮罩) */ private _openFillPlaying = false; /** 封格 dig 遮罩 fill 0→1 是否进行中 */ private _sealFillCoverPlaying = false; /** playOpenEffect 完成回调 */ private _openEffectComplete: (() => void) | null = null; protected onLoad(): void { this.statusSprite = this.node.getComponent(Sprite) || this.node.addComponent(Sprite); // 格子坐标由预制体绝对定位;禁止 TRIMMED/RAW 随 spriteFrame 改 contentSize,否则父节点 Layout 会重排整图 this.statusSprite.sizeMode = Sprite.SizeMode.CUSTOM; this.statusSprite.type = Sprite.Type.SIMPLE; this.iconNode = this.node.getChildByName('icon'); this.iconSprite = this.iconNode?.getComponent(Sprite) ?? null; if (this.iconSprite?.isValid) { this.iconSprite.sizeMode = Sprite.SizeMode.CUSTOM; } if (this.iconNode?.isValid) this.iconNode.active = false; this.addIcon = this.node.getChildByName('addIcon'); this.addIconSprite = this.addIcon?.getComponent(Sprite) ?? null; this._addIconGreenFrame = this.addIconSprite?.spriteFrame ?? null; this.selectNode = this.node.getChildByName('select'); this.redSelectNode = this.node.getChildByName('redSelect'); hideGridUpgradeSpine(this.node); hideGridFallSpine(this.node); hideGridOpenSpine(this.node); } /** 合成/升级成功时播放 spine「升级效果」,播完自动隐藏 */ playUpgradeEffect(): void { playGridUpgradeSpineDeferred(this.node); } /** 文字落到空格时播放 fallSpine「animation」,播完自动隐藏 */ playFallEffect(): void { playGridFallSpineDeferred(this.node); } /** 铲开挖未开启格:openSpine 冒烟 + dig 遮罩 fillRange 1→0(并行,非铲链) */ playOpenEffect(onComplete?: () => void): void { this._openEffectComplete = onComplete ?? null; this._openSpineDone = false; this._openFillDone = false; this.updateGrid(); this._playOpenGridFillReveal(true); playGridOpenSpineDeferred(this.node, this._onOpenSpineRevealDone.bind(this)); } /** * 铲开挖链:仅 fillRange 1→0(与铲 Spine 并行;结束不触发开格完成回调)。 * @param durationSec 渐隐时长;省略则用 fallback */ playShovelDigFillReveal(durationSec?: number): void { this.updateGrid(); this._playOpenGridFillReveal(false, durationSec); } /** * 铲开挖链:铲 Spine 结束后播 openSpine 冒烟(onComplete 可选,仅表示冒烟播完)。 */ playOpenSpineSmoke(onComplete?: () => void): void { this._openEffectComplete = onComplete ?? null; this.updateGrid(); playGridOpenSpineDeferred(this.node, this._onOpenSpineSmokeDone.bind(this)); } /** openSpine 冒烟播完(playOpenEffect 并行链) */ private _onOpenSpineRevealDone(): void { this._openSpineDone = true; this._tryFinishOpenEffect(); } /** openSpine 冒烟播完(铲开挖链) */ private _onOpenSpineSmokeDone(): void { const cb = this._openEffectComplete; this._openEffectComplete = null; this.updateGrid(); cb?.(); } /** dig 遮罩 fillRange 渐隐播完 */ private _onOpenGridFillRevealDone(finishOpenEffect: boolean): void { this._openFillPlaying = false; this._clearOpenGridFillOverlay(); if (!finishOpenEffect) return; this._openFillDone = true; this._tryFinishOpenEffect(); } /** 冒烟与 fill 均结束后回调 */ private _tryFinishOpenEffect(): void { if (!this._openSpineDone || !this._openFillDone) return; const cb = this._openEffectComplete; this._openEffectComplete = null; this.updateGrid(); cb?.(); } /** * 千里眼封格:Put 皮上叠 dig 遮罩,fillRange 0→1(铲子开挖的反向)。 * @param durationSec 渐显时长;省略则用与开挖相同的 fallback */ playSealDigFillCover(durationSec?: number): void { const overlay = this.iconSprite; const overlayNode = this.iconNode; if (!overlay?.isValid || !overlayNode?.isValid || !this.digSprite || !this.putSprite) { this.updateGrid(); return; } const fillSec = durationSec != null && durationSec > 0 ? durationSec : OPEN_GRID_FILL_DURATION_SEC; Tween.stopAllByTarget(overlay); this._sealFillCoverPlaying = true; if (this.statusSprite?.isValid) this.statusSprite.spriteFrame = this.putSprite; overlayNode.active = true; overlayNode.setSiblingIndex(this.node.children.length - 1); overlay.spriteFrame = this.digSprite; overlay.type = Sprite.Type.FILLED; overlay.fillType = Sprite.FillType.HORIZONTAL; overlay.fillStart = 0; overlay.fillRange = 0; tween(overlay) .to(fillSec, { fillRange: 1 }) .call(this._onSealDigFillCoverDone.bind(this)) .start(); } /** 封格遮罩渐显结束:露出可开挖格 */ private _onSealDigFillCoverDone(): void { this._sealFillCoverPlaying = false; this._clearOpenGridFillOverlay(); this.updateGrid(); } /** 播放 dig 遮罩 fillRange 1→0,露出下方可放置格 */ private _playOpenGridFillReveal(finishOpenEffect: boolean, durationSec?: number): void { const overlay = this.iconSprite; const overlayNode = this.iconNode; if (!overlay?.isValid || !overlayNode?.isValid || !this.digSprite) { this._onOpenGridFillRevealDone(finishOpenEffect); return; } const fillSec = durationSec != null && durationSec > 0 ? durationSec : OPEN_GRID_FILL_DURATION_SEC; Tween.stopAllByTarget(overlay); this._openFillPlaying = true; overlayNode.active = true; overlayNode.setSiblingIndex(this.node.children.length - 1); overlay.spriteFrame = this.digSprite; overlay.type = Sprite.Type.FILLED; overlay.fillType = Sprite.FillType.HORIZONTAL; overlay.fillStart = 0; overlay.fillRange = 1; tween(overlay) .to(fillSec, { fillRange: 0 }) .call(this._onOpenGridFillRevealDone.bind(this, finishOpenEffect)) .start(); } /** * 从当前 fillRange 续播 dig 遮罩渐隐(rebuild 打断 tween 后用)。 * @param finishOpenEffect 是否计入 playOpenEffect 完成条件 * @param fromRange 起始 fillRange */ private _resumeOpenGridFillReveal(finishOpenEffect: boolean, fromRange: number): void { const overlay = this.iconSprite; const overlayNode = this.iconNode; if (!overlay?.isValid || !overlayNode?.isValid || !this.digSprite) { this._onOpenGridFillRevealDone(finishOpenEffect); return; } const from = Math.max(0, Math.min(1, fromRange)); if (from <= 0.01) { this._onOpenGridFillRevealDone(finishOpenEffect); return; } Tween.stopAllByTarget(overlay); this._openFillPlaying = true; overlayNode.active = true; overlayNode.setSiblingIndex(this.node.children.length - 1); overlay.spriteFrame = this.digSprite; overlay.type = Sprite.Type.FILLED; overlay.fillType = Sprite.FillType.HORIZONTAL; overlay.fillStart = 0; overlay.fillRange = from; const duration = OPEN_GRID_FILL_DURATION_SEC * from; tween(overlay) .to(duration, { fillRange: 0 }) .call(this._onOpenGridFillRevealDone.bind(this, finishOpenEffect)) .start(); } /** 清理开挖遮罩 tween 与节点 */ private _clearOpenGridFillOverlay(): void { this._openFillPlaying = false; this._sealFillCoverPlaying = false; const overlay = this.iconSprite; if (overlay?.isValid) { Tween.stopAllByTarget(overlay); overlay.fillRange = 0; overlay.type = Sprite.Type.SIMPLE; overlay.spriteFrame = null; } if (this.iconNode?.isValid) { this.iconNode.active = false; } } /** 中断/跳过开挖表现时清理 openSpine 与 dig 遮罩(AI Dig→Put 必调) */ clearDigOpenFx(): void { this._abortOpenEffect(); this.updateGrid(); } /** * 铲挖收尾 rebuild 后修复:续播被打断的 fill,或清掉残留 dig 遮罩并刷回 Put 皮。 */ repairShovelDigVisual(): void { if (this._sealFillCoverPlaying) return; const overlay = this.iconSprite; const overlayNode = this.iconNode; if (this._openFillPlaying && overlay?.isValid && overlayNode?.isValid && overlayNode.active) { this._resumeOpenGridFillReveal(false, overlay.fillRange); this.updateGrid(); return; } if (overlayNode?.isValid && overlayNode.active) { console.log('[MapGrid] Put 格 dig 遮罩残留,强制清理', this.Index); } this.clearDigOpenFx(); } /** 中断进行中的开挖表现 */ private _abortOpenEffect(): void { this._clearOpenGridFillOverlay(); hideGridOpenSpine(this.node); this._openSpineDone = true; this._openFillDone = true; this._openEffectComplete = null; } protected onEnable(): void { this.updateGrid(); } protected onDisable(): void { this._abortOpenEffect(); if (this.statusSprite) this.statusSprite.spriteFrame = null; } updateGrid() { if (this.statusSprite) { if (this.isCanWalk) { if (this._showWalkSprite) { this.statusSprite.spriteFrame = this.walkSprite; this.statusSprite.color = Color.WHITE; } else if (this._showBlockedWalkSprite) { this.statusSprite.spriteFrame = this.walkSprite; this.statusSprite.color = BLOCKED_WALK_TINT; } else { this.statusSprite.spriteFrame = EDITOR ? this.walkSprite : null; this.statusSprite.color = Color.WHITE; } } else if (this.isCanPutText && !this.isCanWalk) { // 可放置优先:dig+putText 双标显示 Put 皮 this.statusSprite.spriteFrame = this.putSprite; // fill 未在播却挂着 dig 遮罩 → 白格看起来像未开挖 if (!this._openFillPlaying && !this._sealFillCoverPlaying && this.iconNode?.isValid && this.iconNode.active) { console.log('[MapGrid] Put 格残留 dig 遮罩,随 updateGrid 清理', this.Index); this._clearOpenGridFillOverlay(); } } else if (this.isCanBeDig && !this.isCanPutText && !this.isCanWalk) { this.statusSprite.spriteFrame = this._sealFillCoverPlaying && this.putSprite ? this.putSprite : this.digSprite; } else { this.statusSprite.spriteFrame = null; } } this.updateAddIcon(); } /** 格子上的 addIcon:拖拽引导时仅在可落子格显示绿色加号 */ private updateAddIcon(): void { if (!this.addIcon || !isValid(this.addIcon)) return; const show = this.isGuideStatus; this.addIcon.active = show; if (!show || !this.addIconSprite) return; if (this._addIconGreenFrame) { this.addIconSprite.spriteFrame = this._addIconGreenFrame; } } /** 格子上的 select / redSelect:背包拖线悬停目标格时显示 */ private updateSelect(): void { if (this.selectNode && isValid(this.selectNode)) { this.selectNode.active = this._isSelectStatus; } const redSelect = this._ensureRedSelectNode(); if (redSelect && isValid(redSelect)) { redSelect.active = this._isRedSelectStatus; } } /** mapGrid 预制体无 redSelect 时,运行时从 select 克隆并替换为红色加号图 */ private _ensureRedSelectNode(): Node | null { if (this.redSelectNode?.isValid) return this.redSelectNode; this.redSelectNode = this.node.getChildByName('redSelect'); if (this.redSelectNode?.isValid) return this.redSelectNode; if (!this.selectNode?.isValid) return null; this.redSelectNode = instantiate(this.selectNode); this.redSelectNode.name = 'redSelect'; this.redSelectNode.parent = this.node; this.redSelectNode.active = false; const spr = this.redSelectNode.getComponent(Sprite); if (spr) { const bundle = assetManager.getBundle(GBundle.BattleGame); const redSf = bundle?.get('texture/ui/红色加号/spriteFrame', SpriteFrame) as SpriteFrame | null; if (redSf) spr.spriteFrame = redSf; } return this.redSelectNode; } }