320 lines
8.9 KiB
Go
320 lines
8.9 KiB
Go
// 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"
|
||
"strings"
|
||
)
|
||
|
||
// 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"`
|
||
SoundGroup string `json:"sound_group"`
|
||
Drops DropList `json:"drops"`
|
||
NoDrop bool `json:"no_drop"` // 破坏不掉落(流体/基岩/玻璃)
|
||
Textures map[string]string `json:"textures"`
|
||
Tint string `json:"tint"` // 植被染色表:grass/foliage(渲染.md §4.1)
|
||
}
|
||
|
||
// DropRule 一条掉落规则(挖矿与矿物.md §4)。
|
||
type DropRule struct {
|
||
Item string `json:"item"`
|
||
Min int `json:"min"`
|
||
Max int `json:"max"`
|
||
Chance float64 `json:"chance"` // 0 表示 1;否则 0–1
|
||
Tool string `json:"tool"`
|
||
RequiredTier int `json:"required_tier"`
|
||
OrElse string `json:"or_else"`
|
||
}
|
||
|
||
// DropList 支持 JSON 字符串(旧式 "cobblestone")或规则数组。
|
||
type DropList []DropRule
|
||
|
||
// UnmarshalJSON 兼容 "drops": "dirt" 与 "drops": [{...}]。
|
||
func (d *DropList) UnmarshalJSON(b []byte) error {
|
||
if len(b) == 0 || string(b) == "null" {
|
||
return nil
|
||
}
|
||
if b[0] == '"' {
|
||
var s string
|
||
if err := json.Unmarshal(b, &s); err != nil {
|
||
return err
|
||
}
|
||
*d = DropList{{Item: s, Min: 1, Max: 1}}
|
||
return nil
|
||
}
|
||
var arr []DropRule
|
||
if err := json.Unmarshal(b, &arr); err != nil {
|
||
return err
|
||
}
|
||
*d = arr
|
||
return nil
|
||
}
|
||
|
||
// DropResult 一次掷骰结果。
|
||
type DropResult struct {
|
||
Item string
|
||
Count int
|
||
}
|
||
|
||
// RollDrops 按工具种类/等级掷掉落(创造模式由调用方跳过)。
|
||
func (d Def) RollDrops(toolType string, toolTier int, rnd func() float64) []DropResult {
|
||
if d.NoDrop {
|
||
return nil
|
||
}
|
||
if rnd == nil {
|
||
rnd = func() float64 { return 1 }
|
||
}
|
||
if len(d.Drops) == 0 {
|
||
if d.Name == "" || d.Fluid {
|
||
return nil
|
||
}
|
||
return []DropResult{{Item: d.Name, Count: 1}}
|
||
}
|
||
var out []DropResult
|
||
for _, r := range d.Drops {
|
||
if r.Tool != "" && r.Tool != toolType {
|
||
continue
|
||
}
|
||
if r.RequiredTier > 0 && toolTier < r.RequiredTier {
|
||
continue
|
||
}
|
||
ch := r.Chance
|
||
if ch <= 0 {
|
||
ch = 1
|
||
}
|
||
itemName := r.Item
|
||
if rnd() > ch {
|
||
if r.OrElse == "" {
|
||
continue
|
||
}
|
||
itemName = r.OrElse
|
||
}
|
||
if itemName == "" {
|
||
continue
|
||
}
|
||
minN, maxN := r.Min, r.Max
|
||
if minN <= 0 {
|
||
minN = 1
|
||
}
|
||
if maxN < minN {
|
||
maxN = minN
|
||
}
|
||
n := minN
|
||
if maxN > minN {
|
||
span := maxN - minN + 1
|
||
n = minN + int(rnd()*float64(span))
|
||
if n > maxN {
|
||
n = maxN
|
||
}
|
||
}
|
||
out = append(out, DropResult{Item: itemName, Count: n})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// BreakSound / PlaceSound / HitSound / StepSound 按 sound_group 发事件(音频.md §6.1)。
|
||
func (d Def) soundGroup() string {
|
||
if d.SoundGroup != "" {
|
||
return d.SoundGroup
|
||
}
|
||
return "stone"
|
||
}
|
||
func (d Def) BreakSound() string { return "block." + d.soundGroup() + ".break" }
|
||
func (d Def) PlaceSound() string { return "block." + d.soundGroup() + ".place" }
|
||
func (d Def) HitSound() string { return "block." + d.soundGroup() + ".hit" }
|
||
func (d Def) StepSound() string { return "block." + d.soundGroup() + ".step" }
|
||
|
||
// 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{}
|
||
}
|
||
|
||
// All 返回全部方块定义(下标即 ID,含空槽 0——小地图配色表等遍历用)。
|
||
func (r *Registry) All() []Def { return r.byID }
|
||
|
||
// 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")。
|
||
// 原版 blocks.json 惯例:四个侧面统一用 "side"(南面可用 "front" 覆盖,如熔炉)。
|
||
// mesh 层传入 east/west/south/north 方位名,这里回退到 side/front,
|
||
// 否则 grass_block 等立面会落到 "block/<name>"(图集中不存在)→ 全图集灰块。
|
||
func (r *Registry) Texture(s State, face string) string {
|
||
d := r.Get(s.ID())
|
||
if t, ok := d.Textures[face]; ok {
|
||
return t
|
||
}
|
||
switch face {
|
||
case "south": // 南面优先 front(熔炉门面),其次 side
|
||
if t, ok := d.Textures["front"]; ok {
|
||
return t
|
||
}
|
||
if t, ok := d.Textures["side"]; ok {
|
||
return t
|
||
}
|
||
case "east", "west", "north":
|
||
if t, ok := d.Textures["side"]; ok {
|
||
return t
|
||
}
|
||
}
|
||
if t, ok := d.Textures["all"]; ok {
|
||
return t
|
||
}
|
||
return "block/" + d.Name
|
||
}
|
||
|
||
// TintTextureKeys 返回「染色表名 → 涉及纹理键」:图集构建后按染色表给这些
|
||
// 纹理乘色(渲染.md §4.1)。跳过被无 tint 方块共享的纹理(dirt)和已着色侧面。
|
||
func (r *Registry) TintTextureKeys() map[string][]string {
|
||
shared := make(map[string]struct{})
|
||
for _, d := range r.byID {
|
||
if d.ID == 0 || d.Tint != "" {
|
||
continue
|
||
}
|
||
for _, k := range d.Textures {
|
||
shared[k] = struct{}{}
|
||
}
|
||
}
|
||
out := make(map[string][]string)
|
||
for _, d := range r.byID {
|
||
if d.ID == 0 || d.Tint == "" {
|
||
continue
|
||
}
|
||
seen := make(map[string]struct{})
|
||
for _, k := range d.Textures {
|
||
if _, dup := seen[k]; dup {
|
||
continue
|
||
}
|
||
if !tintableKey(d.Tint, k, shared) {
|
||
continue
|
||
}
|
||
seen[k] = struct{}{}
|
||
out[d.Tint] = append(out[d.Tint], k)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func tintableKey(tint, key string, shared map[string]struct{}) bool {
|
||
if _, ok := shared[key]; ok {
|
||
return false
|
||
}
|
||
if strings.Contains(key, "dirt") {
|
||
return false
|
||
}
|
||
if tint == "grass" && strings.Contains(key, "side") {
|
||
return false
|
||
}
|
||
return true
|
||
}
|