1377 lines
35 KiB
Go
1377 lines
35 KiB
Go
// 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])
|
||
}
|
||
}
|