GameState resolveCombatFlee(GameState state) { final combat = state.pendingCombat!; final player = state.player; // Calculate flee chance double fleeChance = 0.40; if (player.hasCrew(CrewRole.driver)) fleeChance += 0.25; if (player.hasCrew(CrewRole.lookout)) fleeChance += 0.15; if (player.upgradeLevel('fast_car') > 0) fleeChance += 0.20; fleeChance = fleeChance.clamp(0.1, 0.85); final fled = _random.nextDouble() < fleeChance; final newLog = List.from(state.eventLog); var newPlayer = player; if (fled) { // Successful flee: small heat increase, lose some money final moneyCost = player.money * 0.05; // drop some cash newPlayer = newPlayer.copyWith( heat: (newPlayer.heat + 10).clamp(0, 100), money: (newPlayer.money - moneyCost).clamp(0, double.infinity), comboCount: 0, ); newLog.insert(0, '๐Ÿƒ Escaped from ${combat.enemyName}! Dropped ${_fmt(moneyCost)} while running.'); } else { // Failed flee: worse consequences final invLoss = 0.15 + _random.nextDouble() * 0.15; var newInv = Map.from(newPlayer.inventory); 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 + 20).clamp(0, 100), inventory: newInv, bustCount: newPlayer.bustCount + 1, ); newLog.insert(0, 'โŒ Couldn\'t escape ${combat.enemyName}! Lost ${(invLoss * 100).round()}% of stash.'); } return state.copyWith( player: newPlayer, eventLog: newLog, status: GameStatus.playing, clearPendingCombat: true, ); } /// Accept a special deal ActionResult acceptSpecialDeal(GameState state, SpecialDeal deal) { final player = state.player; final totalCost = deal.totalCost; if (player.money < totalCost) { return ActionResult(success: false, message: 'Need ${_fmt(totalCost)} for this deal!'); } final newTotal = player.totalInventoryCount + deal.quantity; if (newTotal > player.stashLimit) { final space = player.stashLimit - player.totalInventoryCount; return ActionResult(success: false, message: 'Not enough stash space! Need ${deal.quantity} slots, have $space.'); } final newLog = List.from(state.eventLog); if (deal.isSetup) { // It's a trap! Police ambush final heatHit = 25.0 + _random.nextDouble() * 20; final invLoss = 0.3 + _random.nextDouble() * 0.3; var newInv = Map.from(player.inventory); for (final key in newInv.keys.toList()) { newInv[key] = (newInv[key]! * (1 - invLoss)).floor(); if (newInv[key]! == 0) newInv.remove(key); } var newPlayer = player.copyWith( money: (player.money - totalCost * 0.1).clamp(0, double.infinity), // lost advance payment heat: (player.heat + heatHit).clamp(0, 100), inventory: newInv, bustCount: player.bustCount + 1, comboCount: 0, ); newPlayer = _checkAchievements(newPlayer); newLog.insert(0, '๐Ÿš” IT WAS A SETUP! ${deal.npcName} was a cop! Heat +${heatHit.round()}, stash seized!'); return ActionResult( success: false, message: 'IT WAS A TRAP! Cops everywhere!', newState: state.copyWith( player: newPlayer, eventLog: newLog, clearPendingDeal: true, ), ); } else { // Legit deal final newInv = Map.from(player.inventory); newInv[deal.productId] = (newInv[deal.productId] ?? 0) + deal.quantity; var newPlayer = player.copyWith( money: player.money - totalCost, inventory: newInv, heat: (player.heat + 5).clamp(0, 100), // small heat for shady deal ); newPlayer = _checkAchievements(newPlayer); newLog.insert(0, '๐Ÿ’Ž Special deal accepted! ${deal.quantity}x ${deal.productName} for ${_fmt(totalCost)}'); return ActionResult( success: true, message: 'Deal done! ${deal.quantity}x ${deal.productName} acquired.', newState: state.copyWith( player: newPlayer, eventLog: newLog, clearPendingDeal: true, ), ); } } /// Upgrade shop purchases ActionResult buyUpgrade(GameState state, String upgradeId) { final upgrade = kUpgrades.firstWhere((u) => u.id == upgradeId, orElse: () => throw Exception('Unknown upgrade: $upgradeId')); final currentLevel = state.player.upgradeLevel(upgradeId); if (currentLevel >= upgrade.maxLevel) { return ActionResult(success: false, message: '${upgrade.name} is already maxed!'); } final cost = upgrade.costForLevel(currentLevel + 1); if (state.player.money < cost) { return ActionResult(success: false, message: 'Need ${_fmt(cost.toDouble())} for ${upgrade.name}.'); } // Net worth check for unlock if (upgrade.unlockNetWorth > 0 && state.player.netWorth < upgrade.unlockNetWorth) { return ActionResult(success: false, message: 'Need net worth ${_fmt(upgrade.unlockNetWorth)} to unlock.'); } final newUpgrades = Map.from(state.player.upgrades); newUpgrades[upgradeId] = currentLevel + 1; // Stash upgrade: increase stash limit int newStashLimit = state.player.stashLimit; if (upgradeId == 'stash') newStashLimit += 50; var newPlayer = state.player.copyWith( money: state.player.money - cost, upgrades: newUpgrades, stashLimit: newStashLimit, ); newPlayer = _checkAchievements(newPlayer); final newLog = List.from(state.eventLog) ..insert(0, '๐Ÿ›’ Bought ${upgrade.name} (Level ${currentLevel + 1})'); return ActionResult( success: true, message: '${upgrade.name} upgraded to Level ${currentLevel + 1}!', newState: state.copyWith(player: newPlayer, eventLog: newLog), ); } ActionResult borrow(GameState state, int amount) { if (amount < 1000 || amount > 100000) { return ActionResult(success: false, message: 'Can borrow \$1Kโ€“\$100K only.'); } final loan = Loan( id: 'loan_${DateTime.now().millisecondsSinceEpoch}', amount: amount, owed: amount, interestRate: 0.10, dayBorrowed: state.player.daysPassed, dueDay: state.player.daysPassed + 7, ); final newLoans = List.from(state.player.activeLoans)..add(loan); final newPlayer = state.player.copyWith( money: state.player.money + amount, activeLoans: newLoans, ); final newLog = List.from(state.eventLog) ..insert(0, '๐Ÿฆˆ Borrowed ${_fmt(amount.toDouble())} from loan shark. Due in 7 days.'); return ActionResult( success: true, message: 'Borrowed ${_fmt(amount.toDouble())}. Pay back in 7 days or else.', newState: state.copyWith(player: newPlayer, eventLog: newLog, statusMessage: null), ); } ActionResult payLoan(GameState state, String loanId) { final loan = state.player.activeLoans.firstWhere( (l) => l.id == loanId, orElse: () => throw Exception('Loan not found')); if (state.player.money < loan.owed) { return ActionResult(success: false, message: 'Not enough cash! Need ${_fmt(loan.owed.toDouble())}'); } final newLoans = state.player.activeLoans.where((l) => l.id != loanId).toList(); var newPlayer = state.player.copyWith( money: state.player.money - loan.owed, activeLoans: newLoans, repPoints: state.player.repPoints + 10, ); newPlayer = _checkAchievements(newPlayer); final newLog = List.from(state.eventLog) ..insert(0, 'โœ… Paid off loan of ${_fmt(loan.owed.toDouble())}. Loan shark satisfied. +10 rep.'); return ActionResult( success: true, message: 'Loan paid off!', newState: state.copyWith(player: newPlayer, eventLog: newLog, statusMessage: null), ); } ActionResult hireCrew(GameState state, String crewId) { final member = kAvailableCrew.firstWhere((c) => c.id == crewId, orElse: () => throw Exception('Unknown crew member')); if (state.player.crewIds.contains(crewId)) { return ActionResult(success: false, message: '${member.name} is already on your crew.'); } final hireCost = member.salary * 3; if (state.player.money < hireCost) { return ActionResult(success: false, message: 'Need ${_fmt(hireCost.toDouble())} to hire ${member.name} (3-day deposit).'); } final newCrew = List.from(state.player.crewIds)..add(crewId); final newPlayer = state.player.copyWith( money: state.player.money - hireCost, crewIds: newCrew, ); final newLog = List.from(state.eventLog) ..insert(0, '๐Ÿ‘ฅ Hired ${member.name} (${member.role.title}).'); return ActionResult( success: true, message: '${member.name} is on the crew.', newState: state.copyWith(player: newPlayer, eventLog: newLog, statusMessage: null), ); } ActionResult fireCrew(GameState state, String crewId) { final newCrew = List.from(state.player.crewIds)..remove(crewId); final newPlayer = state.player.copyWith(crewIds: newCrew); final member = kAvailableCrew.firstWhere((c) => c.id == crewId, orElse: () => throw Exception('Unknown crew member')); final newLog = List.from(state.eventLog) ..insert(0, '๐Ÿšช Fired ${member.name}.'); return ActionResult( success: true, message: '${member.name} fired.', newState: state.copyWith(player: newPlayer, eventLog: newLog, statusMessage: null), ); } ActionResult bribe(GameState state) { const bribeCost = 500.0; if (state.player.money < bribeCost) { return ActionResult(success: false, message: 'Need \$500 to bribe a cop.'); } final newPlayer = state.player.copyWith( money: state.player.money - bribeCost, heat: (state.player.heat - 25).clamp(0, 100), ); final newLog = List.from(state.eventLog) ..insert(0, '๐Ÿ’ธ Paid off a cop. -25 heat.'); return ActionResult( success: true, message: 'Cop bribed. Heat reduced.', newState: state.copyWith(player: newPlayer, eventLog: newLog, statusMessage: null), ); } ActionResult upgradeStash(GameState state) { final cost = state.player.stashLimit * 100.0; if (state.player.money < cost) { return ActionResult(success: false, message: 'Need ${_fmt(cost)} to upgrade stash.'); } final newLimit = state.player.stashLimit + 50; var newPlayer = state.player.copyWith( money: state.player.money - cost, stashLimit: newLimit, ); newPlayer = _checkAchievements(newPlayer); final newLog = List.from(state.eventLog) ..insert(0, '๐Ÿ“ฆ Stash upgraded to $newLimit units!'); return ActionResult( success: true, message: 'Stash upgraded to $newLimit units!', newState: state.copyWith(player: newPlayer, eventLog: newLog, statusMessage: null), ); } GameState applyEventEffect(GameState state, GameEvent event, String chosenAction) { var player = state.player; final newLog = List.from(state.eventLog); switch (event.type) { case EventType.mugging: if (chosenAction == 'Walk It Off') { final lost = (event.effects['moneyLost'] as double); final actualLost = player.hasCrew(CrewRole.enforcer) ? lost * 0.5 : lost; final finalLost = player.hasCrew(CrewRole.lawyer) ? actualLost * 0.6 : actualLost; player = player.copyWith( money: (player.money - finalLost).clamp(0, double.infinity), eventsSurvived: player.eventsSurvived + 1, ); newLog.insert(0, '๐Ÿ”ซ Mugged. Lost ${_fmt(finalLost)}.'); } break; case EventType.police: final heatUp = (event.effects['heatIncrease'] as double); final invLoss = (event.effects['inventoryLoss'] as double? ?? 0.0); player = player.copyWith( heat: (player.heat + heatUp).clamp(0, 100), eventsSurvived: player.eventsSurvived + 1, bustCount: invLoss > 0 ? player.bustCount + 1 : player.bustCount, ); if (invLoss > 0) { final newInv = Map.from(player.inventory); for (final key in newInv.keys.toList()) { final qty = newInv[key]!; newInv[key] = (qty * (1 - invLoss)).floor(); if (newInv[key]! == 0) newInv.remove(key); } player = player.copyWith(inventory: newInv); newLog.insert(0, '๐Ÿš” Police took ${(invLoss * 100).round()}% of your stash!'); } break; case EventType.informant: if (chosenAction == 'Pay Up') { final cost = event.effects['moneyCost'] as double; final actualCost = player.hasCrew(CrewRole.lawyer) ? cost * 0.6 : cost; player = player.copyWith( money: (player.money - actualCost).clamp(0, double.infinity), heat: (player.heat - (event.effects['heatDecrease'] as double)).clamp(0, 100), eventsSurvived: player.eventsSurvived + 1, ); newLog.insert(0, '๐Ÿคซ Paid informant ${_fmt(actualCost)}. Heat dropped.'); } break; case EventType.marketSpike: case EventType.marketCrash: final productId = event.effects['product'] as String; final mult = event.effects['multiplier'] as double; final newPrices = Map>.from(state.marketPrices); for (final cityId in newPrices.keys) { final cityPrices = Map.from(newPrices[cityId]!); if (cityPrices.containsKey(productId)) { cityPrices[productId] = (cityPrices[productId]! * mult).roundToDouble(); } newPrices[cityId] = cityPrices; } return state.copyWith( player: player.copyWith(eventsSurvived: player.eventsSurvived + 1), marketPrices: newPrices, eventLog: newLog, clearPendingEvent: true, ); case EventType.warNews: final mods = event.effects['productModifiers'] as Map?; if (mods != null) { final newPrices = Map>.from(state.marketPrices); for (final cityId in newPrices.keys) { final cityPrices = Map.from(newPrices[cityId]!); for (final entry in mods.entries) { if (cityPrices.containsKey(entry.key)) { cityPrices[entry.key] = (cityPrices[entry.key]! * (entry.value as num).toDouble()).roundToDouble(); } } newPrices[cityId] = cityPrices; } player = player.copyWith(warActive: true, eventsSurvived: player.eventsSurvived + 1); return state.copyWith( player: player, marketPrices: newPrices, eventLog: newLog, clearPendingEvent: true, ); } player = player.copyWith(warActive: true, eventsSurvived: player.eventsSurvived + 1); break; case EventType.freeProduct: player = player.copyWith(eventsSurvived: player.eventsSurvived + 1); break; case EventType.loanSharkVisit: case EventType.hotTip: case EventType.none: break; } player = _checkAchievements(player); return state.copyWith( player: player, eventLog: newLog, clearPendingEvent: true, ); } /// Check and unlock achievements Player _checkAchievements(Player player) { final Set newAchievements = Set.from(player.achievements); // First deal if (player.dealsMade >= 1) newAchievements.add('first_deal'); // First million if (player.lifetimeCash >= 1000000) newAchievements.add('first_million'); // Ten million if (player.lifetimeCash >= 10000000) newAchievements.add('ten_million'); // Kingpin rep if (player.repLevel.index >= RepLevel.kingpin.index) newAchievements.add('kingpin'); // Cartel boss if (player.repLevel == RepLevel.cartelBoss) newAchievements.add('cartel_boss'); // Globetrotter if (player.visitedCities.length >= 8) newAchievements.add('globetrotter'); // Combo if (player.comboCount >= 5) newAchievements.add('combo_5'); if (player.comboCount >= 10) newAchievements.add('combo_10'); // Boss fights if (player.bossesFought >= 1) newAchievements.add('crime_boss'); // Territory if (player.territories.length >= 3) newAchievements.add('territory_king'); // Survivor if (player.combatWins >= 10) newAchievements.add('survivor'); // Stash master if (player.stashLimit >= 300) newAchievements.add('stash_master'); // Century club if (player.dealsMade >= 100) newAchievements.add('hundred_deals'); // Untouchable: 30 days, 0 busts if (player.daysPassed >= 30 && player.bustCount == 0) newAchievements.add('untouchable'); if (newAchievements.length == player.achievements.length) return player; return player.copyWith(achievements: newAchievements); } String _fmt(double amount) { if (amount >= 1000000) return '\$${(amount / 1000000).toStringAsFixed(1)}M'; if (amount >= 1000) return '\$${(amount / 1000).toStringAsFixed(1)}K'; return '\$${amount.toStringAsFixed(0)}'; } // === EMPIRE: WEAPONS === ActionResult buyWeapon(GameState state, String weaponId) { final weapon = Weapon.fromId(weaponId); if (weapon == null) return ActionResult(success: false, message: 'Unknown weapon.'); if (state.player.ownedWeaponIds.contains(weaponId)) { return ActionResult(success: false, message: 'You already own a ${weapon.name}.'); } if (state.player.repPoints < weapon.requiredRep) { return ActionResult(success: false, message: 'Need ${weapon.requiredRep} rep to buy ${weapon.name}. You have ${state.player.repPoints}.'); } double cost = weapon.price; // General gives 50% off weapons if (state.player.hasPoliticalAbility('weapons_discount')) cost *= 0.5; if (state.player.money < cost) { return ActionResult(success: false, message: 'Need ${_fmt(cost)} for ${weapon.name}. You have ${_fmt(state.player.money)}.'); } final newWeapons = List.from(state.player.ownedWeaponIds)..add(weaponId); // Give starter ammo (2 mags) final newAmmo = Map.from(state.player.ammo); newAmmo[weaponId] = (newAmmo[weaponId] ?? 0) + weapon.magazineSize * 2; // Auto-equip if nothing equipped final newEquipped = state.player.equippedWeaponId ?? weaponId; var newPlayer = state.player.copyWith( money: state.player.money - cost, ownedWeaponIds: newWeapons, ammo: newAmmo, equippedWeaponId: newEquipped, ); newPlayer = _checkAchievements(newPlayer); final newLog = List.from(state.eventLog) ..insert(0, '${weapon.emoji} Bought ${weapon.name} for ${_fmt(cost)}!'); return ActionResult( success: true, message: '${weapon.name} acquired! ${weapon.magazineSize * 2} rounds loaded.', newState: state.copyWith(player: newPlayer, eventLog: newLog), ); } ActionResult equipWeapon(GameState state, String weaponId) { if (!state.player.ownedWeaponIds.contains(weaponId)) { return ActionResult(success: false, message: 'You don\'t own that weapon.'); } final weapon = Weapon.fromId(weaponId)!; final newPlayer = state.player.copyWith(equippedWeaponId: weaponId); return ActionResult( success: true, message: '${weapon.name} equipped!', newState: state.copyWith(player: newPlayer), ); } ActionResult buyAmmo(GameState state, String weaponId, int magazines) { final weapon = Weapon.fromId(weaponId); if (weapon == null) return ActionResult(success: false, message: 'Unknown weapon.'); if (!state.player.ownedWeaponIds.contains(weaponId)) { return ActionResult(success: false, message: 'You don\'t own a ${weapon.name}.'); } final cost = weapon.ammoPricePerMag * magazines; if (state.player.money < cost) { return ActionResult(success: false, message: 'Need ${_fmt(cost)} for $magazines mags of ammo.'); } final rounds = weapon.magazineSize * magazines; final newAmmo = Map.from(state.player.ammo); newAmmo[weaponId] = (newAmmo[weaponId] ?? 0) + rounds; final newPlayer = state.player.copyWith( money: state.player.money - cost, ammo: newAmmo, ); final newLog = List.from(state.eventLog) ..insert(0, '๐Ÿ”ถ Bought $magazines magazines ($rounds rounds) for ${weapon.name} โ€” ${_fmt(cost)}'); return ActionResult( success: true, message: '$rounds rounds loaded for ${weapon.name}!', newState: state.copyWith(player: newPlayer, eventLog: newLog), ); } // === EMPIRE: ARMOR === ActionResult buyArmor(GameState state, String armorId) { final armor = Armor.fromId(armorId); if (armor == null) return ActionResult(success: false, message: 'Unknown armor.'); if (state.player.ownedArmorIds.contains(armorId)) { return ActionResult(success: false, message: 'You already own ${armor.name}.'); } if (state.player.repPoints < armor.requiredRep) { return ActionResult(success: false, message: 'Need ${armor.requiredRep} rep to buy ${armor.name}.'); } if (state.player.money < armor.price) { return ActionResult(success: false, message: 'Need ${_fmt(armor.price)} for ${armor.name}.'); } final newArmors = List.from(state.player.ownedArmorIds)..add(armorId); final newDurability = Map.from(state.player.armorDurability); newDurability[armorId] = armor.maxDurability; // Auto-equip if nothing equipped final newEquipped = state.player.equippedArmorId ?? armorId; var newPlayer = state.player.copyWith( money: state.player.money - armor.price, ownedArmorIds: newArmors, armorDurability: newDurability, equippedArmorId: newEquipped, ); newPlayer = _checkAchievements(newPlayer); final newLog = List.from(state.eventLog) ..insert(0, '${armor.emoji} Bought ${armor.name} for ${_fmt(armor.price)}!'); return ActionResult( success: true, message: '${armor.name} equipped! ${armor.defense}% damage reduction.', newState: state.copyWith(player: newPlayer, eventLog: newLog), ); } ActionResult equipArmor(GameState state, String armorId) { if (!state.player.ownedArmorIds.contains(armorId)) { return ActionResult(success: false, message: 'You don\'t own that armor.'); } final armor = Armor.fromId(armorId)!; final newPlayer = state.player.copyWith(equippedArmorId: armorId); return ActionResult( success: true, message: '${armor.name} equipped! ${armor.defense}% defense.', newState: state.copyWith(player: newPlayer), ); } ActionResult repairArmor(GameState state, String armorId) { final armor = Armor.fromId(armorId); if (armor == null) return ActionResult(success: false, message: 'Unknown armor.'); if (!state.player.ownedArmorIds.contains(armorId)) { return ActionResult(success: false, message: 'You don\'t own that armor.'); } final currentDur = state.player.armorDurability[armorId] ?? armor.maxDurability; final missing = armor.maxDurability - currentDur; if (missing == 0) return ActionResult(success: false, message: '${armor.name} is already at full durability.'); final cost = armor.price * 0.3 * (missing / armor.maxDurability); if (state.player.money < cost) { return ActionResult(success: false, message: 'Need ${_fmt(cost)} to repair ${armor.name}.'); } final newDurability = Map.from(state.player.armorDurability); newDurability[armorId] = armor.maxDurability; final newPlayer = state.player.copyWith( money: state.player.money - cost, armorDurability: newDurability, ); final newLog = List.from(state.eventLog) ..insert(0, '๐Ÿ”ง Repaired ${armor.name} for ${_fmt(cost)}'); return ActionResult( success: true, message: '${armor.name} fully repaired!', newState: state.copyWith(player: newPlayer, eventLog: newLog), ); } // === EMPIRE: POLITICAL === ActionResult buyPoliticalAsset(GameState state, String assetId) { final asset = PoliticalAsset.fromId(assetId); if (asset == null) return ActionResult(success: false, message: 'Unknown political asset.'); if (state.player.politicalAssetIds.contains(assetId)) { return ActionResult(success: false, message: '${asset.name} is already on your payroll.'); } if (state.player.repPoints < asset.requiredRep) { return ActionResult(success: false, message: 'Need ${asset.requiredRep} rep to buy ${asset.title}.'); } if (state.player.money < asset.cost) { return ActionResult(success: false, message: 'Need ${_fmt(asset.cost)} to acquire ${asset.name}.'); } final newAssets = List.from(state.player.politicalAssetIds)..add(assetId); var newPlayer = state.player.copyWith( money: state.player.money - asset.cost, politicalAssetIds: newAssets, ); newPlayer = _checkAchievements(newPlayer); final newLog = List.from(state.eventLog) ..insert(0, '${asset.emoji} ${asset.name} is now on your payroll! Cost: ${_fmt(asset.cost)}'); return ActionResult( success: true, message: '${asset.name} acquired! ${asset.specialAbility}', newState: state.copyWith(player: newPlayer, eventLog: newLog), ); } // === EMPIRE: LAUNDRY === ActionResult buyLaundryFront(GameState state, String frontId) { final front = LaundryFront.fromId(frontId); if (front == null) return ActionResult(success: false, message: 'Unknown business.'); if (state.player.laundryFrontIds.contains(frontId)) { return ActionResult(success: false, message: 'You already own ${front.name}.'); } if (state.player.repPoints < front.requiredRep) { return ActionResult(success: false, message: 'Need ${front.requiredRep} rep to buy ${front.name}.'); } if (state.player.money < front.cost) { return ActionResult(success: false, message: 'Need ${_fmt(front.cost)} for ${front.name}.'); } final newFronts = List.from(state.player.laundryFrontIds)..add(frontId); final newLevels = Map.from(state.player.laundryLevels); newLevels[frontId] = 1; var newPlayer = state.player.copyWith( money: state.player.money - front.cost, laundryFrontIds: newFronts, laundryLevels: newLevels, ); newPlayer = _checkAchievements(newPlayer); final newLog = List.from(state.eventLog) ..insert(0, '${front.emoji} Bought ${front.name}! Cleans ${_fmt(front.dailyClean)}/day.'); return ActionResult( success: true, message: '${front.name} open for business! ${_fmt(front.dailyClean)}/day laundered.', newState: state.copyWith(player: newPlayer, eventLog: newLog), ); } ActionResult upgradeLaundryFront(GameState state, String frontId) { final front = LaundryFront.fromId(frontId); if (front == null) return ActionResult(success: false, message: 'Unknown business.'); if (!state.player.laundryFrontIds.contains(frontId)) { return ActionResult(success: false, message: 'You don\'t own ${front.name}.'); } final currentLevel = state.player.laundryLevels[frontId] ?? 1; if (currentLevel >= front.maxLevel) { return ActionResult(success: false, message: '${front.name} is already maxed out!'); } final cost = front.upgradeCost(currentLevel); if (state.player.money < cost) { return ActionResult(success: false, message: 'Need ${_fmt(cost)} to upgrade ${front.name}.'); } final newLevels = Map.from(state.player.laundryLevels); newLevels[frontId] = currentLevel + 1; final newPlayer = state.player.copyWith( money: state.player.money - cost, laundryLevels: newLevels, ); final newDailyClean = front.cleanAtLevel(currentLevel + 1); final newLog = List.from(state.eventLog) ..insert(0, 'โฌ†๏ธ Upgraded ${front.name} to Level ${currentLevel + 1}! Now cleans ${_fmt(newDailyClean)}/day.'); return ActionResult( success: true, message: '${front.name} upgraded! Now cleans ${_fmt(newDailyClean)}/day.', newState: state.copyWith(player: newPlayer, eventLog: newLog), ); } ActionResult launderCash(GameState state, double amount) { if (amount <= 0) return ActionResult(success: false, message: 'Invalid amount.'); if (state.player.dirtyCash < amount) { return ActionResult(success: false, message: 'Only have ${_fmt(state.player.dirtyCash)} in dirty cash.'); } if (state.player.laundryFrontIds.isEmpty) { return ActionResult(success: false, message: 'You need a laundry front first!'); } // Laundering takes a 15% cut (criminal tax) final cleanAmount = amount * 0.85; final newPlayer = state.player.copyWith( dirtyCash: state.player.dirtyCash - amount, money: state.player.money + cleanAmount, ); final newLog = List.from(state.eventLog) ..insert(0, '๐Ÿงน Laundered ${_fmt(amount)}. Received ${_fmt(cleanAmount)} clean.'); return ActionResult( success: true, message: 'Laundered ${_fmt(amount)}. You received ${_fmt(cleanAmount)} after 15% cut.', newState: state.copyWith(player: newPlayer, eventLog: newLog), ); } /// Player initiates combat against rival territory ActionResult initiateCombat(GameState state) { final player = state.player; if (player.heat > 85) { return ActionResult( success: false, message: '๐Ÿš” Too hot to start a fight right now. Cops everywhere.', ); } final combatEvent = _eventService.initiateRivalCombat(player.currentCityId); return ActionResult( success: true, message: 'You made your move...', newState: state.copyWith(pendingCombat: combatEvent), ); } /// Scaled price helper โ€” prices increase with player wealth double scaledPrice(double basePrice, double netWorth, double scaleDivisor) { final multiplier = 1.0 + (netWorth / scaleDivisor).clamp(0, 10.0); return basePrice * multiplier; } /// Apply a severe bust event: lose ALL product + 50% cash GameState applyBust(GameState state) { final player = state.player; final hasSafeHouse = player.upgradeLevel('safe_house') > 0; final inventoryLoss = hasSafeHouse ? 0.5 : 1.0; var newInv = Map.from(player.inventory); for (final key in newInv.keys.toList()) { final qty = newInv[key]!; newInv[key] = (qty * (1 - inventoryLoss)).floor(); if (newInv[key]! == 0) newInv.remove(key); } final newPlayer = player.copyWith( inventory: newInv, money: (player.money * 0.5).clamp(0, double.infinity), heat: 90.0, bustCount: player.bustCount + 1, comboCount: 0, ); final newLog = List.from(state.eventLog) ..insert(0, '๐Ÿš” BUSTED! ${hasSafeHouse ? "50%" : "ALL"} product seized + 50% cash gone! 30 days heat maxed.'); return state.copyWith( player: newPlayer, eventLog: newLog, statusMessage: '๐Ÿš” BUSTED โ€” You need to lay low.', ); } } // Upgrade definitions class UpgradeDef { final String id; final String emoji; final String name; final String description; final int maxLevel; final double unlockNetWorth; // 0 = always available final List costs; // cost per level const UpgradeDef({ required this.id, required this.emoji, required this.name, required this.description, required this.maxLevel, required this.unlockNetWorth, required this.costs, }); int costForLevel(int level) { if (level <= 0 || level > costs.length) return 0; return costs[level - 1]; } } const List kUpgrades = [ UpgradeDef( id: 'stash', emoji: '๐Ÿ“ฆ', name: 'Stash Expansion', description: '+50 capacity per level', maxLevel: 10, unlockNetWorth: 0, costs: [10000, 20000, 35000, 55000, 80000, 110000, 150000, 200000, 260000, 350000], ), UpgradeDef( id: 'scanner', emoji: '๐Ÿ“ก', name: 'Police Scanner', description: 'Heat gain -40% on all deals', maxLevel: 1, unlockNetWorth: 50000, costs: [25000], ), UpgradeDef( id: 'ac_unit', emoji: 'โ„๏ธ', name: 'AC Unit (Hideout)', description: '+8 heat decay on travel', maxLevel: 1, unlockNetWorth: 30000, costs: [15000], ), UpgradeDef( id: 'fast_car', emoji: '๐ŸŽ๏ธ', name: 'Fast Car', description: 'Travel cost -50%, +20% flee chance', maxLevel: 1, unlockNetWorth: 75000, costs: [40000], ), UpgradeDef( id: 'enforcer_gear', emoji: '๐ŸฅŠ', name: 'Enforcer Gear', description: '+15% combat strength per level', maxLevel: 3, unlockNetWorth: 20000, costs: [12000, 30000, 60000], ), UpgradeDef( id: 'safe_house', emoji: '๐Ÿ ', name: 'Safe House Network', description: 'Stash is never 100% seized on bust', maxLevel: 1, unlockNetWorth: 100000, costs: [50000], ), ];