292 lines
11 KiB
Go
292 lines
11 KiB
Go
//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("世界种子: %d(config/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)
|
||
}
|