Compare commits
2 Commits
1a20501e55
...
900b274a9a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
900b274a9a | ||
|
|
081755bb71 |
@ -4,6 +4,20 @@
|
||||
plus the game shell. It runs on my hand-written fixture **and** auto-integrates with Lane A's
|
||||
`generatePlan` (which landed mid-session). Everything below was verified live in a browser.
|
||||
|
||||
## Round 8 (Fable's ROUND8 → Lane B) — weather (`?weather=1`), carried from R7 · qa GREEN
|
||||
|
||||
New `js/world/weather.js` + `lighting.setSky()`. Seeded per citySeed → clear/overcast/rain
|
||||
(~57/24/19% over 400 seeds, deterministic). **Rain = ONE draw** (camera-following `Points` + streak
|
||||
shader) + wet ground (roughness/darken on shared ground mats, restored on dispose) + matching rainy
|
||||
sky (`lighting.setSky`). Fully procedural (0 depot fetch). Worst view **141 draws** (≤300); composes
|
||||
with `?winmap`+night — **rain outside a lit interior-mapped window** shot saved. Deterministic, 0
|
||||
console errors, **flag-off byte-identical** (shell never constructs it).
|
||||
- **Exposed the `window.PROCITY.weather` contract for Lane D** (decision #1): `{ state, intensity }`,
|
||||
documented in LANE_B_NOTES with the exact shell-wiring 3 lines for F (I don't touch index.html).
|
||||
`?weather=1` seeded (default town rolls clear); `?weather=rain|overcast|clear` forces a state.
|
||||
- Deferred (cut line): vertex-shader wind sway on trees/awnings — noted, not done (touches other lanes'
|
||||
materials); everything else in the R7 brief landed. Tram stretch not attempted.
|
||||
|
||||
## Round 6 (Fable's ROUND6 → Lane B) — placed the orphaned street props · qa GREEN
|
||||
|
||||
Wired the two published-but-unconsumed street GLBs (`street_bin_01`, `bus_shelter_01`) via the
|
||||
|
||||
@ -4,6 +4,23 @@
|
||||
Reviewed against `docs/LANES/LANE_E_ASSETS.md` + `docs/LANES/ROUND3_INSTRUCTIONS.md`. Nothing in
|
||||
other lanes' files was touched.*
|
||||
|
||||
## Round 8 (Fable) — pack kitchen (book/toy) + pack-QA gate
|
||||
|
||||
- **E1 — pack pipeline generalized + book/toy sample packs.** `build_stock_pack.py` now kind-
|
||||
parametrized (record 256×256, **book 96×256 tall-thin spines**, toy 256×256), per-kind parody word
|
||||
banks, same laws. Book/toy sample packs built + staged (`?localdepot`) + indexes committed to
|
||||
`pipeline/packs/` so **Lane C wires book/toy shelves now**. Parody verified deterministic + no
|
||||
distinctive-token leakage across all 3 kinds.
|
||||
- **E2 — pack-QA gate: DONE.** `validate_pack.py` (wired into `validate_manifest.py` → qa gate 3):
|
||||
a bad bake (out-of-range/inverted UV, empty title, bad price_band, dangling atlas, dup id) **fails
|
||||
qa** — proven (inverted-UV+empty-title → exit 1). qa `--strict` GREEN 5/5.
|
||||
- **Tags pushed** (John-delegated): origin/main = HEAD (1a20501) + v1.0/v1.1/v2.0-alpha/v2.0-beta all
|
||||
live on origin — verified.
|
||||
- **Blocker (flagged to Fable):** real ~150–250-item book/toy (+ record) packs need `GODVERSE_DSN`
|
||||
(dealgod Postgres, up on :5432, read from env). One `--kind X --from-db` command each once set.
|
||||
Current packs are synthetic placeholders (real index format). No fal spend.
|
||||
- **On-call** (B rain-streak/puddle, D door-swing tell): none requested.
|
||||
|
||||
## Round 7 (Fable) — the two feeders (ped merge + stock pack)
|
||||
|
||||
- **E1 — ped sub-mesh merge: DONE + committed** (`447188a`, ref `lane-e/round7-peds`). All 19 rigs
|
||||
|
||||
@ -1,5 +1,48 @@
|
||||
# LANE B — NOTES (measured)
|
||||
|
||||
## Round 8 (Fable's ROUND8 → Lane B) — weather (`?weather=1`), carried from R7
|
||||
|
||||
New file `js/world/weather.js` + a one-line `setSky()` on `lighting.js`. Seeded, deterministic, cheap.
|
||||
|
||||
- **Weather state** — `weatherFor(citySeed, day=0)` → `{ state, intensity }`, weights ~57% clear /
|
||||
24% overcast / 19% rain (AU coastal-town plausible; verified over 400 seeds). `day=0` today (one
|
||||
day cycle); a future day-counter rolls new weather. **Seed 20261990 (default town) rolls `clear`**,
|
||||
so `?weather=1` there is v1-identical — use **`?weather=rain`** (or `overcast`) to force a state for
|
||||
demos/smokes, or a rainy seed (7, 424242).
|
||||
- **Rain** — ONE draw: a **camera-following `THREE.Points`** layer with a procedural streak sprite and
|
||||
a fall+wind vertex shader (no CPU per-particle). Plus **wet ground** (darken + drop roughness on the
|
||||
shared ground materials, restored on dispose) and a **matching rainy sky dome** (`lighting.setSky` →
|
||||
summer-storm/monsoon/cold-front). Overcast: greyer dome, no particles. Fully procedural — 0 depot/GLB
|
||||
fetch (the sky swap is a skin JPEG, same class as the town's own).
|
||||
- **Budget/composition/determinism**: worst view **141 draws (day rain) / 126 (night)** — ≤300.
|
||||
Composes with `?winmap` + night — **rain outside a lit interior-mapped window is the money shot**
|
||||
(`docs/shots/laneB/weather_rain_night_winmap.png`). Deterministic per seed. `?noassets` compatible.
|
||||
**0 console errors** in all states. **Flag-off is byte-identical** (the shell simply never constructs
|
||||
weather). `qa.sh --strict` GREEN. *(Deferred, "if cheap": vertex-shader wind sway on trees/awnings —
|
||||
needs touching furniture's tree material + skins' awning material; noted, not done this round.)*
|
||||
|
||||
### `window.PROCITY.weather` contract (Lane D consumes this — decision #1)
|
||||
Shape: **`{ state: 'clear' | 'overcast' | 'rain', intensity: 0..1 }`**, stable for the session.
|
||||
`intensity` is 0 for clear, ~0.3–0.6 overcast, ~0.45–1.0 rain. Set by `createWeather` when the flag is
|
||||
on; **F sets `{ state:'clear', intensity:0 }` when off** so D always reads a valid value (below). D:
|
||||
read it, don't import weather.js.
|
||||
|
||||
### → Fable (F, shell wiring — I don't touch `index.html`)
|
||||
```js
|
||||
import { createWeather } from './js/world/weather.js';
|
||||
const wp = params.get('weather');
|
||||
const weather = (wp && wp !== '0')
|
||||
? createWeather({ scene, plan, skins, lighting, camera, force: /^(clear|overcast|rain)$/.test(wp) ? wp : null })
|
||||
: null; // seeded when ?weather=1; forced when ?weather=rain|overcast|clear
|
||||
window.PROCITY.weather = weather ? weather.state : { state: 'clear', intensity: 0 };
|
||||
// in the street branch of the main loop: if (weather) weather.update(dt);
|
||||
```
|
||||
Flags-table row: `?weather=1` — owner B — landed, default-off, self-contained module (F wires the 3
|
||||
lines above). Smoke: boot `?weather=rain`, assert rain Points present + `PROCITY.weather.state==='rain'`,
|
||||
0 errors.
|
||||
|
||||
---
|
||||
|
||||
## Round 6 (Fable's ROUND6 → Lane B) — place the orphaned street props
|
||||
|
||||
Placed the two published-but-unconsumed street GLBs (`procity_street_bin_01`, `procity_street_bus_shelter_01`)
|
||||
|
||||
@ -1,5 +1,25 @@
|
||||
# LANE E — cross-lane notes
|
||||
|
||||
## Round 8
|
||||
|
||||
**→ Lane C (E1 — book + toy packs staged for you):** `build_stock_pack.py` is generalized over kinds;
|
||||
book/toy **sample packs are staged** (`web/assets/models/stock_{book,toy}_{index.json,atlas_00.webp}`,
|
||||
`?localdepot`) + indexes committed to `pipeline/packs/`. Same schema you proved for records — wire
|
||||
book/toy shelves through the stockAdapter, fail-soft per kind. **Cell geometry:** record 256×256
|
||||
(square), **book 96×256 (tall-thin spine)**, toy 256×256 (box). You read `uv` (I confirmed from your
|
||||
notes), so render each kind on its own shelf geometry; the index also carries `cell_w`/`cell_h` if
|
||||
useful. `artist` carries author (book) / brand (toy). These are **synthetic placeholders** (real index
|
||||
format) until the DSN lands — your fail-soft + wiring are unaffected when real covers swap in.
|
||||
|
||||
**E2 — pack-QA gate is live** (`validate_pack.py` in qa gate 3 via validate_manifest): a bad bake
|
||||
(bad UV/price-band/empty title/dangling atlas) fails qa. If you add index fields, tell me and I'll
|
||||
extend `REQUIRED`.
|
||||
|
||||
**→ Fable (blocker, flagged early per the slip rule):** the **real** book/toy/record packs need
|
||||
**`GODVERSE_DSN`** (dealgod Postgres — up on :5432; builder reads it from env, I don't hunt creds).
|
||||
One command per kind once set (`--kind book --from-db --count 200`). Pipeline + parody + atlas + QA
|
||||
gate all proven on samples; not blocking C (samples staged). No fal spend.
|
||||
|
||||
## Round 7
|
||||
|
||||
**→ Lane D (D1 — the flip gate): merged peds ready to validate.** `web/models/peds/*.glb` (19 rigs)
|
||||
|
||||
BIN
docs/shots/laneB/weather_rain_day.png
Normal file
BIN
docs/shots/laneB/weather_rain_day.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 761 KiB |
BIN
docs/shots/laneB/weather_rain_night_winmap.png
Normal file
BIN
docs/shots/laneB/weather_rain_night_winmap.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
@ -305,3 +305,26 @@ and the same source row yields the same parody every build. Verified determinist
|
||||
Provenance of the real pack: source = Discogs metadata + cover images (recordgod), **metadata
|
||||
parody-transformed** (no real titles/artists ship); covers are real release art (read-only, non-economy
|
||||
per decision #2). Small book/toy packs are a follow-up once the record pack lands.
|
||||
|
||||
## Round 8 — E1/E2: book + toy stock packs + pack-QA gate
|
||||
|
||||
**E1 — pipeline generalized** (`build_stock_pack.py` now kind-parametrized: `record` 256×256 sleeves,
|
||||
`book` **96×256 tall-thin spines**, `toy` 256×256 boxes). Per-kind parody word banks; same laws as
|
||||
the record pack (build-time extract, deterministic parody-transform of metadata, atlas WebPs + JSON
|
||||
index to C's schema — `stock_<kind>_index.json`, C reads `uv` not `cell`). Book/toy **sample packs
|
||||
built + staged** (`web/assets/models/`, `?localdepot`) so Lane C wires book/toy shelves now; indexes
|
||||
committed to `pipeline/packs/` as the contract. Parody verified deterministic + no distinctive-token
|
||||
leakage across all 3 kinds ("Nevermind"/"Nirvana" → "Endless Blue"/"Mullet Milk Crates";
|
||||
"The Hobbit" → "Under the Homestead"/"C. Underwood").
|
||||
|
||||
**E2 — pack-QA gate** (`validate_pack.py`, wired into `validate_manifest.py` → qa gate 3): every
|
||||
`pipeline/packs/stock_*_index.json` entry must have a valid kind, in-range non-inverted UVs, a known
|
||||
price band, non-empty title/artist, unique id, and an atlas listed in `atlases[]` — a bad bake fails
|
||||
qa loudly (proven: inverted-UV + empty-title → exit 1). qa `--strict` GREEN 5/5.
|
||||
|
||||
**Blocker (flagged to Fable/John — real data):** the **real** ~150–250-item book/toy packs (and the
|
||||
real record pack) need **`GODVERSE_DSN`** (dealgod Postgres; up on :5432, read from env, no cred
|
||||
hunt). One command each once set: `build_stock_pack.py --kind book --from-db --count 200`. The
|
||||
current staged packs are synthetic placeholders (real index format); provenance of the real packs =
|
||||
GODVERSE metadata (OpenLibrary/toygod) **parody-transformed** (no real titles/authors/brands ship) +
|
||||
real cover art (read-only, non-economy). No fal spend this round.
|
||||
|
||||
@ -1,154 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PROCITY GODVERSE stock pack — real record sleeves for C's dig (?stock=real), round-7 E2.
|
||||
"""PROCITY GODVERSE stock packs — real stock for C's shelves + dig (?stock=real).
|
||||
|
||||
Build-time only: reads the local recordgod dataset (dealgod Postgres `discogs_release`, the
|
||||
185k-vinyl set), curates ~200-400 sleeves, **parody-transforms titles/artists deterministically**
|
||||
(the no-real-trademarks law covers metadata), fetches cover images, bakes atlas sheets + a JSON
|
||||
index, publishes atlases to the depot. NOTHING queries the dataset at runtime — C reads the built
|
||||
pack (index + atlases) through the depot / ?localdepot=1.
|
||||
Build-time only (nothing queries the datasets at runtime): read a local GODVERSE dataset
|
||||
(dealgod Postgres), curate items, **parody-transform metadata deterministically** (no-real-
|
||||
trademarks law covers titles/artists/authors/brands), fetch cover images, bake atlas WebPs + a
|
||||
JSON index to C's schema (`stock_<kind>_index.json`), stage for ?localdepot / publish to the depot.
|
||||
|
||||
python3 build_stock_pack.py --from-db --count 300 # real pack (needs GODVERSE_DSN + net for covers)
|
||||
python3 build_stock_pack.py --sample 24 # synthetic pipeline test (no DB, solid-colour sleeves)
|
||||
python3 build_stock_pack.py --publish # push built atlases + index to the depot
|
||||
Generalized over KINDS (round-8 E1): `record` (square sleeves), `book` (TALL-thin spines),
|
||||
`toy` (square boxes). C reads `uv`, not `cell`, so each kind just bakes its native cell aspect;
|
||||
the index carries cell_w/cell_h informationally.
|
||||
|
||||
Index schema (proposed to Lane C in LANE_E_NOTES — adjust `INDEX_FIELDS` to their contract):
|
||||
{ "version":1, "atlas_px":2048, "cell":256, "items":[
|
||||
{"id","title","artist","price","price_band","atlas","uv":[u0,v0,u1,v1]} ] }
|
||||
python3 build_stock_pack.py --kind record --from-db --count 300 # real (needs GODVERSE_DSN)
|
||||
python3 build_stock_pack.py --kind book --sample 32 # synthetic pipeline test
|
||||
python3 build_stock_pack.py --kind toy --sample 32
|
||||
python3 build_stock_pack.py --publish # stage all built packs for ?localdepot
|
||||
|
||||
Parody law: real (title,artist) → deterministic seed → parody title/artist recombined from the
|
||||
word banks below. The output shares NO tokens with the source, so zero real-trademark leakage,
|
||||
yet reads as period-plausible AU record-shop stock. Same source row → same parody, every build.
|
||||
Index: { version, kind, atlas_px, cell_w, cell_h, atlases[], count,
|
||||
items:[ {id,title,artist,price,price_band,atlas,uv:[u0,v0,u1,v1]} ] }
|
||||
(For book/toy, `artist` carries author/brand — C's field is source-agnostic per LANE_C_NOTES.)
|
||||
"""
|
||||
import json, os, sys, struct, hashlib, urllib.request
|
||||
import json, os, sys, hashlib, urllib.request
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT = os.path.join(ROOT, "pipeline", "_stockpack")
|
||||
STAGE = os.path.join(ROOT, "web", "assets", "models") # ?localdepot mirror
|
||||
STAGE = os.path.join(ROOT, "web", "assets", "models")
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
INDEX_FIELDS = ("id", "title", "artist", "price", "price_band", "atlas", "uv")
|
||||
|
||||
# ── parody word banks (period-plausible AU record stock; no real acts) ──────────────────
|
||||
ADJ = ["Midnight", "Electric", "Velvet", "Golden", "Crimson", "Neon", "Sunburnt", "Hazy",
|
||||
"Concrete", "Paper", "Silver", "Restless", "Quiet", "Broken", "Endless", "Salt"]
|
||||
NOUN = ["Sessions", "Highway", "Ballroom", "Suburbs", "Tide", "Radio", "Static", "Harvest",
|
||||
"Verandah", "Jetty", "Milk Bar", "Cassette", "Reunion", "Departure", "Anthems", "Blue"]
|
||||
BAND_A = ["The", "Big", "Lonesome", "Riptide", "Powderfinger-ish", "Sunset", "Regional", "Mullet"]
|
||||
BAND_B = ["Galahs", "Dropbears", "Hills Hoists", "Milk Crates", "Southerlies", "Panelvans",
|
||||
"Rissoles", "Servos", "Bottlebrush", "Sandgropers", "Cockatoos", "Utes"]
|
||||
FIRST = ["Bazza", "Shazza", "Kylie", "Darryl", "Noeline", "Trev", "Merv", "Dazza", "Coral", "Kev"]
|
||||
LAST = ["Studebaker", "Fitzroy", "Nullarbor", "Grong", "Wollemi", "Barcoo", "Tuckerbox", "Yakka"]
|
||||
# ── parody word banks per kind (period-AU-plausible; no real acts/authors/brands) ──────────
|
||||
BANKS = {
|
||||
"record": dict(
|
||||
adj=["Midnight", "Electric", "Velvet", "Golden", "Crimson", "Neon", "Sunburnt", "Hazy",
|
||||
"Concrete", "Paper", "Silver", "Restless", "Quiet", "Broken", "Endless", "Salt"],
|
||||
noun=["Sessions", "Highway", "Ballroom", "Suburbs", "Tide", "Radio", "Static", "Harvest",
|
||||
"Verandah", "Jetty", "Milk Bar", "Cassette", "Reunion", "Departure", "Anthems", "Blue"],
|
||||
band_a=["The", "Big", "Lonesome", "Riptide", "Sunset", "Regional", "Mullet", "Neon"],
|
||||
band_b=["Galahs", "Dropbears", "Hills Hoists", "Milk Crates", "Southerlies", "Panelvans",
|
||||
"Rissoles", "Servos", "Bottlebrush", "Sandgropers", "Cockatoos", "Utes"]),
|
||||
"book": dict(
|
||||
adj=["The Lost", "A Quiet", "The Last", "Beneath the", "The Long", "Shadows of the",
|
||||
"The Secret", "Return to", "The Hollow", "Whispers of the", "The Broken", "Under the"],
|
||||
noun=["Verandah", "Paddock", "Lighthouse", "Estuary", "Homestead", "Reef", "Gully",
|
||||
"Terminus", "Foreshore", "Escarpment", "Siding", "Inlet", "Ridge", "Jetty"],
|
||||
band_a=["J.", "M.", "R.", "E.", "P.", "K.", "D.", "A.", "T.", "C."],
|
||||
band_b=["Halloran", "Fenwick", "Marchetti", "Delacroix", "Ngata", "Ashcombe", "Voss",
|
||||
"Pemberton", "Corrigan", "Blaxland", "Underwood", "Rourke"]),
|
||||
"toy": dict(
|
||||
adj=["Turbo", "Mega", "Cosmic", "Wonder", "Rescue", "Galaxy", "Jungle", "Speedy",
|
||||
"Sparkle", "Thunder", "Micro", "Super", "Dino", "Robo", "Magic", "Aqua"],
|
||||
noun=["Racers", "Rangers", "Buddies", "Cruisers", "Squad", "Friends", "Blasters", "Ponies",
|
||||
"Bots", "Beasts", "Patrol", "Tots", "Voyagers", "Crew", "Pals", "Force"],
|
||||
band_a=["Bushland", "Coral Coast", "Redgum", "Southern Cross", "Boomerang", "Kanga",
|
||||
"Wattle", "Billabong", "Outback", "Reef City"],
|
||||
band_b=["Toys", "Playsystems", "Games", "Fun Co.", "Novelties", "Workshop", "Mfg.", "Play Lab"]),
|
||||
}
|
||||
CELL = {"record": (256, 256), "book": (96, 256), "toy": (256, 256)} # book = tall-thin spine
|
||||
GRID = {"record": 8, "book": 16, "toy": 8} # columns per atlas row
|
||||
|
||||
|
||||
def _seed(*parts):
|
||||
return int(hashlib.sha256("|".join(str(p) for p in parts).encode()).hexdigest()[:12], 16)
|
||||
def _seed(*p):
|
||||
return int(hashlib.sha256("|".join(str(x) for x in p).encode()).hexdigest()[:12], 16)
|
||||
|
||||
|
||||
def parody(title, artist, is_group=None):
|
||||
"""Deterministic parody of a real (title, artist). Shares no source tokens."""
|
||||
s = _seed(title or "", artist or "")
|
||||
ptitle = f"{ADJ[s % len(ADJ)]} {NOUN[(s // 7) % len(NOUN)]}"
|
||||
if (s // 13) % 5 == 0:
|
||||
def parody(kind, title, artist):
|
||||
"""Deterministic parody; output shares no source token → zero real-trademark leakage."""
|
||||
b = BANKS[kind]; s = _seed(kind, title or "", artist or "")
|
||||
ptitle = f"{b['adj'][s % len(b['adj'])]} {b['noun'][(s // 7) % len(b['noun'])]}"
|
||||
if kind == "record" and (s // 13) % 5 == 0:
|
||||
ptitle += f", Vol. {1 + (s // 3) % 3}"
|
||||
# solo vs group name, deterministically
|
||||
if is_group is None:
|
||||
is_group = (s % 2 == 0)
|
||||
if is_group:
|
||||
partist = f"{BAND_A[(s // 5) % len(BAND_A)]} {BAND_B[(s // 11) % len(BAND_B)]}".strip()
|
||||
if kind == "book": # "The Lost Verandah" reads as a title already
|
||||
partist = f"{b['band_a'][(s // 17) % len(b['band_a'])]} {b['band_b'][(s // 19) % len(b['band_b'])]}"
|
||||
elif (s % 2 == 0) or kind == "toy":
|
||||
partist = f"{b['band_a'][(s // 5) % len(b['band_a'])]} {b['band_b'][(s // 11) % len(b['band_b'])]}".strip()
|
||||
else:
|
||||
partist = f"{FIRST[(s // 17) % len(FIRST)]} {LAST[(s // 19) % len(LAST)]}"
|
||||
partist = f"{b['band_a'][(s // 17) % len(b['band_a'])]} {b['band_b'][(s // 19) % len(b['band_b'])]}"
|
||||
return ptitle, partist
|
||||
|
||||
|
||||
def price_band(price):
|
||||
if price is None:
|
||||
return "bargain"
|
||||
if price < 8:
|
||||
return "bargain"
|
||||
if price < 25:
|
||||
return "standard"
|
||||
if price < 60:
|
||||
return "collector"
|
||||
return "grail"
|
||||
def price_band(kind, price):
|
||||
p = price if price is not None else 4
|
||||
if kind == "book":
|
||||
return "bargain" if p < 5 else "standard" if p < 15 else "collector" if p < 40 else "grail"
|
||||
if kind == "toy":
|
||||
return "bargain" if p < 6 else "standard" if p < 20 else "collector" if p < 50 else "grail"
|
||||
return "bargain" if p < 8 else "standard" if p < 25 else "collector" if p < 60 else "grail"
|
||||
|
||||
|
||||
# ── data sources ────────────────────────────────────────────────────────────────────────
|
||||
def rows_from_db(count):
|
||||
"""Curate `count` sleeves from dealgod's discogs_release (needs GODVERSE_DSN + psycopg2).
|
||||
Picks rows WITH a cover image + non-empty artist/title, varied by decade for shelf variety."""
|
||||
import psycopg2 # noqa: build-time only
|
||||
DB_QUERY = { # (table, price-source) per kind; images column holds cover URLs (discogs_parse pattern)
|
||||
"record": "discogs_release",
|
||||
"book": "book_edition", # bookgod / OpenLibrary editions (title, author, cover)
|
||||
"toy": "toy_listing", # toygod
|
||||
}
|
||||
|
||||
|
||||
def rows_from_db(kind, count):
|
||||
import psycopg2
|
||||
dsn = os.environ.get("GODVERSE_DSN") or os.environ.get("DB_DSN")
|
||||
if not dsn:
|
||||
raise SystemExit("GODVERSE_DSN not set — the DB dsn (creds) is required for --from-db. "
|
||||
"Ask John / read from dealgod's env. (Nothing queries the DB at runtime.)")
|
||||
con = psycopg2.connect(dsn)
|
||||
cur = con.cursor()
|
||||
cur.execute("""
|
||||
SELECT release_id, artist, title, year,
|
||||
(SELECT current_price FROM prices p WHERE p.release_id=dr.release_id
|
||||
ORDER BY current_price LIMIT 1) AS price,
|
||||
images
|
||||
FROM discogs_release dr
|
||||
WHERE artist <> '' AND title <> '' AND images IS NOT NULL AND images <> '[]'
|
||||
ORDER BY md5(release_id::text) -- deterministic pseudo-random sample
|
||||
LIMIT %s""", (count,))
|
||||
raise SystemExit("GODVERSE_DSN not set — the dealgod Postgres dsn is required for --from-db "
|
||||
"(nothing queries it at runtime). Ask John. Postgres is up on :5432.")
|
||||
con = psycopg2.connect(dsn); cur = con.cursor()
|
||||
tbl = DB_QUERY[kind]
|
||||
cur.execute(f"""SELECT id, artist, title, price, images FROM {tbl}
|
||||
WHERE artist<>'' AND title<>'' AND images IS NOT NULL AND images<>'[]'
|
||||
ORDER BY md5(id::text) LIMIT %s""", (count,))
|
||||
out = []
|
||||
for rid, artist, title, year, price, images in cur.fetchall():
|
||||
for _id, artist, title, price, images in cur.fetchall():
|
||||
imgs = images if isinstance(images, list) else json.loads(images or "[]")
|
||||
url = imgs[0].get("uri") if imgs and isinstance(imgs[0], dict) else (imgs[0] if imgs else None)
|
||||
if url:
|
||||
out.append({"src_id": rid, "artist": artist, "title": title, "price": price, "img": url})
|
||||
out.append({"artist": artist, "title": title, "price": price, "img": url})
|
||||
con.close()
|
||||
return out
|
||||
|
||||
|
||||
def rows_sample(n):
|
||||
"""Synthetic rows for a no-DB pipeline test — solid-colour 'sleeves', fake source metadata."""
|
||||
def rows_sample(kind, n):
|
||||
"""Synthetic rows (solid-colour cells at the kind's aspect) — proves the pipeline with no DB."""
|
||||
from PIL import Image, ImageDraw
|
||||
rows = []
|
||||
cw, ch = CELL[kind]; rows = []
|
||||
os.makedirs(os.path.join(OUT, "_imgs"), exist_ok=True)
|
||||
for i in range(n):
|
||||
s = _seed("sample", i)
|
||||
s = _seed(kind, "sample", i)
|
||||
col = ((s >> 0) & 255, (s >> 8) & 255, (s >> 16) & 255)
|
||||
im = Image.new("RGB", (256, 256), col)
|
||||
d = ImageDraw.Draw(im)
|
||||
d.rectangle([16, 16, 240, 240], outline=(255, 255, 255), width=3)
|
||||
d.ellipse([88, 88, 168, 168], fill=(20, 20, 20)) # a 'record' centre
|
||||
p = os.path.join(OUT, "_imgs", f"s{i}.png")
|
||||
im.save(p)
|
||||
rows.append({"src_id": f"sample-{i}", "artist": f"Real Artist {i}",
|
||||
"title": f"Real Album {i}", "price": 5 + (s % 80), "img": p})
|
||||
im = Image.new("RGB", (cw, ch), col); d = ImageDraw.Draw(im)
|
||||
d.rectangle([2, 2, cw - 3, ch - 3], outline=(255, 255, 255), width=2)
|
||||
if kind == "record":
|
||||
d.ellipse([cw * .34, ch * .34, cw * .66, ch * .66], fill=(20, 20, 20))
|
||||
elif kind == "book":
|
||||
d.line([(cw // 2, 8), (cw // 2, ch - 8)], fill=(255, 255, 255), width=1) # spine crease
|
||||
p = os.path.join(OUT, "_imgs", f"{kind}{i}.png"); im.save(p)
|
||||
rows.append({"artist": f"Src {kind} {i}", "title": f"Src Title {i}",
|
||||
"price": 3 + (s % 55), "img": p})
|
||||
return rows
|
||||
|
||||
|
||||
# ── atlas builder ───────────────────────────────────────────────────────────────────────
|
||||
def build_pack(rows, cell=256, per_atlas_side=8):
|
||||
def build_pack(kind, rows):
|
||||
from PIL import Image
|
||||
per = per_atlas_side * per_atlas_side
|
||||
items, atlases = [], []
|
||||
cw, ch = CELL[kind]; cols = GRID[kind]; rowspp = max(1, 2048 // ch)
|
||||
per = cols * rowspp; items, atlases = [], []
|
||||
for ai in range((len(rows) + per - 1) // per):
|
||||
chunk = rows[ai * per:(ai + 1) * per]
|
||||
sheet = Image.new("RGB", (cell * per_atlas_side, cell * per_atlas_side), (30, 30, 34))
|
||||
aname = f"stock_record_atlas_{ai:02d}.webp"
|
||||
W, H = cols * cw, rowspp * ch
|
||||
sheet = Image.new("RGB", (W, H), (28, 28, 32))
|
||||
aname = f"stock_{kind}_atlas_{ai:02d}.webp"
|
||||
for j, r in enumerate(chunk):
|
||||
gx, gy = j % per_atlas_side, j // per_atlas_side
|
||||
gx, gy = j % cols, j // cols
|
||||
try:
|
||||
cov = _load_img(r["img"]).resize((cell, cell))
|
||||
cov = _load_img(r["img"]).resize((cw, ch))
|
||||
except Exception:
|
||||
continue
|
||||
sheet.paste(cov, (gx * cell, gy * cell))
|
||||
ptitle, partist = parody(r["title"], r["artist"])
|
||||
u0, v0 = gx / per_atlas_side, gy / per_atlas_side
|
||||
item = {"id": f"rec_{len(items):04d}", "title": ptitle, "artist": partist,
|
||||
"price": int(r["price"] or 5), "price_band": price_band(r["price"]),
|
||||
sheet.paste(cov, (gx * cw, gy * ch))
|
||||
ptitle, partist = parody(kind, r["title"], r["artist"])
|
||||
u0, v0 = gx * cw / W, gy * ch / H
|
||||
item = {"id": f"{kind[:3]}_{len(items):04d}", "title": ptitle, "artist": partist,
|
||||
"price": int(r["price"] or 4), "price_band": price_band(kind, r["price"]),
|
||||
"atlas": aname, "uv": [round(u0, 4), round(v0, 4),
|
||||
round(u0 + 1 / per_atlas_side, 4), round(v0 + 1 / per_atlas_side, 4)]}
|
||||
round(u0 + cw / W, 4), round(v0 + ch / H, 4)]}
|
||||
items.append({k: item[k] for k in INDEX_FIELDS})
|
||||
sheet.save(os.path.join(OUT, aname), "WEBP", quality=88)
|
||||
atlases.append(aname)
|
||||
index = {"version": 1, "atlas_px": cell * per_atlas_side, "cell": cell,
|
||||
index = {"version": 1, "kind": kind, "atlas_px": 2048, "cell_w": cw, "cell_h": ch,
|
||||
"count": len(items), "atlases": atlases, "items": items}
|
||||
json.dump(index, open(os.path.join(OUT, "stock_record_index.json"), "w"), indent=1)
|
||||
json.dump(index, open(os.path.join(OUT, f"stock_{kind}_index.json"), "w"), indent=1)
|
||||
return index
|
||||
|
||||
|
||||
@ -165,26 +180,25 @@ def _load_img(src):
|
||||
def main():
|
||||
a = sys.argv
|
||||
if "--publish" in a:
|
||||
# atlases + index publish through the same depot path as GLBs (documented; run when built)
|
||||
import shutil
|
||||
os.makedirs(STAGE, exist_ok=True); n = 0
|
||||
for f in os.listdir(OUT):
|
||||
if f.endswith(".webp") or f == "stock_record_index.json":
|
||||
os.makedirs(STAGE, exist_ok=True)
|
||||
import shutil
|
||||
shutil.copy(os.path.join(OUT, f), os.path.join(STAGE, f))
|
||||
print(f"staged pack → {STAGE} (for ?localdepot). Depot push: adapt publish.py to POST "
|
||||
f"{OUT}/*.webp + index (same /api/upload path).")
|
||||
if f.startswith("stock_") and (f.endswith(".webp") or f.endswith("_index.json")):
|
||||
shutil.copy(os.path.join(OUT, f), os.path.join(STAGE, f)); n += 1
|
||||
print(f"staged {n} pack file(s) → {STAGE} (?localdepot). Depot: publish.py-style POST of the "
|
||||
f".webp atlases + *_index.json via /api/upload.")
|
||||
return
|
||||
kind = a[a.index("--kind") + 1] if "--kind" in a else "record"
|
||||
if "--sample" in a:
|
||||
n = int(a[a.index("--sample") + 1])
|
||||
rows = rows_sample(n)
|
||||
rows = rows_sample(kind, int(a[a.index("--sample") + 1]))
|
||||
elif "--from-db" in a:
|
||||
count = int(a[a.index("--count") + 1]) if "--count" in a else 300
|
||||
rows = rows_from_db(count)
|
||||
rows = rows_from_db(kind, int(a[a.index("--count") + 1]) if "--count" in a else 250)
|
||||
else:
|
||||
print(__doc__); return
|
||||
idx = build_pack(rows)
|
||||
print(f"built pack: {idx['count']} sleeves, {len(idx['atlases'])} atlas(es) → {OUT}")
|
||||
print(f" sample items: {[(i['title'], i['artist'], i['price_band']) for i in idx['items'][:3]]}")
|
||||
idx = build_pack(kind, rows)
|
||||
print(f"built {kind} pack: {idx['count']} items, {len(idx['atlases'])} atlas(es), "
|
||||
f"cell {idx['cell_w']}x{idx['cell_h']} → {OUT}")
|
||||
print(f" sample: {[(i['title'], i['artist'], i['price_band']) for i in idx['items'][:3]]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
461
pipeline/packs/stock_book_index.json
Normal file
461
pipeline/packs/stock_book_index.json
Normal file
@ -0,0 +1,461 @@
|
||||
{
|
||||
"version": 1,
|
||||
"kind": "book",
|
||||
"atlas_px": 2048,
|
||||
"cell_w": 96,
|
||||
"cell_h": 256,
|
||||
"count": 32,
|
||||
"atlases": [
|
||||
"stock_book_atlas_00.webp"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "boo_0000",
|
||||
"title": "A Quiet Reef",
|
||||
"artist": "K. Underwood",
|
||||
"price": 18,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0625,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0001",
|
||||
"title": "Beneath the Terminus",
|
||||
"artist": "K. Ngata",
|
||||
"price": 17,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.0625,
|
||||
0.0,
|
||||
0.125,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0002",
|
||||
"title": "Return to Homestead",
|
||||
"artist": "A. Rourke",
|
||||
"price": 28,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.125,
|
||||
0.0,
|
||||
0.1875,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0003",
|
||||
"title": "The Secret Terminus",
|
||||
"artist": "P. Rourke",
|
||||
"price": 38,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.1875,
|
||||
0.0,
|
||||
0.25,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0004",
|
||||
"title": "The Long Lighthouse",
|
||||
"artist": "C. Fenwick",
|
||||
"price": 24,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.25,
|
||||
0.0,
|
||||
0.3125,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0005",
|
||||
"title": "The Secret Escarpment",
|
||||
"artist": "A. Pemberton",
|
||||
"price": 18,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.3125,
|
||||
0.0,
|
||||
0.375,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0006",
|
||||
"title": "Beneath the Gully",
|
||||
"artist": "P. Ashcombe",
|
||||
"price": 16,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.375,
|
||||
0.0,
|
||||
0.4375,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0007",
|
||||
"title": "Under the Paddock",
|
||||
"artist": "E. Delacroix",
|
||||
"price": 15,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.4375,
|
||||
0.0,
|
||||
0.5,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0008",
|
||||
"title": "The Secret Escarpment",
|
||||
"artist": "J. Marchetti",
|
||||
"price": 31,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.5,
|
||||
0.0,
|
||||
0.5625,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0009",
|
||||
"title": "The Lost Verandah",
|
||||
"artist": "R. Delacroix",
|
||||
"price": 38,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.5625,
|
||||
0.0,
|
||||
0.625,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0010",
|
||||
"title": "A Quiet Estuary",
|
||||
"artist": "E. Rourke",
|
||||
"price": 17,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.625,
|
||||
0.0,
|
||||
0.6875,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0011",
|
||||
"title": "Shadows of the Foreshore",
|
||||
"artist": "R. Ngata",
|
||||
"price": 17,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.6875,
|
||||
0.0,
|
||||
0.75,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0012",
|
||||
"title": "The Last Lighthouse",
|
||||
"artist": "M. Ashcombe",
|
||||
"price": 22,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.75,
|
||||
0.0,
|
||||
0.8125,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0013",
|
||||
"title": "The Secret Homestead",
|
||||
"artist": "C. Rourke",
|
||||
"price": 49,
|
||||
"price_band": "grail",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.8125,
|
||||
0.0,
|
||||
0.875,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0014",
|
||||
"title": "The Lost Verandah",
|
||||
"artist": "C. Ngata",
|
||||
"price": 49,
|
||||
"price_band": "grail",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.875,
|
||||
0.0,
|
||||
0.9375,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0015",
|
||||
"title": "Shadows of the Terminus",
|
||||
"artist": "D. Rourke",
|
||||
"price": 16,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.9375,
|
||||
0.0,
|
||||
1.0,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0016",
|
||||
"title": "Whispers of the Gully",
|
||||
"artist": "A. Rourke",
|
||||
"price": 6,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.0,
|
||||
0.125,
|
||||
0.0625,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0017",
|
||||
"title": "Beneath the Inlet",
|
||||
"artist": "C. Blaxland",
|
||||
"price": 28,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.0625,
|
||||
0.125,
|
||||
0.125,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0018",
|
||||
"title": "A Quiet Siding",
|
||||
"artist": "M. Underwood",
|
||||
"price": 51,
|
||||
"price_band": "grail",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.125,
|
||||
0.125,
|
||||
0.1875,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0019",
|
||||
"title": "Whispers of the Homestead",
|
||||
"artist": "R. Marchetti",
|
||||
"price": 51,
|
||||
"price_band": "grail",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.1875,
|
||||
0.125,
|
||||
0.25,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0020",
|
||||
"title": "A Quiet Gully",
|
||||
"artist": "T. Ashcombe",
|
||||
"price": 54,
|
||||
"price_band": "grail",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.25,
|
||||
0.125,
|
||||
0.3125,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0021",
|
||||
"title": "Under the Reef",
|
||||
"artist": "D. Marchetti",
|
||||
"price": 25,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.3125,
|
||||
0.125,
|
||||
0.375,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0022",
|
||||
"title": "Beneath the Estuary",
|
||||
"artist": "D. Ngata",
|
||||
"price": 20,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.375,
|
||||
0.125,
|
||||
0.4375,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0023",
|
||||
"title": "Whispers of the Foreshore",
|
||||
"artist": "P. Blaxland",
|
||||
"price": 51,
|
||||
"price_band": "grail",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.4375,
|
||||
0.125,
|
||||
0.5,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0024",
|
||||
"title": "The Last Ridge",
|
||||
"artist": "E. Marchetti",
|
||||
"price": 16,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.5,
|
||||
0.125,
|
||||
0.5625,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0025",
|
||||
"title": "Whispers of the Jetty",
|
||||
"artist": "J. Corrigan",
|
||||
"price": 40,
|
||||
"price_band": "grail",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.5625,
|
||||
0.125,
|
||||
0.625,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0026",
|
||||
"title": "Shadows of the Verandah",
|
||||
"artist": "J. Voss",
|
||||
"price": 7,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.625,
|
||||
0.125,
|
||||
0.6875,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0027",
|
||||
"title": "The Lost Jetty",
|
||||
"artist": "J. Blaxland",
|
||||
"price": 4,
|
||||
"price_band": "bargain",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.6875,
|
||||
0.125,
|
||||
0.75,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0028",
|
||||
"title": "Whispers of the Escarpment",
|
||||
"artist": "D. Delacroix",
|
||||
"price": 29,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.75,
|
||||
0.125,
|
||||
0.8125,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0029",
|
||||
"title": "The Last Jetty",
|
||||
"artist": "J. Ngata",
|
||||
"price": 29,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.8125,
|
||||
0.125,
|
||||
0.875,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0030",
|
||||
"title": "The Lost Verandah",
|
||||
"artist": "D. Ngata",
|
||||
"price": 32,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.875,
|
||||
0.125,
|
||||
0.9375,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boo_0031",
|
||||
"title": "Beneath the Lighthouse",
|
||||
"artist": "C. Fenwick",
|
||||
"price": 35,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_book_atlas_00.webp",
|
||||
"uv": [
|
||||
0.9375,
|
||||
0.125,
|
||||
1.0,
|
||||
0.25
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
461
pipeline/packs/stock_record_index.json
Normal file
461
pipeline/packs/stock_record_index.json
Normal file
@ -0,0 +1,461 @@
|
||||
{
|
||||
"version": 1,
|
||||
"kind": "record",
|
||||
"atlas_px": 2048,
|
||||
"cell_w": 256,
|
||||
"cell_h": 256,
|
||||
"count": 32,
|
||||
"atlases": [
|
||||
"stock_record_atlas_00.webp"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "rec_0000",
|
||||
"title": "Paper Suburbs",
|
||||
"artist": "Lonesome Servos",
|
||||
"price": 15,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.125,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0001",
|
||||
"title": "Silver Suburbs, Vol. 2",
|
||||
"artist": "Riptide Rissoles",
|
||||
"price": 6,
|
||||
"price_band": "bargain",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.125,
|
||||
0.0,
|
||||
0.25,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0002",
|
||||
"title": "Velvet Ballroom",
|
||||
"artist": "Riptide Servos",
|
||||
"price": 7,
|
||||
"price_band": "bargain",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.25,
|
||||
0.0,
|
||||
0.375,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0003",
|
||||
"title": "Concrete Verandah",
|
||||
"artist": "The Servos",
|
||||
"price": 48,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.375,
|
||||
0.0,
|
||||
0.5,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0004",
|
||||
"title": "Broken Highway",
|
||||
"artist": "Regional Galahs",
|
||||
"price": 44,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.5,
|
||||
0.0,
|
||||
0.625,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0005",
|
||||
"title": "Neon Jetty",
|
||||
"artist": "Mullet Servos",
|
||||
"price": 17,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.625,
|
||||
0.0,
|
||||
0.75,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0006",
|
||||
"title": "Hazy Highway",
|
||||
"artist": "Big Dropbears",
|
||||
"price": 50,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.75,
|
||||
0.0,
|
||||
0.875,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0007",
|
||||
"title": "Electric Departure",
|
||||
"artist": "Lonesome Dropbears",
|
||||
"price": 30,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.875,
|
||||
0.0,
|
||||
1.0,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0008",
|
||||
"title": "Neon Radio",
|
||||
"artist": "Regional Servos",
|
||||
"price": 49,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.0,
|
||||
0.125,
|
||||
0.125,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0009",
|
||||
"title": "Midnight Static",
|
||||
"artist": "Big Southerlies",
|
||||
"price": 51,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.125,
|
||||
0.125,
|
||||
0.25,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0010",
|
||||
"title": "Electric Tide",
|
||||
"artist": "Sunset Hills Hoists",
|
||||
"price": 3,
|
||||
"price_band": "bargain",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.25,
|
||||
0.125,
|
||||
0.375,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0011",
|
||||
"title": "Hazy Radio",
|
||||
"artist": "Riptide Southerlies",
|
||||
"price": 8,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.375,
|
||||
0.125,
|
||||
0.5,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0012",
|
||||
"title": "Broken Verandah, Vol. 1",
|
||||
"artist": "The Galahs",
|
||||
"price": 41,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.5,
|
||||
0.125,
|
||||
0.625,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0013",
|
||||
"title": "Midnight Tide",
|
||||
"artist": "Big Hills Hoists",
|
||||
"price": 51,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.625,
|
||||
0.125,
|
||||
0.75,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0014",
|
||||
"title": "Silver Highway",
|
||||
"artist": "Riptide Cockatoos",
|
||||
"price": 4,
|
||||
"price_band": "bargain",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.75,
|
||||
0.125,
|
||||
0.875,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0015",
|
||||
"title": "Velvet Anthems, Vol. 3",
|
||||
"artist": "Regional Cockatoos",
|
||||
"price": 37,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.875,
|
||||
0.125,
|
||||
1.0,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0016",
|
||||
"title": "Paper Milk Bar",
|
||||
"artist": "Riptide Utes",
|
||||
"price": 35,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.0,
|
||||
0.25,
|
||||
0.125,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0017",
|
||||
"title": "Silver Suburbs, Vol. 2",
|
||||
"artist": "Lonesome Hills Hoists",
|
||||
"price": 16,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.125,
|
||||
0.25,
|
||||
0.25,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0018",
|
||||
"title": "Broken Tide",
|
||||
"artist": "Lonesome Bottlebrush",
|
||||
"price": 31,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.25,
|
||||
0.25,
|
||||
0.375,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0019",
|
||||
"title": "Midnight Departure",
|
||||
"artist": "The Dropbears",
|
||||
"price": 40,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.375,
|
||||
0.25,
|
||||
0.5,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0020",
|
||||
"title": "Paper Radio, Vol. 3",
|
||||
"artist": "The Sandgropers",
|
||||
"price": 22,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.5,
|
||||
0.25,
|
||||
0.625,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0021",
|
||||
"title": "Silver Reunion",
|
||||
"artist": "The Galahs",
|
||||
"price": 16,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.625,
|
||||
0.25,
|
||||
0.75,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0022",
|
||||
"title": "Hazy Harvest",
|
||||
"artist": "Regional Cockatoos",
|
||||
"price": 37,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.75,
|
||||
0.25,
|
||||
0.875,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0023",
|
||||
"title": "Quiet Departure, Vol. 1",
|
||||
"artist": "Neon Southerlies",
|
||||
"price": 4,
|
||||
"price_band": "bargain",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.875,
|
||||
0.25,
|
||||
1.0,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0024",
|
||||
"title": "Midnight Ballroom, Vol. 1",
|
||||
"artist": "Big Galahs",
|
||||
"price": 29,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.0,
|
||||
0.375,
|
||||
0.125,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0025",
|
||||
"title": "Electric Jetty",
|
||||
"artist": "Big Servos",
|
||||
"price": 46,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.125,
|
||||
0.375,
|
||||
0.25,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0026",
|
||||
"title": "Salt Blue",
|
||||
"artist": "Lonesome Milk Crates",
|
||||
"price": 23,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.25,
|
||||
0.375,
|
||||
0.375,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0027",
|
||||
"title": "Midnight Jetty",
|
||||
"artist": "Big Panelvans",
|
||||
"price": 46,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.375,
|
||||
0.375,
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0028",
|
||||
"title": "Golden Jetty, Vol. 1",
|
||||
"artist": "The Servos",
|
||||
"price": 24,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.5,
|
||||
0.375,
|
||||
0.625,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0029",
|
||||
"title": "Electric Ballroom, Vol. 3",
|
||||
"artist": "Mullet Bottlebrush",
|
||||
"price": 4,
|
||||
"price_band": "bargain",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.625,
|
||||
0.375,
|
||||
0.75,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0030",
|
||||
"title": "Restless Suburbs",
|
||||
"artist": "Sunset Utes",
|
||||
"price": 44,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.75,
|
||||
0.375,
|
||||
0.875,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rec_0031",
|
||||
"title": "Golden Sessions",
|
||||
"artist": "Mullet Southerlies",
|
||||
"price": 3,
|
||||
"price_band": "bargain",
|
||||
"atlas": "stock_record_atlas_00.webp",
|
||||
"uv": [
|
||||
0.875,
|
||||
0.375,
|
||||
1.0,
|
||||
0.5
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
461
pipeline/packs/stock_toy_index.json
Normal file
461
pipeline/packs/stock_toy_index.json
Normal file
@ -0,0 +1,461 @@
|
||||
{
|
||||
"version": 1,
|
||||
"kind": "toy",
|
||||
"atlas_px": 2048,
|
||||
"cell_w": 256,
|
||||
"cell_h": 256,
|
||||
"count": 32,
|
||||
"atlases": [
|
||||
"stock_toy_atlas_00.webp"
|
||||
],
|
||||
"items": [
|
||||
{
|
||||
"id": "toy_0000",
|
||||
"title": "Speedy Patrol",
|
||||
"artist": "Redgum Mfg.",
|
||||
"price": 14,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.125,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0001",
|
||||
"title": "Rescue Voyagers",
|
||||
"artist": "Billabong Games",
|
||||
"price": 45,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.125,
|
||||
0.0,
|
||||
0.25,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0002",
|
||||
"title": "Jungle Racers",
|
||||
"artist": "Coral Coast Playsystems",
|
||||
"price": 55,
|
||||
"price_band": "grail",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.25,
|
||||
0.0,
|
||||
0.375,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0003",
|
||||
"title": "Turbo Tots",
|
||||
"artist": "Southern Cross Toys",
|
||||
"price": 35,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.375,
|
||||
0.0,
|
||||
0.5,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0004",
|
||||
"title": "Jungle Racers",
|
||||
"artist": "Kanga Fun Co.",
|
||||
"price": 28,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.5,
|
||||
0.0,
|
||||
0.625,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0005",
|
||||
"title": "Aqua Buddies",
|
||||
"artist": "Kanga Play Lab",
|
||||
"price": 36,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.625,
|
||||
0.0,
|
||||
0.75,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0006",
|
||||
"title": "Galaxy Friends",
|
||||
"artist": "Bushland Toys",
|
||||
"price": 37,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.75,
|
||||
0.0,
|
||||
0.875,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0007",
|
||||
"title": "Micro Force",
|
||||
"artist": "Redgum Playsystems",
|
||||
"price": 27,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.875,
|
||||
0.0,
|
||||
1.0,
|
||||
0.125
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0008",
|
||||
"title": "Galaxy Friends",
|
||||
"artist": "Kanga Games",
|
||||
"price": 9,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.0,
|
||||
0.125,
|
||||
0.125,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0009",
|
||||
"title": "Speedy Ponies",
|
||||
"artist": "Kanga Games",
|
||||
"price": 18,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.125,
|
||||
0.125,
|
||||
0.25,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0010",
|
||||
"title": "Thunder Patrol",
|
||||
"artist": "Wattle Play Lab",
|
||||
"price": 41,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.25,
|
||||
0.125,
|
||||
0.375,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0011",
|
||||
"title": "Wonder Tots",
|
||||
"artist": "Reef City Fun Co.",
|
||||
"price": 28,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.375,
|
||||
0.125,
|
||||
0.5,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0012",
|
||||
"title": "Thunder Patrol",
|
||||
"artist": "Boomerang Novelties",
|
||||
"price": 54,
|
||||
"price_band": "grail",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.5,
|
||||
0.125,
|
||||
0.625,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0013",
|
||||
"title": "Aqua Crew",
|
||||
"artist": "Billabong Novelties",
|
||||
"price": 18,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.625,
|
||||
0.125,
|
||||
0.75,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0014",
|
||||
"title": "Jungle Pals",
|
||||
"artist": "Billabong Workshop",
|
||||
"price": 23,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.75,
|
||||
0.125,
|
||||
0.875,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0015",
|
||||
"title": "Aqua Squad",
|
||||
"artist": "Billabong Games",
|
||||
"price": 19,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.875,
|
||||
0.125,
|
||||
1.0,
|
||||
0.25
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0016",
|
||||
"title": "Magic Buddies",
|
||||
"artist": "Outback Toys",
|
||||
"price": 12,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.0,
|
||||
0.25,
|
||||
0.125,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0017",
|
||||
"title": "Aqua Blasters",
|
||||
"artist": "Boomerang Games",
|
||||
"price": 34,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.125,
|
||||
0.25,
|
||||
0.25,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0018",
|
||||
"title": "Aqua Blasters",
|
||||
"artist": "Kanga Workshop",
|
||||
"price": 40,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.25,
|
||||
0.25,
|
||||
0.375,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0019",
|
||||
"title": "Thunder Rangers",
|
||||
"artist": "Redgum Toys",
|
||||
"price": 28,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.375,
|
||||
0.25,
|
||||
0.5,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0020",
|
||||
"title": "Sparkle Patrol",
|
||||
"artist": "Boomerang Toys",
|
||||
"price": 27,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.5,
|
||||
0.25,
|
||||
0.625,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0021",
|
||||
"title": "Wonder Buddies",
|
||||
"artist": "Boomerang Mfg.",
|
||||
"price": 36,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.625,
|
||||
0.25,
|
||||
0.75,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0022",
|
||||
"title": "Magic Bots",
|
||||
"artist": "Bushland Toys",
|
||||
"price": 12,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.75,
|
||||
0.25,
|
||||
0.875,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0023",
|
||||
"title": "Jungle Voyagers",
|
||||
"artist": "Bushland Workshop",
|
||||
"price": 13,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.875,
|
||||
0.25,
|
||||
1.0,
|
||||
0.375
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0024",
|
||||
"title": "Thunder Cruisers",
|
||||
"artist": "Billabong Workshop",
|
||||
"price": 18,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.0,
|
||||
0.375,
|
||||
0.125,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0025",
|
||||
"title": "Magic Bots",
|
||||
"artist": "Wattle Games",
|
||||
"price": 39,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.125,
|
||||
0.375,
|
||||
0.25,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0026",
|
||||
"title": "Wonder Beasts",
|
||||
"artist": "Billabong Workshop",
|
||||
"price": 38,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.25,
|
||||
0.375,
|
||||
0.375,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0027",
|
||||
"title": "Robo Blasters",
|
||||
"artist": "Redgum Play Lab",
|
||||
"price": 12,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.375,
|
||||
0.375,
|
||||
0.5,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0028",
|
||||
"title": "Speedy Voyagers",
|
||||
"artist": "Reef City Workshop",
|
||||
"price": 46,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.5,
|
||||
0.375,
|
||||
0.625,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0029",
|
||||
"title": "Wonder Racers",
|
||||
"artist": "Southern Cross Mfg.",
|
||||
"price": 45,
|
||||
"price_band": "collector",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.625,
|
||||
0.375,
|
||||
0.75,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0030",
|
||||
"title": "Cosmic Racers",
|
||||
"artist": "Kanga Fun Co.",
|
||||
"price": 5,
|
||||
"price_band": "bargain",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.75,
|
||||
0.375,
|
||||
0.875,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "toy_0031",
|
||||
"title": "Turbo Beasts",
|
||||
"artist": "Reef City Games",
|
||||
"price": 17,
|
||||
"price_band": "standard",
|
||||
"atlas": "stock_toy_atlas_00.webp",
|
||||
"uv": [
|
||||
0.875,
|
||||
0.375,
|
||||
1.0,
|
||||
0.5
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -128,6 +128,14 @@ def main():
|
||||
if f.startswith("procity_") and f not in record:
|
||||
err(f"manifest GLB not in _published.json (provenance drift): {f}")
|
||||
|
||||
# pack-index QA (round-8 E2): a bad stock-pack bake fails the same gate as the manifest
|
||||
try:
|
||||
import validate_pack
|
||||
if validate_pack.main() != 0:
|
||||
err("stock-pack index validation failed (see pack-QA errors above)")
|
||||
except Exception as e:
|
||||
warn(f"pack-QA skipped: {e}")
|
||||
|
||||
print(f"manifest v{m.get('version')} — facades {len(facade)}, "
|
||||
f"skins {sum(len(v) if isinstance(v, list) else (len(v) if isinstance(v, dict) else 0) for v in sk.values())} groups, "
|
||||
f"GLBs {n_glb}")
|
||||
|
||||
82
pipeline/validate_pack.py
Normal file
82
pipeline/validate_pack.py
Normal file
@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate GODVERSE stock-pack indexes — round-8 E2 QA gate.
|
||||
|
||||
A bad bake (out-of-range UVs, empty titles, unknown price band, dangling atlas ref) must fail QA
|
||||
loudly instead of rendering blank/garbled sleeves in the dig. Validates every
|
||||
`pipeline/packs/stock_<kind>_index.json` (the committed pack contract; atlases live on the depot).
|
||||
Called by validate_manifest.py so it runs inside qa.sh gate 3.
|
||||
|
||||
python3 pipeline/validate_pack.py # exit 0 = all packs valid, 1 = a bad index
|
||||
"""
|
||||
import json, os, sys, glob
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PACKS = os.path.join(ROOT, "pipeline", "packs")
|
||||
KINDS = {"record", "book", "toy"}
|
||||
BANDS = {"bargain", "standard", "collector", "grail"}
|
||||
REQUIRED = ("id", "title", "artist", "price", "price_band", "atlas", "uv")
|
||||
|
||||
|
||||
def validate_index(path):
|
||||
errs = []
|
||||
try:
|
||||
idx = json.load(open(path))
|
||||
except Exception as e:
|
||||
return [f"{os.path.basename(path)}: does not parse: {e}"]
|
||||
name = os.path.basename(path)
|
||||
if idx.get("kind") not in KINDS:
|
||||
errs.append(f"{name}: bad kind {idx.get('kind')!r}")
|
||||
atlases = set(idx.get("atlases", []))
|
||||
if not atlases:
|
||||
errs.append(f"{name}: no atlases listed")
|
||||
items = idx.get("items", [])
|
||||
if not items:
|
||||
errs.append(f"{name}: no items")
|
||||
ids = set()
|
||||
for i, it in enumerate(items):
|
||||
miss = [f for f in REQUIRED if f not in it]
|
||||
if miss:
|
||||
errs.append(f"{name}[{i}]: missing {miss}"); continue
|
||||
if not str(it["title"]).strip() or not str(it["artist"]).strip():
|
||||
errs.append(f"{name}[{it['id']}]: empty title/artist")
|
||||
if it["id"] in ids:
|
||||
errs.append(f"{name}: duplicate id {it['id']}")
|
||||
ids.add(it["id"])
|
||||
if it["price_band"] not in BANDS:
|
||||
errs.append(f"{name}[{it['id']}]: bad price_band {it['price_band']!r}")
|
||||
if not isinstance(it["price"], (int, float)) or it["price"] < 0:
|
||||
errs.append(f"{name}[{it['id']}]: bad price {it['price']!r}")
|
||||
if it["atlas"] not in atlases:
|
||||
errs.append(f"{name}[{it['id']}]: atlas {it['atlas']!r} not in atlases[]")
|
||||
uv = it["uv"]
|
||||
if not (isinstance(uv, list) and len(uv) == 4):
|
||||
errs.append(f"{name}[{it['id']}]: uv must be [u0,v0,u1,v1]"); continue
|
||||
u0, v0, u1, v1 = uv
|
||||
if not (0 <= u0 < u1 <= 1 and 0 <= v0 < v1 <= 1):
|
||||
errs.append(f"{name}[{it['id']}]: uv out of range / inverted: {uv}")
|
||||
return errs
|
||||
|
||||
|
||||
def main():
|
||||
idxs = sorted(glob.glob(os.path.join(PACKS, "stock_*_index.json")))
|
||||
if not idxs:
|
||||
print("pack-QA: no committed pack indexes in pipeline/packs/ — nothing to validate")
|
||||
return 0
|
||||
all_errs = []
|
||||
for p in idxs:
|
||||
e = validate_index(p)
|
||||
all_errs += e
|
||||
idx = json.load(open(p))
|
||||
print(f" {os.path.basename(p):28} kind={idx.get('kind')} items={len(idx.get('items', []))} "
|
||||
f"{'OK' if not e else 'ERR('+str(len(e))+')'}")
|
||||
for e in all_errs:
|
||||
print(f" ERR {e}")
|
||||
if all_errs:
|
||||
print(f"pack-QA FAIL — {len(all_errs)} error(s)")
|
||||
return 1
|
||||
print(f"pack-QA OK — {len(idxs)} pack index(es) valid")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -93,6 +93,10 @@ export function createLighting(scene, plan, skins, { radius = 3, shadows = true
|
||||
function setSegment(i) { clock = ((i % SEG.length) + SEG.length) % SEG.length + 0.001; apply(i); }
|
||||
function stepSegment(d) { setSegment((seg + d + SEG.length) % SEG.length); }
|
||||
|
||||
// Swap the sky-dome skin at runtime (weather.js uses this for a matching rainy/overcast sky).
|
||||
// The per-segment day/night colour tint (skyMat.color in apply) still rides on top of the new map.
|
||||
function setSky(name) { skins.skyTex(name, (t) => { skyMat.map = t; skyMat.needsUpdate = true; }); }
|
||||
|
||||
const _sunOff = new THREE.Vector3();
|
||||
function update(dt, playerPos) {
|
||||
if (!paused) {
|
||||
@ -140,7 +144,7 @@ export function createLighting(scene, plan, skins, { radius = 3, shadows = true
|
||||
apply(seg);
|
||||
return {
|
||||
group, sun, hemi,
|
||||
update, setSegment, stepSegment, getClock, attachRenderer, dispose,
|
||||
update, setSegment, stepSegment, setSky, getClock, attachRenderer, dispose,
|
||||
setPaused: (p) => { paused = p; },
|
||||
togglePaused: () => (paused = !paused),
|
||||
isNight: () => night,
|
||||
|
||||
131
web/js/world/weather.js
Normal file
131
web/js/world/weather.js
Normal file
@ -0,0 +1,131 @@
|
||||
// PROCITY Lane B — weather.js
|
||||
// v2 weather (?weather=1), default-off. Seeded per (citySeed, day): clear / overcast / rain, with
|
||||
// AU-plausible weights. Rain is ONE draw — a camera-following Points layer with a procedural streak
|
||||
// sprite + a falling/wind vertex shader — plus a wet-ground response (roughness/darken on the shared
|
||||
// ground materials) and a matching rainy sky dome (lighting.setSky). Exposes { state, intensity } at
|
||||
// window.PROCITY.weather for other lanes (Lane D reads it for weather-reactive crowds).
|
||||
//
|
||||
// Fully procedural (no fetch beyond the sky JPEG the town already loads). Flag-off is byte-identical:
|
||||
// the shell simply never constructs this, so the default path is untouched.
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { rng, frange, pick } from '../core/prng.js';
|
||||
|
||||
const RAIN_SKIES = ['summer-storm', 'monsoon', 'cold-front'];
|
||||
const OVERCAST_SKIES = ['grey-drizzle', 'cold-front', 'high-cirrus'];
|
||||
|
||||
/**
|
||||
* weatherFor(citySeed, day=0) → { state:'clear'|'overcast'|'rain', intensity:0..1 }
|
||||
* Deterministic. Mostly clear, some overcast, occasional rain (AU coastal-town plausible).
|
||||
* `day` lets a future day-counter roll new weather; today the town has one day, so day=0.
|
||||
*/
|
||||
export function weatherFor(citySeed, day = 0) {
|
||||
const r = rng(citySeed >>> 0, 'weather', day | 0);
|
||||
const x = r();
|
||||
if (x < 0.55) return { state: 'clear', intensity: 0 };
|
||||
if (x < 0.80) return { state: 'overcast', intensity: +frange(r, 0.3, 0.6).toFixed(2) };
|
||||
return { state: 'rain', intensity: +frange(r, 0.45, 1.0).toFixed(2) };
|
||||
}
|
||||
|
||||
// procedural vertical rain-streak sprite (canvas — no fetch)
|
||||
function streakTexture() {
|
||||
const c = document.createElement('canvas'); c.width = 8; c.height = 64;
|
||||
const x = c.getContext('2d');
|
||||
const g = x.createLinearGradient(0, 0, 0, 64);
|
||||
g.addColorStop(0, 'rgba(255,255,255,0)'); g.addColorStop(0.5, 'rgba(255,255,255,0.95)'); g.addColorStop(1, 'rgba(255,255,255,0)');
|
||||
x.fillStyle = g; x.fillRect(3, 0, 2, 64);
|
||||
const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace; t.generateMipmaps = false; t.minFilter = THREE.LinearFilter;
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* createWeather({ scene, plan, skins, lighting, camera, force }) → { group, state, update, dispose }
|
||||
* The shell constructs this only under ?weather; `force` (a state string) overrides the seed for
|
||||
* testing/demos. Call update(dt) each street frame. Sets window.PROCITY.weather to the live state.
|
||||
*/
|
||||
export function createWeather({ scene, plan, skins, lighting, camera, force = null }) {
|
||||
const state = force && /^(clear|overcast|rain)$/.test(force)
|
||||
? { state: force, intensity: force === 'clear' ? 0 : (force === 'rain' ? 0.85 : 0.5) }
|
||||
: weatherFor(plan.citySeed);
|
||||
const group = new THREE.Group(); group.name = 'weather';
|
||||
scene.add(group);
|
||||
const r = rng(plan.citySeed >>> 0, 'weathersky', 0);
|
||||
|
||||
// ── matching sky dome ──
|
||||
if (state.state === 'rain') lighting.setSky(pick(r, RAIN_SKIES));
|
||||
else if (state.state === 'overcast') lighting.setSky(pick(r, OVERCAST_SKIES));
|
||||
|
||||
// ── wet ground (rain only): darken + drop roughness on the shared ground materials ──
|
||||
const groundRestore = [];
|
||||
if (state.state === 'rain') {
|
||||
const gg = scene.getObjectByName('ground');
|
||||
if (gg) gg.traverse((o) => {
|
||||
if (!o.isMesh || !o.material || !o.material.isMeshStandardMaterial) return;
|
||||
const m = o.material;
|
||||
if (m.userData._wet) return; // shared material — touch once, restore once
|
||||
m.userData._wet = true;
|
||||
groundRestore.push({ m, color: m.color.clone(), roughness: m.roughness, metalness: m.metalness });
|
||||
m.color.multiplyScalar(0.6); m.roughness = Math.min(m.roughness, 0.35); m.metalness = 0.18; m.needsUpdate = true;
|
||||
});
|
||||
}
|
||||
|
||||
// ── rain particles: ONE draw (Points), camera-following, procedural streaks ──
|
||||
let rainMat = null;
|
||||
if (state.state === 'rain') {
|
||||
const COUNT = Math.round(700 + state.intensity * 1500);
|
||||
const BOX = 48, BOXH = 28;
|
||||
const pos = new Float32Array(COUNT * 3), phase = new Float32Array(COUNT);
|
||||
for (let i = 0; i < COUNT; i++) {
|
||||
pos[i * 3] = frange(r, -BOX / 2, BOX / 2);
|
||||
pos[i * 3 + 1] = frange(r, -BOXH / 2, BOXH / 2);
|
||||
pos[i * 3 + 2] = frange(r, -BOX / 2, BOX / 2);
|
||||
phase[i] = r() * BOXH;
|
||||
}
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
||||
geo.setAttribute('aPhase', new THREE.BufferAttribute(phase, 1));
|
||||
rainMat = new THREE.ShaderMaterial({
|
||||
transparent: true, depthWrite: false,
|
||||
uniforms: {
|
||||
uTime: { value: 0 }, uTex: { value: streakTexture() }, uBoxH: { value: BOXH },
|
||||
uSpeed: { value: 32 }, uSize: { value: 9.0 }, uWind: { value: 0.18 }, uOpacity: { value: 0.32 + state.intensity * 0.3 },
|
||||
},
|
||||
vertexShader: `
|
||||
uniform float uTime, uBoxH, uSpeed, uSize, uWind; attribute float aPhase;
|
||||
void main(){
|
||||
vec3 p = position;
|
||||
float fall = mod(uTime * uSpeed + aPhase, uBoxH);
|
||||
p.y = position.y - fall; if (p.y < -uBoxH * 0.5) p.y += uBoxH; // fall + wrap
|
||||
p.x += (uBoxH * 0.5 - p.y) * uWind; // wind slant
|
||||
vec4 mv = modelViewMatrix * vec4(p, 1.0);
|
||||
gl_Position = projectionMatrix * mv;
|
||||
gl_PointSize = uSize * 34.0 / max(-mv.z, 1.0);
|
||||
}`,
|
||||
fragmentShader: `
|
||||
uniform sampler2D uTex; uniform float uOpacity;
|
||||
void main(){ vec4 t = texture2D(uTex, gl_PointCoord); if (t.a < 0.04) discard; gl_FragColor = vec4(vec3(0.72, 0.77, 0.86), t.a * uOpacity); }`,
|
||||
});
|
||||
const pts = new THREE.Points(geo, rainMat);
|
||||
pts.frustumCulled = false; // the box always surrounds the camera
|
||||
group.add(pts);
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
group.position.copy(camera.position); // the rain box follows the player
|
||||
if (rainMat) rainMat.uniforms.uTime.value += dt;
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
for (const g of groundRestore) { g.m.color.copy(g.color); g.m.roughness = g.roughness; g.m.metalness = g.metalness; g.m.userData._wet = false; g.m.needsUpdate = true; }
|
||||
group.traverse((o) => {
|
||||
if (o.geometry) o.geometry.dispose();
|
||||
if (o.material) { if (o.material.uniforms && o.material.uniforms.uTex) o.material.uniforms.uTex.value.dispose(); o.material.dispose(); }
|
||||
});
|
||||
scene.remove(group);
|
||||
}
|
||||
|
||||
// publish the contract for other lanes (Lane D reads window.PROCITY.weather)
|
||||
if (typeof window !== 'undefined') { window.PROCITY = window.PROCITY || {}; window.PROCITY.weather = state; }
|
||||
|
||||
return { group, state, update, dispose };
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user