feat: implement crafting table

This commit is contained in:
FlipWind
2026-08-08 23:43:27 +08:00
parent 298441e2e0
commit c1a8ffb323
10 changed files with 547 additions and 0 deletions
+2
View File
@@ -1,3 +1,5 @@
public
themes/hugo-book/**
resources/_gen
*.log
assets/items/*.png
+41
View File
@@ -0,0 +1,41 @@
# 物品贴图库
本目录存放 `crafting` 合成组件使用的物品贴图(PNG)。
## 命名规则
一个物品对应一个文件:`<名称>.png`,文件名(不含 `.png`)就是引用时的名字。
例如放入 `diamond.png` 后,在合成组件中写 `a1="diamond"` 即可显示钻石。
> 推荐使用 16×16(或 32×32)的 Minecraft 像素风贴图,`image-rendering: pixelated` 会保证缩放后依然清晰锐利。
## 自动同步(推荐)
构建 / 开发时由 `scripts/sync-items.ps1` 自动维护本目录:
- sfcraft 自定义贴图(`amethyst_cauldron_blank``exp_totem``lunch_box``pearl_token` 等)
从 [sfcraft 仓库](https://github.com/saltedfishclub/sfcraft/tree/rev/26.2/src/main/resources/assets/sfcraft/textures/item)
自动下载,缺失或远端变更时更新;
- 原版物品(`diamond``stick` 等)在本地缺失时从 Minecraft wiki 自动获取;
- 手动放入的贴图不会被覆盖;找不到的贴图会报错并列出物品 id,阻止构建 / 开发。
请使用 `scripts\build.ps1` / `scripts\dev.ps1` 而非直接运行 `hugo`,详见 `scripts/README.md`
## 组件用法
```markdown
{{< crafting
a1="diamond" a2="diamond" a3="diamond"
b1="" b2="stick" b3=""
c1="" c2="stick" c3=""
out="diamond_pickaxe"
caption="钻石镐(可选)"
>}}
```
- `a1`~`c3`3×3 合成格的九个格子,`out`:输出格;
- 值 = 贴图文件名(不含 `.png`),**留空 / 省略 = 空格子**;
- 需要显示堆叠数量时写 `名称:数量`,例如 `b2="stick:2"`
- `caption` 可选,显示在组件下方居中说明文字;
- 未运行自动同步且贴图缺失时,格子内会显示一个 `?` 占位,方便发现写错的名字。
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+29
View File
@@ -0,0 +1,29 @@
{{- $val := trim (.ctx.Get .key) " " -}}
{{- $item := "" -}}
{{- $count := 0 -}}
{{- if $val -}}
{{- $parts := split $val ":" -}}
{{- $item = index $parts 0 -}}
{{- if gt (len $parts) 1 -}}
{{- $count = int (index $parts 1) -}}
{{- end -}}
{{- end -}}
{{- $img := "" -}}
{{- if $item -}}
{{- $img = resources.Get (printf "items/%s.png" $item) -}}
{{- end -}}
<div class="sfc-slot sfc-{{ .cls }}"
style="left: {{ .x }}; top: {{ .y }};"
{{ with $item }}data-name="{{ . }}"{{ end }}>
{{- if $img -}}
<img class="sfc-item" src="{{ $img.RelPermalink }}"
alt="{{ $item }}" loading="lazy">
{{- if gt $count 1 -}}
<span class="sfc-count">{{ $count }}</span>
{{- end -}}
{{- else if $item -}}
<span class="sfc-missing" title="未找到 assets/items/{{ $item }}.png">?</span>
{{- end -}}
</div>
+124
View File
@@ -0,0 +1,124 @@
{{- /* SFCraft 合成配方组件
- 调用: 页面内写 shortcode, 名为 crafting
- a1..c3 为 3x3 合成格, out 为输出格
- 值为 assets/items/ 下贴图的文件名(不含 .png), 留空表示空格子
- 名称后可加 ":数量" 显示堆叠数, 例如 a1="stick:2"
- 引用的贴图不存在时, 格子内显示 ? 占位提示
- caption 参数可选, 显示在组件下方
*/ -}}
{{- $table := resources.Get "table.png" -}}
{{- $slots := slice
(dict "key" "a1" "cls" "grid" "x" "13.75%" "y" "20%")
(dict "key" "a2" "cls" "grid" "x" "25%" "y" "20%")
(dict "key" "a3" "cls" "grid" "x" "36.25%" "y" "20%")
(dict "key" "b1" "cls" "grid" "x" "13.75%" "y" "42.5%")
(dict "key" "b2" "cls" "grid" "x" "25%" "y" "42.5%")
(dict "key" "b3" "cls" "grid" "x" "36.25%" "y" "42.5%")
(dict "key" "c1" "cls" "grid" "x" "13.75%" "y" "65%")
(dict "key" "c2" "cls" "grid" "x" "25%" "y" "65%")
(dict "key" "c3" "cls" "grid" "x" "36.25%" "y" "65%")
(dict "key" "out" "cls" "out" "x" "70%" "y" "37.5%")
-}}
{{- $cssOnce := .Page.Store.Get "sfc_crafting_css" -}}
{{- if not $cssOnce -}}
{{- .Page.Store.Set "sfc_crafting_css" true -}}
<style>
.sfc-crafting {
position: relative;
width: 100%;
max-width: 540px;
aspect-ratio: 320 / 160;
margin: 1rem 0 1.25rem;
background-repeat: no-repeat;
background-size: 100% 100%;
container-type: inline-size;
}
.sfc-slot {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
}
.sfc-grid { width: 10.625%; height: 21.25%; }
.sfc-out { width: 15.625%; height: 31.25%; }
.sfc-item {
width: 88%;
height: 88%;
object-fit: contain;
image-rendering: pixelated;
filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.35));
}
.sfc-count {
position: absolute;
right: 5%;
bottom: 3%;
font-family: "Minecraft", ui-monospace, Consolas, monospace;
font-size: 12px;
font-size: 2.8cqw;
font-weight: 700;
line-height: 1;
color: #fff;
text-shadow: 0 1px 0 #3f3f3f, 1px 0 0 #3f3f3f,
0 -1px 0 #3f3f3f, -1px 0 0 #3f3f3f,
0 2px 3px rgba(0, 0, 0, 0.55);
}
.sfc-missing {
display: flex;
align-items: center;
justify-content: center;
width: 86%;
height: 86%;
font-size: 4cqw;
font-weight: 700;
color: rgba(0, 0, 0, 0.28);
border: 2px dashed rgba(0, 0, 0, 0.18);
border-radius: 4px;
background:
repeating-linear-gradient(45deg, rgba(255,255,255,0.22), rgba(255,255,255,0.22) 4px, rgba(0,0,0,0.04) 4px, rgba(0,0,0,0.04) 8px);
}
.sfc-slot[data-name]::after {
content: attr(data-name);
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
padding: 3px 9px;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 5px;
background: rgba(18, 18, 18, 0.92);
color: #fff;
font-size: 12px;
white-space: nowrap;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.35);
opacity: 0;
pointer-events: none;
transition: opacity 0.12s ease;
z-index: 30;
}
.sfc-slot[data-name]:hover::after { opacity: 1; }
.sfc-caption {
margin: 0.35rem 0 0;
text-align: center;
font-size: 0.85rem;
color: var(--gray-600, #6c757d);
}
</style>
{{- end -}}
<figure class="sfc-crafting" style="background-image: url('{{ $table.RelPermalink }}');">
{{- range $slots -}}
{{- partial "crafting-slot.html"
(dict
"ctx" $
"key" .key
"cls" .cls
"x" .x
"y" .y
) -}}
{{- end -}}
{{- with .Get "caption" -}}
<figcaption class="sfc-caption">{{ . }}</figcaption>
{{- end -}}
</figure>
+40
View File
@@ -0,0 +1,40 @@
# 构建 / 开发脚本
## 用途
`sync-items.ps1` 会在构建和开发前自动保证物品贴图齐全:
1. 下载 [sfcraft 仓库](https://github.com/saltedfishclub/sfcraft/tree/rev/26.2/src/main/resources/assets/sfcraft/textures/item) 的
全部自定义贴图到 `assets/items/`;
2. 扫描 `content/` 中所有 `crafting` 组件用到的物品 id;
3. 本地缺失的原版物品从 Minecraft wiki 自动获取(自动挑选最新版本渲染);
4. 有任何物品无法找到时, 打印缺失清单并以非零码退出, 阻断构建 / 开发启动。
## 使用
```powershell
.\scripts\build.ps1 # 同步贴图 + hugo 构建
.\scripts\dev.ps1 # 同步贴图 + hugo server
.\scripts\dev.ps1 -NoSync # 仅启动开发服务器, 不重新同步
```
若 PowerShell 执行策略阻止脚本, 请用:
```powershell
powershell -ExecutionPolicy Bypass -File scripts\dev.ps1
```
脚本通过 `$env:HUGO` 或 PATH 查找 hugo, 找不到时也会探测 winget 安装位置。
## 规则细节
- sfcraft 自定义贴图: 每次构建比对远端文件大小, 缺失或变化时更新;
- 原版贴图: 只在本地缺失时下载, **不会覆盖**手动放入 `assets/items/` 的贴图;
- 物品 id 即 `assets/items/` 下的文件名(不含 `.png`);
- wiki 命名规则: `<英文名>_JE<版本>[_BE<版本>].png`, 特殊命名(如 `TNT``Flint_and_Steel`)
`sync-items.ps1` 中的变体生成与别名表处理。
## 网络要求
需要能访问 `api.github.com``raw.githubusercontent.com``zh.minecraft.wiki`
若 CI 或离线环境不允许联网, 建议把 `assets/items/` 下的贴图提交进仓库。
+35
View File
@@ -0,0 +1,35 @@
<#
SFCraft 文档构建入口: 先同步物品贴图, 再执行 hugo 构建。
贴图同步失败(有物品找不到)时终止构建。
用法: powershell -ExecutionPolicy Bypass -File scripts\build.ps1 [hugo 参数...]
#>
[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$HugoArgs
)
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
function Get-HugoPath {
if ($env:HUGO) { return $env:HUGO }
$cmd = Get-Command hugo -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
$p = Get-ChildItem -Path (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Packages') -Recurse -Filter hugo.exe -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($p) { return $p.FullName }
throw '未找到 Hugo。请安装 Hugo, 或将 hugo 可执行文件路径写入环境变量 HUGO。'
}
Write-Host "`n[1/2] 同步物品贴图" -ForegroundColor Cyan
& (Join-Path $scriptDir 'sync-items.ps1')
if ($LASTEXITCODE -ne 0) {
Write-Host "[构建终止] 物品贴图同步失败, 未执行 hugo 构建。" -ForegroundColor Red
exit $LASTEXITCODE
}
Write-Host "`n[2/2] hugo 构建" -ForegroundColor Cyan
$hugo = Get-HugoPath
& $hugo @HugoArgs
exit $LASTEXITCODE
+42
View File
@@ -0,0 +1,42 @@
<#
SFCraft 文档开发服务器入口: 先同步物品贴图, 再启动 hugo server。
贴图同步失败(有物品找不到)时终止启动。
用法: powershell -ExecutionPolicy Bypass -File scripts\dev.ps1 [-NoSync] [hugo server 参数...]
#>
[CmdletBinding()]
param(
[switch]$NoSync,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$HugoArgs
)
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
function Get-HugoPath {
if ($env:HUGO) { return $env:HUGO }
$cmd = Get-Command hugo -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
$p = Get-ChildItem -Path (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Packages') -Recurse -Filter hugo.exe -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($p) { return $p.FullName }
throw '未找到 Hugo。请安装 Hugo, 或将 hugo 可执行文件路径写入环境变量 HUGO。'
}
if (-not $NoSync) {
Write-Host "`n[1/2] 同步物品贴图" -ForegroundColor Cyan
& (Join-Path $scriptDir 'sync-items.ps1')
if ($LASTEXITCODE -ne 0) {
Write-Host "[启动终止] 物品贴图同步失败, 未启动 hugo server。" -ForegroundColor Red
exit $LASTEXITCODE
}
} else {
Write-Host "[跳过] 已指定 -NoSync, 不执行贴图同步" -ForegroundColor Yellow
}
Write-Host "`n[2/2] 启动 hugo server" -ForegroundColor Cyan
$hugo = Get-HugoPath
$serverArgs = @('server')
if ($HugoArgs.Count -gt 0) { $serverArgs += $HugoArgs }
& $hugo @serverArgs
exit $LASTEXITCODE
+233
View File
@@ -0,0 +1,233 @@
<#
SFCraft 文档 · 物品贴图自动同步
流程:
1. 将 sfcraft 仓库 textures/item 目录下所有贴图同步到 assets/items
2. 扫描 content 中所有 crafting 组件, 收集用到的物品 id
3. 对本地缺失的 id, 从 Minecraft wiki 自动获取(按命名规则挑选最新版本)
4. 仍有无法获取的 id 时, 列出并退出(返回码 1), 阻断 build / dev
用法:
powershell -ExecutionPolicy Bypass -File scripts\sync-items.ps1 [-Force]
说明:
- sfcraft 自定义贴图每次与远端比对大小, 缺失或变更时更新
- 原版贴图只在本地缺失时下载, 不会覆盖手动放入 assets/items 的贴图
- 需要联网访问 api.github.com / zh.minecraft.wiki
#>
[CmdletBinding()]
param(
[switch]$Force
)
$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path -Parent $PSScriptRoot
$itemsDir = Join-Path $repoRoot 'assets\items'
$contentDir = Join-Path $repoRoot 'content'
$ua = 'sfcraft-docs-sync/1.0 (https://github.com/saltedfishclub/sfcraft-docs)'
New-Item -ItemType Directory -Path $itemsDir -Force | Out-Null
# ---------- 数据源 ----------
$sfOwner = 'saltedfishclub'
$sfRepo = 'sfcraft'
$sfRef = 'rev/26.2'
$sfDir = 'src/main/resources/assets/sfcraft/textures/item'
$sfListUrl = "https://api.github.com/repos/$sfOwner/$sfRepo/contents/$sfDir`?ref=$sfRef"
$wikiApi = 'https://zh.minecraft.wiki/api.php'
$wikiImg = 'https://zh.minecraft.wiki/images/'
function Write-Step([string]$s) { Write-Host "`n== $s" -ForegroundColor Cyan }
function Invoke-Get([string]$url, [int]$maxRetry = 2) {
for ($i = 0; ; $i++) {
try {
return Invoke-RestMethod -Uri $url -Headers @{ 'User-Agent' = $ua } -TimeoutSec 30
} catch {
if ($i -ge $maxRetry) { throw }
Start-Sleep -Milliseconds 800
}
}
}
# ---------- 1. sfcraft 自定义贴图 ----------
Write-Step '1/4 同步 sfcraft 自定义贴图'
$sfCount = 0
$sfNew = 0
try {
$list = Invoke-Get $sfListUrl
foreach ($f in $list) {
$target = Join-Path $itemsDir $f.name
$need = $Force -or
-not (Test-Path -LiteralPath $target) -or
((Get-Item -LiteralPath $target).Length -ne [int64]$f.size)
if ($need) {
Invoke-WebRequest -Uri $f.download_url -Headers @{ 'User-Agent' = $ua } -OutFile $target -TimeoutSec 60
$sfNew++
}
$sfCount++
}
Write-Host " $sfCount 个文件, 本次更新 $sfNew"
} catch {
Write-Host " [警告] 无法访问 GitHub, 跳过 sfcraft 贴图同步: $($_.Exception.Message)" -ForegroundColor Yellow
}
# ---------- 2. 扫描 crafting 组件 ----------
Write-Step '2/4 扫描组件物品 id'
$ids = @{}
$shortcodePat = [regex]'(?s){{<[ ]*crafting\b(.*?)}}'
$paramPat = [regex]'(?:a[123]|b[123]|c[123]|out)="([^"]*)"'
Get-ChildItem -Path $contentDir -Recurse -Filter '*.md' -File | ForEach-Object {
$text = Get-Content -LiteralPath $_.FullName -Raw -Encoding UTF8
foreach ($m in $shortcodePat.Matches($text)) {
foreach ($p in $paramPat.Matches($m.Groups[1].Value)) {
$val = $p.Groups[1].Value.Trim()
if ($val.Length -gt 0) {
$id = ($val -split ':')[0].Trim()
if ($id.Length -gt 0) { $ids[$id] = $true }
}
}
}
}
$all = @($ids.Keys | Sort-Object)
$need = @($all | Where-Object { -not (Test-Path -LiteralPath (Join-Path $itemsDir "${_}.png")) })
Write-Host " 使用 $($all.Count) 个物品 id, 本地已有 $($all.Count - $need.Count)"
# ---------- 3. 从 Minecraft wiki 获取缺失贴图 ----------
Write-Step '3/4 从 Minecraft wiki 获取缺失贴图'
function Get-WikiCandidates([string]$name) {
$regex = '^' + [regex]::Escape($name) + '(_\(item\))?_JE\d+(\.\d+)*(_BE\d+)?\.png$'
# 两个查询互补:
# <name>_ 宽前缀, 覆盖 (item) 等变体, 如 Diamond_Boots_(item)_JE3_BE2.png
# <name>_JE 窄前缀, 在宽前缀因数量被截断时兜底(裸物品如 diamond 的 JE 渲染排序靠前)
$urls = @(
"$wikiApi`?action=query&list=allimages&aiprefix=$([uri]::EscapeDataString($name + '_'))&ailimit=500&format=json",
"$wikiApi`?action=query&list=allimages&aiprefix=$([uri]::EscapeDataString($name + '_JE'))&ailimit=200&format=json"
)
$seen = @{}
$all = New-Object System.Collections.ArrayList
foreach ($u in $urls) {
try {
$r = Invoke-Get $u
if ($null -ne $r.query.allimages) {
foreach ($n in $r.query.allimages) {
if (-not $seen.ContainsKey($n.name)) {
[void]$seen.Add($n.name, $true)
[void]$all.Add($n.name)
}
}
}
} catch {
Write-Host " [警告] wiki 查询失败 ($name): $($_.Exception.Message)" -ForegroundColor Yellow
}
Start-Sleep -Milliseconds 250
}
return @($all | Where-Object { $_ -match $regex })
}
function Select-BestCandidate([string]$name, [string[]]$candidates) {
$regex = '^' + [regex]::Escape($name) + '(_\(item\))?_JE(\d+)(\.\d+)*(_BE(\d+))?\.png$'
$best = $null
$bestKey = $null
foreach ($c in $candidates) {
$m = [regex]::Match($c, $regex)
if (-not $m.Success) { continue }
$key = @(
$(if ($m.Groups[1].Success) { 1 } else { 0 }), # 优先物品图标 (item)
[int]$m.Groups[2].Value, # JE 版本
$(if ($m.Groups[5].Success) { [int]$m.Groups[5].Value } else { -1 }) # BE 版本
)
if ($null -eq $bestKey) { $best = $c; $bestKey = $key; continue }
for ($i = 0; $i -lt 3; $i++) {
if ($key[$i] -ne $bestKey[$i]) {
if ($key[$i] -gt $bestKey[$i]) { $best = $c; $bestKey = $key }
break
}
}
}
return $best
}
function Get-NameVariants([string]$id) {
$words = @($id -split '_' | Where-Object { $_ })
$title = @($words | ForEach-Object { $_.Substring(0, 1).ToUpper() + $_.Substring(1) }) -join '_'
$connectors = @('and', 'of', 'the', 'for', 'with', 'to', 'from', 'by', 'in', 'on', 'at')
$mixed = @($words | ForEach-Object {
if ($connectors -contains $_) { $_ } else { $_.Substring(0, 1).ToUpper() + $_.Substring(1) }
}) -join '_'
$variants = New-Object System.Collections.ArrayList
[void]$variants.Add($title)
if ($mixed -cne $title) { [void]$variants.Add($mixed) }
if ($id.Length -le 4 -and $id -notmatch '[aeiou]') { [void]$variants.Add($id.ToUpper()) }
$oneWord = @($words | ForEach-Object { $_.Substring(0, 1).ToUpper() + $_.Substring(1) }) -join ''
if ($oneWord -cne $title) { [void]$variants.Add($oneWord) }
$aliases = @{
'tnt' = 'TNT'
'tnt_minecart' = 'TNT_Minecart'
'dragon_breath' = "Dragon's_Breath"
'slime_ball' = 'Slime'
'snow_golem' = 'Snow_Golem'
'ocelot' = 'Ocelot'
}
if ($aliases.ContainsKey($id)) { [void]$variants.Insert(0, $aliases[$id]) }
return @($variants | Select-Object -Unique)
}
$failed = @()
foreach ($id in $need) {
$file = $null
foreach ($name in Get-NameVariants $id) {
$cands = Get-WikiCandidates $name
if ($cands.Count -gt 0) { $file = Select-BestCandidate $name $cands; break }
}
if ($file) {
$target = Join-Path $itemsDir "$id.png"
try {
Invoke-WebRequest -Uri ($wikiImg + [uri]::EscapeDataString($file)) -Headers @{ 'User-Agent' = $ua } -OutFile $target -TimeoutSec 60
$fs = [System.IO.File]::OpenRead($target)
$sig = New-Object byte[] 8
[void]$fs.Read($sig, 0, 8)
$fs.Close()
if (-not ($sig[0] -eq 137 -and $sig[1] -eq 80 -and $sig[2] -eq 78 -and $sig[3] -eq 71)) {
throw '下载内容不是有效 PNG'
}
Write-Host " ok $id <- $file" -ForegroundColor Green
continue
} catch {
Write-Host " fail $id 下载失败: $($_.Exception.Message)" -ForegroundColor Yellow
}
} else {
Write-Host " fail $id 在 wiki 上未找到" -ForegroundColor Yellow
}
$failed += $id
}
# ---------- 4. 汇总错误 ----------
if ($failed.Count -gt 0) {
Write-Host "`n[错误] 以下物品贴图无法自动获取:" -ForegroundColor Red
$failed | Sort-Object | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
Write-Host @"
:
1. id ( assets/items )
2. sfcraft , textures/item
3. Minecraft wiki ,
:
- assets/items/<id>.png
- id
- wiki , scripts/sync-items.ps1 Get-NameVariants
"@ -ForegroundColor Yellow
exit 1
}
Write-Host "`n全部贴图就绪 ✓" -ForegroundColor Green
exit 0
+1
Submodule themes/hugo-book added at bc3454c1db