20 June, 2026

Improved version of Terrain Collapse

V tomto článku bude uvedená vylepšená verzia Terrain Collapse ako aj celkový generátor terénu. Táto verzia pochádza od AI, ale čiastočne, teda bola vylepšená pôvodná verzia. TerrainElement.cs:

using System;
using Unity.VisualScripting;
using UnityEngine;


[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class TerrainElement : MonoBehaviour
{
    private Mesh mesh;
    private MeshFilter meshFilter;
    private Vector3[] verts;
    private Color[] colors;
    private int[] tris;
    private Vector2[] uvs;

    private int indexX;
    private int indexZ;



    private bool TriangulationCheck(Vector3 coord0, Vector3 coord1)
    {
        if (coord0.y == coord1.y)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    private Color VertexColor(float height)
    {
        if (height < 1.5) //1.5
        {
            return Color.black;
        }
        else if (height < 3.5) //3.5
        {
            return Color.red;
        }
        else
        {
            return Color.green;
        }
    }



    public void Initialize(int index_x, int index_z)
    {
        indexX = index_x;
        indexZ = index_z;

        this.name = index_x + "_" + index_z;

        meshFilter = GetComponent();
        mesh = new Mesh();
        meshFilter.mesh = mesh;

        BuildMesh();
    }


    public void BuildMesh()
    {
        //getting width of element and terrain
        int width = TerrainManager.instance.elementWidth;
        int tWidth = TerrainManager.instance.terrainWidth;

        //setting the water position
        this.transform.GetChild(0).transform.position = new Vector3(indexX * width + width / 2, 2.75f, indexZ * width + width / 2);

        verts = new Vector3[width * width * 6];
        colors = new Color[verts.Length];
        uvs = new Vector2[verts.Length];
        tris = new int[verts.Length];

        //pivot point inside of the coord array
        int origin = indexX * width + indexZ * width * (tWidth + 1);

        for (int i = 0, z = 0; z < width; z++)
        {
            //outer loop for z-axis
            for (int x = 0; x < width; x++, i += 6)
            {
                //setting verts                    
                verts[i] = TerrainManager.instance.coordsF[origin + (x * (tWidth + 1) + z)];
                verts[i + 1] = TerrainManager.instance.coordsF[origin + ((x + 1) * (tWidth + 1) + z)];
                verts[i + 2] = TerrainManager.instance.coordsF[origin + ((x + 1) * (tWidth + 1) + z + 1)];
                verts[i + 3] = TerrainManager.instance.coordsF[origin + (x * (tWidth + 1) + z + 1)];

                colors[i] = VertexColor(TerrainManager.instance.coordsF[origin + (x * (tWidth + 1) + z)].y);
                colors[i + 1] = VertexColor(TerrainManager.instance.coordsF[origin + ((x + 1) * (tWidth + 1) + z)].y);
                colors[i + 2] = VertexColor(TerrainManager.instance.coordsF[origin + ((x + 1) * (tWidth + 1) + z + 1)].y);
                colors[i + 3] = VertexColor(TerrainManager.instance.coordsF[origin + (x * (tWidth + 1) + z + 1)].y);



                if (TriangulationCheck(TerrainManager.instance.coordsF[origin + (x * (tWidth + 1) + z)], TerrainManager.instance.coordsF[origin + ((x + 1) * (tWidth + 1) + z + 1)]))
                {
                    //setting extra vertices
                    verts[i + 4] = TerrainManager.instance.coordsF[origin + (x * (tWidth + 1) + z)];
                    verts[i + 5] = TerrainManager.instance.coordsF[origin + ((x + 1) * (tWidth + 1) + z + 1)];

                    //setting vertex colors
                    colors[i + 4] = VertexColor(TerrainManager.instance.coordsF[origin + (x * (tWidth + 1) + z)].y);
                    colors[i + 5] = VertexColor(TerrainManager.instance.coordsF[origin + ((x + 1) * (tWidth + 1) + z + 1)].y);

                    //setting tris
                    tris[i] = i;
                    tris[i + 1] = i + 1;
                    tris[i + 2] = i + 2;
                    tris[i + 3] = i + 4;
                    tris[i + 4] = i + 5;
                    tris[i + 5] = i + 3;
                    //setting uvs
                    uvs[i] = new Vector2(0, 0);
                    uvs[i + 1] = new Vector2(0, 1);
                    uvs[i + 2] = new Vector2(1, 1);
                    uvs[i + 3] = new Vector2(1, 0);
                    uvs[i + 4] = new Vector2(0, 0);
                    uvs[i + 5] = new Vector2(1, 1);
                }

                else
                {
                    //setting extra vertices
                    verts[i + 4] = TerrainManager.instance.coordsF[origin + (x * (tWidth + 1) + z + 1)];
                    verts[i + 5] = TerrainManager.instance.coordsF[origin + ((x + 1) * (tWidth + 1) + z)];

                    //setting vertex colors
                    colors[i + 4] = VertexColor(TerrainManager.instance.coordsF[origin + (x * (tWidth + 1) + z + 1)].y);
                    colors[i + 5] = VertexColor(TerrainManager.instance.coordsF[origin + ((x + 1) * (tWidth + 1) + z)].y);

                    //setting tris
                    tris[i] = i;
                    tris[i + 1] = i + 1;
                    tris[i + 2] = i + 3;
                    tris[i + 3] = i + 4;
                    tris[i + 4] = i + 5;
                    tris[i + 5] = i + 2;
                    //setting uvs
                    uvs[i] = new Vector2(0, 0);
                    uvs[i + 1] = new Vector2(0, 1);
                    uvs[i + 2] = new Vector2(1, 1);
                    uvs[i + 3] = new Vector2(1, 0);
                    uvs[i + 4] = new Vector2(1, 0);
                    uvs[i + 5] = new Vector2(0, 1);
                }


            }
        }

        mesh.vertices = verts;
        mesh.colors = colors;
        mesh.uv = uvs;
        mesh.triangles = tris;
        mesh.RecalculateNormals();

        this.AddComponent();
    }


    public void Rebuild()
    {
        mesh.Clear();
        BuildMesh();
    }



}

TerrainManager.cs:

using NUnit.Framework.Constraints;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;


public class TerrainManager : MonoBehaviour
{
    public static TerrainManager instance;
    public int terrainWidth, elementWidth;
    public TerrainElement terrainPrefab;
    [HideInInspector]
    public Vector3[] coordsF;
    public Vector3[] coordsI;
    [HideInInspector]
    public TerrainElement[] terrainElements;


    private const float baseHeight = 2.75f;
    private const float step = 0.25f;

    // =====================================================================
    // GLOBÁLNE VÝŠKOVÉ LIMITY TERÉNU
    //
    // Užívateľská "úroveň prevýšenia" E:
    //   E = -1 → Y = 2.75f (= baseHeight, najnižšia, "fiktívna" úroveň pod rovinou)
    //   E =  0 → Y = 3.00f (rovina terénu)
    //   E = 10 → Y = 5.50f (najvyššia povolená úroveň)
    // Prepočet výšky: Y = baseHeight + (E + 1) * step.
    //
    // Celá mapa (všetky vertexy) musí ostať v rozsahu [MinTerrainHeight,
    // MaxTerrainHeight]. Limity sú odvodené z úrovní, takže stačí zmeniť
    // Min/MaxElevationLevel a výšky sa prepočítajú.
    // =====================================================================

    public const int MinElevationLevel = -1; // Y = 2.75f
    public const int MaxElevationLevel = 10; // Y = 5.50f

    public const float MinTerrainHeight = baseHeight + (MinElevationLevel + 1) * step; // 2.75f
    public const float MaxTerrainHeight = baseHeight + (MaxElevationLevel + 1) * step; // 5.50f


    // =====================================================================
    // PARAMETRE GENEROVANIA TERÉNU (laditeľné v Inspectore)
    //
    // Hodnoty seaFloorLevel / maxLandLevel sú v užívateľskej úrovni E
    // (rovnaká škála ako vyššie: E=-1 = hladina vody, E=0 = rovina).
    // Vnútorne sa prepočítavajú na "coordsI" úroveň = E + 1.
    // =====================================================================

    [Header("Generovanie terénu")]
    [Tooltip("0 = pri každom spustení iný náhodný terén; iná hodnota = opakovateľný (rovnaký) terén.")]
    public int terrainSeed = 0;

    [Tooltip("Mierka šumu. Menšie číslo = väčšie a plynulejšie útvary (viac rovín); väčšie = členitejší terén.")]
    public float noiseScale = 0.03f;

    [Tooltip("Počet vrstiev šumu (detail). Viac = drobnejšie nerovnosti navrch.")]
    [Range(1, 8)]
    public int octaves = 4;

    [Tooltip("Útlm amplitúdy medzi vrstvami šumu (0..1). Nižšie = hladší terén.")]
    [Range(0f, 1f)]
    public float persistence = 0.5f;

    [Tooltip("Nárast frekvencie medzi vrstvami šumu (typicky 2).")]
    public float lacunarity = 2f;

    [Tooltip("Podiel mapy (0..1), ktorý spadne na vodu/pobrežie. Vyššie = viac vody.")]
    [Range(0f, 0.9f)]
    public float seaThreshold = 0.30f;

    [Tooltip("Najnižšia úroveň E dna pod vodou. Pod -1 = terén klesne pod hladinu a voda je viditeľná.")]
    public int seaFloorLevel = -2;

    [Tooltip("Najvyššia úroveň E pevniny (kopce). Max 10.")]
    public int maxLandLevel = 8;

    [Tooltip("Sploštenie nížin (mocnina). >1 = viac rovín a menej kopcov, =1 = lineárne.")]
    public float flatBias = 1.5f;


    /// 
    /// Vráti true, ak by ďalší krok úpravy terénu na vrchole s aktuálnou výškou
    /// snapY prekročil povolený rozsah:
    ///   • levelUp == true  → nová výška (snapY + step) by presiahla MaxTerrainHeight,
    ///   • levelUp == false → nová výška (snapY - step) by klesla pod MinTerrainHeight.
    ///
    /// Stačí kontrolovať klikaný vrchol: pri LevelUp je práve on najvyšším a pri
    /// LevelDown najnižším bodom zmeny – kaskáda (TerrainCollapse) susedov len
    /// doťahuje smerom k nemu, takže limit neprekročí nič, čo neprekročí on.
    /// 
    public bool WouldExceedElevationLimit(float snapY, bool levelUp)
    {
        const float EPS = 0.0001f;

        if (levelUp)
            return snapY + step > MaxTerrainHeight + EPS;
        else
            return snapY - step < MinTerrainHeight - EPS;
    }


    void Start()
    {
        instance = this;

        coordsF = CreateMap(3.00f);
        coordsI = CreateMap(0);

        // Náhodný terén ešte PRED postavením mesh-ov, aby sa už prvé
        // vykreslenie elementov postavilo z vygenerovaných výšok.
        GenerateRandomTerrain();

        CreateTerrainElements();
    }


    // =====================================================================
    // GENERÁTOR NÁHODNÉHO TERÉNU
    //
    // Postup (inšpirované OpenTTD/TTD – plynulé útvary, veľa rovín):
    //   1) fraktálny Perlin šum (fBm) → plynulé výškové pole 0..1
    //   2) mapovanie šumu na úroveň E s "hladinou" a sploštením nížin
    //   3) EnforceMaxSlope – dotiahnutie na pravidlo "susedia max o 1 úroveň"
    //      (rovnaké pravidlo ako TerrainCollapse v editore) → vznikajú
    //      prirodzené svahy a rovinné plató
    //   4) prepočet coordsI → coordsF (reálne Y výšky pre mesh)
    //
    // Vodná vrstva (child[0] v TerrainElement) sa NEMENÍ – ostáva na Y=2.75.
    // =====================================================================
    public void GenerateRandomTerrain()
    {
        if (coordsI == null || coordsF == null)
            return;

        int size = terrainWidth + 1;

        // Seed: 0 = nový náhodný pri každom spustení.
        if (terrainSeed == 0)
            UnityEngine.Random.InitState(Environment.TickCount);
        else
            UnityEngine.Random.InitState(terrainSeed);

        // Náhodný posun do šumového poľa → iná mapa pri každom behu.
        float offsetX = UnityEngine.Random.Range(-100000f, 100000f);
        float offsetZ = UnityEngine.Random.Range(-100000f, 100000f);

        // Bezpečné orezanie limitov (v úrovni E).
        int maxLandE = Mathf.Clamp(maxLandLevel, MinElevationLevel, MaxElevationLevel);
        int seaFloorE = Mathf.Clamp(seaFloorLevel, -5, maxLandE);

        for (int z = 0; z <= terrainWidth; z++)
        {
            for (int x = 0; x <= terrainWidth; x++)
            {
                int idx = z * size + x;

                // 1) fBm Perlin – súčet niekoľkých vrstiev šumu.
                float amp = 1f, freq = 1f, sum = 0f, ampSum = 0f;
                for (int o = 0; o < octaves; o++)
                {
                    float sx = offsetX + x * noiseScale * freq;
                    float sz = offsetZ + z * noiseScale * freq;
                    sum += amp * Mathf.PerlinNoise(sx, sz);
                    ampSum += amp;
                    amp *= persistence;
                    freq *= lacunarity;
                }
                float n = (ampSum > 0f) ? Mathf.Clamp01(sum / ampSum) : 0f;

                // 2) Mapovanie šumu → úroveň E.
                float e;
                if (n < seaThreshold)
                {
                    // Voda/pobrežie: od dna (seaFloorE) po hladinu (E=-1).
                    float t = (seaThreshold > 0f) ? n / seaThreshold : 0f;
                    e = Mathf.Lerp(seaFloorE, MinElevationLevel, t);
                }
                else
                {
                    // Pevnina: od roviny (E=0) nahor, so sploštením nížin.
                    float t = (n - seaThreshold) / (1f - seaThreshold);
                    t = Mathf.Pow(t, Mathf.Max(0.01f, flatBias));
                    e = Mathf.Lerp(0f, maxLandE, t);
                }

                int eRounded = Mathf.Clamp(Mathf.RoundToInt(e), seaFloorE, maxLandE);

                // coordsI úroveň = E + 1 (rovnaký vzťah ako ConvertCoordsFI).
                coordsI[idx].x = x;
                coordsI[idx].z = z;
                coordsI[idx].y = eRounded + 1;
            }
        }

        // 3) Dotiahnutie na max sklon 1 úrovne medzi susedmi (TT pravidlo).
        EnforceMaxSlope(coordsI);

        // 4) Prepočet na reálne Y výšky používané mesh-om.
        ConvertCoordsFI(false);
    }


    /// 
    /// Zabezpečí, že žiadne dva susedné vrcholy sa nelíšia o viac než 1 úroveň
    /// (rovnaké pravidlo ako TerrainCollapse). Pracuje "iba znižovaním":
    /// každý vrchol môže byť nanajvýš o 1 nad svojím NAJNIŽŠÍM susedom, inak sa
    /// zníži. Tým sa zachovajú nížiny/voda a zo strmých vrcholov vzniknú plynulé
    /// svahy a rovinné plató (presne TT/OpenTTD vzhľad). Postup je monotónne
    /// klesajúci a celočíselný, takže vždy skonverguje.
    /// 
    private void EnforceMaxSlope(Vector3[] ci)
    {
        int size = terrainWidth + 1;

        bool changed = true;
        while (changed)
        {
            changed = false;

            for (int z = 0; z <= terrainWidth; z++)
            {
                for (int x = 0; x <= terrainWidth; x++)
                {
                    int idx = z * size + x;
                    float h = ci[idx].y;

                    float minN = float.MaxValue;
                    if (x > 0) minN = Mathf.Min(minN, ci[idx - 1].y);
                    if (x < terrainWidth) minN = Mathf.Min(minN, ci[idx + 1].y);
                    if (z > 0) minN = Mathf.Min(minN, ci[idx - size].y);
                    if (z < terrainWidth) minN = Mathf.Min(minN, ci[idx + size].y);

                    if (h > minN + 1f)
                    {
                        ci[idx].y = minN + 1f;
                        changed = true;
                    }
                }
            }
        }
    }



    public void TerrainCollapse(int startIndex)
    {
        // Reálny beh nad živým coordsI (pôvodné správanie zachované).
        TerrainCollapse(startIndex, coordsI);
    }

    /// 
    /// Jadro collapse algoritmu parametrizované poľom integer-výšok (ci).
    /// Volá sa buď nad reálnym coordsI, alebo nad pracovnou KÓPIOU pri
    /// simulácii (PredictVertexLevelChanges) – vďaka tomu sa logika
    /// nedupluje a simulácia presne zodpovedá reálnemu výsledku.
    /// 
    private void TerrainCollapse(int startIndex, Vector3[] ci)
    {
        int size = terrainWidth + 1;

        Queue queue = new Queue();
        HashSet visited = new HashSet();

        queue.Enqueue(startIndex);
        visited.Add(startIndex);

        while (queue.Count > 0)
        {
            int currentIndex = queue.Dequeue();

            Vector3 current = ci[currentIndex];
            int cx = (int)current.x;
            int cz = (int)current.z;
            float currentHeight = current.y;

            Vector2Int[] directions =
            {
            new Vector2Int( 1, 0),
            new Vector2Int(-1, 0),
            new Vector2Int( 0, 1),
            new Vector2Int( 0,-1)
        };

            foreach (var dir in directions)
            {
                int nx = cx + dir.x;
                int nz = cz + dir.y;

                if (nx < 0 || nz < 0 || nx > terrainWidth || nz > terrainWidth)
                    continue;

                int neighborIndex = nz * size + nx;

                float neighborHeight = ci[neighborIndex].y;
                float delta = Mathf.Abs(currentHeight - neighborHeight);

                if (delta > 1)
                {
                    if (neighborHeight < currentHeight)
                        ci[neighborIndex].y = currentHeight - 1;
                    else
                        ci[neighborIndex].y = currentHeight + 1;

                    if (!visited.Contains(neighborIndex))
                    {
                        queue.Enqueue(neighborIndex);
                        visited.Add(neighborIndex);
                    }
                }
            }
        }
    }




    public void TerrainVertexLevel(float snapX, float snapY, float snapZ, bool levelUp)
    {
        int coordClick = 0;

        int size = terrainWidth + 1;

        int centerX = Mathf.RoundToInt(snapX);
        int centerZ = Mathf.RoundToInt(snapZ);

        int minX = Mathf.Max(0, centerX - 10);
        int maxX = Mathf.Min(terrainWidth, centerX + 10);

        int minZ = Mathf.Max(0, centerZ - 10);
        int maxZ = Mathf.Min(terrainWidth, centerZ + 10);

        for (int z = minZ; z <= maxZ; z++)
        {
            for (int x = minX; x <= maxX; x++)
            {
                int i = z * size + x;

                if (coordsF[i].x == snapX && coordsF[i].y == snapY && coordsF[i].z == snapZ)
                {
                    coordsF[i].y += levelUp ? 0.25f : -0.25f;
                    coordClick = i;
                }
            }
        }

        ConvertCoordsFI(true);
        TerrainCollapse(coordClick);
        ConvertCoordsFI(false);

        UpdateRegion(coordClick);
    }


    /// 
    /// SIMULÁCIA (dry-run) operácie TerrainVertexLevel BEZ akejkoľvek zmeny
    /// reálneho terénu. Vráti grid-súradnice [vx,vz] VŠETKÝCH vrcholov, ktorých
    /// výška Y by sa po klikoch (vrátane kaskádového TerrainCollapse) zmenila.
    ///
    /// PREČO: úprava jedného vrcholu môže cez TerrainCollapse posunúť aj mnoho
    /// okolitých vrcholov (aby susedné výškové úrovne nelíšili o viac než 1).
    /// Volajúci (GameManager) tak vie ešte PRED reálnou úpravou skontrolovať,
    /// či by sa niektorý z dotknutých vrcholov dotkol obsadeného tile, a ak áno,
    /// operáciu vôbec nespustiť.
    ///
    /// Celý výpočet beží nad KÓPIAMI coordsF/coordsI – živé dáta ostávajú
    /// nedotknuté a mesh sa neprestavuje.
    /// 
    public List PredictVertexLevelChanges(float snapX, float snapY, float snapZ, bool levelUp)
    {
        var changed = new List();

        if (coordsF == null || coordsI == null)
            return changed;

        int size = terrainWidth + 1;

        // Pracovné kópie – reálny terén nemodifikujeme.
        Vector3[] simF = (Vector3[])coordsF.Clone();
        Vector3[] simI = (Vector3[])coordsI.Clone();

        // 1) Rovnaké vyhľadanie klikaného vrcholu ako v TerrainVertexLevel.
        int coordClick = 0;

        int centerX = Mathf.RoundToInt(snapX);
        int centerZ = Mathf.RoundToInt(snapZ);

        int minX = Mathf.Max(0, centerX - 10);
        int maxX = Mathf.Min(terrainWidth, centerX + 10);

        int minZ = Mathf.Max(0, centerZ - 10);
        int maxZ = Mathf.Min(terrainWidth, centerZ + 10);

        for (int z = minZ; z <= maxZ; z++)
        {
            for (int x = minX; x <= maxX; x++)
            {
                int i = z * size + x;

                if (simF[i].x == snapX && simF[i].y == snapY && simF[i].z == snapZ)
                {
                    simF[i].y += levelUp ? 0.25f : -0.25f;
                    coordClick = i;
                }
            }
        }

        // 2) F→I, kaskádový collapse, I→F – všetko nad kópiami.
        ConvertCoordsFI(true, simF, simI);
        TerrainCollapse(coordClick, simI);
        ConvertCoordsFI(false, simF, simI);

        // 3) Pozbierame vrcholy, ktorých Y sa oproti živému stavu zmenilo.
        for (int i = 0; i < coordsF.Length; i++)
        {
            if (!Mathf.Approximately(simF[i].y, coordsF[i].y))
                changed.Add(new Vector2Int(i % size, i / size));
        }

        return changed;
    }





    private Vector3[] CreateMap(float initialHeight)
    {
        int size = (terrainWidth + 1) * (terrainWidth + 1);
        Vector3[] map = new Vector3[size];

        int i = 0;
        for (int z = 0; z <= terrainWidth; z++)
        {
            for (int x = 0; x <= terrainWidth; x++)
            {
                map[i++] = new Vector3(x, initialHeight, z);
            }
        }

        return map;
    }




    public void ConvertCoordsFI(bool floatToInt)
    {
        // Reálna konverzia nad živými poliami (pôvodné správanie).
        ConvertCoordsFI(floatToInt, coordsF, coordsI);
    }

    /// 
    /// Konverzia float↔int výšok parametrizovaná poľami – aby ju vedela
    /// použiť aj simulácia (PredictVertexLevelChanges) nad kópiami.
    /// 
    private void ConvertCoordsFI(bool floatToInt, Vector3[] f, Vector3[] iArr)
    {
        for (int i = 0; i < f.Length; i++)
        {
            if (floatToInt)
            {
                iArr[i].x = f[i].x;
                iArr[i].z = f[i].z;

                iArr[i].y = Mathf.RoundToInt((f[i].y - baseHeight) / step);
            }
            else
            {
                f[i].x = iArr[i].x;
                f[i].z = iArr[i].z;

                f[i].y = baseHeight + iArr[i].y * step;
            }
        }
    }




    private void CreateTerrainElements()
    {
        int tilesPerSide = terrainWidth / elementWidth;
        terrainElements = new TerrainElement[tilesPerSide * tilesPerSide];

        for (int i = 0, z = 0; z < tilesPerSide; z++)
        {
            for (int x = 0; x < tilesPerSide; x++, i++)
            {
                TerrainElement elementInstance = Instantiate(terrainPrefab, this.transform);
                elementInstance.Initialize(x, z);
                terrainElements[i] = elementInstance;
            }
        }
    }



    private void UpdateRegion(int centerIndex)
    {
        Vector3 center = coordsI[centerIndex];

        int minX = Mathf.Max(0, (int)center.x - 10);
        int maxX = Mathf.Min(terrainWidth, (int)center.x + 10);

        int minZ = Mathf.Max(0, (int)center.z - 10);
        int maxZ = Mathf.Min(terrainWidth, (int)center.z + 10);

        int elementSize = elementWidth;
        int tilesPerSide = terrainWidth / elementSize;

        HashSet elementsToUpdate = new HashSet();

        for (int z = minZ; z <= maxZ; z++)
        {
            for (int x = minX; x <= maxX; x++)
            {
                int elementX = x / elementSize;
                int elementZ = z / elementSize;

                int elementIndex = elementZ * tilesPerSide + elementX;

                elementsToUpdate.Add(elementIndex);
            }
        }

        foreach (int i in elementsToUpdate)
        {
            terrainElements[i].Rebuild();
        }
    }

}

Share:

0 comments:

Post a Comment