- 植被染色(渲染.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
153 lines
4.8 KiB
Go
153 lines
4.8 KiB
Go
//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)
|
||
}
|