import { assetManager, instantiate, Node, Prefab, sp, tween, Tween, UIOpacity, UITransform, Vec3 } from 'cc'; import type { EntityId } from 'db://assets/mx/module/ecs'; import { TransformComp } from 'db://assets/mx/module/battle'; import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch'; import { GBundle } from '../../game/ConfigRes'; import { ChapterLevelBuilder } from '../config/ChapterLevelBuilder'; import { MonsterDataManager } from '../../manager/MonsterDataManager'; import { findGridIndexForEntity } from '../runtime/DeployEntityQuery'; import { SkillPresentationEnv } from '../skill/SkillPresentationEnv'; import { TextGrid } from './TextGrid'; const MONSTER_PREFAB_DIR = 'prefab/monster/'; /** 营强化出怪飘怪缩放(勿用对象池预制体,避免抢场上怪物 skin) */ const YING_SUMMON_GLYPH_SCALE = 0.75; /** 上浮像素 */ const YING_SUMMON_FLOAT_UP_PX = 88; /** 飘怪总时长(秒) */ const YING_SUMMON_FLOAT_SEC = 1; /** 营字强化出怪表现参数 */ export interface YingCampSummonFeedback { /** 营塔实体 */ entityId: EntityId; /** 部署玩家 id */ ownerPlayerId: number; /** 出怪 Monster.id */ monsterId: number; /** 本次出怪数量 */ count: number; } /** CD 到点:营字缩放抖动 + 飘当前出怪 */ export function playYingCampSummonFeedback(feedback: YingCampSummonFeedback): void { const row = MonsterDataManager.getMonsterDataById(feedback.monsterId); if (!row) { console.log('[YingCampSummonPresenter] 怪物表缺失', feedback.monsterId); return; } const grid = _resolveTextGrid(feedback.entityId, feedback.ownerPlayerId); grid?.playYingSummonPulse(); const world = _resolveWorldCenter(feedback.entityId, feedback.ownerPlayerId); if (!world) { console.log('[YingCampSummonPresenter] 营格世界坐标缺失', feedback.entityId); return; } const count = Math.max(1, Math.floor(feedback.count)); for (let i = 0; i < count; i++) { _playYingCampMonsterFloater(row, feedback.ownerPlayerId, world, i, count); } } /** 飘怪物:instantiate 独立节点,不占用 battleGameMonster 对象池 */ function _playYingCampMonsterFloater( row: ITableMonster, ownerPlayerId: number, worldCenter: Readonly, index: number, total: number, ): void { const prefabName = ChapterLevelBuilder.resolveMonsterPrefabFromRow(row); if (!prefabName) { console.log('[YingCampSummonPresenter] 怪物预制体名为空', row.id); return; } _loadMonsterFloaterPrefab(prefabName, (prefab) => { if (!prefab?.isValid) return; _mountMonsterFloater(prefab, row, ownerPlayerId, worldCenter, index, total); }); } function _loadMonsterFloaterPrefab(prefabName: string, onReady: (prefab: Prefab | null) => void): void { const assetPath = `${MONSTER_PREFAB_DIR}${prefabName}`; const bundle = assetManager.getBundle(GBundle.BattleGameMonster); if (!bundle) { console.log('[YingCampSummonPresenter] battleGameMonster 未加载', prefabName); onReady(null); return; } const cached = bundle.get(assetPath, Prefab) as Prefab | null; if (cached?.isValid) { onReady(cached); return; } bundle.load(assetPath, Prefab, (err, prefab) => { if (err || !prefab?.isValid) { console.log('[YingCampSummonPresenter] 怪物预制体加载失败', prefabName, String(err)); onReady(null); return; } onReady(prefab); }); } function _mountMonsterFloater( prefab: Prefab, row: ITableMonster, ownerPlayerId: number, worldCenter: Readonly, index: number, total: number, ): void { const flyLayer = SkillPresentationEnv.flyLayer; if (!flyLayer?.isValid) return; const node = instantiate(prefab); if (!node?.isValid) return; const animNode = node.getChildByName('anim'); const sk = animNode?.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton); if (!sk?.isValid) { console.log('[YingCampSummonPresenter] 飘怪 Spine 缺失', row.id); node.destroy(); return; } const enemyCamp = ownerPlayerId !== 0; const skinName = MonsterDataManager.resolveBattleSpineSkin(row, enemyCamp); if (!skinName) { console.log('[YingCampSummonPresenter] 飘怪皮肤名为空', row.id); node.destroy(); return; } if (sk.skeletonData) ensureSpineSkeletonDataRuntime(sk.skeletonData); try { (sk as sp.Skeleton & { defaultSkin: string }).defaultSkin = skinName; sk.setSkin(skinName); sk.setSlotsToSetupPose(); } catch (err) { console.log('[YingCampSummonPresenter] 飘怪 setSkin 失败', skinName, row.id, String(err)); node.destroy(); return; } const runtime = sk.skeletonData?.getRuntimeData?.(true); if (runtime?.findAnimation?.('待机')) { sk.setAnimation(0, '待机', true); } flyLayer.active = true; node.parent = flyLayer; node.layer = flyLayer.layer; node.name = 'yingSummonFloater'; const startLocal = _resolveStartLocal(flyLayer, worldCenter); const spreadX = (index - (total - 1) * 0.5) * 26; node.setPosition(startLocal.x + spreadX, startLocal.y + 8, startLocal.z); node.setScale(YING_SUMMON_GLYPH_SCALE, YING_SUMMON_GLYPH_SCALE, 1); node.setSiblingIndex(flyLayer.children.length - 1); const opacity = node.getComponent(UIOpacity) ?? node.addComponent(UIOpacity); opacity.opacity = 255; const endY = startLocal.y + YING_SUMMON_FLOAT_UP_PX; Tween.stopAllByTarget(node); Tween.stopAllByTarget(opacity); tween(node) .to(YING_SUMMON_FLOAT_SEC, { position: new Vec3(startLocal.x + spreadX, endY, startLocal.z), }, { easing: 'sineOut' }) .call(() => { if (node.isValid) { Tween.stopAllByTarget(node); Tween.stopAllByTarget(opacity); node.destroy(); } if (flyLayer.isValid && flyLayer.children.length === 0) { flyLayer.active = false; } }) .start(); tween(opacity) .delay(YING_SUMMON_FLOAT_SEC * 0.45) .to(YING_SUMMON_FLOAT_SEC * 0.55, { opacity: 0 }, { easing: 'sineIn' }) .start(); } function _resolveStartLocal(flyLayer: Node, worldCenter: Readonly): Vec3 { const flyUt = flyLayer.getComponent(UITransform); return flyUt ? flyUt.convertToNodeSpaceAR(worldCenter) : new Vec3(worldCenter.x, worldCenter.y, worldCenter.z); } function _resolveTextGrid(entityId: EntityId, ownerPlayerId: number): TextGrid | null { const battle = gg.game.CurentBattle; const map = battle?.Map; const session = battle?.session; if (!battle || !map || !session) return null; const player = battle.getPlayer(ownerPlayerId); if (!player) return null; const gridIndex = findGridIndexForEntity(entityId, player, session, map); if (gridIndex == null) return null; const mg = map.getMapGridByIndex(gridIndex); return mg?.node?.getComponentInChildren(TextGrid) ?? null; } function _resolveWorldCenter(entityId: EntityId, ownerPlayerId: number): Vec3 | null { const battle = gg.game.CurentBattle; const map = battle?.Map; const session = battle?.session; if (!battle || !map || !session) return null; const player = battle.getPlayer(ownerPlayerId); if (!player) return null; const gridIndex = findGridIndexForEntity(entityId, player, session, map); if (gridIndex != null) { const fromEnv = SkillPresentationEnv.getGridWorldCenter(gridIndex); if (fromEnv) return fromEnv; const mg = map.getMapGridByIndex(gridIndex); if (mg?.node?.isValid) return mg.node.worldPosition.clone(); } // AI 营塔偶发 composition 回查失败:用 Transform 格心兜底 if (!session.world.isAlive(entityId)) return null; const tf = session.world.getComponent(entityId, TransformComp); if (!tf) return null; const byRowCol = SkillPresentationEnv.getGridWorldCenterByRowCol(tf.row, tf.col); if (byRowCol) return byRowCol; const mg2 = map.getMapGrid(tf.row, tf.col); return mg2?.node?.isValid ? mg2.node.worldPosition.clone() : null; }