diff --git a/Docs/Plans/MiniGame_Refinement_Plan.md b/Docs/Plans/MiniGame_Refinement_Plan.md index 5a0ae28..f65c962 100644 --- a/Docs/Plans/MiniGame_Refinement_Plan.md +++ b/Docs/Plans/MiniGame_Refinement_Plan.md @@ -9,20 +9,24 @@ The Refinement mini-game allows users to strengthen their virtual weapons, parti ### 1. Weapon Refinement - **Progression**: Level- **재련 비용**: `floor(10 × 1.6^level)G` (레벨별 기하급수적 증가) -- **판매 가격**: `floor(현재_단계_비용 × 2)G` +- **판매 가격**: `floor(현재_단계_비용 × 보정계수)G` + - 0~4강: 비용의 **2배** + - 5~25강: 비용의 **(2 + (L-4) × 10)배** (기대값 보정으로 고강화 가치 극대화) - **재련 성공 확률**: - 0→4강: 100% ~ 90% - 4→5강: **5%** (급락) - - 5→10강: 4% ~ 1% - 11→20강: **0.1%** (신화) - 21→25강: **0.01%** (기적) - **전투 보상**: `(패배자_단계_비용 × (패배자_레벨/승리자_레벨) × 2.0) × (0.8~1.2 랜덤)` - **최대 단계**: **25강** (Lvl 25) +- **최대 내구도**: **10 + Level** (강화 단계가 높을수록 내구도가 확장됨) - **전투 규칙**: - 0강 무기 소유자는 전투를 걸 수 없으며, 공격 대상으로 지정될 수도 없습니다. + - **일일 공격 제한**: 각 유저는 **하루 최대 10회**만 전투를 신청할 수 있습니다. (00시 초기화) + - **승리 확률 페널티**: 상대보다 5강 이상 낮을 경우 승률이 급락하며, 10강 차이부터는 **승률 0%**가 적용됩니다. - 전투 참여 시(공격/방어 모두) 내구도 -1. -- **파괴 조건**: - - 재련 실패 시 낮은 확률로 파괴 (0단계 회귀). + - 내구도 0인 상태에서 전투를 시도하면 전투 후 무기가 즉시 **파괴**됩니다. + - 재련 성공 시 내구도가 새로운 **최대치(10+Level)**로 완전 회복(수리)됩니다. - 내구도가 0인 상태에서 전투 시도(공격) 시 전투 후 무기 파괴. as the level increases. - **Risk**: Failure at higher levels can lead to weapon destruction (reset to level 0). diff --git a/prisma/migrations/20260330085217_add_battle_limits/migration.sql b/prisma/migrations/20260330085217_add_battle_limits/migration.sql new file mode 100644 index 0000000..1e028e7 --- /dev/null +++ b/prisma/migrations/20260330085217_add_battle_limits/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "RefinementProfile" ADD COLUMN "dailyBattleCount" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "lastBattleReset" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6941784..5bf37f6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -127,22 +127,24 @@ model MiniGameConfig { // 재련 - 유저 상태 model RefinementProfile { - userId String - guildId String - gold Int @default(1000) - weaponLevel Int @default(0) - maxWeaponLevel Int @default(0) - durability Int @default(10) - tryCount Int @default(0) - successCount Int @default(0) - failCount Int @default(0) - destroyCount Int @default(0) - battleWin Int @default(0) - battleLoss Int @default(0) - isDisabled Boolean @default(false) - lastCheckIn DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + userId String + guildId String + gold Int @default(1000) + weaponLevel Int @default(0) + maxWeaponLevel Int @default(0) + durability Int @default(10) + tryCount Int @default(0) + successCount Int @default(0) + failCount Int @default(0) + destroyCount Int @default(0) + battleWin Int @default(0) + battleLoss Int @default(0) + dailyBattleCount Int @default(0) + lastBattleReset DateTime @default(now()) + isDisabled Boolean @default(false) + lastCheckIn DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@id([userId, guildId]) @@index([guildId, weaponLevel(sort: Desc)]) diff --git a/src/commands/refine.ts b/src/commands/refine.ts index 64676ff..b576f9e 100644 --- a/src/commands/refine.ts +++ b/src/commands/refine.ts @@ -191,6 +191,7 @@ export default { const targetUser = interaction.options.getUser('user') || interaction.user; const profile = await RefinementService.getProfile(targetUser.id, interaction.guildId); + const maxDurability = RefinementService.getMaxDurability(profile.weaponLevel); const embed = new EmbedBuilder() .setTitle(`👤 ${targetUser.username} 님의 재련 프로필`) .setColor(Colors.Blue) @@ -199,8 +200,9 @@ export default { { name: '현재 무기', value: `${profile.weaponLevel} 단계`, inline: true }, { name: '최대 무기', value: `${profile.maxWeaponLevel} 단계`, inline: true }, { name: '소지 골드', value: `${profile.gold} G`, inline: true }, - { name: '내구도', value: `${profile.durability}`, inline: true }, + { name: '내구도', value: `${profile.durability} / ${maxDurability}`, inline: true }, { name: '전투 상태', value: profile.isDisabled ? '❌ 전투 불능' : '✅ 양호', inline: true }, + { name: '일일 공격', value: `${profile.dailyBattleCount} / 10`, inline: true }, { name: '출석', value: profile.lastCheckIn ? profile.lastCheckIn.toLocaleDateString() : '미기록', inline: true }, { name: '통계', value: `시도: ${profile.tryCount} | 성공: ${profile.successCount} | 실패: ${profile.failCount} | 파괴: ${profile.destroyCount}`, inline: false }, { name: '전투', value: `승리: ${profile.battleWin} | 패배: ${profile.battleLoss}`, inline: false } @@ -261,11 +263,11 @@ export default { .setColor(Colors.White) .setDescription('무기를 강화하고 다른 유저와 전투하며 강력한 전사가 되어보세요!') .addFields( - { name: '🛠️ 재련 (/refine try)', value: '골드를 소모하여 무기 수치를 올립니다. 5강부터 확률이 급락하며 **최대 25강**까지 가능합니다.' }, - { name: '⚔️ 전투 (/refine battle)', value: '다른 유저를 공격합니다. 승리자의 레벨이 패배자보다 높을수록 보상이 줄어들며, 약자가 강자를 잡으면 **잭팟**이 터집니다! (보상 80%~120% 랜덤)' }, - { name: '🛡️ 내구도 및 파괴', value: '내구도가 0이 되면 전투 불능이 되며, 이 상태에서 또 공격하면 무기가 즉시 파기됩니다! 재련 시도 로 무기를 수리하세요.' }, + { name: '🛠️ 재련 (/refine try)', value: '골드를 소모하여 강화합니다. 5강부터 확률이 극악이며, 성공 시 **내구도가 최대치(10+레벨)**로 수리됩니다.' }, + { name: '⚔️ 전투 (/refine battle)', value: '다른 유저를 공격합니다. **일일 10회 제한**이 있으며, 5강 이상 차이나는 강자 공격 시 승률이 급락(최대 0%)합니다.' }, + { name: '🛡️ 내구도 및 수명', value: '단계가 높을수록 최대 내구도가 증가하여 더 오래 사용할 수 있습니다. 내구도 0에서 공격 시 무기가 파괴됩니다.' }, { name: '📅 출석 (/refine checkin)', value: '매일 한 번 골드를 수령할 수 있습니다.' }, - { name: '💰 판매 (/refine sell)', value: '무기를 팔고 0단계로 돌아갑니다. 25강 무기는 수백만 골드의 가치를 가집니다.' }, + { name: '💰 판매 (/refine sell)', value: '무기를 팔고 0단계로 돌아갑니다. 고강화 무기는 난이도(기대값)에 비례해 **수억 골드**의 가치를 가집니다.' }, { name: '🔥 피버 타임', value: '서버가 가장 활발한 시간대에 재련 성공 확률이 10% 증가합니다!' } ); diff --git a/src/services/RefinementService.ts b/src/services/RefinementService.ts index a60d4af..2bbd81a 100644 --- a/src/services/RefinementService.ts +++ b/src/services/RefinementService.ts @@ -29,6 +29,13 @@ export class RefinementService { private static START_GOLD = 1000; private static CHECKIN_GOLD = 500; + /** + * 레벨에 따른 최대 내구도 계산 (10 + level) + */ + public static getMaxDurability(level: number): number { + return 10 + level; + } + /** * 레벨에 따른 강화 비용 계산 */ @@ -81,8 +88,8 @@ export class RefinementService { if (success) { newLevel = Math.min(this.MAX_LEVEL, profile.weaponLevel + 1); - // 내구도 회복 (단계 상승 시) - newDurability = 10; + // 내구도 회복 (단계 상승 시 새로운 최대치로) + newDurability = this.getMaxDurability(newLevel); } else { // 실패 시 파괴 확률: level * 1.5% const destroyRate = profile.weaponLevel * 0.015; @@ -125,21 +132,43 @@ export class RefinementService { 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('현재 전투 불능 상태입니다. 무기 수리(재련 시도 등)가 필요합니다.'); - let attackerDestroyed = false; - if (attacker.durability <= 0) { - attackerDestroyed = true; + // 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강 차이 이상은 절대 불가 } - // 전투 로직: 강화도 기반 확률 (간단히 강화도 차이로 계산) - // 우세 확률 = 0.5 + (내강화도 - 상대강화도) * 0.05 (최소 10%, 최대 90%) - const winRate = Math.max(0.1, Math.min(0.9, 0.5 + (attacker.weaponLevel - target.weaponLevel) * 0.05)); - const isAttackerWin = Math.random() <= winRate; + const isAttackerWin = Math.random() < winRate; const winnerId = isAttackerWin ? attackerId : targetId; const loserId = isAttackerWin ? targetId : attackerId; @@ -163,30 +192,29 @@ export class RefinementService { const newTargetDurability = Math.max(0, target.durability - 1); // 무기 파괴/불능 처리 - // 공격자가 내구도 0인 상태에서 공격했으면 전투 후 파괴 - const attackerWeaponLevel = attackerDestroyed ? 0 : attacker.weaponLevel; + const attackerDestroyed = false; await prisma.$transaction([ prisma.refinementProfile.update({ where: { userId_guildId: { userId: attackerId, guildId } }, data: { - gold: isAttackerWin ? { increment: reward } : undefined, + gold: attacker.gold + (isAttackerWin ? reward : 0), durability: newAttackerDurability, - weaponLevel: attackerDestroyed ? 0 : undefined, isDisabled: newAttackerDurability <= 0, - battleWin: isAttackerWin ? { increment: 1 } : undefined, - battleLoss: !isAttackerWin ? { increment: 1 } : undefined, - destroyCount: attackerDestroyed ? { increment: 1 } : undefined, + 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: !isAttackerWin ? { increment: reward } : undefined, + gold: target.gold + (!isAttackerWin ? reward : 0), durability: newTargetDurability, isDisabled: newTargetDurability <= 0, - battleWin: !isAttackerWin ? { increment: 1 } : undefined, - battleLoss: isAttackerWin ? { increment: 1 } : undefined, + battleWin: !isAttackerWin ? target.battleWin + 1 : target.battleWin, + battleLoss: !isAttackerWin ? target.battleLoss : target.battleLoss + 1, } }) ]); @@ -237,10 +265,16 @@ export class RefinementService { */ public static async sellWeapon(userId: string, guildId: string): Promise<{ level: number; price: number; gold: number }> { const profile = await this.getOrCreateProfile(userId, guildId); - if (profile.weaponLevel === 0) throw new Error('0단계 무기는 판매할 수 없습니다.'); - - const currentCost = this.calculateCost(profile.weaponLevel); - const price = Math.floor(currentCost * 2); + 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 } },