2026-08-15 22:47:21 +08:00
|
|
|
|
// Package game 游戏会话:输入 → 玩家 → 世界 → 交互(破坏/放置)的粘合层(架构.md §3 主循环)。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 职责边界:会话只做调度与玩法逻辑,渲染/UI/网络通过接口或外部驱动访问。
|
|
|
|
|
|
package game
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-08-15 23:48:58 +08:00
|
|
|
|
"math"
|
|
|
|
|
|
"math/rand"
|
2026-08-15 22:47:21 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"mc/internal/block"
|
2026-08-15 23:48:58 +08:00
|
|
|
|
"mc/internal/blockentity"
|
2026-08-15 22:47:21 +08:00
|
|
|
|
"mc/internal/camera"
|
2026-08-15 23:48:58 +08:00
|
|
|
|
"mc/internal/entity"
|
2026-08-15 22:47:21 +08:00
|
|
|
|
"mc/internal/inventory"
|
|
|
|
|
|
"mc/internal/item"
|
|
|
|
|
|
"mc/internal/physics"
|
|
|
|
|
|
"mc/internal/raycast"
|
|
|
|
|
|
"mc/internal/recipe"
|
|
|
|
|
|
"mc/internal/world"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Session 单机游戏会话。
|
|
|
|
|
|
type Session struct {
|
|
|
|
|
|
World *world.World
|
|
|
|
|
|
Reg *block.Registry
|
|
|
|
|
|
Items *item.Registry
|
|
|
|
|
|
Recipes *recipe.Registry
|
|
|
|
|
|
Player *physics.Player
|
|
|
|
|
|
Inv *inventory.Inventory
|
|
|
|
|
|
Cam *camera.Camera
|
|
|
|
|
|
|
2026-08-15 23:48:58 +08:00
|
|
|
|
// 实体与方块实体(实体系统.md、物品与背包.md §6)
|
|
|
|
|
|
Entities *entity.Manager
|
|
|
|
|
|
BlockEntities map[[3]int32]*blockentity.Entity
|
|
|
|
|
|
|
2026-08-16 00:01:20 +08:00
|
|
|
|
// 自然生成计时(僵尸与敌对生物.md §5、动物体系.md §6)
|
|
|
|
|
|
spawnTimer float64
|
|
|
|
|
|
animalTimer float64
|
|
|
|
|
|
damageIn float64 // 玩家受击冷却
|
|
|
|
|
|
rng *rand.Rand
|
2026-08-15 23:48:58 +08:00
|
|
|
|
|
2026-08-15 22:47:21 +08:00
|
|
|
|
// 破坏进度状态(方块交互与动画.md §3)
|
|
|
|
|
|
breakTarget raycast.Hit
|
|
|
|
|
|
breaking bool
|
|
|
|
|
|
breakProg float64
|
|
|
|
|
|
|
2026-08-15 23:21:37 +08:00
|
|
|
|
// 玩家生命(僵尸/摔落伤害后置接入,实体系统.md §6)
|
|
|
|
|
|
Health int
|
|
|
|
|
|
|
2026-08-15 22:47:21 +08:00
|
|
|
|
// 创造模式(跳过进度直接破坏)
|
|
|
|
|
|
Creative bool
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewSession 创建会话。
|
|
|
|
|
|
func NewSession(w *world.World, reg *block.Registry, items *item.Registry, recipes *recipe.Registry, p *physics.Player, inv *inventory.Inventory, cam *camera.Camera) *Session {
|
2026-08-15 23:48:58 +08:00
|
|
|
|
return &Session{
|
|
|
|
|
|
World: w, Reg: reg, Items: items, Recipes: recipes, Player: p, Inv: inv, Cam: cam,
|
|
|
|
|
|
Health: 20, Entities: entity.NewManager(),
|
|
|
|
|
|
BlockEntities: make(map[[3]int32]*blockentity.Entity),
|
|
|
|
|
|
rng: rand.New(rand.NewSource(12345)),
|
|
|
|
|
|
}
|
2026-08-15 22:47:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 23:21:37 +08:00
|
|
|
|
// PlayerHealth 玩家当前生命(HUD 用)。
|
|
|
|
|
|
func (s *Session) PlayerHealth() int { return s.Health }
|
|
|
|
|
|
|
2026-08-15 22:47:21 +08:00
|
|
|
|
// Tick 每 tick(20Hz)会话逻辑(架构.md §3.2)。
|
|
|
|
|
|
func (s *Session) Tick(dt float64) {
|
|
|
|
|
|
// 物理积分(速度由输入层写入 Player.Vel)
|
|
|
|
|
|
s.Player.Tick(s.World, dt)
|
|
|
|
|
|
// 区块调度(主线程预算 8ms,区块管理.md §4.3)
|
|
|
|
|
|
s.World.Update(s.Player.Pos[0], s.Player.Pos[1], s.Player.Pos[2], 8*time.Millisecond)
|
|
|
|
|
|
// 破坏进度累积
|
|
|
|
|
|
if s.breaking {
|
|
|
|
|
|
s.accumulateBreak(dt)
|
|
|
|
|
|
}
|
2026-08-15 23:48:58 +08:00
|
|
|
|
// 实体 AI(实体系统.md §3 限频决策在内部处理)
|
|
|
|
|
|
s.Entities.Tick(dt, s.World, s.Player.Pos)
|
|
|
|
|
|
// 方块实体(熔炉烧炼,物品与背包.md §6)
|
|
|
|
|
|
s.tickBlockEntities()
|
|
|
|
|
|
// 掉落物拾取(实体系统.md §5)
|
|
|
|
|
|
for _, d := range s.Entities.PickupNearby(s.Player.Pos[0], s.Player.Pos[1], s.Player.Pos[2]) {
|
|
|
|
|
|
s.Inv.Add(item.Stack{Name: d.Item, Count: uint8(min(d.Count, 127))})
|
|
|
|
|
|
}
|
|
|
|
|
|
// 夜间刷僵尸(僵尸与敌对生物.md §5:黑暗处、距玩家 24 格、限频)
|
|
|
|
|
|
s.spawnZombies(dt)
|
2026-08-16 00:01:20 +08:00
|
|
|
|
// 白天刷动物(动物体系.md §6:草面、亮度≥9、距玩家 16–32、限频)
|
|
|
|
|
|
s.spawnAnimals(dt)
|
2026-08-15 23:48:58 +08:00
|
|
|
|
// 僵尸近身伤害(僵尸与敌对生物.md §3:3 点/秒)
|
|
|
|
|
|
s.applyZombieDamage(dt)
|
2026-08-15 22:47:21 +08:00
|
|
|
|
// 相机跟随玩家眼睛
|
|
|
|
|
|
s.Cam.Pos = s.Player.Eye()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Cast 从相机射线拾取(方块交互与动画.md §2)。
|
|
|
|
|
|
func (s *Session) Cast(maxDist float64) (raycast.Hit, bool) {
|
|
|
|
|
|
f := s.Cam.Forward()
|
|
|
|
|
|
return raycast.Cast(s.Cam.Pos, f, maxDist, func(x, y, z int32) bool {
|
|
|
|
|
|
return s.Reg.IsSolid(s.World.Block(x, y, z))
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BeginBreak 按下左键:锁定破坏目标(进度从 0 开始)。
|
|
|
|
|
|
func (s *Session) BeginBreak() {
|
|
|
|
|
|
h, ok := s.Cast(5)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
s.EndBreak()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
s.breakTarget = h
|
|
|
|
|
|
s.breaking = true
|
|
|
|
|
|
s.breakProg = 0
|
|
|
|
|
|
if s.Creative {
|
|
|
|
|
|
s.BreakNow()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// EndBreak 松开左键/换目标:进度清零。
|
|
|
|
|
|
func (s *Session) EndBreak() {
|
|
|
|
|
|
s.breaking = false
|
|
|
|
|
|
s.breakProg = 0
|
|
|
|
|
|
s.breakTarget = raycast.Hit{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BreakProgress 当前破坏进度(0–1,UI 裂纹用)。
|
|
|
|
|
|
func (s *Session) BreakProgress() float64 { return s.breakProg }
|
|
|
|
|
|
|
|
|
|
|
|
// BreakTarget 当前破坏目标(渲染选择框/裂纹用)。
|
|
|
|
|
|
func (s *Session) BreakTarget() raycast.Hit { return s.breakTarget }
|
|
|
|
|
|
|
|
|
|
|
|
// breakSpeed 每秒破坏进度(方块交互与动画.md §3、挖矿与矿物.md §3 同公式)。
|
|
|
|
|
|
func (s *Session) breakSpeed(def block.Def, tool item.Stack) float64 {
|
|
|
|
|
|
speed := 1.0 // 空手
|
|
|
|
|
|
if !tool.Empty() {
|
|
|
|
|
|
if d, ok := s.Items.Get(tool.Name); ok && d.Tool != "" {
|
|
|
|
|
|
if d.Tier >= def.RequiredTier {
|
|
|
|
|
|
speed = d.Speed
|
|
|
|
|
|
} else {
|
|
|
|
|
|
speed = 0.05 // 工具门槛不足:极慢(挖矿与矿物.md §3 slow 策略)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if def.Hardness <= 0 {
|
|
|
|
|
|
return 1e9 // 不可破坏(如基岩 hardness=-1 → 瞬时?不:返回 0 更合理)
|
|
|
|
|
|
}
|
|
|
|
|
|
return speed / def.Hardness
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// accumulateBreak 累积破坏进度,完成即摧毁(掉落进背包 + 耐久消耗)。
|
|
|
|
|
|
func (s *Session) accumulateBreak(dt float64) {
|
|
|
|
|
|
st := s.World.Block(s.breakTarget.X, s.breakTarget.Y, s.breakTarget.Z)
|
|
|
|
|
|
def := s.Reg.Get(st.ID())
|
|
|
|
|
|
if def.ID == 0 {
|
|
|
|
|
|
s.EndBreak() // 目标已消失
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
tool := s.Inv.SelectedStack()
|
|
|
|
|
|
s.breakProg += s.breakSpeed(def, tool) * dt
|
|
|
|
|
|
if s.breakProg >= 1.0 {
|
|
|
|
|
|
s.destroy(s.breakTarget)
|
|
|
|
|
|
s.EndBreak()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 23:48:58 +08:00
|
|
|
|
// destroy 摧毁方块:替换为空气 + 掉落物实体(实体系统.md §5)。
|
2026-08-15 22:47:21 +08:00
|
|
|
|
func (s *Session) destroy(h raycast.Hit) {
|
|
|
|
|
|
st := s.World.Block(h.X, h.Y, h.Z)
|
|
|
|
|
|
def := s.Reg.Get(st.ID())
|
|
|
|
|
|
s.World.SetBlock(h.X, h.Y, h.Z, block.Air)
|
2026-08-15 23:48:58 +08:00
|
|
|
|
// 方块实体随之销毁(物品与背包.md §6)
|
|
|
|
|
|
delete(s.BlockEntities, [3]int32{h.X, h.Y, h.Z})
|
2026-08-15 22:47:21 +08:00
|
|
|
|
drop := def.Drops
|
|
|
|
|
|
if drop == "" {
|
|
|
|
|
|
drop = def.Name
|
|
|
|
|
|
}
|
|
|
|
|
|
if drop != "" {
|
2026-08-15 23:48:58 +08:00
|
|
|
|
s.Entities.SpawnDrop(drop, 1, float64(h.X)+0.5, float64(h.Y), float64(h.Z)+0.5)
|
2026-08-15 22:47:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// BreakNow 立即破坏(创造模式)。
|
|
|
|
|
|
func (s *Session) BreakNow() {
|
|
|
|
|
|
if !s.breaking {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
s.destroy(s.breakTarget)
|
|
|
|
|
|
s.EndBreak()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PlaceSelected 放置选中槽物品(右键,方块交互与动画.md §6)。
|
|
|
|
|
|
func (s *Session) PlaceSelected() {
|
|
|
|
|
|
sel := s.Inv.SelectedStack()
|
|
|
|
|
|
if sel.Empty() {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
d, ok := s.Items.Get(sel.Name)
|
|
|
|
|
|
if !ok || d.Place == "" {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
bid, ok := s.Reg.ID(d.Place)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 射线拾取放置位
|
|
|
|
|
|
h, ok := s.Cast(5)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
px, py, pz := h.Prev[0], h.Prev[1], h.Prev[2]
|
|
|
|
|
|
// 目标必须为空且不与玩家 AABB 重叠(方块交互与动画.md §10 踩坑)
|
|
|
|
|
|
if s.World.Block(px, py, pz) != block.Air {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if s.overlapsPlayer(px, py, pz) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
s.World.SetBlock(px, py, pz, block.NewState(bid, 0))
|
2026-08-15 23:48:58 +08:00
|
|
|
|
// 方块实体创建(物品与背包.md §6:熔炉)
|
|
|
|
|
|
if d.Place == "furnace" {
|
|
|
|
|
|
s.BlockEntities[[3]int32{px, py, pz}] = blockentity.NewFurnace(px, py, pz)
|
|
|
|
|
|
}
|
2026-08-15 22:47:21 +08:00
|
|
|
|
// 消耗一个物品
|
|
|
|
|
|
s.Inv.Remove(sel.Name, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// overlapsPlayer 放置位是否与玩家 AABB 重叠。
|
|
|
|
|
|
func (s *Session) overlapsPlayer(x, y, z int32) bool {
|
|
|
|
|
|
p := s.Player
|
|
|
|
|
|
px, py, pz := p.Pos[0], p.Pos[1], p.Pos[2]
|
|
|
|
|
|
hw := p.Width / 2
|
|
|
|
|
|
// 方块 AABB [x,x+1]×[y,y+1]×[z,z+1] 与玩家 AABB 相交判定
|
|
|
|
|
|
return float64(x)+1 > px-hw && float64(x) < px+hw &&
|
|
|
|
|
|
float64(y)+1 > py && float64(y) < py+p.Height &&
|
|
|
|
|
|
float64(z)+1 > pz-hw && float64(z) < pz+hw
|
|
|
|
|
|
}
|
2026-08-15 23:48:58 +08:00
|
|
|
|
|
|
|
|
|
|
// tickBlockEntities 熔炉烧炼并产出(物品与背包.md §6)。
|
|
|
|
|
|
func (s *Session) tickBlockEntities() {
|
|
|
|
|
|
for _, be := range s.BlockEntities {
|
|
|
|
|
|
if be.Kind != blockentity.KindFurnace {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if inputName, ok := be.Tick(); ok {
|
|
|
|
|
|
// 产出:按熔炼配方放入输出槽
|
|
|
|
|
|
if rc, found := s.Recipes.MatchSmelting(inputName); found {
|
|
|
|
|
|
be.Insert(blockentity.SlotOutput, item.Stack{Name: rc.Result.Item, Count: uint8(min(rc.Result.Count, 127))}, s.Items)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// spawnZombies 夜间刷怪(僵尸与敌对生物.md §5:方块光 ≤7、距玩家 24–48 格、每 5 秒一只)。
|
|
|
|
|
|
func (s *Session) spawnZombies(dt float64) {
|
|
|
|
|
|
s.spawnTimer -= dt
|
|
|
|
|
|
if s.spawnTimer > 0 {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
s.spawnTimer = 5.0
|
|
|
|
|
|
for i := 0; i < 16; i++ {
|
|
|
|
|
|
ang := s.rng.Float64() * 2 * math.Pi
|
|
|
|
|
|
dist := 24 + s.rng.Float64()*24
|
|
|
|
|
|
x := int32(s.Player.Pos[0] + math.Cos(ang)*dist)
|
|
|
|
|
|
z := int32(s.Player.Pos[2] + math.Sin(ang)*dist)
|
|
|
|
|
|
// 找地表(从顶向下第一个非空气)
|
|
|
|
|
|
var y int32
|
|
|
|
|
|
for y = 255; y > 0; y-- {
|
|
|
|
|
|
if s.World.Block(x, y, z) != block.Air {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 生成点两格空气 + 黑暗
|
|
|
|
|
|
if s.World.Block(x, y+1, z) != block.Air || s.World.Block(x, y+2, z) != block.Air {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if s.World.BlockLight(x, y+1, z) > 7 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
s.Entities.Spawn(entity.KindZombie, float64(x)+0.5, float64(y+1), float64(z)+0.5)
|
|
|
|
|
|
return // 每周期最多 1 只
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-16 00:01:20 +08:00
|
|
|
|
// spawnAnimals 白天刷动物(动物体系.md §6:草面、天空光 ≥9、距玩家 16–32、每 10 秒一只)。
|
|
|
|
|
|
func (s *Session) spawnAnimals(dt float64) {
|
|
|
|
|
|
s.animalTimer -= dt
|
|
|
|
|
|
if s.animalTimer > 0 {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
s.animalTimer = 10.0
|
|
|
|
|
|
for i := 0; i < 16; i++ {
|
|
|
|
|
|
ang := s.rng.Float64() * 2 * math.Pi
|
|
|
|
|
|
dist := 16 + s.rng.Float64()*16
|
|
|
|
|
|
x := int32(s.Player.Pos[0] + math.Cos(ang)*dist)
|
|
|
|
|
|
z := int32(s.Player.Pos[2] + math.Sin(ang)*dist)
|
|
|
|
|
|
var y int32
|
|
|
|
|
|
for y = 255; y > 0; y-- {
|
|
|
|
|
|
if s.World.Block(x, y, z) != block.Air {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if s.World.Block(x, y+1, z) != block.Air {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
// 只在草面上且白天(天空光 ≥9)
|
|
|
|
|
|
if s.World.Block(x, y, z).ID() != s.grassID() {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if s.World.SkyLight(x, y+1, z) < 9 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
e := s.Entities.Spawn(entity.KindAnimal, float64(x)+0.5, float64(y+1), float64(z)+0.5)
|
|
|
|
|
|
e.Data = &entity.Animal{}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// grassID 草方块 ID(注册表查询缓存值)。
|
|
|
|
|
|
func (s *Session) grassID() uint16 {
|
|
|
|
|
|
id, ok := s.Reg.ID("grass_block")
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return 0
|
|
|
|
|
|
}
|
|
|
|
|
|
return id
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 23:48:58 +08:00
|
|
|
|
// applyZombieDamage 僵尸近身伤害(僵尸与敌对生物.md §3:约 3 点/秒)。
|
|
|
|
|
|
func (s *Session) applyZombieDamage(dt float64) {
|
|
|
|
|
|
if s.Health <= 0 {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
s.damageIn -= dt
|
|
|
|
|
|
if s.damageIn > 0 {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, e := range s.Entities.List() {
|
|
|
|
|
|
if e.Kind != entity.KindZombie || e.Dead {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
dx, dz := e.Pos[0]-s.Player.Pos[0], e.Pos[2]-s.Player.Pos[2]
|
|
|
|
|
|
if dx*dx+dz*dz < 1.6*1.6 && e.Pos[1]-s.Player.Pos[1] < 2 {
|
|
|
|
|
|
s.Health -= 3
|
|
|
|
|
|
s.damageIn = 1.0
|
|
|
|
|
|
if s.Health <= 0 {
|
|
|
|
|
|
s.Health = 0
|
|
|
|
|
|
s.respawn()
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// respawn 死亡重生:回出生点 + 满血(实体系统.md §6)。
|
|
|
|
|
|
func (s *Session) respawn() {
|
|
|
|
|
|
s.Health = 20
|
|
|
|
|
|
s.Player.Pos = [3]float64{8.5, 120, 8.5}
|
|
|
|
|
|
s.Player.Vel = [3]float64{}
|
|
|
|
|
|
s.damageIn = 2.0
|
|
|
|
|
|
}
|