feat: NSIS 中文安装器(年糕历险记-Setup-v0.1.0.exe 实际构建+静默安装验证)、菜单鼠标可见性修复、首页 UI 原版复刻(土质背景/原版按钮/splash/版本角标)、全 goroutine 崩溃兜底
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,8 @@ func (s *serverApp) Tick(dt float64) {
|
||||
}
|
||||
|
||||
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", "server.log"), logx.LevelInfo)
|
||||
|
||||
Reference in New Issue
Block a user