import { createTournamentRng } from '@sammo-ts/common'; import { TournamentType, type TournamentBattleInput, type TournamentBattleResult, type TournamentBattleLogEntry, } from './types.js'; const clampPositive = (value: number, fallback = 0): number => { if (!Number.isFinite(value)) { return fallback; } return value; }; const round = (value: number): number => Math.round(value); const getLogRatio = (lvl1: number, lvl2: number): number => { if (lvl1 >= lvl2) { return 1 + Math.log10(1 + lvl1 - lvl2) / 10; } return 1 - Math.log10(1 + lvl2 - lvl1) / 10; }; const resolveTournamentStat = (type: TournamentType, stats: { leadership: number; strength: number; intel: number }): number => { switch (type) { case TournamentType.LEADERSHIP: return stats.leadership; case TournamentType.STRENGTH: return stats.strength; case TournamentType.INTEL: return stats.intel; case TournamentType.TOTAL: default: return (stats.leadership + stats.strength + stats.intel) * (7 / 15); } }; export const resolveTournamentBattle = (input: TournamentBattleInput): TournamentBattleResult => { const { attacker, defender, battleType, context } = input; const attackerStat = resolveTournamentStat(input.type, attacker.stats); const defenderStat = resolveTournamentStat(input.type, defender.stats); const rng = createTournamentRng(input.baseSeed, { openYear: context.openYear, openMonth: context.openMonth, stage: context.stage, phase: context.phase, matchIndex: context.matchIndex, participantIndex: 0, extraSeed: `${attacker.id}:${defender.id}`, }); const energyBaseAttacker = round(attackerStat * getLogRatio(attacker.level, defender.level) * 10); const energyBaseDefender = round(defenderStat * getLogRatio(attacker.level, defender.level) * 10); let energyAttacker = energyBaseAttacker; let energyDefender = energyBaseDefender; const maxTurns = battleType === 0 ? 10 : 100; const log: string[] = []; const logEntries: TournamentBattleLogEntry[] = []; log.push(`${attacker.name} (${energyBaseAttacker}) vs (${energyBaseDefender}) ${defender.name}`); let totalDamageAttacker = 0; let totalDamageDefender = 0; let selected = 2; for (let phase = 1; phase <= maxTurns; phase += 1) { const baseDamageAttacker = round(defenderStat * (rng.nextInt(21) + 90) / 130); const baseDamageDefender = round(attackerStat * (rng.nextInt(21) + 90) / 130); let damageAttacker = baseDamageAttacker; let damageDefender = baseDamageDefender; if (attackerStat >= rng.nextInt(100)) { damageDefender += round(attackerStat * (rng.nextInt(41) + 10) / 130); } if (defenderStat >= rng.nextInt(100)) { damageAttacker += round(defenderStat * (rng.nextInt(41) + 10) / 130); } let criticalAttacker = false; let criticalDefender = false; let factorAttacker = 1; let factorDefender = 1; if (energyBaseAttacker / 5 > energyAttacker && damageAttacker > damageDefender && attackerStat >= rng.nextInt(300)) { factorDefender = round((rng.nextInt(301) + 200) / 100); criticalAttacker = true; log.push(`${attacker.name}의 분노의 일격!`); } if (energyBaseDefender / 5 > energyDefender && damageDefender > damageAttacker && defenderStat >= rng.nextInt(300)) { factorAttacker = round((rng.nextInt(301) + 200) / 100); criticalDefender = true; log.push(`${defender.name}의 분노의 일격!`); } damageAttacker = round(damageAttacker * factorAttacker); damageDefender = round(damageDefender * factorDefender); if (phase === 1) { if (attackerStat * 0.9 > defenderStat && attackerStat >= rng.nextInt(400)) { damageDefender += round(attackerStat * (rng.nextInt(31) + 70) / 100); } if (defenderStat * 0.9 > attackerStat && defenderStat >= rng.nextInt(400)) { damageAttacker += round(defenderStat * (rng.nextInt(31) + 70) / 100); } } else { if (!criticalAttacker && attackerStat >= rng.nextInt(1000)) { damageDefender += round(attackerStat * (rng.nextInt(31) + 20) / 100); } if (!criticalDefender && defenderStat >= rng.nextInt(1000)) { damageAttacker += round(defenderStat * (rng.nextInt(31) + 20) / 100); } } damageAttacker = clampPositive(round(damageAttacker), 0); damageDefender = clampPositive(round(damageDefender), 0); energyAttacker -= damageAttacker; energyDefender -= damageDefender; totalDamageAttacker += damageAttacker; totalDamageDefender += damageDefender; const entryText = `● ${String(phase).padStart(2, '0')}合 : ` + `${String(round(energyAttacker)).padStart(3, '0')}` + `(-${String(round(damageAttacker)).padStart(3, '0')})` + ' vs ' + `(-${String(round(damageDefender)).padStart(3, '0')})` + `${String(round(energyDefender)).padStart(3, '0')}`; log.push(entryText); logEntries.push({ phase, attackerEnergy: round(energyAttacker), defenderEnergy: round(energyDefender), attackerDamage: damageAttacker, defenderDamage: damageDefender, text: entryText, }); if (energyAttacker <= 0 && energyDefender <= 0) { if (battleType === 0) { selected = 2; break; } selected = energyAttacker > energyDefender ? 0 : 1; break; } if (energyAttacker <= 0) { selected = 1; break; } if (energyDefender <= 0) { selected = 0; break; } } if (selected === 0) { log.push(`${attacker.name} 승리!`); } else if (selected === 1) { log.push(`${defender.name} 승리!`); } else { log.push(`${attacker.name} 무승부!`); } const winnerId = selected === 0 ? attacker.id : selected === 1 ? defender.id : null; const loserId = selected === 0 ? defender.id : selected === 1 ? attacker.id : null; return { winnerId, loserId, draw: selected === 2, rounds: logEntries.length, totalDamage: { attacker: totalDamageAttacker, defender: totalDamageDefender, }, log, logEntries, }; };