Files
ngzz-mc/internal/game/game.go

644 lines
18 KiB
Go
Raw Normal View History

// 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
}