You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/script/battle/skill/WangMuConfig.ts

307 lines
10 KiB

3 weeks ago
import { Vec3 } from 'cc';
import { createMembershipSet, membershipAdd, membershipFromArray, membershipDedupeArray, type MembershipSet, type ReadonlyMembershipSet } from '../../../mx/tools/MembershipSet';
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;
}
interface Extra12 {
attrCode: number;
wan: number;
group3: ReadonlyMembershipSet<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) {
const wan = e.group3.has(targetWeapon1Id) ? e.wan * 2 : e.wan;
if (e.attrCode === 1) atkWan += wan;
else if (e.attrCode === 3) critWan += wan;
else if (e.attrCode === 4) critDamWan += 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);
}
function collectType12(caster: EntityId): Extra12[] {
const out: Extra12[] = [];
for (const player of SkillPresentationEnv.getPlayersForCaster(caster)) {
for (const entry of player.selectedSkills) {
if (entry.weapon !== WANG_MU_WEAPON1_ID) continue;
const raw = String(entry.skill_special ?? '').trim();
if (!raw) continue;
const p = raw.split(',').map(Number);
if (p[0] !== 12 || p.length < 3) continue;
out.push({
attrCode: p[1] ?? 0,
wan: p[2] ?? 0,
group3: parseIdSet(entry.group3),
});
}
}
return out;
}
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 parseIdSet(raw: string): MembershipSet<number> {
const out = createMembershipSet<number>();
for (const part of String(raw ?? '').split(',')) {
const n = Number(part);
if (Number.isFinite(n) && n > 0) membershipAdd(out, n);
}
return out;
}
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)}`,
);
}