เพิ่มการเก็บตำแหน่งกล้อง และ Seed เพื่อให้กลับมาแสดงผลที่ตำแหน่งเดิม

เพิ่มการเก็บตำแหน่งกล้อง และ Seed เพื่อให้กลับมาแสดงผลที่ตำแหน่งเดิม
/*
 * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
 * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
 */
package com.umgutils.game;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Random;

/**
 *
 * @author Thada
 * คลาสหลักสำหรับเกมสร้างแผนที่
 * สร้าง JFrame และเพิ่ม GamePanel เข้าไป
 * คลาสหลักสำหรับเกมสร้างแผนที่
 * สร้าง JFrame และเพิ่ม GamePanel เข้าไป
 * คลาสหลักสำหรับเกมสร้างแผนที่
 * สร้าง JFrame และเพิ่ม GamePanel เข้าไป
 */
public class MapGame {

    public static void main(String[] args) {
        // สร้าง Frame หลักของโปรแกรม
        JFrame frame = new JFrame("Map Based Game - ใช้ปุ่มลูกศรหรือลากเมาส์เพื่อเคลื่อนที่");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(800, 800);
        frame.setLocationRelativeTo(null); // แสดงหน้าต่างกลางจอ

        // สร้างและเพิ่ม GamePanel ที่เป็นหัวใจหลักของเกม
        GamePanel gamePanel = new GamePanel();
        frame.add(gamePanel);

        // เพิ่ม listener เพื่อดักจับ event ตอนปิดหน้าต่าง และทำการเซฟเกม
        frame.addWindowListener(new java.awt.event.WindowAdapter() {
            @Override
            public void windowClosing(java.awt.event.WindowEvent windowEvent) {
                gamePanel.saveGame();
            }
        });

        frame.setVisible(true);
    }

    /**
     * GamePanel ทำหน้าที่วาดแผนที่และจัดการการควบคุม
     */
    static class GamePanel extends JPanel {

        // --- เพิ่มค่าคงที่สำหรับไฟล์เซฟ ---
        private static final String SAVE_FILE = "save.dat";

        // --- ค่าคงที่สำหรับปรับแต่งเกม ---
        private static final int TILE_SIZE = 16; // ขนาดของแต่ละไทล์ (pixel)
        private static final int CHUNK_WIDTH = 32; // จำนวนไทล์ในแนวนอนของ 1 chunk
        private static final int CHUNK_HEIGHT = 32; // จำนวนไทล์ในแนวตั้งของ 1 chunk
        private static final int MOVE_SPEED = 10; // ความเร็วในการเคลื่อนที่ (pixel)
        private static final double NOISE_SCALE = 0.05; // สเกลของ Perlin noise (ค่ายิ่งน้อย แผนที่ยิ่งเรียบ)
        private static final double RESOURCE_NOISE_SCALE = 0.2; // สเกลของ noise สำหรับสร้างทรัพยากร
        private static final double RESOURCE_THRESHOLD = 0.7; // ค่า noise ที่จะเริ่มสร้างทรัพยากร (0.0 - 1.0)

        // --- ตัวแปรสถานะของเกม ---
        private int cameraX = 0; // ตำแหน่งกล้องแนวนอน (World Coordinate)
        private int cameraY = 0; // ตำแหน่งกล้องแนวตั้ง (World Coordinate)

        // --- ตัวแปรสำหรับ Seed ของโลก ---
        private int worldSeed;
        private int resourceSeed;
        
        // --- ตัวแปรสำหรับจัดการการลากเมาส์ ---
        private Point mouseDragStart;
        private Point cameraStart;


        // Cache สำหรับเก็บ Chunk ที่สร้างแล้ว
        private final Map<Point, int[][]> chunkCache = new HashMap<>();
        // ประกาศตัวแปรไว้ก่อน แต่ยังไม่สร้าง object
        private PerlinNoise noiseGenerator;
        private PerlinNoise resourceNoiseGenerator;

        public GamePanel() {
            this.setFocusable(true); // ทำให้ Panel นี้รับ input จาก keyboard ได้
            this.setBackground(Color.BLACK);

            // โหลดเกม (ซึ่งจะตั้งค่า seed และตำแหน่ง)
            loadGame();

            // *** สร้าง Noise Generators โดยใช้ Seed ที่โหลดมา ***
            this.noiseGenerator = new PerlinNoise(worldSeed);
            this.resourceNoiseGenerator = new PerlinNoise(resourceSeed);

            // เพิ่ม KeyListener เพื่อรับการกดปุ่ม
            this.addKeyListener(new KeyAdapter() {
                @Override
                public void keyPressed(KeyEvent e) {
                    handleKeyPress(e.getKeyCode());
                }
            });
            
            // --- เพิ่ม Mouse Listener สำหรับการลากแผนที่ ---
            MouseAdapter mouseAdapter = new MouseAdapter() {
                @Override
                public void mousePressed(MouseEvent e) {
                    // เมื่อกดเมาส์, บันทึกจุดเริ่มต้นของการลาก และตำแหน่งของกล้อง ณ ตอนนั้น
                    mouseDragStart = e.getPoint();
                    cameraStart = new Point(cameraX, cameraY);
                    setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
                }

                @Override
                public void mouseDragged(MouseEvent e) {
                    if (mouseDragStart != null) {
                        // คำนวณระยะทางที่เมาส์ลากไป
                        int dx = e.getX() - mouseDragStart.x;
                        int dy = e.getY() - mouseDragStart.y;
                        
                        // อัปเดตตำแหน่งกล้องตามระยะที่ลาก (ลากขวากล้องไปซ้าย, ลากซ้ายกล้องไปขวา)
                        cameraX = cameraStart.x - dx;
                        cameraY = cameraStart.y - dy;
                        
                        // วาดแผนที่ใหม่
                        repaint();
                    }
                }

                @Override
                public void mouseReleased(MouseEvent e) {
                    // เมื่อปล่อยเมาส์, รีเซ็ตค่าการลาก
                    mouseDragStart = null;
                    cameraStart = null;
                    setCursor(Cursor.getDefaultCursor());
                }
            };
            
            addMouseListener(mouseAdapter);
            addMouseMotionListener(mouseAdapter);
        }

        /**
         * จัดการการกดปุ่มเพื่อเคลื่อนที่
         * @param keyCode รหัสของปุ่มที่ถูกกด
         */
        private void handleKeyPress(int keyCode) {
            switch (keyCode) {
                case KeyEvent.VK_UP:
                    cameraY -= MOVE_SPEED;
                    break;
                case KeyEvent.VK_DOWN:
                    cameraY += MOVE_SPEED;
                    break;
                case KeyEvent.VK_LEFT:
                    cameraX -= MOVE_SPEED;
                    break;
                case KeyEvent.VK_RIGHT:
                    cameraX += MOVE_SPEED;
                    break;
            }
            repaint(); // สั่งให้วาดหน้าจอใหม่หลังจากเคลื่อนที่
        }

        /**
         * เมธอดสำหรับเซฟตำแหน่งและ Seed ปัจจุบันลงไฟล์
         */
        public void saveGame() {
            Properties properties = new Properties();
            properties.setProperty("cameraX", String.valueOf(cameraX));
            properties.setProperty("cameraY", String.valueOf(cameraY));
            // --- เซฟค่า Seed ลงไปด้วย ---
            properties.setProperty("worldSeed", String.valueOf(worldSeed));
            properties.setProperty("resourceSeed", String.valueOf(resourceSeed));

            // ใช้ try-with-resources เพื่อให้แน่ใจว่า stream ถูกปิดเสมอ
            try (OutputStream output = new FileOutputStream(SAVE_FILE)) {
                properties.store(output, "Map Game Save Data");
                System.out.println("Game saved successfully!");
            } catch (IOException io) {
                System.err.println("Error saving game state: " + io.getMessage());
            }
        }

        /**
         * เมธอดสำหรับโหลดตำแหน่งและ Seed จากไฟล์
         */
        private void loadGame() {
            File saveFile = new File(SAVE_FILE);
            // ตรวจสอบว่ามีไฟล์เซฟอยู่หรือไม่
            if (!saveFile.exists()) {
                System.out.println("No save file found. Creating a new world.");
                // --- ถ้าไม่มีเซฟ ให้สุ่ม Seed ใหม่สำหรับโลกใบนี้ ---
                Random rand = new Random();
                this.worldSeed = rand.nextInt();
                this.resourceSeed = rand.nextInt();
                this.cameraX = 0;
                this.cameraY = 0;
                return; // จบการทำงาน
            }

            Properties properties = new Properties();
            // ใช้ try-with-resources เพื่อให้แน่ใจว่า stream ถูกปิดเสมอ
            try (InputStream input = new FileInputStream(SAVE_FILE)) {
                properties.load(input);
                // โหลดค่าและแปลงเป็น Integer
                this.cameraX = Integer.parseInt(properties.getProperty("cameraX", "0"));
                this.cameraY = Integer.parseInt(properties.getProperty("cameraY", "0"));
                // --- โหลดค่า Seed กลับมา ---
                this.worldSeed = Integer.parseInt(properties.getProperty("worldSeed"));
                this.resourceSeed = Integer.parseInt(properties.getProperty("resourceSeed"));
                
                System.out.println("Game loaded successfully! Starting at (" + cameraX + ", " + cameraY + ")");
            } catch (IOException | NumberFormatException ex) {
                // จัดการกรณีไฟล์เสียหายหรืออ่านค่าไม่ได้
                System.err.println("Could not load save file: " + ex.getMessage() + ". Creating a new world.");
                // --- ถ้าไฟล์เซฟเสีย ก็สร้างโลกใหม่ ---
                Random rand = new Random();
                this.worldSeed = rand.nextInt();
                this.resourceSeed = rand.nextInt();
                this.cameraX = 0;
                this.cameraY = 0;
            }
        }

        /**
         * เมธอดหลักที่ใช้ในการวาด Component ทั้งหมด
         */
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            // คำนวณขอบเขตของ Chunk ที่ต้องวาดบนหน้าจอ
            int startChunkX = (int) Math.floor((double) cameraX / (CHUNK_WIDTH * TILE_SIZE));
            int startChunkY = (int) Math.floor((double) cameraY / (CHUNK_HEIGHT * TILE_SIZE));
            int endChunkX = (int) Math.floor((double) (cameraX + getWidth()) / (CHUNK_WIDTH * TILE_SIZE));
            int endChunkY = (int) Math.floor((double) (cameraY + getHeight()) / (CHUNK_HEIGHT * TILE_SIZE));
            
            // วนลูปเพื่อวาดทุก Chunk ที่มองเห็น
            for (int cy = startChunkY; cy <= endChunkY; cy++) {
                for (int cx = startChunkX; cx <= endChunkX; cx++) {
                    drawChunk(g, cx, cy);
                }
            }
             // วาดตำแหน่งปัจจุบันของกล้องและ Seed เพื่อการดีบัก
            g.setColor(Color.WHITE);
            g.drawString(String.format("Position: (%d, %d)", cameraX, cameraY), 10, 20);
            g.drawString(String.format("World Seed: %d", worldSeed), 10, 40);
        }
        
        /**
         * วาด Chunk 1 อันที่ตำแหน่ง (chunkX, chunkY)
         * @param g Graphics object
         * @param chunkX ตำแหน่ง Chunk แนวนอน
         * @param chunkY ตำแหน่ง Chunk แนวตั้ง
         */
        private void drawChunk(Graphics g, int chunkX, int chunkY) {
            Point chunkPos = new Point(chunkX, chunkY);
            int[][] tiles;

            // ตรวจสอบว่า Chunk นี้อยู่ใน cache หรือไม่
            if (chunkCache.containsKey(chunkPos)) {
                tiles = chunkCache.get(chunkPos); // ถ้ามีอยู่แล้ว ก็ดึงจาก cache
            } else {
                tiles = generateChunk(chunkX, chunkY); // ถ้ายังไม่มี ก็สร้างใหม่
                chunkCache.put(chunkPos, tiles); // แล้วเก็บลง cache
            }

            // วนลูปเพื่อวาดทุกไทล์ใน Chunk
            for (int y = 0; y < CHUNK_HEIGHT; y++) {
                for (int x = 0; x < CHUNK_WIDTH; x++) {
                    int tileType = tiles[y][x];
                    Color tileColor = getTileColor(tileType);

                    // คำนวณตำแหน่งที่จะวาดบนหน้าจอ
                    int drawX = chunkX * CHUNK_WIDTH * TILE_SIZE + x * TILE_SIZE - cameraX;
                    int drawY = chunkY * CHUNK_HEIGHT * TILE_SIZE + y * TILE_SIZE - cameraY;

                    g.setColor(tileColor);
                    g.fillRect(drawX, drawY, TILE_SIZE, TILE_SIZE);
                }
            }
        }
        
        /**
         * สร้างข้อมูลไทล์สำหรับ Chunk ที่กำหนดโดยใช้ Perlin Noise
         * @param chunkX ตำแหน่ง Chunk แนวนอน
         * @param chunkY ตำแหน่ง Chunk แนวตั้ง
         * @return 2D array ของ tile types
         */
        private int[][] generateChunk(int chunkX, int chunkY) {
            int[][] tiles = new int[CHUNK_HEIGHT][CHUNK_WIDTH];
            for (int y = 0; y < CHUNK_HEIGHT; y++) {
                for (int x = 0; x < CHUNK_WIDTH; x++) {
                    // คำนวณตำแหน่งไทล์ในโลกจริง (World Coordinate)
                    int worldX = chunkX * CHUNK_WIDTH + x;
                    int worldY = chunkY * CHUNK_HEIGHT + y;

                    // สร้างค่า noise สำหรับภูมิประเทศ
                    double elevation = noiseGenerator.eval(worldX * NOISE_SCALE, worldY * NOISE_SCALE);
                    
                    // กำหนดประเภทไทล์ตามค่า noise
                    int tileType;
                    if (elevation < -0.4) tileType = 0; // Deep Water
                    else if (elevation < -0.2) tileType = 1; // Shallow Water
                    else if (elevation < -0.1) tileType = 2; // Sand
                    else if (elevation < 0.5) tileType = 3; // Grass
                    else if (elevation < 0.8) tileType = 4; // Rock
                    else tileType = 5; // Snow

                    // ถ้าเป็นพื้นหญ้า (Grass) ให้ลองสุ่มทรัพยากร
                    if (tileType == 3) {
                        double resourceValue = resourceNoiseGenerator.eval(worldX * RESOURCE_NOISE_SCALE, worldY * RESOURCE_NOISE_SCALE);
                        // resourceValue จะมีค่าระหว่าง -1 ถึง 1 เราจึงต้องปรับให้อยู่ในช่วง 0-1
                        if ((resourceValue + 1) / 2 > RESOURCE_THRESHOLD) {
                           tileType = 6; // Resource
                        }
                    }

                    tiles[y][x] = tileType;
                }
            }
            return tiles;
        }

        /**
         * คืนค่าสีสำหรับไทล์แต่ละประเภท
         * @param tileType รหัสของประเภทไทล์
         * @return Color object
         */
        private Color getTileColor(int tileType) {
            switch (tileType) {
                case 0: return new Color(0, 0, 128); // Deep Water
                case 1: return new Color(0, 100, 255); // Shallow Water
                case 2: return new Color(240, 230, 140); // Sand
                case 3: return new Color(34, 139, 34);  // Grass
                case 4: return new Color(139, 137, 137); // Rock
                case 5: return new Color(255, 250, 250); // Snow
                case 6: return Color.RED; // Resource
                default: return Color.BLACK;
            }
        }
    }

    /**
     * Perlin Noise Generator
     * นี่คือการ υλοποίηση ของ "Improved Perlin Noise" โดย Ken Perlin
     * เพื่อให้ไฟล์นี้ทำงานได้ด้วยตัวเองโดยไม่ต้องพึ่ง library ภายนอก
     */
     static class PerlinNoise {
        private final int[] p = new int[512];
        
        public PerlinNoise(int seed) {
            int[] permutation = new int[256];
            for (int i = 0; i < 256; i++) {
                permutation[i] = i;
            }
            Random rand = new Random(seed);
            for (int i = 255; i > 0; i--) {
                int index = rand.nextInt(i + 1);
                int temp = permutation[index];
                permutation[index] = permutation[i];
                permutation[i] = temp;
            }
            for (int i = 0; i < 256; i++) {
                p[i] = p[i + 256] = permutation[i];
            }
        }

        public double eval(double x, double y) {
            int X = (int) Math.floor(x) & 255;
            int Y = (int) Math.floor(y) & 255;
            x -= Math.floor(x);
            y -= Math.floor(y);
            double u = fade(x);
            double v = fade(y);
            int A = p[X] + Y;
            int B = p[X + 1] + Y;
            return lerp(v, lerp(u, grad(p[A], x, y), grad(p[B], x - 1, y)),
                    lerp(u, grad(p[A + 1], x, y - 1), grad(p[B + 1], x - 1, y - 1)));
        }

        private static double fade(double t) {
            return t * t * t * (t * (t * 6 - 15) + 10);
        }

        private static double lerp(double t, double a, double b) {
            return a + t * (b - a);
        }

        private static double grad(int hash, double x, double y) {
            int h = hash & 15;
            double u = h < 8 ? x : y;
            double v = h < 4 ? y : h == 12 || h == 14 ? x : 0;
            return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
        }
    }
}