yexingmin 8 hours ago
commit 5cd0cfd1b7
  1. 4
      assets/bundles/first/config/Battlebuff.json
  2. 5
      assets/script/battle/presentation/FlySwordAtkPresenter.ts
  3. 73
      assets/script/battle/presentation/SkillVisualChannel.ts
  4. 1
      assets/script/battle/view/HitLeanBack.ts
  5. 80
      assets/script/ui/BattleGame/Monster.ts

@ -131,7 +131,7 @@
"id": 10,
"name": "寻金尺",
"limit": 1,
"desc": "铲子变为金铲子,开挖地块时获得金币",
"desc": "解锁地块时额外产生金币",
"icon": "寻金尺",
"quality": 4,
"target": 0,
@ -179,7 +179,7 @@
"id": 13,
"name": "滞行牌",
"limit": 1,
"desc": "敌方怪物移动速度-10%",
"desc": "怪物行进速度-10%",
"icon": "滞行牌",
"quality": 1,
"target": 0,

@ -20,6 +20,8 @@ interface FlySwordPending {
buff2: string;
/** 距发射(秒) */
wait: number;
/** 发次(弧线上下交错) */
shotIndex: number;
/** 表现通道 */
channel: SkillVisualChannel;
}
@ -62,6 +64,7 @@ export class FlySwordHost {
buff1,
buff2,
wait: launched * Math.max(0, intervalSec),
shotIndex: launched,
channel,
});
launched += 1;
@ -84,7 +87,7 @@ export class FlySwordHost {
p.wait -= dt;
if (p.wait > 0) continue;
this._pending.splice(i, 1);
if (!p.channel.launchFlySword(p.caster, p.abilityId, p.target, p.scale, p.buff1, p.buff2)) {
if (!p.channel.launchFlySword(p.caster, p.abilityId, p.target, p.scale, p.buff1, p.buff2, p.shotIndex)) {
console.log('[FlySword] 发射失败', p.target);
}
}

@ -103,6 +103,12 @@ const WATER_BALL_SPEED = BOW_ATK_ARROW_SPEED;
const FLY_SWORD_PREFAB = '飞剑';
/** 飞剑默认 Spine 动画 */
const FLY_SWORD_ANIM = '飞剑';
/** 飞剑弧高占射程比例(弦中点上下偏移) */
const FLY_SWORD_ARC_T = 0.32;
/** 飞剑弧高下限(像素) */
const FLY_SWORD_ARC_MIN = 56;
/** 同向高阶剑弧高相对基础的递增值 */
const FLY_SWORD_ARC_TIER = 0.38;
/** battleHero/prefab/skill/玉帝飞剑 */
const YUDI_FLY_SWORD_PREFAB = '玉帝飞剑';
/** 飞剑预制体默认动画(带光效) */
@ -182,6 +188,49 @@ class WaterBallRecycle {
}
}
/**
* 飞剑飞行绑定:追踪落点 + 落地回池。
*/
class FlySwordRecycle {
private static readonly _pool: FlySwordRecycle[] = [];
private _channel: SkillVisualChannel | null = null;
private _node: Node | null = null;
private _target: EntityId = 0;
/** 每帧刷新终点;目标失效则沿用上次 */
readonly resolveToWorld = (_node: Node): Vec3 | null => {
if (!(this._target > 0)) return null;
return SkillPresentationEnv.getTargetWorldPos(this._target);
};
/** 落地:回池飞剑 */
readonly onLand = (): void => {
const channel = this._channel;
const node = this._node;
this._channel = null;
this._node = null;
this._target = 0;
FlySwordRecycle._pool.push(this);
if (channel && node) channel.recycleWaterBall(node);
};
/**
* 取得绑定。
* @param channel 表现通道
* @param node 飞剑
* @param target 锁定目标
*/
static obtain(channel: SkillVisualChannel, node: Node, target: EntityId): FlySwordRecycle {
const rec = FlySwordRecycle._pool.length > 0
? FlySwordRecycle._pool.pop()!
: new FlySwordRecycle();
rec._channel = channel;
rec._node = node;
rec._target = target;
return rec;
}
}
/** 待发射的弓箭 */
interface BowArrowLaunch {
/** 代数,过期则丢弃 */
@ -1382,16 +1431,17 @@ export class SkillVisualChannel {
}
/**
* 飞剑武器主动:实例化「飞剑」直线飞向目标,落地结算。
* 飞剑武器主动:弧线追踪飞向目标;多发时上下弧高交错。
* @param caster 施法者
* @param abilityId Ability id
* @param target 锁定目标
* @param scale 伤害倍率
* @param buff1 atk_buff1
* @param buff2 atk_buff2
* @param shotIndex 发次(0 起,奇偶上下弧)
* @returns 是否发出
*/
launchFlySword(caster: EntityId, abilityId: string, target: EntityId, scale: number, buff1: string, buff2: string): boolean {
launchFlySword(caster: EntityId, abilityId: string, target: EntityId, scale: number, buff1: string, buff2: string, shotIndex = 0): boolean {
const fromNode = this._weapon.skinAnim?.node ?? this._weapon.aim ?? this._weapon.root;
const fromWorld = fromNode?.isValid
? fromNode.worldPosition
@ -1411,17 +1461,19 @@ export class SkillVisualChannel {
const duration = Math.max(0.12, dist / WATER_BALL_SPEED);
const node = this._spawnFlySwordPrefab(this._arrowFrom);
if (!node) return false;
const arcHeight = this._flySwordArcHeight(dist, shotIndex);
const land = FlySwordRecycle.obtain(this, node, target);
SkillTransformDriver.animateParabola({
node,
duration,
fromWorld: this._arrowFrom,
toWorld: this._arrowTo,
arcHeight: 0,
arcHeight,
resolveToWorld: land.resolveToWorld,
easing: SkillTransformDriver.easeLinear,
faceTangent: true,
});
SkillTransformDriver.stepNow();
const land = WaterBallRecycle.obtain(this, node);
getBowAtkHost().spawnVolley({
caster,
target,
@ -1435,6 +1487,19 @@ export class SkillVisualChannel {
return true;
}
/**
* 飞剑弧高:奇偶上下交错,同向越高阶弧越大。
* @param dist 射程
* @param shotIndex 发次
*/
private _flySwordArcHeight(dist: number, shotIndex: number): number {
const idx = Math.max(0, Math.floor(shotIndex));
const base = Math.max(FLY_SWORD_ARC_MIN, dist * FLY_SWORD_ARC_T);
const sign = (idx % 2 === 0) ? 1 : -1;
const tier = Math.floor(idx / 2);
return sign * base * (1 + tier * FLY_SWORD_ARC_TIER);
}
launchVolleyShot(caster: EntityId, abilityId: string, target: EntityId, scale: number, buff1: string, buff2: string, shotIndex: number, aimWorld: Readonly<Vec3>): boolean {
const castAnim = String(this._castAnimParams?.animation ?? '').trim();
if (this._isStaffWeapon() || this._isStaffProjectileAnim(castAnim)) {

@ -59,6 +59,7 @@ export function playHitLeanBack(target: Node, hitDirX: number, hitDirY = 0): voi
if (!target.isValid) return;
target.angle = 0;
target.setScale(1, 1, 1);
delete (target as Node & { [PROXY_KEY]?: LeanProxy })[PROXY_KEY];
})
.start();
}

@ -29,6 +29,8 @@ export enum MonsterTag {
const BLOOD_BAR_SORT_LAYER = 3;
/** 血条底框 sortingOrder;进度子节点 +1,避免底框盖住填充 */
const BLOOD_BAR_SORT_ORDER = 8;
/** Boss 左右朝向:累计水平位移超过此值才切面(世界像素;speed=25 时约 0.5s 内可切) */
const BOSS_FACE_DX_MIN = 2;
/** 还魂丹复活特效(battleHero/prefab/skill/小怪复活效果) */
const REVIVE_FX_PREFAB = '小怪复活效果';
/** 还魂丹复活 Spine 动画 */
@ -108,6 +110,12 @@ export class Monster extends Component {
/** 加速特效朝向用:上一帧世界坐标 */
private _accelFxLastX = Number.NaN;
private _accelFxLastY = Number.NaN;
/** Boss 左右朝向:上一帧世界 X */
private _faceLastX = Number.NaN;
/** Boss 左右朝向:未达阈值的累计水平位移 */
private _faceAccX = 0;
/** Boss 朝向:资源默认朝右,+1=朝右,-1=朝左 */
private _faceSign = 1;
/** playSpine 传入的基础倍速(不含 BattleRuntime.TimeScale) */
private _animBaseTimeScale = 1;
@ -136,6 +144,7 @@ export class Monster extends Component {
this.useMxMovement = !!gg.game.CurentBattle?.useMxMovement;
this.init();
this._resetMoveFacing();
this._lastAttackTime = 0;
if (this.mechanismImmobile) {
this.state = MonsterState.STOP;
@ -155,12 +164,80 @@ export class Monster extends Component {
}
protected update(_dt: number): void {
if (!this._accelerateFx?.isValid || !this._accelerateFx.active) return;
const battle = gg.game.CurentBattle;
if (!battle || battle.IsGameOver || battle.IsGamePause) return;
this._tickMoveFacing();
if (!this._accelerateFx?.isValid || !this._accelerateFx.active) return;
this._tickAccelerateFxAngle();
}
/**
* Boss 按水平位移切左右朝向:只翻 skin 下怪物体节点(如 BOSS土)。
* 血条挂在模板 root 的 bloodPos,不受影响;受击后仰作用在 skin,也不冲掉体朝向。
*/
private _tickMoveFacing(): void {
if (!this.Data || !MonsterDataManager.isBossType(this.Data.type)) return;
if (this.IsDead || this.mechanismImmobile) return;
const body = this._bossBodyNode();
if (!body || !this.node?.isValid) return;
const x = this.node.worldPosition.x;
if (!Number.isFinite(this._faceLastX)) {
this._faceLastX = x;
this._faceAccX = 0;
this._applyFaceSign(body, this._faceSign);
return;
}
const dx = x - this._faceLastX;
this._faceLastX = x;
this._faceAccX += dx;
if (Math.abs(this._faceAccX) >= BOSS_FACE_DX_MIN) {
// 资源默认朝右:右移 +1,左移 -1
this._faceSign = this._faceAccX > 0 ? 1 : -1;
this._faceAccX = 0;
}
this._applyFaceSign(body, this._faceSign);
}
/**
* skin 下的怪物体节点(挂载的皮肤预制体根)。
*/
private _bossBodyNode(): Node | null {
const skin = this.skin;
if (!skin?.isValid) return null;
const kids = skin.children;
for (let i = 0; i < kids.length; i++) {
const child = kids[i];
if (child?.isValid) return child;
}
return null;
}
/**
* 写入怪物体左右朝向。
* @param body skin 下怪物节点
* @param sign ±1
*/
private _applyFaceSign(body: Node, sign: number): void {
const s = sign < 0 ? -1 : 1;
const sy = Math.abs(body.scale.y) || 1;
const sz = body.scale.z || 1;
if (Math.sign(body.scale.x || 1) === s && Math.abs(Math.abs(body.scale.x) - 1) < 1e-3) return;
body.setScale(s, sy, sz);
}
/** 复位 Boss 朝向(出池 / 回池) */
private _resetMoveFacing(): void {
this._faceLastX = Number.NaN;
this._faceAccX = 0;
this._faceSign = 1;
if (this.skin?.isValid) {
this.skin.setScale(1, Math.abs(this.skin.scale.y) || 1, this.skin.scale.z || 1);
}
const body = this._bossBodyNode();
if (!body) return;
body.setScale(1, Math.abs(body.scale.y) || 1, body.scale.z || 1);
}
/** 战斗 TimeScale 变化时同步 Spine 播放倍速 */
private _syncAnimBattleTimeScale(): void {
if (!this.anim?.isValid || this._isDeathAnimPlaying()) return;
@ -1473,6 +1550,7 @@ export class Monster extends Component {
this.clearAddHpEff();
this.clearHpCapEff();
this.clearReviveFx();
this._resetMoveFacing();
if (this.anim?.isValid) {
BattleSpinePlayback.clearPendingComplete(this.anim);
BattleSpinePlayback.resetTracks(this.anim);

Loading…
Cancel
Save