Files
groot 47c523dd98
release-tag / release-image (push) Successful in 3m51s
RC-14
2026-08-14 06:17:30 +02:00

296 lines
13 KiB
Go

package artifact
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"html"
"strings"
"unicode/utf8"
)
func imageMIME(ext string) string {
switch strings.ToLower(strings.TrimPrefix(ext, ".")) {
case "jpg", "jpeg":
return "image/jpeg"
case "webp":
return "image/webp"
case "svg":
return "image/svg+xml"
default:
return "image/png"
}
}
func trimLabel(s string, maxRunes int) string {
s = strings.Join(strings.Fields(strings.TrimSpace(s)), " ")
if s == "" {
return "NEURAL HUNT"
}
if utf8.RuneCountInString(s) <= maxRunes {
return s
}
r := []rune(s)
return string(r[:maxRunes-1]) + "…"
}
type rarityStyle struct {
Name string
AccentA string
AccentB string
MetalBase string
MetalEdge string
PillFill string
PillText string
EditionFill string
EditionStroke string
EditionText string
HeaderLine string
OuterGlow string
OuterGlowOpacity string
HoloOpacityA string
HoloOpacityB string
BorderOpacityA string
BorderOpacityB string
SparkleBoost int
FoilPattern string
BadgeText string
BadgeSubtext string
}
func cardRarityStyle(traits collectionTraits) rarityStyle {
base := rarityStyle{
Name: traits.Rarity,
AccentA: traits.AccentA,
AccentB: traits.AccentB,
MetalBase: "#f4f7ff",
MetalEdge: "#e9f2ff",
PillFill: "#060a12",
PillText: "#ffffff",
EditionFill: "#ffffff",
EditionStroke: "#ffffff",
EditionText: "#ffffff",
HeaderLine: "#ffc400",
OuterGlow: traits.AccentA,
OuterGlowOpacity: ".00",
HoloOpacityA: ".10",
HoloOpacityB: ".12",
BorderOpacityA: ".28",
BorderOpacityB: ".72",
SparkleBoost: 0,
BadgeText: traits.Rarity,
BadgeSubtext: "RIFT EDITION",
}
switch strings.ToUpper(strings.TrimSpace(traits.Rarity)) {
case "COMMON":
base.AccentA = "#7e8ca5"
base.AccentB = "#5f6c83"
base.MetalBase = "#d8dee9"
base.MetalEdge = "#aeb8c8"
base.HeaderLine = "#b9c2d4"
base.OuterGlowOpacity = ".00"
base.HoloOpacityA = ".05"
base.HoloOpacityB = ".06"
base.BorderOpacityA = ".20"
base.BorderOpacityB = ".45"
base.BadgeSubtext = "COMMON"
case "UNCOMMON":
base.AccentA = "#60eaa9"
base.AccentB = "#2dbf9a"
base.MetalBase = "#e8fff5"
base.MetalEdge = "#b4f1db"
base.HeaderLine = "#7bffc8"
base.HoloOpacityA = ".10"
base.HoloOpacityB = ".14"
base.BorderOpacityB = ".68"
base.SparkleBoost = 8
base.BadgeSubtext = "UNCOMMON"
case "RARE":
base.AccentA = "#6ae4ff"
base.AccentB = "#6f7bff"
base.MetalBase = "#f0fbff"
base.MetalEdge = "#c8dcff"
base.HeaderLine = "#7ed8ff"
base.HoloOpacityA = ".16"
base.HoloOpacityB = ".18"
base.BorderOpacityA = ".30"
base.BorderOpacityB = ".82"
base.SparkleBoost = 18
base.FoilPattern = `<path d="M38 220 C250 120 420 30 710 20" stroke="url(#foilA)" stroke-width="24" stroke-linecap="round" opacity=".34" fill="none"/><path d="M120 1160 C420 1120 710 1020 986 820" stroke="url(#foilA)" stroke-width="18" stroke-linecap="round" opacity=".24" fill="none"/>`
base.BadgeSubtext = "RARE"
case "ULTRA RARE":
base.AccentA = "#ffcf58"
base.AccentB = "#ff69c9"
base.MetalBase = "#fff6d8"
base.MetalEdge = "#ffe7a6"
base.HeaderLine = "#ffd86c"
base.OuterGlow = "#ffd670"
base.OuterGlowOpacity = ".30"
base.HoloOpacityA = ".24"
base.HoloOpacityB = ".28"
base.BorderOpacityA = ".36"
base.BorderOpacityB = ".92"
base.SparkleBoost = 34
base.FoilPattern = `<path d="M-80 220 L400 -40 L790 120 L210 470 Z" fill="url(#foilA)" opacity=".20"/><path d="M160 1540 L1130 1030 L1130 1210 L300 1540 Z" fill="url(#foilB)" opacity=".22"/><ellipse cx="840" cy="340" rx="220" ry="120" fill="url(#foilA)" opacity=".18"/>`
base.BadgeSubtext = "ULTRA"
case "SPECIAL ILLUSTRATION RARE":
base.AccentA = "#ffd46a"
base.AccentB = "#8b5fff"
base.MetalBase = "#fff7df"
base.MetalEdge = "#fff0bf"
base.PillFill = "#fff6d0"
base.PillText = "#241535"
base.EditionFill = "#fff8da"
base.EditionStroke = "#ffe596"
base.EditionText = "#241535"
base.HeaderLine = "#ffe17c"
base.OuterGlow = "#ffd76d"
base.OuterGlowOpacity = ".42"
base.HoloOpacityA = ".32"
base.HoloOpacityB = ".36"
base.BorderOpacityA = ".46"
base.BorderOpacityB = "1.00"
base.SparkleBoost = 56
base.FoilPattern = `<path d="M-80 180 L350 -30 L860 70 L1040 250 L540 470 Z" fill="url(#foilA)" opacity=".28"/><path d="M130 1480 C260 1180 520 990 1020 880 L1020 1160 L430 1540 Z" fill="url(#foilB)" opacity=".24"/><circle cx="846" cy="264" r="170" fill="url(#foilA)" opacity=".22"/><path d="M84 226 C170 176 250 136 332 102" stroke="#fff6ca" stroke-width="4" stroke-opacity=".56" stroke-linecap="round" fill="none"/><path d="M700 1240 C820 1170 910 1090 984 990" stroke="#fff1b4" stroke-width="4" stroke-opacity=".42" stroke-linecap="round" fill="none"/>`
base.BadgeText = "SIR"
base.BadgeSubtext = "SPECIAL"
}
return base
}
func raritySparkleCount(style rarityStyle) int {
count := 42 + style.SparkleBoost
if count < 16 {
count = 16
}
return count
}
func renderSparkles(h [32]byte, style rarityStyle) string {
count := raritySparkleCount(style)
var sparkles strings.Builder
for i := 0; i < count; i++ {
b0 := int(h[i%32])
b1 := int(h[(i*7+5)%32])
xv := 62 + (b0*37+i*97)%900
yv := 54 + (b1*29+i*83)%1420
r := 1 + (b0+i)%4
op := 10 + (b1 % 30)
if style.SparkleBoost > 0 && i%9 == 0 {
r += 2
op += 18
}
if style.SparkleBoost > 24 && i%13 == 0 {
fmt.Fprintf(&sparkles, `<path d="M%d %d l7 0 l0 -7 l3 0 l0 7 l7 0 l0 3 l-7 0 l0 7 l-3 0 l0 -7 l-7 0 z" fill="#ffffff" opacity="0.%02d"/>`, xv, yv, minInt(98, op))
continue
}
fmt.Fprintf(&sparkles, `<circle cx="%d" cy="%d" r="%d" fill="white" opacity="0.%02d"/>`, xv, yv, r, minInt(98, op))
}
return sparkles.String()
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
// renderCardSVG converts raw generated artwork into the deterministic final
// collectible card. Keeping the typography/layout outside the image model makes
// all cards line up exactly and avoids model-generated fake text/logos.
func renderCardSVG(art []byte, artExt string, x win, traits collectionTraits) []byte {
encoded := base64.StdEncoding.EncodeToString(art)
series := html.EscapeString(strings.ToUpper(trimLabel(x.DisplayName, 28)))
theme := html.EscapeString(strings.ToUpper(trimLabel(traits.ThemeName, 24)))
rarity := html.EscapeString(traits.Rarity)
completed := x.Completed.UTC().Format("2006.01.02")
mime := imageMIME(artExt)
editionLabel := "WINNING EDITION"
bitLabel := fmt.Sprintf("%d-BIT HUNT", x.RangeBits)
if strings.EqualFold(strings.TrimSpace(x.Origin), "admin_drop") {
editionLabel = "ADMIN DROP"
bitLabel = "COLLECTIBLE"
}
style := cardRarityStyle(traits)
h := sha256.Sum256([]byte(x.ID + "|card|" + x.Winner + "|" + x.Seed))
sparkles := renderSparkles(h, style)
svg := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1536" viewBox="0 0 1024 1536" role="img" aria-label="Neural Hunt collectible card">
<defs>
<clipPath id="cardClip"><rect x="26" y="26" width="972" height="1484" rx="58"/></clipPath>
<clipPath id="artClip"><rect x="42" y="42" width="940" height="1452" rx="48"/></clipPath>
<linearGradient id="metal" x1="0" y1="0" x2="1" y2="1"><stop stop-color="%s"/><stop offset=".18" stop-color="%s"/><stop offset=".48" stop-color="#101725"/><stop offset=".73" stop-color="%s"/><stop offset="1" stop-color="%s"/></linearGradient>
<linearGradient id="topShade" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#050811" stop-opacity=".82"/><stop offset="1" stop-color="#050811" stop-opacity="0"/></linearGradient>
<linearGradient id="bottomShade" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#07101d" stop-opacity="0"/><stop offset=".34" stop-color="#07101d" stop-opacity=".58"/><stop offset="1" stop-color="#030711" stop-opacity=".96"/></linearGradient>
<linearGradient id="holo" x1="0" y1="0" x2="1" y2="1"><stop stop-color="%s" stop-opacity=".78"/><stop offset=".42" stop-color="#ffffff" stop-opacity=".22"/><stop offset=".67" stop-color="%s" stop-opacity=".70"/><stop offset="1" stop-color="#ffffff" stop-opacity=".08"/></linearGradient>
<linearGradient id="foilA" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#ffffff" stop-opacity=".00"/><stop offset=".2" stop-color="%s" stop-opacity=".35"/><stop offset=".5" stop-color="#ffffff" stop-opacity=".12"/><stop offset=".76" stop-color="%s" stop-opacity=".30"/><stop offset="1" stop-color="#ffffff" stop-opacity=".00"/></linearGradient>
<linearGradient id="foilB" x1="0" y1="1" x2="1" y2="0"><stop stop-color="#ffffff" stop-opacity=".00"/><stop offset=".18" stop-color="%s" stop-opacity=".22"/><stop offset=".55" stop-color="#ffffff" stop-opacity=".10"/><stop offset=".86" stop-color="%s" stop-opacity=".26"/><stop offset="1" stop-color="#ffffff" stop-opacity=".00"/></linearGradient>
<radialGradient id="outerGlow" cx="50%%" cy="50%%" r="60%%"><stop offset="0%%" stop-color="%s" stop-opacity="%s"/><stop offset="78%%" stop-color="%s" stop-opacity="0"/></radialGradient>
<filter id="shadow"><feDropShadow dx="0" dy="18" stdDeviation="22" flood-color="#000000" flood-opacity=".55"/></filter>
<filter id="glow"><feDropShadow dx="0" dy="0" stdDeviation="16" flood-color="%s" flood-opacity="%s"/></filter>
</defs>
<g filter="url(#shadow)">
<rect x="18" y="18" width="988" height="1500" rx="66" fill="url(#metal)"/>
<rect x="28" y="28" width="968" height="1480" rx="56" fill="#0b101b"/>
<rect x="18" y="18" width="988" height="1500" rx="66" fill="url(#outerGlow)" opacity=".55"/>
</g>
<g clip-path="url(#artClip)">
<image x="42" y="42" width="940" height="1452" preserveAspectRatio="xMidYMid slice" href="data:%s;base64,%s"/>
<rect x="42" y="42" width="940" height="360" fill="url(#topShade)"/>
<rect x="42" y="940" width="940" height="554" fill="url(#bottomShade)"/>
<path d="M-80 270 L760 -80 L1100 80 L180 480 Z" fill="url(#holo)" opacity="%s"/>
<path d="M120 1530 L1130 1030 L1130 1190 L330 1530 Z" fill="url(#holo)" opacity="%s"/>
%s
<g>%s</g>
</g>
<rect x="42" y="42" width="940" height="1452" rx="48" fill="none" stroke="#ffffff" stroke-opacity="%s" stroke-width="2"/>
<rect x="49" y="49" width="926" height="1438" rx="42" fill="none" stroke="url(#holo)" stroke-opacity="%s" stroke-width="3" filter="url(#glow)"/>
<!-- Header -->
<g font-family="Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Arial,sans-serif" fill="#ffffff">
<text x="76" y="104" font-size="22" font-weight="800" letter-spacing="5" opacity=".76">NEURAL HUNT</text>
<line x1="76" y1="118" x2="286" y2="118" stroke="%s" stroke-width="5" stroke-linecap="round" opacity=".92"/>
<text x="76" y="154" font-size="17" font-weight="700" letter-spacing="4" opacity=".76">%s</text>
<g transform="translate(738 72)">
<rect width="198" height="62" rx="31" fill="%s" fill-opacity=".88" stroke="url(#holo)" stroke-width="2.4"/>
<text x="99" y="30" text-anchor="middle" font-size="18" font-weight="900" letter-spacing="2" fill="%s">%s</text>
<text x="99" y="47" text-anchor="middle" font-size="9.5" font-weight="800" letter-spacing="2.4" fill="%s" opacity=".82">%s</text>
</g>
</g>
<!-- Bottom collectible information panel -->
<g font-family="Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Arial,sans-serif" fill="#ffffff">
<text x="76" y="1234" font-size="15" font-weight="800" letter-spacing="5" opacity=".62">%s</text>
<text x="76" y="1290" font-size="42" font-weight="900" letter-spacing=".8">%s</text>
<line x1="76" y1="1320" x2="948" y2="1320" stroke="url(#holo)" stroke-width="2" opacity=".72"/>
<g transform="translate(76 1345)">
<rect width="216" height="50" rx="25" fill="%s" fill-opacity=".16" stroke="%s" stroke-opacity=".38"/>
<text x="108" y="32" text-anchor="middle" font-size="14" font-weight="900" letter-spacing="1.8" fill="%s">%s</text>
</g>
<text x="76" y="1414" font-size="16" font-weight="900" letter-spacing="1.8" fill="%s">%s</text>
<text x="76" y="1442" font-size="14" font-weight="700" letter-spacing="2" opacity=".56">%s · ORIGINAL FULL-ART SERIES</text>
<circle cx="930" cy="1437" r="11" fill="%s"/><circle cx="902" cy="1437" r="7" fill="%s"/>
</g>
</svg>`,
style.MetalBase, style.AccentA, style.AccentB, style.MetalEdge,
style.AccentA, style.AccentB,
style.AccentA, style.AccentB,
style.AccentB, style.AccentA,
style.OuterGlow, style.OuterGlowOpacity, style.OuterGlow,
style.OuterGlow, style.OuterGlowOpacity,
mime, encoded, style.HoloOpacityA, style.HoloOpacityB, style.FoilPattern, sparkles,
style.BorderOpacityA, style.BorderOpacityB,
style.HeaderLine,
series,
style.PillFill, style.PillText, html.EscapeString(style.BadgeText), style.PillText, html.EscapeString(style.BadgeSubtext),
editionLabel, theme,
style.EditionFill, style.EditionStroke, style.EditionText, bitLabel,
style.AccentA, rarity,
completed, style.AccentA, style.AccentB,
)
return []byte(svg)
}