298 lines
7.5 KiB
Go
298 lines
7.5 KiB
Go
//go:build gl
|
||
|
||
// 客户端输入处理:键鼠状态采集(UI与输入.md §6)。
|
||
//
|
||
// 设计:回调只更新状态,游戏逻辑在 tick 里消费状态(主循环解耦)。
|
||
package main
|
||
|
||
import (
|
||
"math"
|
||
|
||
"mc/internal/game"
|
||
"mc/internal/physics"
|
||
|
||
"github.com/go-gl/glfw/v3.3/glfw"
|
||
)
|
||
|
||
// inputState 键鼠输入状态。
|
||
type inputState struct {
|
||
win *glfw.Window
|
||
|
||
// 按键状态
|
||
forward, back, left, right, jump, sneak, sprint bool
|
||
|
||
// 鼠标
|
||
lastX, lastY float64
|
||
firstMouse bool
|
||
dyaw, dpitch float64 // 累计角度增量(弧度,tick 消费)
|
||
sens float64 // 灵敏度(设置界面可调,client.yaml mouse_sensitivity)
|
||
|
||
// 按键事件(tick 消费)
|
||
breakDown bool
|
||
breakHeld bool
|
||
placeDown bool
|
||
placeHeld bool
|
||
selectSlot int // -1 无
|
||
toggleInv bool // E 打开/关闭物品栏
|
||
dropItem bool // Q 丢弃选中槽 1 个物品
|
||
|
||
// 鼠标与 UI 事件(UI与输入.md §6 屏幕交互)
|
||
cursorX, cursorY float64
|
||
clickX, clickY float32
|
||
clicked bool
|
||
clickedRight bool
|
||
escPressed bool
|
||
toggleFS bool // F11 / Alt+Enter(渲染线程消费,渲染.md §5.4)
|
||
|
||
textMode bool
|
||
chars []rune
|
||
backspace bool
|
||
}
|
||
|
||
// newInputState 创建输入状态并注册回调。
|
||
func newInputState(win *glfw.Window) *inputState {
|
||
in := &inputState{win: win, firstMouse: true, selectSlot: -1, sens: 0.0025}
|
||
win.SetKeyCallback(in.keyCallback)
|
||
win.SetMouseButtonCallback(in.mouseButtonCallback)
|
||
win.SetCursorPosCallback(in.cursorCallback)
|
||
win.SetScrollCallback(in.scrollCallback)
|
||
win.SetCharCallback(in.charCallback)
|
||
return in
|
||
}
|
||
|
||
// SetSensitivity 设置鼠标灵敏度(弧度/像素,设置界面)。
|
||
func (in *inputState) SetSensitivity(s float64) {
|
||
if s > 0 {
|
||
in.sens = s
|
||
}
|
||
}
|
||
|
||
// Sensitivity 当前灵敏度。
|
||
func (in *inputState) Sensitivity() float64 { return in.sens }
|
||
|
||
// keyCallback 键盘回调(动作映射:WASD/空格/Shift/Ctrl/Esc)。
|
||
func (in *inputState) keyCallback(_ *glfw.Window, key glfw.Key, _ int, action glfw.Action, mods glfw.ModifierKey) {
|
||
down := action != glfw.Release
|
||
switch key {
|
||
case glfw.KeyW:
|
||
in.forward = down
|
||
case glfw.KeyS:
|
||
in.back = down
|
||
case glfw.KeyA:
|
||
in.left = down
|
||
case glfw.KeyD:
|
||
in.right = down
|
||
case glfw.KeySpace:
|
||
in.jump = down
|
||
case glfw.KeyLeftShift:
|
||
in.sneak = down
|
||
case glfw.KeyLeftControl:
|
||
in.sprint = down
|
||
case glfw.KeyEscape:
|
||
if action == glfw.Press {
|
||
in.escPressed = true // 暂停/继续由屏幕状态机处理
|
||
}
|
||
case glfw.KeyBackspace:
|
||
if action != glfw.Release && in.textMode {
|
||
in.backspace = true
|
||
}
|
||
case glfw.Key1, glfw.Key2, glfw.Key3, glfw.Key4, glfw.Key5, glfw.Key6, glfw.Key7, glfw.Key8, glfw.Key9:
|
||
if action == glfw.Press {
|
||
in.selectSlot = int(key) - int(glfw.Key1)
|
||
}
|
||
case glfw.KeyE:
|
||
if action == glfw.Press {
|
||
in.toggleInv = true // 物品栏开关(UI还原.md §3.3)
|
||
}
|
||
case glfw.KeyQ:
|
||
if action == glfw.Press {
|
||
in.dropItem = true // 丢弃物品(UI与输入.md §5)
|
||
}
|
||
case glfw.KeyF11:
|
||
if action == glfw.Press {
|
||
in.toggleFS = true
|
||
}
|
||
case glfw.KeyEnter, glfw.KeyKPEnter:
|
||
if action == glfw.Press && mods&glfw.ModAlt != 0 {
|
||
in.toggleFS = true
|
||
}
|
||
}
|
||
}
|
||
|
||
func (in *inputState) charCallback(_ *glfw.Window, char rune) {
|
||
if !in.textMode {
|
||
return
|
||
}
|
||
in.chars = append(in.chars, char)
|
||
}
|
||
|
||
// mouseButtonCallback 鼠标按键:左键破坏(按住累积)、右键放置(UI与输入.md §6 长按)。
|
||
func (in *inputState) mouseButtonCallback(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, _ glfw.ModifierKey) {
|
||
switch button {
|
||
case glfw.MouseButtonLeft:
|
||
if action == glfw.Press {
|
||
in.breakDown = true
|
||
in.clicked = true
|
||
in.clickX = float32(in.cursorX)
|
||
in.clickY = float32(in.cursorY)
|
||
}
|
||
in.breakHeld = action != glfw.Release
|
||
case glfw.MouseButtonRight:
|
||
if action == glfw.Press {
|
||
in.placeDown = true
|
||
in.clickedRight = true
|
||
in.clickX = float32(in.cursorX)
|
||
in.clickY = float32(in.cursorY)
|
||
}
|
||
in.placeHeld = action != glfw.Release
|
||
}
|
||
}
|
||
|
||
// cursorCallback 鼠标移动:yaw/pitch 累积(渲染.md §2 主线程消费)。
|
||
// 约定:鼠标右移 dx>0 应右转(yaw 减小——yaw=0 朝向 -Z,右转朝 +X);
|
||
// 鼠标上移 dy<0 应抬头(pitch 增大)。
|
||
func (in *inputState) cursorCallback(_ *glfw.Window, x, y float64) {
|
||
in.cursorX, in.cursorY = x, y
|
||
if in.firstMouse {
|
||
in.lastX, in.lastY = x, y
|
||
in.firstMouse = false
|
||
return
|
||
}
|
||
dx, dy := x-in.lastX, y-in.lastY
|
||
in.lastX, in.lastY = x, y
|
||
in.dyaw -= dx * in.sens
|
||
in.dpitch -= dy * in.sens
|
||
}
|
||
|
||
// scrollCallback 滚轮切热键栏(UI与输入.md §6)。
|
||
func (in *inputState) scrollCallback(_ *glfw.Window, _, yoff float64) {
|
||
if yoff > 0 {
|
||
in.selectSlot = -2 // 上滚
|
||
} else if yoff < 0 {
|
||
in.selectSlot = -3 // 下滚
|
||
}
|
||
}
|
||
|
||
// apply 每 tick 把输入状态转成玩家动作(game.Session 驱动)。
|
||
func (in *inputState) apply(s *game.Session) {
|
||
cam := s.Cam
|
||
p := s.Player
|
||
|
||
// 视角(俯仰钳制在相机上,而非增量上——旧实现钳增量,抬头可翻转超过 ±90°)
|
||
if in.dyaw != 0 || in.dpitch != 0 {
|
||
cam.Yaw += in.dyaw
|
||
cam.Pitch += in.dpitch
|
||
in.dyaw, in.dpitch = 0, 0
|
||
const maxPitch = math.Pi/2 - 0.01
|
||
if cam.Pitch > maxPitch {
|
||
cam.Pitch = maxPitch
|
||
}
|
||
if cam.Pitch < -maxPitch {
|
||
cam.Pitch = -maxPitch
|
||
}
|
||
}
|
||
// 游戏中左键=破坏,不是 UI 点击:清除遗留点击状态,防止暂停菜单收到过期点击
|
||
in.clicked = false
|
||
|
||
// 水平移动(物理与碰撞.md §3:走/冲刺/潜行)
|
||
f := cam.Forward()
|
||
r := cam.Right()
|
||
wish := [2]float64{}
|
||
if in.forward {
|
||
wish[0] += f[0]
|
||
wish[1] += f[2]
|
||
}
|
||
if in.back {
|
||
wish[0] -= f[0]
|
||
wish[1] -= f[2]
|
||
}
|
||
if in.right {
|
||
wish[0] += r[0]
|
||
wish[1] += r[2]
|
||
}
|
||
if in.left {
|
||
wish[0] -= r[0]
|
||
wish[1] -= r[2]
|
||
}
|
||
speed := 4.3
|
||
if p.InWater {
|
||
speed = 2.2
|
||
if in.sprint && in.forward {
|
||
speed = 5.4
|
||
}
|
||
} else if in.sprint {
|
||
speed *= 1.3
|
||
}
|
||
if in.sneak && !p.InWater {
|
||
speed *= 0.3
|
||
}
|
||
l := math.Sqrt(wish[0]*wish[0] + wish[1]*wish[1])
|
||
if l > 0 {
|
||
p.Vel[0] = wish[0] / l * speed
|
||
p.Vel[2] = wish[1] / l * speed
|
||
} else {
|
||
p.Vel[0] = 0
|
||
p.Vel[2] = 0
|
||
}
|
||
|
||
// 跳跃 / 游泳(物理与碰撞.md §4:水中空格上浮,不要求落地)
|
||
if in.jump {
|
||
if p.InWater {
|
||
p.SwimUp()
|
||
} else if p.OnGround {
|
||
p.Jump()
|
||
}
|
||
}
|
||
if in.sneak && p.InWater {
|
||
p.SwimDown()
|
||
}
|
||
p.SetPose(s.World, in.sneak && !p.InWater, in.sprint && in.forward && p.InWater)
|
||
|
||
// 热键栏切换(UI与输入.md §6:上滚槽位−1,下滚+1,对齐原版 Java)
|
||
switch in.selectSlot {
|
||
case -2:
|
||
s.Inv.Selected = (s.Inv.Selected + 8) % 9
|
||
case -3:
|
||
s.Inv.Selected = (s.Inv.Selected + 1) % 9
|
||
default:
|
||
if in.selectSlot >= 0 && in.selectSlot < 9 {
|
||
s.Inv.Selected = in.selectSlot
|
||
}
|
||
}
|
||
in.selectSlot = -1
|
||
|
||
// 左键:按下边沿攻击一次;按住连续破坏(方块交互与动画.md §3)
|
||
if in.breakDown {
|
||
if !s.Attack() {
|
||
if _, ok := s.Cast(5); !ok {
|
||
s.SwingMiss()
|
||
}
|
||
s.BeginBreak()
|
||
}
|
||
in.breakDown = false
|
||
} else if in.breakHeld {
|
||
if !s.Breaking() {
|
||
s.BeginBreak()
|
||
} else {
|
||
s.ContinueBreak()
|
||
}
|
||
} else if s.Breaking() || s.BreakProgress() != 0 {
|
||
s.EndBreak()
|
||
}
|
||
|
||
// 放置(右键;命中工作台/熔炉/箱子则打开界面)
|
||
if in.placeDown {
|
||
if !s.TryOpenCrafting() && !s.TryOpenFurnace() && !s.TryOpenChest() && !s.TryFlickLever() && !s.TryUseEntity() && !s.TryEat() {
|
||
s.PlaceSelected()
|
||
}
|
||
in.placeDown = false
|
||
}
|
||
// 丢弃选中槽 1 个物品(UI与输入.md §5 Q 键)
|
||
if in.dropItem {
|
||
s.DropSelected()
|
||
in.dropItem = false
|
||
}
|
||
}
|
||
|
||
var _ = physics.Player{} // 保持引用(后续输入扩展用)
|