1
This commit is contained in:
211
server-node/src/modules/runtime/runtimeBuildModule.ts
Normal file
211
server-node/src/modules/runtime/runtimeBuildModule.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
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<TItem extends RuntimeInventoryItemLike = RuntimeInventoryItemLike> = {
|
||||
playerEquipment: RuntimeEquipmentLoadout<TItem>;
|
||||
activeBuildBuffs?: RuntimeBuildBuff[];
|
||||
playerCharacter?: RuntimeCharacterLike | null;
|
||||
};
|
||||
|
||||
export type BuildContributionRow = {
|
||||
label: string;
|
||||
source: 'buff' | 'weapon' | 'armor' | 'relic' | 'character';
|
||||
fitScore: number;
|
||||
sourceCoefficient: number;
|
||||
bonusDelta: number;
|
||||
attributeSimilarities: Record<string, number>;
|
||||
attributeWeights: Record<string, number>;
|
||||
attributeContributions: Record<string, number>;
|
||||
attributeModifierDeltas: Record<string, number>;
|
||||
};
|
||||
|
||||
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<TBuff extends RuntimeBuildBuff>(
|
||||
baseBuffs: TBuff[] | null | undefined,
|
||||
additions: TBuff[] | null | undefined,
|
||||
) {
|
||||
const merged = new Map<string, TBuff>();
|
||||
|
||||
[...(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<TItem extends RuntimeInventoryItemLike>(
|
||||
state: RuntimeGameStateLike<TItem>,
|
||||
character: RuntimeCharacterLike,
|
||||
) {
|
||||
const tags = new Set<string>();
|
||||
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>,
|
||||
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>,
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user