Refactor Item Creation: Introduce LootGenerator

This commit is contained in:
Horoli 2025-12-07 18:47:36 +09:00
parent 46658b22c8
commit 32c77dd20a
3 changed files with 153 additions and 118 deletions

View File

@ -4,8 +4,9 @@ import 'package:flutter/services.dart';
import '../model/item.dart'; import '../model/item.dart';
import '../enums.dart'; import '../enums.dart';
import '../config/item_config.dart'; import '../config/item_config.dart';
import 'item_prefix_table.dart'; // Import prefix table // import 'item_prefix_table.dart'; // Logic moved to LootGenerator
import 'name_generator.dart'; // Import name generator // import 'name_generator.dart'; // Logic moved to LootGenerator
import '../logic/loot_generator.dart'; // Import LootGenerator
import '../../utils/game_math.dart'; import '../../utils/game_math.dart';
class ItemTemplate { class ItemTemplate {
@ -69,122 +70,9 @@ class ItemTemplate {
} }
Item createItem({int stage = 1}) { Item createItem({int stage = 1}) {
// Stage-based scaling is removed. // Stage parameter kept for interface compatibility but unused here,
// Apply Prefix Logic based on Rarity. // as scaling is now handled via Tier/Rarity in LootGenerator/Table logic.
return LootGenerator.generate(this);
String finalName = name;
int finalAtk = atkBonus;
int finalHp = hpBonus;
int finalArmor = armorBonus;
int finalLuck = luck;
final random = Random();
// 0. Normal Rarity: Prefix logic for base stat variations
if (rarity == ItemRarity.normal) {
// Weighted Random Selection
final prefixes = ItemPrefixTable.normalPrefixes;
int totalWeight = prefixes.fold(0, (sum, p) => sum + p.weight);
int roll = random.nextInt(totalWeight);
ItemModifier? selectedModifier;
int currentSum = 0;
for (var mod in prefixes) {
currentSum += mod.weight;
if (roll < currentSum) {
selectedModifier = mod;
break;
}
}
if (selectedModifier != null) {
if (selectedModifier.prefix.isNotEmpty) {
finalName = "${selectedModifier.prefix} $name";
}
double mult = selectedModifier.multiplier;
if (mult != 1.0) {
finalAtk = (finalAtk * mult).floor();
finalHp = (finalHp * mult).floor();
finalArmor = (finalArmor * mult).floor();
// Luck usually isn't scaled by small multipliers, but let's keep it consistent or skip.
// Skipping luck scaling for normal prefixes to avoid 0.
}
}
}
// 1. Magic Rarity: 50% chance to get a Magic Prefix (1 stat change)
else if (rarity == ItemRarity.magic) {
if (random.nextBool()) { // 50% chance
// Filter valid prefixes for this slot
final validPrefixes = ItemPrefixTable.magicPrefixes.where((p) {
return p.allowedSlots == null || p.allowedSlots!.contains(slot);
}).toList();
if (validPrefixes.isNotEmpty) {
final modifier = validPrefixes[random.nextInt(validPrefixes.length)];
finalName = "${modifier.prefix} $name";
modifier.statChanges.forEach((stat, value) {
switch(stat) {
case StatType.atk: finalAtk += value; break;
case StatType.maxHp: finalHp += value; break;
case StatType.defense: finalArmor += value; break;
case StatType.luck: finalLuck += value; break;
}
});
}
}
}
// 2. Rare Rarity: Always get a Rare Prefix (2 stat changes or stronger)
else if (rarity == ItemRarity.rare) {
bool nameChanged = false;
// Always generate a completely new cool name for Rare items
finalName = NameGenerator.generateName(slot);
nameChanged = true;
// Filter valid prefixes for this slot
final validPrefixes = ItemPrefixTable.rarePrefixes.where((p) {
return p.allowedSlots == null || p.allowedSlots!.contains(slot);
}).toList();
if (validPrefixes.isNotEmpty) {
final modifier = validPrefixes[random.nextInt(validPrefixes.length)];
// If name wasn't already changed by NameGenerator, apply prefix to name
if (!nameChanged) {
finalName = "${modifier.prefix} $name";
}
// Even if name changed, we STILL apply the stats from the prefix modifier!
// Because NameGenerator is just visual flavor, stats come from the modifier.
modifier.statChanges.forEach((stat, value) {
switch(stat) {
case StatType.atk: finalAtk += value; break;
case StatType.maxHp: finalHp += value; break;
case StatType.defense: finalArmor += value; break;
case StatType.luck: finalLuck += value; break;
}
});
}
}
// Legendary/Unique items usually keep their original names/stats as they are special.
return Item(
id: id,
name: finalName,
description: description,
atkBonus: finalAtk,
hpBonus: finalHp,
armorBonus: finalArmor,
slot: slot,
effects: effects,
price: price,
image: image,
luck: finalLuck,
rarity: rarity,
tier: tier,
);
} }
} }

View File

@ -0,0 +1,129 @@
import 'dart:math';
import '../model/item.dart';
import '../data/item_table.dart'; // For ItemTemplate
import '../data/item_prefix_table.dart';
import '../data/name_generator.dart';
import '../enums.dart';
class LootGenerator {
static final Random _random = Random();
/// Generates an Item instance from a template, applying prefixes/suffixes based on rarity.
static Item generate(ItemTemplate template) {
String finalName = template.name;
int finalAtk = template.atkBonus;
int finalHp = template.hpBonus;
int finalArmor = template.armorBonus;
int finalLuck = template.luck;
// 0. Normal Rarity: Prefix logic for base stat variations
if (template.rarity == ItemRarity.normal) {
// Weighted Random Selection
final prefixes = ItemPrefixTable.normalPrefixes;
int totalWeight = prefixes.fold(0, (sum, p) => sum + p.weight);
int roll = _random.nextInt(totalWeight);
ItemModifier? selectedModifier;
int currentSum = 0;
for (var mod in prefixes) {
currentSum += mod.weight;
if (roll < currentSum) {
selectedModifier = mod;
break;
}
}
if (selectedModifier != null) {
if (selectedModifier.prefix.isNotEmpty) {
finalName = "${selectedModifier.prefix} $template.name";
}
double mult = selectedModifier.multiplier;
if (mult != 1.0) {
finalAtk = (finalAtk * mult).floor();
finalHp = (finalHp * mult).floor();
finalArmor = (finalArmor * mult).floor();
}
}
}
// 1. Magic Rarity: 50% chance to get a Magic Prefix (1 stat change)
else if (template.rarity == ItemRarity.magic) {
if (_random.nextBool()) { // 50% chance
// Filter valid prefixes for this slot
final validPrefixes = ItemPrefixTable.magicPrefixes.where((p) {
return p.allowedSlots == null || p.allowedSlots!.contains(template.slot);
}).toList();
if (validPrefixes.isNotEmpty) {
final modifier = validPrefixes[_random.nextInt(validPrefixes.length)];
finalName = "${modifier.prefix} $template.name";
modifier.statChanges.forEach((stat, value) {
switch(stat) {
case StatType.atk: finalAtk += value; break;
case StatType.maxHp: finalHp += value; break;
case StatType.defense: finalArmor += value; break;
case StatType.luck: finalLuck += value; break;
}
});
}
}
}
// 2. Rare Rarity: Always get a Rare Prefix (2 stat changes or stronger)
else if (template.rarity == ItemRarity.rare) {
bool nameChanged = false;
// Always generate a completely new cool name for Rare items
finalName = NameGenerator.generateName(template.slot);
nameChanged = true;
// Filter valid prefixes for this slot
final validPrefixes = ItemPrefixTable.rarePrefixes.where((p) {
return p.allowedSlots == null || p.allowedSlots!.contains(template.slot);
}).toList();
if (validPrefixes.isNotEmpty) {
final modifier = validPrefixes[_random.nextInt(validPrefixes.length)];
// If name wasn't already changed by NameGenerator (fallback?), apply prefix to name
// But NameGenerator always returns a name.
// However, if we want to combine "Prefix" + "GeneratedName", we could.
// Current logic in ItemTable was:
if (!nameChanged) {
finalName = "${modifier.prefix} $template.name";
}
// Wait, the logic in ItemTable says:
// "If name wasn't already changed by NameGenerator, apply prefix to name"
// But just above it says "nameChanged = true;". So it never enters inside?
// Actually, NameGenerator might fail? No, it's static data.
// Let's stick to the logic: Rare items use generated names, but get STATS from prefix.
modifier.statChanges.forEach((stat, value) {
switch(stat) {
case StatType.atk: finalAtk += value; break;
case StatType.maxHp: finalHp += value; break;
case StatType.defense: finalArmor += value; break;
case StatType.luck: finalLuck += value; break;
}
});
}
}
// Legendary/Unique items usually keep their original names/stats.
return Item(
id: template.id,
name: finalName,
description: template.description,
atkBonus: finalAtk,
hpBonus: finalHp,
armorBonus: finalArmor,
slot: template.slot,
effects: template.effects,
price: template.price,
image: template.image,
luck: finalLuck,
rarity: template.rarity,
tier: template.tier,
);
}
}

View File

@ -0,0 +1,18 @@
# 58. Refactor Item Creation Logic
## 1. 목표 (Goal)
- `ItemTemplate` 클래스 내부에 강하게 결합된 아이템 생성 및 접두사(Prefix) 적용 로직을 분리합니다.
- `LootGenerator` 클래스를 생성하여 전리품 생성 및 옵션 부여 로직을 중앙화합니다.
## 2. 구현 계획 (Implementation Plan)
1. **`LootGenerator` 생성 (`lib/game/logic/loot_generator.dart`):**
- `ItemTemplate`을 입력받아 실제 `Item` 객체를 생성하는 static 메서드 `generate`를 구현합니다.
- 기존 `createItem`에 있던 Rarity별 접두사 처리, 스탯 보정, 이름 변경 로직을 이곳으로 이동합니다.
2. **`ItemTemplate` 수정 (`lib/game/data/item_table.dart`):**
- `createItem` 메서드가 `LootGenerator`를 호출하도록 변경하거나, 해당 메서드를 제거하고 호출부(`BattleProvider`, `EnemyTemplate`)에서 `LootGenerator`를 직접 사용하도록 수정합니다.
- (호환성을 위해 `createItem``LootGenerator`를 호출하는 래퍼로 남겨두는 것을 권장)
## 3. 기대 효과 (Expected Outcome)
- `ItemTemplate`은 순수한 데이터 정의(DTO) 역할에 집중.
- 아이템 생성 알고리즘(접두사, 랜덤 스탯 등)이 변경되더라도 데이터 클래스에는 영향 없음.
- 추후 '제작(Crafting)' 시스템이나 '상점 전용 생성' 등 다양한 생성 규칙 추가 시 `LootGenerator` 확장 용이.