GameState resolveCombatFight(GameState state) { final combat = state.pendingCombat!; final player = state.player; final isBoss = combat.type == CombatType.bossChallenge; // Calculate player fight strength with weapons/armor int playerStr = 30; // base if (player.hasCrew(CrewRole.enforcer)) playerStr += 30; final enforcerUpgrade = player.upgradeLevel('enforcer_gear'); playerStr += enforcerUpgrade * 15; playerStr += player.weaponCombatBonus; playerStr = playerStr.clamp(0, 100); final newLog = List.from(state.eventLog); var newPlayer = player; // Consume ammo on fight Map newAmmo = Map.from(player.ammo); if (player.equippedWeaponId != null) { final weapon = player.equippedWeapon; if (weapon != null) { final roundsUsed = (weapon.fireRate * 3).round(); final current = newAmmo[player.equippedWeaponId!] ?? 0; newAmmo[player.equippedWeaponId!] = (current - roundsUsed).clamp(0, 9999); } } // Apply armor damage reduction final armorDefense = player.armorDefensePercent; final effectiveEnemyStr = (combat.enemyStrength * (1.0 - armorDefense / 100.0)).round(); final roll = _random.nextDouble() * 100; final winThreshold = (playerStr - effectiveEnemyStr + 50).clamp(10, 90).toDouble(); final playerWins = roll < winThreshold; // Damage armor on fight Map newArmorDurability = Map.from(player.armorDurability); if (player.equippedArmorId != null) { final armor = player.equippedArmor; if (armor != null) { final damage = playerWins ? 10 : 25; final current = newArmorDurability[player.equippedArmorId!] ?? armor.maxDurability; final newDur = (current - damage).clamp(0, armor.maxDurability); newArmorDurability[player.equippedArmorId!] = newDur; if (armor.regenerates && newDur > 0) { newArmorDurability[player.equippedArmorId!] = (newDur + 5).clamp(0, armor.maxDurability); } } } if (playerWins) { // === WIN === final heatDec = (combat.winRewards['heatDecrease'] as num?)?.toDouble() ?? 20.0; var repGain = (combat.winRewards['repGain'] as num?)?.toInt() ?? 50; var moneyGain = (combat.winRewards['money'] as num?)?.toDouble() ?? 1000.0; final claimTerritory = combat.winRewards['territory'] == true; // Boss bonuses if (isBoss) { moneyGain += combat.enemyStrength * 500.0; repGain += combat.enemyStrength * 5; } // Generate loot final lootItems = isBoss ? LootPools.generateBossLoot(player.currentCityId, combat.enemyStrength) : []; double totalLootValue = 0; for (final loot in lootItems) { totalLootValue += loot.cashValue; newLog.insert(0, '${loot.rarity.emoji} ${loot.rarity.label}: ${loot.name} (\$${_fmt(loot.cashValue)})'); } moneyGain += totalLootValue; // Update territories Set newTerritories = Set.from(newPlayer.territories); if (claimTerritory) newTerritories.add(player.currentCityId); // Update gangTerritories status to controlled Map newGangTerritories = Map.from(newPlayer.gangTerritories); if (claimTerritory) { final current = newGangTerritories[player.currentCityId]; if (current != null) { newGangTerritories[player.currentCityId] = current.copyWith( status: TerritoryStatus.controlled, ); } else { newGangTerritories[player.currentCityId] = GangTerritory( cityId: player.currentCityId, status: TerritoryStatus.controlled, ); } } newPlayer = newPlayer.copyWith( heat: (newPlayer.heat - heatDec).clamp(0, 100), repPoints: newPlayer.repPoints + repGain, money: newPlayer.money + moneyGain, combatWins: newPlayer.combatWins + 1, territories: newTerritories, gangTerritories: newGangTerritories, bossesFought: isBoss ? newPlayer.bossesFought + 1 : newPlayer.bossesFought, eventsSurvived: newPlayer.eventsSurvived + 1, ammo: newAmmo, armorDurability: newArmorDurability, ); if (isBoss) { newLog.insert(0, '👑 BOSS DEFEATED! Bonus loot: +\$${_fmt(moneyGain)} cash, +$repGain rep'); } if (claimTerritory) { newLog.insert(0, '🏴 YOU NOW CONTROL ${player.currentCityId.toUpperCase()}! Passive income unlocked!'); } newLog.insert(0, '💪 FIGHT WON vs ${combat.enemyName}! Rep +$repGain, Heat -${heatDec.round()}'); } else { // === LOSE === final heatInc = (combat.loseConsequences['heatIncrease'] as num?)?.toDouble() ?? 20.0; final invLoss = (combat.loseConsequences['inventoryLoss'] as num?)?.toDouble() ?? 0.25; final moneyLossFrac = (combat.loseConsequences['moneyLoss'] as num?)?.toDouble() ?? 0.1; var newInv = Map.from(newPlayer.inventory); if (invLoss > 0) { for (final key in newInv.keys.toList()) { newInv[key] = (newInv[key]! * (1 - invLoss)).floor(); if (newInv[key]! == 0) newInv.remove(key); } } newPlayer = newPlayer.copyWith( heat: (newPlayer.heat + heatInc).clamp(0, 100), money: (newPlayer.money * (1 - moneyLossFrac)).clamp(0, double.infinity), inventory: newInv, bustCount: newPlayer.bustCount + 1, ammo: newAmmo, armorDurability: newArmorDurability, ); newLog.insert(0, '💀 FIGHT LOST vs ${combat.enemyName}! Lost ${(invLoss * 100).round()}% stash, took +${heatInc.round()} heat.'); } newPlayer = _checkAchievements(newPlayer); return state.copyWith( player: newPlayer, eventLog: newLog, status: GameStatus.playing, clearPendingCombat: true, ); }