From 5d0524ca52db621e403a34950b0e171acfef619b Mon Sep 17 00:00:00 2001 From: NianGao Dev Date: Sun, 16 Aug 2026 22:32:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(game):=20=E6=8E=89=E8=90=BD=E7=89=A9?= =?UTF-8?q?=E5=8D=95=E5=A0=86=E5=8F=A0=E4=B8=8A=E9=99=90=209999=20+=20?= =?UTF-8?q?=E6=8B=BE=E5=8F=96=E6=8C=89=E6=A0=88=E4=B8=8A=E9=99=90=E6=8B=86?= =?UTF-8?q?=E5=88=86=EF=BC=9B=E6=8C=96=E6=8E=98=E6=8C=89=E4=BD=8F=E5=BE=AA?= =?UTF-8?q?=E7=8E=AF=E6=8C=A5=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 地上掉落合并上限 64→9999,超出生成新堆(TestDropStackCap) - 拾取按物品栈上限拆成多栈入包,放不下回吐地面 - 按住左键挖掘时每 0.3 秒循环挥动手部(swingTimer), 挥动幅度收窄并抬高持手位置保持可见 - 新增 -digtest/-swingtest dev 截图测试开关 --- cmd/client/client_gl.go | 53 +++++++++++++++++++++++++++------- internal/entity/drops.go | 8 ++--- internal/entity/entity_test.go | 25 ++++++++++++++++ internal/game/game.go | 27 +++++++++++++++-- 文档/场景/实体系统.md | 2 +- 5 files changed, 97 insertions(+), 18 deletions(-) diff --git a/cmd/client/client_gl.go b/cmd/client/client_gl.go index 22a7faa0..81fb6cbc 100644 --- a/cmd/client/client_gl.go +++ b/cmd/client/client_gl.go @@ -66,6 +66,8 @@ var ( invTest = flag.Bool("invtest", false, "dev 测试:截图前打开物品栏(E 键界面)并放入若干物品") miniDump = flag.String("minimapdump", "", "dev 诊断:保存一次小地图图像到该路径") waterTest = flag.Bool("underwatertest", false, "dev 测试:截图前置水下渲染状态(验证蓝色叠加)") + digTest = flag.Bool("digtest", false, "dev 测试:截图前在视线前方挖一个 2×2×2 的坑(验证挖洞后洞壁光照)") + swingTest = flag.Bool("swingtest", false, "dev 测试:截图帧强制手部挥动(验证挖掘挥动动画)") ) // clientApp 客户端应用:把会话与渲染粘合起来(engine.Ticker + engine.Renderer)。 @@ -116,13 +118,14 @@ type clientApp struct { cursorReq atomic.Int32 // 手持方块(tick 计算 UV → 渲染线程上传,渲染.md §5.2) - handInit bool - handSlot int - handHas bool - handDirty bool - handUVs [6][4]float32 - handTint [3]float32 // 手持颜色乘数(空手肤色拳 / 持方块 1,1,1) - swing float64 // 挥动动画剩余量(左/右键触发,渲染线程衰减) + handInit bool + handSlot int + handHas bool + handDirty bool + handUVs [6][4]float32 + handTint [3]float32 // 手持颜色乘数(空手肤色拳 / 持方块 1,1,1) + swing float64 // 挥动动画剩余量(左/右键触发,渲染线程衰减) + swingTimer float64 // 按住挖掘时周期性挥动的节拍器 // 水下状态(渲染线程读;tick 更新) underwater bool @@ -152,6 +155,7 @@ type clientApp struct { invShot bool // dev 测试:-invtest 已注入 miniDumped bool // dev 诊断:-minimapdump 已保存 waterShot bool // dev 测试:-underwatertest 已注入 + digShot bool // dev 测试:-digtest 已注入 } // worldMeshJob 待上传网格。 @@ -302,6 +306,16 @@ func (a *clientApp) Tick(dt float64) { if a.in.breakDown || a.in.placeDown { a.swing = 1.0 } + // 按住挖掘时周期性挥动(原版挖掘节奏,约每 0.3 秒一次) + if a.in.breakHeld { + a.swingTimer -= dt + if a.swingTimer <= 0 { + a.swingTimer = 0.3 + a.swing = 1.0 + } + } else { + a.swingTimer = 0 + } a.in.apply(a.sess) // 水下检测(相机所在方块为水 → 水下渲染效果) camB := a.sess.World.Block(int32(a.sess.Cam.Pos[0]), int32(a.sess.Cam.Pos[1]), int32(a.sess.Cam.Pos[2])) @@ -798,7 +812,21 @@ func (a *clientApp) Render() { } a.cursorReq.Store(-1) } - // dev 测试:截图前注入破坏裂纹/实体/ESC/工作台/设置/物品栏/水下(必须在 3D 绘制之前,截图帧才能拍到) + // dev 测试:挖坑类测试提前注入(留足网格重建时间);其余截图前注入 + if a.frames == 100 && *digTest && !a.digShot { + a.digShot = true + if h, ok := a.sess.Cast(5); ok { + for dy := int32(0); dy < 2; dy++ { + for dx := int32(-1); dx <= 1; dx++ { + for dz := int32(-1); dz <= 1; dz++ { + a.sess.World.SetBlock(h.X+dx, h.Y-dy, h.Z+dz, block.Air) + } + } + } + a.logf("digtest 注入: 挖坑 (%d,%d,%d)", h.X, h.Y, h.Z) + } + } + // dev 测试:截图前注入破坏裂纹/实体/ESC/工作台/设置/物品栏/水下/挖坑(必须在 3D 绘制之前,截图帧才能拍到) if a.frames == 195 && (*crackStage >= 0 || *spawnTest != "" || *escTest || *craftTest || *optTest || *invTest || *waterTest) { a.injectDevVisuals() } @@ -849,6 +877,9 @@ func (a *clientApp) Render() { } // 手持方块上传与绘制(渲染.md §5.2;上传必须是渲染线程) // 挥动动画:左/右键后手部斜向下摆,逐帧重传立方体 + if *swingTest && a.frames >= 198 { + a.swing = 1.0 // dev 测试:截图帧强制挥动 + } if a.swing > 0 { a.swing -= 0.12 if a.swing < 0 { @@ -865,10 +896,10 @@ func (a *clientApp) Render() { handTint = a.handTint } a.mu.Unlock() - swingPos := [3]float32{0.46, -0.44, -0.66} + swingPos := [3]float32{0.46, -0.34, -0.66} if a.swing > 0 { - swingPos[0] -= float32(a.swing) * 0.22 // 挥动:右移 + 下压 - swingPos[1] -= float32(a.swing) * 0.18 + swingPos[0] -= float32(a.swing) * 0.15 // 挥动:向屏幕中心下方摆动(保持手部可见) + swingPos[1] -= float32(a.swing) * 0.10 handDirty = true // 挥动期间逐帧重传位置 } if handDirty { diff --git a/internal/entity/drops.go b/internal/entity/drops.go index 180510e9..38f8d9ae 100644 --- a/internal/entity/drops.go +++ b/internal/entity/drops.go @@ -8,10 +8,10 @@ import ( // 掉落物参数(实体系统.md §5)。 const ( - dropMergeDist = 1.0 // 同物品合并距离 - dropStackMax = 64 // 合并上限 - dropPickupDist = 1.5 // 拾取半径 - dropPerChunk = 32 // 每区块掉落物上限 + dropMergeDist = 1.0 // 同物品合并距离 + dropStackMax = 9999 // 单堆叠合并上限(地上大堆;拾取时按背包栈上限拆分) + dropPickupDist = 1.5 // 拾取半径 + dropPerChunk = 32 // 每区块掉落物上限 ) // dropPickupDelay 生成后可拾取延迟(测试可置 0)。 diff --git a/internal/entity/entity_test.go b/internal/entity/entity_test.go index 1baae10c..c299dc96 100644 --- a/internal/entity/entity_test.go +++ b/internal/entity/entity_test.go @@ -87,6 +87,31 @@ func TestDropMerge(t *testing.T) { } } +// TestDropStackCap 单堆叠上限 9999:合并到上限后,多余部分生成新堆(实体系统.md §5)。 +func TestDropStackCap(t *testing.T) { + m := NewManager() + m.SpawnDrop("cobblestone", 9999, 0.5, 64, 0.5) + m.SpawnDrop("cobblestone", 5, 0.6, 64, 0.5) // 9999+5 超上限 → 不合并,生成新堆 + cnt, total := 0, 0 + for _, e := range m.List() { + if e.Kind != KindItemDrop { + continue + } + d, ok := e.Data.(*Drop) + if !ok { + t.Fatalf("掉落物数据缺失: %+v", e.Data) + } + cnt++ + total += d.Count + if d.Count > dropStackMax { + t.Fatalf("单堆超上限: %d > %d", d.Count, dropStackMax) + } + } + if cnt != 2 || total != 10004 { + t.Fatalf("期望 2 堆共 10004,实际 %d 堆共 %d", cnt, total) + } +} + // TestBlueprint 房屋蓝图确定性 + 建造执行顺序(村民系统.md §6)。 func TestBlueprint(t *testing.T) { a := Blueprint([3]int32{10, 64, 10}, 42) diff --git a/internal/game/game.go b/internal/game/game.go index dff7dc7d..596979d9 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -91,9 +91,9 @@ func (s *Session) Tick(dt float64) { s.Entities.Tick(dt, s.World, s.Player.Pos) // 方块实体(熔炉烧炼,物品与背包.md §6) s.tickBlockEntities() - // 掉落物拾取(实体系统.md §5) + // 掉落物拾取(实体系统.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))}) + s.pickupDrop(d) } // 夜间刷僵尸(僵尸与敌对生物.md §5:黑暗处、距玩家 24 格、限频) s.spawnZombies(dt) @@ -246,6 +246,29 @@ func (s *Session) DropSelected() { 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 + } + leftover := s.Inv.Add(item.Stack{Name: d.Item, Count: uint8(chunk)}) + 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 diff --git a/文档/场景/实体系统.md b/文档/场景/实体系统.md index fd804dfa..f9608d7a 100644 --- a/文档/场景/实体系统.md +++ b/文档/场景/实体系统.md @@ -53,7 +53,7 @@ spawn → tick(激活区块内)→ damage → death → 掉落物 → despaw ## 5. 掉落物实体 - 破坏方块 → 掉落物实体(物品栈 + 世界位置 + 初速度)。 -- **合并**:同物品栈(id+meta 相同)且距离 < 1 格时合并(上限 64)。 +- **合并**:同物品栈(id+meta 相同)且距离 < 1 格时合并(单堆上限 9999,超出生成新堆;拾取时按背包栈上限拆分入包,装不下的重新掉回地面)。 - 拾取:玩家 AABB 与掉落物重叠 → 进背包(走 物品与背包.md 规则);拾取冷却 0.5s 防瞬吸。 - 上限:每个区块掉落物 ≤ 32 个,超出清除最旧(防堆积卡服)。