master
humingxing 1 week ago
parent 148599eff6
commit 4cb9a14ffe
  1. 6
      assets/bundles/battleHero/prefab/skill/水弹.prefab
  2. 16
      assets/bundles/first/config/Skill.json
  3. BIN
      assets/res/battleGameWeapon/spine/武器/法杖/冰锥.png
  4. 134
      assets/res/battleGameWeapon/spine/武器/法杖/冰锥.png.meta
  5. BIN
      assets/res/battleGameWeapon/spine/武器/长枪/土块溅射.png
  6. 134
      assets/res/battleGameWeapon/spine/武器/长枪/土块溅射.png.meta
  7. 16
      assets/script/battle/config/baseAtk/SkillAbilityBuilder.ts
  8. 12
      assets/script/battle/presentation/AtkTypePresenters.ts
  9. 78
      assets/script/battle/skill/Skill.ts
  10. 5
      assets/script/battle/view/CocosBattleCombatPresenter.ts
  11. 66
      assets/script/ui/BattleGame/Monster.ts
  12. 22
      assets/script/ui/BattleGame/UIBattleGame.ts
  13. 2
      docs/方案-出怪逻辑实现.md
  14. 6
      docs/方案-战斗技能逻辑层.md
  15. 2
      docs/方案-战斗表现数据驱动.md
  16. BIN
      配置表/excelList/技能表.xlsx

@ -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,

@ -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,

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@ -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"
}
}

@ -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<SkillPresentationParams, 'hitCount' | 'hitIntervalSec'> {
private static _countIntervalHitParams(row: ITableSkill, atkParts: number[], atkHead: number): Pick<SkillPresentationParams, 'hitCount' | 'hitIntervalSec' | 'damageWan'> {
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;

@ -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 => {};
}
/**

@ -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 表现上下文

@ -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);

@ -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);
}

@ -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();

@ -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. 默认参数

@ -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:每段按施法者射程圆重新索敌,非开战锁定名单 |

@ -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`;占锁循环「暴风雪技能」至圈伤结束 |

Loading…
Cancel
Save