removed default telegram gifts
This commit is contained in:
parent
139967399d
commit
b1836c78e8
25109 changed files with 1040 additions and 757922 deletions
185
internal/seed/giftdemo/lottie.go
Normal file
185
internal/seed/giftdemo/lottie.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
package giftdemo
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// This file hand-builds small, fully original 512x512 Lottie animations from
|
||||
// geometric primitives (polystars, ellipses, rings). They are deliberately
|
||||
// simple placeholders — the point is a legally-clean demo asset set the admin
|
||||
// can later replace with real artwork, not studio-grade graphics. Everything
|
||||
// here is emitted as plain shape/transform JSON, with no expressions and no
|
||||
// external assets, so it passes the Star Gift animation validator unchanged.
|
||||
|
||||
const (
|
||||
canvasSize = 512
|
||||
center = canvasSize / 2
|
||||
frameRate = 60
|
||||
// 120 frames @ 60fps = 2s loop, well under the 30s ceiling.
|
||||
outPoint = 120
|
||||
)
|
||||
|
||||
// rgb is a 0..1 normalized colour triplet, the form Lottie fills expect.
|
||||
type rgb [3]float64
|
||||
|
||||
func fromHex(v int) rgb {
|
||||
return rgb{
|
||||
float64((v>>16)&0xff) / 255,
|
||||
float64((v>>8)&0xff) / 255,
|
||||
float64(v&0xff) / 255,
|
||||
}
|
||||
}
|
||||
|
||||
type motion int
|
||||
|
||||
const (
|
||||
motionSpin motion = iota
|
||||
motionPulse
|
||||
motionSpinPulse
|
||||
)
|
||||
|
||||
// shapeKind picks which primitive the layer draws.
|
||||
type shapeKind int
|
||||
|
||||
const (
|
||||
shapeStar shapeKind = iota // pointed star (polystar type 1)
|
||||
shapePolygon
|
||||
shapeRing // ellipse outline + inner disc, used for "coin"
|
||||
shapeBurst
|
||||
)
|
||||
|
||||
type lottieSpec struct {
|
||||
kind shapeKind
|
||||
points int // star / polygon point count
|
||||
fill rgb
|
||||
stroke rgb
|
||||
strokeW float64 // 0 = no stroke
|
||||
radius float64
|
||||
motion motion
|
||||
}
|
||||
|
||||
// prop builds a static Lottie animated-value wrapper {a:0,k:value}.
|
||||
func prop(value any) map[string]any { return map[string]any{"a": 0, "k": value} }
|
||||
|
||||
// easing handles are arrays (never strings), so the validator's expression
|
||||
// check — which only rejects string-valued "x" keys — never trips on them.
|
||||
func keyframe(t float64, value []float64) map[string]any {
|
||||
return map[string]any{
|
||||
"t": t,
|
||||
"s": value,
|
||||
"i": map[string]any{"x": []float64{0.6}, "y": []float64{1}},
|
||||
"o": map[string]any{"x": []float64{0.4}, "y": []float64{0}},
|
||||
}
|
||||
}
|
||||
|
||||
func spinRotation() map[string]any {
|
||||
return map[string]any{"a": 1, "k": []any{
|
||||
keyframe(0, []float64{0}),
|
||||
keyframe(outPoint, []float64{360}),
|
||||
}}
|
||||
}
|
||||
|
||||
func pulseScale() map[string]any {
|
||||
return map[string]any{"a": 1, "k": []any{
|
||||
keyframe(0, []float64{100, 100, 100}),
|
||||
keyframe(outPoint/2, []float64{114, 114, 114}),
|
||||
keyframe(outPoint, []float64{100, 100, 100}),
|
||||
}}
|
||||
}
|
||||
|
||||
func transform(m motion) map[string]any {
|
||||
rotation := prop(0.0)
|
||||
scale := prop([]float64{100, 100, 100})
|
||||
switch m {
|
||||
case motionSpin:
|
||||
rotation = spinRotation()
|
||||
case motionPulse:
|
||||
scale = pulseScale()
|
||||
case motionSpinPulse:
|
||||
rotation = spinRotation()
|
||||
scale = pulseScale()
|
||||
}
|
||||
return map[string]any{
|
||||
"o": prop(100.0),
|
||||
"r": rotation,
|
||||
"p": prop([]float64{center, center, 0}),
|
||||
"a": prop([]float64{0, 0, 0}),
|
||||
"s": scale,
|
||||
"sk": prop(0.0),
|
||||
"sa": prop(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
func fill(c rgb) map[string]any {
|
||||
return map[string]any{
|
||||
"ty": "fl", "nm": "Fill", "r": 1,
|
||||
"o": prop(100.0),
|
||||
"c": prop([]float64{c[0], c[1], c[2]}),
|
||||
}
|
||||
}
|
||||
|
||||
func stroke(c rgb, width float64) map[string]any {
|
||||
return map[string]any{
|
||||
"ty": "st", "nm": "Stroke", "lc": 2, "lj": 2, "ml": 4,
|
||||
"o": prop(100.0),
|
||||
"w": prop(width),
|
||||
"c": prop([]float64{c[0], c[1], c[2]}),
|
||||
}
|
||||
}
|
||||
|
||||
func polystar(points int, starType int, outer, innerRatio float64) map[string]any {
|
||||
return map[string]any{
|
||||
"ty": "sr", "nm": "Polystar", "sy": starType,
|
||||
"d": 1,
|
||||
"pt": prop(float64(points)),
|
||||
"p": prop([]float64{0, 0}),
|
||||
"r": prop(0.0),
|
||||
"ir": prop(outer * innerRatio),
|
||||
"is": prop(0.0),
|
||||
"or": prop(outer),
|
||||
"os": prop(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
func ellipse(radius float64) map[string]any {
|
||||
return map[string]any{
|
||||
"ty": "el", "nm": "Ellipse", "d": 1,
|
||||
"p": prop([]float64{0, 0}),
|
||||
"s": prop([]float64{radius * 2, radius * 2}),
|
||||
}
|
||||
}
|
||||
|
||||
// renderLottie serializes one spec to Lottie JSON bytes.
|
||||
func renderLottie(spec lottieSpec) ([]byte, error) {
|
||||
var shapes []any
|
||||
switch spec.kind {
|
||||
case shapeStar:
|
||||
shapes = append(shapes, polystar(spec.points, 1, spec.radius, 0.5))
|
||||
case shapePolygon:
|
||||
shapes = append(shapes, polystar(spec.points, 2, spec.radius, 0.5))
|
||||
case shapeBurst:
|
||||
shapes = append(shapes, polystar(spec.points, 1, spec.radius, 0.32))
|
||||
case shapeRing:
|
||||
shapes = append(shapes, ellipse(spec.radius))
|
||||
}
|
||||
shapes = append(shapes, fill(spec.fill))
|
||||
if spec.strokeW > 0 {
|
||||
shapes = append(shapes, stroke(spec.stroke, spec.strokeW))
|
||||
}
|
||||
// A coin gets a smaller contrasting inner disc for a bit of depth.
|
||||
if spec.kind == shapeRing {
|
||||
shapes = append(shapes, ellipse(spec.radius*0.55), fill(spec.stroke))
|
||||
}
|
||||
|
||||
layer := map[string]any{
|
||||
"ddd": 0, "ind": 1, "ty": 4, "nm": "gift", "sr": 1,
|
||||
"ks": transform(spec.motion),
|
||||
"ao": 0,
|
||||
"shapes": shapes,
|
||||
"ip": 0, "op": outPoint, "st": 0, "bm": 0,
|
||||
}
|
||||
root := map[string]any{
|
||||
"v": "5.7.4", "fr": frameRate, "ip": 0, "op": outPoint,
|
||||
"w": canvasSize, "h": canvasSize, "nm": "owpengram-demo-gift",
|
||||
"ddd": 0, "assets": []any{}, "layers": []any{layer},
|
||||
}
|
||||
return json.Marshal(root)
|
||||
}
|
||||
259
internal/seed/giftdemo/source.go
Normal file
259
internal/seed/giftdemo/source.go
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
// Package giftdemo is an in-memory catalog of small, fully original demo Star
|
||||
// Gifts (geometric Lottie authored here — not Telegram's copyrighted assets).
|
||||
// It backs the admin console's "Default gifts" import source: operators can
|
||||
// import these to demo the complete gift surface (upgrade + craft) without any
|
||||
// third-party artwork. Nothing here is enabled automatically; gifts appear in
|
||||
// the catalog only once an operator imports them.
|
||||
package giftdemo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// Preparer normalizes raw Lottie bytes into the canonical Star Gift animation
|
||||
// pair. *stargifts.Service satisfies it.
|
||||
type Preparer interface {
|
||||
PrepareAnimation(fileName string, data []byte) (domain.StarGiftAnimation, error)
|
||||
}
|
||||
|
||||
const (
|
||||
seedActor = "system-default-gifts"
|
||||
seedCommandID = "default-gifts-v1"
|
||||
)
|
||||
|
||||
type attrSpec struct {
|
||||
name string
|
||||
spec lottieSpec
|
||||
permille int // >0 for a normal (drawable) attribute
|
||||
crafted bool // craft-only model; permille must be 0 and rarity named
|
||||
rarity domain.StarGiftAttributeRarityKind
|
||||
}
|
||||
|
||||
type backdropSpec struct {
|
||||
name string
|
||||
center int
|
||||
edge int
|
||||
pattern int
|
||||
text int
|
||||
permille int
|
||||
}
|
||||
|
||||
type upgradeSpec struct {
|
||||
upgradeStars int64
|
||||
supplyTotal int
|
||||
slug string
|
||||
models []attrSpec
|
||||
patterns []attrSpec
|
||||
backdrops []backdropSpec
|
||||
}
|
||||
|
||||
type giftSpec struct {
|
||||
title string
|
||||
stars int64
|
||||
convert int64
|
||||
base lottieSpec
|
||||
limited bool
|
||||
availability int
|
||||
requirePremium bool
|
||||
birthday bool
|
||||
upgrade *upgradeSpec
|
||||
}
|
||||
|
||||
// GiftInfo is a catalog listing entry for the import picker.
|
||||
type GiftInfo struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Stars int64 `json:"stars"`
|
||||
ConvertStars int64 `json:"convert_stars"`
|
||||
UpgradeStars int64 `json:"upgrade_stars"`
|
||||
Upgradeable bool `json:"upgradeable"`
|
||||
Craftable bool `json:"craftable"`
|
||||
Limited bool `json:"limited"`
|
||||
Availability int `json:"availability"`
|
||||
RequirePremium bool `json:"require_premium"`
|
||||
ModelCount int `json:"model_count"`
|
||||
PatternCount int `json:"pattern_count"`
|
||||
BackdropCount int `json:"backdrop_count"`
|
||||
CraftedCount int `json:"crafted_count"`
|
||||
}
|
||||
|
||||
// List returns the built-in demo gifts, ids 1..N in display order.
|
||||
func List() []GiftInfo {
|
||||
gifts := demoGifts()
|
||||
out := make([]GiftInfo, 0, len(gifts))
|
||||
for i, spec := range gifts {
|
||||
info := GiftInfo{
|
||||
ID: i + 1, Title: spec.title, Stars: spec.stars, ConvertStars: spec.convert,
|
||||
Limited: spec.limited, Availability: spec.availability, RequirePremium: spec.requirePremium,
|
||||
}
|
||||
if spec.upgrade != nil {
|
||||
info.Upgradeable = true
|
||||
info.UpgradeStars = spec.upgrade.upgradeStars
|
||||
info.ModelCount = len(spec.upgrade.models)
|
||||
info.PatternCount = len(spec.upgrade.patterns)
|
||||
info.BackdropCount = len(spec.upgrade.backdrops)
|
||||
for _, m := range spec.upgrade.models {
|
||||
if m.crafted {
|
||||
info.CraftedCount++
|
||||
}
|
||||
}
|
||||
info.Craftable = info.CraftedCount > 0
|
||||
}
|
||||
out = append(out, info)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func specByID(id int) (giftSpec, int, bool) {
|
||||
gifts := demoGifts()
|
||||
if id < 1 || id > len(gifts) {
|
||||
return giftSpec{}, 0, false
|
||||
}
|
||||
return gifts[id-1], id - 1, true
|
||||
}
|
||||
|
||||
// BaseAnimationJSON returns the normalized Lottie JSON of a gift's base sticker,
|
||||
// used by the admin preview player.
|
||||
func BaseAnimationJSON(prep Preparer, id int) ([]byte, bool, error) {
|
||||
spec, _, ok := specByID(id)
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
anim, err := prepare(prep, spec.title, spec.base)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return anim.JSON, true, nil
|
||||
}
|
||||
|
||||
// BuildBundle assembles the complete catalog+collectible write for one demo
|
||||
// gift. The bundle carries no official provenance (OfficialGiftID stays 0), so
|
||||
// these import as ordinary locally-authored catalog entries.
|
||||
func BuildBundle(prep Preparer, id, sortOrder int) (domain.StarGiftCatalogBundleWrite, string, error) {
|
||||
spec, _, ok := specByID(id)
|
||||
if !ok {
|
||||
return domain.StarGiftCatalogBundleWrite{}, "", fmt.Errorf("unknown demo gift id %d", id)
|
||||
}
|
||||
write, err := buildBundle(prep, spec, sortOrder)
|
||||
if err != nil {
|
||||
return domain.StarGiftCatalogBundleWrite{}, "", err
|
||||
}
|
||||
return write, spec.title, nil
|
||||
}
|
||||
|
||||
// CatalogReader is the read side used to skip gifts already present by title.
|
||||
type CatalogReader interface {
|
||||
Catalog(ctx context.Context) ([]domain.StarGift, error)
|
||||
}
|
||||
|
||||
// PresentTitles returns the set of demo gift titles already in the catalog, so
|
||||
// callers can import idempotently.
|
||||
func PresentTitles(ctx context.Context, reader CatalogReader) (map[string]struct{}, error) {
|
||||
existing, err := reader.Catalog(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
demo := map[string]struct{}{}
|
||||
for _, spec := range demoGifts() {
|
||||
demo[spec.title] = struct{}{}
|
||||
}
|
||||
present := map[string]struct{}{}
|
||||
for _, gift := range existing {
|
||||
if _, ok := demo[gift.Title]; ok {
|
||||
present[gift.Title] = struct{}{}
|
||||
}
|
||||
}
|
||||
return present, nil
|
||||
}
|
||||
|
||||
func buildBundle(prep Preparer, spec giftSpec, sortOrder int) (domain.StarGiftCatalogBundleWrite, error) {
|
||||
baseAnim, err := prepare(prep, spec.title, spec.base)
|
||||
if err != nil {
|
||||
return domain.StarGiftCatalogBundleWrite{}, err
|
||||
}
|
||||
catalog := domain.StarGiftCatalogWrite{
|
||||
Title: spec.title,
|
||||
Stars: spec.stars,
|
||||
ConvertStars: spec.convert,
|
||||
Enabled: true,
|
||||
SortOrder: sortOrder,
|
||||
Animation: baseAnim,
|
||||
Actor: seedActor,
|
||||
CommandID: seedCommandID,
|
||||
}
|
||||
if spec.limited {
|
||||
catalog.Limited = true
|
||||
catalog.AvailabilityTotal = spec.availability
|
||||
catalog.AvailabilityRemains = spec.availability
|
||||
}
|
||||
catalog.RequirePremium = spec.requirePremium
|
||||
catalog.Birthday = spec.birthday
|
||||
|
||||
write := domain.StarGiftCatalogBundleWrite{Catalog: catalog}
|
||||
if spec.upgrade == nil {
|
||||
return write, nil
|
||||
}
|
||||
|
||||
up := spec.upgrade
|
||||
models, err := buildAttributes(prep, up.models, domain.StarGiftCollectibleModel)
|
||||
if err != nil {
|
||||
return domain.StarGiftCatalogBundleWrite{}, err
|
||||
}
|
||||
patterns, err := buildAttributes(prep, up.patterns, domain.StarGiftCollectiblePattern)
|
||||
if err != nil {
|
||||
return domain.StarGiftCatalogBundleWrite{}, err
|
||||
}
|
||||
backdrops := make([]domain.StarGiftCollectibleAttribute, 0, len(up.backdrops))
|
||||
for i, b := range up.backdrops {
|
||||
backdrops = append(backdrops, domain.StarGiftCollectibleAttribute{
|
||||
Kind: domain.StarGiftCollectibleBackdrop, Name: b.name, BackdropID: i + 1,
|
||||
CenterColor: b.center, EdgeColor: b.edge, PatternColor: b.pattern, TextColor: b.text,
|
||||
RarityKind: domain.StarGiftRarityPermille, RarityPermille: b.permille, SortOrder: i,
|
||||
})
|
||||
}
|
||||
write.Collectible = &domain.StarGiftCollectibleWrite{
|
||||
UpgradeStars: up.upgradeStars,
|
||||
SupplyTotal: up.supplyTotal,
|
||||
SlugPrefix: up.slug,
|
||||
Models: models,
|
||||
Patterns: patterns,
|
||||
Backdrops: backdrops,
|
||||
Actor: seedActor,
|
||||
CommandID: seedCommandID,
|
||||
}
|
||||
return write, nil
|
||||
}
|
||||
|
||||
func buildAttributes(prep Preparer, specs []attrSpec, kind domain.StarGiftCollectibleAttributeKind) ([]domain.StarGiftCollectibleAttribute, error) {
|
||||
out := make([]domain.StarGiftCollectibleAttribute, 0, len(specs))
|
||||
for i, s := range specs {
|
||||
anim, err := prepare(prep, s.name, s.spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attr := domain.StarGiftCollectibleAttribute{
|
||||
Kind: kind, Name: s.name, SortOrder: i, Animation: &anim,
|
||||
}
|
||||
if s.crafted {
|
||||
attr.Crafted = true
|
||||
attr.RarityKind = s.rarity
|
||||
attr.RarityPermille = 0
|
||||
} else {
|
||||
attr.RarityKind = domain.StarGiftRarityPermille
|
||||
attr.RarityPermille = s.permille
|
||||
}
|
||||
out = append(out, attr)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func prepare(prep Preparer, name string, spec lottieSpec) (domain.StarGiftAnimation, error) {
|
||||
data, err := renderLottie(spec)
|
||||
if err != nil {
|
||||
return domain.StarGiftAnimation{}, err
|
||||
}
|
||||
return prep.PrepareAnimation(name+".json", data)
|
||||
}
|
||||
133
internal/seed/giftdemo/source_test.go
Normal file
133
internal/seed/giftdemo/source_test.go
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
package giftdemo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/app/stargifts"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
type fakeBlob struct{ data map[string][]byte }
|
||||
|
||||
func (b *fakeBlob) Name() string { return "localfs" }
|
||||
func (b *fakeBlob) Put(_ context.Context, data []byte) (string, error) {
|
||||
sum := sha256.Sum256(data)
|
||||
key := hex.EncodeToString(sum[:])
|
||||
b.data[key] = append([]byte(nil), data...)
|
||||
return key, nil
|
||||
}
|
||||
func (b *fakeBlob) Get(_ context.Context, key string) ([]byte, error) {
|
||||
return append([]byte(nil), b.data[key]...), nil
|
||||
}
|
||||
|
||||
func newService() *stargifts.Service {
|
||||
return stargifts.NewService(memory.NewStarGiftStore(), &fakeBlob{data: map[string][]byte{}}, 2)
|
||||
}
|
||||
|
||||
func TestListDescribesFullGiftSurface(t *testing.T) {
|
||||
list := List()
|
||||
if len(list) != 5 {
|
||||
t.Fatalf("List has %d gifts, want 5", len(list))
|
||||
}
|
||||
upgradeable, craftable, limited, premium := 0, 0, 0, 0
|
||||
for _, g := range list {
|
||||
if g.ID < 1 || g.Title == "" || g.Stars <= 0 {
|
||||
t.Fatalf("bad gift info: %+v", g)
|
||||
}
|
||||
if g.Upgradeable {
|
||||
upgradeable++
|
||||
}
|
||||
if g.Craftable {
|
||||
craftable++
|
||||
}
|
||||
if g.Limited {
|
||||
limited++
|
||||
}
|
||||
if g.RequirePremium {
|
||||
premium++
|
||||
}
|
||||
}
|
||||
if upgradeable != 4 || craftable != 3 || limited != 2 || premium != 1 {
|
||||
t.Fatalf("surface counts upgradeable=%d craftable=%d limited=%d premium=%d", upgradeable, craftable, limited, premium)
|
||||
}
|
||||
}
|
||||
|
||||
// Every demo gift must build into a valid catalog+collectible bundle and
|
||||
// import cleanly through the real service (which materializes and validates
|
||||
// the whole pool). Limited/premium flags must survive onto the stored gift.
|
||||
func TestBuildBundleImportsEndToEnd(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := newService()
|
||||
|
||||
for _, info := range List() {
|
||||
write, title, err := BuildBundle(svc, info.ID, info.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("build %q: %v", info.Title, err)
|
||||
}
|
||||
if title != info.Title {
|
||||
t.Fatalf("title mismatch %q != %q", title, info.Title)
|
||||
}
|
||||
if _, err := svc.CreateCatalogBundle(ctx, write); err != nil {
|
||||
t.Fatalf("import %q: %v", info.Title, err)
|
||||
}
|
||||
}
|
||||
|
||||
catalog, err := svc.Catalog(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(catalog) != 5 {
|
||||
t.Fatalf("catalog has %d, want 5", len(catalog))
|
||||
}
|
||||
limited, premium, upgradeable := 0, 0, 0
|
||||
for _, g := range catalog {
|
||||
if g.Limited {
|
||||
limited++
|
||||
}
|
||||
if g.RequirePremium {
|
||||
premium++
|
||||
}
|
||||
if g.UpgradeStars > 0 {
|
||||
upgradeable++
|
||||
}
|
||||
}
|
||||
if limited != 2 || premium != 1 || upgradeable != 4 {
|
||||
t.Fatalf("stored flags limited=%d premium=%d upgradeable=%d", limited, premium, upgradeable)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseAnimationJSONRenders(t *testing.T) {
|
||||
svc := newService()
|
||||
data, ok, err := BaseAnimationJSON(svc, 1)
|
||||
if err != nil || !ok || len(data) == 0 {
|
||||
t.Fatalf("base animation id=1: ok=%v err=%v len=%d", ok, err, len(data))
|
||||
}
|
||||
if _, ok, _ := BaseAnimationJSON(svc, 99); ok {
|
||||
t.Fatalf("id=99 should not exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPresentTitles(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
svc := newService()
|
||||
write, _, err := BuildBundle(svc, 1, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.CreateCatalogBundle(ctx, write); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
present, err := PresentTitles(ctx, svc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(present) != 1 {
|
||||
t.Fatalf("present=%v, want exactly the one imported title", present)
|
||||
}
|
||||
if _, ok := present["OwpenGram Spark"]; !ok {
|
||||
t.Fatalf("expected Spark present, got %v", present)
|
||||
}
|
||||
}
|
||||
150
internal/seed/giftdemo/specs.go
Normal file
150
internal/seed/giftdemo/specs.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
package giftdemo
|
||||
|
||||
import "telesrv/internal/domain"
|
||||
|
||||
// Palette (hex) shared across the demo assets.
|
||||
const (
|
||||
colGold = 0xF5C542
|
||||
colAmber = 0xF59E0B
|
||||
colBlue = 0x2563EB
|
||||
colCyan = 0x38BDF8
|
||||
colViolet = 0x7C3AED
|
||||
colEmerald = 0x10B981
|
||||
colRose = 0xF43F5E
|
||||
colWhite = 0xFFFFFF
|
||||
colSlate = 0x1E293B
|
||||
colMidnight = 0x0B1220
|
||||
colDeepEm = 0x065F46
|
||||
)
|
||||
|
||||
func star(points int, fill, stroke int, strokeW float64, m motion) lottieSpec {
|
||||
return lottieSpec{kind: shapeStar, points: points, fill: fromHex(fill), stroke: fromHex(stroke), strokeW: strokeW, radius: 168, motion: m}
|
||||
}
|
||||
|
||||
func polygon(points, fill, stroke int, strokeW float64, m motion) lottieSpec {
|
||||
return lottieSpec{kind: shapePolygon, points: points, fill: fromHex(fill), stroke: fromHex(stroke), strokeW: strokeW, radius: 150, motion: m}
|
||||
}
|
||||
|
||||
func ring(fill, inner int, m motion) lottieSpec {
|
||||
return lottieSpec{kind: shapeRing, fill: fromHex(fill), stroke: fromHex(inner), radius: 150, motion: m}
|
||||
}
|
||||
|
||||
func burst(points, fill int, m motion) lottieSpec {
|
||||
return lottieSpec{kind: shapeBurst, points: points, fill: fromHex(fill), radius: 150, motion: m}
|
||||
}
|
||||
|
||||
// Four colour-only backdrops reused across every upgradeable gift (backdrops
|
||||
// carry no animation asset, so sharing them is free).
|
||||
func demoBackdrops() []backdropSpec {
|
||||
return []backdropSpec{
|
||||
{name: "Midnight", center: colSlate, edge: colMidnight, pattern: colCyan, text: colWhite, permille: 400},
|
||||
{name: "Sunset", center: colAmber, edge: colRose, pattern: colWhite, text: colSlate, permille: 300},
|
||||
{name: "Emerald", center: colEmerald, edge: colDeepEm, pattern: colWhite, text: colWhite, permille: 200},
|
||||
{name: "Royal", center: colViolet, edge: colBlue, pattern: colGold, text: colWhite, permille: 100},
|
||||
}
|
||||
}
|
||||
|
||||
// demoGifts returns the five demo gifts in display order.
|
||||
func demoGifts() []giftSpec {
|
||||
return []giftSpec{
|
||||
{
|
||||
// #1 — cheapest, plain, not upgradeable.
|
||||
title: "OwpenGram Spark",
|
||||
stars: 15,
|
||||
convert: 15,
|
||||
base: burst(8, colGold, motionPulse),
|
||||
},
|
||||
{
|
||||
// #2 — standard upgradeable, no crafting.
|
||||
title: "OwpenGram Star",
|
||||
stars: 50,
|
||||
convert: 50,
|
||||
base: star(5, colBlue, colCyan, 10, motionSpin),
|
||||
upgrade: &upgradeSpec{
|
||||
upgradeStars: 200,
|
||||
supplyTotal: 10000,
|
||||
slug: "owg-star",
|
||||
models: []attrSpec{
|
||||
{name: "Sapphire", spec: star(5, colBlue, colCyan, 12, motionSpin), permille: 600},
|
||||
{name: "Frost", spec: star(6, colCyan, colWhite, 10, motionSpin), permille: 400},
|
||||
},
|
||||
patterns: []attrSpec{
|
||||
{name: "Halo", spec: burst(12, colCyan, motionPulse), permille: 700},
|
||||
{name: "Drift", spec: burst(8, colBlue, motionPulse), permille: 300},
|
||||
},
|
||||
backdrops: demoBackdrops(),
|
||||
},
|
||||
},
|
||||
{
|
||||
// #3 — upgradeable + craftable.
|
||||
title: "OwpenGram Coin",
|
||||
stars: 100,
|
||||
convert: 75,
|
||||
base: ring(colAmber, colSlate, motionSpin),
|
||||
upgrade: &upgradeSpec{
|
||||
upgradeStars: 400,
|
||||
supplyTotal: 8000,
|
||||
slug: "owg-coin",
|
||||
models: []attrSpec{
|
||||
{name: "Bronze", spec: ring(colAmber, colSlate, motionSpin), permille: 600},
|
||||
{name: "Silver", spec: ring(colWhite, colSlate, motionSpin), permille: 400},
|
||||
{name: "Molten", spec: ring(colRose, colAmber, motionSpinPulse), crafted: true, rarity: domain.StarGiftRarityRare},
|
||||
},
|
||||
patterns: []attrSpec{
|
||||
{name: "Gleam", spec: burst(10, colGold, motionPulse), permille: 700},
|
||||
{name: "Ember", spec: burst(6, colAmber, motionPulse), permille: 300},
|
||||
},
|
||||
backdrops: demoBackdrops(),
|
||||
},
|
||||
},
|
||||
{
|
||||
// #4 — limited edition, upgradeable + craftable.
|
||||
title: "OwpenGram Gem",
|
||||
stars: 250,
|
||||
convert: 200,
|
||||
base: polygon(6, colViolet, colWhite, 10, motionSpinPulse),
|
||||
limited: true,
|
||||
availability: 5000,
|
||||
upgrade: &upgradeSpec{
|
||||
upgradeStars: 800,
|
||||
supplyTotal: 3000,
|
||||
slug: "owg-gem",
|
||||
models: []attrSpec{
|
||||
{name: "Amethyst", spec: polygon(6, colViolet, colWhite, 12, motionSpin), permille: 600},
|
||||
{name: "Verdant", spec: polygon(6, colEmerald, colWhite, 12, motionSpin), permille: 400},
|
||||
{name: "Prism", spec: polygon(8, colCyan, colWhite, 10, motionSpinPulse), crafted: true, rarity: domain.StarGiftRarityEpic},
|
||||
},
|
||||
patterns: []attrSpec{
|
||||
{name: "Facet", spec: burst(12, colViolet, motionPulse), permille: 700},
|
||||
{name: "Shine", spec: burst(8, colWhite, motionPulse), permille: 300},
|
||||
},
|
||||
backdrops: demoBackdrops(),
|
||||
},
|
||||
},
|
||||
{
|
||||
// #5 — premium-gated, limited, the full stack.
|
||||
title: "OwpenGram Crown",
|
||||
stars: 1000,
|
||||
convert: 800,
|
||||
base: star(3, colGold, colAmber, 12, motionSpinPulse),
|
||||
limited: true,
|
||||
availability: 500,
|
||||
requirePremium: true,
|
||||
upgrade: &upgradeSpec{
|
||||
upgradeStars: 2000,
|
||||
supplyTotal: 500,
|
||||
slug: "owg-crown",
|
||||
models: []attrSpec{
|
||||
{name: "Regal", spec: star(3, colGold, colAmber, 14, motionSpinPulse), permille: 600},
|
||||
{name: "Noble", spec: star(5, colAmber, colGold, 12, motionSpin), permille: 400},
|
||||
{name: "Eternal", spec: star(6, colGold, colWhite, 12, motionSpinPulse), crafted: true, rarity: domain.StarGiftRarityLegendary},
|
||||
},
|
||||
patterns: []attrSpec{
|
||||
{name: "Aura", spec: burst(12, colGold, motionPulse), permille: 700},
|
||||
{name: "Crest", spec: burst(8, colWhite, motionPulse), permille: 300},
|
||||
},
|
||||
backdrops: demoBackdrops(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue