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

236 lines
6.4 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.
//go:build gl
// 界面屏幕:主菜单与暂停菜单(UI还原.md §2:427×240 × guiScale)。
package ui
// Button 一个按钮:区域在 427×240 虚拟空间。
type Button struct {
X, Y, W, H float32
Tex uint32
LabelW float32
LabelH float32
Action string
}
// TitleScreen 主菜单(UI还原.md §2:3D 全景背景 + 缩放控件)。
type TitleScreen struct {
TitleTex uint32
TitleW float32
TitleH float32
SplashTex uint32
SplashW float32
SplashH float32
BgTex uint32
BtnTex uint32
BtnHover uint32
Buttons []Button
Version string
Copyright string
VersionTex uint32
VersionW float32
VersionH float32
CopyrightTex uint32
CopyrightW float32
CopyrightH float32
scale, ox, oy float32
worldBg bool
}
// Layout 按窗口与整数 guiScale 放置 427×240 虚拟画布(UI还原.md §3.1)。
func (s *TitleScreen) Layout(winW, winH, scale float32) {
if scale < 1 {
scale = 1
}
s.scale = scale
vw, vh := 427*scale, 240*scale
s.ox = (winW - vw) / 2
s.oy = (winH - vh) / 2
}
func (s *TitleScreen) mapped(vx, vy, vw, vh float32) (x, y, w, h float32) {
sc := s.scale
if sc < 1 {
sc = 1
}
return s.ox + vx*sc, s.oy + vy*sc, vw * sc, vh * sc
}
// SetWorldBg 已绘制 3D 世界时只加遮罩,不再铺泥土墙。
func (s *TitleScreen) SetWorldBg(ok bool) { s.worldBg = ok }
// HitTest 命中检测(屏幕像素)。
func (s *TitleScreen) HitTest(mx, my float32) string {
for _, b := range s.Buttons {
x, y, w, h := s.mapped(b.X, b.Y, b.W, b.H)
if mx >= x && mx <= x+w && my >= y && my <= y+h {
return b.Action
}
}
return ""
}
// Draw 绘制主菜单(遮罩 → 标题+splash → 按钮 → 角标)。
func (s *TitleScreen) Draw(r *Renderer, mx, my float32) {
w, h := float32(r.w), float32(r.h)
if s.worldBg {
r.DrawRect(0, 0, w, h, [4]float32{0, 0, 0, 0.38})
} else if s.BgTex != 0 {
r.pushQuad(quad{x: 0, y: 0, w: w, h: h, u0: 0, v0: 0, u1: 1, v1: 1,
r: 1, g: 1, b: 1, a: 1, tex: s.BgTex})
r.DrawRect(0, 0, w, h, [4]float32{0, 0, 0, 0.25})
} else {
r.DrawRect(0, 0, w, h, [4]float32{0.08, 0.1, 0.12, 1})
}
sc := s.scale
if sc < 1 {
sc = 1
}
if s.TitleTex != 0 && s.TitleH > 0 {
th := float32(42) * sc
tw := s.TitleW / s.TitleH * th
r.pushQuad(quad{x: w/2 - tw/2, y: s.oy + 16*sc, w: tw, h: th,
u0: 0, v0: 0, u1: 1, v1: 1, r: 1, g: 1, b: 1, a: 1, tex: s.TitleTex})
if s.SplashTex != 0 {
sh := float32(12) * sc
sw := s.SplashW / s.SplashH * sh
r.pushQuad(quad{x: w/2 + tw/2 - sw, y: s.oy + 16*sc + th + 2*sc, w: sw, h: sh,
u0: 0, v0: 0, u1: 1, v1: 1, r: 1, g: 1, b: 1, a: 1, tex: s.SplashTex})
}
}
for _, b := range s.Buttons {
x, y, bw, bh := s.mapped(b.X, b.Y, b.W, b.H)
hover := mx >= x && mx <= x+bw && my >= y && my <= y+bh
tex := s.BtnTex
if hover && s.BtnHover != 0 {
tex = s.BtnHover
}
if tex != 0 {
r.pushQuad(quad{x: x, y: y, w: bw, h: bh, u0: 0, v0: 0, u1: 1, v1: 1,
r: 1, g: 1, b: 1, a: 1, tex: tex})
}
if b.Tex != 0 {
lw, lh := b.LabelW*sc*0.55, b.LabelH*sc*0.55
r.pushQuad(quad{x: x + (bw-lw)/2, y: y + (bh-lh)/2, w: lw, h: lh,
u0: 0, v0: 0, u1: 1, v1: 1, r: 1, g: 1, b: 1, a: 1, tex: b.Tex})
}
}
if s.VersionTex != 0 {
r.pushQuad(quad{x: 8, y: h - s.VersionH - 8, w: s.VersionW, h: s.VersionH,
u0: 0, v0: 0, u1: 1, v1: 1, r: 1, g: 1, b: 1, a: 1, tex: s.VersionTex})
} else if s.Version != "" {
r.DrawText(8, h-12, s.Version, 1, [4]float32{1, 1, 1, 1})
}
if s.CopyrightTex != 0 {
r.pushQuad(quad{x: w - s.CopyrightW - 8, y: h - s.CopyrightH - 8, w: s.CopyrightW, h: s.CopyrightH,
u0: 0, v0: 0, u1: 1, v1: 1, r: 1, g: 1, b: 1, a: 1, tex: s.CopyrightTex})
} else if s.Copyright != "" {
r.DrawText(w-r.TextWidth(s.Copyright, 1)-8, h-12, s.Copyright, 1, [4]float32{1, 1, 1, 1})
}
}
// PauseScreen 暂停菜单(UI还原.md §2:半透明遮罩 + 缩放按钮)。
type PauseScreen struct {
TitleTex uint32
TitleW float32
TitleH float32
BtnTex uint32
BtnHover uint32
Buttons []Button
scale, ox, oy float32
}
// Layout 与主菜单同一套 427×240 缩放。
func (s *PauseScreen) Layout(winW, winH, scale float32) {
if scale < 1 {
scale = 1
}
s.scale = scale
vw, vh := 427*scale, 240*scale
s.ox = (winW - vw) / 2
s.oy = (winH - vh) / 2
}
func (s *PauseScreen) mapped(vx, vy, vw, vh float32) (x, y, w, h float32) {
sc := s.scale
if sc < 1 {
sc = 1
}
return s.ox + vx*sc, s.oy + vy*sc, vw * sc, vh * sc
}
// HitTest 命中检测。
func (s *PauseScreen) HitTest(mx, my float32) string {
for _, b := range s.Buttons {
x, y, w, h := s.mapped(b.X, b.Y, b.W, b.H)
if mx >= x && mx <= x+w && my >= y && my <= y+h {
return b.Action
}
}
return ""
}
// Draw 绘制暂停菜单。
func (s *PauseScreen) Draw(r *Renderer, mx, my float32) {
w, h := float32(r.w), float32(r.h)
r.DrawRect(0, 0, w, h, [4]float32{0, 0, 0, 0.6})
sc := s.scale
if sc < 1 {
sc = 1
}
if s.TitleTex != 0 && s.TitleH > 0 {
th := float32(18) * sc
tw := s.TitleW / s.TitleH * th
r.pushQuad(quad{x: w/2 - tw/2, y: s.oy + 28*sc, w: tw, h: th,
u0: 0, v0: 0, u1: 1, v1: 1, r: 1, g: 1, b: 1, a: 1, tex: s.TitleTex})
}
for _, b := range s.Buttons {
x, y, bw, bh := s.mapped(b.X, b.Y, b.W, b.H)
hover := mx >= x && mx <= x+bw && my >= y && my <= y+bh
tex := s.BtnTex
if hover && s.BtnHover != 0 {
tex = s.BtnHover
}
if tex != 0 {
r.pushQuad(quad{x: x, y: y, w: bw, h: bh, u0: 0, v0: 0, u1: 1, v1: 1,
r: 1, g: 1, b: 1, a: 1, tex: tex})
}
if b.Tex != 0 {
lw, lh := b.LabelW*sc*0.55, b.LabelH*sc*0.55
r.pushQuad(quad{x: x + (bw-lw)/2, y: y + (bh-lh)/2, w: lw, h: lh,
u0: 0, v0: 0, u1: 1, v1: 1, r: 1, g: 1, b: 1, a: 1, tex: b.Tex})
}
}
}
// DeathScreen 死亡界面(UI还原.md §2:原因 + 重生 / 回标题;screen=11)。
type DeathScreen struct {
PauseScreen
Cause string
CauseTex uint32
CauseW float32
CauseH float32
}
func (s *DeathScreen) Draw(r *Renderer, mx, my float32) {
s.PauseScreen.Draw(r, mx, my)
sc := s.scale
if sc < 1 {
sc = 1
}
w := float32(r.w)
if s.CauseTex != 0 && s.CauseH > 0 {
th := float32(14) * sc
tw := s.CauseW / s.CauseH * th
r.pushQuad(quad{x: w/2 - tw/2, y: s.oy + 56*sc, w: tw, h: th,
u0: 0, v0: 0, u1: 1, v1: 1, r: 1, g: 1, b: 1, a: 1, tex: s.CauseTex})
return
}
if s.Cause != "" {
r.DrawText(w/2-r.TextWidth(s.Cause, 1)/2, s.oy+56*sc, s.Cause, 1, [4]float32{1, 0.7, 0.7, 1})
}
}