🎮ArcadeLab

Раст

by TurboMeteor43
1285 lines45.7 KB
▶ Play
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using UnityEngine.Rendering.Universal;

// ============================================================
// 1. ГЛАВНЫЙ КОНТРОЛЛЕР
// ============================================================
public class GameController : MonoBehaviour
{
    public static GameController Instance;
    
    [Header("Мир")]
    public int worldWidth = 120;
    public int worldHeight = 80;
    public int seed = 42;
    public Tilemap groundTilemap;
    public Tilemap wallTilemap;
    public Tilemap decorationTilemap;
    public Tilemap waterTilemap;
    public TileBase[] tiles;
    
    [Header("Игрок")]
    public GameObject playerPrefab;
    public Transform playerSpawn;
    
    [Header("UI")]
    public InventoryUI inventoryUI;
    public QuestUI questUI;
    public CraftingUI craftingUI;
    public GuideUI guideUI;
    public MobileControls mobileControls;
    public Text debugText;
    public GameObject pauseMenu;
    
    [Header("Настройки")]
    public int renderDistance = 15;
    public float dayLength = 600f;
    public int maxInventorySize = 50;
    public float autoSaveInterval = 60f;
    
    private Player _player;
    private WorldGenerator _worldGenerator;
    private QuestManager _questManager;
    private DayNightCycle _dayNightCycle;
    private GuideSystem _guideSystem;
    private List<Chunk> _activeChunks = new List<Chunk>();
    private Dictionary<Vector2Int, Chunk> _chunkMap = new Dictionary<Vector2Int, Chunk>();
    private Vector2Int _currentChunk;
    private float _tickTimer;
    private float _autoSaveTimer;
    private bool _isPaused;

    void Awake()
    {
        Instance = this;
        Application.targetFrameRate = 60;
        Screen.sleepTimeout = SleepTimeout.NeverSleep;
    }
    
    void Start()
    {
        InitializeSystems();
        GenerateWorld();
        SpawnPlayer();
        StartCoroutine(UpdateChunks());
        _guideSystem.ShowMessage("Добро пожаловать! Я проводник. Начни с добычи древесины.");
    }

    void Update()
    {
        if (_isPaused) return;
        
        _tickTimer += Time.deltaTime;
        if (_tickTimer >= 0.5f) { _tickTimer = 0; Tick(); }
        
        _autoSaveTimer += Time.deltaTime;
        if (_autoSaveTimer >= autoSaveInterval) { _autoSaveTimer = 0; SaveWorld(); }
        
        if (Input.GetKeyDown(KeyCode.E) || Input.GetKeyDown(KeyCode.Tab)) inventoryUI.Toggle();
        if (Input.GetKeyDown(KeyCode.C)) craftingUI.Toggle();
        if (Input.GetKeyDown(KeyCode.Q)) questUI.Toggle();
        if (Input.GetKeyDown(KeyCode.G)) guideUI.Toggle();
        if (Input.GetKeyDown(KeyCode.F3)) debugText.enabled = !debugText.enabled;
        if (Input.GetKeyDown(KeyCode.Escape)) TogglePause();
        if (Input.GetKeyDown(KeyCode.R)) SaveWorld();
        if (Input.GetKeyDown(KeyCode.L)) LoadWorld();
    }
    
    void InitializeSystems()
    {
        _worldGenerator = new WorldGenerator(seed, worldWidth, worldHeight);
        _questManager = new QuestManager();
        _dayNightCycle = new DayNightCycle(dayLength);
        _guideSystem = new GuideSystem();
        
        inventoryUI.Initialize(maxInventorySize);
        craftingUI.Initialize();
        questUI.Initialize();
        guideUI.Initialize(_guideSystem);
        
        if (Application.isMobilePlatform && mobileControls != null)
            mobileControls.gameObject.SetActive(true);
    }
    
    void GenerateWorld()
    {
        for (int x = 0; x < worldWidth; x++)
            for (int y = 0; y < worldHeight; y++)
                SetBlock(x, y, _worldGenerator.GetBlockType(x, y));
        
        _worldGenerator.GenerateTrees(SetBlock);
        _worldGenerator.GenerateOres(SetBlock);
        _worldGenerator.GenerateDungeons(SetBlock);
        _worldGenerator.GenerateWater(SetBlock);
    }
    
    void SpawnPlayer()
    {
        Vector3 spawnPos = playerSpawn.position;
        for (int y = 0; y < 20; y++)
        {
            Vector3 testPos = spawnPos + Vector3.up * y;
            if (!IsSolid(Mathf.RoundToInt(testPos.x), Mathf.RoundToInt(testPos.y)))
            { spawnPos = testPos; break; }
        }
        
        GameObject obj = Instantiate(playerPrefab, spawnPos, Quaternion.identity);
        _player = obj.GetComponent<Player>();
        _player.Initialize(inventoryUI, _guideSystem);
        Camera.main.GetComponent<CameraFollow>().SetTarget(obj.transform);
    }
    
    void Tick()
    {
        _dayNightCycle.Tick();
        _questManager.UpdateQuests(_player?.GetInventory());
        foreach (var chunk in _activeChunks) chunk.Tick();
    }
    
    void TogglePause()
    {
        _isPaused = !_isPaused;
        pauseMenu.SetActive(_isPaused);
        Time.timeScale = _isPaused ? 0 : 1;
    }
    
    IEnumerator UpdateChunks()
    {
        while (true)
        {
            if (!_isPaused && _player != null)
            {
                Vector2Int pc = GetChunkPosition(_player.transform.position);
                if (pc != _currentChunk) { _currentChunk = pc; UpdateActiveChunks(); }
            }
            yield return new WaitForSeconds(0.5f);
        }
    }
    
    void UpdateActiveChunks()
    {
        List<Chunk> toRemove = new List<Chunk>();
        foreach (var kvp in _chunkMap)
            if (Vector2Int.Distance(kvp.Key, _currentChunk) > renderDistance)
                toRemove.Add(kvp.Value);
        
        foreach (var chunk in toRemove)
        { chunk.Unload(); _chunkMap.Remove(chunk.Position); _activeChunks.Remove(chunk); }
        
        for (int x = -renderDistance; x <= renderDistance; x++)
            for (int y = -renderDistance; y <= renderDistance; y++)
            {
                Vector2Int pos = _currentChunk + new Vector2Int(x, y);
                if (!_chunkMap.ContainsKey(pos))
                {
                    Chunk chunk = new Chunk(pos);
                    chunk.Load(this);
                    _chunkMap[pos] = chunk;
                    _activeChunks.Add(chunk);
                }
            }
    }
    
    Vector2Int GetChunkPosition(Vector3 worldPos) =>
        new Vector2Int(Mathf.FloorToInt(worldPos.x / 16), Mathf.FloorToInt(worldPos.y / 16));
    
    public void SetBlock(int x, int y, BlockType type)
    {
        if (x < 0 || x >= worldWidth || y < 0 || y >= worldHeight) return;
        Vector3Int pos = new Vector3Int(x, y, 0);
        Tilemap target = GetTilemap(type);
        target.SetTile(pos, GetTileBase(type));
    }
    
    Tilemap GetTilemap(BlockType type)
    {
        switch (type)
        {
            case BlockType.Grass: case BlockType.Dirt: case BlockType.Stone: case BlockType.Sand:
                return groundTilemap;
            case BlockType.Wood: case BlockType.Leaf: case BlockType.Flower:
                return decorationTilemap;
            case BlockType.Water:
                return waterTilemap;
            default:
                return wallTilemap;
        }
    }
    
    TileBase GetTileBase(BlockType type)
    {
        int idx = (int)type;
        return (idx >= 0 && idx < tiles.Length) ? tiles[idx] : null;
    }
    
    public BlockType GetBlock(int x, int y)
    {
        if (x < 0 || x >= worldWidth || y < 0 || y >= worldHeight) return BlockType.Air;
        Vector3Int pos = new Vector3Int(x, y, 0);
        TileBase t = groundTilemap.GetTile(pos) ?? decorationTilemap.GetTile(pos) ?? waterTilemap.GetTile(pos);
        if (t == null) return BlockType.Air;
        for (int i = 0; i < tiles.Length; i++) if (tiles[i] == t) return (BlockType)i;
        return BlockType.Air;
    }
    
    public bool IsSolid(int x, int y)
    {
        BlockType t = GetBlock(x, y);
        return t != BlockType.Air && t != BlockType.Water && t != BlockType.Flower;
    }
    
    public Player GetPlayer() => _player;
    public QuestManager GetQuestManager() => _questManager;
    public GuideSystem GetGuideSystem() => _guideSystem;
    public int GetWorldWidth() => worldWidth;
    public int GetWorldHeight() => worldHeight;
    public InventoryUI GetInventoryUI() => inventoryUI;
    public CraftingUI GetCraftingUI() => craftingUI;
    public QuestUI GetQuestUI() => questUI;
    public GuideUI GetGuideUI() => guideUI;
    public bool IsPaused() => _isPaused;
    
    public void SaveWorld()
    {
        try
        {
            string path = Application.persistentDataPath + "/world.save";
            using BinaryWriter w = new BinaryWriter(File.Open(path, FileMode.Create));
            w.Write(worldWidth); w.Write(worldHeight); w.Write(seed);
            w.Write(_dayNightCycle.GetTime());
            for (int x = 0; x < worldWidth; x++)
                for (int y = 0; y < worldHeight; y++)
                    w.Write((int)GetBlock(x, y));
            _player?.Save(w);
            _questManager.Save(w);
            inventoryUI.Save(w);
            _guideSystem.ShowMessage("Мир сохранён!");
        }
        catch (Exception e) { Debug.LogError("Ошибка сохранения: " + e.Message); }
    }
    
    public void LoadWorld()
    {
        try
        {
            string path = Application.persistentDataPath + "/world.save";
            if (!File.Exists(path)) { _guideSystem.ShowMessage("Сохранение не найдено"); return; }
            using BinaryReader r = new BinaryReader(File.Open(path, FileMode.Open));
            int w = r.ReadInt32(), h = r.ReadInt32(), s = r.ReadInt32();
            float time = r.ReadSingle();
            if (w != worldWidth || h != worldHeight) { _guideSystem.ShowMessage("Размеры не совпадают"); return; }
            for (int x = 0; x < worldWidth; x++)
                for (int y = 0; y < worldHeight; y++)
                    SetBlock(x, y, (BlockType)r.ReadInt32());
            _player?.Load(r);
            _questManager.Load(r);
            inventoryUI.Load(r);
            _dayNightCycle.SetTime(time);
            _guideSystem.ShowMessage("Мир загружен!");
        }
        catch (Exception e) { Debug.LogError("Ошибка загрузки: " + e.Message); }
    }
}

// ============================================================
// 2. ГЕНЕРАТОР МИРА
// ============================================================
public class WorldGenerator
{
    private int _seed, _width, _height;
    private System.Random _random;
    private float[,] _heightMap, _caveMap, _moistureMap;
    
    public WorldGenerator(int seed, int width, int height)
    {
        _seed = seed; _width = width; _height = height;
        _random = new System.Random(seed);
        _heightMap = new float[width, height];
        _caveMap = new float[width, height];
        _moistureMap = new float[width, height];
        
        for (int x = 0; x < width; x++)
        {
            float v = 0, s = 1, a = 1;
            for (int i = 0; i < 6; i++)
            { v += Mathf.PerlinNoise((x + seed) * s / 80f, seed * s / 80f) * a; s *= 2.2f; a *= 0.45f; }
            for (int y = 0; y < height; y++)
                _heightMap[x, y] = v * height / 3.5f + height / 4f;
        }
        
        for (int x = 0; x < width; x++)
            for (int y = 0; y < height; y++)
            {
                _caveMap[x, y] = Mathf.PerlinNoise((x + seed * 2) / 35f, (y + seed * 3) / 35f);
                _moistureMap[x, y] = Mathf.PerlinNoise((x + seed * 5) / 60f, (y + seed * 7) / 60f);
            }
    }
    
    public BlockType GetBlockType(int x, int y)
    {
        float h = _heightMap[x, y], c = _caveMap[x, y], m = _moistureMap[x, y];
        if (y < _height / 4f && m > 0.6f) return BlockType.Water;
        if (y > h + 12) return BlockType.Air;
        if (y < h - 30) return c > 0.45f ? BlockType.Air : BlockType.Stone;
        if (y < h - 25) return BlockType.Stone;
        if (y < h) return BlockType.Dirt;
        if (Mathf.Abs(y - h) < 1f) return m > 0.5f ? BlockType.Grass : (m > 0.3f ? BlockType.Sand : BlockType.Dirt);
        if (Mathf.Abs(y - h) < 2f && m > 0.4f && _random.NextDouble() < 0.1f) return BlockType.Flower;
        return BlockType.Air;
    }
    
    public void GenerateTrees(Action<int, int, BlockType> set)
    {
        for (int x = 3; x < _width - 3; x++)
            for (int y = 3; y < _height - 3; y++)
                if (GetBlockType(x, y) == BlockType.Grass && _random.NextDouble() < 0.025f && _moistureMap[x, y] > 0.3f)
                {
                    int height = _random.Next(5, 9);
                    for (int i = 0; i < height; i++) if (y + i < _height) set(x, y + i, BlockType.Wood);
                    for (int dx = -3; dx <= 3; dx++)
                        for (int dy = -2; dy <= 2; dy++)
                            if (Mathf.Abs(dx) + Mathf.Abs(dy) <= 4)
                            { int lx = x + dx, ly = y + height - 3 + dy; if (lx >= 0 && lx < _width && ly >= 0 && ly < _height) set(lx, ly, BlockType.Leaf); }
                }
    }
    
    public void GenerateOres(Action<int, int, BlockType> set)
    {
        for (int i = 0; i < 80; i++)
        {
            int x = _random.Next(5, _width - 5), y = _random.Next(10, _height / 2);
            if (GetBlockType(x, y) == BlockType.Stone)
            {
                set(x, y, BlockType.IronOre);
                for (int j = 0; j < 3; j++)
                { int dx = _random.Next(-2, 3), dy = _random.Next(-2, 3); if (GetBlockType(x + dx, y + dy) == BlockType.Stone) set(x + dx, y + dy, BlockType.IronOre); }
            }
        }
        for (int i = 0; i < 20; i++)
        {
            int x = _random.Next(10, _width - 10), y = _random.Next(5, _height / 3);
            if (GetBlockType(x, y) == BlockType.Stone) set(x, y, BlockType.GoldOre);
        }
    }
    
    public void GenerateDungeons(Action<int, int, BlockType> set)
    {
        for (int i = 0; i < 5; i++)
        {
            int x = _random.Next(15, _width - 15), y = _random.Next(5, _height / 3);
            for (int dx = -3; dx <= 3; dx++)
                for (int dy = -3; dy <= 3; dy++)
                    if (Mathf.Abs(dx) == 3 || Mathf.Abs(dy) == 3) set(x + dx, y + dy, BlockType.Stone);
            set(x, y, BlockType.Chest);
            if (_random.NextDouble() < 0.5f) set(x + 1, y, BlockType.Chest);
        }
    }
    
    public void GenerateWater(Action<int, int, BlockType> set)
    {
        for (int x = 0; x < _width; x++)
            for (int y = 0; y < _height / 4; y++)
                if (GetBlockType(x, y) == BlockType.Air && _moistureMap[x, y] > 0.7f)
                    set(x, y, BlockType.Water);
    }
}

// ============================================================
// 3. ИГРОК
// ============================================================
public class Player : MonoBehaviour
{
    [Header("Движение")]
    public float speed = 4f;
    public float jumpForce = 7f;
    public float gravity = -25f;
    public int reachDistance = 5;
    public float fallResetY = -50f;
    
    [Header("Инвентарь")]
    public int hotbarSize = 9;
    public GameObject blockHighlight;
    public SpriteRenderer highlightRenderer;
    
    private Rigidbody2D _rb;
    private Vector2 _velocity;
    private bool _isGrounded;
    private Inventory _inventory;
    private InventoryUI _inventoryUI;
    private GuideSystem _guideSystem;
    private int _selectedSlot;
    private Vector3Int _targetBlock;
    private Camera _cam;
    private bool _isBuilding;
    private float _hInput;
    private bool _jumpInput, _breakInput, _placeInput;
    private MobileControls _mobile;
    
    void Start()
    {
        _rb = GetComponent<Rigidbody2D>();
        _cam = Camera.main;
        _inventory = new Inventory(hotbarSize + 41);
        blockHighlight.SetActive(false);
        _mobile = FindObjectOfType<MobileControls>();
    }
    
    public void Initialize(InventoryUI ui, GuideSystem guide)
    {
        _inventoryUI = ui; _guideSystem = guide;
        _inventoryUI.SetInventory(_inventory);
        _inventoryUI.SetPlayer(this);
        _inventory.AddItem(new Item(BlockType.Wood, 10));
        _inventory.AddItem(new Item(BlockType.Dirt, 20));
        _inventory.AddItem(new Item(BlockType.Stone, 5));
        _inventory.AddItem(new Item(BlockType.Grass, 3));
    }
    
    void Update()
    {
        if (GameController.Instance == null || GameController.Instance.IsPaused()) return;
        
        if (transform.position.y < fallResetY)
        { transform.position = new Vector3(0, 20, 0); _guideSystem?.ShowMessage("Возврат из бездны!"); }
        
        HandleInput();
        HandleMovement();
        HandleInteraction();
        HandleHotbar();
        HandleBuildMode();
        UpdateHighlight();
    }
    
    void HandleInput()
    {
        if (_mobile != null && _mobile.gameObject.activeSelf)
        {
            _hInput = _mobile.GetHorizontal();
            _jumpInput = _mobile.GetJump();
            _breakInput = _mobile.GetBreak();
            _placeInput = _mobile.GetPlace();
        }
        else
        {
            _hInput = Input.GetAxis("Horizontal");
            _jumpInput = Input.GetButtonDown("Jump");
            _breakInput = Input.GetMouseButtonDown(0);
            _placeInput = Input.GetMouseButtonDown(1);
        }
    }
    
    void HandleMovement()
    {
        _velocity.x = _hInput * speed;
        if (_jumpInput && _isGrounded) _velocity.y = jumpForce;
        if (!_isGrounded) _velocity.y += gravity * Time.deltaTime;
        _rb.velocity = _velocity;
        
        RaycastHit2D hit = Physics2D.Raycast(transform.position, Vector2.down, 0.7f);
        _isGrounded = hit.collider != null && hit.collider.gameObject.layer == LayerMask.NameToLayer("Ground");
        if (_hInput != 0) transform.localScale = new Vector3(Mathf.Sign(_hInput), 1, 1);
    }
    
    void HandleInteraction()
    {
        Vector2 mousePos = _cam.ScreenToWorldPoint(Input.mousePosition);
        Vector2 dir = (mousePos - (Vector2)transform.position).normalized;
        
        if (_isBuilding)
        {
            _targetBlock = GetTargetBlock(mousePos);
            if (_targetBlock != null)
            {
                blockHighlight.transform.position = _targetBlock + Vector3.one * 0.5f;
                blockHighlight.SetActive(true);
                if (_placeInput) PlaceBlock();
            }
            return;
        }
        
        for (int i = 0; i < reachDistance; i++)
        {
            Vector2 pos = (Vector2)transform.position + dir * (i + 0.5f);
            Vector3Int gp = new Vector3Int(Mathf.RoundToInt(pos.x), Mathf.RoundToInt(pos.y), 0);
            if (GameController.Instance.IsSolid(gp.x, gp.y))
            {
                _targetBlock = gp;
                blockHighlight.transform.position = gp + Vector3.one * 0.5f;
                blockHighlight.SetActive(true);
                if (_breakInput) BreakBlock();
                else if (_placeInput) PlaceBlockAdjacent();
                return;
            }
        }
        blockHighlight.SetActive(false);
    }
    
    Vector3Int GetTargetBlock(Vector2 mousePos) =>
        Vector2.Distance(mousePos, transform.position) < reachDistance ?
        new Vector3Int(Mathf.RoundToInt(mousePos.x), Mathf.RoundToInt(mousePos.y), 0) : Vector3Int.zero;
    
    void BreakBlock()
    {
        if (_targetBlock == null) return;
        BlockType t = GameController.Instance.GetBlock(_targetBlock.x, _targetBlock.y);
        if (t != BlockType.Air && t != BlockType.Water)
        {
            GameController.Instance.SetBlock(_targetBlock.x, _targetBlock.y, BlockType.Air);
            _inventory.AddItem(new Item(t, 1));
            foreach (var q in GameController.Instance.GetQuestManager().GetActiveQuests())
                if (q.Type == QuestType.Gather && q.Target == t) q.Progress++;
            _guideSystem?.ShowMessage($"Добыто: {t}");
        }
    }
    
    void PlaceBlockAdjacent()
    {
        if (_targetBlock == null) return;
        Vector3Int pos = _targetBlock;
        Vector2 dir = (Vector2)_targetBlock - (Vector2)transform.position;
        if (Mathf.Abs(dir.x) > Mathf.Abs(dir.y)) pos.x += dir.x > 0 ? -1 : 1;
        else pos.y += dir.y > 0 ? -1 : 1;
        PlaceBlockAt(pos);
    }
    
    void PlaceBlock() { if (_targetBlock != null) PlaceBlockAt(_targetBlock); }
    
    void PlaceBlockAt(Vector3Int pos)
    {
        if (pos.x < 0 || pos.x >= GameController.Instance.GetWorldWidth() ||
            pos.y < 0 || pos.y >= GameController.Instance.GetWorldHeight()) return;
        if (!GameController.Instance.IsSolid(pos.x, pos.y))
        {
            Item item = _inventory.GetItem(_selectedSlot);
            if (item != null && item.Count > 0 && item.Type != BlockType.Air && item.Type != BlockType.Water)
            {
                GameController.Instance.SetBlock(pos.x, pos.y, item.Type);
                item.Count--;
                if (item.Count <= 0) _inventory.SetItem(_selectedSlot, null);
                _inventoryUI.UpdateUI();
                _guideSystem?.ShowMessage($"Установлен: {item.Type}");
            }
        }
    }
    
    void UpdateHighlight()
    {
        if (!blockHighlight.activeSelf) return;
        highlightRenderer.color = _isBuilding ? new Color(0, 1, 0, 0.3f) : new Color(1, 1, 1, 0.2f);
    }
    
    void HandleHotbar()
    {
        for (int i = 0; i < hotbarSize; i++) if (Input.GetKeyDown(KeyCode.Alpha1 + i)) _selectedSlot = i;
        float scroll = Input.GetAxis("Mouse ScrollWheel");
        if (scroll != 0) { _selectedSlot += (int)Mathf.Sign(scroll); _selectedSlot = (_selectedSlot + hotbarSize) % hotbarSize; }
    }
    
    void HandleBuildMode()
    {
        if (Input.GetKeyDown(KeyCode.B) || (_mobile != null && _mobile.GetBuildToggle())) _isBuilding = !_isBuilding;
    }
    
    public Inventory GetInventory() => _inventory;
    
    public void Save(BinaryWriter w)
    {
        w.Write(transform.position.x); w.Write(transform.position.y);
        w.Write(_selectedSlot);
        _inventory.Save(w);
    }
    
    public void Load(BinaryReader r)
    {
        transform.position = new Vector3(r.ReadSingle(), r.ReadSingle(), 0);
        _selectedSlot = r.ReadInt32();
        _inventory.Load(r);
        _inventoryUI.UpdateUI();
    }
}

// ============================================================
// 4. ИНВЕНТАРЬ
// ============================================================
[System.Serializable]
public class Item
{
    public BlockType Type;
    public int Count;
    public Item(BlockType t, int c) { Type = t; Count = c; }
    public Sprite GetSprite() => Resources.Load<Sprite>($"Items/{Type}");
}

public class Inventory
{
    private Item[] _items;
    public int Length => _items.Length;
    public Inventory(int size) { _items = new Item[size]; }
    public Item GetItem(int idx) { return (idx >= 0 && idx < _items.Length) ? _items[idx] : null; }
    public void SetItem(int idx, Item item) { if (idx >= 0 && idx < _items.Length) _items[idx] = item; }
    
    public void AddItem(Item item)
    {
        for (int i = 0; i < _items.Length; i++)
            if (_items[i] != null && _items[i].Type == item.Type) { _items[i].Count += item.Count; return; }
        for (int i = 0; i < _items.Length; i++)
            if (_items[i] == null) { _items[i] = new Item(item.Type, item.Count); return; }
    }
    
    public int GetItemCount(BlockType type)
    {
        int c = 0;
        foreach (var item in _items) if (item != null && item.Type == type) c += item.Count;
        return c;
    }
    
    public void RemoveItem(BlockType type, int count)
    {
        for (int i = 0; i < _items.Length && count > 0; i++)
        {
            if (_items[i] != null && _items[i].Type == type)
            {
                int take = Mathf.Min(_items[i].Count, count);
                _items[i].Count -= take;
                count -= take;
                if (_items[i].Count <= 0) _items[i] = null;
            }
        }
    }
    
    public void Save(BinaryWriter w)
    {
        w.Write(_items.Length);
        foreach (var item in _items)
        {
            if (item == null) { w.Write(-1); continue; }
            w.Write((int)item.Type); w.Write(item.Count);
        }
    }
    
    public void Load(BinaryReader r)
    {
        int size = r.ReadInt32();
        _items = new Item[size];
        for (int i = 0; i < size; i++)
        {
            int type = r.ReadInt32();
            if (type < 0) continue;
            _items[i] = new Item((BlockType)type, r.ReadInt32());
        }
    }
}

// ============================================================
// 5. UI ИНВЕНТАРЯ
// ============================================================
public class InventoryUI : MonoBehaviour
{
    public GameObject slotPrefab;
    public Transform slotContainer;
    public Transform hotbarContainer;
    public Text titleText;
    public GameObject panel;
    
    private Inventory _inventory;
    private Player _player;
    private List<InventorySlot> _slots = new List<InventorySlot>();
    private bool _isOpen;
    
    public void Initialize(int maxSize)
    {
        panel.SetActive(false);
        for (int i = 0; i < maxSize; i++)
        {
            Transform parent = i < 9 ? hotbarContainer : slotContainer;
            GameObject go = Instantiate(slotPrefab, parent);
            var slot = go.GetComponent<InventorySlot>();
            slot.SetIndex(i);
            _slots.Add(slot);
        }
    }
    
    public void SetInventory(Inventory inv) { _inventory = inv; UpdateUI(); }
    public void SetPlayer(Player p) { _player = p; }
    
    public void UpdateUI()
    {
        for (int i = 0; i < _slots.Count; i++)
            _slots[i].SetItem(_inventory?.GetItem(i));
    }
    
    public void Toggle()
    {
        _isOpen = !_isOpen;
        panel.SetActive(_isOpen);
        if (_isOpen) UpdateUI();
    }
    
    public void Save(BinaryWriter w) { _inventory?.Save(w); }
    public void Load(BinaryReader r) { _inventory?.Load(r); UpdateUI(); }
}

public class InventorySlot : MonoBehaviour, IPointerClickHandler
{
    public Image icon;
    public Text countText;
    public Image highlight;
    private int _index;
    private Item _item;
    
    public void SetIndex(int idx) { _index = idx; }
    public void SetItem(Item item)
    {
        _item = item;
        if (item == null) { icon.sprite = null; icon.color = Color.clear; countText.text = ""; return; }
        icon.sprite = item.GetSprite();
        icon.color = Color.white;
        countText.text = item.Count > 1 ? item.Count.ToString() : "";
    }
    public void OnPointerClick(PointerEventData e) { }
}

// ============================================================
// 6. КРАФТ
// ============================================================
public class CraftingUI : MonoBehaviour
{
    public GameObject panel;
    public Transform recipeContainer;
    public GameObject recipePrefab;
    public Button craftButton;
    public Image resultImage;
    public Text resultName;
    public Text resultCount;
    public Text ingredientsText;
    
    private Inventory _inventory;
    private List<Recipe> _recipes = new List<Recipe>();
    private Recipe _selectedRecipe;
    private bool _isOpen;
    
    void Start()
    {
        panel.SetActive(false);
        LoadRecipes();
        craftButton.onClick.AddListener(Craft);
    }
    
    void LoadRecipes()
    {
        _recipes.Add(new Recipe("Деревянная кирка", new Dictionary<BlockType, int> { { BlockType.Wood, 3 } }, BlockType.Planks, 1));
        _recipes.Add(new Recipe("Каменная кирка", new Dictionary<BlockType, int> { { BlockType.Cobblestone, 3 } }, BlockType.Cobblestone, 1));
        _recipes.Add(new Recipe("Деревянный меч", new Dictionary<BlockType, int> { { BlockType.Wood, 2 } }, BlockType.Planks, 1));
        _recipes.Add(new Recipe("Сундук", new Dictionary<BlockType, int> { { BlockType.Wood, 8 } }, BlockType.Chest, 1));
        _recipes.Add(new Recipe("Верстак", new Dictionary<BlockType, int> { { BlockType.Wood, 4 } }, BlockType.CraftingTable, 1));
        _recipes.Add(new Recipe("Стекло", new Dictionary<BlockType, int> { { BlockType.Sand, 3 } }, BlockType.Glass, 3));
        _recipes.Add(new Recipe("Кирпич", new Dictionary<BlockType, int> { { BlockType.Dirt, 2 } }, BlockType.Brick, 4));
    }
    
    public void Initialize() { _inventory = GameController.Instance?.GetPlayer()?.GetInventory(); UpdateUI(); }
    
    public void Toggle()
    {
        _isOpen = !_isOpen;
        panel.SetActive(_isOpen);
        if (_isOpen) UpdateUI();
    }
    
    void UpdateUI()
    {
        foreach (Transform c in recipeContainer) Destroy(c.gameObject);
        foreach (var r in _recipes)
        {
            GameObject go = Instantiate(recipePrefab, recipeContainer);
            go.GetComponent<RecipeUI>().SetRecipe(r, this);
        }
        UpdateCraftButton();
    }
    
    public void SelectRecipe(Recipe r)
    {
        _selectedRecipe = r;
        resultImage.sprite = GetSprite(r.Result);
        resultName.text = r.Name;
        resultCount.text = $"x{r.ResultCount}";
        ingredientsText.text = string.Join("\n", r.Ingredients.Select(kvp => $"{kvp.Key}: {kvp.Value}"));
        UpdateCraftButton();
    }
    
    void UpdateCraftButton()
    {
        if (_selectedRecipe == null || _inventory == null) { craftButton.interactable = false; return; }
        bool can = true;
        foreach (var kvp in _selectedRecipe.Ingredients)
            if (_inventory.GetItemCount(kvp.Key) < kvp.Value) { can = false; break; }
        craftButton.interactable = can;
    }
    
    void Craft()
    {
        if (_selectedRecipe == null || _inventory == null) return;
        foreach (var kvp in _selectedRecipe.Ingredients) _inventory.RemoveItem(kvp.Key, kvp.Value);
        _inventory.AddItem(new Item(_selectedRecipe.Result, _selectedRecipe.ResultCount));
        GameController.Instance?.GetGuideSystem()?.ShowMessage($"Создано: {_selectedRecipe.Name}");
        UpdateUI();
        GameController.Instance?.GetInventoryUI()?.UpdateUI();
    }
    
    Sprite GetSprite(BlockType t) => Resources.Load<Sprite>($"Items/{t}");
}

public class Recipe
{
    public string Name;
    public Dictionary<BlockType, int> Ingredients;
    public BlockType Result;
    public int ResultCount;
    public Recipe(string n, Dictionary<BlockType, int> ing, BlockType r, int c)
    { Name = n; Ingredients = ing; Result = r; ResultCount = c; }
}

public class RecipeUI : MonoBehaviour
{
    public Text nameText;
    public Image iconImage;
    private Recipe _recipe;
    private CraftingUI _craftingUI;
    
    public void SetRecipe(Recipe r, CraftingUI ui)
    {
        _recipe = r; _craftingUI = ui;
        nameText.text = r.Name;
        iconImage.sprite = Resources.Load<Sprite>($"Items/{r.Result}");
        GetComponent<Button>().onClick.AddListener(() => _craftingUI.SelectRecipe(_recipe));
    }
}

// ============================================================
// 7. КВЕСТЫ
// ============================================================
public class QuestManager
{
    private List<Quest> _activeQuests = new List<Quest>();
    private List<Quest> _completedQuests = new List<Quest>();
    private List<Quest> _availableQuests = new List<Quest>();
    private int _questIndex;
    
    public QuestManager()
    {
        _availableQuests.Add(new Quest("Сбор древесины", "Собери 15 дерева", QuestType.Gather, BlockType.Wood, 15, new Item(BlockType.Planks, 5), 10));
        _availableQuests.Add(new Quest("Добыча камня", "Добудь 30 камня", QuestType.Gather, BlockType.Stone, 30, new Item(BlockType.Cobblestone, 10), 20));
        _availableQuests.Add(new Quest("Исследование подземелий", "Найди 3 подземелья", QuestType.Explore, BlockType.Chest, 3, new Item(BlockType.GoldOre, 5), 50));
        _availableQuests.Add(new Quest("Создание верстака", "Создай верстак", QuestType.Craft, BlockType.CraftingTable, 1, new Item(BlockType.Planks, 10), 15));
        _activeQuests.Add(_availableQuests[0]); _questIndex = 1;
    }
    
    public void UpdateQuests(Inventory inv)
    {
        for (int i = _activeQuests.Count - 1; i >= 0; i--)
        {
            var q = _activeQuests[i];
            if (!q.IsComplete)
            {
                q.CheckProgress(inv);
                if (q.IsComplete)
                {
                    inv?.AddItem(q.Reward);
                    _completedQuests.Add(q);
                    _activeQuests.RemoveAt(i);
                    GameController.Instance?.GetGuideSystem()?.ShowMessage($"Квест выполнен: {q.Name}!");
                    if (_questIndex < _availableQuests.Count)
                    {
                        _activeQuests.Add(_availableQuests[_questIndex]);
                        GameController.Instance?.GetGuideSystem()?.ShowMessage($"Новый квест: {_availableQuests[_questIndex].Name}");
                        _questIndex++;
                    }
                }
            }
        }
        if (_activeQuests.Count == 0 && _questIndex < _availableQuests.Count)
        {
            _activeQuests.Add(_availableQuests[_questIndex]);
            GameController.Instance?.GetGuideSystem()?.ShowMessage($"Новый квест: {_availableQuests[_questIndex].Name}");
            _questIndex++;
        }
    }
    
    public List<Quest> GetActiveQuests() => _activeQuests;
    public List<Quest> GetCompletedQuests() => _completedQuests;
    
    public void Save(BinaryWriter w)
    {
        w.Write(_questIndex);
        w.Write(_activeQuests.Count);
        foreach (var q in _activeQuests) q.Save(w);
        w.Write(_completedQuests.Count);
        foreach (var q in _completedQuests) q.Save(w);
    }
    
    public void Load(BinaryReader r)
    {
        _questIndex = r.ReadInt32();
        int ac = r.ReadInt32();
        _activeQuests.Clear();
        for (int i = 0; i < ac; i++) { var q = new Quest("", "", QuestType.Gather, BlockType.Air, 0, null, 0); q.Load(r); _activeQuests.Add(q); }
        int cc = r.ReadInt32();
        _completedQuests.Clear();
        for (int i = 0; i < cc; i++) { var q = new Quest("", "", QuestType.Gather, BlockType.Air, 0, null, 0); q.Load(r); _completedQuests.Add(q); }
    }
}

public class Quest
{
    public string Name, Description;
    public QuestType Type;
    public BlockType Target;
    public int Required, Progress, RewardXP;
    public Item Reward;
    public bool IsComplete;
    
    public Quest(string n, string d, QuestType t, BlockType target, int req, Item reward, int xp)
    { Name = n; Description = d; Type = t; Target = target; Required = req; Reward = reward; RewardXP = xp; Progress = 0; IsComplete = false; }
    
    public void CheckProgress(Inventory inv)
    {
        if (IsComplete) return;
        switch (Type)
        {
            case QuestType.Gather: Progress = Mathf.Min(Progress + 1, Required); break;
            case QuestType.Explore: break;
            case QuestType.Craft: break;
        }
        if (Progress >= Required) IsComplete = true;
    }
    
    public void Save(BinaryWriter w)
    {
        w.Write(Name); w.Write(Description); w.Write((int)Type);
        w.Write((int)Target); w.Write(Required); w.Write(Progress);
        w.Write(IsComplete); w.Write(RewardXP);
        w.Write(Reward != null);
        if (Reward != null) { w.Write((int)Reward.Type); w.Write(Reward.Count); }
    }
    
    public void Load(BinaryReader r)
    {
        Name = r.ReadString(); Description = r.ReadString();
        Type = (QuestType)r.ReadInt32(); Target = (BlockType)r.ReadInt32();
        Required = r.ReadInt32(); Progress = r.ReadInt32();
        IsComplete = r.ReadBoolean(); RewardXP = r.ReadInt32();
        if (r.ReadBoolean()) Reward = new Item((BlockType)r.ReadInt32(), r.ReadInt32());
    }
}

public enum QuestType { Gather, Kill, Explore, Craft }

public class QuestUI : MonoBehaviour
{
    public GameObject panel;
    public Transform questContainer;
    public GameObject questPrefab;
    public Text titleText;
    private bool _isOpen;
    
    public void Initialize() { panel.SetActive(false); }
    public void Toggle()
    {
        _isOpen = !_isOpen;
        panel.SetActive(_isOpen);
        if (_isOpen) UpdateUI();
    }
    
    void UpdateUI()
    {
        foreach (Transform c in questContainer) Destroy(c.gameObject);
        var qs = GameController.Instance?.GetQuestManager()?.GetActiveQuests();
        if (qs == null) return;
        foreach (var q in qs)
        {
            GameObject go = Instantiate(questPrefab, questContainer);
            go.GetComponent<QuestEntry>().SetQuest(q);
        }
    }
}

public class QuestEntry : MonoBehaviour
{
    public Text nameText;
    public Text progressText;
    public Text rewardText;
    
    public void SetQuest(Quest q)
    {
        nameText.text = q.Name;
        progressText.text = $"{q.Progress}/{q.Required}";
        rewardText.text = $"Награда: {q.Reward?.Type} x{q.Reward?.Count ?? 0}";
    }
}

// ============================================================
// 8. ПРОВОДНИК
// ============================================================
public class GuideSystem
{
    private List<string> _messages = new List<string>();
    private float _lastMessageTime;
    private const float DELAY = 3f;
    
    public GuideSystem()
    {
        _messages.Add("Добро пожаловать! Я буду помогать.");
        _messages.Add("ЛКМ - добыча, ПКМ - установка.");
        _messages.Add("E - инвентарь, C - крафт, Q - квесты.");
        _messages.Add("Собирайте ресурсы и создавайте инструменты.");
        _messages.Add("Исследуйте мир и находите подземелья!");
        _messages.Add("R - сохранить, L - загрузить.");
    }
    
    public void ShowMessage(string msg)
    {
        if (Time.time - _lastMessageTime < DELAY) return;
        _lastMessageTime = Time.time;
        _messages.Insert(0, msg);
        if (_messages.Count > 50) _messages.RemoveAt(_messages.Count - 1);
        if (GuideUI.Instance != null) GuideUI.Instance.ShowMessage(msg);
    }
    
    public string GetRandomTip()
    {
        string[] tips = {
            "Создайте деревянную кирку!", "Ищите руды глубоко под землёй.",
            "Дерево - отличный ресурс.", "Стройте дом для защиты.",
            "Крафтите улучшенные инструменты."
        };
        return tips[UnityEngine.Random.Range(0, tips.Length)];
    }
    
    public List<string> GetMessages() => _messages;
}

public class GuideUI : MonoBehaviour
{
    public static GuideUI Instance;
    public GameObject panel;
    public Text messageText;
    public Text tipText;
    public Transform messageContainer;
    public GameObject messagePrefab;
    public Button tipButton;
    public Button closeButton;
    
    private GuideSystem _guide;
    private bool _isOpen;
    private float _msgTimer;
    
    void Awake() { Instance = this; }
    
    public void Initialize(GuideSystem g)
    {
        _guide = g;
        panel.SetActive(false);
        tipButton.onClick.AddListener(ShowTip);
        closeButton.onClick.AddListener(() => Toggle());
    }
    
    public void Toggle()
    {
        _isOpen = !_isOpen;
        panel.SetActive(_isOpen);
        if (_isOpen) UpdateUI();
    }
    
    public void ShowMessage(string msg)
    {
        messageText.text = msg;
        _msgTimer = 4f;
        GameObject go = Instantiate(messagePrefab, messageContainer);
        go.GetComponent<Text>().text = "▶ " + msg;
        if (messageContainer.childCount > 20) Destroy(messageContainer.GetChild(0).gameObject);
    }
    
    void Update()
    {
        if (_msgTimer > 0) { _msgTimer -= Time.deltaTime; if (_msgTimer <= 0) messageText.text = "Готов..."; }
    }
    
    void ShowTip() { string t = _guide.GetRandomTip(); tipText.text = "💡 " + t; ShowMessage(t); }
    
    void UpdateUI()
    {
        foreach (Transform c in messageContainer) Destroy(c.gameObject);
        var msgs = _guide.GetMessages();
        for (int i = msgs.Count - 1; i >= 0; i--)
        { GameObject go = Instantiate(messagePrefab, messageContainer); go.GetComponent<Text>().text = "▶ " + msgs[i]; }
    }
}

// ============================================================
// 9. МОБИЛЬНОЕ УПРАВЛЕНИЕ
// ============================================================
public class MobileControls : MonoBehaviour
{
    public Joystick movementJoystick;
    public Button jumpButton, breakButton, placeButton, buildButton;
    public Button inventoryButton, craftButton, questButton, guideButton;
    
    private float _h;
    private bool _jump, _break, _place, _build;
    
    void Start()
    {
        jumpButton.onClick.AddListener(() => _jump = true);
        breakButton.onClick.AddListener(() => _break = true);
        placeButton.onClick.AddListener(() => _place = true);
        buildButton.onClick.AddListener(() => _build = true);
        inventoryButton.onClick.AddListener(() => GameController.Instance?.GetInventoryUI()?.Toggle());
        craftButton.onClick.AddListener(() => GameController.Instance?.GetCraftingUI()?.Toggle());
        questButton.onClick.AddListener(() => GameController.Instance?.GetQuestUI()?.Toggle());
        guideButton.onClick.AddListener(() => GameController.Instance?.GetGuideUI()?.Toggle());
    }
    
    void Update()
    {
        _h = movementJoystick.Horizontal;
        if (_jump) { _jump = false; }
        if (_break) { _break = false; }
        if (_place) { _place = false; }
        if (_build) { _build = false; }
    }
    
    public float GetHorizontal() => _h;
    public bool GetJump() => _jump;
    public bool GetBreak() => _break;
    public bool GetPlace() => _place;
    public bool GetBuildToggle() => _build;
}

// ============================================================
// 10. ДЕНЬ/НОЧЬ
// ============================================================
public class DayNightCycle
{
    private float _time;
    private float _dayLength;
    private Light2D _globalLight;
    
    public DayNightCycle(float len)
    {
        _dayLength = len;
        _time = 0;
        _globalLight = GameObject.FindObjectOfType<Light2D>();
    }
    
    public void Tick()
    {
        _time += Time.deltaTime;
        if (_time > _dayLength) _time = 0;
        if (_globalLight != null)
        {
            float i = GetLightIntensity();
            _globalLight.intensity = i * 0.8f + 0.2f;
            _globalLight.color = i > 0.7f ? new Color(1, 0.95f, 0.8f) : (i > 0.4f ? new Color(0.8f, 0.7f, 0.9f) : new Color(0.2f, 0.1f, 0.3f));
        }
    }
    
    public float GetLightIntensity()
    {
        float n = _time / _dayLength;
        return Mathf.Clamp01(Mathf.Sin(n * 2 * Mathf.PI) * 0.6f + 0.4f);
    }
    
    public float GetTime() => _time;
    public void SetTime(float t) => _time = t;
}

// ============================================================
// 11. ЧАНКИ
// ============================================================
public class Chunk
{
    public Vector2Int Position;
    private GameObject _go;
    private Tilemap _tilemap;
    
    public Chunk(Vector2Int pos) { Position = pos; }
    
    public void Load(GameController ctrl)
    {
        _go = new GameObject($"Chunk_{Position.x}_{Position.y}");
        _go.transform.position = new Vector3(Position.x * 16, Position.y * 16, 0);
        _tilemap = _go.AddComponent<Tilemap>();
        _go.AddComponent<TilemapRenderer>();
        for (int x = 0; x < 16; x++)
            for (int y = 0; y < 16; y++)
            {
                BlockType t = ctrl.GetBlock(Position.x * 16 + x, Position.y * 16 + y);
                if (t != BlockType.Air) _tilemap.SetTile(new Vector3Int(x, y, 0), ctrl.GetTileBase(t));
            }
    }
    
    public void Unload() { if (_go != null) GameObject.Destroy(_go); }
    public void Tick() { }
}

// ============================================================
// 12. КАМЕРА
// ============================================================
public class CameraFollow : MonoBehaviour
{
    private Transform _target;
    public Vector3 offset = new Vector3(0, 0, -10);
    public float smoothness = 0.125f;
    public void SetTarget(Transform t) { _target = t; }
    void LateUpdate()
    {
        if (_target == null) return;
        transform.position = Vector3.Lerp(transform.position, _target.position + offset, smoothness);
    }
}

// ============================================================
// 13. ТИПЫ БЛОКОВ
// ============================================================
public enum BlockType
{
    Air = 0, Grass = 1, Dirt = 2, Stone = 3, Wood = 4, Leaf = 5,
    IronOre = 6, GoldOre = 7, Chest = 8, CraftingTable = 9,
    Furnace = 10, Torch = 11, Sand = 12, Water = 13, Flower = 14,
    Planks = 15, Cobblestone = 16, Glass = 17, Brick = 18
}

// ============================================================
// 14. ДЖОЙСТИК (СТАНДАРТНЫЙ)
// ============================================================
public class Joystick : MonoBehaviour, IDragHandler, IPointerUpHandler, IPointerDownHandler
{
    public Image background;
    public Image handle;
    public float handleRange = 1;
    
    private Vector2 _input = Vector2.zero;
    public float Horizontal => _input.x;
    public float Vertical => _input.y;
    
    public void OnPointerDown(PointerEventData e) { OnDrag(e); }
    public void OnPointerUp(PointerEventData e) { _input = Vector2.zero; handle.rectTransform.anchoredPosition = Vector2.zero; }
    
    public void OnDrag(PointerEventData e)
    {
        Vector2 pos = Vector2.zero;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(background.rectTransform, e.position, e.pressEventCamera, out pos);
        pos.x /= background.rectTransform.sizeDelta.x;
        pos.y /= background.rectTransform.sizeDelta.y;
        _input = Vector2.ClampMagnitude(pos, 1);
        handle.rectTransform.anchoredPosition = _input * handleRange * 50;
    }
}

Game Source: Раст

Creator: TurboMeteor43

Libraries: none

Complexity: complex (1285 lines, 45.7 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-turbometeor43" to link back to the original. Then publish at arcadelab.ai/publish.