feat(balance): CHA(매력) 스탯 활성화 — 상점 할인 + 드롭률 보정

- shop_service: CHA 기반 구매 할인 (CHA-10)×1%, 최대 15%
- item_service: CHA 기반 희귀 아이템 드롭률 보정 (CHA-10)×0.5%
- 호출부(game_mutations, market_service, resurrection_service)에 CHA 전달
- 기존 데드 스탯이었던 CHA가 게임 로직에 반영됨
This commit is contained in:
JiWoong Sul
2026-03-31 00:12:52 +09:00
parent ea54b4c501
commit 69e7695cb7
5 changed files with 43 additions and 16 deletions

View File

@@ -26,6 +26,7 @@ class GameMutations {
name: name, name: name,
slot: slot, slot: slot,
level: level, level: level,
cha: state.stats.cha,
); );
final updatedEquip = state.equipment final updatedEquip = state.equipment

View File

@@ -60,23 +60,34 @@ class ItemService {
// 희귀도 결정 // 희귀도 결정
// ============================================================================ // ============================================================================
/// 희귀도 결정 (고정 확률) /// 희귀도 결정 (고정 확률 + CHA 보정)
/// ///
/// 확률 분포: /// 기본 확률 분포:
/// - Common: 34% /// - Common: 34%
/// - Uncommon: 40% /// - Uncommon: 40%
/// - Rare: 20% /// - Rare: 20%
/// - Epic: 5% /// - Epic: 5%
/// - Legendary: 1% /// - Legendary: 1%
ItemRarity determineRarity(int level) { ///
/// CHA 보정(charisma bonus): (CHA - 10) * 0.5% 추가 희귀 확률.
/// 보정값만큼 Common 확률이 줄고, Rare 이상 확률이 증가.
ItemRarity determineRarity(int level, {int cha = 0}) {
final roll = rng.nextInt(100); final roll = rng.nextInt(100);
// Legendary: 0-0 (1%) // CHA 보정: (CHA - 10) * 0.5, 0~10 범위
if (roll < 1) return ItemRarity.legendary; final chaBonus = ((cha - 10) * 0.5).clamp(0.0, 10.0);
// Epic: 1-5 (5%)
if (roll < 6) return ItemRarity.epic; // 보정된 임계값 (chaBonus만큼 희귀 쪽으로 이동)
// Rare: 6-25 (20%) final legendaryThreshold = 1.0 + chaBonus * 0.1; // 최대 2%
if (roll < 26) return ItemRarity.rare; final epicThreshold = 6.0 + chaBonus * 0.3; // 최대 9%
final rareThreshold = 26.0 + chaBonus * 0.6; // 최대 32%
// Legendary
if (roll < legendaryThreshold) return ItemRarity.legendary;
// Epic
if (roll < epicThreshold) return ItemRarity.epic;
// Rare
if (roll < rareThreshold) return ItemRarity.rare;
// Uncommon: 26-65 (40%) // Uncommon: 26-65 (40%)
if (roll < 66) return ItemRarity.uncommon; if (roll < 66) return ItemRarity.uncommon;
// Common: 66-99 (34%) // Common: 66-99 (34%)
@@ -331,8 +342,9 @@ class ItemService {
required EquipmentSlot slot, required EquipmentSlot slot,
required int level, required int level,
ItemRarity? rarity, ItemRarity? rarity,
int cha = 0,
}) { }) {
final itemRarity = rarity ?? determineRarity(level); final itemRarity = rarity ?? determineRarity(level, cha: cha);
final stats = generateItemStats( final stats = generateItemStats(
level: level, level: level,
rarity: itemRarity, rarity: itemRarity,

View File

@@ -56,7 +56,7 @@ class MarketService {
slot: slot, slot: slot,
targetRarity: ItemRarity.common, targetRarity: ItemRarity.common,
); );
final price = shopService.calculateBuyPrice(item); final price = shopService.calculateBuyPrice(item, cha: state.stats.cha);
if (nextState.inventory.gold >= price) { if (nextState.inventory.gold >= price) {
nextState = nextState.copyWith( nextState = nextState.copyWith(

View File

@@ -105,6 +105,7 @@ class ResurrectionService {
playerLevel: state.traits.level, playerLevel: state.traits.level,
currentGold: state.inventory.gold, currentGold: state.inventory.gold,
currentEquipment: state.equipment, currentEquipment: state.equipment,
cha: state.stats.cha,
); );
// 장비 적용 // 장비 적용

View File

@@ -18,10 +18,21 @@ class ShopService {
/// 장비 구매 가격 계산 /// 장비 구매 가격 계산
/// ///
/// 가격 = 아이템 레벨 * 50 * 희귀도 배율 /// 가격 = 아이템 레벨 * 50 * 희귀도 배율 * (1 - CHA 할인율)
int calculateBuyPrice(EquipmentItem item) { /// CHA 할인율(charisma discount): (CHA - 10) * 1%, 최대 15%, 최소 0%
int calculateBuyPrice(EquipmentItem item, {int cha = 0}) {
if (item.isEmpty) return 0; if (item.isEmpty) return 0;
return (item.level * 50 * _getRarityPriceMultiplier(item.rarity)).round(); final basePrice = (item.level * 50 * _getRarityPriceMultiplier(item.rarity))
.round();
final discount = chaDiscount(cha);
return (basePrice * (1.0 - discount)).round();
}
/// CHA 기반 할인율 계산
///
/// (CHA - 10) * 0.01, 0~0.15 범위로 클램프
static double chaDiscount(int cha) {
return ((cha - 10) * 0.01).clamp(0.0, 0.15);
} }
/// 장비 판매 가격 계산 /// 장비 판매 가격 계산
@@ -211,6 +222,7 @@ class ShopService {
required int playerLevel, required int playerLevel,
required int currentGold, required int currentGold,
required Equipment currentEquipment, required Equipment currentEquipment,
int cha = 0,
}) { }) {
var remainingGold = currentGold; var remainingGold = currentGold;
final purchasedItems = <EquipmentItem>[]; final purchasedItems = <EquipmentItem>[];
@@ -230,7 +242,7 @@ class ShopService {
targetRarity: ItemRarity.common, // 부활 시 Common만 구매 targetRarity: ItemRarity.common, // 부활 시 Common만 구매
); );
final price = calculateBuyPrice(shopItem); final price = calculateBuyPrice(shopItem, cha: cha);
if (price <= remainingGold) { if (price <= remainingGold) {
remainingGold -= price; remainingGold -= price;
purchasedItems.add(shopItem); purchasedItems.add(shopItem);
@@ -254,6 +266,7 @@ class ShopService {
required int currentGold, required int currentGold,
required EquipmentSlot slot, required EquipmentSlot slot,
ItemRarity? preferredRarity, ItemRarity? preferredRarity,
int cha = 0,
}) { }) {
final item = generateShopItem( final item = generateShopItem(
playerLevel: playerLevel, playerLevel: playerLevel,
@@ -261,7 +274,7 @@ class ShopService {
targetRarity: preferredRarity, targetRarity: preferredRarity,
); );
final price = calculateBuyPrice(item); final price = calculateBuyPrice(item, cha: cha);
if (price > currentGold) return null; if (price > currentGold) return null;
return PurchaseResult( return PurchaseResult(