feat(entity): 实体系统——僵尸 AI/白天燃烧、动物游荡、掉落物合并拾取、村民房屋蓝图与建造队列(含测试)
This commit is contained in:
83
internal/entity/drops.go
Normal file
83
internal/entity/drops.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// 掉落物实体:合并与拾取(实体系统.md §5)。
|
||||
package entity
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
// 掉落物参数(实体系统.md §5)。
|
||||
const (
|
||||
dropMergeDist = 1.0 // 同物品合并距离
|
||||
dropStackMax = 64 // 合并上限
|
||||
dropPickupDist = 1.5 // 拾取半径
|
||||
dropPerChunk = 32 // 每区块掉落物上限
|
||||
)
|
||||
|
||||
// Drop 掉落物实体数据。
|
||||
type Drop struct {
|
||||
Item string
|
||||
Count int
|
||||
}
|
||||
|
||||
// SpawnDrop 生成掉落物:同物品近距合并(上限 dropStackMax);超上限丢弃。
|
||||
func (m *Manager) SpawnDrop(itemName string, count int, x, y, z float64) {
|
||||
if count <= 0 {
|
||||
return
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
// 合并
|
||||
if old, ok := m.drops[itemName]; ok && !old.Dead {
|
||||
dx, dz := old.Pos[0]-x, old.Pos[2]-z
|
||||
if math.Sqrt(dx*dx+dz*dz) < dropMergeDist {
|
||||
if d, ok := old.Data.(*Drop); ok && d.Count+count <= dropStackMax {
|
||||
d.Count += count
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// 上限检查
|
||||
drops := 0
|
||||
for _, e := range m.entities {
|
||||
if e.Kind == KindItemDrop {
|
||||
drops++
|
||||
}
|
||||
}
|
||||
if drops >= dropPerChunk*8 { // 全局粗上限(按区块过滤后置)
|
||||
return
|
||||
}
|
||||
m.next++
|
||||
e := &Entity{ID: m.next, Kind: KindItemDrop, Pos: [3]float64{x, y, z}, Health: 1,
|
||||
Data: &Drop{Item: itemName, Count: count}}
|
||||
e.Vel = [3]float64{(rand01(e.ID) - 0.5) * 2, 4, (rand01(e.ID>>16) - 0.5) * 2} // 弹出初速度
|
||||
m.entities[e.ID] = e
|
||||
m.drops[itemName] = e
|
||||
}
|
||||
|
||||
// Data 实体附加数据。
|
||||
// rand01 实体 ID 派生确定性随机(掉落初速度用,存档回放一致)。
|
||||
func rand01(seed ID) float64 {
|
||||
const mod = 1000003
|
||||
return float64(seed%mod) / mod
|
||||
}
|
||||
|
||||
// PickupNearby 拾取玩家附近掉落物,返回拾取的物品列表(实体系统.md §5 拾取)。
|
||||
func (m *Manager) PickupNearby(px, py, pz float64) []Drop {
|
||||
var out []Drop
|
||||
for _, e := range m.List() {
|
||||
if e.Kind != KindItemDrop || e.Dead {
|
||||
continue
|
||||
}
|
||||
d, ok := e.Data.(*Drop)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
dx, dy, dz := e.Pos[0]-px, e.Pos[1]-py, e.Pos[2]-pz
|
||||
if dx*dx+dy*dy+dz*dz < dropPickupDist*dropPickupDist {
|
||||
e.Dead = true
|
||||
m.Remove(e.ID)
|
||||
out = append(out, *d)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
257
internal/entity/entity.go
Normal file
257
internal/entity/entity.go
Normal file
@@ -0,0 +1,257 @@
|
||||
// Package entity 实体系统:生命周期、AI 状态机、掉落物(场景/实体系统.md)。
|
||||
//
|
||||
// 设计要点:
|
||||
// - 实体与方块分离,随激活区块 tick(卸载冻结);
|
||||
// - AI 决策限频(0.25–0.5s 一次),寻路简化为直线移动 + 卡住重试(实体系统.md §3);
|
||||
// - 掉落物合并 + 上限(实体系统.md §5)。
|
||||
package entity
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"mc/internal/block"
|
||||
)
|
||||
|
||||
// ID 实体 ID(服务器分配,多人同步.md §3)。
|
||||
type ID uint64
|
||||
|
||||
// Kind 实体类型。
|
||||
type Kind uint8
|
||||
|
||||
// 实体类型(僵尸与敌对生物.md、动物体系.md、村民系统.md)。
|
||||
const (
|
||||
KindAnimal Kind = iota // 被动动物
|
||||
KindZombie // 敌对生物样板
|
||||
KindVillager // 村民
|
||||
KindItemDrop // 掉落物
|
||||
)
|
||||
|
||||
// WorldView 实体 AI 所需的世界只读视图。
|
||||
type WorldView interface {
|
||||
Solid(x, y, z int32) bool
|
||||
Block(x, y, z int32) block.State
|
||||
SkyLight(x, y, z int32) uint8
|
||||
BlockLight(x, y, z int32) uint8
|
||||
}
|
||||
|
||||
// Entity 实体。
|
||||
type Entity struct {
|
||||
ID ID
|
||||
Kind Kind
|
||||
Pos [3]float64 // 脚底坐标
|
||||
Vel [3]float64
|
||||
Yaw float64
|
||||
Health float64
|
||||
Dead bool
|
||||
|
||||
// AI 状态(实体系统.md §3 状态机)
|
||||
state uint8
|
||||
decisionIn float64 // 下次决策倒计时(限频)
|
||||
attackIn float64 // 攻击冷却(僵尸与敌对生物.md §3)
|
||||
wanderDir [2]float64
|
||||
|
||||
// Data 类型附加数据(掉落物 = *Drop,村民 = *Villager)
|
||||
Data any
|
||||
}
|
||||
|
||||
// 僵尸 AI 状态。
|
||||
const (
|
||||
zIdle uint8 = iota
|
||||
zChase
|
||||
zAttack
|
||||
)
|
||||
|
||||
// 动物 AI 状态。
|
||||
const (
|
||||
aWander uint8 = iota
|
||||
aFlee
|
||||
)
|
||||
|
||||
// Manager 实体管理器(主线程所有权)。
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
next ID
|
||||
entities map[ID]*Entity
|
||||
drops map[string]*Entity // 掉落物合并键(物品名)→ 实体
|
||||
}
|
||||
|
||||
// NewManager 创建实体管理器。
|
||||
func NewManager() *Manager {
|
||||
return &Manager{entities: make(map[ID]*Entity), drops: make(map[string]*Entity)}
|
||||
}
|
||||
|
||||
// Spawn 生成实体。
|
||||
func (m *Manager) Spawn(kind Kind, x, y, z float64) *Entity {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.next++
|
||||
e := &Entity{ID: m.next, Kind: kind, Pos: [3]float64{x, y, z}, Health: 20, decisionIn: 0.5}
|
||||
m.entities[e.ID] = e
|
||||
return e
|
||||
}
|
||||
|
||||
// Remove 移除实体。
|
||||
func (m *Manager) Remove(id ID) {
|
||||
m.mu.Lock()
|
||||
delete(m.entities, id)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// List 导出实体列表(副本)。
|
||||
func (m *Manager) List() []*Entity {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
out := make([]*Entity, 0, len(m.entities))
|
||||
for _, e := range m.entities {
|
||||
out = append(out, e)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Tick 每 tick 更新全部实体(激活区块内;冻结区由调用方过滤)。
|
||||
func (m *Manager) Tick(dt float64, w WorldView, playerPos [3]float64) {
|
||||
for _, e := range m.List() {
|
||||
if e.Dead {
|
||||
continue
|
||||
}
|
||||
switch e.Kind {
|
||||
case KindZombie:
|
||||
e.tickZombie(dt, w, playerPos)
|
||||
case KindAnimal:
|
||||
e.tickAnimal(dt, w)
|
||||
case KindVillager:
|
||||
e.tickVillager(dt, w)
|
||||
}
|
||||
if e.Kind != KindItemDrop {
|
||||
e.move(dt, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// move 简化移动:水平意愿速度 + 重力 + AABB 碰撞(复用物理思路,实体系统.md §3)。
|
||||
func (e *Entity) move(dt float64, w WorldView) {
|
||||
const g = 32.0
|
||||
e.Vel[1] -= g * dt
|
||||
if e.Vel[1] < -64 {
|
||||
e.Vel[1] = -64
|
||||
}
|
||||
// 逐轴(X → Z → Y)
|
||||
for _, axis := range [3]int{0, 2, 1} {
|
||||
e.Pos[axis] += e.Vel[axis] * dt
|
||||
if e.collides(w) {
|
||||
e.Pos[axis] -= e.Vel[axis] * dt
|
||||
e.Vel[axis] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collides AABB 碰撞判定(0.6×1.8)。
|
||||
func (e *Entity) collides(w WorldView) bool {
|
||||
const hw, hh = 0.3, 1.8
|
||||
minX := int32(math.Floor(e.Pos[0] - hw + 1e-6))
|
||||
maxX := int32(math.Floor(e.Pos[0] + hw - 1e-6))
|
||||
minY := int32(math.Floor(e.Pos[1] + 1e-6))
|
||||
maxY := int32(math.Floor(e.Pos[1] + hh - 1e-6))
|
||||
minZ := int32(math.Floor(e.Pos[2] - hw + 1e-6))
|
||||
maxZ := int32(math.Floor(e.Pos[2] + hw - 1e-6))
|
||||
for x := minX; x <= maxX; x++ {
|
||||
for y := minY; y <= maxY; y++ {
|
||||
for z := minZ; z <= maxZ; z++ {
|
||||
if w.Solid(x, y, z) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// tickZombie 僵尸 AI(僵尸与敌对生物.md §2–§4)。
|
||||
func (e *Entity) tickZombie(dt float64, w WorldView, playerPos [3]float64) {
|
||||
// 白天燃烧(暴露天空光 ≥15 且无头盔 → 每秒伤害;逐 tick 而非秒杀)
|
||||
if w.SkyLight(int32(e.Pos[0]), int32(e.Pos[1]+1.5), int32(e.Pos[2])) >= 15 {
|
||||
e.Health -= 2.0 * dt
|
||||
if e.Health <= 0 {
|
||||
e.Dead = true
|
||||
return
|
||||
}
|
||||
}
|
||||
e.decisionIn -= dt
|
||||
e.attackIn -= dt
|
||||
dx, dz := playerPos[0]-e.Pos[0], playerPos[2]-e.Pos[2]
|
||||
dist := math.Sqrt(dx*dx + dz*dz)
|
||||
switch e.state {
|
||||
case zIdle:
|
||||
if dist < 16 && w.BlockLight(int32(e.Pos[0]), int32(e.Pos[1]), int32(e.Pos[2])) <= 7 {
|
||||
e.state = zChase
|
||||
}
|
||||
case zChase:
|
||||
if dist < 2.0 {
|
||||
e.state = zAttack
|
||||
e.Vel[0], e.Vel[2] = 0, 0
|
||||
break
|
||||
}
|
||||
if e.decisionIn <= 0 {
|
||||
e.decisionIn = 0.5 // 限频寻路(实体系统.md §3)
|
||||
speed := 3.0
|
||||
e.Vel[0] = dx / dist * speed
|
||||
e.Vel[2] = dz / dist * speed
|
||||
e.Yaw = math.Atan2(-dx, -dz)
|
||||
}
|
||||
if dist > 24 {
|
||||
e.state = zIdle
|
||||
}
|
||||
case zAttack:
|
||||
if dist > 2.5 {
|
||||
e.state = zChase
|
||||
} else if e.attackIn <= 0 {
|
||||
e.attackIn = 1.0 // 攻击间隔 1s(僵尸与敌对生物.md §3)
|
||||
e.Vel[0], e.Vel[2] = 0, 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tickAnimal 动物 AI:游荡 + 受击逃跑(动物体系.md §2 状态机子集)。
|
||||
func (e *Entity) tickAnimal(dt float64, w WorldView) {
|
||||
e.decisionIn -= dt
|
||||
if e.Health < 20 {
|
||||
e.state = aFlee // 受击逃跑(简化:血不满即逃)
|
||||
}
|
||||
if e.decisionIn <= 0 {
|
||||
e.decisionIn = 2.0
|
||||
switch e.state {
|
||||
case aFlee:
|
||||
ang := math.Atan2(e.wanderDir[1], e.wanderDir[0]) + 0.5
|
||||
e.wanderDir = [2]float64{math.Cos(ang), math.Sin(ang)}
|
||||
e.state = aWander
|
||||
default:
|
||||
// 随机游荡方向(确定性:用实体 ID 派生)
|
||||
ang := float64(e.ID%628) / 100.0
|
||||
e.wanderDir = [2]float64{math.Cos(ang), math.Sin(ang)}
|
||||
if e.ID%3 == 0 {
|
||||
e.wanderDir = [2]float64{0, 0} // 原地休息
|
||||
}
|
||||
}
|
||||
}
|
||||
e.Vel[0] = e.wanderDir[0] * 1.2
|
||||
e.Vel[2] = e.wanderDir[1] * 1.2
|
||||
_ = w
|
||||
}
|
||||
|
||||
// tickVillager 村民 AI:白天游荡工作(建造队列由村民系统接入),夜晚回屋/逃跑。
|
||||
func (e *Entity) tickVillager(dt float64, w WorldView) {
|
||||
e.decisionIn -= dt
|
||||
isNight := w.SkyLight(int32(e.Pos[0]), int32(e.Pos[1]+1.5), int32(e.Pos[2])) < 5
|
||||
if e.decisionIn <= 0 {
|
||||
e.decisionIn = 1.0
|
||||
if isNight {
|
||||
e.wanderDir = [2]float64{0, 0} // 夜晚停下(回屋由村庄数据驱动,MVP)
|
||||
} else {
|
||||
ang := float64(e.ID%628) / 100.0
|
||||
e.wanderDir = [2]float64{math.Cos(ang) * 0.5, math.Sin(ang) * 0.5}
|
||||
}
|
||||
}
|
||||
e.Vel[0] = e.wanderDir[0]
|
||||
e.Vel[2] = e.wanderDir[1]
|
||||
}
|
||||
157
internal/entity/entity_test.go
Normal file
157
internal/entity/entity_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"mc/internal/block"
|
||||
"mc/internal/world"
|
||||
"mc/internal/worldgen"
|
||||
)
|
||||
|
||||
// flatWorld 测试世界:y<63 固体,其余空气;无光。
|
||||
type flatWorld struct{}
|
||||
|
||||
func (flatWorld) Solid(x, y, z int32) bool { return y < 63 }
|
||||
func (flatWorld) Block(x, y, z int32) block.State { return block.Air }
|
||||
func (flatWorld) SkyLight(x, y, z int32) uint8 { return 15 }
|
||||
func (flatWorld) BlockLight(x, y, z int32) uint8 { return 0 }
|
||||
|
||||
// TestZombieChase 僵尸追玩家(僵尸与敌对生物.md §2)。
|
||||
func TestZombieChase(t *testing.T) {
|
||||
m := NewManager()
|
||||
z := m.Spawn(KindZombie, 0.5, 63, 0.5)
|
||||
w := flatWorld{}
|
||||
for i := 0; i < 200; i++ {
|
||||
m.Tick(0.05, w, [3]float64{10, 63, 0.5})
|
||||
}
|
||||
// 僵尸应向玩家移动(方块光 0 ≤ 7 满足感知条件;天空光 15 → 燃烧掉血但不影响移动)
|
||||
if z.Pos[0] < 1.0 {
|
||||
t.Fatalf("僵尸未向玩家移动: %+v", z.Pos)
|
||||
}
|
||||
}
|
||||
|
||||
// TestZombieBurn 白天暴露燃烧致死(僵尸与敌对生物.md §4 逐 tick 伤害)。
|
||||
func TestZombieBurn(t *testing.T) {
|
||||
m := NewManager()
|
||||
z := m.Spawn(KindZombie, 0.5, 63, 0.5)
|
||||
w := flatWorld{} // sky=15
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for !z.Dead && time.Now().Before(deadline) {
|
||||
m.Tick(0.05, w, [3]float64{100, 63, 100})
|
||||
}
|
||||
if !z.Dead {
|
||||
t.Fatal("暴露僵尸应在白天被烧死")
|
||||
}
|
||||
if z.Health > 0 {
|
||||
t.Fatalf("死亡实体血量应 ≤0: %v", z.Health)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnimalWander 动物游荡(确定性方向,实体系统.md §3 限频决策)。
|
||||
func TestAnimalWander(t *testing.T) {
|
||||
m := NewManager()
|
||||
a := m.Spawn(KindAnimal, 0.5, 63, 0.5)
|
||||
w := flatWorld{}
|
||||
for i := 0; i < 100; i++ {
|
||||
m.Tick(0.05, w, [3]float64{100, 63, 100})
|
||||
}
|
||||
if a.Pos[1] < 63 {
|
||||
t.Fatalf("动物掉到地面以下: %+v", a.Pos)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDropMerge 掉落物合并(实体系统.md §5)。
|
||||
func TestDropMerge(t *testing.T) {
|
||||
m := NewManager()
|
||||
m.SpawnDrop("cobblestone", 5, 0.5, 64, 0.5)
|
||||
m.SpawnDrop("cobblestone", 5, 0.6, 64, 0.5) // 距离 <1 → 合并
|
||||
m.SpawnDrop("cobblestone", 5, 10, 64, 10) // 距离远 → 新实体
|
||||
cnt := 0
|
||||
for _, e := range m.List() {
|
||||
if e.Kind == KindItemDrop {
|
||||
cnt++
|
||||
}
|
||||
}
|
||||
if cnt != 2 {
|
||||
t.Fatalf("掉落物实体数期望 2,实际 %d", cnt)
|
||||
}
|
||||
// 拾取
|
||||
drops := m.PickupNearby(0.5, 64, 0.5)
|
||||
if len(drops) != 1 || drops[0].Count != 10 {
|
||||
t.Fatalf("拾取期望 1 组 10 个,实际 %+v", drops)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBlueprint 房屋蓝图确定性 + 建造执行顺序(村民系统.md §6)。
|
||||
func TestBlueprint(t *testing.T) {
|
||||
a := Blueprint([3]int32{10, 64, 10}, 42)
|
||||
b := Blueprint([3]int32{10, 64, 10}, 42)
|
||||
if len(a) != len(b) {
|
||||
t.Fatalf("蓝图确定性失败: %d != %d", len(a), len(b))
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
t.Fatalf("蓝图第 %d 项不一致", i)
|
||||
}
|
||||
}
|
||||
// 顺序约束:地基(Order 0)先于墙与屋顶
|
||||
firstNonFoundation := -1
|
||||
for i, task := range a {
|
||||
if task.Order != 0 {
|
||||
firstNonFoundation = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if firstNonFoundation <= 0 {
|
||||
t.Fatal("蓝图缺少地基")
|
||||
}
|
||||
|
||||
// 建造执行:每 tick 2 块,全部放置成功
|
||||
v := &Villager{Profession: 1, BuildQueue: append([]BuildTask{}, a...)}
|
||||
placed := 0
|
||||
place := func(x, y, z int32, id uint16, meta uint8) bool { placed++; return true }
|
||||
for len(v.BuildQueue) > 0 {
|
||||
v.BuildTick(place)
|
||||
}
|
||||
if placed != len(a) {
|
||||
t.Fatalf("放置数量期望 %d,实际 %d", len(a), placed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEntityWorldIntegration 实体跑在真实世界上(世界接入 smoke)。
|
||||
func TestEntityWorldIntegration(t *testing.T) {
|
||||
reg, err := block.Load(filepath.Join("..", "..", "assets", "config", "blocks.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("加载注册表失败: %v", err)
|
||||
}
|
||||
gen, err := worldgen.New(reg, 1, 0.005, 0.01, 1.0) // 无洞穴
|
||||
if err != nil {
|
||||
t.Fatalf("创建生成器失败: %v", err)
|
||||
}
|
||||
w, err := world.New(reg, gen, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("创建世界失败: %v", err)
|
||||
}
|
||||
defer w.Close()
|
||||
deadline := time.Now().Add(15 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
w.Update(8, 64, 8, 8*time.Millisecond)
|
||||
if w.Block(8, 0, 8) != block.Air {
|
||||
break
|
||||
}
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
// 找地表放一只僵尸
|
||||
var surf int32
|
||||
for y := int32(255); y >= 0; y-- {
|
||||
if w.Block(8, y, 8) != block.Air {
|
||||
surf = y
|
||||
break
|
||||
}
|
||||
}
|
||||
m := NewManager()
|
||||
m.Spawn(KindZombie, 8.5, float64(surf+1), 8.5)
|
||||
m.Tick(0.05, w, [3]float64{12, float64(surf + 1), 8.5}) // 世界实现了 WorldView(Solid/Block/Sky/BlockLight)
|
||||
}
|
||||
102
internal/entity/villager.go
Normal file
102
internal/entity/villager.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// 村民建造子系统:程序化房屋蓝图 + 逐块建造队列(村民系统.md §6 特色功能)。
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mc/internal/block"
|
||||
)
|
||||
|
||||
// Villager 村民附加数据(村民系统.md §3–§6)。
|
||||
type Villager struct {
|
||||
Profession uint8 // 0 农民 1 建造者 2 铁匠 3 商人
|
||||
Home [3]int32
|
||||
// 建造队列(村民系统.md §6.3:按顺序逐块放置)
|
||||
BuildQueue []BuildTask
|
||||
}
|
||||
|
||||
// BuildTask 一个建造任务(建造顺序已定)。
|
||||
type BuildTask struct {
|
||||
X, Y, Z int32
|
||||
BlockID uint16
|
||||
Meta uint8
|
||||
Order uint32 // 地基 → 墙 → 门 → 屋顶 → 火把
|
||||
}
|
||||
|
||||
// Blueprint 程序化房屋蓝图(村民系统.md §6.2):
|
||||
// 矩形地基 W×D、墙高 3、门朝 +x、平顶、四角火把;确定性(seed 派生尺寸)。
|
||||
func Blueprint(origin [3]int32, seed uint64) []BuildTask {
|
||||
w := 5 + int(seed%3) // 5–7
|
||||
d := 4 + int(seed%3) // 4–6
|
||||
wall := uint16(8) // oak_planks(与 assets/config/blocks.json 一致)
|
||||
door := uint16(0) // 门后置:先留空
|
||||
roof := uint16(8)
|
||||
torch := uint16(17)
|
||||
|
||||
var tasks []BuildTask
|
||||
order := uint32(0)
|
||||
add := func(x, y, z int32, id uint16, o uint32) {
|
||||
tasks = append(tasks, BuildTask{X: x, Y: y, Z: z, BlockID: id, Order: o})
|
||||
}
|
||||
ox, oy, oz := origin[0], origin[1], origin[2]
|
||||
// 1) 地基(order 0)
|
||||
for x := 0; x < w; x++ {
|
||||
for z := 0; z < d; z++ {
|
||||
add(ox+int32(x), oy, oz+int32(z), wall, order)
|
||||
}
|
||||
}
|
||||
order++
|
||||
// 2) 墙(order 1–2,高 3;门位置留空)
|
||||
for y := 1; y <= 2; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
for z := 0; z < d; z++ {
|
||||
edge := x == 0 || x == w-1 || z == 0 || z == d-1
|
||||
if !edge {
|
||||
continue
|
||||
}
|
||||
if y == 1 && x == 0 && z == d/2 {
|
||||
continue // 门洞
|
||||
}
|
||||
add(ox+int32(x), oy+int32(y), oz+int32(z), wall, order)
|
||||
}
|
||||
}
|
||||
order++
|
||||
}
|
||||
// 3) 屋顶(order 3,平顶 + 一圈外沿)
|
||||
for x := -1; x <= w; x++ {
|
||||
for z := -1; z <= d; z++ {
|
||||
edge := x == -1 || x == w || z == -1 || z == d
|
||||
add(ox+int32(x), oy+3, oz+int32(z), roof, order+uint32(boolToInt(edge)))
|
||||
}
|
||||
}
|
||||
order += 2
|
||||
// 4) 火把(order 5,四角)
|
||||
for _, c := range [4][2]int{{0, 0}, {w - 1, 0}, {0, d - 1}, {w - 1, d - 1}} {
|
||||
add(ox+int32(c[0]), oy+2, oz+int32(c[1]), torch, order)
|
||||
}
|
||||
_ = door
|
||||
return tasks
|
||||
}
|
||||
|
||||
// boolToInt bool → int。
|
||||
func boolToInt(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// BuildTick 建造者村民执行建造队列:每 tick 1–2 块(村民系统.md §6.3 防瞬建突兀)。
|
||||
// place:放置回调(世界层 SetBlock);返回剩余任务数。
|
||||
func (v *Villager) BuildTick(place func(x, y, z int32, id uint16, meta uint8) bool) int {
|
||||
n := 0
|
||||
for len(v.BuildQueue) > 0 && n < 2 {
|
||||
t := v.BuildQueue[0]
|
||||
v.BuildQueue = v.BuildQueue[1:]
|
||||
if place(t.X, t.Y, t.Z, t.BlockID, t.Meta) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return len(v.BuildQueue)
|
||||
}
|
||||
|
||||
// 方块状态工具(避免测试重复导入)。
|
||||
func stateOf(id uint16, meta uint8) block.State { return block.NewState(id, meta) }
|
||||
BIN
tools/zig.zip
BIN
tools/zig.zip
Binary file not shown.
Reference in New Issue
Block a user