Files
ngzz-mc/internal/game/game.go
NianGao Dev 9e2f5a6186 fix(game): 崩溃修复(卸载区块实体冻结)+ 立面光照、掉落可见、打击感、水下效果、空手拳头、血条左/饥饿右
- 崩溃(index out of range):实体所在区块卸载后失去碰撞支撑无限下坠到
  负坐标,光查询越界 panic——WorldView 增 ChunkLoaded,未加载区块实体冻结;
  chunk 光查询加越界防御(双保险)
- 立面光照:mesh 每面取**邻格**光照(原版光照模型)——此前用方块自身光照
  (sky=0)导致悬崖/矿洞立面全黑(泥土竖向黑色根因)
- 掉落可见:生成后 1 秒拾取延迟 + 图标加大到 0.45
- 打击感:攻击命中音(block.hit)+ 实体受击白闪(uTint)+ 手持挥动动画
- 水下效果:相机入水蓝色叠加(渲染.md §8 水体)
- 空手显示肤色拳头(白羊毛 × 肤色 tint),持方块正常显示
- HUD 布局:血条左下、饥饿右下(经典布局)
2026-08-16 20:59:28 +08:00

644 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package game 游戏会话:输入 → 玩家 → 世界 → 交互(破坏/放置)的粘合层(架构.md §3 主循环)。
//
// 职责边界:会话只做调度与玩法逻辑,渲染/UI/网络通过接口或外部驱动访问。
package game
import (
"math"
"math/rand"
"time"
"mc/internal/block"
"mc/internal/blockentity"
"mc/internal/camera"
"mc/internal/entity"
"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
// 实体与方块实体(实体系统.md、物品与背包.md §6)
Entities *entity.Manager
BlockEntities map[[3]int32]*blockentity.Entity
// 自然生成计时(僵尸与敌对生物.md §5、动物体系.md §6)
spawnTimer float64
animalTimer float64
damageIn float64 // 玩家受击冷却
rng *rand.Rand
// 破坏进度状态(方块交互与动画.md §3)
breakTarget raycast.Hit
breaking bool
breakProg float64
// 玩家生命(僵尸/摔落伤害后置接入,实体系统.md §6)
Health int
// 创造模式(跳过进度直接破坏)
Creative bool
// 工作台状态(工作台与合成表.md §5:3×3 网格 + 输出格)
craftOpen bool
craftGrid [9]item.Stack
// Sound 音效回调(客户端注入,音频.md §2 事件驱动;nil 则静默)
Sound func(key string, x, y, z float64)
// 村民生成(村民系统.md §2:村庄中心 → 村民;已生成过的中心不再生成)
villagerTimer float64
villageSpawn map[[2]int32]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 {
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)),
villageSpawn: make(map[[2]int32]bool),
}
}
// PlayerHealth 玩家当前生命(HUD 用)。
func (s *Session) PlayerHealth() int { return s.Health }
// 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)
}
// 实体 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)
// 白天刷动物(动物体系.md §6:草面、亮度≥9、距玩家 16–32、限频)
s.spawnAnimals(dt)
// 村庄村民生成(村民系统.md §2:已加载区块内村庄中心 → 3 名村民)
s.spawnVillagers(dt)
// 僵尸近身伤害(僵尸与敌对生物.md §3:3 点/秒)
s.applyZombieDamage(dt)
// 相机跟随玩家眼睛
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.Sound != nil {
s.Sound("block.hit", float64(h.X)+0.5, float64(h.Y)+0.5, float64(h.Z)+0.5)
}
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 }
// SetBreakVisual 直接设置破坏进度显示状态(客户端渲染层裂纹叠加/开发测试用;
// 不修改世界数据,不驱动 accumulateBreak)。
func (s *Session) SetBreakVisual(x, y, z int32, prog float64) {
s.breakTarget = raycast.Hit{X: x, Y: y, Z: z}
s.breakProg = prog
}
// AttackInRange 从相机射线寻找可达实体(半径 0.6,动物体系.md §5 攻击判定)。
func (s *Session) AttackInRange(maxDist float64) (entity.ID, bool) {
o := s.Cam.Pos
d := s.Cam.Forward()
var best entity.ID
bestT := maxDist
for _, e := range s.Entities.List() {
if e.Dead {
continue
}
// 实体中心(脚底 + 半高)
c := [3]float64{e.Pos[0], e.Pos[1] + 0.9, e.Pos[2]}
oc := [3]float64{c[0] - o[0], c[1] - o[1], c[2] - o[2]}
t := oc[0]*d[0] + oc[1]*d[1] + oc[2]*d[2] // 射线参数
if t <= 0 || t > bestT {
continue
}
// 点到射线距离(实体半径 0.6,与碰撞体一致)
dx := oc[0] - t*d[0]
dy := oc[1] - t*d[1]
dz := oc[2] - t*d[2]
if dx*dx+dy*dy+dz*dz < 0.6*0.6 {
best, bestT = e.ID, t
}
}
return best, best != 0
}
// Attack 攻击视线内最近的实体:伤害 + 击退 + 死亡掉落(动物体系.md §5、僵尸与敌对生物.md §3)。
// 命中返回 true(调用方不再开始破坏方块)。
func (s *Session) Attack() bool {
id, ok := s.AttackInRange(5)
if !ok {
return false
}
e := s.Entities.Get(id)
if e == nil || e.Dead {
return false
}
e.Health -= 2
e.HitFlash = 0.15 // 受击白闪(打击反馈,客户端渲染)
// 击退(沿视线水平方向)
f := s.Cam.Forward()
e.Vel[0] += f[0] * 4
e.Vel[2] += f[2] * 4
if s.Sound != nil {
s.Sound("entity.hit", e.Pos[0], e.Pos[1]+1, e.Pos[2])
}
if e.Health > 0 {
return true
}
e.Dead = true
// 死亡掉落(与 vanilla 一致:动物掉肉,僵尸掉腐肉)
loot := ""
switch e.Kind {
case entity.KindZombie:
loot = "rotten_flesh"
case entity.KindAnimal:
switch e.ID % 4 {
case 0:
loot = "beef"
case 1:
loot = "porkchop"
case 2:
loot = "mutton"
default:
loot = "chicken"
}
}
if loot != "" {
s.Entities.SpawnDrop(loot, 1, e.Pos[0], e.Pos[1], e.Pos[2])
}
s.Entities.Remove(id)
return true
}
// DropSelected 丢弃选中槽 1 个物品(UI与输入.md §5 Q 键;掉落在视线前方)。
func (s *Session) DropSelected() {
st := s.Inv.Slots[s.Inv.Selected]
if st.Empty() {
return
}
st.Count--
if st.Count == 0 {
s.Inv.Slots[s.Inv.Selected] = item.Stack{}
} else {
s.Inv.Slots[s.Inv.Selected] = st
}
f := s.Cam.Forward()
s.Entities.SpawnDrop(st.Name, 1,
s.Player.Pos[0]+f[0]*1.5, s.Player.Pos[1]+1, s.Player.Pos[2]+f[2]*1.5)
}
// spawnVillagers 村庄村民生成(村民系统.md §2):扫描已加载区块,村庄中心处生成 3 名村民。
func (s *Session) spawnVillagers(dt float64) {
s.villagerTimer -= dt
if s.villagerTimer > 0 {
return
}
s.villagerTimer = 4.0
for _, c := range s.World.ActiveChunks() {
center, ok := s.World.VillageAt(c.CX*16+8, c.CZ*16+8)
if !ok || s.villageSpawn[center] {
continue
}
s.villageSpawn[center] = true
// 找村庄中心地表
var y int32
for y = 255; y > 0; y-- {
if s.World.Block(center[0], y, center[1]) != block.Air {
break
}
}
for i := 0; i < 3; i++ {
e := s.Entities.Spawn(entity.KindVillager,
float64(center[0])+float64(i%2)*2, float64(y+1), float64(center[1])+float64(i/2)*2)
e.Data = &entity.Villager{}
}
if s.Sound != nil {
s.Sound("entity.villager.ambient", float64(center[0]), float64(y+1), float64(center[1]))
}
}
}
// 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()
}
}
// destroy 摧毁方块:替换为空气 + 掉落物实体(实体系统.md §5)。
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)
// 方块实体随之销毁(物品与背包.md §6)
delete(s.BlockEntities, [3]int32{h.X, h.Y, h.Z})
if s.Sound != nil {
s.Sound("block."+def.Name+".break", float64(h.X)+0.5, float64(h.Y)+0.5, float64(h.Z)+0.5)
}
if def.NoDrop {
return
}
drop := def.Drops
if drop == "" {
drop = def.Name
}
if drop != "" {
s.Entities.SpawnDrop(drop, 1, float64(h.X)+0.5, float64(h.Y), float64(h.Z)+0.5)
}
}
// 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))
if s.Sound != nil {
s.Sound("block.place", float64(px)+0.5, float64(py)+0.5, float64(pz)+0.5)
}
// 方块实体创建(物品与背包.md §6:熔炉)
if d.Place == "furnace" {
s.BlockEntities[[3]int32{px, py, pz}] = blockentity.NewFurnace(px, py, pz)
}
// 消耗一个物品
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
}
// ---- 工作台合成(工作台与合成表.md §5)----
// craftingTableID 工作台方块 ID(注册表查询缓存值)。
func (s *Session) craftingTableID() uint16 {
id, ok := s.Reg.ID("crafting_table")
if !ok {
return 0
}
return id
}
// TryOpenCrafting 视线 5 格内命中工作台则打开合成界面(右键触发)。
// 返回 true 表示已打开(调用方不应再执行放置)。
func (s *Session) TryOpenCrafting() bool {
h, ok := s.Cast(5)
if !ok {
return false
}
if s.World.Block(h.X, h.Y, h.Z).ID() != s.craftingTableID() {
return false
}
s.craftOpen = true
return true
}
// OpenCrafting 直接打开合成界面(开发测试/后续按键绑定用)。
func (s *Session) OpenCrafting() { s.craftOpen = true }
// CraftingOpen 合成界面是否打开。
func (s *Session) CraftingOpen() bool { return s.craftOpen }
// CraftSlot 读网格槽(只读,UI 用)。
func (s *Session) CraftSlot(i int) item.Stack {
if i < 0 || i >= 9 {
return item.Stack{}
}
return s.craftGrid[i]
}
// CraftingSwap 网格槽与手持栈交换/合并(同物品合并到上限),返回新的手持栈。
func (s *Session) CraftingSwap(i int, held item.Stack) item.Stack {
if i < 0 || i >= 9 {
return held
}
cur := s.craftGrid[i]
switch {
case held.Empty():
s.craftGrid[i] = item.Stack{}
return cur
case cur.Empty():
s.craftGrid[i] = held
return item.Stack{}
case cur.Name == held.Name:
merged, remain := inventory.Merge(held.Limit(s.Items), cur, held)
s.craftGrid[i] = merged
return remain
default:
s.craftGrid[i] = held
return cur
}
}
// craftResult 按网格匹配配方(有序优先,再无序),返回产物。
func (s *Session) craftResult() (item.Stack, bool) {
var g [3][3]string
counts := make(map[string]int)
for i := 0; i < 9; i++ {
st := s.craftGrid[i]
if st.Empty() {
continue
}
g[i/3][i%3] = st.Name
counts[st.Name]++
}
if len(counts) == 0 {
return item.Stack{}, false
}
// 有序配方优先(避免无序规则误吞有序网格,工作台与合成表.md §4)
if rc, ok := s.Recipes.MatchShaped(g); ok {
return item.Stack{Name: rc.Result.Item, Count: uint8(min(rc.Result.Count, 127))}, true
}
if rc, ok := s.Recipes.MatchShapeless(counts); ok {
return item.Stack{Name: rc.Result.Item, Count: uint8(min(rc.Result.Count, 127))}, true
}
return item.Stack{}, false
}
// CraftingResult 当前网格的合成产物(无则 ok=false,UI 输出格用)。
func (s *Session) CraftingResult() (item.Stack, bool) { return s.craftResult() }
// CraftingTakeResult 执行一次合成:消耗网格材料并返回产物。
// 网格匹配失败返回 false(材料不动)。
func (s *Session) CraftingTakeResult() (item.Stack, bool) {
res, ok := s.craftResult()
if !ok {
return item.Stack{}, false
}
for i := range s.craftGrid {
if s.craftGrid[i].Empty() {
continue
}
if s.craftGrid[i].Count <= 1 {
s.craftGrid[i] = item.Stack{}
} else {
s.craftGrid[i].Count--
}
}
return res, true
}
// CloseCrafting 关闭合成界面:网格剩余材料退回背包(放不下掉落地面)。
func (s *Session) CloseCrafting() {
if !s.craftOpen {
return
}
for i := range s.craftGrid {
if s.craftGrid[i].Empty() {
continue
}
if rem := s.Inv.Add(s.craftGrid[i]); !rem.Empty() {
s.Entities.SpawnDrop(rem.Name, int(rem.Count), s.Player.Pos[0], s.Player.Pos[1]+1, s.Player.Pos[2])
}
s.craftGrid[i] = item.Stack{}
}
s.craftOpen = false
}
// 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 只
}
}
// 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
}
// 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
}