feat(world): 方块注册表、坐标工具、区块数据结构、确定性世界生成(含测试)
This commit is contained in:
142
internal/block/registry.go
Normal file
142
internal/block/registry.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Package block 方块注册表:加载 blocks.json,提供方块定义与状态查询(设计.md §6.1)。
|
||||
//
|
||||
// 设计要点:
|
||||
// - 运行时状态 = 数字 ID(uint16)+ meta(uint8),打包为 State(uint32);
|
||||
// - ID 一旦发布不可变(ID 稳定性铁律,热更新.md §4.4);
|
||||
// - isSolid / isOpaque / light 等判定为「单一来源」,光照、物理、渲染共用。
|
||||
package block
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// State 运行时方块状态:ID(低 16 位)+ meta(高 8 位)。
|
||||
type State uint32
|
||||
|
||||
// Air 空气(ID 0)。
|
||||
const Air State = 0
|
||||
|
||||
// NewState 打包状态。
|
||||
func NewState(id uint16, meta uint8) State { return State(uint32(id) | uint32(meta)<<16) }
|
||||
|
||||
// ID 返回方块 ID。
|
||||
func (s State) ID() uint16 { return uint16(s & 0xFFFF) }
|
||||
|
||||
// Meta 返回附加数据(朝向/含水/阶段等)。
|
||||
func (s State) Meta() uint8 { return uint8(s >> 16) }
|
||||
|
||||
// Def 方块定义(blocks.json 条目,设计.md §2.3)。
|
||||
type Def struct {
|
||||
ID uint16 `json:"id"`
|
||||
Name string `json:"-"`
|
||||
Hardness float64 `json:"hardness"`
|
||||
Resistance float64 `json:"resistance"`
|
||||
Light uint8 `json:"light"`
|
||||
Transparent bool `json:"transparent"`
|
||||
Solid bool `json:"solid"`
|
||||
Cutout bool `json:"cutout"`
|
||||
Translucent bool `json:"translucent"`
|
||||
Fluid bool `json:"fluid"`
|
||||
RequiredTier int `json:"required_tier"`
|
||||
Drops string `json:"drops"`
|
||||
Textures map[string]string `json:"textures"`
|
||||
}
|
||||
|
||||
// Registry 方块注册表。
|
||||
type Registry struct {
|
||||
byID []Def
|
||||
byName map[string]uint16
|
||||
}
|
||||
|
||||
// Load 解析 blocks.json 构建注册表(按 id 索引;ID 0 保留给空气,其余不要求连续但必须有唯一)。
|
||||
func Load(path string) (*Registry, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("block.Load %s: %w", path, err)
|
||||
}
|
||||
var doc struct {
|
||||
Blocks map[string]Def `json:"blocks"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
return nil, fmt.Errorf("block.Load %s: %w", path, err)
|
||||
}
|
||||
r := &Registry{byName: make(map[string]uint16, len(doc.Blocks))}
|
||||
maxID := uint16(0)
|
||||
for name, d := range doc.Blocks {
|
||||
if d.ID == 0 {
|
||||
return nil, fmt.Errorf("block.Load: 方块 %q 使用保留 ID 0(空气)", name)
|
||||
}
|
||||
if d.ID > maxID {
|
||||
maxID = d.ID
|
||||
}
|
||||
if _, dup := r.byName[name]; dup {
|
||||
return nil, fmt.Errorf("block.Load: 方块重名 %q", name)
|
||||
}
|
||||
r.byName[name] = d.ID
|
||||
}
|
||||
// 按 ID 建立索引(下标 0 = 空气零值)
|
||||
r.byID = make([]Def, int(maxID)+1)
|
||||
for name, d := range doc.Blocks {
|
||||
d.Name = name
|
||||
if r.byID[d.ID].ID != 0 {
|
||||
return nil, fmt.Errorf("block.Load: ID %d 被重复使用", d.ID)
|
||||
}
|
||||
r.byID[d.ID] = d
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Get 按 ID 取定义;越界返回零值(视为空气)。
|
||||
func (r *Registry) Get(id uint16) Def {
|
||||
if int(id) < len(r.byID) {
|
||||
return r.byID[id]
|
||||
}
|
||||
return Def{}
|
||||
}
|
||||
|
||||
// ID 按名称取 ID。
|
||||
func (r *Registry) ID(name string) (uint16, bool) {
|
||||
id, ok := r.byName[name]
|
||||
return id, ok
|
||||
}
|
||||
|
||||
// Name 按 ID 取名称。
|
||||
func (r *Registry) Name(id uint16) string { return r.Get(id).Name }
|
||||
|
||||
// IsSolid 实体可站立/碰撞判定(场景/物理与碰撞.md §2)。
|
||||
func (r *Registry) IsSolid(s State) bool { return s != Air && r.Get(s.ID()).Solid }
|
||||
|
||||
// IsOpaque 完全遮光判定(场景/光照.md §7 分类表)。
|
||||
func (r *Registry) IsOpaque(s State) bool {
|
||||
if s == Air {
|
||||
return false
|
||||
}
|
||||
d := r.Get(s.ID())
|
||||
return !d.Transparent
|
||||
}
|
||||
|
||||
// Light 光源亮度(0–15,场景/光照.md §4)。
|
||||
func (r *Registry) Light(s State) uint8 { return r.Get(s.ID()).Light }
|
||||
|
||||
// IsCutout alpha test 类型(树叶/玻璃,渲染.md §6)。
|
||||
func (r *Registry) IsCutout(s State) bool { return r.Get(s.ID()).Cutout }
|
||||
|
||||
// IsTranslucent 半透明类型(水/彩色玻璃,需要排序 pass)。
|
||||
func (r *Registry) IsTranslucent(s State) bool { return r.Get(s.ID()).Translucent }
|
||||
|
||||
// IsFluid 液体判定(水/岩浆)。
|
||||
func (r *Registry) IsFluid(s State) bool { return r.Get(s.ID()).Fluid }
|
||||
|
||||
// Texture 取方块某个面的纹理键(如 "up"/"side"/"all")。
|
||||
func (r *Registry) Texture(s State, face string) string {
|
||||
d := r.Get(s.ID())
|
||||
if t, ok := d.Textures[face]; ok {
|
||||
return t
|
||||
}
|
||||
if t, ok := d.Textures["all"]; ok {
|
||||
return t
|
||||
}
|
||||
return "block/" + d.Name
|
||||
}
|
||||
73
internal/block/registry_test.go
Normal file
73
internal/block/registry_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package block
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// blocksPath 指向仓库 assets/config/blocks.json(测试运行目录为 internal/block)。
|
||||
var blocksPath = filepath.Join("..", "..", "assets", "config", "blocks.json")
|
||||
|
||||
// TestLoad 验证注册表加载与 ID 解析。
|
||||
func TestLoad(t *testing.T) {
|
||||
r, err := Load(blocksPath)
|
||||
if err != nil {
|
||||
t.Fatalf("加载注册表失败: %v", err)
|
||||
}
|
||||
id, ok := r.ID("stone")
|
||||
if !ok || id != 1 {
|
||||
t.Fatalf("stone ID 期望 1,实际 %d (ok=%v)", id, ok)
|
||||
}
|
||||
if r.Name(1) != "stone" {
|
||||
t.Fatalf("ID 1 名称期望 stone,实际 %q", r.Name(1))
|
||||
}
|
||||
}
|
||||
|
||||
// TestStates 验证状态打包/解包与判定表(单一来源)。
|
||||
func TestStates(t *testing.T) {
|
||||
r, err := Load(blocksPath)
|
||||
if err != nil {
|
||||
t.Fatalf("加载注册表失败: %v", err)
|
||||
}
|
||||
torch, _ := r.ID("torch")
|
||||
water, _ := r.ID("water")
|
||||
stone, _ := r.ID("stone")
|
||||
|
||||
s := NewState(torch, 3)
|
||||
if s.ID() != torch || s.Meta() != 3 {
|
||||
t.Fatalf("状态打包/解包异常: %+v", s)
|
||||
}
|
||||
if r.Light(s) != 14 {
|
||||
t.Fatalf("火把亮度期望 14,实际 %d", r.Light(s))
|
||||
}
|
||||
if r.IsOpaque(s) {
|
||||
t.Fatal("火把不应完全遮光")
|
||||
}
|
||||
if !r.IsOpaque(NewState(stone, 0)) {
|
||||
t.Fatal("石头应完全遮光")
|
||||
}
|
||||
if r.IsSolid(NewState(water, 0)) {
|
||||
t.Fatal("水不应为实体碰撞方块")
|
||||
}
|
||||
if !r.IsFluid(NewState(water, 0)) {
|
||||
t.Fatal("水应为液体")
|
||||
}
|
||||
if Air.ID() != 0 {
|
||||
t.Fatalf("空气 ID 期望 0")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTexture 验证纹理键解析(all 面回退)。
|
||||
func TestTexture(t *testing.T) {
|
||||
r, err := Load(blocksPath)
|
||||
if err != nil {
|
||||
t.Fatalf("加载注册表失败: %v", err)
|
||||
}
|
||||
grass, _ := r.ID("grass_block")
|
||||
if tex := r.Texture(NewState(grass, 0), "up"); tex != "block/grass_block_top" {
|
||||
t.Fatalf("草方块顶面纹理期望 block/grass_block_top,实际 %q", tex)
|
||||
}
|
||||
if tex := r.Texture(NewState(grass, 0), "down"); tex != "block/dirt" {
|
||||
t.Fatalf("草方块底面纹理期望 block/dirt,实际 %q", tex)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user