Files
ngzz-mc/cmd/client/screens_gl.go
2026-09-19 14:21:08 +08:00

292 lines
11 KiB
Go
Raw 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——土质背景、原版按钮控件、
// 中文标题「年糕历险记」、黄色 splash、版本/版权角标。
package main
import (
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"os"
"path/filepath"
"mc/internal/assets"
"mc/internal/cjkfont"
"mc/internal/ui"
)
// cjkPaths 系统 CJK 字体候选。
var cjkPaths = []string{
filepath.Join(os.Getenv("WINDIR"), "Fonts", "msyh.ttc"),
filepath.Join(os.Getenv("WINDIR"), "Fonts", "msyh.ttf"),
}
// loadCJK 加载系统 CJK 字体(失败返回 nil → 中文标签退化不显示)。
// 按字号缓存:不同字号(标题 96 / splash 24 / 按钮 18需要各自的光栅化实例。
var cjkCache = map[float64]*cjkfont.Renderer{}
func loadCJK(size float64) *cjkfont.Renderer {
if r, ok := cjkCache[size]; ok {
return r
}
for _, p := range cjkPaths {
if _, err := os.Stat(p); err == nil {
if r, err := cjkfont.Load(p, size); err == nil {
cjkCache[size] = r
return r
}
}
}
return nil
}
// buildScreens 构建主菜单与暂停菜单(原版布局与控件)。
func buildScreens(r *ui.Renderer, am *assets.Manager, lang *assets.Lang) (*ui.TitleScreen, *ui.PauseScreen) {
// 原版按钮控件widget/button.png 200×20 与悬停高亮)
loadPng := func(key string) (uint32, bool) {
p, ok := am.ResolveTexture(key)
if !ok {
return 0, false
}
f, err := os.Open(p)
if err != nil {
return 0, false
}
img, err := png.Decode(f)
_ = f.Close()
if err != nil {
return 0, false
}
return r.UploadImage(img), true
}
btnTex, _ := loadPng("gui/sprites/widget/button")
btnHover, _ := loadPng("gui/sprites/widget/button_highlighted")
// 土质背景16px dirt 平铺 1280×720原版菜单背景质感
bgTex := tileBackground(r, am)
// 中文文本光栅化(按请求字号取缓存渲染器)
text := func(s string, size float64, c color.RGBA) (uint32, float32, float32) {
cjk := loadCJK(size)
if cjk == nil || s == "" {
return 0, 0, 0
}
img := cjk.Draw(s, c)
return r.UploadImage(img), float32(img.Bounds().Dx()), float32(img.Bounds().Dy())
}
tr := func(key, fallback string, size float64, c color.RGBA) (uint32, float32, float32) {
s := fallback
if lang != nil && lang.Get(key) != key {
s = lang.Get(key)
}
return text(s, size, c)
}
// 标题与 splash原版大标题 + 黄色 splash 于其右下)
titleTex, tw, th := tr("menu.title", "年糕历险记", 96, color.RGBA{255, 255, 255, 255})
splashTex, sw, sh := tr("menu.splash", "牛气冲天!", 24, color.RGBA{255, 255, 85, 255})
// 按钮(原版 200×20、间距 24、居中UI还原.md §2 布局)
mkBtn := func(key, fallback, action string, y float32) ui.Button {
tex, lw, lh := tr(key, fallback, 18, color.RGBA{255, 255, 255, 255})
return ui.Button{X: (427 - 200) / 2, Y: y, W: 200, H: 20, Tex: tex, LabelW: lw, LabelH: lh, Action: action}
}
title := &ui.TitleScreen{
TitleTex: titleTex, TitleW: tw, TitleH: th,
SplashTex: splashTex, SplashW: sw, SplashH: sh,
BgTex: bgTex, BtnTex: btnTex, BtnHover: btnHover,
Version: "年糕历险记 v0.1.0",
Copyright: "Copyright NianGao Studio. 请勿分发!",
}
// 版本/版权角标:中文走 CJK 光栅ASCII 位图字体缺中文字形)
if vTex, vw, vh := tr("menu.version", "年糕历险记 v0.1.0", 12, color.RGBA{220, 220, 220, 255}); vTex != 0 {
title.VersionTex, title.VersionW, title.VersionH = vTex, vw, vh
}
if cTex, cw, ch := text("Copyright NianGao Studio. 请勿分发!", 12, color.RGBA{220, 220, 220, 255}); cTex != 0 {
title.CopyrightTex, title.CopyrightW, title.CopyrightH = cTex, cw, ch
}
title.Buttons = []ui.Button{
mkBtn("menu.singleplayer", "单人游戏", "select", 108),
mkBtn("menu.multiplayer", "多人游戏", "multi", 132),
mkBtn("menu.options", "设置…", "options", 156),
mkBtn("menu.quit", "退出游戏", "quit", 180),
}
pauseTitle, ptw, pth := tr("menu.game", "游戏菜单", 32, color.RGBA{255, 255, 255, 255})
pause := &ui.PauseScreen{
TitleTex: pauseTitle, TitleW: ptw, TitleH: pth,
BtnTex: btnTex, BtnHover: btnHover,
}
pause.Buttons = []ui.Button{
mkBtn("gui.resume", "继续游戏", "resume", 72),
mkBtn("options.title", "设置…", "options", 94),
mkBtn("menu.save", "保存存档", "save", 116),
mkBtn("menu.saveAndQuit", "保存并退出", "saveQuit", 138),
mkBtn("menu.quitWorld", "退出游戏", "quitWorld", 160),
}
return title, pause
}
func buildDeathScreen(r *ui.Renderer, lang *assets.Lang, btnTex, btnHover uint32) *ui.DeathScreen {
text := func(s string, size float64, c color.RGBA) (uint32, float32, float32) {
cjk := loadCJK(size)
if cjk == nil || s == "" {
return 0, 0, 0
}
img := cjk.Draw(s, c)
return r.UploadImage(img), float32(img.Bounds().Dx()), float32(img.Bounds().Dy())
}
tr := func(key, fallback string, size float64, c color.RGBA) (uint32, float32, float32) {
s := fallback
if lang != nil && lang.Get(key) != key {
s = lang.Get(key)
}
return text(s, size, c)
}
mkBtn := func(key, fallback, action string, y float32) ui.Button {
tex, lw, lh := tr(key, fallback, 18, color.RGBA{255, 255, 255, 255})
return ui.Button{X: 113.5, Y: y, W: 200, H: 20, Tex: tex, LabelW: lw, LabelH: lh, Action: action}
}
title, tw, th := tr("deathScreen.title", "你死了!", 32, color.RGBA{255, 80, 80, 255})
d := &ui.DeathScreen{
PauseScreen: ui.PauseScreen{
TitleTex: title, TitleW: tw, TitleH: th,
BtnTex: btnTex, BtnHover: btnHover,
Buttons: []ui.Button{
mkBtn("deathScreen.respawn", "重生", "respawn", 100),
mkBtn("deathScreen.titleScreen", "返回标题", "title", 124),
},
},
}
return d
}
func buildSelectWorldScreen(r *ui.Renderer, lang *assets.Lang, btnTex, btnHover uint32) *ui.SelectWorldScreen {
text := func(s string, size float64, c color.RGBA) (uint32, float32, float32) {
cjk := loadCJK(size)
if cjk == nil || s == "" {
return 0, 0, 0
}
img := cjk.Draw(s, c)
return r.UploadImage(img), float32(img.Bounds().Dx()), float32(img.Bounds().Dy())
}
tr := func(key, fallback string, size float64, c color.RGBA) (uint32, float32, float32) {
s := fallback
if lang != nil && lang.Get(key) != key {
s = lang.Get(key)
}
return text(s, size, c)
}
white := color.RGBA{255, 255, 255, 255}
mk := func(key, fallback, action string, x, y float32) ui.Button {
tex, lw, lh := tr(key, fallback, 16, white)
return ui.Button{X: x, Y: y, W: 148, H: 20, Tex: tex, LabelW: lw, LabelH: lh, Action: action}
}
s := &ui.SelectWorldScreen{BtnTex: btnTex, BtnHover: btnHover, Selected: -1}
s.TitleTex, s.TitleW, s.TitleH = tr("selectWorld.title", "选择世界", 32, white)
s.EmptyTex, s.EmptyW, s.EmptyH = tr("selectWorld.empty", "还没有世界", 18, white)
s.ConfirmQTex, s.ConfirmQW, s.ConfirmQH = tr("selectWorld.deleteQuestion", "确定要删除这个世界吗?", 18, white)
s.ConfirmTex, s.ConfirmW, s.ConfirmH = tr("selectWorld.deleteWarning", "将永远消失!(很长一段时间!)", 16, color.RGBA{255, 85, 85, 255})
s.Buttons = []ui.Button{
mk("selectWorld.select", "进入选中的世界", "play", 63, 186),
mk("selectWorld.create", "创建新的世界", "create", 215, 186),
mk("selectWorld.delete", "删除", "delete", 63, 210),
mk("gui.cancel", "取消", "cancel", 215, 210),
}
return s
}
func buildCreateWorldScreen(r *ui.Renderer, lang *assets.Lang, btnTex, btnHover uint32) *ui.CreateWorldScreen {
text := func(s string, size float64, c color.RGBA) (uint32, float32, float32) {
cjk := loadCJK(size)
if cjk == nil || s == "" {
return 0, 0, 0
}
img := cjk.Draw(s, c)
return r.UploadImage(img), float32(img.Bounds().Dx()), float32(img.Bounds().Dy())
}
tr := func(key, fallback string, size float64, c color.RGBA) (uint32, float32, float32) {
s := fallback
if lang != nil && lang.Get(key) != key {
s = lang.Get(key)
}
return text(s, size, c)
}
white := color.RGBA{255, 255, 255, 255}
mk := func(key, fallback, action string, x, y float32) ui.Button {
tex, lw, lh := tr(key, fallback, 16, white)
return ui.Button{X: x, Y: y, W: 148, H: 20, Tex: tex, LabelW: lw, LabelH: lh, Action: action}
}
s := &ui.CreateWorldScreen{BtnTex: btnTex, BtnHover: btnHover}
s.TitleTex, s.TitleW, s.TitleH = tr("createWorld.title", "创建新的世界", 32, white)
s.NameLblTex, s.NameLblW, s.NameLblH = tr("createWorld.worldName", "世界名称", 16, white)
s.SeedLblTex, s.SeedLblW, s.SeedLblH = tr("createWorld.seed", "世界种子", 16, white)
s.Buttons = []ui.Button{
mk("createWorld.create", "创建新的世界", "create", 63, 186),
mk("gui.cancel", "取消", "cancel", 215, 186),
}
return s
}
// buildOptionsScreen 构建设置界面(灵敏度/音量滑条、种子显示、按键教程列表)。
func buildOptionsScreen(r *ui.Renderer, lang *assets.Lang, btnTex, btnHover uint32, seed int64) *ui.OptionsScreen {
text := func(s string, size float64, c color.RGBA) (uint32, float32, float32) {
cjk := loadCJK(size)
if cjk == nil || s == "" {
return 0, 0, 0
}
img := cjk.Draw(s, c)
return r.UploadImage(img), float32(img.Bounds().Dx()), float32(img.Bounds().Dy())
}
tr := func(key, fallback string, size float64, c color.RGBA) (uint32, float32, float32) {
s := fallback
if lang != nil && lang.Get(key) != key {
s = lang.Get(key)
}
return text(s, size, c)
}
white := color.RGBA{255, 255, 255, 255}
s := &ui.OptionsScreen{BtnTex: btnTex, BtnHover: btnHover}
s.TitleTex, s.TitleW, s.TitleH = tr("options.title", "设置", 40, white)
s.SensLabelTex, s.SensLabelW, s.SensLabelH = text("鼠标灵敏度", 18, white)
s.VolLabelTex, s.VolLabelW, s.VolLabelH = text("声音音量", 18, white)
s.SeedLabelTex, s.SeedLabelW, s.SeedLabelH = text(fmt.Sprintf("世界种子: %dconfig/client.yaml world.seed", seed), 16, white)
s.ControlsTex, s.ControlsW, s.ControlsH = text(
"按键教程:\n"+
"W/A/S/D 移动 空格 跳跃 Shift 潜行 Ctrl 冲刺\n"+
"鼠标左键 攻击/破坏 鼠标右键 放置/使用\n"+
"E 打开背包 Q 丢弃物品 1-9/滚轮 切换快捷栏(上滚左、下滚右)\n"+
"F11 / Alt+Enter 全屏 Esc 暂停/返回",
16, white)
s.DoneLabelTex, s.DoneLabelW, s.DoneLabelH = tr("options.done", "完成", 16, white)
return s
}
// tileBackground 把 dirt 纹理平铺成全屏背景(像素风土墙)。
func tileBackground(r *ui.Renderer, am *assets.Manager) uint32 {
p, ok := am.ResolveTexture("block/dirt")
if !ok {
return 0
}
f, err := os.Open(p)
if err != nil {
return 0
}
tile, err := png.Decode(f)
_ = f.Close()
if err != nil {
return 0
}
const w, h = 1280, 720
bg := image.NewRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y += 16 {
for x := 0; x < w; x += 16 {
draw.Draw(bg, image.Rect(x, y, x+16, y+16), tile, image.Point{}, draw.Src)
}
}
return r.UploadImage(bg)
}