import { Vec3 } from 'cc'; import type { BattleSession } from 'db://assets/mx/module/battle'; import { BattleTeamId } from 'db://assets/mx/module/battle/ecs/BattleTeamId'; import { AttrId, AttributeComp, TeamComp } from 'db://assets/mx/module/battle/ecs/Components'; import type { EntityId } from 'db://assets/mx/module/ecs'; import { TableNames } from '../../game/ConfigTableData'; import { WANG_MU_WEAPON1_ID } from '../heros/HeroActorRes'; import { WANGMU_ULT_BUFF_GROUP } from '../combat/TowerPositiveBuffState'; import { getTowerLoadoutService } from '../combat/TowerLoadoutService'; import { SkillPresentationEnv } from './SkillPresentationEnv'; import { isDesignDebugEnabled } from '../debug/DesignDebug'; export const ULT_FX_HOLD_SEC = 1.5; export const ULT_FX_FADE_SEC = 0.3; const FIELD_ATK_GROUP = 'wangmu_field_atk'; export interface WangMuUltParams { radiusPx: number; atkWan: number; aspdWan: number; critWan: number; critDamWan: number; durationSec: number; } /** 旧词条 type12 加攻扩展(配表已删,collect 恒空) */ interface Extra12 { attrCode: number; wan: number; } export function readWangMuUltParams( caster: EntityId, skillParts?: readonly number[] | null, ): WangMuUltParams | null { const parts = skillParts ?? SkillPresentationEnv.getWuSpinSkillParts(caster); if (!parts?.length || parts[0] !== 9 || parts.length < 7) return null; return { radiusPx: parts[1] ?? 0, atkWan: parts[2] ?? 0, aspdWan: parts[3] ?? 0, critWan: parts[4] ?? 0, critDamWan: parts[5] ?? 0, durationSec: parts[6] ?? 0, }; } export function applyWangMuUltBuffs( caster: EntityId, center: Readonly, radiusPx: number, skillParts?: readonly number[] | null, ): void { const session = SkillPresentationEnv.getBattleSession(); if (!session) return; const params = readWangMuUltParams(caster, skillParts); if (!params) return; const extras = collectType12(caster); const grantUltCrit = casterHasSpecialHead(caster, 13); const r2 = radiusPx * radiusPx; forEachAlly(session, caster, (id) => { const pos = allyWorldPos(id); if (!pos) return; const dx = pos.x - center.x; const dy = pos.y - center.y; if (dx * dx + dy * dy > r2) return; const weapon1Id = getTowerLoadoutService().getLoadout(id)?.weapon1Id ?? 0; applyTimedBuffs(session, id, params, extras, weapon1Id, grantUltCrit); }); refreshWangMuFieldAtkSteal(session); } export function refreshWangMuFieldAtkSteal(session: BattleSession): void { const loadoutSvc = getTowerLoadoutService(); session.world.view(TeamComp).forEach((id, team) => { if (!session.world.isAlive(id)) return; if (BattleTeamId.isEnemyTeam(team.team)) return; if (loadoutSvc.getLoadout(id)?.weapon1Id !== WANG_MU_WEAPON1_ID) return; applyFieldAtkSteal(session, id, team.ownerId); }); } function applyTimedBuffs( session: BattleSession, target: EntityId, params: WangMuUltParams, extras: readonly Extra12[], targetWeapon1Id: number, grantUltCrit: boolean, ): void { let atkWan = params.atkWan; let critWan = params.critWan; let critDamWan = params.critDamWan; for (const e of extras) { if (e.attrCode === 1) atkWan += e.wan; else if (e.attrCode === 3) critWan += e.wan; else if (e.attrCode === 4) critDamWan += e.wan; } const remain = params.durationSec; const attrs = session.attributes; if (atkWan) { attrs.addModifier(target, { attr: AttrId.Attack, op: 'Mul', value: 1 + atkWan / 10000, source: 'wangmu_ult_atk', group: WANGMU_ULT_BUFF_GROUP, remain, }); } if (params.aspdWan) { attrs.addModifier(target, { attr: 'aspd_add', op: 'Add', value: params.aspdWan, source: 'wangmu_ult_aspd', group: WANGMU_ULT_BUFF_GROUP, remain, }); } if (critWan) { attrs.addModifier(target, { attr: AttrId.CritRate, op: 'Add', value: critWan / 10000, source: 'wangmu_ult_crit', group: WANGMU_ULT_BUFF_GROUP, remain, }); } if (critDamWan) { attrs.addModifier(target, { attr: AttrId.CritDamage, op: 'Add', value: critDamWan / 10000, source: 'wangmu_ult_cdam', group: WANGMU_ULT_BUFF_GROUP, remain, }); } if (grantUltCrit) { attrs.addModifier(target, { attr: 'ult_can_crit', op: 'Add', value: 1, source: 'wangmu_ult_skcrit', group: WANGMU_ULT_BUFF_GROUP, remain, }); } } function applyFieldAtkSteal(session: BattleSession, caster: EntityId, ownerId: number): void { session.attributes.removeModifiersByGroup(caster, FIELD_ATK_GROUP); const wan = readType14Wan(caster); if (wan <= 0) return; const loadoutSvc = getTowerLoadoutService(); const allyLines: string[] = []; let sum = 0; forEachAllyByOwner(session, ownerId, (id) => { if (id === caster) return; const atk = attackWithoutPostMul(session, id); const w1 = loadoutSvc.getLoadout(id)?.weapon1Id ?? 0; sum += atk; allyLines.push( ` ${weapon1Name(w1)} entity=${id} 抽成用攻击=${fmtAtk(atk)}`, ); }); const extra = sum * wan / 10000; const before = session.attributes.getAttr(caster, AttrId.Attack); if (extra > 0) { session.attributes.addModifier(caster, { attr: AttrId.Attack, op: 'PostMulAdd', value: extra, source: FIELD_ATK_GROUP, group: FIELD_ATK_GROUP, }); } const after = session.attributes.getAttr(caster, AttrId.Attack); logWangMuFieldAtkSteal(caster, wan, allyLines, sum, extra, before, after); } /** 旧词条 Skill.skill_special type12 / group3 已删;无表字段可驱动 */ function collectType12(_caster: EntityId): Extra12[] { return []; } function casterHasSpecialHead(caster: EntityId, head: number): boolean { for (const raw of SkillPresentationEnv.getSkillSpecials(caster)) { if (Number(String(raw).split(',')[0]) === head) return true; } return false; } function readType14Wan(caster: EntityId): number { let wan = 0; for (const raw of SkillPresentationEnv.getSkillSpecials(caster)) { const p = String(raw).split(',').map(Number); if (p[0] === 14) wan += p[1] ?? 0; } return wan; } function forEachAlly(session: BattleSession, caster: EntityId, fn: (id: EntityId) => void): void { const team = session.world.getComponent(caster, TeamComp); if (!team) return; forEachAllyByOwner(session, team.ownerId, fn); } function forEachAllyByOwner(session: BattleSession, ownerId: number, fn: (id: EntityId) => void): void { session.world.view(TeamComp).forEach((id, team) => { if (!session.world.isAlive(id)) return; if (BattleTeamId.isEnemyTeam(team.team)) return; if (team.ownerId !== ownerId) return; fn(id); }); } function allyWorldPos(entityId: EntityId): Vec3 | null { const origins = SkillPresentationEnv.getCompositionCastOrigins(entityId); if (!origins.length) return SkillPresentationEnv.getCasterGridCenter(entityId); if (origins.length === 1) return origins[0]; let x = 0; let y = 0; let z = 0; for (const p of origins) { x += p.x; y += p.y; z += p.z; } const n = origins.length; return new Vec3(x / n, y / n, z / n); } function attackWithoutPostMul(session: BattleSession, id: EntityId): number { const full = session.attributes.getAttr(id, AttrId.Attack); const attr = session.world.getComponent(id, AttributeComp); if (!attr) return full; let post = 0; for (const m of attr.modifiers) { if (m.attr === AttrId.Attack && m.op === 'PostMulAdd') post += m.value; } return full - post; } function weapon1Name(weapon1Id: number): string { if (weapon1Id <= 0) return `未知(w1=${weapon1Id})`; const row = gg.data.table.getTableData(TableNames.Hero, weapon1Id); return `${row?.name ?? '未知'}(w1=${weapon1Id})`; } function fmtAtk(n: number): string { return n.toFixed(2); } /** 策划验数:1656 天下归心(skill_special 14,万分比) */ function logWangMuFieldAtkSteal( caster: EntityId, wan: number, allyLines: readonly string[], sum: number, extra: number, before: number, after: number, ): void { if (!isDesignDebugEnabled()) return; const pct = (wan / 100).toFixed(1); console.log( '[王母][1656天下归心]', `entity=${caster}`, `\n 配表 skill_special=14,${wan} → 获得全场其他己方攻击之和的 ${wan}/10000=${pct}%`, '\n 各单位用最终攻击扣掉 PostMulAdd(抽成不吃攻击万分比,也不套抽成);王母自身不计入', allyLines.length ? `\n${allyLines.join('\n')}` : '\n (场上无其他己方)', `\n 合计=${fmtAtk(sum)}`, `\n 公式: ${fmtAtk(sum)} × ${wan}/10000 = ${fmtAtk(extra)}`, `\n 写入王母 Attack PostMulAdd=+${fmtAtk(extra)}`, `\n 王母最终攻击=(base+Add)×Mul+PostMulAdd 抽成前=${fmtAtk(before)} → 抽成后=${fmtAtk(after)}`, ); }