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.

673 lines
24 KiB

3 weeks ago
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;
2 weeks ago
/**
* 是否参与解锁。
* 解锁区域 ID 大于 0 时按区域依次解锁;为 0 时仍是铲子可开挖格。
*/
@property({ displayName: '是否可被解锁' })
3 weeks ago
get isCanBeDig(): boolean {
return this._isCanBeDig;
}
set isCanBeDig(value: boolean) {
this._isCanBeDig = value;
this.updateGrid();
2 weeks ago
this._refreshSiblingUnlockSkin();
3 weeks ago
}
@property
private _isCanBeDig: boolean = false;
2 weeks ago
/** 解锁区域 ID。同一 ID 的格子一起解锁,必须先完成较小的 ID。条件读关卡表 unlockland。 */
2 weeks ago
@property({ displayName: '解锁区域ID' })
get unlockAreaId(): number {
return this._unlockAreaId;
}
set unlockAreaId(value: number) {
this._unlockAreaId = value;
this.updateGrid();
this._refreshSiblingUnlockSkin();
}
@property
private _unlockAreaId = 0;
/** 当前正在解锁的区域(由 MapAreaUnlock 写入) */
get waitingUnlock(): boolean {
return this._waitingUnlock;
}
set waitingUnlock(value: boolean) {
if (this._waitingUnlock === value) return;
this._waitingUnlock = value;
this.updateGrid();
}
private _waitingUnlock = false;
/**
* 未解锁的区域格:勾选可解锁且区域 ID 大于 0,还不能放文字。
*/
get isAreaUnlock(): boolean {
return this._isCanBeDig && this._unlockAreaId > 0 && !this._isCanPutText && !this._isCanWalk;
}
3 weeks ago
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;
2 weeks ago
@property({ type: SpriteFrame })
waitUnlockedSprite: SpriteFrame = null;
3 weeks ago
/**是否是引导状态(拖拽时可落子格:仅控制 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) {
2 weeks ago
// 解锁区域已开:Dig 皮;普通可放置:Put 皮
this.statusSprite.spriteFrame = this._unlockAreaId > 0 ? this.digSprite : this.putSprite;
3 weeks ago
// 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) {
2 weeks ago
if (this._sealFillCoverPlaying && this.putSprite) {
this.statusSprite.spriteFrame = this.putSprite;
} else if (this._showsWaitUnlock()) {
2 weeks ago
// 当前可解锁区域
2 weeks ago
this.statusSprite.spriteFrame = this.waitUnlockedSprite;
2 weeks ago
} else if (this._unlockAreaId > 0) {
// 还没轮到的解锁区域
this.statusSprite.spriteFrame = this.putSprite;
2 weeks ago
} else {
this.statusSprite.spriteFrame = this.digSprite;
}
3 weeks ago
}
else {
this.statusSprite.spriteFrame = null;
}
}
this.updateAddIcon();
}
2 weeks ago
/** 区域 ID 变化后,刷新同层其它解锁格的等待图 */
private _refreshSiblingUnlockSkin(): void {
const parent = this.node.parent;
if (!parent) return;
const children = parent.children;
for (let i = 0; i < children.length; i++) {
if (children[i] === this.node) continue;
const grid = children[i].getComponent(MapGrid);
if (!grid?.isCanBeDig || grid.unlockAreaId <= 0) continue;
grid.updateGrid();
}
}
/**
2 weeks ago
* 当前可解锁区域显示 Wait Unlocked;未轮到的区域用 Put。
2 weeks ago
* 编辑器里取同层最小解锁区域 ID,方便直接看第一块。
*/
private _showsWaitUnlock(): boolean {
if (this._waitingUnlock) return true;
if (!EDITOR || !this.isAreaUnlock) return false;
const parent = this.node.parent;
if (!parent) return false;
let minId = this._unlockAreaId;
const children = parent.children;
for (let i = 0; i < children.length; i++) {
const grid = children[i].getComponent(MapGrid);
if (!grid?.isAreaUnlock) continue;
if (grid.unlockAreaId < minId) minId = grid.unlockAreaId;
}
return this._unlockAreaId === minId;
}
3 weeks ago
/** 格子上的 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;
}
}