Files
ngzz-mc/cmd/client/screens_gl.go
NianGao Dev d52bf1126f feat(client): 植被染色、破坏裂纹叠加、手持方块与实体广告牌渲染;原版 HUD 精灵
- 植被染色(渲染.md §4.1):blocks.json 增 tint 标记,图集构建后按
  colormap/grass|foliage 中心色乘色——原版材质包草顶/树叶为灰度图,不染色全灰
- 破坏裂纹(方块交互与动画.md §4):destroy_stage_0..9 裂纹盒叠加破坏目标方块
- 第一人称手持方块(渲染.md §5.2):选中槽放置方块六面 UV 视图空间立方体
- 实体广告牌(渲染.md §5.3):僵尸/村民/动物按贴图渲染,动物按 ID 轮换外观
- HUD 换原版精灵:crosshair/hotbar/hotbar_selection/heart/food(UI还原.md §3.4)
- 主菜单标题/按钮标签按请求字号光栅化(修复 24px 固定字号);版本角标 CJK
- 恢复背面剔除(相机修复后 '脚下黑块' 误诊解除);ESC 光标请求原子化防竞态
- dev 测试参数:-give/-crackstage/-spawntest/-esctest
2026-08-16 10:08:40 +08:00

153 lines
4.8 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 (
"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: 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. 请勿分发!",
}
// 版本/版权角标:中文走 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", "单人游戏", "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)
}