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.
 
 
 
 
AdLike2048/assets/script/battle/view/YingCampSummonPresenter.ts

224 lines
8.2 KiB

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<Vec3>,
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<Vec3>,
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>): 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;
}