|
|
|
|
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<Vec3>,
|
|
|
|
|
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<ITableHero>(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)}`,
|
|
|
|
|
);
|
|
|
|
|
}
|