A mobile trading game where you flip cars for profit!
Loads about 140 MB — desktop recommended
Welcome to Car Flip — a hybrid casual mobile game where you build your fortune by buying, opening, and selling cars. **How it works:** Open containers to discover cars of different segments and rarities. Match them to the right customers, negotiate deals, and grow your wealth from a small-time dealer to a legendary car mogul. **Features:** - 11 car segments — from Street to GodTier - 6 rarity tiers — Common to Secret - Dynamic customer system with unique requests and stat preferences - Upgrade system to boost your business - Economy balancing — every decision matters - Rescue system so you never get stuck - Clean UI and satisfying container opening animations **Developed solo** Using Unity and C#. Available on Google Play.
Bug Fix
WebGL Build
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using System.Linq;
public class CustomerManager : MonoBehaviour
{
public static CustomerManager Instance { get; private set; }
[SerializeField] private GameObject wantPrefab;
[SerializeField] private Transform spawnPoint;
[SerializeField] private Transform salesPoint;
[SerializeField] private CustomerPool customerPool;
private GameObject currentWantObj;
private TMP_Text wantText;
private Coroutine timeoutCoroutine;
private Customer currentCustomer;
private Queue<Customer> dailyQueue = new Queue<Customer>();
public Customer CurrentCustomer => currentCustomer;
private static readonly Dictionary<int, List<(int count, float weight)>> wantCountTable = new()
{
{ 0, new List<(int, float)> { (1, 100) } },
{ 3, new List<(int, float)> { (1, 70f), (2, 30f) } },
{ 10, new List<(int, float)> { (1, 30f), (2, 60f), (3, 10f) } },
{ 40, new List<(int, float)> { (2, 50f), (3, 50f) } },
};
private void Awake()
{
if (Instance == null) Instance = this;
else Destroy(gameObject);
}
public void StartDay()
{
dailyQueue.Clear();
int count = DayManager.Instance.DailyCustomerCount();
for (int i = 0; i < count; i++)
{
Customer customer = customerPool.Get();
if (customer == null) continue;
dailyQueue.Enqueue(customer);
}
if (TutorialManager.Instance.InTutorial)
{
UIManager.Instance.ShowInfoPanel.GetComponent<ShowInfoPanelScript>().OpenControl = true;
UIManager.Instance.ShowInfo(
"Welcome to Car Flip!\n\nCustomers want specific cars \nwith specific stats.\nRead their request,go to shop,\n buy and open a container, \nfind the right car and \nsell it for profit!",
UIManager.InfoType.Info
);
}
float firstDelay = TutorialManager.Instance.InTutorial ? 0.5f : Random.Range(2f, 4f);
Invoke(nameof(SendNextCustomer), firstDelay);
}
public void StartDayWithCount(int count)
{
dailyQueue.Clear();
for (int i = 0; i < count; i++)
{
Customer customer = customerPool.Get();
if (customer == null) continue;
dailyQueue.Enqueue(customer);
}
Invoke(nameof(SendNextCustomer), Random.Range(2f, 4f));
}
public void OnCustomerDone()
{
UIManager.Instance.SetCustomerWantsHUD("No Customer!");
StartCoroutine(GoBackThenSendNext());
}
public int InQueueCount()
{
return dailyQueue.Count + (currentCustomer != null ? 1 : 0);
}
public void CustomerGoingBack()
{
if (currentCustomer == null) return;
if (timeoutCoroutine != null)
{
StopCoroutine(timeoutCoroutine);
timeoutCoroutine = null;
}
Animator anim = currentCustomer.gameObject.GetComponentInChildren<Animator>();
StartCoroutine(MoveBack(currentCustomer, anim));
}
private IEnumerator GoBackThenSendNext()
{
CustomerGoingBack();
yield return new WaitUntil(() => currentCustomer == null);
yield return new WaitForSeconds(Random.Range(5f, 10f));
if (dailyQueue.Count == 0)
{
CustomerGoingBack();
UIManager.Instance.ShowInfoPanel.GetComponent<ShowInfoPanelScript>().OpenControl = true;
DayManager.Instance.SetNight();
UIManager.Instance.ShowInfo("No More Customers! End the Day.", UIManager.InfoType.Info);
yield break;
}
SendNextCustomer();
}
private void SendNextCustomer()
{
if (UIManager.Instance.MenuPanel.activeSelf || UIManager.Instance.ShowInfoPanel.activeSelf)
{
float retryDelay = TutorialManager.Instance.InTutorial ? 0.5f : Random.Range(3f, 6f);
Invoke(nameof(SendNextCustomer), retryDelay);
return;
}
foreach (var customer in customerPool.GetAll())
{
if (customer.gameObject.activeSelf && customer != currentCustomer)
customer.Reset();
}
float worth = CurrencyManager.Instance.GetPlayerWorth();
int day = DayManager.Instance.Day;
var occupiedSlots = SlotManager.Instance.GetOccupiedCarSlots()
.Where(s => s.CurrentCar != null).ToList();
Customer next = dailyQueue.Dequeue();
next.SetUse(true);
float carBasedChance = day <= 3 ? 0.8f : 0.4f;
if (occupiedSlots.Count > 0 && Random.value < carBasedChance)
{
var slot = occupiedSlots[Random.Range(0, occupiedSlots.Count)];
Car car = slot.CurrentCar;
float speed = Mathf.Max(0f, car.CarStats.Speed + Random.Range(-8f, 8f));
float comfort = Mathf.Max(0f, car.CarStats.Comfort + Random.Range(-8f, 8f));
float safety = Mathf.Max(0f, car.CarStats.Safety + Random.Range(-8f, 8f));
int showCount = Random.Range(1, 3);
int hideFirst = Random.Range(0, 3);
HideStat(hideFirst, ref speed, ref comfort, ref safety);
if (showCount == 1)
{
int hideSecond = Random.Range(0, 3);
while (hideSecond == hideFirst) hideSecond = Random.Range(0, 3);
HideStat(hideSecond, ref speed, ref comfort, ref safety);
}
next.Initialize(car.Segment, speed, comfort, safety, car.AveragePrice, customerPool.CustomerVisuals);
}
else
{
int wantCount = GetWantCount(day);
var (segment, speedValue, comfortValue, safetyValue, estimatedPrice) = GetCustomerWant(wantCount, worth, day);
next.Initialize(segment, speedValue, comfortValue, safetyValue, estimatedPrice, customerPool.CustomerVisuals);
}
SpawnAndSendToSalesSlot(next);
}
private void HideStat(int index, ref float speed, ref float comfort, ref float safety)
{
switch (index)
{
case 0: speed = 0f; break;
case 1: comfort = 0f; break;
default: safety = 0f; break;
}
}
private void SpawnAndSendToSalesSlot(Customer customer)
{
GameObject wantObj = Instantiate(wantPrefab, customer.transform.position, customer.transform.rotation, customer.transform);
wantObj.transform.localPosition = new Vector3(-2f, 3f, 0f);
wantText = wantObj.GetComponentInChildren<TMP_Text>();
if (wantText != null)
wantText.text = customer.GetCustomerWantsText();
currentWantObj = wantObj;
currentCustomer = customer;
UIManager.Instance.SetCustomerWantsHUD(currentCustomer);
customer.transform.position = spawnPoint.position;
customer.gameObject.SetActive(true);
Animator anim = customer.gameObject.GetComponentInChildren<Animator>();
StartCoroutine(MoveToSalesPoint(customer, anim));
if (TutorialManager.Instance.InTutorial)
TutorialManager.Instance.SetAndShowTutorial("Visit the customer and check their request!");
}
private IEnumerator MoveToSalesPoint(Customer customer, Animator anim)
{
float speed = 3f;
while (customer != null && customer.gameObject.activeSelf &&
Vector3.Distance(customer.transform.position, salesPoint.position) > 0.1f)
{
Vector3 direction = (spawnPoint.position - customer.transform.position).normalized;
if (direction != Vector3.zero)
customer.transform.rotation = Quaternion.LookRotation(direction);
if (anim != null) anim.SetFloat("Speed", 1);
customer.transform.position = Vector3.MoveTowards(
customer.transform.position, salesPoint.position, speed * Time.deltaTime);
yield return null;
}
if (anim != null) anim.SetFloat("Speed", 0);
if (customer != null) customer.transform.position = salesPoint.position;
if (!TutorialManager.Instance.InTutorial)
{
if (timeoutCoroutine != null) StopCoroutine(timeoutCoroutine);
timeoutCoroutine = StartCoroutine(CustomerTimeout(customer));
}
}
private IEnumerator CustomerTimeout(Customer customer)
{
float elapsed = 0f;
float timeout = 90f;
while (elapsed < timeout)
{
if (!UIManager.Instance.MenuPanel.activeSelf && !UIManager.Instance.ShowInfoPanel.activeSelf)
elapsed += Time.unscaledDeltaTime;
yield return null;
}
timeoutCoroutine = null;
if (currentCustomer == customer)
{
if (UIManager.Instance.CustomerWaitPanel.activeSelf)
UIManager.Instance.JustHidePanel(UIManager.Instance.CustomerWaitPanel);
UIManager.Instance.ShowInfoPanel.GetComponent<ShowInfoPanelScript>().OpenControl = true;
UIManager.Instance.ShowInfo("I have to go, sorry!", UIManager.InfoType.Warning);
OnCustomerDone();
}
}
private IEnumerator MoveBack(Customer customer, Animator anim)
{
if (currentWantObj != null)
{
wantText.text = "";
Destroy(currentWantObj);
currentWantObj = null;
}
float speed = 3f;
while (customer != null && Vector3.Distance(customer.transform.position, spawnPoint.position) > 0.1f)
{
Vector3 direction = (salesPoint.position - customer.transform.position).normalized;
if (direction != Vector3.zero)
customer.transform.rotation = Quaternion.LookRotation(direction);
if (anim != null) anim.SetFloat("Speed", 1);
customer.transform.position = Vector3.MoveTowards(
customer.transform.position, spawnPoint.position, speed * Time.deltaTime);
yield return null;
}
if (anim != null) anim.SetFloat("Speed", 0);
if (customer != null)
{
customer.transform.position = spawnPoint.position;
customer.Reset();
if (currentCustomer == customer)
currentCustomer = null;
}
}
private int GetWantCount(int currentDay)
{
List<(int count, float weight)> table = wantCountTable[0];
foreach (var key in wantCountTable.Keys)
if (currentDay >= key) table = wantCountTable[key];
float total = table.Sum(e => e.weight);
float roll = Random.Range(0f, total);
float current = 0f;
foreach (var entry in table)
{
current += entry.weight;
if (roll <= current) return entry.count;
}
return 1;
}
private (Segment wantedSegment, float speedValue, float comfortValue, float safetyValue, float estimatedPrice) GetCustomerWant(int wantCount, float worth, int day)
{
Segment segment = GetSegmentFromWorthAndDay(worth, day);
List<int> options = new List<int> { 1, 2, 3 };
for (int i = 0; i < options.Count; i++)
{
int j = Random.Range(i, options.Count);
(options[i], options[j]) = (options[j], options[i]);
}
float speedValue = 0f, comfortValue = 0f, safetyValue = 0f;
int statCount = Mathf.Min(wantCount, 3);
var (_, estimatedPrice, chosenRarity) = GetWantedStatAndPrice(segment);
for (int i = 0; i < statCount; i++)
{
var (val, _, _) = GetWantedStatAndPrice(segment, chosenRarity);
switch (options[i])
{
case 1: speedValue = val; break;
case 2: comfortValue = val; break;
case 3: safetyValue = val; break;
}
}
return (segment, speedValue, comfortValue, safetyValue, estimatedPrice);
}
private Segment GetSegmentFromWorthAndDay(float worth, int day)
{
if (DayManager.Instance.Day == 1)
return Segment.Street;
var containerTypes = new[]
{
(ContainerType.Street, Segment.Street),
(ContainerType.City, Segment.City),
(ContainerType.Offroad, Segment.Offroad),
(ContainerType.Muscle, Segment.Muscle),
(ContainerType.Sport, Segment.Sport),
(ContainerType.Turbo, Segment.Turbo),
(ContainerType.Exotic, Segment.Exotic),
(ContainerType.Hypercar, Segment.Hypercar),
(ContainerType.HypercarPlus, Segment.HypercarPlus),
(ContainerType.Elite, Segment.Elite),
(ContainerType.GodTier, Segment.GodTier),
};
var weights = new List<(Segment seg, float weight)>();
const float idealSpendRatio = 0.3f;
foreach (var ct in containerTypes)
{
float price = ContainerManager.Instance.GetContainerPrice(ct.Item1);
float spendRatio = price / worth;
if (spendRatio > 3f) continue;
if (spendRatio < 0.04f) continue;
float diff = Mathf.Abs(spendRatio - idealSpendRatio);
float weight = 1f / (diff + 0.05f);
weights.Add((ct.Item2, weight));
}
if (weights.Count == 0) weights.Add((Segment.Street, 1f));
float total = weights.Sum(x => x.weight);
float roll = Random.Range(0f, total);
float current = 0f;
foreach (var entry in weights)
{
current += entry.weight;
if (roll <= current) return entry.seg;
}
return weights.Last().seg;
}
private (float statValue, float estimatedPrice, Rarity rarity) GetWantedStatAndPrice(Segment segment, Rarity? inputRarity = null)
{
Rarity chosenRarity;
if (inputRarity.HasValue)
{
chosenRarity = inputRarity.Value;
}
else
{
var table = ContainerManager.Instance.GetLootTableForSegment(segment);
float total = table.Sum(e => e.weight);
float roll = Random.Range(0f, total);
float current = 0f;
chosenRarity = Rarity.Common;
foreach (var entry in table)
{
current += entry.weight;
if (roll <= current) { chosenRarity = entry.rarity; break; }
}
}
var statRange = CarStatsManager.Instance.statRanges[(segment, chosenRarity)];
float statValue = Random.Range(statRange.min, statRange.max);
float basePower = Mathf.Pow(statValue * 3f, 1.5f);
float rarityMult = CarStatsManager.Instance.rarityMultipliers[chosenRarity];
float segMult = CarStatsManager.Instance.segmentMultipliers[segment];
float price = basePower * rarityMult * segMult;
return (statValue, price, chosenRarity);
}
}