feat: NSIS 中文安装器(年糕历险记-Setup-v0.1.0.exe 实际构建+静默安装验证)、菜单鼠标可见性修复、首页 UI 原版复刻(土质背景/原版按钮/splash/版本角标)、全 goroutine 崩溃兜底

This commit is contained in:
NianGao Dev
2026-08-16 06:46:25 +08:00
parent 854440216f
commit 6dd5a8122a
434 changed files with 63991 additions and 106 deletions

View File

@@ -199,14 +199,15 @@ func (a *clientApp) Render() {
a.rend.DrawChunks(func(i int) (int32, int32) { return chunks[i].CX, chunks[i].CZ }, len(chunks))
// UI pass(UI与输入.md §2:3D 之后、状态隔离)
a.uiR.Begin()
mx, my := float32(a.in.cursorX), float32(a.in.cursorY)
switch a.screen {
case 0:
a.titleScreen.Draw(a.uiR)
a.titleScreen.Draw(a.uiR, mx, my)
case 1:
a.hud.Draw(a.uiR)
case 2:
a.hud.Draw(a.uiR) // 暂停时世界仍可见(UI还原.md §2)
a.pauseScreen.Draw(a.uiR)
a.pauseScreen.Draw(a.uiR, mx, my)
}
a.uiR.Flush()
a.rend.EndFrame()
@@ -223,7 +224,8 @@ func (a *clientApp) logf(format string, args ...any) {
}
func main() {
defer crash.Guard(paths.Join("logs", "crash.log")) // 崩溃兜底:闪退可诊断
crash.SetLogPath(paths.Join("logs", "crash.log")) // 全局崩溃日志(含 goroutine 兜底)
defer crash.Guard(paths.Join("logs", "crash.log")) // 主线程崩溃兜底:闪退可诊断
flag.Parse()
log, err := logx.New(paths.Join("logs", "client.log"), logx.LevelInfo)
@@ -331,7 +333,8 @@ func main() {
}
defer rend.Shutdown()
rend.SetAtlas(atlasImg)
rend.SetCursorMode(true)
// 主菜单状态:鼠标可见(仅游戏内捕获,UI与输入.md §6)
rend.SetCursorMode(false)
// 光影包(渲染.md §9:未安装 → 默认光影;坏包 → 回退默认)
if sp, err := render.LoadShaderPack(am); err != nil {
@@ -367,12 +370,12 @@ func main() {
hud.SetItemAtlas(itemTex, itemUV)
}
// 界面屏幕(中文标题「年糕历险记」+ 按钮,UI还原.md §2)
// 界面屏幕(原版复刻:土质背景/原版按钮/中文标题,UI还原.md §2)
lang, err := am.LoadLang(cfg.Client.Language)
if err != nil {
log.Warnf("语言加载失败: %v", err)
}
titleScreen, pauseScreen := buildScreens(uiR, lang)
titleScreen, pauseScreen := buildScreens(uiR, am, lang)
// 6) 输入(鼠标捕获 + 键鼠回调)
in := newInputState(rend.Window())

View File

@@ -1,10 +1,14 @@
//go:build gl
// 界面屏幕构建:中文标题与按钮(UI还原.md §2、§4 中文回退)。
// 界面屏幕构建:原版复刻(UI还原.md §2)——土质背景、原版按钮控件、
// 中文标题「年糕历险记」、黄色 splash、版本/版权角标。
package main
import (
"image"
"image/color"
"image/draw"
"image/png"
"os"
"path/filepath"
@@ -19,55 +23,7 @@ var cjkPaths = []string{
filepath.Join(os.Getenv("WINDIR"), "Fonts", "msyh.ttf"),
}
// buildScreens 构建主菜单与暂停菜单(按钮纹理 = 中文文本光栅化)。
func buildScreens(r *ui.Renderer, lang *assets.Lang) (*ui.TitleScreen, *ui.PauseScreen) {
cjk := loadCJK()
text := func(key, fallback string, size float64) (uint32, float32, float32) {
label := fallback
if lang != nil {
label = lang.Get(key)
}
if cjk != nil {
img := cjk.Draw(label, color.RGBA{255, 255, 255, 255})
tex := r.UploadImage(img)
return tex, float32(img.Bounds().Dx()), float32(img.Bounds().Dy())
}
return 0, 0, 0
}
titleTex, titleW, titleH := text("menu.title", "年糕历险记", 48)
if titleW == 0 {
titleW, titleH = 320, 48 // 无 CJK 字体时占位
}
// 主菜单按钮(居中列,UI还原.md §2)
btn := func(key, fallback, action string, y float32, w, h int) ui.Button {
tex, tw, th := text(key, fallback, 24)
x := float32(w)/2 - tw/2
return ui.Button{X: x, Y: y, W: tw, H: th, Tex: tex, Action: action}
}
title := &ui.TitleScreen{}
title.TitleTex = titleTex
title.TitleW = titleW
title.TitleH = titleH
title.Buttons = []ui.Button{
btn("menu.singleplayer", "单人游戏", "play", 300, 1280, 720),
btn("menu.multiplayer", "多人游戏", "multi", 350, 1280, 720),
btn("menu.quit", "退出游戏", "quit", 400, 1280, 720),
}
_ = titleTex
_ = titleH
pause := &ui.PauseScreen{}
pause.TitleTex, pause.TitleW, pause.TitleH = text("menu.game", "游戏菜单", 32)
pause.Buttons = []ui.Button{
btn("gui.resume", "继续游戏", "resume", 300, 1280, 720),
btn("menu.quit", "保存并退出", "quit", 350, 1280, 720),
}
return title, pause
}
// loadCJK 加载系统 CJK 字体(失败返回 nil → 按钮退化不显示文本)。
// loadCJK 加载系统 CJK 字体(失败返回 nil → 中文标签退化不显示)。
func loadCJK() *cjkfont.Renderer {
for _, p := range cjkPaths {
if _, err := os.Stat(p); err == nil {
@@ -78,3 +34,106 @@ func loadCJK() *cjkfont.Renderer {
}
return nil
}
// buildScreens 构建主菜单与暂停菜单(原版布局与控件)。
func buildScreens(r *ui.Renderer, am *assets.Manager, lang *assets.Lang) (*ui.TitleScreen, *ui.PauseScreen) {
cjk := loadCJK()
// 原版按钮控件(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) {
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: 1280/2 - 100, 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. 请勿分发!",
}
title.Buttons = []ui.Button{
mkBtn("menu.singleplayer", "单人游戏", "play", 390),
mkBtn("menu.multiplayer", "多人游戏", "multi", 414),
mkBtn("menu.options", "设置…", "", 438),
mkBtn("menu.quit", "退出游戏", "quit", 462),
}
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", 300),
mkBtn("menu.quit", "保存并退出", "quit", 324),
}
return title, pause
}
// 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)
}