- 音频(音频.md §2):Session.Sound 事件回调,破坏/放置/攻击/村民环境音接入, 客户端 audio.Manager 3D 衰减 + 监听器跟随 + 音量可调 - 原版掉落:草方块→泥土、石头→圆石、树叶无掉落(no_drop);掉落物实体 用物品图标小广告牌渲染(物品纹理并入实体图集) - 动物攻击(动物体系.md §5):左键优先射线命中实体(半径 0.6 球体判定), 伤害/击退/死亡移除+肉类掉落;僵尸掉腐肉 - 村庄村民(村民系统.md §2):Generator.VillageAt 定位村庄中心,会话扫描 已加载区块在村庄生成 3 名村民 - 种子与灵敏度配置:client.yaml world.seed/name、mouse_sensitivity; 存档与种子联动;鼠标左右反转修复(右移应右转) - 设置界面(UI与输入.md §6):屏幕状态 4,灵敏度/音量滑条、世界种子显示、 按键教程多行列表(cjkfont 支持 \\n)、完成按钮;主菜单/暂停菜单入口 - 准星指向方块名:左下角 CJK 显示(lang 本地化,tick 光栅化渲染线程上传) - 截图统一存项目根目录 jietu/(paths.RootDir;dev 构建识别 bin/ 布局)
78 lines
2.9 KiB
Go
78 lines
2.9 KiB
Go
package camera
|
||
|
||
import (
|
||
"math"
|
||
"testing"
|
||
)
|
||
|
||
// apply 用视图矩阵变换世界点(列主序语义,与 GL 一致)。
|
||
func apply(m *[16]float32, wx, wy, wz float64) (vx, vy, vz float64) {
|
||
vx = float64(m[0])*wx + float64(m[4])*wy + float64(m[8])*wz + float64(m[12])
|
||
vy = float64(m[1])*wx + float64(m[5])*wy + float64(m[9])*wz + float64(m[13])
|
||
vz = float64(m[2])*wx + float64(m[6])*wy + float64(m[10])*wz + float64(m[14])
|
||
return
|
||
}
|
||
|
||
// TestViewPitchSteep 验证陡俯仰角(低头 60°)下:
|
||
// 相机正下方地面 → 相机前方(z<0)且屏幕下方(y<0);
|
||
// 相机前方地面 → 相机前方;远处地面 → 相机前方且低于视轴下方。
|
||
// 旧实现俯仰符号翻转:正下方/正前方地面点被映射到相机背后(z>0),
|
||
// 低头时画面只剩天空——「视角不跟随鼠标」的根因。
|
||
func TestViewPitchSteep(t *testing.T) {
|
||
c := New([3]float64{8.5, 66.66, 8.5}, 0, -60*math.Pi/180, 70)
|
||
m := c.View()
|
||
cases := []struct {
|
||
name string
|
||
wx, wy, wz float64
|
||
wantBelow bool // 期望屏幕中心下方
|
||
}{
|
||
{"below", 8.5, 64.0, 8.5, true}, // 正下方地面
|
||
{"ahead", 8.5, 64.0, 5.0, false}, // 前方地面
|
||
{"far-ahead", 8.5, 64.0, 0.0, false}, // 远处地面
|
||
}
|
||
for _, tc := range cases {
|
||
vx, vy, vz := apply(&m, tc.wx, tc.wy, tc.wz)
|
||
t.Logf("%s: view=(%.2f,%.2f,%.2f)", tc.name, vx, vy, vz)
|
||
if vz >= 0 {
|
||
t.Errorf("%s: 地面点应在相机前方 (z<0),得到 z=%.2f", tc.name, vz)
|
||
}
|
||
if tc.wantBelow && vy >= 0 {
|
||
t.Errorf("%s: 正下方地面应在屏幕下方 (y<0),得到 y=%.2f", tc.name, vy)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestViewPitchUp 抬头 60°:远处地面应仍在相机前方;正下方(与视线夹角 150°)应在相机后方。
|
||
func TestViewPitchUp(t *testing.T) {
|
||
c := New([3]float64{8.5, 66.66, 8.5}, 0, 60*math.Pi/180, 70)
|
||
m := c.View()
|
||
// 远处地面点(平视前方远处,y 比眼睛低)
|
||
_, _, vz := apply(&m, 8.5, 66.0, -50)
|
||
if vz >= 0 {
|
||
t.Errorf("抬头时远处地面应在相机前方 (z<0),得到 z=%.2f", vz)
|
||
}
|
||
// 正下方点:与视线方向夹角 150°>90°,应在相机后方(z>0)
|
||
_, _, vz = apply(&m, 8.5, 50, 8.5)
|
||
if vz <= 0 {
|
||
t.Errorf("抬头时正下方应在相机后方 (z>0),得到 z=%.2f", vz)
|
||
}
|
||
}
|
||
|
||
// TestViewForward 相机前方向(Forward() 与世界原点前方一点)应映射到视图空间 -z 且 x≈0。
|
||
func TestViewForward(t *testing.T) {
|
||
for _, tc := range []struct{ yaw, pitch float64 }{
|
||
{0, 0}, {0, -0.6}, {0.9, 0.3}, {-2.1, 0.77},
|
||
} {
|
||
c := New([3]float64{1, 2, 3}, tc.yaw, tc.pitch, 70)
|
||
f := c.Forward()
|
||
m := c.View()
|
||
vx, vy, vz := apply(&m, 1+f[0]*5, 2+f[1]*5, 3+f[2]*5)
|
||
if vz >= 0 {
|
||
t.Errorf("yaw=%.2f pitch=%.2f: 前方点应在相机前方,得到 z=%.2f", tc.yaw, tc.pitch, vz)
|
||
}
|
||
if math.Abs(vx) > 1e-3 || math.Abs(vy) > 1e-3 {
|
||
t.Errorf("yaw=%.2f pitch=%.2f: 前方点应位于视轴中心,得到 (%.3f,%.3f)", tc.yaw, tc.pitch, vx, vy)
|
||
}
|
||
}
|
||
}
|