feat: add subweapon support, weapon type logic, and compact item card UI options

This commit is contained in:
2026-04-28 01:39:39 +09:00
parent 0420e23939
commit 0e0748540e
46 changed files with 1795 additions and 577 deletions
+59 -28
View File
@@ -43,6 +43,9 @@ class EnemyIntent {
}
class BattleProvider with ChangeNotifier {
static final Duration _turnEffectVisualDelay =
AnimationConfig.floatingTextDuration + const Duration(milliseconds: 100);
late Character player;
late Character enemy; // Kept for compatibility, active during Battle/Elite
@@ -52,8 +55,6 @@ class BattleProvider with ChangeNotifier {
final BattleLogManager _logManager = BattleLogManager();
bool isPlayerTurn = true;
int _turnTransactionId = 0; // To prevent async race conditions
bool skipAnimations = false; // Sync with SettingsProvider
int stage = 1;
int turnCount = 1;
List<Item> rewardOptions = [];
@@ -62,6 +63,19 @@ class BattleProvider with ChangeNotifier {
List<String> get logs => _logManager.logs;
int get lastGoldReward => _lastGoldReward;
StageType get nextStageType => getStageTypeFor(stage + 1);
StageType getStageTypeFor(int stageNumber) {
if (stageNumber % GameConfig.eliteStageInterval == 0) {
return StageType.elite;
} else if (stageNumber % GameConfig.shopStageInterval == 0) {
return StageType.shop;
} else if (stageNumber % GameConfig.restStageInterval == 0) {
return StageType.rest;
}
return StageType.battle;
}
void refreshUI() {
notifyListeners();
@@ -75,6 +89,10 @@ class BattleProvider with ChangeNotifier {
final _effectEventController = StreamController<EffectEvent>.broadcast();
Stream<EffectEvent> get effectStream => _effectEventController.stream;
// Heal Event Stream
final _healEventController = StreamController<HealEvent>.broadcast();
Stream<HealEvent> get healStream => _healEventController.stream;
// Dependency injection
final ShopProvider shopProvider;
final Random _random; // Injected Random instance
@@ -88,6 +106,7 @@ class BattleProvider with ChangeNotifier {
void dispose() {
_damageEventController.close(); // StreamController 닫기
_effectEventController.close();
_healEventController.close();
super.dispose();
}
@@ -136,8 +155,8 @@ class BattleProvider with ChangeNotifier {
player.gold = GameConfig.startingGold;
// Add new status effect items for testing
player.addToInventory(ItemTable.weapons[3].createItem()); // Stunning Hammer
player.addToInventory(ItemTable.weapons[4].createItem()); // Jagged Dagger
player.addToInventory(ItemTable.weapons[6].createItem()); // Stunning Hammer
player.addToInventory(ItemTable.weapons[9].createItem()); // Jagged Dagger
player.addToInventory(ItemTable.weapons[5].createItem()); // Sunderer Axe
player.addToInventory(ItemTable.shields[3].createItem()); // Cursed Shield
@@ -164,18 +183,7 @@ class BattleProvider with ChangeNotifier {
// Reset Player Armor at start of new stage
player.armor = 0;
StageType type;
// Stage Type Logic
if (stage % GameConfig.eliteStageInterval == 0) {
type = StageType.elite; // Every 10th stage is a Boss/Elite
} else if (stage % GameConfig.shopStageInterval == 0) {
type = StageType.shop; // Every 5th stage is a Shop (except 10, 20...)
} else if (stage % GameConfig.restStageInterval == 0) {
type = StageType.rest; // Every 8th stage is a Rest
} else {
type = StageType.battle;
}
StageType type = getStageTypeFor(stage);
// Prepare Data based on Type
Character? newEnemy;
@@ -239,8 +247,7 @@ class BattleProvider with ChangeNotifier {
// 0. Ensure Pre-emptive Enemy Defense is applied (if not already via animation)
applyPendingEnemyDefense();
// Update Enemy Status Effects at the start of Player's turn (user request)
enemy.updateStatusEffects(); // 1. Check for Defense Forbidden status
// 1. Check for Defense Forbidden status (Player)
if (type == ActionType.defend &&
player.hasStatus(StatusEffectType.defenseForbidden)) {
_addLog("Cannot defend! You are under Defense Forbidden status.");
@@ -262,7 +269,7 @@ class BattleProvider with ChangeNotifier {
// If a visual effect occurred (bleed, stun), wait a bit before action
if (turnEffect.effectTriggered) {
await Future.delayed(const Duration(milliseconds: 800));
await Future.delayed(_turnEffectVisualDelay);
}
if (!turnEffect.canAct) {
@@ -390,7 +397,7 @@ class BattleProvider with ChangeNotifier {
void _endPlayerTurn() {
// Update durations at end of turn
player.updateStatusEffects();
player.updateEndOfTurnStatusEffects();
// Check if enemy is dead from bleed
if (enemy.isDead) {
@@ -519,6 +526,9 @@ class BattleProvider with ChangeNotifier {
effectTriggered = true;
}
// 3. Update durations for start-of-turn effects immediately
character.updateStartOfTurnStatusEffects();
return TurnEffectResult(
canAct: !isStunned,
effectTriggered: effectTriggered,
@@ -550,7 +560,7 @@ class BattleProvider with ChangeNotifier {
// If a visual effect occurred (bleed, stun), wait a bit before action
if (turnEffect.effectTriggered) {
await Future.delayed(const Duration(milliseconds: 800));
await Future.delayed(_turnEffectVisualDelay);
}
if (turnEffect.canAct && currentEnemyIntent != null) {
@@ -665,7 +675,7 @@ class BattleProvider with ChangeNotifier {
if (player.isDead) return; // Game Over check
// Update enemy status at the end of their turn
enemy.updateStatusEffects();
enemy.updateEndOfTurnStatusEffects();
// Generate NEXT intent
_generateEnemyIntent();
@@ -765,16 +775,20 @@ class BattleProvider with ChangeNotifier {
notifyListeners();
}
bool selectReward(Item item) {
bool selectReward(Item item, {bool completeStage = true}) {
if (item.id == "reward_skip") {
_addLog("Skipped reward.");
_completeStage();
if (completeStage) {
_completeStage();
}
return true;
} else {
bool added = player.addToInventory(item);
if (added) {
_addLog("Added ${item.name} to inventory.");
_completeStage();
if (completeStage) {
_completeStage();
}
return true;
} else {
_addLog("Inventory is full! Could not take ${item.name}.");
@@ -783,6 +797,10 @@ class BattleProvider with ChangeNotifier {
}
}
void completeStage() {
_completeStage();
}
void _completeStage() {
// Heal player after selecting reward
int healAmount = GameMath.floor(
@@ -802,9 +820,20 @@ class BattleProvider with ChangeNotifier {
notifyListeners();
}
void equipItem(Item item) {
if (player.equip(item)) {
_addLog("Equipped ${item.name}.");
void equipItem(Item item, {EquipmentSlot? targetSlot}) {
final success = targetSlot == null
? player.equip(item)
: player.equipToSlot(item, targetSlot);
if (success) {
final slotName = targetSlot == null
? item.typeName
: targetSlot == EquipmentSlot.weapon
? "Main Weapon"
: targetSlot == EquipmentSlot.shield
? "Subweapon"
: targetSlot.name;
_addLog("Equipped ${item.name} as $slotName.");
} else {
_addLog(
"Failed to equip ${item.name}.",
@@ -857,6 +886,7 @@ class BattleProvider with ChangeNotifier {
int healedAmount = player.hp - currentHp;
if (healedAmount > 0) {
_addLog("Used ${item.name}. Recovered $healedAmount HP.");
_healEventController.sink.add(HealEvent(amount: healedAmount, target: HealTarget.player));
effectApplied = true;
} else {
_addLog("Used ${item.name}. HP is already full.");
@@ -1095,6 +1125,7 @@ class BattleProvider with ChangeNotifier {
type: target.hasStatus(StatusEffectType.vulnerable)
? DamageType.vulnerable
: DamageType.normal,
risk: event.risk,
),
);
_addLog("${attacker.name} dealt $damageToHp damage to ${target.name}.");
+2 -2
View File
@@ -17,9 +17,9 @@ class ShopProvider with ChangeNotifier {
void generateShopItems(int stage) {
ItemTier currentTier = ItemTier.tier1;
if (stage > GameConfig.tier2StageMax)
if (stage > GameConfig.tier2StageMax) {
currentTier = ItemTier.tier3;
else if (stage > GameConfig.tier1StageMax)
} else if (stage > GameConfig.tier1StageMax)
currentTier = ItemTier.tier2;
availableItems = [];