Files
Genarrative/src/data/buildDamage.ts
2026-04-28 20:25:37 +08:00

912 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
AttributeVector,
Character,
CustomWorldProfile,
EquipmentLoadout,
GameState,
InventoryItem,
SceneHostileNpc,
TimedBuildBuff,
WorldAttributeSchema,
} from '../types';
import { WorldType } from '../types';
import {
resolveCriticalStrike,
resolveRoleCombatStats,
} from './attributeCombat';
import {
resolveAttributeSchema,
resolveCharacterAttributeProfile,
} from './attributeResolver';
import { normalizeAttributeVector } from './attributeValidation';
import { getBuildTagAttributeSimilarityProfile } from './buildTagAttributeAffinity';
import {
buildSetBuildTagLabel,
getBuildTagDefinition,
getCharacterCombatTags,
getSceneMonsterCombatTags,
getTimedBuildBuffTags,
normalizeBuildRole,
normalizeBuildTags,
} from './buildTags';
import { getRuntimeCustomWorldProfile } from './customWorldRuntime';
import { getEquipmentBonuses } from './equipmentEffects';
const MAX_ACTIVE_BUILD_TAGS = 8;
export const BASE_TAG_BONUS = 0.12;
export const MAX_BUILD_BONUS = 0.6;
export type BuildContributionQuality =
| 'common'
| 'fine'
| 'rare'
| 'epic'
| 'legendary';
export type BuildContributionResourceLabels = {
maxHp?: string | null;
maxMp?: string | null;
};
export type BuildTagSource =
| 'buff'
| 'character'
| 'weapon'
| 'armor'
| 'relic'
| 'set'
| 'monster';
type ResolvedBuildTag = {
label: string;
source: BuildTagSource;
priority: number;
relatedTags?: string[];
};
export type BuildContributionRow = {
label: string;
source: BuildTagSource;
fitScore: number;
sourceCoefficient: number;
bonusDelta: number;
attributeSimilarities: AttributeVector;
attributeWeights: AttributeVector;
attributeContributions: AttributeVector;
attributeModifierDeltas: AttributeVector;
};
export type BuildDamageBreakdown = {
tags: string[];
baseTagCount: number;
buildDamageBonus: number;
buildDamageMultiplier: number;
rows: BuildContributionRow[];
};
export type BuildContributionAttributeRow = {
slotId: string;
label: string;
similarity: number;
weight: number;
value: number;
modifierDelta: number;
percent: number;
};
export type OutgoingDamageResult = {
damage: number;
isCritical: boolean;
critChance: number;
critDamageMultiplier: number;
attackPowerMultiplier: number;
};
type BuildContributionTarget = {
slotId: string;
label: string;
};
type ResolvedTagAffinity = {
rawSimilarity: AttributeVector;
};
const BUILD_CONTRIBUTION_QUALITY_LEVELS: Array<{
tier: BuildContributionQuality;
label: string;
minimumBonus: number;
colorRatio: number;
}> = [
{ tier: 'legendary', label: '传说', minimumBonus: 0.06, colorRatio: 1 },
{ tier: 'epic', label: '史诗', minimumBonus: 0.045, colorRatio: 0.78 },
{ tier: 'rare', label: '稀有', minimumBonus: 0.03, colorRatio: 0.56 },
{ tier: 'fine', label: '优秀', minimumBonus: 0.018, colorRatio: 0.32 },
{ tier: 'common', label: '普通', minimumBonus: 0, colorRatio: 0.08 },
];
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
function roundNumber(value: number, digits = 4) {
const factor = 10 ** digits;
return Math.round(value * factor) / factor;
}
function getSourceCoefficient(source: BuildTagSource) {
switch (source) {
case 'buff':
return 1;
case 'character':
return 0.9;
case 'weapon':
return 0.85;
case 'armor':
return 0.75;
case 'relic':
return 0.8;
case 'set':
return 0.9;
case 'monster':
return 0.88;
default:
return 0.8;
}
}
function pushTag(
target: ResolvedBuildTag[],
label: string | null | undefined,
source: BuildTagSource,
priority: number,
relatedTags?: string[],
) {
const normalizedLabel = label?.trim();
if (!normalizedLabel) return;
target.push({
label: normalizedLabel,
source,
priority,
relatedTags:
relatedTags && relatedTags.length > 0 ? [...relatedTags] : undefined,
});
}
function getItemBuildTags(item: InventoryItem | null) {
if (!item?.buildProfile) return [];
return normalizeBuildTags([
normalizeBuildRole(item.buildProfile.role),
...(item.buildProfile.tags ?? []),
]);
}
function getLoadoutBuildTags(loadout: EquipmentLoadout | null | undefined) {
if (!loadout) return [];
const tags: ResolvedBuildTag[] = [];
const setPieces = new Map<
string,
{ count: number; tags: string[]; setName: string }
>();
(
[
['weapon', loadout.weapon],
['armor', loadout.armor],
['relic', loadout.relic],
] as const
).forEach(([slotId, item]) => {
if (!item) return;
const itemTags = getItemBuildTags(item);
itemTags.forEach((tag) => pushTag(tags, tag, slotId, 60));
const setId = item.buildProfile?.setId?.trim();
const setName = item.buildProfile?.setName?.trim();
if (!setId || !setName) return;
const entry = setPieces.get(setId) ?? {
count: 0,
tags: [],
setName,
};
entry.count += 1;
entry.tags = [...new Set([...entry.tags, ...itemTags])];
setPieces.set(setId, entry);
});
setPieces.forEach((entry) => {
if (entry.count < 2) return;
pushTag(
tags,
buildSetBuildTagLabel(entry.setName, entry.count),
'set',
entry.count >= 3 ? 70 : 65,
entry.tags,
);
});
return tags;
}
function dedupeAndLimitTags(tags: ResolvedBuildTag[]) {
const bestByLabel = new Map<string, ResolvedBuildTag>();
tags.forEach((tag) => {
const existing = bestByLabel.get(tag.label);
if (!existing || tag.priority > existing.priority) {
bestByLabel.set(tag.label, tag);
}
});
return [...bestByLabel.values()]
.sort(
(left, right) =>
right.priority - left.priority ||
left.label.localeCompare(right.label, 'zh-CN'),
)
.slice(0, MAX_ACTIVE_BUILD_TAGS);
}
function averageAttributeVectors(
vectors: AttributeVector[],
slotIds: readonly string[],
) {
if (vectors.length === 0) {
const evenShare = 1 / Math.max(slotIds.length, 1);
return Object.fromEntries(slotIds.map((slotId) => [slotId, evenShare]));
}
return Object.fromEntries(
slotIds.map((slotId) => [
slotId,
roundNumber(
vectors.reduce((sum, vector) => sum + (vector[slotId] ?? 0), 0) /
vectors.length,
4,
),
]),
);
}
function resolveTagAffinity(
tag: ResolvedBuildTag,
schema: WorldAttributeSchema,
) {
const definition = getBuildTagDefinition(tag.label);
if (definition) {
return {
rawSimilarity: getBuildTagAttributeSimilarityProfile(
definition.id,
schema,
).rawSimilarity,
} satisfies ResolvedTagAffinity;
}
const relatedSchemaAffinities = (tag.relatedTags ?? []).flatMap(
(relatedTag) => {
const relatedDefinition = getBuildTagDefinition(relatedTag);
if (!relatedDefinition) {
return [];
}
return [
getBuildTagAttributeSimilarityProfile(relatedDefinition.id, schema)
.rawSimilarity,
];
},
);
const rawSimilarity = averageAttributeVectors(
relatedSchemaAffinities,
schema.slots.map((slot) => slot.slotId),
);
return {
rawSimilarity,
} satisfies ResolvedTagAffinity;
}
function resolveContributionTargets(
schema: WorldAttributeSchema,
_resourceLabels?: BuildContributionResourceLabels | null,
) {
return schema.slots.map((slot) => ({
slotId: slot.slotId,
label: slot.name,
})) satisfies BuildContributionTarget[];
}
function buildAttributeContributions(
tagAffinity: ResolvedTagAffinity,
schema: WorldAttributeSchema,
sourceCoefficient: number,
resourceLabels?: BuildContributionResourceLabels | null,
) {
const targets = resolveContributionTargets(schema, resourceLabels);
const slotIds = targets.map((target) => target.slotId);
const rawSimilarity = Object.fromEntries(
targets.map((target) => {
return [
target.slotId,
roundNumber(tagAffinity.rawSimilarity[target.slotId] ?? 0, 4),
];
}),
);
const normalizedAffinity = normalizeAttributeVector(rawSimilarity, slotIds);
const effectiveSlotIds = new Set(
[...slotIds]
.sort((left, right) => {
const difference =
(normalizedAffinity[right] ?? 0) - (normalizedAffinity[left] ?? 0);
if (Math.abs(difference) > 0.0001) {
return difference;
}
return left.localeCompare(right, 'zh-CN');
})
.slice(0, 2),
);
const attributeContributions = Object.fromEntries(
slotIds.map((slotId) => [
slotId,
roundNumber(
effectiveSlotIds.has(slotId) ? (normalizedAffinity[slotId] ?? 0) : 0,
4,
),
]),
);
const attributeWeights = Object.fromEntries(
slotIds.map((slotId) => [
slotId,
roundNumber(
effectiveSlotIds.has(slotId) ? (normalizedAffinity[slotId] ?? 0) : 0,
4,
),
]),
);
const attributeModifierDeltas = Object.fromEntries(
slotIds.map((slotId) => [
slotId,
roundNumber(
BASE_TAG_BONUS *
sourceCoefficient *
(attributeContributions[slotId] ?? 0),
4,
),
]),
);
const fitScore = roundNumber(
slotIds.reduce(
(sum, slotId) => sum + (attributeContributions[slotId] ?? 0),
0,
),
4,
);
return {
fitScore,
attributeWeights,
normalizedAffinity,
attributeContributions,
attributeModifierDeltas,
};
}
function buildBreakdownFromTags(
tags: ResolvedBuildTag[],
schema: WorldAttributeSchema,
resourceLabels?: BuildContributionResourceLabels | null,
): BuildDamageBreakdown {
if (tags.length === 0) {
return {
tags: [],
baseTagCount: 0,
buildDamageBonus: 0,
buildDamageMultiplier: 1,
rows: [],
};
}
const rows = tags.map((currentTag) => {
const tagAffinity = resolveTagAffinity(currentTag, schema);
const sourceCoefficient = getSourceCoefficient(currentTag.source);
const {
fitScore,
attributeWeights,
normalizedAffinity,
attributeContributions,
attributeModifierDeltas,
} = buildAttributeContributions(
tagAffinity,
schema,
sourceCoefficient,
resourceLabels,
);
const bonusDelta = roundNumber(
Object.values(attributeModifierDeltas).reduce(
(sum, value) => sum + value,
0,
),
4,
);
return {
label: currentTag.label,
source: currentTag.source,
fitScore,
sourceCoefficient,
bonusDelta,
attributeSimilarities: normalizedAffinity,
attributeWeights,
attributeContributions,
attributeModifierDeltas,
} satisfies BuildContributionRow;
});
const buildDamageBonus = roundNumber(
clamp(
rows.reduce((sum, row) => sum + row.bonusDelta, 0),
0,
MAX_BUILD_BONUS,
),
4,
);
const buildDamageMultiplier = roundNumber(1 + buildDamageBonus, 4);
return {
tags: tags.map((tag) => tag.label),
baseTagCount: tags.length,
buildDamageBonus,
buildDamageMultiplier,
rows,
};
}
export function getBuildSourceLabel(source: BuildTagSource) {
switch (source) {
case 'buff':
return '\u589e\u76ca';
case 'character':
return '\u89d2\u8272\u56fa\u6709';
case 'weapon':
return '\u6b66\u5668';
case 'armor':
return '\u62a4\u7532';
case 'relic':
return '\u9970\u54c1';
case 'set':
return '\u5957\u88c5';
case 'monster':
return '\u602a\u7269';
default:
return '\u6807\u7b7e';
}
}
export function getBuildContributionQuality(
bonusDelta: number,
): (typeof BUILD_CONTRIBUTION_QUALITY_LEVELS)[number] {
const fallbackLevel =
BUILD_CONTRIBUTION_QUALITY_LEVELS[
BUILD_CONTRIBUTION_QUALITY_LEVELS.length - 1
] ?? BUILD_CONTRIBUTION_QUALITY_LEVELS[0]!;
return (
BUILD_CONTRIBUTION_QUALITY_LEVELS.find(
(level) => bonusDelta >= level.minimumBonus,
) ?? fallbackLevel
);
}
export function getBuildContributionQualityLabel(bonusDelta: number) {
return getBuildContributionQuality(bonusDelta).label;
}
export function getBuildContributionQualityRatio(bonusDelta: number) {
return getBuildContributionQuality(bonusDelta).colorRatio;
}
export function formatBuildContributionPercent(value: number, digits = 1) {
const percentValue = roundNumber(value * 100, digits);
const normalizedDigits = Math.max(0, digits);
return `${percentValue >= 0 ? '+' : ''}${percentValue.toFixed(normalizedDigits)}%`;
}
export function getBuildContributionAttributeRows(
row: Pick<
BuildContributionRow,
| 'attributeContributions'
| 'attributeModifierDeltas'
| 'attributeSimilarities'
| 'attributeWeights'
>,
schema: WorldAttributeSchema,
options: {
minimumValue?: number;
resourceLabels?: BuildContributionResourceLabels | null;
} = {},
) {
const minimumValue = options.minimumValue ?? 0.0001;
const totalModifierDelta = Object.values(
row.attributeModifierDeltas ?? {},
).reduce((sum, value) => sum + value, 0);
const targets = resolveContributionTargets(schema, options.resourceLabels);
return targets
.map((target) => {
const value = roundNumber(
row.attributeContributions[target.slotId] ?? 0,
4,
);
const modifierDelta = roundNumber(
row.attributeModifierDeltas?.[target.slotId] ?? 0,
4,
);
const percent =
totalModifierDelta > 0
? roundNumber(modifierDelta / totalModifierDelta, 4)
: 0;
return {
slotId: target.slotId,
label: target.label,
similarity: roundNumber(
row.attributeSimilarities?.[target.slotId] ?? 0,
4,
),
weight: roundNumber(row.attributeWeights?.[target.slotId] ?? 0, 4),
value,
modifierDelta,
percent,
} satisfies BuildContributionAttributeRow;
})
.filter(
(entry) =>
entry.value > minimumValue || entry.modifierDelta > minimumValue,
)
.sort(
(left, right) =>
right.modifierDelta - left.modifierDelta ||
left.label.localeCompare(right.label, 'zh-CN'),
);
}
export function describeBuildContribution(
row: Pick<
BuildContributionRow,
| 'attributeContributions'
| 'attributeModifierDeltas'
| 'attributeSimilarities'
| 'attributeWeights'
>,
schema: WorldAttributeSchema,
options: {
limit?: number;
resourceLabels?: BuildContributionResourceLabels | null;
} = {},
) {
const limit = options.limit ?? 2;
const topRows = getBuildContributionAttributeRows(row, schema, options).slice(
0,
limit,
);
if (topRows.length === 0) {
return '\u6682\u65e0\u53ef\u89c1\u5c5e\u6027\u52a0\u6210';
}
return topRows
.map(
(entry) =>
`${entry.label} ${formatBuildContributionPercent(entry.modifierDelta)}`,
)
.join('');
}
function getPlayerBuffs(gameState: GameState) {
return (gameState.activeBuildBuffs ?? []).filter(
(buff) => (buff.durationTurns ?? 0) > 0,
);
}
export function tickBuildBuffs(buffs: TimedBuildBuff[] | null | undefined) {
return (buffs ?? [])
.map((buff) => ({
...buff,
durationTurns: Math.max(0, (buff.durationTurns ?? 0) - 1),
}))
.filter((buff) => buff.durationTurns > 0);
}
export function appendBuildBuffs(
baseBuffs: TimedBuildBuff[] | null | undefined,
additions: TimedBuildBuff[] | null | undefined,
) {
const merged = new Map<string, TimedBuildBuff>();
[...(baseBuffs ?? []), ...(additions ?? [])].forEach((buff) => {
const existing = merged.get(buff.id);
if (
!existing ||
(buff.durationTurns ?? 0) >= (existing.durationTurns ?? 0)
) {
merged.set(buff.id, {
...buff,
tags: normalizeBuildTags(buff.tags),
});
}
});
return [...merged.values()].filter(
(buff) => buff.tags.length > 0 && buff.durationTurns > 0,
);
}
export function getPlayerBuildDamageBreakdown(
gameState: GameState,
character: Character,
) {
const tags: ResolvedBuildTag[] = [];
getTimedBuildBuffTags(getPlayerBuffs(gameState)).forEach((tag) =>
pushTag(tags, tag, 'buff', 100),
);
getCharacterCombatTags(character).forEach((tag) =>
pushTag(tags, tag, 'character', 90),
);
getLoadoutBuildTags(gameState.playerEquipment).forEach((tag) =>
tags.push(tag),
);
return buildBreakdownFromTags(
dedupeAndLimitTags(tags),
resolveAttributeSchema(gameState.worldType, gameState.customWorldProfile),
);
}
export function getCompanionBuildDamageBreakdown(
character: Character,
worldType: WorldType | null = null,
customWorldProfile: CustomWorldProfile | null = getRuntimeCustomWorldProfile(),
) {
const tags: ResolvedBuildTag[] = [];
getCharacterCombatTags(character).forEach((tag) =>
pushTag(tags, tag, 'character', 90),
);
const resolvedWorldType =
worldType ?? (customWorldProfile ? WorldType.CUSTOM : WorldType.WUXIA);
return buildBreakdownFromTags(
dedupeAndLimitTags(tags),
resolveAttributeSchema(resolvedWorldType, customWorldProfile),
);
}
export function getMonsterBuildDamageBreakdown(
monster: SceneHostileNpc,
worldType: WorldType | null = null,
customWorldProfile: CustomWorldProfile | null = getRuntimeCustomWorldProfile(),
) {
const tags: ResolvedBuildTag[] = [];
getSceneMonsterCombatTags(monster).forEach((tag) =>
pushTag(tags, tag, 'monster', 90),
);
const resolvedWorldType =
worldType ??
(monster.attributeProfile?.schemaId?.includes('xianxia')
? WorldType.XIANXIA
: customWorldProfile
? WorldType.CUSTOM
: WorldType.WUXIA);
return buildBreakdownFromTags(
dedupeAndLimitTags(tags),
resolveAttributeSchema(resolvedWorldType, customWorldProfile),
);
}
export function calculateOutgoingDamage(
baseDamage: number,
options: {
functionMultiplier?: number;
equipmentMultiplier?: number;
buildMultiplier?: number;
attackPowerMultiplier?: number;
} = {},
) {
return Math.max(
1,
Math.round(
baseDamage *
(options.functionMultiplier ?? 1) *
(options.equipmentMultiplier ?? 1) *
(options.buildMultiplier ?? 1) *
(options.attackPowerMultiplier ?? 1),
),
);
}
export function calculateOutgoingDamageResult(
baseDamage: number,
options: {
functionMultiplier?: number;
equipmentMultiplier?: number;
buildMultiplier?: number;
attackPowerMultiplier?: number;
criticalHit?: boolean;
critDamageMultiplier?: number;
critChance?: number;
} = {},
): OutgoingDamageResult {
const baseResolvedDamage = calculateOutgoingDamage(baseDamage, options);
const isCritical = options.criticalHit ?? false;
const critDamageMultiplier = options.critDamageMultiplier ?? 1;
return {
damage: Math.max(
1,
Math.round(baseResolvedDamage * (isCritical ? critDamageMultiplier : 1)),
),
isCritical,
critChance: options.critChance ?? 0,
critDamageMultiplier,
attackPowerMultiplier: options.attackPowerMultiplier ?? 1,
};
}
export function resolvePlayerOutgoingDamage(
gameState: GameState,
character: Character,
baseDamage: number,
functionMultiplier = 1,
) {
const attributeProfile = resolveCharacterAttributeProfile(
character,
gameState.worldType,
gameState.customWorldProfile,
);
const combatStats = resolveRoleCombatStats(attributeProfile);
const buildBreakdown = getPlayerBuildDamageBreakdown(gameState, character);
const equipmentBonuses = getEquipmentBonuses(gameState.playerEquipment);
return calculateOutgoingDamage(baseDamage, {
functionMultiplier,
equipmentMultiplier: equipmentBonuses.outgoingDamageMultiplier,
buildMultiplier: buildBreakdown.buildDamageMultiplier,
attackPowerMultiplier: combatStats.attackPowerMultiplier,
});
}
export function resolvePlayerOutgoingDamageResult(
gameState: GameState,
character: Character,
baseDamage: number,
functionMultiplier = 1,
critRollSeed?: string,
) {
const attributeProfile = resolveCharacterAttributeProfile(
character,
gameState.worldType,
gameState.customWorldProfile,
);
const combatStats = resolveRoleCombatStats(attributeProfile);
const criticalStrike = critRollSeed
? resolveCriticalStrike(attributeProfile, critRollSeed)
: null;
const buildBreakdown = getPlayerBuildDamageBreakdown(gameState, character);
const equipmentBonuses = getEquipmentBonuses(gameState.playerEquipment);
return calculateOutgoingDamageResult(baseDamage, {
functionMultiplier,
equipmentMultiplier: equipmentBonuses.outgoingDamageMultiplier,
buildMultiplier: buildBreakdown.buildDamageMultiplier,
attackPowerMultiplier: combatStats.attackPowerMultiplier,
criticalHit: criticalStrike?.isCritical ?? false,
critChance: combatStats.critChance,
critDamageMultiplier: combatStats.critDamageMultiplier,
});
}
export function resolveCompanionOutgoingDamage(
character: Character,
baseDamage: number,
functionMultiplier = 1,
worldType: WorldType | null = null,
customWorldProfile: CustomWorldProfile | null = getRuntimeCustomWorldProfile(),
) {
const attributeProfile = resolveCharacterAttributeProfile(
character,
worldType,
customWorldProfile,
);
const combatStats = resolveRoleCombatStats(attributeProfile);
const buildBreakdown = getCompanionBuildDamageBreakdown(
character,
worldType,
customWorldProfile,
);
return calculateOutgoingDamage(baseDamage, {
functionMultiplier,
buildMultiplier: buildBreakdown.buildDamageMultiplier,
attackPowerMultiplier: combatStats.attackPowerMultiplier,
});
}
export function resolveCompanionOutgoingDamageResult(
character: Character,
baseDamage: number,
functionMultiplier = 1,
worldType: WorldType | null = null,
customWorldProfile: CustomWorldProfile | null = getRuntimeCustomWorldProfile(),
critRollSeed?: string,
) {
const attributeProfile = resolveCharacterAttributeProfile(
character,
worldType,
customWorldProfile,
);
const combatStats = resolveRoleCombatStats(attributeProfile);
const criticalStrike = critRollSeed
? resolveCriticalStrike(attributeProfile, critRollSeed)
: null;
const buildBreakdown = getCompanionBuildDamageBreakdown(
character,
worldType,
customWorldProfile,
);
return calculateOutgoingDamageResult(baseDamage, {
functionMultiplier,
buildMultiplier: buildBreakdown.buildDamageMultiplier,
attackPowerMultiplier: combatStats.attackPowerMultiplier,
criticalHit: criticalStrike?.isCritical ?? false,
critChance: combatStats.critChance,
critDamageMultiplier: combatStats.critDamageMultiplier,
});
}
export function resolveMonsterOutgoingDamage(
monster: SceneHostileNpc,
baseDamage: number,
functionMultiplier = 1,
worldType: WorldType | null = null,
customWorldProfile: CustomWorldProfile | null = getRuntimeCustomWorldProfile(),
) {
const combatStats = resolveRoleCombatStats(monster.attributeProfile);
const buildBreakdown = getMonsterBuildDamageBreakdown(
monster,
worldType,
customWorldProfile,
);
return calculateOutgoingDamage(baseDamage, {
functionMultiplier,
buildMultiplier: buildBreakdown.buildDamageMultiplier,
attackPowerMultiplier: combatStats.attackPowerMultiplier,
});
}
export function resolveMonsterOutgoingDamageResult(
monster: SceneHostileNpc,
baseDamage: number,
functionMultiplier = 1,
worldType: WorldType | null = null,
customWorldProfile: CustomWorldProfile | null = getRuntimeCustomWorldProfile(),
critRollSeed?: string,
) {
const combatStats = resolveRoleCombatStats(monster.attributeProfile);
const criticalStrike = critRollSeed
? resolveCriticalStrike(monster.attributeProfile, critRollSeed)
: null;
const buildBreakdown = getMonsterBuildDamageBreakdown(
monster,
worldType,
customWorldProfile,
);
return calculateOutgoingDamageResult(baseDamage, {
functionMultiplier,
buildMultiplier: buildBreakdown.buildDamageMultiplier,
attackPowerMultiplier: combatStats.attackPowerMultiplier,
criticalHit: criticalStrike?.isCritical ?? false,
critChance: combatStats.critChance,
critDamageMultiplier: combatStats.critDamageMultiplier,
});
}