# ============================================================
#  Mini Minecraft 3D — Ursina + 无限地图 + 背包（修复出生掉落）
#  WASD 移动 | SPACE 跳 | SHIFT 冲刺 | 左键挖 | 右键放
#  1-7 选方块 | B 背包 | ESC 退出
# ============================================================
import os
import math
import random
from math import floor

from PIL import Image
from ursina import *
from ursina.prefabs.first_person_controller import FirstPersonController

# ============================================================
# 1. 程序化生成像素纹理
# ============================================================
TEX_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'mc_textures')
os.makedirs(TEX_DIR, exist_ok=True)

def make_texture_file(rgb, path, noise=22, size=16):
    img = Image.new('RGB', (size, size))
    px = img.load()
    rnd = random.Random(hash(path) & 0xffff)
    for y in range(size):
        for x in range(size):
            n = rnd.randint(-noise, noise)
            px[x, y] = (
                max(0, min(255, rgb[0] + n)),
                max(0, min(255, rgb[1] + n)),
                max(0, min(255, rgb[2] + n)),
            )
    img.save(path)

BLOCK_RGB = {
    1: ( 95, 159,  53),   # 草
    2: (134,  96,  67),   # 土
    3: (128, 128, 128),   # 石
    4: (102,  76,  43),   # 木
    5: ( 60, 143,  57),   # 叶
    6: (219, 205, 148),   # 沙
    7: (157, 120,  70),   # 木板
}
BLOCK_NAMES = {1:'Grass', 2:'Dirt', 3:'Stone', 4:'Wood',
               5:'Leaves', 6:'Sand', 7:'Planks'}

for bid, rgb in BLOCK_RGB.items():
    p = os.path.join(TEX_DIR, f'block_{bid}.png')
    if not os.path.exists(p):
        make_texture_file(rgb, p)

# ============================================================
# 2. 启动 Ursina
# ============================================================
app = Ursina(asset_folder=os.path.dirname(TEX_DIR))
window.title = 'Mini Minecraft 3D'
window.borderless = False

BLOCK_TEX = {bid: f'mc_textures/block_{bid}.png' for bid in BLOCK_RGB}

# ============================================================
# 3. 世界参数
# ============================================================
CHUNK_SIZE   = 8
LOAD_RADIUS  = 3
GROUND_DEPTH = 2

chunk_data         = {}
block_entities     = {}
loaded_chunks      = set()
last_player_chunk  = None

# ============================================================
# 4. 背包数据
# ============================================================
inventory = {i: 10 for i in range(1, 8)}
selected_block = 1

# ============================================================
# 5. 地形高度
# ============================================================
def terrain_height(x, z):
    h  = 2.5 * math.sin(x * 0.30) * math.cos(z * 0.30)
    h += 1.5 * math.sin(x * 0.70 + 1.2) * math.cos(z * 0.60 + 0.5)
    h += 0.8 * math.sin(x * 1.30 + 2.1) * math.cos(z * 1.40 + 1.7)
    return int(round(h))

# ============================================================
# 6. 区块地形生成
# ============================================================
def generate_chunk_terrain(cx, cz):
    rng = random.Random((cx * 73856093) ^ (cz * 19349663) ^ 0x9E3779B9)
    blocks = {}
    for lx in range(CHUNK_SIZE):
        for lz in range(CHUNK_SIZE):
            x = cx * CHUNK_SIZE + lx
            z = cz * CHUNK_SIZE + lz
            h = terrain_height(x, z)
            for y in range(h - GROUND_DEPTH, h + 1):
                if y == h:
                    bid = 6 if h <= -1 else 1
                elif y >= h - 1:
                    bid = 2
                else:
                    bid = 3
                blocks[(x, y, z)] = bid

    for _ in range(rng.randint(0, 1)):
        lx = rng.randint(2, CHUNK_SIZE - 3)
        lz = rng.randint(2, CHUNK_SIZE - 3)
        x = cx * CHUNK_SIZE + lx
        z = cz * CHUNK_SIZE + lz
        h = terrain_height(x, z)
        if h < 0:
            continue
        trunk_h = rng.randint(3, 5)
        for i in range(1, trunk_h + 1):
            blocks[(x, h + i, z)] = 4
        top_y = h + trunk_h
        for dx in range(-2, 3):
            for dz in range(-2, 3):
                for dy in range(-1, 2):
                    if abs(dx) + abs(dz) + abs(dy) > 3:
                        continue
                    p = (x + dx, top_y + dy, z + dz)
                    if (0 <= p[0] - cx * CHUNK_SIZE < CHUNK_SIZE and
                        0 <= p[2] - cz * CHUNK_SIZE < CHUNK_SIZE and
                        p not in blocks):
                        blocks[p] = 5
        for dx in range(-1, 2):
            for dz in range(-1, 2):
                p = (x + dx, top_y + 2, z + dz)
                if (0 <= p[0] - cx * CHUNK_SIZE < CHUNK_SIZE and
                    0 <= p[2] - cz * CHUNK_SIZE < CHUNK_SIZE and
                    p not in blocks):
                    blocks[p] = 5
    return blocks

# ============================================================
# 7. 方块实体
# ============================================================
class Block(Entity):
    def __init__(self, grid_pos, block_id):
        x, y, z = grid_pos
        super().__init__(
            model='cube',
            texture=BLOCK_TEX[block_id],
            color=color.white,
            position=(x + 0.5, y + 0.5, z + 0.5),
            collider='box',
        )
        self.grid_pos = grid_pos
        self.block_id = block_id

# ============================================================
# 8. 区块加载 / 卸载
# ============================================================
def load_chunk(cx, cz):
    if (cx, cz) in loaded_chunks:
        return
    loaded_chunks.add((cx, cz))
    if (cx, cz) not in chunk_data:
        chunk_data[(cx, cz)] = generate_chunk_terrain(cx, cz)
    for pos, bid in chunk_data[(cx, cz)].items():
        if pos not in block_entities:
            block_entities[pos] = Block(pos, bid)

def unload_chunk(cx, cz):
    if (cx, cz) not in loaded_chunks:
        return
    loaded_chunks.discard((cx, cz))
    for pos in list(chunk_data.get((cx, cz), {}).keys()):
        e = block_entities.pop(pos, None)
        if e is not None:
            destroy(e)

def update_chunks(force=False):
    global last_player_chunk
    px, pz = floor(player.x), floor(player.z)
    pcx = px // CHUNK_SIZE
    pcz = pz // CHUNK_SIZE
    cur = (pcx, pcz)
    if not force and cur == last_player_chunk:
        return
    last_player_chunk = cur

    needed = set()
    for dx in range(-LOAD_RADIUS, LOAD_RADIUS + 1):
        for dz in range(-LOAD_RADIUS, LOAD_RADIUS + 1):
            needed.add((pcx + dx, pcz + dz))

    to_load = sorted(needed - loaded_chunks,
                     key=lambda c: (c[0] - pcx) ** 2 + (c[1] - pcz) ** 2)
    for ck in to_load:
        load_chunk(*ck)

    for ck in loaded_chunks - needed:
        unload_chunk(*ck)

# ============================================================
# 9. 方块操作
# ============================================================
def chunk_key_of(pos):
    return (pos[0] // CHUNK_SIZE, pos[2] // CHUNK_SIZE)

def place_block(pos, bid):
    ck = chunk_key_of(pos)
    chunk_data.setdefault(ck, {})[pos] = bid
    if ck in loaded_chunks:
        old = block_entities.pop(pos, None)
        if old is not None:
            destroy(old)
        block_entities[pos] = Block(pos, bid)

def remove_block_data(pos):
    ck = chunk_key_of(pos)
    if ck in chunk_data:
        chunk_data[ck].pop(pos, None)
    block_entities.pop(pos, None)

# ============================================================
# 10. 天空 & 光照
# ============================================================
Sky()
sun = DirectionalLight(shadows=True)
sun.look_at(Vec3(1, -1, -1))
AmbientLight(color=color.rgba(160, 160, 160, 255))

# ============================================================
# 11. ★ 关键修复：先加载出生点区块，再创建玩家
# ============================================================
spawn_x, spawn_z = 0.5, 0.5
spawn_cx = floor(spawn_x) // CHUNK_SIZE
spawn_cz = floor(spawn_z) // CHUNK_SIZE

# 手动加载出生点周围 3x3 区块（玩家一出生脚下就有实体方块）
for dx in range(-1, 2):
    for dz in range(-1, 2):
        load_chunk(spawn_cx + dx, spawn_cz + dz)

print(f"[MC] 出生点区块已加载，共 {len(block_entities)} 个方块")

# 出生高度：地形高度 + 6（给几帧缓冲时间）
ground_h = terrain_height(0, 0)
start_y = ground_h + 6
print(f"[MC] 地面 y={ground_h}，出生 y={start_y}")

player = FirstPersonController(position=(spawn_x, start_y, spawn_z))
player.speed   = 8
player.gravity = 1.2
mouse.locked   = True

# 出生点记录（保底传送用）
SPAWN_POS = (spawn_x, ground_h + 3, spawn_z)

# ============================================================
# 12. 背包 UI
# ============================================================
Entity(parent=camera.ui, model='quad',
       color=color.rgba(0, 0, 0, 170),
       scale=(0.74, 0.11), position=(0, -0.42), z=0.2)

slot_bgs    = []
slot_counts = []

for i in range(7):
    bx = -0.30 + i * 0.10
    bg = Entity(parent=camera.ui, model='quad',
                color=color.rgba(70, 70, 70, 220),
                scale=(0.085, 0.085), position=(bx, -0.42), z=0.1)
    Entity(parent=camera.ui, model='quad',
           color=color.rgb(*BLOCK_RGB[i + 1]),
           scale=(0.068, 0.068), position=(bx, -0.42), z=0.05)
    cnt = Text(parent=camera.ui, text='0', scale=0.8, color=color.white,
               position=(bx + 0.024, -0.464), z=0.0)
    slot_bgs.append(bg)
    slot_counts.append(cnt)

Text(parent=camera.ui,
     text="WASD Move | SPACE Jump | SHIFT Sprint | LMB Break | RMB Place",
     position=(-0.85, 0.47), scale=0.6,
     background=True, background_color=color.rgba(0, 0, 0, 130))
Text(parent=camera.ui,
     text="1-7 Select | B Inventory | ESC Quit",
     position=(-0.85, 0.43), scale=0.6,
     background=True, background_color=color.rgba(0, 0, 0, 130))

info_text = Text(parent=camera.ui,
                 text=f"Block: {BLOCK_NAMES[1]}",
                 position=(0.55, 0.47), scale=0.75, color=color.yellow,
                 background=True, background_color=color.rgba(0, 0, 0, 130))

inventory_panel = Entity(parent=camera.ui, enabled=False)
Entity(parent=inventory_panel, model='quad',
       color=color.rgba(15, 15, 15, 235),
       scale=(0.60, 0.68), position=(0, 0), z=0.5)

Text(parent=inventory_panel, text="INVENTORY",
     origin=(0, 0), y=0.29, scale=1.6, color=color.yellow)
Text(parent=inventory_panel, text="Press B to close",
     origin=(0, 0), y=-0.31, scale=0.75, color=color.gray)

inv_row_counts = []
for i in range(7):
    y = 0.20 - i * 0.07
    Entity(parent=inventory_panel, model='quad',
           color=color.rgb(*BLOCK_RGB[i + 1]),
           scale=(0.045, 0.045), position=(-0.22, y), z=0.4)
    Text(parent=inventory_panel, text=BLOCK_NAMES[i + 1],
         position=(-0.16, y - 0.015), scale=0.95, color=color.white)
    ct = Text(parent=inventory_panel, text="0",
              position=(0.22, y - 0.015), scale=0.95, color=color.yellow)
    inv_row_counts.append(ct)

def refresh_inventory_ui():
    for i in range(7):
        bid = i + 1
        s = str(inventory.get(bid, 0))
        if slot_counts[i].text != s:
            slot_counts[i].text = s
        if inv_row_counts[i].text != s:
            inv_row_counts[i].text = s

def refresh_selection_ui():
    for i in range(7):
        if i + 1 == selected_block:
            slot_bgs[i].color = color.rgba(255, 235, 150, 240)
        else:
            slot_bgs[i].color = color.rgba(70, 70, 70, 220)

refresh_inventory_ui()
refresh_selection_ui()

# ============================================================
# 13. 放置合法性检测
# ============================================================
def can_place(pos):
    px, py, pz = player.x, player.y, player.z
    bx, by, bz = pos
    if (bx + 0.5 > px - 0.4 and bx - 0.5 < px + 0.4 and
        by + 0.5 > py       and by - 0.5 < py + 1.8 and
        bz + 0.5 > pz - 0.4 and bz - 0.5 < pz + 0.4):
        return False
    return True

# ============================================================
# 14. 输入
# ============================================================
def input(key):
    global selected_block

    if key == 'b':
        inventory_panel.enabled = not inventory_panel.enabled
        if inventory_panel.enabled:
            refresh_inventory_ui()
        return

    if key == 'escape':
        inventory_panel.enabled = False
        return

    if inventory_panel.enabled:
        return

    if key in '1234567':
        selected_block = int(key)
        info_text.text = f"Block: {BLOCK_NAMES[selected_block]}"
        refresh_selection_ui()
        return

    target = mouse.hovered_entity
    if target is None or not hasattr(target, 'block_id'):
        return

    if key == 'left mouse down':
        bid = target.block_id
        pos = target.grid_pos
        remove_block_data(pos)
        inventory[bid] = inventory.get(bid, 0) + 1
        refresh_inventory_ui()
        target.collider = None
        target.animate_scale(0.05, duration=0.1)
        destroy(target, delay=0.1)

    elif key == 'right mouse down':
        if inventory.get(selected_block, 0) <= 0:
            return
        n = mouse.normal
        nx = round(target.x + n.x - 0.5)
        ny = round(target.y + n.y - 0.5)
        nz = round(target.z + n.z - 0.5)
        pos = (nx, ny, nz)
        if pos in block_entities:
            return
        if not can_place(pos):
            return
        place_block(pos, selected_block)
        inventory[selected_block] -= 1
        refresh_inventory_ui()

# ============================================================
# 15. ★ 每帧更新：加保底传送
# ============================================================
def update():
    update_chunks()

    # 保底：掉出世界就传送回出生点
    if player.y < -20:
        player.position = SPAWN_POS
        # 清空下落速度（不同 Ursina 版本属性名不同，逐个尝试）
        for attr in ('velocity_y', 'falling_speed', 'air_time', 'vy'):
            if hasattr(player, attr):
                try:
                    setattr(player, attr, 0)
                except Exception:
                    pass
        print("[MC] 掉出世界，已传送回出生点")

# ============================================================
# 16. 启动
# ============================================================
app.run()