diff --git a/assets/bundles/battleHero/prefab/skill/水弹.prefab b/assets/bundles/battleHero/prefab/skill/水弹.prefab index 8cdd929..bf71a1b 100644 --- a/assets/bundles/battleHero/prefab/skill/水弹.prefab +++ b/assets/bundles/battleHero/prefab/skill/水弹.prefab @@ -124,8 +124,8 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 322, - "height": 122 + "width": 139, + "height": 50 }, "_anchorPoint": { "__type__": "cc.Vec2", @@ -161,7 +161,7 @@ "a": 255 }, "_spriteFrame": { - "__uuid__": "d0abd767-3ed4-49ca-b7dd-e295c41cd319@f9941", + "__uuid__": "4ad93541-2be9-43ab-8d9b-6f7fbd86a297@f9941", "__expectedType__": "cc.SpriteFrame" }, "_type": 0, diff --git a/assets/bundles/first/config/Skill.json b/assets/bundles/first/config/Skill.json index 2c73bbc..d5915dd 100644 --- a/assets/bundles/first/config/Skill.json +++ b/assets/bundles/first/config/Skill.json @@ -888,7 +888,7 @@ "level": 0, "cd": 20, "priority": 100, - "animation": "悟空专属技能效果", + "animation": "原地旋转大风车", "showeffect": "", "target_stage": 0, "range": 0, @@ -910,7 +910,7 @@ "level": 0, "cd": 20, "priority": 100, - "animation": "攻击1", + "animation": "悟空专属技能效果", "showeffect": "", "target_stage": 0, "range": 0, @@ -954,7 +954,7 @@ "level": 0, "cd": 20, "priority": 100, - "animation": "悟空专属技能效果", + "animation": "原地旋转大风车", "showeffect": "", "target_stage": 0, "range": 0, @@ -976,7 +976,7 @@ "level": 0, "cd": 20, "priority": 100, - "animation": "攻击1", + "animation": "悟空专属技能效果", "showeffect": "", "target_stage": 0, "range": 0, @@ -1020,7 +1020,7 @@ "level": 0, "cd": 20, "priority": 100, - "animation": "悟空专属技能效果", + "animation": "原地旋转大风车", "showeffect": "", "target_stage": 0, "range": 0, @@ -1042,7 +1042,7 @@ "level": 0, "cd": 20, "priority": 100, - "animation": "攻击1", + "animation": "悟空专属技能效果击1", "showeffect": "", "target_stage": 0, "range": 0, @@ -1086,7 +1086,7 @@ "level": 0, "cd": 20, "priority": 100, - "animation": "悟空专属技能效果", + "animation": "原地旋转大风车", "showeffect": "", "target_stage": 0, "range": 0, @@ -1108,7 +1108,7 @@ "level": 0, "cd": 0, "priority": 100, - "animation": "攻击1", + "animation": "悟空专属技能效果", "showeffect": "", "target_stage": 0, "range": 0, diff --git a/assets/res/battleGameWeapon/spine/武器/法杖/冰锥.png b/assets/res/battleGameWeapon/spine/武器/法杖/冰锥.png new file mode 100644 index 0000000..3151db0 Binary files /dev/null and b/assets/res/battleGameWeapon/spine/武器/法杖/冰锥.png differ diff --git a/assets/res/battleGameWeapon/spine/武器/法杖/冰锥.png.meta b/assets/res/battleGameWeapon/spine/武器/法杖/冰锥.png.meta new file mode 100644 index 0000000..1618c4b --- /dev/null +++ b/assets/res/battleGameWeapon/spine/武器/法杖/冰锥.png.meta @@ -0,0 +1,134 @@ +{ + "ver": "1.0.27", + "importer": "image", + "imported": true, + "uuid": "4ad93541-2be9-43ab-8d9b-6f7fbd86a297", + "files": [ + ".json", + ".png" + ], + "subMetas": { + "6c48a": { + "importer": "texture", + "uuid": "4ad93541-2be9-43ab-8d9b-6f7fbd86a297@6c48a", + "displayName": "冰锥", + "id": "6c48a", + "name": "texture", + "userData": { + "wrapModeS": "clamp-to-edge", + "wrapModeT": "clamp-to-edge", + "imageUuidOrDatabaseUri": "4ad93541-2be9-43ab-8d9b-6f7fbd86a297", + "isUuid": true, + "visible": false, + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "anisotropy": 0 + }, + "ver": "1.0.22", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + }, + "f9941": { + "importer": "sprite-frame", + "uuid": "4ad93541-2be9-43ab-8d9b-6f7fbd86a297@f9941", + "displayName": "冰锥", + "id": "f9941", + "name": "spriteFrame", + "userData": { + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 0, + "trimY": 0, + "width": 139, + "height": 50, + "rawWidth": 139, + "rawHeight": 50, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "packable": true, + "pixelsToUnit": 100, + "pivotX": 0.5, + "pivotY": 0.5, + "meshType": 0, + "vertices": { + "rawPosition": [ + -69.5, + -25, + 0, + 69.5, + -25, + 0, + -69.5, + 25, + 0, + 69.5, + 25, + 0 + ], + "indexes": [ + 0, + 1, + 2, + 2, + 1, + 3 + ], + "uv": [ + 0, + 50, + 139, + 50, + 0, + 0, + 139, + 0 + ], + "nuv": [ + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1 + ], + "minPos": [ + -69.5, + -25, + 0 + ], + "maxPos": [ + 69.5, + 25, + 0 + ] + }, + "isUuid": true, + "imageUuidOrDatabaseUri": "4ad93541-2be9-43ab-8d9b-6f7fbd86a297@6c48a", + "atlasUuid": "", + "trimType": "auto" + }, + "ver": "1.0.12", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + } + }, + "userData": { + "type": "sprite-frame", + "fixAlphaTransparencyArtifacts": false, + "hasAlpha": true, + "redirect": "4ad93541-2be9-43ab-8d9b-6f7fbd86a297@6c48a" + } +} diff --git a/assets/res/battleGameWeapon/spine/武器/长枪/土块溅射.png b/assets/res/battleGameWeapon/spine/武器/长枪/土块溅射.png new file mode 100644 index 0000000..f8f47ab Binary files /dev/null and b/assets/res/battleGameWeapon/spine/武器/长枪/土块溅射.png differ diff --git a/assets/res/battleGameWeapon/spine/武器/长枪/土块溅射.png.meta b/assets/res/battleGameWeapon/spine/武器/长枪/土块溅射.png.meta new file mode 100644 index 0000000..8747581 --- /dev/null +++ b/assets/res/battleGameWeapon/spine/武器/长枪/土块溅射.png.meta @@ -0,0 +1,134 @@ +{ + "ver": "1.0.27", + "importer": "image", + "imported": true, + "uuid": "84452a93-0349-435e-b732-8f7814e38a98", + "files": [ + ".json", + ".png" + ], + "subMetas": { + "6c48a": { + "importer": "texture", + "uuid": "84452a93-0349-435e-b732-8f7814e38a98@6c48a", + "displayName": "土块溅射", + "id": "6c48a", + "name": "texture", + "userData": { + "wrapModeS": "clamp-to-edge", + "wrapModeT": "clamp-to-edge", + "imageUuidOrDatabaseUri": "84452a93-0349-435e-b732-8f7814e38a98", + "isUuid": true, + "visible": false, + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "anisotropy": 0 + }, + "ver": "1.0.22", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + }, + "f9941": { + "importer": "sprite-frame", + "uuid": "84452a93-0349-435e-b732-8f7814e38a98@f9941", + "displayName": "土块溅射", + "id": "f9941", + "name": "spriteFrame", + "userData": { + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 0, + "trimY": 0, + "width": 139, + "height": 50, + "rawWidth": 139, + "rawHeight": 50, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "packable": true, + "pixelsToUnit": 100, + "pivotX": 0.5, + "pivotY": 0.5, + "meshType": 0, + "vertices": { + "rawPosition": [ + -69.5, + -25, + 0, + 69.5, + -25, + 0, + -69.5, + 25, + 0, + 69.5, + 25, + 0 + ], + "indexes": [ + 0, + 1, + 2, + 2, + 1, + 3 + ], + "uv": [ + 0, + 50, + 139, + 50, + 0, + 0, + 139, + 0 + ], + "nuv": [ + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1 + ], + "minPos": [ + -69.5, + -25, + 0 + ], + "maxPos": [ + 69.5, + 25, + 0 + ] + }, + "isUuid": true, + "imageUuidOrDatabaseUri": "84452a93-0349-435e-b732-8f7814e38a98@6c48a", + "atlasUuid": "", + "trimType": "auto" + }, + "ver": "1.0.12", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + } + }, + "userData": { + "type": "sprite-frame", + "fixAlphaTransparencyArtifacts": false, + "hasAlpha": true, + "redirect": "84452a93-0349-435e-b732-8f7814e38a98@6c48a" + } +} diff --git a/assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts b/assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts index 86bcd8e..86d81cb 100644 --- a/assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts +++ b/assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts @@ -42,9 +42,9 @@ export interface SkillPresentationParams { hitCount?: number; /** atk_type=5 / 10 / 14 / 16 / 18 / 20 段间间隔(秒) */ hitIntervalSec?: number; - /** atk_type=18 / 20 主目标圆半径(像素) */ + /** atk_type=18 主目标圆 / 20 释放点圆 半径(像素) */ aroundPrimaryRadius?: number; - /** atk_type=18 / 20 每段伤害万分比 */ + /** atk_type=14 / 18 / 20 每段伤害万分比 */ damageWan?: number; } @@ -361,10 +361,10 @@ export class SkillAbilityBuilder { * @param atkParts atk_para 分段 * @param atkHead 攻击类型头 */ - private static _countIntervalHitParams(row: ITableSkill, atkParts: number[], atkHead: number): Pick { + private static _countIntervalHitParams(row: ITableSkill, atkParts: number[], atkHead: number): Pick { const parsed = this._parseCountIntervalWan(row, atkParts, atkHead); if (!parsed) return {}; - return { hitCount: parsed.hitCount, hitIntervalSec: parsed.intervalSec }; + return { hitCount: parsed.hitCount, hitIntervalSec: parsed.intervalSec, damageWan: parsed.wan }; } /** @@ -664,9 +664,9 @@ export class SkillAbilityBuilder { } // 12 散射 / 13 连发 / 17 不穿透散射 / 19 冰锥:伤害由 BowAtkHost 弹道结算,无 deferred 段 - // 18 / 20 目标圆多段:每段按落点圆重新索敌结算,无 deferred 段 + // 14 360°多段 / 18 / 20:每段按圆重新索敌结算,无 deferred 段 // 飞剑主动(atk_type=10 子集):落地结算,无 deferred 段 - if (atkHead === 12 || atkHead === 13 || atkHead === 17 || atkHead === 18 || atkHead === 19 || atkHead === 20 + if (atkHead === 12 || atkHead === 13 || atkHead === 14 || atkHead === 17 || atkHead === 18 || atkHead === 19 || atkHead === 20 || isFlySwordSkillId(row.id)) { return []; } @@ -675,9 +675,9 @@ export class SkillAbilityBuilder { return []; } - if (atkHead === 5 || atkHead === 10 || atkHead === 14 || atkHead === 16) { + if (atkHead === 5 || atkHead === 10 || atkHead === 16) { let hitCount = Math.max(1, Math.floor(atkParts[2] || 1)); - if (atkHead === 10 || atkHead === 14) { + if (atkHead === 10) { const parsed = this._parseCountIntervalWan(row, atkParts, atkHead); if (!parsed) return []; hitCount = parsed.hitCount; diff --git a/assets/script/battle/presentation/AtkTypePresenters.ts b/assets/script/battle/presentation/AtkTypePresenters.ts index 2707300..2cd82c3 100644 --- a/assets/script/battle/presentation/AtkTypePresenters.ts +++ b/assets/script/battle/presentation/AtkTypePresenters.ts @@ -63,6 +63,7 @@ export class LineRectAtkPresenter implements ISkillPresenter { /** * atk_type=5 直线多段 / 14 360°多段 / 16 扇形多段:按 hitIntervalSec 多次 hit,每段独立结算范围内伤害。 + * atk_type=14:武器动画循环至全部段结束(与伤害时长同步,如原地旋转大风车)。 */ export class LineMultiHitAtkPresenter implements ISkillPresenter { present(ctx: SkillPresentationContext, host: Skill): void { @@ -72,19 +73,28 @@ export class LineMultiHitAtkPresenter implements ISkillPresenter { } const skillKind = String(ctx.params.skillKind ?? ''); const followWeapon = skillKind === 'weapon' || host.visualChannel.useWeapon; + const atkHead = Math.floor(Number(ctx.params.atkTypeHead ?? ctx.params.atkType) || 0); + const loopCast = atkHead === 14; const seq = host.beginPresentationCast(ctx); if (!(seq > 0)) return; + if (loopCast) host.disarmAnimComplete(); const animTs = host.visualChannel.playCastAnim({ caster: ctx.caster, animation: String(ctx.params.animation ?? ''), followWeapon, skillKind, primaryTarget: ctx.targets[0] ?? null, - onAnimComplete: host.onPresentationAnimCompleteBound, + holdCasting: loopCast, + loopWeapon: loopCast, + onAnimComplete: loopCast + ? LineMultiHitAtkPresenter._noopAnimComplete + : host.onPresentationAnimCompleteBound, }); const firstDelay = PRES_HIT_AT_BASE / Math.max(0.01, animTs); host.schedulePresentationMultiHit(seq, ctx, firstDelay); } + + private static _noopAnimComplete = (): void => {}; } /** diff --git a/assets/script/battle/skill/Skill.ts b/assets/script/battle/skill/Skill.ts index 35b2e5b..58ca5f2 100644 --- a/assets/script/battle/skill/Skill.ts +++ b/assets/script/battle/skill/Skill.ts @@ -28,7 +28,7 @@ import { SkillPresenterRouter } from '../presentation/SkillPresenterRouter'; import { getAllySkillExtraHost } from '../presentation/AllySkillExtraHost'; import { getFaXiangHost } from '../presentation/FaXiangHost'; import { SkillVisualChannel } from '../presentation/SkillVisualChannel'; -import { getGroundCircleHost } from '../presentation/ThrownCircleAtkPresenter'; +import { getGroundCircleHost, logicPosOf, pulseHostileCircle } from '../presentation/ThrownCircleAtkPresenter'; import { getStaffSmashHost } from '../presentation/StaffSmashPresenter'; import { SkillPresentationEnv } from './SkillPresentationEnv'; import { SkillShowEffectPlayer } from './SkillShowEffect'; @@ -471,6 +471,10 @@ export class Skill extends Component { private _presMultiHitSeq = 0; private _presMultiHitIndex = 0; private _presMultiHitCtx: SkillPresentationContext | null = null; + /** atk_type=14 当前脉冲用的表现上下文 */ + private _presCirclePulseCtx: SkillPresentationContext | null = null; + /** atk_type=14 当前脉冲伤害倍率 */ + private _presCirclePulseScale = 0; /** 12 散射 / 13 连发 */ private _presBowShape = 0; /** 散射是否已出手 */ @@ -504,22 +508,21 @@ export class Skill extends Component { private _onPresMultiHitTick = (): void => { const seq = this._presMultiHitSeq; const ctx = this._presMultiHitCtx; - if (!ctx || !this.isPresentationCast(seq) || !ctx.isAlive) return; + if (!ctx || !this.isPresentationCast(seq) || !ctx.isAlive) { + this._finishMultiHitCast(seq, ctx); + return; + } const atkHead = Math.floor(Number(ctx.params.atkTypeHead ?? ctx.params.atkType) || 0); if (atkHead === 10 && !this._prepareSequentialSmash(ctx)) { - this._presMultiHitCtx = null; - getStaffSmashHost().stopSpawning(ctx.caster); - if (this._presAnimSeq === seq && !this._presAnimComplete) return; - this.finishPresentationCast(seq, false); + this._finishMultiHitCast(seq, ctx, true); return; } - ctx.hit(); + if (atkHead === 14) this._pulseCasterCircleHit(ctx); + else ctx.hit(); const hitCount = Math.max(1, Math.floor(Number(ctx.params.hitCount) || 1)); const next = this._presMultiHitIndex + 1; if (next >= hitCount) { - this._presMultiHitCtx = null; - if (this._presAnimSeq === seq && !this._presAnimComplete) return; - this.finishPresentationCast(seq, false); + this._finishMultiHitCast(seq, ctx); return; } this._presMultiHitIndex = next; @@ -527,6 +530,61 @@ export class Skill extends Component { this.schedulePresentation(this._onPresMultiHitTick, interval); }; + /** + * atk_type=14:以施法者为圆心、射程为半径,本段重新索敌并结算。 + * @param ctx 表现上下文 + */ + private _pulseCasterCircleHit(ctx: SkillPresentationContext): void { + const session = SkillPresentationEnv.getBattleSession(); + if (!session || !session.world.isAlive(ctx.caster)) return; + const center = logicPosOf(session, ctx.caster); + if (!center) { + console.log('[Skill] atk_type=14 无释放点', ctx.params.skillId); + return; + } + let radius = Math.max(0, Number(ctx.params.stageRangePx) || 0); + if (!(radius > 0)) { + radius = Math.max(0, session.attributes.getAttr(ctx.caster, AttrId.Range) || 0); + } + if (!(radius > 0)) { + console.log('[Skill] atk_type=14 无有效射程', ctx.params.skillId); + return; + } + const wan = Number(ctx.params.damageWan); + if (!Number.isFinite(wan)) { + console.log('[Skill] atk_type=14 伤害万分比无效', ctx.params.skillId, ctx.params.atkPara); + return; + } + this._presCirclePulseCtx = ctx; + this._presCirclePulseScale = wan / 10000; + pulseHostileCircle(session, ctx.caster, center.x, center.y, radius, this._onCasterCircleHit); + this._presCirclePulseCtx = null; + } + + private readonly _onCasterCircleHit = (id: EntityId): void => { + const ctx = this._presCirclePulseCtx; + if (!ctx?.isAlive) return; + ctx.dealDamage(id, { scale: this._presCirclePulseScale, damageType: 'caster_circle' }); + }; + + /** + * 多段命中结束:atk_type=14 先停循环武器动画再收招。 + * @param seq 播控序号 + * @param ctx 表现上下文(可空) + * @param stopSmash 是否停连砸生成 + */ + private _finishMultiHitCast(seq: number, ctx: SkillPresentationContext | null, stopSmash = false): void { + const atkHead = ctx + ? Math.floor(Number(ctx.params.atkTypeHead ?? ctx.params.atkType) || 0) + : 0; + this._presMultiHitCtx = null; + if (stopSmash && ctx) getStaffSmashHost().stopSpawning(ctx.caster); + if (atkHead === 14) this.visualChannel.endHoldCast(); + if (!this.isPresentationCast(seq)) return; + if (this._presAnimSeq === seq && !this._presAnimComplete) return; + this.finishPresentationCast(seq, false); + } + /** * atk_type=10:本段只打一个存活单体。无存活目标返回 false。 * @param ctx 表现上下文 diff --git a/assets/script/battle/view/CocosBattleCombatPresenter.ts b/assets/script/battle/view/CocosBattleCombatPresenter.ts index 4d14582..9ed3cc5 100644 --- a/assets/script/battle/view/CocosBattleCombatPresenter.ts +++ b/assets/script/battle/view/CocosBattleCombatPresenter.ts @@ -48,6 +48,7 @@ type MonsterView = IBattleMonsterView & { clearSlowFx?(): void; showFreezeFx?(durationSec: number): void; clearFreezeFx?(): void; + clearFreezeFxImmediate?(): void; showBurnFx?(durationSec: number): void; clearBurnFx?(): void; showBleedFx?(durationSec: number): void; @@ -223,7 +224,7 @@ export class CocosBattleCombatPresenter { case BattleEventType.EntityKilled: this._onStunRemoved(event.victim); this._onSlowRemoved(event.victim); - this._onFreezeRemoved(event.victim); + this._getMonsterView(event.victim)?.clearFreezeFxImmediate?.(); this._onBurnRemoved(event.victim); this._onBleedRemoved(event.victim); this._onGhostBiteRemoved(event.victim); @@ -232,7 +233,7 @@ export class CocosBattleCombatPresenter { case BattleEventType.UnitDespawned: this._onStunRemoved(event.entity); this._onSlowRemoved(event.entity); - this._onFreezeRemoved(event.entity); + this._getMonsterView(event.entity)?.clearFreezeFxImmediate?.(); this._onBurnRemoved(event.entity); this._onBleedRemoved(event.entity); this._onGhostBiteRemoved(event.entity); diff --git a/assets/script/ui/BattleGame/Monster.ts b/assets/script/ui/BattleGame/Monster.ts index a6414b1..a226ada 100644 --- a/assets/script/ui/BattleGame/Monster.ts +++ b/assets/script/ui/BattleGame/Monster.ts @@ -873,15 +873,25 @@ export class Monster extends Component { }; /** - * 显示冰冻覆盖(prefab/冰冻),挂怪体播「怪物冻结」;按时长或 clear 时播「怪物冻结2」再回收。 - * @param durationSec 持续秒 + * 显示冰冻覆盖(prefab/冰冻):定格「怪物冻结」首帧冰块;刷新时长不重播,避免闪。 + * 解冻由 clearFreezeFx 播「怪物冻结2」。 + * @param _durationSec 持续秒(表现由 BuffRemoved 驱动结束,参数仅兼容调用方) */ - showFreezeFx(durationSec: number): void { + showFreezeFx(_durationSec: number): void { if (!this.node?.isValid || this.IsDead) return; const parent = this.skin?.isValid ? this.skin : this.node; + + if (this._freezeFx?.isValid && !this._freezeFxThawing) { + if (this._freezeFx.parent !== parent) { + this._freezeFx.parent = parent; + this._freezeFx.setPosition(0, 0, 0); + } + this._freezeFx.active = true; + return; + } + ++this._freezeFxSeq; this._freezeFxThawing = false; - this.unschedule(this._onFreezeFxTimeout); if (!this._freezeFx?.isValid) { let node: Node | null = null; @@ -899,27 +909,24 @@ export class Monster extends Component { node.setPosition(0, 0, 0); node.active = true; this._freezeFx = node; - } else if (this._freezeFx.parent !== parent) { + } else { this._freezeFx.parent = parent; this._freezeFx.setPosition(0, 0, 0); this._freezeFx.active = true; } const sk = this._freezeFx.getComponentInChildren(sp.Skeleton); - if (sk?.isValid) { - sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.SHARED_CACHE); - BattleSpinePlayback.play(sk, FREEZE_FX_ANIM, { loop: true }); - } - - const dur = Math.max(0.05, durationSec); - this.scheduleOnce(this._onFreezeFxTimeout, dur); + if (sk?.isValid) this._holdFreezeIcePose(sk); } /** - * 解冻:播「怪物冻结2」后回池;无节点则直接结束。 + * 解冻:播「怪物冻结2」破冰后回池;死亡则立即回收。 */ clearFreezeFx(): void { - this.unschedule(this._onFreezeFxTimeout); + if (this.IsDead) { + this.clearFreezeFxImmediate(); + return; + } if (this._freezeFxThawing) return; const node = this._freezeFx; if (!node?.isValid) { @@ -933,33 +940,39 @@ export class Monster extends Component { return; } this._freezeFxThawing = true; - const seq = ++this._freezeFxSeq; + ++this._freezeFxSeq; + // 附件切换动画须 REALTIME;破冰前恢复倍速 + sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME); + sk.timeScale = BattleSpinePlayback.resolveCombinedTimeScale(1); const ok = BattleSpinePlayback.play(sk, FREEZE_THAW_ANIM, { loop: false, onComplete: this._onFreezeThawComplete, }); - if (!ok) { - if (seq !== this._freezeFxSeq) return; - this._dropFreezeFx(); - } + if (!ok) this._dropFreezeFx(); } /** 立即回池冰冻特效(死亡/清池,不解冻动画) */ clearFreezeFxImmediate(): void { this._freezeFxSeq++; - this.unschedule(this._onFreezeFxTimeout); this._dropFreezeFx(); } - private _onFreezeFxTimeout = (): void => { - this.clearFreezeFx(); - }; - private readonly _onFreezeThawComplete = (): void => { if (!this._freezeFxThawing) return; this._dropFreezeFx(); }; + /** + * 「怪物冻结」末帧会卸附件;定格首帧保持冰块,禁止 loop。 + * @param sk 冰冻 Spine + */ + private _holdFreezeIcePose(sk: sp.Skeleton): void { + BattleSpinePlayback.clearPendingComplete(sk); + sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME); + sk.setAnimation(0, FREEZE_FX_ANIM, false); + sk.timeScale = 0; + } + /** 回池并清空冰冻节点引用 */ private _dropFreezeFx(): void { this._freezeFxThawing = false; @@ -968,7 +981,10 @@ export class Monster extends Component { if (!node?.isValid) return; const sk = node.getComponentInChildren(sp.Skeleton); BattleSpinePlayback.clearPendingComplete(sk); - BattleSpinePlayback.resetTracks(sk); + if (sk?.isValid) { + sk.timeScale = 1; + BattleSpinePlayback.resetTracks(sk); + } gg.res.putNode(node); } diff --git a/assets/script/ui/BattleGame/UIBattleGame.ts b/assets/script/ui/BattleGame/UIBattleGame.ts index 79017f6..57f1f3e 100644 --- a/assets/script/ui/BattleGame/UIBattleGame.ts +++ b/assets/script/ui/BattleGame/UIBattleGame.ts @@ -81,6 +81,8 @@ const REFRESH_FILL_RANGE_MIN = -0.1; const REFRESH_BOUNCE_AMP = 0.1; /** 单段弹起/回落时长(秒) */ const REFRESH_BOUNCE_SEC = 0.22; +/** 开局未放置数字则强制开怪的等待秒数 */ +const FIRST_PLACE_COMBAT_TIMEOUT_SEC = 10; /** 砚台 Spine:资源里名字取反,出字过程播「待机」 */ const INKSTONE_SPINE_SPAWN = '待机'; @@ -440,7 +442,9 @@ export class UIBattleGame extends Auto_BattleGame { this._refreshWaveUi(); } - /** 开局不立刻出怪:等本机玩家第一次把数字放到地图后再 startCombat */ + /** + * 开局不立刻出怪:本机首次放数字开怪,或超时后强制开怪。 + */ private _listenFirstPlaceCombat(): void { this._unbindFirstPlaceCombat(); const bag = this._battle?.bagSession; @@ -449,10 +453,12 @@ export class UIBattleGame extends Auto_BattleGame { return; } bag.on(MergeEventType.TilePlaced, this._onFirstPlayerNumberPlaced); + this.scheduleOnce(this._onFirstPlaceCombatTimeout, FIRST_PLACE_COMBAT_TIMEOUT_SEC); } - /** 卸掉首次放字开怪监听 */ + /** 卸掉首次放字开怪监听与超时 */ private _unbindFirstPlaceCombat(): void { + this.unschedule(this._onFirstPlaceCombatTimeout); this._battle?.bagSession?.off(MergeEventType.TilePlaced, this._onFirstPlayerNumberPlaced); } @@ -465,6 +471,18 @@ export class UIBattleGame extends Auto_BattleGame { if (event.type !== MergeEventType.TilePlaced) return; const tileType = Math.floor(Number(WeaponComposeConfigProvider.instance.getWeapon2Type(event.weapon2Id))); if (!isElementGlyphType(tileType)) return; + this._startCombatFromFirstPlaceGate(); + }; + + /** 超时仍未放数字:强制开怪 */ + private readonly _onFirstPlaceCombatTimeout = (): void => { + if (this._combatStarted) return; + this._startCombatFromFirstPlaceGate(); + }; + + /** 首次放字 / 超时共用:开波并卸监听 */ + private _startCombatFromFirstPlaceGate(): void { + if (this._combatStarted) return; this._unbindFirstPlaceCombat(); this._combatStarted = true; gg.battle.startCombat(); diff --git a/docs/方案-出怪逻辑实现.md b/docs/方案-出怪逻辑实现.md index ac7018e..6b6ec93 100644 --- a/docs/方案-出怪逻辑实现.md +++ b/docs/方案-出怪逻辑实现.md @@ -103,7 +103,7 @@ UIPanelHome ← pickRandomBattleLevelId 进关 2. `GameController.enterBattle` → 预载 → `gg.battle.start` 3. `ChapterLevelBuilder.build` → `LevelWaveParser.toWaveDefs` 4. `UIBattleGame` → `loadMap` → `applyMapTopologyToSession(..., 'pvp')` -5. `CocosBattleMonsterPresenter.bind`;**本机玩家第一次把数字放到地图后** `startCombat` → 双口出怪 + 寻路表现 +5. `CocosBattleMonsterPresenter.bind`;**本机玩家第一次把数字放到地图后**,或开局 **10 秒仍未放置**,`startCombat` → 双口出怪 + 寻路表现 ## 4. 默认参数 diff --git a/docs/方案-战斗技能逻辑层.md b/docs/方案-战斗技能逻辑层.md index d80b932..a8098bf 100644 --- a/docs/方案-战斗技能逻辑层.md +++ b/docs/方案-战斗技能逻辑层.md @@ -109,7 +109,7 @@ Hero(合成等级 level) ### atk_type 14 360° 多段 -圈半径=`Skill.range`(≤0 则塔射程)。`atk_para`=`段数,间隔秒,伤害万分比`(例 `5,0.2,5000` = 5 段、间隔 0.2s、每段 50%)。开战锁圈内敌,每段 `hit()` 结算一段。有 `showeffect` 也只播预兆,**多段伤害出手即按 `PRES_HIT_AT_BASE` 起跳**,不等预兆。 +圈半径=`Skill.range`(≤0 则塔射程)。`atk_para`=`段数,间隔秒,伤害万分比`(例 `5,0.2,5000` = 5 段、间隔 0.2s、每段 50%)。**无 deferred 段**;开战仅作施法门槛,**每段伤害以施法者为圆心 `pulseHostileCircle` 重新索敌**后 `dealDamage`(含 `atk_buff`)。有 `showeffect` 也只播预兆,**多段伤害出手即按 `PRES_HIT_AT_BASE` 起跳**,不等预兆。**武器动画 `loopWeapon` 循环至全部段结束**(与伤害时长同步,如棍神通「原地旋转大风车」),段尽 `endHoldCast` 收招。 ### atk_type 10 连续多个单体 @@ -140,7 +140,7 @@ Hero(合成等级 level) ### atk_buff 13 冰冻 -参数:`概率,冰冻秒`。挂 `debuff_13`(定身 + 禁施法)。表现用 `冰冻` 预制体覆盖怪物播「怪物冻结」,解冻播「怪物冻结2」并飘「冰冻」。部署 loadout 不施加。 +参数:`概率,冰冻秒`。挂 `debuff_13`(定身 + 禁施法)。表现用 `冰冻` 预制体覆盖怪物:REALTIME 定格「怪物冻结」首帧冰块(不 loop,避免末帧卸附件闪烁);`BuffRemoved` 时播「怪物冻结2」破冰再回池;飘「冰冻」。部署 loadout 不施加。 ### atk_buff 14 点燃 @@ -199,3 +199,5 @@ Hero(合成等级 level) | 2026-10-08 | atk_type=18:丢出 `飞棍.prefab`(按半径缩放),落地后持续圈伤 | | 2026-10-08 | atk_type=20:主目标圆心放置 `暴风雪.prefab`;占锁并循环播「暴风雪技能」至圈伤结束 | | 2026-10-09 | 冰锥发射物改「冰锥」;冰冻覆盖「冰冻」播怪物冻结/解冻怪物冻结2 | +| 2026-10-09 | atk_type=14:武器动画循环至多段伤害结束(棍神通大风车与时长同步) | +| 2026-10-09 | atk_type=14:每段按施法者射程圆重新索敌,非开战锁定名单 | diff --git a/docs/方案-战斗表现数据驱动.md b/docs/方案-战斗表现数据驱动.md index d39ea7f..aca2643 100644 --- a/docs/方案-战斗表现数据驱动.md +++ b/docs/方案-战斗表现数据驱动.md @@ -144,6 +144,8 @@ M| 1 / 3 / 6 / 7 / 15 | 通道播动画 → drain hit(15:逻辑已锁主目 | 2026-09-30 | 补注册 atk_type=11 召唤(10051~10055)→ SummonAtkPresenter;表内 1~17 表现已全覆盖,仅 9(将武器形 30406)按兜底结算待策划定义攻效 | | 2026-10-08 | atk_type=19 冰锥 → IceConeAtkPresenter(水弹直线落地);18/20 已有独立 Presenter | | 2026-10-09 | atk_type=19 / 分裂冰锥发射物改「冰锥」;debuff_13 用「冰冻」覆盖播怪物冻结/解冻 | +| 2026-10-09 | atk_type=14:holdCasting+loopWeapon 至多段结束(棍神通「原地旋转大风车」) | +| 2026-10-09 | atk_type=14:每段 `pulseHostileCircle` 重扫施法者射程圆结算 | | 2026-10-08 | 飞剑主动 skillId 210101~250101 → FlySwordAtkPresenter(飞剑.prefab 落地伤);同 atk_type=10 的飞棍仍 StaffSmash | | 2026-10-08 | atk_type=18 ThrownCircle:改丢 `飞棍.prefab`(半径缩放=半径/175),落地后圈伤 | | 2026-10-08 | atk_type=20 GroundCircle:主目标圆心放置 `暴风雪.prefab`;占锁循环「暴风雪技能」至圈伤结束 | diff --git a/配置表/excelList/技能表.xlsx b/配置表/excelList/技能表.xlsx index 1d373d9..17648ce 100644 Binary files a/配置表/excelList/技能表.xlsx and b/配置表/excelList/技能表.xlsx differ