import { getEquipmentBonuses, type RuntimeEquipmentLoadout, } from './runtimeEquipmentModule.js'; type RuntimeCharacterLike = { attributes: { strength: number; agility: number; intelligence: number; spirit: number; }; }; type RuntimeBuildBuff = { id: string; name: string; tags: string[]; durationTurns: number; }; type RuntimeInventoryItemLike = { buildProfile?: { role: string; tags: string[]; synergy: string[]; forgeRank: number; }; }; type RuntimeGameStateLike = { playerEquipment: RuntimeEquipmentLoadout; activeBuildBuffs?: RuntimeBuildBuff[]; playerCharacter?: RuntimeCharacterLike | null; }; export type BuildContributionRow = { label: string; source: 'buff' | 'weapon' | 'armor' | 'relic' | 'character'; fitScore: number; sourceCoefficient: number; bonusDelta: number; attributeSimilarities: Record; attributeWeights: Record; attributeContributions: Record; attributeModifierDeltas: Record; }; export type BuildDamageBreakdown = { tags: string[]; baseTagCount: number; buildDamageBonus: number; buildDamageMultiplier: number; rows: BuildContributionRow[]; }; export type OutgoingDamageResult = { damage: number; isCritical: boolean; critChance: number; critDamageMultiplier: number; attackPowerMultiplier: number; }; function roundNumber(value: number, digits = 4) { const factor = 10 ** digits; return Math.round(value * factor) / factor; } function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)); } function hashSeed(seed: string) { let hash = 0; for (let index = 0; index < seed.length; index += 1) { hash = (hash * 31 + seed.charCodeAt(index)) >>> 0; } return hash; } export function appendBuildBuffs( baseBuffs: TBuff[] | null | undefined, additions: TBuff[] | null | undefined, ) { const merged = new Map(); [...(baseBuffs ?? []), ...(additions ?? [])].forEach((buff) => { const existing = merged.get(buff.id); if (!existing || (buff.durationTurns ?? 0) >= (existing.durationTurns ?? 0)) { merged.set(buff.id, { ...buff, tags: [...new Set(buff.tags.map((tag) => tag.trim()).filter(Boolean))], }); } }); return [...merged.values()].filter( (buff) => buff.tags.length > 0 && buff.durationTurns > 0, ); } function collectBuildTags( state: RuntimeGameStateLike, character: RuntimeCharacterLike, ) { const tags = new Set(); state.activeBuildBuffs ?.filter((buff) => (buff.durationTurns ?? 0) > 0) .forEach((buff) => buff.tags.forEach((tag) => tags.add(tag))); (['weapon', 'armor', 'relic'] as const).forEach((slot) => { const item = state.playerEquipment[slot]; item?.buildProfile?.tags?.forEach((tag) => tags.add(tag)); if (item?.buildProfile?.role) { tags.add(item.buildProfile.role); } }); if (character.attributes.agility >= 10) tags.add('快剑'); if (character.attributes.strength >= 10) tags.add('重击'); if (character.attributes.spirit >= 10) tags.add('续战'); if (character.attributes.intelligence >= 8) tags.add('法力'); return [...tags].filter(Boolean).slice(0, 8); } export function getPlayerBuildDamageBreakdown< TState extends RuntimeGameStateLike, TItem extends RuntimeInventoryItemLike, >(state: TState, character: RuntimeCharacterLike) { const tags = collectBuildTags(state, character); const rows = tags.map((tag, index) => { const bonusDelta = roundNumber(0.03 + Math.min(index, 3) * 0.01, 4); return { label: tag, source: index === 0 ? 'buff' : 'weapon', fitScore: roundNumber(0.6 + Math.max(0, 3 - index) * 0.08, 4), sourceCoefficient: 1, bonusDelta, attributeSimilarities: {}, attributeWeights: {}, attributeContributions: {}, attributeModifierDeltas: {}, } satisfies BuildContributionRow; }); const buildDamageBonus = roundNumber( clamp(rows.reduce((sum, row) => sum + row.bonusDelta, 0), 0, 0.6), 4, ); return { tags, baseTagCount: tags.length, buildDamageBonus, buildDamageMultiplier: roundNumber(1 + buildDamageBonus, 4), rows, } satisfies BuildDamageBreakdown; } export function resolvePlayerOutgoingDamageResult< TState extends RuntimeGameStateLike, TItem extends RuntimeInventoryItemLike, >( state: TState, character: RuntimeCharacterLike, baseDamage: number, functionMultiplier = 1, critRollSeed?: string, ) { const buildBreakdown = getPlayerBuildDamageBreakdown(state, character); const equipmentBonuses = getEquipmentBonuses(state.playerEquipment); const attackPowerMultiplier = roundNumber( 1 + (character.attributes.strength * 0.01 + character.attributes.agility * 0.006 + character.attributes.spirit * 0.004), 4, ); const critChance = roundNumber( clamp(0.08 + character.attributes.agility * 0.01, 0.08, 0.45), 4, ); const critDamageMultiplier = roundNumber( 1.45 + character.attributes.strength * 0.01, 4, ); const roll = critRollSeed ? (hashSeed(critRollSeed) % 1000) / 1000 : 1; const isCritical = roll < critChance; const damage = Math.max( 1, Math.round( baseDamage * functionMultiplier * equipmentBonuses.outgoingDamageMultiplier * buildBreakdown.buildDamageMultiplier * attackPowerMultiplier * (isCritical ? critDamageMultiplier : 1), ), ); return { damage, isCritical, critChance, critDamageMultiplier, attackPowerMultiplier, } satisfies OutgoingDamageResult; }