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.
224 lines
8.2 KiB
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;
|
|
}
|
|
|