init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

915
src/data/buildDamage.ts Normal file
View File

@@ -0,0 +1,915 @@
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;
definition: 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;
definition: 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,
definition: slot.definition,
})) 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,
definition: target.definition,
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,
});
}