Files
ngzz-mc/internal/game/game.go
2026-09-19 14:21:08 +08:00

1377 lines
35 KiB
Go
Raw Permalink 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"
"sort"
"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/redstone"
"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
Hunger int
Saturation float64
Exhaustion float64
starveAcc float64
wasGround bool
Dead bool
DeathCause string
Spawn [3]float64
// 创造模式(跳过进度直接破坏、不掉落)
Creative bool
eatLeft float64
eatName string
eatAcc float64
walkDist float64
nextStep float64
fallDist float64
swimAcc float64
wasInWater bool
drownAcc float64
destroyTicks int
// 工作台状态(工作台与合成表.md §5:3×3 网格 + 输出格)
craftOpen bool
craftGrid [9]item.Stack
// 物品栏 2×2 合成(UI还原.md §3.3:会话临时网格,关闭退回)
invGrid [4]item.Stack
// 容器(熔炉/箱子)
contKind int // 0 无 / 1 熔炉 / 2 箱子
contPos [3]int32
Unlocked map[string]bool // 配方书解锁(结果物品名)
Trades *entity.TradeTable
tradeOpen bool
tradeID entity.ID
// Pickup 拾取成功回调(客户端飞入动画;nil 则忽略)
Pickup func(name string, x, y, z float64, slot int)
// Sound 音效回调(客户端注入,音频.md §2 事件驱动;nil 则静默)
Sound func(key string, x, y, z float64)
// 村民生成(村民系统.md §2:村庄中心 → 村民;已生成过的中心不再生成)
villagerTimer float64
villageSpawn map[[2]int32]bool
WorldSeed int64
TickN int64
rsQ *redstone.Queue
rsDirty bool
fluidQ [][3]int32
}
// 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 {
s := &Session{
World: w, Reg: reg, Items: items, Recipes: recipes, Player: p, Inv: inv, Cam: cam,
Health: 20, Hunger: 20, Saturation: 5, Entities: entity.NewManager(),
BlockEntities: make(map[[3]int32]*blockentity.Entity),
rng: rand.New(rand.NewSource(12345)),
villageSpawn: make(map[[2]int32]bool),
Unlocked: make(map[string]bool),
rsQ: redstone.NewQueue(),
}
if p != nil {
s.Spawn = p.Pos
}
return s
}
// PlayerHealth 玩家当前生命(HUD 用)。
func (s *Session) PlayerHealth() int { return s.Health }
// PlayerHunger 饥饿值 0–20(HUD 鸡腿)。
func (s *Session) PlayerHunger() int { return s.Hunger }
// PlayerAir 氧气剩余秒数(HUD 气泡)。
func (s *Session) PlayerAir() float64 { return s.Player.Air }
// Tick 每 tick(20Hz)会话逻辑(架构.md §3.2)。
func (s *Session) Tick(dt float64) {
if s.Dead {
s.World.Update(s.Player.Pos[0], s.Player.Pos[1], s.Player.Pos[2], 8*time.Millisecond)
s.Entities.Tick(dt, s.World, s.Player.Pos)
s.tickBlockEntities()
return
}
vy := s.Player.Vel[1]
s.Player.Tick(s.World, dt)
s.tickFallDamage(dt, vy)
s.tickDrown(dt)
s.tickEat(dt)
s.tickHunger(dt)
s.tickFootsteps(dt)
s.tickEntitySounds(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)
s.tickVillagerBuild()
// 方块实体(熔炉烧炼,物品与背包.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.pickupDrop(d)
}
// 夜间刷僵尸(僵尸与敌对生物.md §5:黑暗处、距玩家 24 格、限频)
s.spawnZombies(dt)
// 白天刷动物(动物体系.md §6:草面、亮度≥9、距玩家 16–32、限频)
s.spawnAnimals(dt)
// 村庄村民生成(村民系统.md §2:已加载区块内村庄中心 → 3 名村民)
s.spawnVillagers(dt)
s.tickWorldRules()
// 僵尸近身伤害(僵尸与敌对生物.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() {
if s.Dead {
return
}
h, ok := s.Cast(5)
if !ok {
s.EndBreak()
return
}
s.breakTarget = h
s.breaking = true
s.breakProg = 0
s.destroyTicks = 0
if s.Creative {
s.BreakNow()
}
}
// EndBreak 松开左键/换目标:进度清零。
func (s *Session) EndBreak() {
s.breaking = false
s.breakProg = 0
s.breakTarget = raycast.Hit{}
}
// Breaking 是否正在破坏(输入层按住续挖用)。
func (s *Session) Breaking() bool { return s.breaking }
// ContinueBreak 按住左键:未在挖则开始;准星换块则重置(方块交互与动画.md §3)。
func (s *Session) ContinueBreak() {
h, ok := s.Cast(5)
if !ok {
if s.breaking {
s.EndBreak()
}
return
}
if s.breaking && s.breakTarget.X == h.X && s.breakTarget.Y == h.Y && s.breakTarget.Z == h.Z {
return
}
s.BeginBreak()
}
// 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 {
if s.Dead {
return false
}
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 // 受击白闪(打击反馈,客户端渲染)
s.damageHeldTool(2)
// 击退(沿视线水平方向)
f := s.Cam.Forward()
e.Vel[0] += f[0] * 4
e.Vel[2] += f[2] * 4
if s.Sound != nil {
s.Sound("entity.player.attack.strong", s.Player.Pos[0], s.Player.Pos[1]+1, s.Player.Pos[2])
}
if e.Health > 0 {
if s.Sound != nil {
s.Sound(hurtSound(e), e.Pos[0], e.Pos[1]+1, e.Pos[2])
}
return true
}
if s.Sound != nil {
s.Sound(deathSound(e), e.Pos[0], e.Pos[1]+1, e.Pos[2])
}
e.Dead = true
// 死亡掉落(与 vanilla 一致:动物掉肉,僵尸掉腐肉)
loot := ""
switch e.Kind {
case entity.KindZombie:
loot = "rotten_flesh"
case entity.KindAnimal:
switch entity.AnimalSpecies(e) {
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
}
// SwingMiss 挥空:无实体且无准星方块时由输入层在按下边沿调用(音频.md §6.2)。
func (s *Session) SwingMiss() {
if s.Sound == nil {
return
}
s.Sound("entity.player.attack.nodamage", s.Player.Pos[0], s.Player.Pos[1]+1, s.Player.Pos[2])
}
// 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)
}
// pickupDrop 拾取掉落:按物品栈上限拆分成多栈放入背包;背包放不下的部分
// 重新掉回地面(物品与背包.md §3、实体系统.md §5)。
func (s *Session) pickupDrop(d entity.Drop) {
rem := d.Count
for rem > 0 {
chunk := rem
limit := 64
if def, ok := s.Items.Get(d.Item); ok && def.Stack > 0 && def.Stack < limit {
limit = def.Stack
}
if chunk > limit {
chunk = limit
}
before := s.Inv.CountItem(d.Item)
leftover := s.Inv.Add(item.Stack{Name: d.Item, Count: uint8(chunk)})
if leftover.Empty() && s.Pickup != nil {
slot := 0
for i, st := range s.Inv.Slots {
if i >= inventory.StorageSize {
break
}
if st.Name == d.Item {
slot = i
break
}
}
s.Pickup(d.Item, s.Player.Pos[0], s.Player.Pos[1]+1, s.Player.Pos[2], slot)
_ = before
}
if !leftover.Empty() {
// 背包满:剩余重新掉回地面(含未放入部分)
s.Entities.SpawnDrop(d.Item, rem, s.Player.Pos[0], s.Player.Pos[1]+1, s.Player.Pos[2])
return
}
rem -= chunk
}
}
// 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)
v := &entity.Villager{Profession: uint8(i % 4)}
if v.Profession == 1 {
ox, oz := center[0]+int32(8+i*8), center[1]+int32(i*4)
v.BuildQueue = entity.Blueprint([3]int32{ox, y, oz}, uint64(center[0])*31+uint64(i))
}
e.Data = v
}
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
}
if s.Sound != nil && s.destroyTicks%4 == 0 {
s.Sound(def.HitSound(), float64(s.breakTarget.X)+0.5, float64(s.breakTarget.Y)+0.5, float64(s.breakTarget.Z)+0.5)
}
s.destroyTicks++
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)
s.notifyNeighbors(h.X, h.Y, h.Z)
// 方块实体随之销毁(物品与背包.md §6)
delete(s.BlockEntities, [3]int32{h.X, h.Y, h.Z})
if s.Sound != nil {
s.Sound(def.BreakSound(), float64(h.X)+0.5, float64(h.Y)+0.5, float64(h.Z)+0.5)
}
if !s.Creative {
s.damageHeldTool(1)
s.AddExhaustion(0.005)
}
if s.Creative || def.NoDrop {
return
}
toolType, toolTier := "", 0
if d, ok := s.Items.Get(s.Inv.SelectedStack().Name); ok {
toolType, toolTier = d.Tool, d.Tier
}
for _, drop := range def.RollDrops(toolType, toolTier, s.rng.Float64) {
if drop.Count > 0 {
s.Entities.SpawnDrop(drop.Item, drop.Count, 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() {
if s.Dead {
return
}
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))
s.notifyNeighbors(px, py, pz)
if s.Sound != nil {
s.Sound(s.Reg.Get(bid).PlaceSound(), 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)
}
if d.Place == "chest" {
s.BlockEntities[[3]int32{px, py, pz}] = blockentity.NewChest(px, py, pz)
}
// 消耗一个物品
s.Inv.Remove(sel.Name, 1)
if sel.Name == "water_bucket" {
s.Inv.Add(item.Stack{Name: "bucket", Count: 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
}
func (s *Session) blockID(name string) uint16 {
id, ok := s.Reg.ID(name)
if !ok {
return 0
}
return id
}
// TryOpenFurnace 右键熔炉。
func (s *Session) TryOpenFurnace() bool {
h, ok := s.Cast(5)
if !ok || s.World.Block(h.X, h.Y, h.Z).ID() != s.blockID("furnace") {
return false
}
pos := [3]int32{h.X, h.Y, h.Z}
if s.BlockEntities[pos] == nil {
s.BlockEntities[pos] = blockentity.NewFurnace(h.X, h.Y, h.Z)
}
s.contKind, s.contPos = 1, pos
return true
}
// TryOpenChest 右键箱子。
func (s *Session) TryOpenChest() bool {
h, ok := s.Cast(5)
if !ok || s.World.Block(h.X, h.Y, h.Z).ID() != s.blockID("chest") {
return false
}
pos := [3]int32{h.X, h.Y, h.Z}
if s.BlockEntities[pos] == nil {
s.BlockEntities[pos] = blockentity.NewChest(h.X, h.Y, h.Z)
}
s.contKind, s.contPos = 2, pos
return true
}
// ContainerOpen 打开的容器:1 熔炉 / 2 箱子 / 0 无。
func (s *Session) ContainerOpen() int { return s.contKind }
// ContainerEntity 当前打开的方块实体。
func (s *Session) ContainerEntity() *blockentity.Entity {
if s.contKind == 0 {
return nil
}
return s.BlockEntities[s.contPos]
}
// CloseContainer 关闭熔炉/箱子。
func (s *Session) CloseContainer() { s.contKind = 0 }
// ExportBlockEntities 存档用快照。
func (s *Session) ExportBlockEntities() []blockentity.Snapshot {
out := make([]blockentity.Snapshot, 0, len(s.BlockEntities))
for _, e := range s.BlockEntities {
out = append(out, e.Serialize())
}
return out
}
// ImportBlockEntities 读档。
func (s *Session) ImportBlockEntities(list []blockentity.Snapshot) {
s.BlockEntities = make(map[[3]int32]*blockentity.Entity, len(list))
for _, snap := range list {
e := blockentity.Deserialize(snap)
s.BlockEntities[[3]int32{e.X, e.Y, e.Z}] = e
}
}
// UnlockRecipe 解锁配方(按产物名)。
func (s *Session) UnlockRecipe(name string) {
if name == "" {
return
}
if s.Unlocked == nil {
s.Unlocked = make(map[string]bool)
}
s.Unlocked[name] = true
}
// RecipeUnlocked 是否已解锁。
func (s *Session) RecipeUnlocked(name string) bool {
return s.Unlocked != nil && s.Unlocked[name]
}
// ExportUnlocked 存档用已解锁配方名。
func (s *Session) ExportUnlocked() []string {
if len(s.Unlocked) == 0 {
return nil
}
out := make([]string, 0, len(s.Unlocked))
for name, ok := range s.Unlocked {
if ok {
out = append(out, name)
}
}
sort.Strings(out)
return out
}
// ImportUnlocked 读档。
func (s *Session) ImportUnlocked(list []string) {
s.Unlocked = make(map[string]bool, len(list))
for _, name := range list {
if name != "" {
s.Unlocked[name] = true
}
}
}
// DiscoverCraftable 背包能做出的合成配方标为已解锁。
func (s *Session) DiscoverCraftable() {
if s.Recipes == nil {
return
}
for _, rc := range s.Recipes.CraftingRecipes() {
need := recipe.NeedCounts(rc)
ok := true
for name, n := range need {
if s.Inv.CountItem(name) < n {
ok = false
break
}
}
if ok {
s.UnlockRecipe(rc.Result.Item)
}
}
}
// 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]
}
// CraftGridPtr 3×3 网格槽指针(拖拽均分)。
func (s *Session) CraftGridPtr(i int) *item.Stack {
if i < 0 || i >= 9 {
return nil
}
return &s.craftGrid[i]
}
// InvCraftGridPtr 物品栏 2×2 槽指针。
func (s *Session) InvCraftGridPtr(i int) *item.Stack {
if i < 0 || i >= 4 {
return nil
}
return &s.invGrid[i]
}
// CraftingSwap 网格槽与手持栈左键交换/合并,返回新的手持栈。
func (s *Session) CraftingSwap(i int, held item.Stack) item.Stack {
return s.CraftingClick(i, held, false)
}
// CraftingClick 网格槽左/右键(UI与输入.md §4)。
func (s *Session) CraftingClick(i int, held item.Stack, right bool) item.Stack {
if i < 0 || i >= 9 {
return held
}
cur := s.craftGrid[i]
limit := held.Limit(s.Items)
if held.Empty() {
limit = cur.Limit(s.Items)
}
var ns, nh item.Stack
if right {
ns, nh = inventory.ClickRight(cur, held, limit, true)
} else {
ns, nh = inventory.ClickLeft(cur, held, limit, true)
}
s.craftGrid[i] = ns
return nh
}
// CraftingShiftGrid Shift+左键:网格 → 背包,返回是否移动。
func (s *Session) CraftingShiftGrid(i int) bool {
if i < 0 || i >= 9 || s.craftGrid[i].Empty() {
return false
}
rem := s.Inv.ShiftIntoStorage(s.craftGrid[i])
s.craftGrid[i] = rem
return rem.Empty()
}
// CraftingShiftInv Shift+左键:背包槽 → 网格。
func (s *Session) CraftingShiftInv(idx int) bool {
if idx < 0 || idx >= inventory.StorageSize || s.Inv.Slots[idx].Empty() {
return false
}
src := s.Inv.Slots[idx]
s.Inv.Slots[idx] = item.Stack{}
grid := s.craftGrid[:]
rem := inventory.ShiftIntoSlots(s.Items, grid, src)
copy(s.craftGrid[:], grid)
if !rem.Empty() {
s.Inv.Slots[idx] = rem
return !inventory.CanMerge(src, rem) || rem.Count != src.Count
}
return true
}
// 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 s.stackFromRecipe(rc), true
}
if rc, ok := s.Recipes.MatchShapeless(counts); ok {
return s.stackFromRecipe(rc), true
}
return item.Stack{}, false
}
func (s *Session) stackFromRecipe(rc *recipe.Recipe) item.Stack {
st := item.Stack{Name: rc.Result.Item, Count: uint8(min(rc.Result.Count, 127))}
if d, ok := s.Items.Get(st.Name); ok && d.Durability > 0 {
st.Durability = int32(d.Durability)
}
return st
}
// 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--
}
}
s.UnlockRecipe(res.Name)
return res, true
}
// CraftingTakeResultAll Shift 批量合成:产物进背包,直到材料不足或背包满(上限 64 次)。
func (s *Session) CraftingTakeResultAll() int {
n := 0
for n < inventory.MaxCraftBulk {
res, ok := s.craftResult()
if !ok || !s.Inv.Fits(res) {
break
}
taken, ok := s.CraftingTakeResult()
if !ok {
break
}
if rem := s.Inv.Add(taken); !rem.Empty() {
s.Entities.SpawnDrop(rem.Name, int(rem.Count), s.Player.Pos[0], s.Player.Pos[1]+1, s.Player.Pos[2])
break
}
n++
}
return n
}
// 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
}
// InvCraftSlot 读物品栏 2×2 格。
func (s *Session) InvCraftSlot(i int) item.Stack {
if i < 0 || i >= 4 {
return item.Stack{}
}
return s.invGrid[i]
}
// InvCraftClick 物品栏 2×2 左/右键。
func (s *Session) InvCraftClick(i int, held item.Stack, right bool) item.Stack {
if i < 0 || i >= 4 {
return held
}
cur := s.invGrid[i]
limit := held.Limit(s.Items)
if held.Empty() {
limit = cur.Limit(s.Items)
}
var ns, nh item.Stack
if right {
ns, nh = inventory.ClickRight(cur, held, limit, true)
} else {
ns, nh = inventory.ClickLeft(cur, held, limit, true)
}
s.invGrid[i] = ns
return nh
}
// InvCraftShift 2×2 格 Shift → 背包。
func (s *Session) InvCraftShift(i int) bool {
if i < 0 || i >= 4 || s.invGrid[i].Empty() {
return false
}
rem := s.Inv.ShiftIntoStorage(s.invGrid[i])
s.invGrid[i] = rem
return rem.Empty()
}
func (s *Session) invCraftResult() (item.Stack, bool) {
var g [3][3]string
counts := make(map[string]int)
for i := 0; i < 4; i++ {
st := s.invGrid[i]
if st.Empty() {
continue
}
g[i/2][i%2] = st.Name
counts[st.Name]++
}
if len(counts) == 0 {
return item.Stack{}, false
}
if rc, ok := s.Recipes.MatchShaped(g); ok {
return s.stackFromRecipe(rc), true
}
if rc, ok := s.Recipes.MatchShapeless(counts); ok {
return s.stackFromRecipe(rc), true
}
return item.Stack{}, false
}
// InvCraftResult 物品栏 2×2 当前产物。
func (s *Session) InvCraftResult() (item.Stack, bool) { return s.invCraftResult() }
// InvCraftTakeResult 物品栏 2×2 执行一次合成。
func (s *Session) InvCraftTakeResult() (item.Stack, bool) {
res, ok := s.invCraftResult()
if !ok {
return item.Stack{}, false
}
for i := range s.invGrid {
if s.invGrid[i].Empty() {
continue
}
if s.invGrid[i].Count <= 1 {
s.invGrid[i] = item.Stack{}
} else {
s.invGrid[i].Count--
}
}
s.UnlockRecipe(res.Name)
return res, true
}
// InvCraftTakeResultAll 物品栏输出格 Shift 批量合成。
func (s *Session) InvCraftTakeResultAll() int {
n := 0
for n < inventory.MaxCraftBulk {
res, ok := s.invCraftResult()
if !ok || !s.Inv.Fits(res) {
break
}
taken, ok := s.InvCraftTakeResult()
if !ok {
break
}
if rem := s.Inv.Add(taken); !rem.Empty() {
s.Entities.SpawnDrop(rem.Name, int(rem.Count), s.Player.Pos[0], s.Player.Pos[1]+1, s.Player.Pos[2])
break
}
n++
}
return n
}
// CloseInvCraft 关闭物品栏:2×2 余料退回背包。
func (s *Session) CloseInvCraft() {
for i := range s.invGrid {
if s.invGrid[i].Empty() {
continue
}
if rem := s.Inv.Add(s.invGrid[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.invGrid[i] = item.Stack{}
}
}
// CraftingFillRecipe 从背包把配方填入 3×3 网格;材料不够则不改动。
func (s *Session) CraftingFillRecipe(rc *recipe.Recipe) bool {
if rc == nil || (rc.Type != "crafting_shaped" && rc.Type != "crafting_shapeless") {
return false
}
need := recipe.NeedCounts(rc)
have := make(map[string]int, len(need))
for name := range need {
have[name] = s.Inv.CountItem(name)
}
for _, st := range s.craftGrid {
if !st.Empty() {
have[st.Name] += int(st.Count)
}
}
for name, n := range need {
if have[name] < n {
return false
}
}
// 网格先退回背包
for i := range s.craftGrid {
if s.craftGrid[i].Empty() {
continue
}
if rem := s.Inv.Add(s.craftGrid[i]); !rem.Empty() {
s.craftGrid[i] = rem
return false
}
s.craftGrid[i] = item.Stack{}
}
cells := recipe.PlaceCells(rc)
for i, name := range cells {
if name == "" {
continue
}
if !s.Inv.TakeOne(name) {
// 理论上已校验,失败则停
return false
}
s.craftGrid[i] = item.Stack{Name: name, Count: 1}
}
return true
}
// 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{Species: s.rng.Intn(4)}
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.kill("被僵尸杀死了")
}
return
}
}
}
func speciesKey(e *entity.Entity) string {
switch e.Kind {
case entity.KindZombie:
return "zombie"
case entity.KindVillager:
return "villager"
case entity.KindAnimal:
switch entity.AnimalSpecies(e) {
case 0:
return "cow"
case 1:
return "pig"
case 2:
return "sheep"
default:
return "chicken"
}
default:
return "generic"
}
}
func hurtSound(e *entity.Entity) string {
return "entity." + speciesKey(e) + ".hurt"
}
func deathSound(e *entity.Entity) string {
return "entity." + speciesKey(e) + ".death"
}
func ambientSound(e *entity.Entity) string {
return "entity." + speciesKey(e) + ".ambient"
}
func (s *Session) tickEntitySounds(dt float64) {
if s.Sound == nil {
return
}
for _, e := range s.Entities.List() {
if e.Dead || e.Kind == entity.KindItemDrop {
continue
}
e.AmbientT++
if s.rng.Intn(1000) < e.AmbientT {
e.AmbientT = -80
s.Sound(ambientSound(e), e.Pos[0], e.Pos[1]+1, e.Pos[2])
}
e.WalkDist += math.Hypot(e.Vel[0], e.Vel[2]) * dt * 0.6
if e.WalkDist < 1 {
continue
}
e.WalkDist -= math.Floor(e.WalkDist)
bx := int32(math.Floor(e.Pos[0]))
by := int32(math.Floor(e.Pos[1] - 0.05))
bz := int32(math.Floor(e.Pos[2]))
def := s.Reg.Get(s.World.Block(bx, by, bz).ID())
if def.ID != 0 {
s.Sound(def.StepSound(), e.Pos[0], e.Pos[1], e.Pos[2])
}
}
}
func (s *Session) tickFallDamage(dt, vy float64) {
p := s.Player
if p.Flying || p.InWater {
s.fallDist = 0
return
}
if !p.OnGround {
if vy < 0 {
s.fallDist += -vy * dt
}
return
}
dist := s.fallDist
s.fallDist = 0
if dist < 3 {
return
}
dmg := 0
if vy < -16 {
dmg = int((-vy - 16) / 4)
}
if dmg > 0 {
if s.Sound != nil {
s.Sound("entity.player.big_fall", p.Pos[0], p.Pos[1], p.Pos[2])
}
s.Health -= dmg
if s.Sound != nil {
s.Sound("entity.player.hurt", p.Pos[0], p.Pos[1]+1, p.Pos[2])
}
if s.Health <= 0 {
s.kill("摔死了")
}
return
}
if s.Sound != nil {
s.Sound("entity.player.small_fall", p.Pos[0], p.Pos[1], p.Pos[2])
}
}
func (s *Session) tickDrown(dt float64) {
if s.Player.EyeInWater && s.Player.Air <= 0 {
s.drownAcc += dt
if s.drownAcc >= 1 {
s.drownAcc = 0
s.Health -= 4
if s.Sound != nil {
s.Sound("entity.player.hurt", s.Player.Pos[0], s.Player.Pos[1]+1, s.Player.Pos[2])
}
if s.Health <= 0 {
s.kill("淹死了")
}
}
} else {
s.drownAcc = 0
}
}
func (s *Session) tickFootsteps(dt float64) {
p := s.Player
if p.InWater {
if !s.wasInWater && s.Sound != nil {
s.Sound("entity.generic.splash", p.Pos[0], p.Pos[1], p.Pos[2])
}
s.wasInWater = true
s.swimAcc += math.Hypot(p.Vel[0], p.Vel[2]) * dt
if s.swimAcc >= 0.8 {
s.swimAcc = 0
if s.Sound != nil {
s.Sound("entity.generic.swim", p.Pos[0], p.Pos[1], p.Pos[2])
}
}
return
}
s.wasInWater = false
if !p.OnGround {
return
}
s.walkDist += math.Hypot(p.Vel[0], p.Vel[2]) * dt * 0.6
if s.walkDist < s.nextStep {
return
}
s.nextStep = math.Floor(s.walkDist) + 1
bx := int32(math.Floor(p.Pos[0]))
by := int32(math.Floor(p.Pos[1] - 0.05))
bz := int32(math.Floor(p.Pos[2]))
def := s.Reg.Get(s.World.Block(bx, by, bz).ID())
if def.ID == 0 {
return
}
if s.Sound != nil {
s.Sound(def.StepSound(), p.Pos[0], p.Pos[1], p.Pos[2])
}
}
// TryEat 右键食物:饥饿未满则开始 1.6s 食用(音频.md §6.1)。
func (s *Session) TryEat() bool {
if s.Dead || s.eatLeft > 0 || s.Hunger >= 20 {
return false
}
sel := s.Inv.SelectedStack()
d, ok := s.Items.Get(sel.Name)
if !ok || d.Food <= 0 {
return false
}
s.eatLeft = 1.6
s.eatName = sel.Name
s.eatAcc = 0
return true
}
func (s *Session) tickEat(dt float64) {
if s.eatLeft <= 0 {
return
}
s.eatLeft -= dt
s.eatAcc += dt
if s.eatAcc >= 0.2 {
s.eatAcc = 0
if s.Sound != nil {
s.Sound("entity.generic.eat", s.Player.Pos[0], s.Player.Pos[1]+1.5, s.Player.Pos[2])
}
}
if s.eatLeft > 0 {
return
}
d, ok := s.Items.Get(s.eatName)
if ok {
s.Hunger += d.Food
if s.Hunger > 20 {
s.Hunger = 20
}
s.Saturation += d.Saturation
if s.Saturation > float64(s.Hunger) {
s.Saturation = float64(s.Hunger)
}
s.Inv.Remove(s.eatName, 1)
}
s.eatName = ""
if s.Sound != nil {
s.Sound("entity.player.burp", s.Player.Pos[0], s.Player.Pos[1]+1.5, s.Player.Pos[2])
}
}