← back to home

Car Flip

V1.1.2

A mobile trading game where you flip cars for profit!

Engine Unity
Language C#
Platform Mobile
Genre Hybrid Casual
Store Play Store
Platform WebGL
Other Playable
Released March 2026
PLAYABLE DEMO / MEDIA

Loads about 140 MB — desktop recommended

ABOUT THIS PROJECT

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.

SCREENSHOTS
VERSION HISTORY
1.1.2 2.09.2026

Bug Fix

1.1.1 1.09.2026

WebGL Build

CODE SNIPPETS
CustomerManager.cs
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);
    }
}