Kord/src/services/RefinementService.ts

310 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<RefineResult> {
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<BattleResult> {
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;
}
}