토너먼트 준비

This commit is contained in:
2026-01-23 16:45:36 +00:00
parent 835886c7a9
commit 2d87cf881e
19 changed files with 744 additions and 4 deletions
+1
View File
@@ -15,5 +15,6 @@ export * from './ports/worldSnapshot.js';
export * from './scenario/index.js';
export * from './triggers/index.js';
export * from './turn/index.js';
export * from './tournament/index.js';
export * from './world/index.js';
export * from './war/index.js';
+186
View File
@@ -0,0 +1,186 @@
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(`<S>●</> <Y>${attacker.name}</> <C>(${energyBaseAttacker})</> vs <C>(${energyBaseDefender})</> <Y>${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(`<S>●</> <Y>${attacker.name}</>의 분노의 일격!`);
}
if (energyBaseDefender / 5 > energyDefender && damageDefender > damageAttacker && defenderStat >= rng.nextInt(300)) {
factorAttacker = round((rng.nextInt(301) + 200) / 100);
criticalDefender = true;
log.push(`<S>●</> <Y>${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 =
`<S>●</> ${String(phase).padStart(2, '0')}合 : ` +
`<C>${String(round(energyAttacker)).padStart(3, '0')}</>` +
`<span class="ev_highlight">(-${String(round(damageAttacker)).padStart(3, '0')})</span>` +
' vs ' +
`<span class="ev_highlight">(-${String(round(damageDefender)).padStart(3, '0')})</span>` +
`<C>${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(`<S>●</> <Y>${attacker.name}</> <S>승리</>!`);
} else if (selected === 1) {
log.push(`<S>●</> <Y>${defender.name}</> <S>승리</>!`);
} else {
log.push(`<S>●</> <Y>${attacker.name}</> <S>무승부</>!`);
}
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,
};
};
+2
View File
@@ -0,0 +1,2 @@
export * from './types.js';
export * from './battle.js';
+60
View File
@@ -0,0 +1,60 @@
export const TournamentType = {
TOTAL: 0,
LEADERSHIP: 1,
STRENGTH: 2,
INTEL: 3,
} as const;
export type TournamentType = (typeof TournamentType)[keyof typeof TournamentType];
export interface TournamentStats {
leadership: number;
strength: number;
intel: number;
}
export interface TournamentParticipant {
id: number;
name: string;
stats: TournamentStats;
level: number;
}
export interface TournamentBattleLogEntry {
phase: number;
attackerEnergy: number;
defenderEnergy: number;
attackerDamage: number;
defenderDamage: number;
text: string;
}
export interface TournamentBattleResult {
winnerId: number | null;
loserId: number | null;
draw: boolean;
rounds: number;
totalDamage: {
attacker: number;
defender: number;
};
log: string[];
logEntries: TournamentBattleLogEntry[];
}
export interface TournamentBattleContext {
openYear: number;
openMonth: number;
stage: number;
phase: number;
matchIndex: number;
}
export interface TournamentBattleInput {
type: TournamentType;
battleType: 0 | 1; // 0: 승무패, 1: 승패
attacker: TournamentParticipant;
defender: TournamentParticipant;
context: TournamentBattleContext;
baseSeed: string;
}