import { prisma } from '../database'; import { logger } from '../utils/logger'; import { FeverService } from './FeverService'; export interface RefineResult { success: boolean; destroyed: boolean; levelBefore: number; levelAfter: number; cost: number; remainingGold: number; } export interface BattleResult { winnerId: string; loserId: string; attackerId: string; targetId: string; reward: number; attackerLevel: number; targetLevel: number; attackerDurability: number; targetDurability: number; destroyed: boolean; // 공격자 무기 파괴 여부 (내구도 0에서 공격 시) } export class RefinementService { private static MAX_LEVEL = 25; private static START_GOLD = 1000; private static CHECKIN_GOLD = 500; /** * 레벨에 따른 최대 내구도 계산 (10 + level) */ public static getMaxDurability(level: number): number { return 10 + level; } /** * 레벨에 따른 강화 비용 계산 */ public static calculateCost(level: number): number { return Math.floor(10 * Math.pow(1.6, level)); } /** * 재련 시도 */ public static async tryRefine(userId: string, guildId: string): Promise { const profile = await this.getOrCreateProfile(userId, guildId); if (profile.weaponLevel >= this.MAX_LEVEL) { throw new Error(`이미 최대 단계(${this.MAX_LEVEL}강)에 도달했습니다. 무기를 판매하고 다시 시작하세요!`); } // 비용 계산: floor(10 * 1.6^level) const cost = this.calculateCost(profile.weaponLevel); if (profile.gold < cost) { throw new Error('골드가 부족합니다.'); } const fever = await FeverService.getFeverBonus(guildId); const level = profile.weaponLevel; let baseSuccessRate = 0; if (level === 0) baseSuccessRate = 1.0; else if (level === 1) baseSuccessRate = 0.98; else if (level === 2) baseSuccessRate = 0.95; else if (level === 3) baseSuccessRate = 0.92; else if (level === 4) baseSuccessRate = 0.90; else if (level === 5) baseSuccessRate = 0.05; // 급격한 하향 구간 시작 else if (level === 6) baseSuccessRate = 0.04; else if (level === 7) baseSuccessRate = 0.03; else if (level === 8) baseSuccessRate = 0.02; else if (level < 10) baseSuccessRate = 0.01; else if (level < 20) baseSuccessRate = 0.001; // 0.1% else baseSuccessRate = 0.0001; // 0.01% (신화적 구간: 21-25강) const successRate = baseSuccessRate + (fever.active ? fever.bonusRate : 0); const random = Math.random(); const success = random <= successRate; let destroyed = false; let newLevel = profile.weaponLevel; let newDurability = profile.durability; if (success) { newLevel = Math.min(this.MAX_LEVEL, profile.weaponLevel + 1); // 내구도 회복 (단계 상승 시 새로운 최대치로) newDurability = this.getMaxDurability(newLevel); } else { // 실패 시 파괴 확률: level * 1.5% const destroyRate = profile.weaponLevel * 0.015; if (Math.random() <= destroyRate) { destroyed = true; newLevel = 0; newDurability = 10; // 파괴 시 초기화 } } const updatedProfile = await prisma.refinementProfile.update({ where: { userId_guildId: { userId, guildId } }, data: { gold: { decrement: cost }, weaponLevel: newLevel, durability: newDurability, isDisabled: newDurability > 0 ? false : undefined, maxWeaponLevel: { set: Math.max(profile.maxWeaponLevel, newLevel) }, tryCount: { increment: 1 }, successCount: success ? { increment: 1 } : undefined, failCount: !success ? { increment: 1 } : undefined, destroyCount: destroyed ? { increment: 1 } : undefined, } }); return { success, destroyed, levelBefore: profile.weaponLevel, levelAfter: newLevel, cost, remainingGold: updatedProfile.gold, }; } /** * 전투 수행 (일방적) */ public static async startBattle(attackerId: string, targetId: string, guildId: string): Promise { const attacker = await this.getOrCreateProfile(attackerId, guildId); const target = await this.getOrCreateProfile(targetId, guildId); // 0. 일일 공격 횟수 체크 및 리셋 (KST 기준 자정 리셋) const now = new Date(); const lastReset = attacker.lastBattleReset || new Date(0); const isNewDay = now.toDateString() !== lastReset.toDateString(); let currentDailyCount = isNewDay ? 0 : attacker.dailyBattleCount; if (currentDailyCount >= 10) { throw new Error('일일 공격 제한(10회)에 도달했습니다. 내일 다시 도전하세요!'); } if (attacker.weaponLevel === 0 || target.weaponLevel === 0) { throw new Error('0강 무기 소유자는 전투에 참여할 수 없습니다. 최소 1강 이상 강화하세요.'); } if (attacker.isDisabled) throw new Error('현재 전투 불능 상태입니다. 무기 수리(재련 시도 등)가 필요합니다.'); // 1. 승패 확률 계산 (비선형 패널티 적용) const diff = attacker.weaponLevel - target.weaponLevel; let winRate = 0.5; if (diff >= 0) { winRate = Math.min(0.99, 0.5 + diff * 0.05); } else { // 약자 공격 시 페널티 (5강 차이부터 급락) if (diff === -1) winRate = 0.4; else if (diff === -2) winRate = 0.3; else if (diff === -3) winRate = 0.2; else if (diff === -4) winRate = 0.1; else if (diff === -5) winRate = 0.05; else if (diff === -6) winRate = 0.04; else if (diff === -7) winRate = 0.03; else if (diff === -8) winRate = 0.02; else if (diff === -9) winRate = 0.01; else winRate = 0; // 10강 차이 이상은 절대 불가 } const isAttackerWin = Math.random() < winRate; const winnerId = isAttackerWin ? attackerId : targetId; const loserId = isAttackerWin ? targetId : attackerId; // 보상 계산: 승리자 역배율 적용 (TargetCost × TargetLevel/WinnerLevel × 2.0) const levelDiff = Math.abs(attacker.weaponLevel - target.weaponLevel); const winnerLevel = isAttackerWin ? attacker.weaponLevel : target.weaponLevel; const loserLevel = isAttackerWin ? target.weaponLevel : attacker.weaponLevel; const loserCost = this.calculateCost(loserLevel); // 승자 레벨이 높을수록 보상 삭감 계수 (1.0 ~ 0.04 사이) const ratio = Math.min(1.0, loserLevel / winnerLevel); const baseReward = Math.floor(loserCost * ratio * 2.0); // 랜덤폭 (Min 80% ~ Max 120%) const randomFactor = 0.8 + Math.random() * 0.4; const reward = Math.floor(baseReward * randomFactor) + (levelDiff * 100); // 내구도 감소 (양쪽 모두 -1) const newAttackerDurability = Math.max(0, attacker.durability - 1); const newTargetDurability = Math.max(0, target.durability - 1); // 무기 파괴/불능 처리 const attackerDestroyed = false; await prisma.$transaction([ prisma.refinementProfile.update({ where: { userId_guildId: { userId: attackerId, guildId } }, data: { gold: attacker.gold + (isAttackerWin ? reward : 0), durability: newAttackerDurability, isDisabled: newAttackerDurability <= 0, battleWin: isAttackerWin ? attacker.battleWin + 1 : attacker.battleWin, battleLoss: isAttackerWin ? attacker.battleLoss : attacker.battleLoss + 1, dailyBattleCount: currentDailyCount + 1, lastBattleReset: now, } }), prisma.refinementProfile.update({ where: { userId_guildId: { userId: targetId, guildId } }, data: { gold: target.gold + (!isAttackerWin ? reward : 0), durability: newTargetDurability, isDisabled: newTargetDurability <= 0, battleWin: !isAttackerWin ? target.battleWin + 1 : target.battleWin, battleLoss: !isAttackerWin ? target.battleLoss : target.battleLoss + 1, } }) ]); return { winnerId, loserId, attackerId, targetId, reward, attackerLevel: attacker.weaponLevel, targetLevel: target.weaponLevel, attackerDurability: newAttackerDurability, targetDurability: newTargetDurability, destroyed: attackerDestroyed, }; } /** * 일일 출석 */ public static async checkIn(userId: string, guildId: string): Promise<{ goldAdded: number; totalGold: number }> { const profile = await this.getOrCreateProfile(userId, guildId); const now = new Date(); if (profile.lastCheckIn) { const last = new Date(profile.lastCheckIn); if (last.getUTCFullYear() === now.getUTCFullYear() && last.getUTCMonth() === now.getUTCMonth() && last.getUTCDate() === now.getUTCDate()) { throw new Error('오늘은 이미 출석했습니다.'); } } const updated = await prisma.refinementProfile.update({ where: { userId_guildId: { userId, guildId } }, data: { gold: { increment: this.CHECKIN_GOLD }, lastCheckIn: now, } }); return { goldAdded: this.CHECKIN_GOLD, totalGold: updated.gold }; } /** * 무기 판매 */ public static async sellWeapon(userId: string, guildId: string): Promise<{ level: number; price: number; gold: number }> { const profile = await this.getOrCreateProfile(userId, guildId); const level = profile.weaponLevel; const cost = this.calculateCost(level); // 기대값 기반 판매가 개편 // 0-4: 2배, 5-25: (2 + (L-4)*10)배 let multiplier = 2; if (level >= 5) { multiplier = 2 + (level - 4) * 10; } const price = Math.floor(cost * multiplier); const updated = await prisma.refinementProfile.update({ where: { userId_guildId: { userId, guildId } }, data: { gold: { increment: price }, weaponLevel: 0, durability: 10, // 판매 후 초기화 시 내구도 복구 isDisabled: false, } }); return { level: profile.weaponLevel, price, gold: updated.gold }; } public static async getProfile(userId: string, guildId: string) { return this.getOrCreateProfile(userId, guildId); } private static async getOrCreateProfile(userId: string, guildId: string) { let profile = await prisma.refinementProfile.findUnique({ where: { userId_guildId: { userId, guildId } } }); if (!profile) { profile = await prisma.refinementProfile.create({ data: { userId, guildId, gold: this.START_GOLD } }); } return profile; } }