|
|
|
|
import {
|
|
|
|
|
_decorator, Button, Canvas, Component, instantiate, Label, Node, Rect, Sprite, Tween, tween, UIOpacity, UITransform, Vec3, view, Widget,
|
|
|
|
|
} from 'cc';
|
|
|
|
|
import { SpriteLoad } from 'db://assets/mx/components/SpriteLoad';
|
|
|
|
|
import { GBundle } from '../../game/ConfigRes';
|
|
|
|
|
import { BATTLE_HERO_WEAPON_PATH } from '../../battle/res/BattleHeroResCollector';
|
|
|
|
|
import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime';
|
|
|
|
|
import type { EntityId } from 'db://assets/mx/module/ecs';
|
|
|
|
|
import type { WeaponDropRecord } from '../../battle/combat/WeaponDropService';
|
|
|
|
|
import { BattleAudio } from '../../battle/audio/BattleAudio';
|
|
|
|
|
|
|
|
|
|
const { ccclass } = _decorator;
|
|
|
|
|
|
|
|
|
|
/** 战场宝石掉落预制体名(battleHero/prefab/weapon) */
|
|
|
|
|
const WORLD_DROP_PREFAB = '宝石掉落';
|
|
|
|
|
/** 暂停列表战利品预制体名(旧「武器」) */
|
|
|
|
|
const PAUSE_LIST_PREFAB = '武器';
|
|
|
|
|
/** 掉落/列表图标子节点名 */
|
|
|
|
|
const ICON_NODE = '图标';
|
|
|
|
|
/** 战场掉落品质根节点(子节点名 1~4) */
|
|
|
|
|
const QUALITY_ROOT = '品质';
|
|
|
|
|
/** 品质子节点档位数 */
|
|
|
|
|
const QUALITY_MAX = 4;
|
|
|
|
|
/** 结算模板图标子节点名(ui_gemTemp/icon) */
|
|
|
|
|
const SETTLE_ICON_NODE = 'icon';
|
|
|
|
|
/** 碎片数量底节点名 */
|
|
|
|
|
const NUM_BG_NODE = 'numBg';
|
|
|
|
|
/** 碎片数量文本节点名 */
|
|
|
|
|
const NUM_LABEL_NODE = 'lb_weaponNum';
|
|
|
|
|
/** 暂停列表敌方角标节点名(预制体已挂贴图,仅控制显隐) */
|
|
|
|
|
const ENEMY_MARK_NODE = '敌方';
|
|
|
|
|
/** 掉落根节点默认尺寸(与「宝石掉落」预制体 UITransform 一致) */
|
|
|
|
|
const DROP_W = 101;
|
|
|
|
|
const DROP_H = 101;
|
|
|
|
|
/** 重力(像素/秒²) */
|
|
|
|
|
const GRAVITY = -3200;
|
|
|
|
|
/** 落地弹性 */
|
|
|
|
|
const BOUNCE = 0.42;
|
|
|
|
|
/** 落地水平摩擦 */
|
|
|
|
|
const GROUND_FRICTION = 0.55;
|
|
|
|
|
/** 视为静止的速度 */
|
|
|
|
|
const SETTLE_SPEED = 90;
|
|
|
|
|
/** 屏幕内边距(像素),避免贴边被裁 */
|
|
|
|
|
const SCREEN_PAD = 8;
|
|
|
|
|
/** 飞向暂停按钮时长 */
|
|
|
|
|
const FLY_SEC = 0.55;
|
|
|
|
|
/** 到达后渐隐时长 */
|
|
|
|
|
const FADE_SEC = 0.18;
|
|
|
|
|
|
|
|
|
|
/** 掉落阶段 */
|
|
|
|
|
enum DropPhase {
|
|
|
|
|
/** 空闲 / 暂停列表展示 */
|
|
|
|
|
Idle = 0,
|
|
|
|
|
/** 物理掉落中 */
|
|
|
|
|
Drop = 1,
|
|
|
|
|
/** 等待玩家点击拾取 */
|
|
|
|
|
WaitClick = 2,
|
|
|
|
|
/** 飞向暂停按钮 */
|
|
|
|
|
Fly = 3,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 「宝石掉落」预制体视图:物理落地、点击拾取、飞入暂停按钮。
|
|
|
|
|
*/
|
|
|
|
|
@ccclass('WeaponDropView')
|
|
|
|
|
export class WeaponDropView extends Component {
|
|
|
|
|
private _phase = DropPhase.Idle;
|
|
|
|
|
private _battle: ProjectBattleHandle | null = null;
|
|
|
|
|
private _pauseBtn: Node | null = null;
|
|
|
|
|
private _needClick = false;
|
|
|
|
|
private _vx = 0;
|
|
|
|
|
private _vy = 0;
|
|
|
|
|
private _groundY = 0;
|
|
|
|
|
private _minX = 0;
|
|
|
|
|
private _maxX = 0;
|
|
|
|
|
private _minY = 0;
|
|
|
|
|
private _maxY = 0;
|
|
|
|
|
private _hasBounds = false;
|
|
|
|
|
private readonly _pos = new Vec3();
|
|
|
|
|
private readonly _onTouchEnd = this._handleClick.bind(this);
|
|
|
|
|
private readonly _onFlyDone = this._finishFly.bind(this);
|
|
|
|
|
private readonly _onFadeDone = this._recycle.bind(this);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 克隆结算页 ui_gemTemp 模板。
|
|
|
|
|
* @param template BattleSuccess / BattleFail 下 ui_gemTemp
|
|
|
|
|
*/
|
|
|
|
|
static takeSettlePrefab(template: Node | null | undefined): Node | null {
|
|
|
|
|
if (!template?.isValid) {
|
|
|
|
|
console.log('[WeaponDrop] ui_gemTemp 模板无效');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const node = instantiate(template);
|
|
|
|
|
node.active = true;
|
|
|
|
|
return node;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 从节点池取「宝石掉落」预制体(战场世界掉落) */
|
|
|
|
|
static takePrefab(): Node | null {
|
|
|
|
|
return WeaponDropView._takeNamedPrefab(WORLD_DROP_PREFAB);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 从节点池取「武器」预制体(暂停战利品列表) */
|
|
|
|
|
static takePauseListPrefab(): Node | null {
|
|
|
|
|
return WeaponDropView._takeNamedPrefab(PAUSE_LIST_PREFAB);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 取 weapon 目录下指定预制体。
|
|
|
|
|
* @param prefabName 预制体名
|
|
|
|
|
*/
|
|
|
|
|
private static _takeNamedPrefab(prefabName: string): Node | null {
|
|
|
|
|
let node: Node | null = null;
|
|
|
|
|
try {
|
|
|
|
|
node = gg.res.getNode(`${BATTLE_HERO_WEAPON_PATH}/`, prefabName, GBundle.BattleHero);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.log('[WeaponDrop] 预制体加载异常', prefabName, String(e));
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (!node?.isValid) {
|
|
|
|
|
console.log('[WeaponDrop] 预制体缺失', prefabName);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return node;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 战场掉落:品质光柱 + 图标。
|
|
|
|
|
* @param node 「宝石掉落」根节点
|
|
|
|
|
* @param record 掉落记录
|
|
|
|
|
*/
|
|
|
|
|
static applyVisual(node: Node, record: WeaponDropRecord): void {
|
|
|
|
|
WeaponDropView.clearFragmentCount(node);
|
|
|
|
|
const weapon = gg.equip.getEquip(record.weaponId);
|
|
|
|
|
if (!weapon) {
|
|
|
|
|
console.warn('[WeaponDrop] 宝石表缺失', record.weaponId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
WeaponDropView._applyQualityMark(node, record.quality, record.weaponId);
|
|
|
|
|
const iconName = String(weapon.icon ?? '').trim();
|
|
|
|
|
if (!iconName) {
|
|
|
|
|
console.warn('[WeaponDrop] icon 缺失', record.weaponId);
|
|
|
|
|
}
|
|
|
|
|
const icon = node.getChildByName(ICON_NODE);
|
|
|
|
|
if (!icon?.isValid) {
|
|
|
|
|
console.warn('[WeaponDrop] 缺少图标节点', record.weaponId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
WeaponDropView._setSprite(icon, iconName ? `weapon/${iconName}` : '', true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 暂停列表:旧「武器」底框 + 图标。
|
|
|
|
|
* @param node 「武器」根节点
|
|
|
|
|
* @param record 掉落记录
|
|
|
|
|
*/
|
|
|
|
|
static applyPauseListVisual(node: Node, record: WeaponDropRecord): void {
|
|
|
|
|
WeaponDropView.clearFragmentCount(node);
|
|
|
|
|
const weapon = gg.equip.getEquip(record.weaponId);
|
|
|
|
|
if (!weapon) {
|
|
|
|
|
console.warn('[WeaponDrop] 宝石表缺失', record.weaponId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const bgIndex = WeaponDropView._gemFrameIndex(record.quality, record.weaponId);
|
|
|
|
|
WeaponDropView._setSprite(node, `weapon/宝石框大${bgIndex}`);
|
|
|
|
|
const bgSprite = node.getComponent(Sprite);
|
|
|
|
|
if (bgSprite) bgSprite.type = Sprite.Type.SLICED;
|
|
|
|
|
const iconName = String(weapon.icon ?? '').trim();
|
|
|
|
|
if (!iconName) {
|
|
|
|
|
console.warn('[WeaponDrop] icon 缺失', record.weaponId);
|
|
|
|
|
}
|
|
|
|
|
const icon = node.getChildByName(ICON_NODE);
|
|
|
|
|
if (!icon?.isValid) {
|
|
|
|
|
console.warn('[WeaponDrop] 缺少图标节点', record.weaponId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
WeaponDropView._setSprite(icon, iconName ? `weapon/${iconName}` : '', true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 结算列表:根节点品质框,子节点 icon 为宝石图。
|
|
|
|
|
* @param node ui_gemTemp 克隆根节点
|
|
|
|
|
* @param record 掉落记录
|
|
|
|
|
*/
|
|
|
|
|
static applySettleVisual(node: Node, record: WeaponDropRecord): void {
|
|
|
|
|
const weapon = gg.equip.getEquip(record.weaponId);
|
|
|
|
|
if (!weapon) {
|
|
|
|
|
console.warn('[WeaponDrop] 宝石表缺失', record.weaponId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const bgIndex = WeaponDropView._gemFrameIndex(record.quality, record.weaponId);
|
|
|
|
|
WeaponDropView._setSprite(node, `weapon/宝石框大${bgIndex}`);
|
|
|
|
|
const bgSprite = node.getComponent(Sprite);
|
|
|
|
|
if (bgSprite) bgSprite.type = Sprite.Type.SLICED;
|
|
|
|
|
const iconName = String(weapon.icon ?? '').trim();
|
|
|
|
|
if (!iconName) {
|
|
|
|
|
console.warn('[WeaponDrop] icon 缺失', record.weaponId);
|
|
|
|
|
}
|
|
|
|
|
const icon = node.getChildByName(SETTLE_ICON_NODE);
|
|
|
|
|
if (!icon?.isValid) {
|
|
|
|
|
console.warn('[WeaponDrop] 缺少 icon 节点', record.weaponId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
WeaponDropView._setSprite(icon, iconName ? `weapon/${iconName}` : '', true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 销毁结算 ui_gemTemp 克隆体(勿回战场武器池) */
|
|
|
|
|
static recycleSettleNode(node: Node): void {
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
node.getComponent(WeaponDropView)?.resetMotion();
|
|
|
|
|
node.destroy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 暂停列表:敌方奖励显示「敌方」角标,我方奖励隐藏。
|
|
|
|
|
* @param node 武器预制体根节点
|
|
|
|
|
* @param isSelf 是否己方击杀掉落
|
|
|
|
|
*/
|
|
|
|
|
static applySideMark(node: Node, isSelf: boolean): void {
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
const mark = node?.find(ENEMY_MARK_NODE);
|
|
|
|
|
if (!mark?.isValid) {
|
|
|
|
|
console.log('[WeaponDrop] 缺少敌方节点');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
mark.active = !isSelf;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 隐藏敌方角标(战场掉落 / 回池) */
|
|
|
|
|
static clearSideMark(node: Node | null | undefined): void {
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
const mark = node?.find(ENEMY_MARK_NODE);
|
|
|
|
|
if (mark?.isValid) mark.active = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 结算列表:显示武器碎片数量。
|
|
|
|
|
* @param node 武器预制体根节点
|
|
|
|
|
* @param count 碎片数量
|
|
|
|
|
*/
|
|
|
|
|
static applyFragmentCount(node: Node, count: number): void {
|
|
|
|
|
const numBg = node.getChildByName(NUM_BG_NODE);
|
|
|
|
|
if (!numBg?.isValid) {
|
|
|
|
|
console.log('[WeaponDrop] 缺少 numBg');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const num = Math.max(0, Math.floor(count));
|
|
|
|
|
if (num <= 0) {
|
|
|
|
|
numBg.active = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
numBg.active = true;
|
|
|
|
|
const lb = numBg.getChildByName(NUM_LABEL_NODE)?.getComponent(Label);
|
|
|
|
|
if (!lb) {
|
|
|
|
|
console.log('[WeaponDrop] 缺少 lb_weaponNum');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
lb.string = `${num}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 隐藏碎片数量(战场掉落 / 回池) */
|
|
|
|
|
static clearFragmentCount(node: Node): void {
|
|
|
|
|
const numBg = node.getChildByName(NUM_BG_NODE);
|
|
|
|
|
if (!numBg?.isValid) return;
|
|
|
|
|
numBg.active = false;
|
|
|
|
|
const lb = numBg.getChildByName(NUM_LABEL_NODE)?.getComponent(Label);
|
|
|
|
|
if (lb) lb.string = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Weapon.quality → 宝石框大资源下标(结算模板用,现有资源 1~5)。
|
|
|
|
|
* @param quality 品质
|
|
|
|
|
* @param weaponId 宝石 id(仅日志)
|
|
|
|
|
*/
|
|
|
|
|
private static _gemFrameIndex(quality: number, weaponId: number): number {
|
|
|
|
|
const q = Number(quality) | 0;
|
|
|
|
|
if (q < 1 || q > 5) {
|
|
|
|
|
console.warn('[WeaponDrop] 宝石品质无对应宝石框大', weaponId, q);
|
|
|
|
|
}
|
|
|
|
|
return q;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 切换「品质」下 1~4 子节点(表品质 5 钳到 4)。
|
|
|
|
|
* @param node 掉落根
|
|
|
|
|
* @param quality Weapon.quality
|
|
|
|
|
* @param weaponId 宝石 id(日志)
|
|
|
|
|
*/
|
|
|
|
|
private static _applyQualityMark(node: Node, quality: number, weaponId: number): void {
|
|
|
|
|
const root = node.getChildByName(QUALITY_ROOT);
|
|
|
|
|
if (!root?.isValid) {
|
|
|
|
|
console.warn('[WeaponDrop] 缺少品质节点', weaponId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let q = Math.floor(Number(quality) || 0);
|
|
|
|
|
if (q < 1 || q > QUALITY_MAX) {
|
|
|
|
|
console.warn('[WeaponDrop] 品质越界,钳到 1~4', weaponId, quality);
|
|
|
|
|
q = Math.max(1, Math.min(QUALITY_MAX, q > 0 ? q : 1));
|
|
|
|
|
}
|
|
|
|
|
const name = String(q);
|
|
|
|
|
const children = root.children;
|
|
|
|
|
for (let i = 0; i < children.length; i++) {
|
|
|
|
|
const child = children[i];
|
|
|
|
|
if (!child?.isValid) continue;
|
|
|
|
|
child.active = child.name === name;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 加载 icons/weapon 图。
|
|
|
|
|
* @param node 目标节点
|
|
|
|
|
* @param path bundle 内路径
|
|
|
|
|
* @param fitIcon 是否把图标压到预制体尺寸
|
|
|
|
|
*/
|
|
|
|
|
private static _setSprite(node: Node, path: string, fitIcon = false): void {
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
if (!path) {
|
|
|
|
|
node.active = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
node.active = true;
|
|
|
|
|
node.getOrAddComponent(SpriteLoad).setSprite(path, GBundle.Items);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 世界掉落:物理落地后,己方等待点击、敌方自动飞入。
|
|
|
|
|
* @param battle 当前战斗
|
|
|
|
|
* @param pauseBtn 暂停按钮
|
|
|
|
|
* @param startWorld 死亡世界坐标
|
|
|
|
|
* @param needClick 是否需要点击拾取
|
|
|
|
|
*/
|
|
|
|
|
playWorldDrop(battle: ProjectBattleHandle, pauseBtn: Node | null, startWorld: Readonly<Vec3>, needClick: boolean): void {
|
|
|
|
|
this.resetMotion();
|
|
|
|
|
WeaponDropView.clearSideMark(this.node);
|
|
|
|
|
this._battle = battle;
|
|
|
|
|
this._pauseBtn = pauseBtn;
|
|
|
|
|
this._needClick = needClick;
|
|
|
|
|
const node = this.node;
|
|
|
|
|
this._captureScreenBounds();
|
|
|
|
|
this._pos.set(startWorld.x, startWorld.y, startWorld.z);
|
|
|
|
|
this._clampInScreen(this._pos, false);
|
|
|
|
|
node.setWorldPosition(this._pos);
|
|
|
|
|
node.setScale(1, 1, 1);
|
|
|
|
|
node.angle = 0;
|
|
|
|
|
this._setOpacity(255);
|
|
|
|
|
this._vx = (Math.random() - 0.5) * 280;
|
|
|
|
|
this._vy = 120 + Math.random() * 80;
|
|
|
|
|
this._groundY = this._pos.y - (18 + Math.random() * 28);
|
|
|
|
|
if (this._hasBounds) {
|
|
|
|
|
this._groundY = Math.min(this._maxY, Math.max(this._minY, this._groundY));
|
|
|
|
|
}
|
|
|
|
|
this._phase = DropPhase.Drop;
|
|
|
|
|
this._setClickable(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 取战斗面板/画布世界包围盒,按掉落物半尺寸得到可停留范围 */
|
|
|
|
|
private _captureScreenBounds(): void {
|
|
|
|
|
this._hasBounds = false;
|
|
|
|
|
let panelBox: Rect | null = null;
|
|
|
|
|
let canvasBox: Rect | null = null;
|
|
|
|
|
let n: Node | null = this.node.parent;
|
|
|
|
|
while (n?.isValid) {
|
|
|
|
|
const ut = n.getComponent(UITransform);
|
|
|
|
|
const widget = n.getComponent(Widget);
|
|
|
|
|
if (ut && widget && (widget.alignFlags & 45) === 45) {
|
|
|
|
|
const worldBox = ut.getBoundingBoxToWorld();
|
|
|
|
|
if (worldBox.width > 1 && worldBox.height > 1) {
|
|
|
|
|
panelBox = worldBox;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (n.getComponent(Canvas) && ut) {
|
|
|
|
|
const worldBox = ut.getBoundingBoxToWorld();
|
|
|
|
|
if (worldBox.width > 1 && worldBox.height > 1) {
|
|
|
|
|
canvasBox = worldBox;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
n = n.parent;
|
|
|
|
|
}
|
|
|
|
|
let box = panelBox ?? canvasBox;
|
|
|
|
|
if (!box) {
|
|
|
|
|
box = this._fallbackScreenBox();
|
|
|
|
|
}
|
|
|
|
|
if (!box) {
|
|
|
|
|
console.log('[WeaponDrop] 无法取得画布范围,掉落可能出屏');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const selfUt = this.node.getComponent(UITransform);
|
|
|
|
|
const hw = Math.max(1, (selfUt?.width ?? DROP_W) * 0.5) + SCREEN_PAD;
|
|
|
|
|
const hh = Math.max(1, (selfUt?.height ?? DROP_H) * 0.5) + SCREEN_PAD;
|
|
|
|
|
this._minX = box.x + hw;
|
|
|
|
|
this._maxX = box.x + box.width - hw;
|
|
|
|
|
this._minY = box.y + hh;
|
|
|
|
|
this._maxY = box.y + box.height - hh;
|
|
|
|
|
if (this._minX > this._maxX) {
|
|
|
|
|
const midX = box.x + box.width * 0.5;
|
|
|
|
|
this._minX = midX;
|
|
|
|
|
this._maxX = midX;
|
|
|
|
|
}
|
|
|
|
|
if (this._minY > this._maxY) {
|
|
|
|
|
const midY = box.y + box.height * 0.5;
|
|
|
|
|
this._minY = midY;
|
|
|
|
|
this._maxY = midY;
|
|
|
|
|
}
|
|
|
|
|
this._hasBounds = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Canvas / Widget 均不可用时,用可见区域与父节点 UITransform 估算世界包围盒 */
|
|
|
|
|
private _fallbackScreenBox(): Rect | null {
|
|
|
|
|
let anchor: Node | null = this.node.parent;
|
|
|
|
|
while (anchor?.isValid && !anchor.getComponent(UITransform)) {
|
|
|
|
|
anchor = anchor.parent;
|
|
|
|
|
}
|
|
|
|
|
const ut = anchor?.getComponent(UITransform);
|
|
|
|
|
if (!ut) return null;
|
|
|
|
|
const visible = view.getVisibleSize();
|
|
|
|
|
const halfW = visible.width * 0.5;
|
|
|
|
|
const halfH = visible.height * 0.5;
|
|
|
|
|
const center = ut.convertToWorldSpaceAR(new Vec3(0, 0, 0));
|
|
|
|
|
return new Rect(center.x - halfW, center.y - halfH, visible.width, visible.height);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 把掉落中心钳在屏幕内;碰到左右边时反向水平速度。
|
|
|
|
|
* @param pos 世界坐标
|
|
|
|
|
* @param bounce 是否反弹速度
|
|
|
|
|
*/
|
|
|
|
|
private _clampInScreen(pos: Vec3, bounce: boolean): void {
|
|
|
|
|
if (!this._hasBounds) return;
|
|
|
|
|
if (pos.x < this._minX) {
|
|
|
|
|
pos.x = this._minX;
|
|
|
|
|
if (bounce) this._vx = Math.abs(this._vx);
|
|
|
|
|
} else if (pos.x > this._maxX) {
|
|
|
|
|
pos.x = this._maxX;
|
|
|
|
|
if (bounce) this._vx = -Math.abs(this._vx);
|
|
|
|
|
}
|
|
|
|
|
if (pos.y < this._minY) {
|
|
|
|
|
pos.y = this._minY;
|
|
|
|
|
if (bounce && this._vy < 0) this._vy = 0;
|
|
|
|
|
} else if (pos.y > this._maxY) {
|
|
|
|
|
pos.y = this._maxY;
|
|
|
|
|
if (bounce && this._vy > 0) this._vy = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 暂停列表:仅展示,关闭交互 */
|
|
|
|
|
playDisplay(): void {
|
|
|
|
|
Tween.stopAllByTarget(this.node);
|
|
|
|
|
const op = this.node.getComponent(UIOpacity);
|
|
|
|
|
if (op) Tween.stopAllByTarget(op);
|
|
|
|
|
this.node.off(Node.EventType.TOUCH_END, this._onTouchEnd, this);
|
|
|
|
|
this._setClickable(false);
|
|
|
|
|
this._phase = DropPhase.Idle;
|
|
|
|
|
this._battle = null;
|
|
|
|
|
this._pauseBtn = null;
|
|
|
|
|
this._needClick = false;
|
|
|
|
|
this._vx = 0;
|
|
|
|
|
this._vy = 0;
|
|
|
|
|
this.node.angle = 0;
|
|
|
|
|
this.node.setScale(1, 1, 1);
|
|
|
|
|
this._setOpacity(255);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 停动画、解点击、复位运动状态(回池前调用) */
|
|
|
|
|
resetMotion(): void {
|
|
|
|
|
this._stopMotion();
|
|
|
|
|
WeaponDropView.clearSideMark(this.node);
|
|
|
|
|
this._phase = DropPhase.Idle;
|
|
|
|
|
this._battle = null;
|
|
|
|
|
this._pauseBtn = null;
|
|
|
|
|
this._needClick = false;
|
|
|
|
|
this._vx = 0;
|
|
|
|
|
this._vy = 0;
|
|
|
|
|
this._hasBounds = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 停 tween / 点击;销毁过程中子节点可能已空,禁止 find */
|
|
|
|
|
private _stopMotion(): void {
|
|
|
|
|
const node = this.node;
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
Tween.stopAllByTarget(node);
|
|
|
|
|
const op = node.getComponent(UIOpacity);
|
|
|
|
|
if (op) Tween.stopAllByTarget(op);
|
|
|
|
|
node.off(Node.EventType.TOUCH_END, this._onTouchEnd, this);
|
|
|
|
|
this._setClickable(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected update(dt: number): void {
|
|
|
|
|
if (this._phase !== DropPhase.Drop) return;
|
|
|
|
|
if (this._battle?.IsGamePause) return;
|
|
|
|
|
const node = this.node;
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
this._vy += GRAVITY * dt;
|
|
|
|
|
node.getWorldPosition(this._pos);
|
|
|
|
|
this._pos.x += this._vx * dt;
|
|
|
|
|
this._pos.y += this._vy * dt;
|
|
|
|
|
this._clampInScreen(this._pos, true);
|
|
|
|
|
if (this._pos.y <= this._groundY && this._vy < 0) {
|
|
|
|
|
this._pos.y = this._groundY;
|
|
|
|
|
this._vy *= -BOUNCE;
|
|
|
|
|
this._vx *= GROUND_FRICTION;
|
|
|
|
|
this._clampInScreen(this._pos, true);
|
|
|
|
|
if (Math.abs(this._vy) < SETTLE_SPEED && Math.abs(this._vx) < SETTLE_SPEED) {
|
|
|
|
|
this._pos.y = this._groundY;
|
|
|
|
|
this._clampInScreen(this._pos, false);
|
|
|
|
|
node.setWorldPosition(this._pos);
|
|
|
|
|
node.angle = 0;
|
|
|
|
|
this._onLanded();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
node.setWorldPosition(this._pos);
|
|
|
|
|
node.angle = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected onDestroy(): void {
|
|
|
|
|
this._stopMotion();
|
|
|
|
|
this._phase = DropPhase.Idle;
|
|
|
|
|
this._battle = null;
|
|
|
|
|
this._pauseBtn = null;
|
|
|
|
|
this._needClick = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 落地:敌方自动飞入,己方等待点击 */
|
|
|
|
|
private _onLanded(): void {
|
|
|
|
|
this._vx = 0;
|
|
|
|
|
this._vy = 0;
|
|
|
|
|
if (this._needClick) {
|
|
|
|
|
this._phase = DropPhase.WaitClick;
|
|
|
|
|
this._setClickable(true);
|
|
|
|
|
this.node.on(Node.EventType.TOUCH_END, this._onTouchEnd, this);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._startFly();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 玩家点击拾取 */
|
|
|
|
|
private _handleClick(): void {
|
|
|
|
|
if (this._phase !== DropPhase.WaitClick) return;
|
|
|
|
|
this.node.off(Node.EventType.TOUCH_END, this._onTouchEnd, this);
|
|
|
|
|
this._setClickable(false);
|
|
|
|
|
this._startFly();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 飞向暂停按钮后渐隐回池 */
|
|
|
|
|
private _startFly(): void {
|
|
|
|
|
const node = this.node;
|
|
|
|
|
const target = this._pauseBtn;
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
this._phase = DropPhase.Fly;
|
|
|
|
|
if (!target?.isValid) {
|
|
|
|
|
console.log('[WeaponDrop] 暂停按钮无效,直接回收');
|
|
|
|
|
this._recycle();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Tween.stopAllByTarget(node);
|
|
|
|
|
tween(node)
|
|
|
|
|
.to(FLY_SEC, { worldPosition: target.worldPosition.clone(), scale: new Vec3(0.45, 0.45, 1) }, { easing: 'sineIn' })
|
|
|
|
|
.call(this._onFlyDone)
|
|
|
|
|
.start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 到达暂停按钮后渐隐 */
|
|
|
|
|
private _finishFly(): void {
|
|
|
|
|
if (!this.node?.isValid) return;
|
|
|
|
|
const op = this.node.getComponent(UIOpacity) ?? this.node.addComponent(UIOpacity);
|
|
|
|
|
Tween.stopAllByTarget(op);
|
|
|
|
|
op.opacity = 255;
|
|
|
|
|
tween(op).to(FADE_SEC, { opacity: 0 }).call(this._onFadeDone).start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 回池 */
|
|
|
|
|
private _recycle(): void {
|
|
|
|
|
if (!this.node?.isValid) return;
|
|
|
|
|
this.resetMotion();
|
|
|
|
|
this._setOpacity(255);
|
|
|
|
|
this.node.setScale(1, 1, 1);
|
|
|
|
|
this.node.angle = 0;
|
|
|
|
|
gg.res.putNode(this.node);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 设置透明度。
|
|
|
|
|
* @param value 0~255
|
|
|
|
|
*/
|
|
|
|
|
private _setOpacity(value: number): void {
|
|
|
|
|
const op = this.node.getComponent(UIOpacity) ?? this.node.addComponent(UIOpacity);
|
|
|
|
|
op.opacity = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 开关点击拾取。
|
|
|
|
|
* @param on 是否可点
|
|
|
|
|
*/
|
|
|
|
|
private _setClickable(on: boolean): void {
|
|
|
|
|
const node = this.node;
|
|
|
|
|
if (!node?.isValid) return;
|
|
|
|
|
let btn = node.getComponent(Button);
|
|
|
|
|
if (on && !btn) btn = node.addComponent(Button);
|
|
|
|
|
if (!btn) return;
|
|
|
|
|
btn.transition = Button.Transition.NONE;
|
|
|
|
|
btn.interactable = on;
|
|
|
|
|
btn.enabled = on;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 击杀武器碎片掉落表现。
|
|
|
|
|
*/
|
|
|
|
|
export class BattleWeaponDropPresenter {
|
|
|
|
|
private _battle: ProjectBattleHandle | null = null;
|
|
|
|
|
private _flyLayer: Node | null = null;
|
|
|
|
|
private _pauseBtn: Node | null = null;
|
|
|
|
|
private _resolveDeathWorldPos: ((victim: EntityId) => Vec3 | null) | null = null;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 绑定战斗、飞行层、暂停按钮与死亡坐标。
|
|
|
|
|
* @param battle 当前战斗
|
|
|
|
|
* @param flyLayer 飞行层
|
|
|
|
|
* @param pauseBtn 暂停按钮
|
|
|
|
|
* @param resolveDeathWorldPos 死亡世界坐标
|
|
|
|
|
*/
|
|
|
|
|
bind(battle: ProjectBattleHandle | null, flyLayer: Node | null, pauseBtn: Node | null, resolveDeathWorldPos: (victim: EntityId) => Vec3 | null): void {
|
|
|
|
|
this.unbind();
|
|
|
|
|
this._battle = battle;
|
|
|
|
|
this._flyLayer = flyLayer;
|
|
|
|
|
this._pauseBtn = pauseBtn;
|
|
|
|
|
this._resolveDeathWorldPos = resolveDeathWorldPos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 注销并回收场上掉落节点 */
|
|
|
|
|
unbind(): void {
|
|
|
|
|
this._recycleAlive();
|
|
|
|
|
this._battle = null;
|
|
|
|
|
this._flyLayer = null;
|
|
|
|
|
this._pauseBtn = null;
|
|
|
|
|
this._resolveDeathWorldPos = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 击杀回调:取出待播掉落并生成世界节点。
|
|
|
|
|
* @param victim 死亡实体
|
|
|
|
|
*/
|
|
|
|
|
onEntityKilled(victim: EntityId): void {
|
|
|
|
|
const battle = this._battle;
|
|
|
|
|
if (!battle) return;
|
|
|
|
|
const rec = battle.weaponDrop.shiftPendingVisual();
|
|
|
|
|
if (!rec) return;
|
|
|
|
|
const from = this._resolveDeathWorldPos?.(victim);
|
|
|
|
|
if (!from) {
|
|
|
|
|
console.log('[WeaponDrop] 无死亡坐标', victim, rec.weaponId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this._spawnWorld(battle, rec, from);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 在死亡点生成世界掉落。
|
|
|
|
|
* @param battle 当前战斗
|
|
|
|
|
* @param record 掉落记录
|
|
|
|
|
* @param startWorld 死亡世界坐标
|
|
|
|
|
*/
|
|
|
|
|
private _spawnWorld(battle: ProjectBattleHandle, record: WeaponDropRecord, startWorld: Readonly<Vec3>): void {
|
|
|
|
|
const layer = this._flyLayer;
|
|
|
|
|
if (!layer?.isValid) {
|
|
|
|
|
console.log('[WeaponDrop] flyLayer 未绑定');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const node = WeaponDropView.takePrefab();
|
|
|
|
|
if (!node) return;
|
|
|
|
|
if (!layer.active) layer.active = true;
|
|
|
|
|
layer.addChild(node);
|
|
|
|
|
WeaponDropView.applyVisual(node, record);
|
|
|
|
|
const view = node.getComponent(WeaponDropView) ?? node.addComponent(WeaponDropView);
|
|
|
|
|
view.playWorldDrop(battle, this._pauseBtn, startWorld, record.isSelf);
|
|
|
|
|
BattleAudio.playFragmentDrop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 回收飞行层上尚未回池的掉落节点 */
|
|
|
|
|
private _recycleAlive(): void {
|
|
|
|
|
const layer = this._flyLayer;
|
|
|
|
|
if (!layer?.isValid) return;
|
|
|
|
|
const children = layer.children.slice();
|
|
|
|
|
for (let i = 0; i < children.length; i++) {
|
|
|
|
|
const node = children[i];
|
|
|
|
|
if (!node?.isValid) continue;
|
|
|
|
|
const view = node.getComponent(WeaponDropView);
|
|
|
|
|
if (!view) continue;
|
|
|
|
|
view.resetMotion();
|
|
|
|
|
gg.res.putNode(node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|