| import base64 |
| import hashlib |
| import html |
| import json |
| import math |
| import os |
| import tempfile |
| import zipfile |
| from dataclasses import dataclass |
| from datetime import datetime, timezone |
| from io import BytesIO |
|
|
| import gradio as gr |
| import numpy as np |
| from PIL import Image, ImageDraw, ImageFont |
|
|
|
|
| MODEL_ID = "dreamwall-local-semantic-fingerprint-v1" |
| GRID = 32 |
| SCALE = 12 |
| PUBLIC_SPACE_URL = "https://build-small-hackathon-dreamwall-mc.hf.space" |
| RESOURCE_PACK_URL = "https://huggingface.co/spaces/build-small-hackathon/dreamwall-mc/resolve/main/resource-pack/AfterBlockMuseum.zip" |
| RESOURCE_PACK_SHA1 = "03487f018e2062e254b5ea443396f29d099f8b67" |
| RESOURCE_PACK_PATH = "resource-pack/AfterBlockMuseum.zip" |
| PAPER_PLUGIN_JAR_PATH = "server-kit/dreamwall-paper-bridge-0.1.0.jar" |
| PAPER_PLUGIN_SHA1 = "fd0d87ebcebe6749cbd7023c44d7c7b2c99e2170" |
| PAPER_PLUGIN_SHA256 = "22e99a4205b07e9df386519e2c19a14e56aa775fe69c14c77771ac996387cc44" |
| PREBUILT_WORLD_PATH = "server-kit/afterblock-demo-world.zip" |
| PREBUILT_WORLD_SHA1 = "c438d7dcb98493f436e0ca32aa6c8f73035fbdc0" |
| PREBUILT_WORLD_SHA256 = "e4634fb17b6aefcb1f075701727cb3c34bb94ce1886dcbb6c729dc0cb4515a6a" |
| TEXTURE_PAGE_SIZE = 36 |
| DEFAULT_IMPORT_PROMPT = "blue school bag from exam week" |
| DEFAULT_IMPORT_STORY = "It carried my laptop, exam panic, snacks, and the mornings I kept showing up." |
| DEFAULT_IMPORT_OWNER = "@Wildstash" |
| PEBBLEHOST_SFTP_HOST = "uk144.pebblehost.net" |
| PEBBLEHOST_SFTP_PORT = "2222" |
| PEBBLEHOST_SFTP_USER = "itsarnavsalkade@gmail.com.5ea1f567" |
| MINECRAFT_JOIN_ADDRESS = os.getenv("AFTERBLOCK_MINECRAFT_JOIN_ADDRESS", "uk144.pebblehost.net") |
| MINECRAFT_JOIN_NOTE = os.getenv( |
| "AFTERBLOCK_MINECRAFT_JOIN_NOTE", |
| "Use the Minecraft port shown in the PebbleHost panel. SFTP port 2222 is only for uploads.", |
| ) |
|
|
|
|
| BLOCKS = [ |
| ("white_wool", (234, 236, 230)), |
| ("black_wool", (25, 25, 25)), |
| ("gray_wool", (78, 82, 86)), |
| ("light_gray_wool", (156, 160, 162)), |
| ("brown_wool", (114, 73, 43)), |
| ("red_wool", (160, 39, 34)), |
| ("orange_wool", (230, 118, 31)), |
| ("yellow_wool", (246, 198, 45)), |
| ("lime_wool", (96, 187, 50)), |
| ("green_wool", (74, 124, 42)), |
| ("cyan_wool", (22, 156, 156)), |
| ("light_blue_wool", (92, 168, 224)), |
| ("blue_wool", (53, 70, 164)), |
| ("purple_wool", (126, 61, 181)), |
| ("magenta_wool", (190, 67, 181)), |
| ("pink_wool", (239, 141, 172)), |
| ("sandstone", (218, 203, 143)), |
| ("moss_block", (89, 110, 45)), |
| ("deepslate", (62, 62, 68)), |
| ("amethyst_block", (133, 89, 184)), |
| ("prismarine", (99, 156, 151)), |
| ("glowstone", (241, 203, 118)), |
| ("obsidian", (28, 22, 38)), |
| ("sea_lantern", (172, 205, 190)), |
| ] |
|
|
| MOOD_WORDS = { |
| "cozy": ["cozy", "warm", "cottage", "soft", "home", "lantern"], |
| "cursed": ["cursed", "haunted", "eldritch", "broken", "void", "forbidden"], |
| "ancient": ["ancient", "ruin", "temple", "fossil", "myth", "buried"], |
| "mechanical": ["machine", "gear", "factory", "robot", "engine", "circuit"], |
| "wild": ["forest", "storm", "moss", "ocean", "swamp", "wind"], |
| "royal": ["castle", "king", "queen", "gold", "throne", "banner"], |
| } |
|
|
| CANVAS_SIZE = 12 |
| PLOT_SCALE = 32 |
| GALLERY_ORIGIN_X = -(CANVAS_SIZE // 2) * PLOT_SCALE |
| GALLERY_ORIGIN_Y = 80 |
| GALLERY_ORIGIN_Z = -(CANVAS_SIZE // 2) * PLOT_SCALE |
| EXISTING_ARTWORKS = [ |
| { |
| "title": "Bird above the broken sky", |
| "player": "anonymous_heron", |
| "prompt": "a bird in the sky over a silver tree", |
| "x": 4, |
| "z": 5, |
| "moods": ["wild", "cozy"], |
| "value": 72, |
| }, |
| { |
| "title": "Company sigil in emerald glass", |
| "player": "founder_ghost", |
| "prompt": "an ai logo for my company made of emerald glass", |
| "x": 5, |
| "z": 5, |
| "moods": ["mechanical", "royal"], |
| "value": 81, |
| }, |
| { |
| "title": "Cloud treaty", |
| "player": "sky_bidder", |
| "prompt": "clouds gathering around a public tree", |
| "x": 5, |
| "z": 4, |
| "moods": ["wild", "ancient"], |
| "value": 64, |
| }, |
| { |
| "title": "Nether receipt", |
| "player": "redacted", |
| "prompt": "a cursed vending machine that sells memories", |
| "x": 8, |
| "z": 8, |
| "moods": ["cursed", "mechanical"], |
| "value": 69, |
| }, |
| ] |
|
|
| LIVING_WALL_PROMPTS = [ |
| "a lonely moon whale carrying broken radio signals", |
| "a neon forest growing through an old arcade machine", |
| "a thunder bird protecting a tiny public library", |
| "glass mushrooms orbiting a redstone lighthouse", |
| "a crown made of moss floating over a village gate", |
| "a cloud dragon sleeping inside a circuit board", |
| ] |
|
|
| MUSEUM_HALLS = [ |
| "Hall of Firsts", |
| "Hall of Companions", |
| "Hall of Turning Points", |
| "Hall of Worlds", |
| "Hall of Soft Things", |
| "Hall of Tools", |
| "Hall of Lost Signals", |
| "Grand Painting Hall", |
| "Animal Spirit Grove", |
| ] |
|
|
| HALL_PROFILES = { |
| "Hall of Firsts": { |
| "when": "when an object marks a first threshold", |
| "world": "sunlit origin cabinets, first-step lamps, gold plaques", |
| "minecraft": "warm sandstone, gold pressure plates, first-light redstone lamps", |
| }, |
| "Hall of Companions": { |
| "when": "when something stayed close through ordinary days", |
| "world": "pocket pedestals, quiet benches, green glass cases", |
| "minecraft": "moss blocks, item frames, low lanterns near visitor paths", |
| }, |
| "Hall of Turning Points": { |
| "when": "when a choice, exam, trade, or pressure changed direction", |
| "world": "angled corridors, copper rails, redstone decision marks", |
| "minecraft": "copper blocks, comparator lamps, signs with before/after lines", |
| }, |
| "Hall of Worlds": { |
| "when": "when a relic opened a world larger than the room", |
| "world": "portal shelves, blue map floors, miniature skylines", |
| "minecraft": "blue wool, maps, frames, glowstone portal corners", |
| }, |
| "Hall of Soft Things": { |
| "when": "when the memory protects, quiets, or shelters", |
| "world": "felt-lit corners, soft rails, low pink glass", |
| "minecraft": "pink wool, beds, candles, carpeted alcoves", |
| }, |
| "Hall of Tools": { |
| "when": "when the object helped the owner act on the world", |
| "world": "workbench rows, grey vaults, calibrated stands", |
| "minecraft": "deepslate, anvils, crafting tables, inspection signs", |
| }, |
| "Hall of Lost Signals": { |
| "when": "when private signal survives public noise", |
| "world": "dim receivers, violet static, archive slits", |
| "minecraft": "amethyst, note blocks, redstone repeaters, low particles", |
| }, |
| "Grand Painting Hall": { |
| "when": "when imagination becomes a place", |
| "world": "large frame walls, myth bays, painted sky panels", |
| "minecraft": "item frames, terracotta walls, banner rails, map-art slots", |
| }, |
| "Animal Spirit Grove": { |
| "when": "when the artifact behaves like a living companion", |
| "world": "moss pens, moonlit tracks, creature-name stones", |
| "minecraft": "moss carpets, fences, leaves, particle trails", |
| }, |
| } |
|
|
| DEMO_ARTIFACTS = { |
| "Star Wars childhood book": { |
| "owner_name": "Arnav", |
| "owner_handle": "@Wildstash", |
| "input_type": "memory_prompt", |
| "source_prompt": "a worn Star Wars childhood book with bent corners and a silver spaceship on the cover", |
| "memory_text": "I kept reopening it because the galaxy felt bigger than my room.", |
| }, |
| "AirPods from first year of university": { |
| "owner_name": "Arnav", |
| "owner_handle": "@Wildstash", |
| "input_type": "object_photo", |
| "source_prompt": "white AirPods from my first year of university", |
| "memory_text": "They carried private worlds through public noise during my first year away.", |
| }, |
| "Monitor used for first Bitcoin trade": { |
| "owner_name": "Arnav", |
| "owner_handle": "@Wildstash", |
| "input_type": "object_photo", |
| "source_prompt": "a black monitor used for my first Bitcoin trade", |
| "memory_text": "A screen where numbers first started feeling like weather.", |
| }, |
| "School bag carried through exams": { |
| "owner_name": "Arnav", |
| "owner_handle": "@Wildstash", |
| "input_type": "object_photo", |
| "source_prompt": "a school bag carried through exams", |
| "memory_text": "It held pencils, snacks, panic, and the weight of every morning.", |
| }, |
| "Prompted painting: red dragon above childhood home": { |
| "owner_name": "Arnav", |
| "owner_handle": "@Wildstash", |
| "input_type": "painting_prompt", |
| "source_prompt": "a red dragon above my childhood home", |
| "memory_text": "A fantasy guardian hovering over the place I learned to imagine from.", |
| }, |
| } |
|
|
| SOCIAL_HANDLES = [ |
| "@mossbyte", "@skyreceipt", "@quietbag", "@pixelmira", "@redstoneivy", |
| "@lostadapter", "@softsignal", "@bookishnova", "@lampkeeper", "@tinymoon", |
| "@schoolghost", "@mintmonitor", "@cloudpocket", "@driftcase", "@noiseferry", |
| "@glasssparrow", "@examrunner", "@coppernote", "@homebutton", "@tidyvoid", |
| ] |
|
|
| OBJECT_LIBRARY = [ |
| ("Star Wars childhood book", "memory_prompt", "a worn Star Wars childhood book with bent corners", "I kept reopening it because the galaxy felt bigger than my room.", "book"), |
| ("AirPods", "object_photo", "white AirPods from first year of university", "They carried private worlds through public noise.", "earbuds"), |
| ("Bitcoin monitor", "object_photo", "black monitor used for a first Bitcoin trade", "Numbers first started feeling like weather.", "monitor"), |
| ("Exam school bag", "object_photo", "school bag carried through exams", "It held pencils, snacks, panic, and every morning.", "school bag"), |
| ("Red dragon painting", "painting_prompt", "a red dragon above my childhood home", "A fantasy guardian over the place I learned to imagine.", "painting"), |
| ("Water bottle", "object_photo", "scratched blue water bottle", "It waited on every desk like a tiny reservoir.", "water bottle"), |
| ("Desk lamp", "object_photo", "warm desk lamp beside late night notes", "It kept one square of the room awake.", "lamp"), |
| ("Microphone", "object_photo", "black microphone used for first recordings", "It learned my voice before anyone else did.", "microphone"), |
| ("Tissue packet", "object_photo", "small tissue packet from a long train ride", "It was ordinary until the day was not.", "tissue"), |
| ("Keyboard", "object_photo", "keyboard with shiny worn keys", "It translated nervous thoughts into work.", "keyboard"), |
| ("Phone charger", "object_photo", "fraying white phone charger", "It rescued the last percent of the day.", "charger"), |
| ("Coffee mug", "object_photo", "ceramic coffee mug with a chipped handle", "It warmed decisions before they became plans.", "mug"), |
| ("Running shoes", "object_photo", "old running shoes with rain marks", "They carried promises the body almost kept.", "shoes"), |
| ("House key", "object_photo", "single house key on a faded ring", "It made a door feel like a return.", "key"), |
| ("Toy car", "memory_prompt", "tiny red toy car from a childhood shelf", "It was speed before I knew distance.", "toy car"), |
| ("Notebook", "object_photo", "notebook full of half-started ideas", "It preserved versions of me that did not ship.", "notebook"), |
| ("Glasses", "object_photo", "black glasses with a scratched lens", "They made the world sharper and less certain.", "glasses"), |
| ("Wallet", "object_photo", "old wallet with expired cards", "It held small permissions to be outside.", "wallet"), |
| ("USB drive", "object_photo", "silver USB drive with unknown files", "It carries a sealed room of forgotten work.", "usb drive"), |
| ("Alarm clock", "object_photo", "small alarm clock from exam mornings", "It was the sound of responsibility arriving.", "clock"), |
| ("Calculator", "object_photo", "scientific calculator with faded buttons", "It made difficult answers feel briefly mechanical.", "calculator"), |
| ("Camera", "object_photo", "compact camera with a scratched lens", "It saved proof that the room once looked different.", "camera"), |
| ("Cassette tape", "object_photo", "old cassette tape with a handwritten label", "It held a voice from before everything became searchable.", "cassette"), |
| ("Lucky coin", "object_photo", "small lucky coin kept in a wallet", "It was not worth much until it became a ritual.", "coin"), |
| ("Game controller", "object_photo", "game controller with worn thumbsticks", "It made imaginary places answer back.", "game controller"), |
| ("Headphones", "object_photo", "black headphones used on long nights", "They made a private room around the head.", "headphones"), |
| ("Lunch box", "object_photo", "steel lunch box from school days", "It carried home into the middle of the day.", "lunch box"), |
| ("Medicine box", "object_photo", "small medicine box in a drawer", "It remembered the days the body needed backup.", "medicine box"), |
| ("Memory card", "object_photo", "tiny memory card with old photos", "It kept a city of images in almost no space.", "memory card"), |
| ("Paint brush", "object_photo", "paint brush stained with blue acrylic", "It turned hesitation into visible color.", "paint brush"), |
| ("Passport", "object_photo", "passport with airport stamps", "It proved the same person could cross many rooms.", "passport"), |
| ("Pencil", "object_photo", "short pencil chewed near the eraser", "It carried ideas before they were confident.", "pencil"), |
| ("Photo frame", "object_photo", "wooden photo frame beside a bed", "It made one memory stand still on purpose.", "photo frame"), |
| ("Plush toy", "object_photo", "soft plush toy from an old shelf", "It was a guardian that never needed batteries.", "plush toy"), |
| ("Polaroid", "object_photo", "faded polaroid from a bright afternoon", "It made a square of time feel touchable.", "polaroid"), |
| ("Remote", "object_photo", "TV remote with one missing button", "It controlled a room more than it should have.", "remote"), |
| ("Ring", "object_photo", "simple ring with a small scratch", "It made a promise small enough to wear.", "ring"), |
| ("Train ticket", "object_photo", "creased train ticket from a late ride", "It marked the distance between two versions of the day.", "train ticket"), |
| ("Umbrella", "object_photo", "black umbrella with bent ribs", "It made bad weather negotiable.", "umbrella"), |
| ("Watch", "object_photo", "old watch that runs a little slow", "It kept time in its own accent.", "watch"), |
| ] |
|
|
|
|
| @dataclass |
| class ArtResult: |
| image: Image.Image |
| profile: dict |
| palette: list |
| commands: str |
| report: str |
| trace: str |
| server_packet: str |
| canvas_report: str |
| valuation_packet: str |
|
|
|
|
| def stable_seed(text: str) -> int: |
| digest = hashlib.sha256(text.encode("utf-8")).hexdigest() |
| return int(digest[:16], 16) |
|
|
|
|
| def embedding(text: str) -> np.ndarray: |
| lowered = text.lower() |
| vec = np.zeros(96, dtype=np.float32) |
| words = [word.strip(".,!?;:()[]{}\"'") for word in lowered.split()] |
| for idx, word in enumerate(words): |
| if not word: |
| continue |
| digest = hashlib.blake2b(word.encode("utf-8"), digest_size=32).digest() |
| for offset, byte in enumerate(digest): |
| slot = (byte + idx * 17 + offset * 7) % len(vec) |
| vec[slot] += ((byte / 255.0) * 2.0 - 1.0) * (1.0 + min(len(word), 12) / 12.0) |
| for mood, mood_words in MOOD_WORDS.items(): |
| hits = sum(1 for word in mood_words if word in lowered) |
| if hits: |
| mood_seed = stable_seed(mood) |
| rng = np.random.default_rng(mood_seed) |
| vec += rng.normal(0, 0.22 * hits, size=len(vec)).astype(np.float32) |
| if not np.any(vec): |
| vec[0] = 1.0 |
| norm = float(np.linalg.norm(vec)) |
| return vec / max(norm, 1e-6) |
|
|
|
|
| def top_moods(text: str, vec: np.ndarray) -> list[str]: |
| lowered = text.lower() |
| scored = [] |
| for mood, words in MOOD_WORDS.items(): |
| lexical = sum(1 for word in words if word in lowered) * 0.28 |
| mood_vec = embedding(" ".join(words)) |
| semantic = float(np.dot(vec, mood_vec)) |
| scored.append((semantic + lexical, mood)) |
| return [mood for _, mood in sorted(scored, reverse=True)[:3]] |
|
|
|
|
| def palette_from_vector(vec: np.ndarray, seed: int, moods: list[str]) -> list[tuple[str, tuple[int, int, int]]]: |
| rng = np.random.default_rng(seed) |
| block_vecs = np.array([rgb for _, rgb in BLOCKS], dtype=np.float32) / 255.0 |
| anchors = np.abs(vec[: len(BLOCKS)]) + 1e-4 |
| weights = anchors / max(float(anchors.sum()), 1e-6) |
| chosen = list(rng.choice(len(BLOCKS), size=7, replace=False, p=weights)) |
|
|
| mood_boosts = { |
| "cozy": ["orange_wool", "yellow_wool", "glowstone", "brown_wool"], |
| "cursed": ["obsidian", "purple_wool", "black_wool", "amethyst_block"], |
| "ancient": ["sandstone", "moss_block", "deepslate", "brown_wool"], |
| "mechanical": ["gray_wool", "light_gray_wool", "deepslate", "cyan_wool"], |
| "wild": ["moss_block", "green_wool", "prismarine", "sea_lantern"], |
| "royal": ["yellow_wool", "red_wool", "purple_wool", "blue_wool"], |
| } |
| for mood in moods: |
| for name in mood_boosts.get(mood, []): |
| idx = next(i for i, block in enumerate(BLOCKS) if block[0] == name) |
| if idx not in chosen: |
| chosen[-1] = idx |
| break |
| return [BLOCKS[i] for i in chosen] |
|
|
|
|
| def generate_grid(vec: np.ndarray, seed: int, palette: list) -> np.ndarray: |
| rng = np.random.default_rng(seed) |
| grid = np.zeros((GRID, GRID), dtype=np.int32) |
| freq_a = 1.4 + abs(vec[3]) * 5 |
| freq_b = 1.2 + abs(vec[9]) * 4 |
| symmetry = abs(vec[12]) > 0.19 |
| center_bias = abs(vec[27]) |
|
|
| for y in range(GRID): |
| for x in range(GRID): |
| nx = (x / GRID) - 0.5 |
| ny = (y / GRID) - 0.5 |
| wave = math.sin((nx * freq_a + vec[1]) * math.pi * 2) |
| wave += math.cos((ny * freq_b + vec[2]) * math.pi * 2) |
| ring = math.sin((math.hypot(nx, ny) * (6 + abs(vec[18]) * 12) + vec[4]) * math.pi) |
| noise = rng.normal(0, 0.42) |
| score = wave + ring * (0.7 + center_bias) + noise |
| idx = int(abs(score * 997 + vec[(x + y) % len(vec)] * 113)) % len(palette) |
| grid[y, x] = idx |
| if symmetry: |
| grid[:, GRID // 2 :] = np.fliplr(grid[:, : GRID // 2]) |
| return grid |
|
|
|
|
| def render_grid(grid: np.ndarray, palette: list) -> Image.Image: |
| img = Image.new("RGB", (GRID * SCALE, GRID * SCALE), (0, 0, 0)) |
| draw = ImageDraw.Draw(img) |
| for y in range(GRID): |
| for x in range(GRID): |
| _, color = palette[int(grid[y, x])] |
| draw.rectangle( |
| [x * SCALE, y * SCALE, (x + 1) * SCALE - 1, (y + 1) * SCALE - 1], |
| fill=color, |
| ) |
| for i in range(0, GRID * SCALE, SCALE * 4): |
| draw.line([(i, 0), (i, GRID * SCALE)], fill=(35, 28, 22), width=1) |
| draw.line([(0, i), (GRID * SCALE, i)], fill=(35, 28, 22), width=1) |
| return img |
|
|
|
|
| def compact_commands(grid: np.ndarray, palette: list, origin: str) -> str: |
| commands = [ |
| "# Paste these into Minecraft with WorldEdit installed.", |
| "# Stand near the gallery wall. Set pos1/pos2 manually if needed.", |
| f"# Suggested origin: {origin}", |
| "//wand", |
| "//pos1", |
| "//pos2", |
| "# Build the 32x32 mural as wool/block stripes. Each line is one row.", |
| ] |
| for y in range(GRID): |
| runs = [] |
| start = 0 |
| current = int(grid[y, 0]) |
| for x in range(1, GRID + 1): |
| if x == GRID or int(grid[y, x]) != current: |
| block = palette[current][0] |
| runs.append(f"{start}-{x - 1}:{block}") |
| if x < GRID: |
| start = x |
| current = int(grid[y, x]) |
| commands.append(f"# row {y:02d}: " + ", ".join(runs)) |
| commands.append("# Plugin hook idea: convert the row runs into setblock/fill calls at the wall anchor.") |
| return "\n".join(commands) |
|
|
|
|
| def row_runs(grid: np.ndarray, palette: list) -> list[list[dict]]: |
| rows = [] |
| for y in range(GRID): |
| runs = [] |
| start = 0 |
| current = int(grid[y, 0]) |
| for x in range(1, GRID + 1): |
| if x == GRID or int(grid[y, x]) != current: |
| runs.append( |
| { |
| "x1": start, |
| "x2": x - 1, |
| "y": y, |
| "block": palette[current][0], |
| } |
| ) |
| if x < GRID: |
| start = x |
| current = int(grid[y, x]) |
| rows.append(runs) |
| return rows |
|
|
|
|
| def prompt_density(prompt: str) -> float: |
| words = [word.strip(".,!?;:()[]{}\"'").lower() for word in prompt.split()] |
| words = [word for word in words if word] |
| if not words: |
| return 0.0 |
| unique_ratio = len(set(words)) / len(words) |
| long_word_ratio = sum(1 for word in words if len(word) >= 7) / len(words) |
| symbol_hits = sum(1 for word in words if word in {"bird", "tree", "cloud", "logo", "castle", "machine", "temple", "sky"}) |
| return min(1.0, unique_ratio * 0.55 + long_word_ratio * 0.25 + min(symbol_hits, 4) * 0.05) |
|
|
|
|
| def plot_for_seed(seed: int) -> dict: |
| x = seed % CANVAS_SIZE |
| z = (seed // CANVAS_SIZE) % CANVAS_SIZE |
| return { |
| "x": int(x), |
| "z": int(z), |
| "world_x": int((x - CANVAS_SIZE // 2) * PLOT_SCALE), |
| "world_z": int((z - CANVAS_SIZE // 2) * PLOT_SCALE), |
| "size": PLOT_SCALE, |
| } |
|
|
|
|
| def nearby_artworks(plot: dict) -> list[dict]: |
| near = [] |
| for art in EXISTING_ARTWORKS: |
| distance = abs(art["x"] - plot["x"]) + abs(art["z"] - plot["z"]) |
| if distance <= 2: |
| near.append({**art, "distance": distance}) |
| return sorted(near, key=lambda item: (item["distance"], -item["value"]))[:3] |
|
|
|
|
| def fusion_lines(prompt: str, player: str, moods: list[str], plot: dict) -> list[str]: |
| neighbors = nearby_artworks(plot) |
| if not neighbors: |
| return [ |
| "No nearby fusion yet. This plot becomes a new anchor others can build around.", |
| "Value grows if future prompts land nearby and reuse its symbols.", |
| ] |
|
|
| lines = [] |
| for art in neighbors: |
| shared_moods = sorted(set(moods).intersection(art["moods"])) |
| if shared_moods: |
| reason = f"shared {', '.join(shared_moods)} mood" |
| else: |
| reason = "spatial collision without mood overlap" |
| lines.append( |
| f"{player} fuses with {art['player']} at ({art['x']}, {art['z']}): " |
| f"{reason}. New concept: {prompt} woven into '{art['title']}'." |
| ) |
| return lines |
|
|
|
|
| def valuation(prompt: str, moods: list[str], palette_names: list[str], plot: dict) -> dict: |
| density = prompt_density(prompt) |
| neighbors = nearby_artworks(plot) |
| adjacency = min(1.0, sum(max(0, 3 - item["distance"]) for item in neighbors) / 6) |
| mood_diversity = len(set(moods)) / max(1, len(MOOD_WORDS)) |
| palette_rarity = len(set(palette_names).intersection({"obsidian", "amethyst_block", "sea_lantern", "glowstone"})) / 4 |
| score = 25 + density * 28 + adjacency * 24 + mood_diversity * 12 + palette_rarity * 16 |
| votes = int(3 + score // 8 + len(neighbors) * 2) |
| reserve = int(max(5, score * 1.7)) |
| return { |
| "creative_value": round(score, 2), |
| "syntactic_density": round(density, 3), |
| "context_adjacency": round(adjacency, 3), |
| "mood_diversity": round(mood_diversity, 3), |
| "palette_rarity": round(palette_rarity, 3), |
| "suggested_votes": votes, |
| "demo_reserve_points": reserve, |
| "market_note": "Demo points only; no real-money sale or blockchain required for the hackathon.", |
| } |
|
|
|
|
| def canvas_report(prompt: str, player: str, moods: list[str], palette_names: list[str], plot: dict) -> tuple[str, str]: |
| value = valuation(prompt, moods, palette_names, plot) |
| fusions = fusion_lines(prompt, player, moods, plot) |
| report = [ |
| f"Plot assigned: ({plot['x']}, {plot['z']}) -> Minecraft origin ({plot['world_x']}, 80, {plot['world_z']})", |
| f"Creative value: {value['creative_value']} demo points", |
| f"Suggested opening auction reserve: {value['demo_reserve_points']} demo points", |
| "", |
| "Why this plot has value:", |
| f"- syntactic density: {value['syntactic_density']}", |
| f"- context adjacency: {value['context_adjacency']}", |
| f"- mood diversity: {value['mood_diversity']}", |
| f"- palette rarity: {value['palette_rarity']}", |
| "", |
| "Fusion events:", |
| ] |
| report.extend(f"- {line}" for line in fusions) |
| packet = { |
| "protocol": "dreamwall.market.v1", |
| "plot": plot, |
| "valuation": value, |
| "fusion_events": fusions, |
| "auction": { |
| "mode": "demo_points", |
| "reserve": value["demo_reserve_points"], |
| "votes": value["suggested_votes"], |
| "real_money": False, |
| "blockchain": False, |
| }, |
| } |
| return "\n".join(report), json.dumps(packet, indent=2) |
|
|
|
|
| def object_guess_for(input_type: str, source_prompt: str, memory_text: str) -> str: |
| text = f"{source_prompt} {memory_text}".lower() |
| guesses = [ |
| ("phone", ["phone", "iphone", "android", "smartphone", "mobile"]), |
| ("water bottle", ["water bottle", "waater", "bottle", "canteen", "thermos"]), |
| ("plush toy", ["teddy", "teddy bear", "bear", "plush", "soft toy", "stuffed"]), |
| ("game controller", ["xbox controller", "xbox", "controller", "gamepad", "game controller", "console controller"]), |
| ("sticker", ["sticker", "decal", "label", "logo", "openai"]), |
| ("shoes", ["shoe", "shoes", "sneaker", "sneakers", "trainers"]), |
| ("book", ["book", "novel", "comic", "star wars", "cover"]), |
| ("earbuds", ["airpods", "earpods", "earbuds", "headphones", "music"]), |
| ("monitor", ["monitor", "screen", "display", "trade", "bitcoin"]), |
| ("school bag", ["bag", "backpack", "exams", "school"]), |
| ("painting", ["painting", "dragon", "prompted", "canvas"]), |
| ("animal spirit", ["pet", "animal", "dog", "cat", "bird", "creature"]), |
| ("tool", ["tool", "keyboard", "mouse", "camera", "phone"]), |
| ] |
| for guess, hints in guesses: |
| if any(hint in text for hint in hints): |
| return guess |
| if input_type == "painting_prompt": |
| return "prompted painting" |
| if input_type == "animal_spirit": |
| return "animal spirit" |
| return "personal relic" |
|
|
|
|
| def hall_for_artifact(input_type: str, object_guess: str, source_prompt: str, memory_text: str, seed: int) -> tuple[str, str]: |
| text = f"{object_guess} {source_prompt} {memory_text}".lower() |
| if input_type == "painting_prompt": |
| return "Grand Painting Hall", "paintings that were imagined before they were found" |
| if input_type == "animal_spirit" or any(word in text for word in ["pet", "animal", "dog", "cat", "bird", "creature"]): |
| return "Animal Spirit Grove", "the artifact behaves more like a companion than an object" |
| if any(word in text for word in ["first", "university", "bitcoin", "started"]): |
| return "Hall of Firsts", "the memory marks a first threshold" |
| if any(word in text for word in ["teddy", "plush", "soft toy", "sleep", "guardian"]): |
| return "Hall of Soft Things", "the memory is quiet, protective, and intimate" |
| if any(word in text for word in ["sticker", "decal", "logo", "signal", "lost", "radio", "noise"]): |
| return "Hall of Lost Signals", "the artifact carries private signal through public static" |
| if any(word in text for word in ["xbox", "controller", "gamepad", "console", "thumbstick"]): |
| return "Hall of Worlds", "it made imaginary worlds respond to the owner's hands" |
| if any(word in text for word in ["airpods", "bag", "bottle", "water", "phone", "shoe", "friend", "carried", "companion"]): |
| return "Hall of Companions", "it stayed close to the owner through ordinary days" |
| if any(word in text for word in ["exam", "trade", "turning", "changed", "panic"]): |
| return "Hall of Turning Points", "the object sits near a decision or pressure point" |
| if any(word in text for word in ["star wars", "galaxy", "home", "world", "dragon"]): |
| return "Hall of Worlds", "it opened a world larger than the room around it" |
| if any(word in text for word in ["soft", "noise", "private", "morning"]): |
| return "Hall of Soft Things", "the memory is quiet, protective, and intimate" |
| if any(word in text for word in ["monitor", "tool", "screen", "keyboard", "camera"]): |
| return "Hall of Tools", "the artifact helped the owner act on the world" |
| hall = MUSEUM_HALLS[seed % len(MUSEUM_HALLS)] |
| return hall, "the museum placed it by symbolic resonance" |
|
|
|
|
| def museum_zone_for(hall: str, seed: int) -> str: |
| zones = { |
| "Hall of Firsts": ["threshold alcove", "first-light row", "origin cabinet"], |
| "Hall of Companions": ["pocket gallery", "everyday pedestal", "quiet bench"], |
| "Hall of Turning Points": ["pressure corridor", "exam arch", "decision stair"], |
| "Hall of Worlds": ["portal bay", "map room", "myth shelf"], |
| "Hall of Soft Things": ["hush wing", "warm glass case", "felt-lit corner"], |
| "Hall of Tools": ["workbench row", "instrument vault", "copper desk"], |
| "Hall of Lost Signals": ["static aisle", "radio archive", "dim receiver"], |
| "Grand Painting Hall": ["large frame wall", "dragon bay", "painted sky"], |
| "Animal Spirit Grove": ["moss enclosure", "companion path", "moonlit pen"], |
| } |
| choices = zones.get(hall, ["west wing"]) |
| return choices[seed % len(choices)] |
|
|
|
|
| def curation_scores( |
| source_prompt: str, |
| memory_text: str, |
| input_type: str, |
| moods: list[str], |
| palette_names: list[str], |
| plot: dict, |
| ) -> dict: |
| text = f"{source_prompt} {memory_text}".lower() |
| density = prompt_density(text) |
| neighbors = nearby_artworks(plot) |
| adjacency = min(1.0, sum(max(0, 3 - item["distance"]) for item in neighbors) / 6) |
| palette_rarity = len(set(palette_names).intersection({"obsidian", "amethyst_block", "sea_lantern", "glowstone"})) / 4 |
| def score(words: list[str], base: float = 28.0) -> int: |
| hits = sum(1 for word in words if word in text) |
| return int(min(100, base + density * 35 + hits * 14)) |
|
|
| scores = { |
| "nostalgia": score(["childhood", "school", "first", "home", "kept", "morning"]), |
| "symbolism": score(["dragon", "galaxy", "signal", "cover", "weather", "guardian"]), |
| "identity": score(["my", "first", "owner", "university", "trade"], 34), |
| "companionship": score(["carried", "airpods", "bag", "private", "through"], 30), |
| "transformation": score(["first", "trade", "exam", "changed", "started"], 26), |
| "rarity": int(min(100, 35 + len(set(moods)) * 10 + density * 25)), |
| "palette_rarity": int(round(palette_rarity * 100)), |
| "adjacency_resonance": int(round(adjacency * 100)), |
| "visitor_echoes": int(min(100, 12 + len(neighbors) * 17 + density * 34)), |
| } |
| weighted = ( |
| scores["nostalgia"] * 0.16 |
| + scores["symbolism"] * 0.14 |
| + scores["identity"] * 0.14 |
| + scores["companionship"] * 0.12 |
| + scores["transformation"] * 0.12 |
| + scores["rarity"] * 0.1 |
| + scores["palette_rarity"] * 0.08 |
| + scores["adjacency_resonance"] * 0.08 |
| + scores["visitor_echoes"] * 0.06 |
| ) |
| scores["curation_score"] = int(round(weighted)) |
| return scores |
|
|
|
|
| def spirit_for_artifact(title: str, object_guess: str, memory_text: str, hall: str, seed: int) -> dict: |
| traits_bank = { |
| "book": ["wide-eyed", "paper-worn", "portal-minded"], |
| "earbuds": ["private", "signal-carrying", "soft-spoken"], |
| "monitor": ["watchful", "electric", "threshold-bound"], |
| "school bag": ["patient", "burdened", "loyal"], |
| "phone": ["signal-lit", "pocket-sized", "watchful"], |
| "water bottle": ["clear", "steady", "everyday"], |
| "plush toy": ["soft", "protective", "sleep-worn"], |
| "game controller": ["responsive", "world-opening", "thumb-worn"], |
| "sticker": ["signal-marked", "thin", "identity-bound"], |
| "shoes": ["road-worn", "patient", "routine-bound"], |
| "painting": ["mythic", "paint-lit", "protective"], |
| "animal spirit": ["restless", "companionable", "wild"], |
| "personal relic": ["quiet", "symbolic", "half-remembered"], |
| } |
| traits = traits_bank.get(object_guess, traits_bank["personal relic"]) |
| suffix = ["mote", "keeper", "echo", "sprite", "shade", "wisp"][seed % 6] |
| spirit_name = "".join(word.capitalize() for word in keywords_for_title(title)[:1]) + suffix.capitalize() |
| first_line = f"I am what remained when {object_guess} became a place." |
| if "airpods" in object_guess or object_guess == "earbuds": |
| first_line = "I carried private worlds through public noise." |
| elif object_guess == "book": |
| first_line = "I opened a galaxy small enough to fit in your hands." |
| elif object_guess == "monitor": |
| first_line = "I watched numbers become weather." |
| elif object_guess == "school bag": |
| first_line = "I carried the mornings you were not ready for." |
| elif object_guess == "phone": |
| first_line = "I kept private signals close enough to answer." |
| elif object_guess == "water bottle": |
| first_line = "I kept ordinary stamina within reach." |
| elif object_guess == "plush toy": |
| first_line = "I guarded sleep without needing to explain why." |
| elif object_guess == "game controller": |
| first_line = "I made faraway worlds answer through your hands." |
| elif object_guess == "sticker": |
| first_line = "I turned a surface into a signal." |
| elif object_guess == "shoes": |
| first_line = "I carried the route until it became routine." |
| elif object_guess == "painting": |
| first_line = "I guard the home that imagination returned to." |
| questions = [ |
| "What do you remember?", |
| "Why are you in this hall?", |
| "What should visitors notice?", |
| ] |
| responses = [ |
| first_line, |
| f"The curator placed me in {hall} because this memory still has a shape.", |
| f"Look for the detail that does not shout: {memory_text[:110]}", |
| ] |
| return { |
| "spirit_name": spirit_name, |
| "spirit_traits": traits, |
| "spirit_first_line": first_line, |
| "rules": [ |
| "speak only from the object, memory, and artifact lore", |
| "do not behave like a generic assistant", |
| "answer as a quiet museum presence", |
| ], |
| "sample_visitor_questions": questions, |
| "sample_spirit_responses": responses, |
| } |
|
|
|
|
| def resonance_links_for_artifact(hall: str, moods: list[str], plot: dict) -> list[dict]: |
| links = [] |
| for art in nearby_artworks(plot): |
| shared = sorted(set(moods).intersection(art["moods"])) |
| links.append( |
| { |
| "title": art["title"], |
| "creator": art["player"], |
| "distance": art["distance"], |
| "resonance": "shared mood" if shared else "nearby museum placement", |
| "shared_moods": shared, |
| "hall_echo": hall, |
| } |
| ) |
| return links |
|
|
|
|
| def passport_html(artifact: dict) -> str: |
| palette = artifact["palette"][:5] |
| swatches = "".join( |
| f"<span class='museum-swatch' title='{name}' style='background: rgb({rgb[0]}, {rgb[1]}, {rgb[2]})'></span>" |
| for name, rgb in [next((block for block in BLOCKS if block[0] == item), ("stone", (120, 120, 120))) for item in palette] |
| ) |
| coords = artifact["minecraft_coordinates"] |
| item = artifact.get("resource_pack_item", {}) |
| share_url = artifact.get("share_url") or artifact_share_url(artifact) |
| share_fragment = artifact_passport_fragment(artifact) |
| qr_payload = artifact.get("qr_payload") or share_url |
| qr_src = qr_code_data_uri(qr_payload) |
| qr = ( |
| f"<img class='passport-qr-image' src='{qr_src}' alt='QR code for this relic'>" |
| if qr_src |
| else qr_matrix_html(qr_payload) |
| ) |
| preview_url = artifact_preview_url(artifact) |
| preview = ( |
| f"<img class='passport-artifact-image' src='{html_escape(preview_url)}' alt='{html_escape(artifact['object_guess'])} picture'>" |
| if preview_url |
| else f"<div class='passport-object-mark'>{swatches}</div>" |
| ) |
| return f""" |
| <section class="passport-card" id="afterblock-passport-{html_escape(artifact['artifact_id'])}"> |
| <header class="passport-header"> |
| <div> |
| <div class="passport-kicker">AfterBlock Passport</div> |
| <h2>{html_escape(artifact['title'])}</h2> |
| <p class="passport-owner">{html_escape(owner_display(artifact))} · {html_escape(artifact['object_guess'])}</p> |
| </div> |
| <div class="passport-share"> |
| <div class="passport-share-actions"> |
| <a class="passport-share-link" href="{html_escape(share_fragment)}">Open scan</a> |
| <a class="passport-share-icon" href="{html_escape(share_url)}" target="_blank" rel="noreferrer" aria-label="Open share link" title="Open share link"><span aria-hidden="true">↗</span></a> |
| </div> |
| <span class="hf-icon">HF</span> |
| <span>scan passport</span> |
| </div> |
| </header> |
| <div class="passport-grid"> |
| <div class="passport-preview"> |
| {preview} |
| <strong>{html_escape(artifact['object_guess'].title())}</strong> |
| <em>{html_escape(compact_text(artifact['memory_text'], 96))}</em> |
| </div> |
| <div class="passport-facts"> |
| <div class="coordinate-cards"> |
| <span><small>X</small><strong>{coords['x']}</strong></span> |
| <span><small>Y</small><strong>{coords['y']}</strong></span> |
| <span><small>Z</small><strong>{coords['z']}</strong></span> |
| <span><small>Item code</small><strong>{item.get('custom_model_data', 0)}</strong></span> |
| </div> |
| <div class="passport-route"> |
| <strong>{html_escape(artifact['hall'])}</strong> |
| <span>{html_escape(artifact['zone'])}</span> |
| <span>Plot {artifact['plot']['x']}, {artifact['plot']['z']}</span> |
| </div> |
| <p><strong>Caption</strong> {html_escape(artifact['memory_text'])}</p> |
| <p><strong>Plaque</strong> {html_escape(artifact['plaque_line'])}</p> |
| <div class="passport-scan"> |
| {qr} |
| <div> |
| <strong>Scan result</strong> |
| <p>Opens a scanned passport panel with this relic's route, exact XYZ, and Minecraft command.</p> |
| <span>{html_escape(share_url)}</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| </section> |
| """ |
|
|
|
|
| def relic_profile_html(artifact: dict) -> str: |
| item = artifact.get("resource_pack_item", {}) |
| coords = artifact["minecraft_coordinates"] |
| share_url = artifact.get("share_url") or artifact_share_url(artifact) |
| share_fragment = artifact_passport_fragment(artifact) |
| command = minecraft_give_command(artifact) |
| return f""" |
| <section class="relic-profile-page" id="relic-profile-{html_escape(artifact['artifact_id'])}"> |
| <header> |
| <div> |
| <span class="profile-kicker">{html_escape(artifact['hall'])} / {html_escape(artifact['zone'])}</span> |
| <h2>{html_escape(artifact['title'])}</h2> |
| <p>{html_escape(owner_display(artifact))} · CMD {item.get('custom_model_data', 0)} · XYZ {coords['x']} {coords['y']} {coords['z']}</p> |
| </div> |
| <div class="profile-share-actions"> |
| <a href="{html_escape(share_fragment)}" class="profile-share">Open scan</a> |
| <a href="{html_escape(share_url)}" target="_blank" rel="noreferrer" class="profile-share-icon" aria-label="Open share link" title="Open share link"><span aria-hidden="true">↗</span></a> |
| </div> |
| </header> |
| <div class="profile-grid"> |
| <article> |
| <h3>History</h3> |
| <p>{html_escape(artifact.get('memory_text', artifact.get('plaque_line', '')))}</p> |
| <p class="profile-lore">{html_escape(artifact.get('spirit_first_line', artifact.get('plaque_line', '')))}</p> |
| </article> |
| <article> |
| <h3>Minecraft Relic</h3> |
| <code>{html_escape(command)}</code> |
| <p>{html_escape(artifact.get('lore_short', artifact.get('placement_reason', '')))}</p> |
| </article> |
| </div> |
| </section> |
| """ |
|
|
|
|
| def floor_map_html(artifact: dict) -> str: |
| cells = [] |
| for z in range(CANVAS_SIZE): |
| for x in range(CANVAS_SIZE): |
| active = x == artifact["plot"]["x"] and z == artifact["plot"]["z"] |
| near = abs(x - artifact["plot"]["x"]) + abs(z - artifact["plot"]["z"]) <= 2 |
| label = "X" if active else "" |
| cells.append( |
| f"<span class='floor-cell {'active' if active else ''} {'near' if near and not active else ''}' " |
| f"title='Plot {x}, {z}'>{label}</span>" |
| ) |
| return f""" |
| <section class="passport-map-card"> |
| <header> |
| <h3>Museum floor grid</h3> |
| <p>Hover the grid to preview selection. The active plot pulses at {artifact['plot']['x']}, {artifact['plot']['z']}.</p> |
| </header> |
| <div class='floor-map'>{''.join(cells)}</div> |
| </section> |
| """ |
|
|
|
|
| def html_escape(value) -> str: |
| return html.escape(str(value or ""), quote=True) |
|
|
|
|
| def owner_display(artifact: dict) -> str: |
| handle = (artifact.get("owner_handle") or "").strip() |
| if handle: |
| return handle |
| return (artifact.get("owner_name") or "anonymous visitor").strip() |
|
|
|
|
| def minecraft_give_command(artifact: dict) -> str: |
| item = artifact.get("resource_pack_item", {}) |
| custom_model_data = item.get("custom_model_data", 0) |
| return f"/give @p minecraft:paper[minecraft:custom_model_data={custom_model_data}] 1" |
|
|
|
|
| def artifact_share_url(artifact: dict) -> str: |
| return f"{PUBLIC_SPACE_URL}/{artifact_passport_fragment(artifact)}" |
|
|
|
|
| def artifact_passport_fragment(artifact: dict) -> str: |
| token = artifact_share_token(artifact) |
| return f"#passport={token}" |
|
|
|
|
| def artifact_share_token(artifact: dict) -> str: |
| item = artifact.get("resource_pack_item", {}) |
| coords = artifact.get("minecraft_coordinates", {}) |
| payload = { |
| "type": "afterblock.passport.v1", |
| "artifact_id": artifact.get("artifact_id", "unknown"), |
| "title": compact_text(artifact.get("title", "AfterBlock Relic"), 72), |
| "owner": compact_text(owner_display(artifact), 32), |
| "object": compact_text(artifact.get("object_guess", "relic"), 32), |
| "hall": compact_text(artifact.get("hall", "AfterBlock Museum"), 42), |
| "zone": compact_text(artifact.get("zone", "Museum route"), 38), |
| "caption": compact_text(artifact.get("memory_text", artifact.get("plaque_line", "")), 180), |
| "command": minecraft_give_command(artifact), |
| "xyz": { |
| "x": coords.get("x", ""), |
| "y": coords.get("y", ""), |
| "z": coords.get("z", ""), |
| }, |
| "cmd": item.get("custom_model_data", 0), |
| } |
| encoded = base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode("utf-8")).decode("ascii") |
| return encoded.rstrip("=") |
|
|
|
|
| def artifact_preview_url(artifact: dict) -> str: |
| item = artifact.get("resource_pack_item", {}) |
| item_id = item.get("id", "missing") |
| if item_id and item_id != "missing": |
| return texture_preview_url(item_id) |
| return "" |
|
|
|
|
| def qr_code_data_uri(payload: str) -> str: |
| try: |
| import qrcode |
| except Exception: |
| return "" |
|
|
| qr = qrcode.QRCode( |
| version=None, |
| error_correction=qrcode.constants.ERROR_CORRECT_M, |
| box_size=7, |
| border=3, |
| ) |
| qr.add_data(payload) |
| qr.make(fit=True) |
| image = qr.make_image(fill_color="#12100c", back_color="#f8edcf").convert("RGB") |
| buffer = BytesIO() |
| image.save(buffer, format="PNG") |
| encoded = base64.b64encode(buffer.getvalue()).decode("ascii") |
| return f"data:image/png;base64,{encoded}" |
|
|
|
|
| def qr_matrix_html(payload: str) -> str: |
| seed = stable_seed(payload) |
| cells = [] |
| for y in range(11): |
| for x in range(11): |
| finder = ( |
| (x < 3 and y < 3) |
| or (x > 7 and y < 3) |
| or (x < 3 and y > 7) |
| ) |
| on = finder or ((seed >> ((x * 11 + y) % 61)) & 1) |
| cells.append(f"<span class='{'on' if on else ''}'></span>") |
| return f"<div class='mini-qr' aria-label='Share QR code'>{''.join(cells)}</div>" |
|
|
|
|
| def coordinates_html(artifact: dict) -> str: |
| coords = artifact["minecraft_coordinates"] |
| plot = artifact["plot"] |
| item = artifact.get("resource_pack_item", {}) |
| return f""" |
| <section class="waypoint-strip"> |
| <div> |
| <span>X</span> |
| <strong>{coords['x']}</strong> |
| </div> |
| <div> |
| <span>Y</span> |
| <strong>{coords['y']}</strong> |
| </div> |
| <div> |
| <span>Z</span> |
| <strong>{coords['z']}</strong> |
| </div> |
| <div> |
| <span>Plot</span> |
| <strong>{plot['x']}, {plot['z']}</strong> |
| </div> |
| <div> |
| <span>CMD</span> |
| <strong>{item.get('custom_model_data', 0)}</strong> |
| </div> |
| </section> |
| """ |
|
|
|
|
| def hall_for_plot_cell(plot_x: int, plot_z: int) -> str: |
| region_x = min(2, max(0, plot_x // 4)) |
| region_z = min(2, max(0, plot_z // 4)) |
| return MUSEUM_HALLS[region_z * 3 + region_x] |
|
|
|
|
| def living_route_map_html(artifact: dict) -> str: |
| coords = artifact["minecraft_coordinates"] |
| plot = artifact["plot"] |
| plot_x = int(plot["x"]) |
| plot_z = int(plot["z"]) |
| entry_col = CANVAS_SIZE // 2 |
| entry_x = GALLERY_ORIGIN_X + ((CANVAS_SIZE - 1) * PLOT_SCALE) // 2 |
| entry_z = GALLERY_ORIGIN_Z - 18 |
| elbow_x = coords["x"] |
| elbow_z = entry_z |
| cells = [] |
| route_min = min(entry_col, plot_x) |
| route_max = max(entry_col, plot_x) |
| for z in range(CANVAS_SIZE): |
| for x in range(CANVAS_SIZE): |
| hall = hall_for_plot_cell(x, z) |
| cell_classes = ["map-cell"] |
| if z == 0 and route_min <= x <= route_max: |
| cell_classes.append("route") |
| if x == plot_x and 0 <= z <= plot_z: |
| cell_classes.append("route") |
| if x == entry_col and z == 0: |
| cell_classes.append("entry") |
| if x == plot_x and z == plot_z: |
| cell_classes.append("target") |
| hall_rgb = hall_color(hall) |
| hall_hex = hex_color(hall_rgb) |
| label = f"{x},{z}" |
| title = "YOU ARE HERE" if "entry" in cell_classes else label |
| if "target" in cell_classes: |
| title = f"{artifact['title']} plot {label}" |
| cells.append( |
| f"<span class='{' '.join(cell_classes)}' style='--hall-color:{hall_hex}' title='{html_escape(title)}'>" |
| f"<b>{html_escape(label)}</b></span>" |
| ) |
| return f""" |
| <section class="living-route-map"> |
| <div class="route-map-copy"> |
| <span>Living map</span> |
| <h4>YOU ARE HERE -> {html_escape(artifact['title'])}</h4> |
| <p>The glowing trail marks the walk from the entry beacon to the relic pad. Hover a square to read its plot; the relic square pulses.</p> |
| </div> |
| <div class="route-map-grid" aria-label="12 by 12 AfterBlock Minecraft plot map"> |
| {''.join(cells)} |
| </div> |
| <div class="route-map-legend"> |
| <span><b class="legend-entry"></b> YOU ARE HERE</span> |
| <span><b class="legend-route"></b> lit route</span> |
| <span><b class="legend-target"></b> relic plot</span> |
| </div> |
| <details class="route-map-proof"> |
| <summary>Exact Minecraft coordinates</summary> |
| <div><span>Entry beacon</span><strong>X {entry_x} Y {GALLERY_ORIGIN_Y} Z {entry_z}</strong></div> |
| <div><span>Route elbow</span><strong>X {elbow_x} Y {GALLERY_ORIGIN_Y} Z {elbow_z}</strong></div> |
| <div><span>Relic pad</span><strong>X {coords['x']} Y {coords['y']} Z {coords['z']}</strong></div> |
| <div><span>Formula</span><strong>{GALLERY_ORIGIN_X} + plot * {PLOT_SCALE}</strong></div> |
| </details> |
| </section> |
| """ |
|
|
|
|
| def minecraft_campus_view_html(artifact: dict) -> str: |
| coords = artifact["minecraft_coordinates"] |
| plot = artifact["plot"] |
| plot_x = int(plot["x"]) |
| plot_z = int(plot["z"]) |
| entry_col = CANVAS_SIZE // 2 |
| entry_x = GALLERY_ORIGIN_X + ((CANVAS_SIZE - 1) * PLOT_SCALE) // 2 |
| entry_z = GALLERY_ORIGIN_Z - 18 |
| tile_w = 52 |
| tile_h = 28 |
| origin_left = 360 |
| origin_top = 26 |
|
|
| def iso_position(x: float, z: float) -> tuple[float, float]: |
| return ( |
| origin_left + (x - z) * (tile_w / 2), |
| origin_top + (x + z) * (tile_h / 2), |
| ) |
|
|
| route_min = min(entry_col, plot_x) |
| route_max = max(entry_col, plot_x) |
| tiles = [] |
| for z in range(CANVAS_SIZE): |
| for x in range(CANVAS_SIZE): |
| hall = hall_for_plot_cell(x, z) |
| classes = ["campus-tile"] |
| if z == 0 and route_min <= x <= route_max: |
| classes.append("route") |
| if x == plot_x and 0 <= z <= plot_z: |
| classes.append("route") |
| if x == plot_x and z == plot_z: |
| classes.append("target") |
| if x == entry_col and z == 0: |
| classes.append("entry-proxy") |
| left, top = iso_position(x, z) |
| label = f"{x},{z}" |
| title = f"{hall} plot {label}" |
| if x == plot_x and z == plot_z: |
| title = f"{artifact['title']} at plot {label}" |
| tiles.append( |
| f"<span class='{' '.join(classes)}' style='left:{left:.1f}px;top:{top:.1f}px;--hall-color:{hex_color(hall_color(hall))}' title='{html_escape(title)}'>" |
| f"<b>{html_escape(label)}</b></span>" |
| ) |
|
|
| gates = [] |
| for index, hall in enumerate(MUSEUM_HALLS): |
| region_x = index % 3 |
| region_z = index // 3 |
| gate_x = region_x * 4 + 1.5 |
| gate_z = region_z * 4 + 1.5 |
| left, top = iso_position(gate_x, gate_z) |
| gates.append( |
| f"<span class='campus-gate' style='left:{left:.1f}px;top:{top - 20:.1f}px;--hall-color:{hex_color(hall_color(hall))}' title='{html_escape(hall)}'>" |
| f"<b>{html_escape(hall.replace('Hall of ', '').replace('Grand ', ''))}</b></span>" |
| ) |
|
|
| entry_left, entry_top = iso_position(entry_col, -1.2) |
| target_left, target_top = iso_position(plot_x, plot_z) |
| return f""" |
| <section class="minecraft-campus-view"> |
| <div class="campus-heading"> |
| <span>In-world view</span> |
| <h4>The museum lights the path for {html_escape(artifact['title'])}</h4> |
| <p>The entry beacon, hall gates, route lights, and relic pad mirror the Paper museum world.</p> |
| </div> |
| <div class="campus-stage" aria-label="Isometric Minecraft-style AfterBlock campus"> |
| <div class="campus-stage-inner"> |
| {''.join(tiles)} |
| {''.join(gates)} |
| <span class="campus-entry-beacon" style="left:{entry_left:.1f}px;top:{entry_top:.1f}px" title="YOU ARE HERE at X {entry_x} Y {GALLERY_ORIGIN_Y} Z {entry_z}"><b>YOU<br>ARE<br>HERE</b></span> |
| <span class="campus-relic-pin" style="left:{target_left:.1f}px;top:{target_top - 42:.1f}px" title="{html_escape(artifact['title'])}"><b>{html_escape(compact_text(artifact['title'], 18))}</b></span> |
| </div> |
| </div> |
| <div class="campus-proof-strip"> |
| <span>You start at X {entry_x} Y {GALLERY_ORIGIN_Y} Z {entry_z}</span> |
| <span>Walk to plot {plot_x},{plot_z}</span> |
| <span>Relic pad X {coords['x']} Y {coords['y']} Z {coords['z']}</span> |
| </div> |
| </section> |
| """ |
|
|
|
|
| def waypointcraft_html(artifact: dict) -> str: |
| coords = artifact["minecraft_coordinates"] |
| item = artifact.get("resource_pack_item", {}) |
| command = minecraft_give_command(artifact) |
| living_map = living_route_map_html(artifact) |
| campus_view = minecraft_campus_view_html(artifact) |
| return f""" |
| <section class="waypoint-card"> |
| <header> |
| <h3>Living Museum Map</h3> |
| <p>{html_escape(artifact['hall'])} / {html_escape(artifact['zone'])}</p> |
| </header> |
| <div class="placement-hero-strip"> |
| <span>YOU ARE HERE</span> |
| <strong>{html_escape(artifact['title'])}</strong> |
| <em>Plot {artifact['plot']['x']}, {artifact['plot']['z']} -> XYZ {coords['x']} {coords['y']} {coords['z']}</em> |
| </div> |
| {campus_view} |
| {living_map} |
| <details class="waypoint-appendix"> |
| <summary>Minecraft proof details</summary> |
| <div class="waypoint-grid"> |
| <div><span>World XYZ</span><strong>{coords['x']} {coords['y']} {coords['z']}</strong></div> |
| <div><span>CustomModelData</span><strong>{item.get('custom_model_data', 0)}</strong></div> |
| <div><span>Base item</span><strong>{html_escape(item.get('recommended_item', 'minecraft:paper'))}</strong></div> |
| <div><span>Model</span><strong>{html_escape(item.get('model', 'afterblock:item/missing'))}</strong></div> |
| </div> |
| <code>{html_escape(command)}</code> |
| </details> |
| </section> |
| """ |
|
|
|
|
| def server_setup_html() -> str: |
| return f""" |
| <section class="server-setup join-minecraft"> |
| <div> |
| <h2>Join Minecraft</h2> |
| <p>No downloads are needed to visit the demo world. Create a relic in the Space, join the server, then place the same relic at its lit plot.</p> |
| </div> |
| <div class="museum-built-card"> |
| <span>World already built</span> |
| <strong>9 halls · 144 relic plots · living entry atlas</strong> |
| <p>The Space map and Paper world share one coordinate contract: plot <code>x,z</code> becomes Minecraft XYZ using the same origin and 32-block plot size.</p> |
| </div> |
| <div class="join-card"> |
| <span>Server address</span> |
| <strong>{html_escape(MINECRAFT_JOIN_ADDRESS)}</strong> |
| <p>{html_escape(MINECRAFT_JOIN_NOTE)}</p> |
| </div> |
| <div class="server-grid"> |
| <article> |
| <span>1</span> |
| <h3>Verify the world</h3> |
| <p>Confirms the museum, living atlas, entry beacon, and plot grid are present.</p> |
| <p><code>/dreamwall museum check</code></p> |
| </article> |
| <article> |
| <span>2</span> |
| <h3>See the object</h3> |
| <p><code>/dreamwall pack</code></p> |
| <p>Loads the hosted object models.</p> |
| </article> |
| <article> |
| <span>3</span> |
| <h3>Place the relic</h3> |
| <p><code>/dreamwall import object | story | @owner</code></p> |
| <p>Paste the command generated after placing a relic in the Space.</p> |
| </article> |
| </div> |
| <details class="server-appendix"> |
| <summary>Owner setup appendix</summary> |
| <p>Only open this if you are installing on a host. The appendix contains the Paper plugin, hosted pack URL, optional prebuilt world, config, and SFTP helper. No panel password is included or stored.</p> |
| <p><code>{html_escape(PEBBLEHOST_SFTP_USER)}@{html_escape(PEBBLEHOST_SFTP_HOST)}:{html_escape(PEBBLEHOST_SFTP_PORT)}</code></p> |
| <div class="server-formula"> |
| <strong>Coordinate contract</strong> |
| <code>world_x = {GALLERY_ORIGIN_X} + plot_x * {PLOT_SCALE}</code> |
| <code>world_z = {GALLERY_ORIGIN_Z} + plot_z * {PLOT_SCALE}</code> |
| <code>world_y = {GALLERY_ORIGIN_Y}</code> |
| </div> |
| </details> |
| </section> |
| """ |
|
|
|
|
| def server_handoff_html(artifact: dict) -> str: |
| coords = artifact["minecraft_coordinates"] |
| item = artifact.get("resource_pack_item", {}) |
| cmd = item.get("custom_model_data", 0) |
| import_command = visitor_import_command(artifact) |
| return f""" |
| <section class="relic-server-handoff"> |
| <div> |
| <span>Run in Minecraft</span> |
| <strong>{html_escape(artifact['title'])}</strong> |
| <p>Ready for Minecraft: paste this visitor command to generate the same object, light the route, engrave the name, and place the relic at plot {artifact['plot']['x']}, {artifact['plot']['z']}.</p> |
| </div> |
| <div class="visitor-command-card"> |
| <span>Visitor command</span> |
| <code>{html_escape(import_command)}</code> |
| <small>No SFTP or downloads for visitors. The server owner installs the bridge once; every visitor can paste their own object/story/signature.</small> |
| </div> |
| <div class="handoff-proof"> |
| <b>XYZ {coords['x']} {coords['y']} {coords['z']}</b> |
| <b>CMD {cmd}</b> |
| </div> |
| </section> |
| """ |
|
|
|
|
| def minecraft_command_part(value: str, fallback: str, limit: int) -> str: |
| cleaned = clean_text(value, fallback).replace("|", "/") |
| return compact_text(cleaned, limit) |
|
|
|
|
| def visitor_import_command(artifact: dict) -> str: |
| prompt = minecraft_command_part(artifact.get("source_prompt", DEFAULT_IMPORT_PROMPT), DEFAULT_IMPORT_PROMPT, 84) |
| story = minecraft_command_part(artifact.get("memory_text", DEFAULT_IMPORT_STORY), DEFAULT_IMPORT_STORY, 112) |
| owner = minecraft_command_part(artifact.get("owner_handle") or artifact.get("owner_name") or DEFAULT_IMPORT_OWNER, DEFAULT_IMPORT_OWNER, 32) |
| return f"/dreamwall import {prompt} | {story} | {owner}" |
|
|
|
|
| def demo_path_html() -> str: |
| return f""" |
| <section class="demo-path"> |
| <header> |
| <h2>Best Hackathon Demo Path</h2> |
| <p>Keep the story simple: the Space is the museum terminal, Minecraft is the persistent place where the memory lives.</p> |
| </header> |
| <div class="demo-path-grid"> |
| <article> |
| <span>What works</span> |
| <ul> |
| <li>One ordinary object becomes a hall, profile, passport, item model, and exact Minecraft coordinate.</li> |
| <li>The same `CustomModelData` appears in the 3D preview, passport, packet, resource pack, and Paper bridge.</li> |
| <li>The server proof is physical: a visitor gets a route compass and walks from `YOU ARE HERE` to the generated plot.</li> |
| </ul> |
| </article> |
| <article> |
| <span>What to avoid</span> |
| <ul> |
| <li>Do not lead with generic AI generation, blockchain, auctions, or a huge feature list.</li> |
| <li>Do not spend the video browsing texture shelves unless a judge asks.</li> |
| <li>Do not describe the spirit as a chatbot; it is the relic profile and museum lore.</li> |
| </ul> |
| </article> |
| </div> |
| <div class="judge-scorecard"> |
| <article> |
| <span>Winning signal</span> |
| <strong>One memory becomes a place</strong> |
| <p>Lead with the Space creating a relic, then prove that the same relic exists at an exact Minecraft coordinate.</p> |
| </article> |
| <article> |
| <span>Risk to cut</span> |
| <strong>Do not demo a feature buffet</strong> |
| <p>Skip auctions, large texture browsing, and abstract spirit talk unless a judge asks. They dilute the Minecraft proof.</p> |
| </article> |
| <article> |
| <span>Judge proof</span> |
| <strong>Walk the route</strong> |
| <p>Show <code>/dreamwall museum check</code>, the route compass, the engraved nameplate, the lectern passport, and the profile button.</p> |
| </article> |
| <article> |
| <span>Why this is hard to copy</span> |
| <strong>Space-to-server contract</strong> |
| <p>The packet, resource pack, server kit, exact XYZ formula, and visible install card all describe the same museum state.</p> |
| </article> |
| </div> |
| <ol class="demo-script"> |
| <li><b>0:00</b> Type one relic and story caption in Place in Museum.</li> |
| <li><b>0:25</b> Show the generated Minecraft-style model, hall, plot, and command.</li> |
| <li><b>0:50</b> Open Living Map, show `YOU ARE HERE`, the glowing route, and the exact plot.</li> |
| <li><b>1:15</b> Open Passport + Profile for the share link, QR scan, relic history, and visitor signature.</li> |
| <li><b>1:35</b> Open Join Minecraft, show that the world is already built, then switch to Minecraft and run the proof commands.</li> |
| <li><b>2:05</b> Show the atlas target, then hold the route compass and follow the lit floor from `YOU ARE HERE` to the relic.</li> |
| <li><b>2:30</b> Show the resource-pack item, read the engraved nameplate, open the lectern passport, then right-click the profile button for the relic history.</li> |
| </ol> |
| <div class="demo-command-strip"> |
| <code>/dreamwall pack</code> |
| <code>/dreamwall museum check</code> |
| <code>/dreamwall import object | story | @owner</code> |
| </div> |
| <p class="demo-closing-line">Closing line: AfterBlock turns the things people would throw away into places they can visit.</p> |
| <div class="demo-proof-note"> |
| <span>Coordinate proof</span> |
| <code>world_x = {GALLERY_ORIGIN_X} + plot_x * {PLOT_SCALE}</code> |
| <code>world_y = {GALLERY_ORIGIN_Y}</code> |
| <code>world_z = {GALLERY_ORIGIN_Z} + plot_z * {PLOT_SCALE}</code> |
| </div> |
| </section> |
| """ |
|
|
|
|
| def clean_text(value: str, fallback: str) -> str: |
| cleaned = " ".join(str(value or "").split()) |
| return cleaned or fallback |
|
|
|
|
| def yaml_value(value: str) -> str: |
| return json.dumps(" ".join(str(value or "").split())) |
|
|
|
|
| def server_kit_text(artifact: dict) -> str: |
| coords = artifact["minecraft_coordinates"] |
| item = artifact.get("resource_pack_item", {}) |
| command = minecraft_give_command(artifact) |
| config = f"""space-url: "{PUBLIC_SPACE_URL}" |
| resource-pack-url: "{RESOURCE_PACK_URL}" |
| resource-pack-sha1: "{RESOURCE_PACK_SHA1}" |
| offer-resource-pack-on-join: false |
| poll-enabled: false |
| poll-seconds: 30 |
| default-import-prompt: {yaml_value(artifact.get("source_prompt", DEFAULT_IMPORT_PROMPT))} |
| default-import-story: {yaml_value(artifact.get("memory_text", DEFAULT_IMPORT_STORY))} |
| default-import-owner: {yaml_value(artifact.get("owner_handle") or artifact.get("owner_name") or DEFAULT_IMPORT_OWNER)} |
| gallery-world: "world" |
| canvas-size: {CANVAS_SIZE} |
| plot-size: {PLOT_SCALE} |
| gallery-origin: |
| x: {GALLERY_ORIGIN_X} |
| y: {GALLERY_ORIGIN_Y} |
| z: {GALLERY_ORIGIN_Z} |
| gallery-facing: "east" |
| """ |
| total_plots = CANVAS_SIZE * CANVAS_SIZE |
| return "\n".join( |
| [ |
| "AFTERBLOCK MINECRAFT SERVER KIT", |
| "", |
| f"Relic: {artifact['title']}", |
| f"Owner: {owner_display(artifact)}", |
| f"Route: {artifact['hall']} / {artifact['zone']}", |
| f"Plot: {artifact['plot']['x']}, {artifact['plot']['z']}", |
| f"XYZ: {coords['x']} {coords['y']} {coords['z']}", |
| f"CustomModelData: {item.get('custom_model_data', 0)}", |
| f"Give command: {command}", |
| "", |
| "1) Upload these files to PebbleHost", |
| "plugins/dreamwall-paper-bridge-0.1.0.jar", |
| "AfterBlockMuseum.zip", |
| "", |
| "2) plugins/DreamWall/config.yml", |
| config.rstrip(), |
| "", |
| "3) First-run in-game commands", |
| "/dreamwall pack", |
| "/dreamwall museum check", |
| "/dreamwall import", |
| "Optional if the prebuilt world was not installed: /dreamwall museum build", |
| "", |
| "4) Expected proof", |
| "AfterBlock museum check:", |
| f"Plot pads: {total_plots}/{total_plots}", |
| f"Relic focus blocks: {total_plots}/{total_plots}", |
| "YOU ARE HERE beacon: present", |
| f"Imported relic lands at XYZ {coords['x']} {coords['y']} {coords['z']} with CMD {item.get('custom_model_data', 0)}, a route compass, an engraved nameplate, a readable passport lectern, and a right-click profile button.", |
| "", |
| "5) Live Space endpoint used by the Paper bridge", |
| f"{PUBLIC_SPACE_URL}/gradio_api/call/quick_curate", |
| "", |
| "6) Bundle checksums", |
| f"dreamwall-paper-bridge-0.1.0.jar sha1={PAPER_PLUGIN_SHA1}", |
| f"AfterBlockMuseum.zip sha1={RESOURCE_PACK_SHA1}", |
| ] |
| ) |
|
|
|
|
| def server_config_kit_text( |
| space_url: str = PUBLIC_SPACE_URL, |
| resource_pack_url: str = RESOURCE_PACK_URL, |
| resource_pack_sha1: str = RESOURCE_PACK_SHA1, |
| gallery_world: str = "world", |
| offer_pack_on_join: bool = False, |
| import_prompt: str = DEFAULT_IMPORT_PROMPT, |
| import_story: str = DEFAULT_IMPORT_STORY, |
| import_owner: str = DEFAULT_IMPORT_OWNER, |
| ) -> str: |
| config, clean_space_url, _, _, _ = server_config_yaml( |
| space_url, |
| resource_pack_url, |
| resource_pack_sha1, |
| gallery_world, |
| offer_pack_on_join, |
| import_prompt, |
| import_story, |
| import_owner, |
| ) |
| return "\n".join( |
| [ |
| "AFTERBLOCK PAPER SERVER CONFIG", |
| "", |
| "Paste this into plugins/DreamWall/config.yml after uploading dreamwall-paper-bridge-0.1.0.jar.", |
| "", |
| config.rstrip(), |
| "", |
| "First-run commands", |
| "/dreamwall pack", |
| "/dreamwall museum check", |
| "/dreamwall import", |
| "Optional if the prebuilt world was not installed: /dreamwall museum build", |
| "", |
| "Default relic imported by /dreamwall import", |
| f"Prompt: {clean_text(import_prompt, DEFAULT_IMPORT_PROMPT)}", |
| f"Story: {clean_text(import_story, DEFAULT_IMPORT_STORY)}", |
| f"Owner: {clean_text(import_owner, DEFAULT_IMPORT_OWNER)}", |
| "", |
| "Coordinate contract kept fixed for judge proof", |
| f"world_x = {GALLERY_ORIGIN_X} + plot_x * {PLOT_SCALE}", |
| f"world_y = {GALLERY_ORIGIN_Y}", |
| f"world_z = {GALLERY_ORIGIN_Z} + plot_z * {PLOT_SCALE}", |
| "", |
| "Live endpoint used by /dreamwall import", |
| f"{clean_space_url}/gradio_api/call/quick_curate", |
| "", |
| "Included downloads", |
| "Paper plugin: server-kit/dreamwall-paper-bridge-0.1.0.jar", |
| "Resource pack: resource-pack/AfterBlockMuseum.zip", |
| "Prebuilt world: server-kit/afterblock-demo-world.zip", |
| "ZIP metadata: afterblock-server-profile.json and afterblock-demo-proof.json", |
| "Upload helper: UPLOAD_TO_SERVER.md and install-afterblock-paper.sh", |
| ] |
| ) |
|
|
|
|
| def server_config_yaml( |
| space_url: str = PUBLIC_SPACE_URL, |
| resource_pack_url: str = RESOURCE_PACK_URL, |
| resource_pack_sha1: str = RESOURCE_PACK_SHA1, |
| gallery_world: str = "world", |
| offer_pack_on_join: bool = False, |
| import_prompt: str = DEFAULT_IMPORT_PROMPT, |
| import_story: str = DEFAULT_IMPORT_STORY, |
| import_owner: str = DEFAULT_IMPORT_OWNER, |
| ) -> tuple[str, str, str, str, str]: |
| clean_space_url = (space_url or PUBLIC_SPACE_URL).strip().rstrip("/") |
| clean_pack_url = (resource_pack_url or RESOURCE_PACK_URL).strip() |
| clean_pack_sha1 = (resource_pack_sha1 or RESOURCE_PACK_SHA1).strip() |
| clean_gallery_world = " ".join(str(gallery_world or "world").split()) or "world" |
| clean_import_prompt = clean_text(import_prompt, DEFAULT_IMPORT_PROMPT) |
| clean_import_story = clean_text(import_story, DEFAULT_IMPORT_STORY) |
| clean_import_owner = clean_text(import_owner, DEFAULT_IMPORT_OWNER) |
| offer_pack = "true" if bool(offer_pack_on_join) else "false" |
| config = f"""space-url: "{clean_space_url}" |
| resource-pack-url: "{clean_pack_url}" |
| resource-pack-sha1: "{clean_pack_sha1}" |
| offer-resource-pack-on-join: {offer_pack} |
| poll-enabled: false |
| poll-seconds: 30 |
| default-import-prompt: {yaml_value(clean_import_prompt)} |
| default-import-story: {yaml_value(clean_import_story)} |
| default-import-owner: {yaml_value(clean_import_owner)} |
| gallery-world: "{clean_gallery_world}" |
| canvas-size: {CANVAS_SIZE} |
| plot-size: {PLOT_SCALE} |
| gallery-origin: |
| x: {GALLERY_ORIGIN_X} |
| y: {GALLERY_ORIGIN_Y} |
| z: {GALLERY_ORIGIN_Z} |
| gallery-facing: "east" |
| """ |
| return config, clean_space_url, clean_pack_url, clean_pack_sha1, clean_gallery_world |
|
|
|
|
| def server_demo_proof_manifest( |
| clean_space_url: str, |
| clean_pack_url: str, |
| clean_pack_sha1: str, |
| clean_gallery_world: str, |
| clean_import_prompt: str, |
| clean_import_story: str, |
| clean_import_owner: str, |
| ) -> dict: |
| return { |
| "type": "afterblock.paper-demo-proof.v1", |
| "space_url": clean_space_url, |
| "live_endpoint": f"{clean_space_url}/gradio_api/call/quick_curate", |
| "default_import": { |
| "prompt": clean_import_prompt, |
| "story": clean_import_story, |
| "owner": clean_import_owner, |
| }, |
| "coordinate_contract": { |
| "world": clean_gallery_world, |
| "canvas_size": CANVAS_SIZE, |
| "plot_scale": PLOT_SCALE, |
| "total_plots": CANVAS_SIZE * CANVAS_SIZE, |
| "origin": { |
| "x": GALLERY_ORIGIN_X, |
| "y": GALLERY_ORIGIN_Y, |
| "z": GALLERY_ORIGIN_Z, |
| }, |
| "formula": "world_x = -192 + plot_x * 32; world_y = 80; world_z = -192 + plot_z * 32", |
| }, |
| "resource_pack": { |
| "url": clean_pack_url, |
| "sha1": clean_pack_sha1, |
| "textures": 3200, |
| "models": 3200, |
| "paper_overrides": 3200, |
| }, |
| "bundle": { |
| "plugin_sha1": PAPER_PLUGIN_SHA1, |
| "plugin_sha256": PAPER_PLUGIN_SHA256, |
| "prebuilt_world_sha1": PREBUILT_WORLD_SHA1, |
| "prebuilt_world_sha256": PREBUILT_WORLD_SHA256, |
| }, |
| "expected_commands": [ |
| "/dreamwall pack", |
| "/dreamwall museum check", |
| "/dreamwall import", |
| ], |
| "expected_proof": [ |
| "museum check reports 144/144 plot pads", |
| "YOU ARE HERE beacon is present", |
| "living entry atlas is present", |
| "route compass points from entry to the generated plot", |
| "entry atlas marks the imported relic plot", |
| "relic appears with an engraved nameplate, lectern passport, and profile button", |
| ], |
| "blocked_external_step": "PebbleHost upload requires the panel/SFTP password outside this ZIP.", |
| } |
|
|
|
|
| def server_owner_profile_manifest( |
| clean_space_url: str, |
| clean_pack_url: str, |
| clean_pack_sha1: str, |
| clean_gallery_world: str, |
| clean_import_prompt: str, |
| clean_import_story: str, |
| clean_import_owner: str, |
| ) -> dict: |
| return { |
| "type": "afterblock.server-profile.v1", |
| "purpose": "Share this profile with a Paper server owner so they can install the same persistent AfterBlock museum configured in the Space.", |
| "space": { |
| "url": clean_space_url, |
| "quick_curate_endpoint": f"{clean_space_url}/gradio_api/call/quick_curate", |
| }, |
| "server": { |
| "minecraft_world": clean_gallery_world, |
| "resource_pack_url": clean_pack_url, |
| "resource_pack_sha1": clean_pack_sha1, |
| "requires_secret": "Only the server panel or SFTP secret is external. No secret is stored in this kit.", |
| }, |
| "default_import": { |
| "prompt": clean_import_prompt, |
| "story": clean_import_story, |
| "owner": clean_import_owner, |
| }, |
| "upload_map": [ |
| { |
| "kit_path": "plugins/dreamwall-paper-bridge-0.1.0.jar", |
| "server_path": "plugins/dreamwall-paper-bridge-0.1.0.jar", |
| "reason": "Paper command bridge for /dreamwall pack, museum check, import, and optional museum rebuild.", |
| }, |
| { |
| "kit_path": "plugins/DreamWall/config.yml", |
| "server_path": "plugins/DreamWall/config.yml", |
| "reason": "Connects the server to this Space, resource pack, fixed coordinate contract, and default relic.", |
| }, |
| { |
| "kit_path": "AfterBlockMuseum.zip", |
| "server_path": "AfterBlockMuseum.zip", |
| "reason": "Minecraft resource pack with 3200 item textures/models and paper overrides.", |
| }, |
| { |
| "kit_path": "afterblock-demo-world.zip", |
| "server_path": "afterblock-demo-world.zip", |
| "reason": "Optional prebuilt Paper world with the verified memory spine, 144 plot pads, YOU ARE HERE beacon, and living entry atlas already built.", |
| }, |
| ], |
| "first_run_commands": [ |
| "/dreamwall pack", |
| "/dreamwall museum check", |
| "/dreamwall import", |
| ], |
| "verification_commands": [ |
| "/dreamwall museum check", |
| "/dreamwall import here", |
| ], |
| "helper_files": [ |
| "UPLOAD_TO_SERVER.md", |
| "install-afterblock-paper.sh", |
| ], |
| "coordinate_contract": { |
| "world": clean_gallery_world, |
| "origin": {"x": GALLERY_ORIGIN_X, "y": GALLERY_ORIGIN_Y, "z": GALLERY_ORIGIN_Z}, |
| "plot_scale": PLOT_SCALE, |
| "canvas_size": CANVAS_SIZE, |
| "formula": "world_x = -192 + plot_x * 32; world_y = 80; world_z = -192 + plot_z * 32", |
| }, |
| } |
|
|
|
|
| def server_owner_install_card_html( |
| space_url: str = PUBLIC_SPACE_URL, |
| resource_pack_url: str = RESOURCE_PACK_URL, |
| resource_pack_sha1: str = RESOURCE_PACK_SHA1, |
| gallery_world: str = "world", |
| offer_pack_on_join: bool = False, |
| import_prompt: str = DEFAULT_IMPORT_PROMPT, |
| import_story: str = DEFAULT_IMPORT_STORY, |
| import_owner: str = DEFAULT_IMPORT_OWNER, |
| ) -> str: |
| _, clean_space_url, clean_pack_url, clean_pack_sha1, clean_gallery_world = server_config_yaml( |
| space_url, |
| resource_pack_url, |
| resource_pack_sha1, |
| gallery_world, |
| offer_pack_on_join, |
| import_prompt, |
| import_story, |
| import_owner, |
| ) |
| clean_import_prompt = clean_text(import_prompt, DEFAULT_IMPORT_PROMPT) |
| clean_import_story = clean_text(import_story, DEFAULT_IMPORT_STORY) |
| clean_import_owner = clean_text(import_owner, DEFAULT_IMPORT_OWNER) |
| profile = server_owner_profile_manifest( |
| clean_space_url, |
| clean_pack_url, |
| clean_pack_sha1, |
| clean_gallery_world, |
| clean_import_prompt, |
| clean_import_story, |
| clean_import_owner, |
| ) |
| upload_rows = "\n".join( |
| f""" |
| <li> |
| <code>{html_escape(row['kit_path'])}</code> |
| <span>{html_escape(row['server_path'])}</span> |
| </li> |
| """ |
| for row in profile["upload_map"] |
| ) |
| commands = "".join(f"<code>{html_escape(command)}</code>" for command in profile["first_run_commands"]) |
| verify = "".join(f"<code>{html_escape(command)}</code>" for command in profile["verification_commands"]) |
| return f""" |
| <section class="server-owner-card"> |
| <header> |
| <div> |
| <span>Server owner install card</span> |
| <h3>{html_escape(clean_gallery_world)} / AfterBlock Museum</h3> |
| </div> |
| <b>{CANVAS_SIZE * CANVAS_SIZE} plots</b> |
| </header> |
| <div class="server-owner-grid"> |
| <article> |
| <span>Space endpoint</span> |
| <code>{html_escape(profile['space']['quick_curate_endpoint'])}</code> |
| </article> |
| <article> |
| <span>Resource pack SHA1</span> |
| <code>{html_escape(clean_pack_sha1)}</code> |
| </article> |
| <article> |
| <span>Default relic</span> |
| <strong>{html_escape(clean_import_prompt)}</strong> |
| <p>{html_escape(clean_import_story)}</p> |
| <em>{html_escape(clean_import_owner)}</em> |
| </article> |
| </div> |
| <div class="server-owner-columns"> |
| <div> |
| <h4>Upload map</h4> |
| <ol>{upload_rows}</ol> |
| </div> |
| <div> |
| <h4>Run in Minecraft</h4> |
| <div class="server-owner-command-strip">{commands}</div> |
| <h4>Verify</h4> |
| <div class="server-owner-command-strip">{verify}</div> |
| </div> |
| </div> |
| <p class="server-owner-note">Appendix ZIP below: it includes this card as <code>afterblock-server-profile.json</code>, plus <code>UPLOAD_TO_SERVER.md</code> and <code>install-afterblock-paper.sh</code>; it does not contain any server panel password or SFTP secret.</p> |
| </section> |
| """ |
|
|
|
|
| def server_upload_guide_md( |
| clean_space_url: str, |
| clean_pack_url: str, |
| clean_pack_sha1: str, |
| clean_gallery_world: str, |
| ) -> str: |
| return f"""# Upload AfterBlock to a Paper Server |
| |
| This public kit is safe to share. It contains the Paper plugin, config, resource pack, demo proof files, and a helper script, but no server secret. |
| |
| ## Upload Map |
| |
| ```text |
| plugins/dreamwall-paper-bridge-0.1.0.jar -> plugins/dreamwall-paper-bridge-0.1.0.jar |
| plugins/DreamWall/config.yml -> plugins/DreamWall/config.yml |
| AfterBlockMuseum.zip -> AfterBlockMuseum.zip |
| afterblock-demo-world.zip -> afterblock-demo-world.zip |
| server.properties.append -> optional server.properties reference |
| ``` |
| |
| The resource pack can also stay hosted here: |
| |
| ```text |
| {clean_pack_url} |
| sha1={clean_pack_sha1} |
| ``` |
| |
| ## File Manager Path |
| |
| If your host has a web file manager, upload the files above, restart Paper, then run the Minecraft commands below as an op player. |
| |
| ## SFTP Helper |
| |
| From this ZIP folder, run: |
| |
| ```bash |
| AFTERBLOCK_SFTP_HOST="{PEBBLEHOST_SFTP_HOST}" \\ |
| AFTERBLOCK_SFTP_PORT="{PEBBLEHOST_SFTP_PORT}" \\ |
| AFTERBLOCK_SFTP_USER="{PEBBLEHOST_SFTP_USER}" \\ |
| sh install-afterblock-paper.sh |
| ``` |
| |
| `AFTERBLOCK_SERVER_ROOT` is optional and defaults to `.`. |
| The SFTP password is typed only into the SFTP prompt from your host panel password. It is not stored in this kit. |
| |
| ## Minecraft First Run |
| |
| ```text |
| /dreamwall pack |
| /dreamwall museum check |
| /dreamwall import |
| ``` |
| |
| Only run `/dreamwall museum build` if the prebuilt demo world was not installed. |
| |
| Fast nearby proof: |
| |
| ```text |
| /dreamwall import here |
| ``` |
| |
| Expected proof in `{clean_gallery_world}`: |
| |
| ```text |
| 144 plot pads |
| YOU ARE HERE beacon |
| route compass from entry to the generated plot |
| engraved nameplate, passport lectern, profile button, and CustomModelData item |
| ``` |
| |
| Space endpoint: |
| |
| ```text |
| {clean_space_url}/gradio_api/call/quick_curate |
| ``` |
| """ |
|
|
|
|
| def server_upload_helper_sh() -> str: |
| return """#!/usr/bin/env sh |
| set -eu |
| |
| ROOT="${AFTERBLOCK_SERVER_ROOT:-.}" |
| HOST="${AFTERBLOCK_SFTP_HOST:-uk144.pebblehost.net}" |
| PORT="${AFTERBLOCK_SFTP_PORT:-2222}" |
| USER_NAME="${AFTERBLOCK_SFTP_USER:-itsarnavsalkade@gmail.com.5ea1f567}" |
| |
| required_files=" |
| plugins/dreamwall-paper-bridge-0.1.0.jar |
| plugins/DreamWall/config.yml |
| AfterBlockMuseum.zip |
| afterblock-demo-world.zip |
| afterblock-demo-proof.json |
| afterblock-server-profile.json |
| server.properties.append |
| " |
| |
| echo "AfterBlock Paper server kit" |
| echo |
| |
| missing=0 |
| for file in $required_files; do |
| if [ ! -e "$file" ]; then |
| echo "Missing: $file" |
| missing=1 |
| fi |
| done |
| |
| if [ "$missing" -ne 0 ]; then |
| echo |
| echo "Run this script from the unzipped afterblock-paper-server-kit folder." |
| exit 1 |
| fi |
| |
| cat <<'MAP' |
| Upload map: |
| plugins/dreamwall-paper-bridge-0.1.0.jar -> plugins/dreamwall-paper-bridge-0.1.0.jar |
| plugins/DreamWall/config.yml -> plugins/DreamWall/config.yml |
| AfterBlockMuseum.zip -> AfterBlockMuseum.zip |
| afterblock-demo-world.zip -> afterblock-demo-world.zip |
| |
| After restart, run in Minecraft: |
| /dreamwall pack |
| /dreamwall museum check |
| /dreamwall import |
| /dreamwall import here |
| |
| Only run /dreamwall museum build if you did not install the prebuilt demo world. |
| MAP |
| |
| if [ -z "$HOST" ] || [ -z "$USER_NAME" ]; then |
| cat <<'ENV' |
| |
| No SFTP target was provided, so nothing was uploaded. |
| To override the prefilled PebbleHost target, set AFTERBLOCK_SFTP_HOST and AFTERBLOCK_SFTP_USER: |
| |
| AFTERBLOCK_SFTP_HOST="other-sftp-host" AFTERBLOCK_SFTP_PORT="2222" AFTERBLOCK_SFTP_USER="other-sftp-user" sh install-afterblock-paper.sh |
| |
| ENV |
| exit 0 |
| fi |
| |
| batch_file="${TMPDIR:-/tmp}/afterblock-sftp-batch.$$" |
| trap 'rm -f "$batch_file"' EXIT |
| |
| cat > "$batch_file" <<EOF |
| -mkdir ${ROOT}/plugins |
| -mkdir ${ROOT}/plugins/DreamWall |
| put plugins/dreamwall-paper-bridge-0.1.0.jar ${ROOT}/plugins/dreamwall-paper-bridge-0.1.0.jar |
| put plugins/DreamWall/config.yml ${ROOT}/plugins/DreamWall/config.yml |
| put AfterBlockMuseum.zip ${ROOT}/AfterBlockMuseum.zip |
| put afterblock-demo-world.zip ${ROOT}/afterblock-demo-world.zip |
| put afterblock-demo-proof.json ${ROOT}/afterblock-demo-proof.json |
| put afterblock-server-profile.json ${ROOT}/afterblock-server-profile.json |
| put server.properties.append ${ROOT}/server.properties.append |
| EOF |
| |
| echo |
| echo "Opening SFTP batch for $USER_NAME@$HOST:$PORT" |
| echo "SFTP will prompt for your panel password; the kit does not store it." |
| sftp -P "$PORT" -b "$batch_file" "$USER_NAME@$HOST" |
| """ |
|
|
|
|
| def server_config_zip( |
| space_url: str = PUBLIC_SPACE_URL, |
| resource_pack_url: str = RESOURCE_PACK_URL, |
| resource_pack_sha1: str = RESOURCE_PACK_SHA1, |
| gallery_world: str = "world", |
| offer_pack_on_join: bool = False, |
| import_prompt: str = DEFAULT_IMPORT_PROMPT, |
| import_story: str = DEFAULT_IMPORT_STORY, |
| import_owner: str = DEFAULT_IMPORT_OWNER, |
| ) -> str: |
| config, clean_space_url, clean_pack_url, clean_pack_sha1, clean_gallery_world = server_config_yaml( |
| space_url, |
| resource_pack_url, |
| resource_pack_sha1, |
| gallery_world, |
| offer_pack_on_join, |
| import_prompt, |
| import_story, |
| import_owner, |
| ) |
| clean_import_prompt = clean_text(import_prompt, DEFAULT_IMPORT_PROMPT) |
| clean_import_story = clean_text(import_story, DEFAULT_IMPORT_STORY) |
| clean_import_owner = clean_text(import_owner, DEFAULT_IMPORT_OWNER) |
| proof_manifest = server_demo_proof_manifest( |
| clean_space_url, |
| clean_pack_url, |
| clean_pack_sha1, |
| clean_gallery_world, |
| clean_import_prompt, |
| clean_import_story, |
| clean_import_owner, |
| ) |
| owner_profile = server_owner_profile_manifest( |
| clean_space_url, |
| clean_pack_url, |
| clean_pack_sha1, |
| clean_gallery_world, |
| clean_import_prompt, |
| clean_import_story, |
| clean_import_owner, |
| ) |
| kit_dir = tempfile.mkdtemp(prefix="afterblock-server-kit-") |
| zip_path = os.path.join(kit_dir, "afterblock-paper-server-kit.zip") |
| readme = f"""# AfterBlock Paper Server Kit |
| |
| This kit configures a Paper server as a persistent AfterBlock Museum for: |
| |
| {clean_space_url} |
| |
| ## Upload |
| |
| This ZIP is self-contained for the server side: |
| |
| 1. Upload `plugins/dreamwall-paper-bridge-0.1.0.jar` to the server's `plugins/` folder. |
| 2. Upload `AfterBlockMuseum.zip` to the server root, or use the hosted pack URL below. |
| 3. Upload `plugins/DreamWall/config.yml` to the same path on the server. |
| 4. Optional: unzip `afterblock-demo-world.zip` into the server root before restart for a prebuilt atlas-ready memory-spine world. It already contains the 144 plot pads, `YOU ARE HERE` beacon, and living entry atlas; run `/dreamwall museum build` later only if you want to refresh it. |
| 5. Restart Paper. |
| |
| ## First Run |
| |
| ```text |
| /dreamwall pack |
| /dreamwall museum check |
| /dreamwall import |
| ``` |
| |
| Only run `/dreamwall museum build` if the prebuilt demo world was not installed. |
| |
| Use `/dreamwall import here` for a fast nearby proof during recording. |
| |
| `/dreamwall import` uses the configured default relic: |
| |
| ```text |
| prompt={clean_import_prompt} |
| story={clean_import_story} |
| owner={clean_import_owner} |
| ``` |
| |
| ## Coordinate Contract |
| |
| ```text |
| world_x = {GALLERY_ORIGIN_X} + plot_x * {PLOT_SCALE} |
| world_y = {GALLERY_ORIGIN_Y} |
| world_z = {GALLERY_ORIGIN_Z} + plot_z * {PLOT_SCALE} |
| world = {clean_gallery_world} |
| ``` |
| |
| ## Resource Pack |
| |
| ```text |
| {clean_pack_url} |
| sha1={clean_pack_sha1} |
| ``` |
| |
| ## Included Files |
| |
| ```text |
| plugins/dreamwall-paper-bridge-0.1.0.jar |
| plugins/DreamWall/config.yml |
| AfterBlockMuseum.zip |
| afterblock-demo-world.zip |
| afterblock-demo-proof.json |
| afterblock-server-profile.json |
| UPLOAD_TO_SERVER.md |
| install-afterblock-paper.sh |
| server.properties.append |
| README.md |
| ``` |
| |
| ## Server Owner Profile |
| |
| `afterblock-server-profile.json` is the shareable install card for a server owner. It includes the configured Space URL, world, resource pack, default relic, upload map, first-run commands, verification commands, and helper file names without storing any panel or SFTP secret. |
| |
| ## Upload Helper |
| |
| `UPLOAD_TO_SERVER.md` gives the file-manager path and the exact first-run Minecraft commands. |
| `install-afterblock-paper.sh` is prefilled for the PebbleHost SFTP target `{PEBBLEHOST_SFTP_USER}@{PEBBLEHOST_SFTP_HOST}:{PEBBLEHOST_SFTP_PORT}` and prompts for the panel password at upload time. No password is stored. |
| |
| ## Demo Proof File |
| |
| `afterblock-demo-proof.json` is included for judges and future installers. It records the Space endpoint, default relic, coordinate contract, expected `/dreamwall museum check` proof, resource-pack counts, checksums, and the one external blocker: PebbleHost upload still needs the panel/SFTP password. |
| |
| ## Checksums |
| |
| ```text |
| dreamwall-paper-bridge-0.1.0.jar sha1={PAPER_PLUGIN_SHA1} |
| dreamwall-paper-bridge-0.1.0.jar sha256={PAPER_PLUGIN_SHA256} |
| AfterBlockMuseum.zip sha1={clean_pack_sha1} |
| afterblock-demo-world.zip sha1={PREBUILT_WORLD_SHA1} |
| afterblock-demo-world.zip sha256={PREBUILT_WORLD_SHA256} |
| ``` |
| """ |
| server_properties = f"""# Optional server.properties lines |
| resource-pack={clean_pack_url} |
| resource-pack-sha1={clean_pack_sha1} |
| """ |
| with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: |
| zf.writestr("plugins/DreamWall/config.yml", config) |
| zf.writestr("README.md", readme) |
| zf.writestr("server.properties.append", server_properties) |
| zf.writestr( |
| "UPLOAD_TO_SERVER.md", |
| server_upload_guide_md( |
| clean_space_url, |
| clean_pack_url, |
| clean_pack_sha1, |
| clean_gallery_world, |
| ), |
| ) |
| zf.writestr("install-afterblock-paper.sh", server_upload_helper_sh()) |
| zf.writestr("afterblock-demo-proof.json", json.dumps(proof_manifest, indent=2) + "\n") |
| zf.writestr("afterblock-server-profile.json", json.dumps(owner_profile, indent=2) + "\n") |
| if os.path.exists(PAPER_PLUGIN_JAR_PATH): |
| zf.write(PAPER_PLUGIN_JAR_PATH, "plugins/dreamwall-paper-bridge-0.1.0.jar") |
| if os.path.exists(RESOURCE_PACK_PATH): |
| zf.write(RESOURCE_PACK_PATH, "AfterBlockMuseum.zip") |
| if os.path.exists(PREBUILT_WORLD_PATH): |
| zf.write(PREBUILT_WORLD_PATH, "afterblock-demo-world.zip") |
| return zip_path |
|
|
|
|
| def server_config_bundle( |
| space_url: str = PUBLIC_SPACE_URL, |
| resource_pack_url: str = RESOURCE_PACK_URL, |
| resource_pack_sha1: str = RESOURCE_PACK_SHA1, |
| gallery_world: str = "world", |
| offer_pack_on_join: bool = False, |
| import_prompt: str = DEFAULT_IMPORT_PROMPT, |
| import_story: str = DEFAULT_IMPORT_STORY, |
| import_owner: str = DEFAULT_IMPORT_OWNER, |
| ) -> tuple[str, str, str]: |
| return ( |
| server_config_kit_text( |
| space_url, |
| resource_pack_url, |
| resource_pack_sha1, |
| gallery_world, |
| offer_pack_on_join, |
| import_prompt, |
| import_story, |
| import_owner, |
| ), |
| server_config_zip( |
| space_url, |
| resource_pack_url, |
| resource_pack_sha1, |
| gallery_world, |
| offer_pack_on_join, |
| import_prompt, |
| import_story, |
| import_owner, |
| ), |
| server_owner_install_card_html( |
| space_url, |
| resource_pack_url, |
| resource_pack_sha1, |
| gallery_world, |
| offer_pack_on_join, |
| import_prompt, |
| import_story, |
| import_owner, |
| ), |
| ) |
|
|
|
|
| def social_tag_for(owner_handle: str) -> str: |
| handle = (owner_handle or "").strip() |
| if handle and not handle.startswith("@"): |
| return f"@{handle}" |
| return handle |
|
|
|
|
| def visitor_identity(signature: str) -> tuple[str, str]: |
| raw = " ".join(str(signature or "").split()) |
| if not raw: |
| return "Visitor", "" |
| handle = next((part for part in raw.split() if part.startswith("@") and len(part) > 1), "") |
| if handle: |
| owner_name = raw.replace(handle, "").strip() or handle[1:] |
| return owner_name, social_tag_for(handle) |
| return raw, "" |
|
|
|
|
| def compact_text(value: str, limit: int = 34) -> str: |
| value = " ".join(str(value or "").split()) |
| if len(value) <= limit: |
| return value |
| return value[: max(0, limit - 1)].rstrip() + "…" |
|
|
|
|
| def demo_collection_artifact(index: int) -> dict: |
| base = OBJECT_LIBRARY[index % len(OBJECT_LIBRARY)] |
| name, input_kind, prompt, memory, object_kind = base |
| handle = SOCIAL_HANDLES[index % len(SOCIAL_HANDLES)] |
| variant = index // len(OBJECT_LIBRARY) |
| owner = handle.strip("@").replace("_", " ").title() |
| suffixes = ["launch", "rain", "exam", "midnight", "first-room"] |
| source_prompt = f"{prompt}, {suffixes[variant % len(suffixes)]} variant {variant + 1}" |
| memory_text = f"{memory} Visitor echo #{index + 1}." |
| artifact = build_museum_artifact(owner, handle, input_kind, source_prompt, memory_text) |
| artifact["catalog_index"] = index + 1 |
| artifact["object_kind"] = object_kind |
| artifact["texture_path"] = f"assets/afterblock_textures/items/{object_kind.replace(' ', '_')}_{index + 1:04d}.png" |
| artifact["resource_pack_item"] = resource_pack_item_for(object_kind, index) |
| return artifact |
|
|
|
|
| def texture_key_for(object_guess: str) -> str: |
| key = object_guess.replace(" ", "_") |
| aliases = { |
| "prompted_painting": "painting", |
| "personal_relic": "notebook", |
| "animal_spirit": "toy_car", |
| "school_bag": "school_bag", |
| "water_bottle": "water_bottle", |
| "usb_drive": "usb_drive", |
| "phone": "phone", |
| "plush_toy": "plush_toy", |
| "sticker": "sticker", |
| "shoes": "shoes", |
| "controller": "game_controller", |
| "xbox_controller": "game_controller", |
| } |
| return aliases.get(key, key) |
|
|
|
|
| def resource_manifest() -> list[dict]: |
| path = "assets/afterblock_textures/afterblock_manifest.json" |
| try: |
| with open(path, "r", encoding="utf-8") as handle: |
| return json.load(handle) |
| except OSError: |
| return [] |
|
|
|
|
| def resource_pack_stats() -> dict: |
| manifest = resource_manifest() |
| kinds = {item.get("kind", "unknown") for item in manifest} |
| shapes = {item.get("shape", "unknown") for item in manifest} |
| profiles = {item.get("model_profile", "legacy") for item in manifest} |
| materials = {item.get("material", "untextured") for item in manifest} |
| return { |
| "total_items": len(manifest), |
| "total_kinds": len(kinds), |
| "total_shapes": len(shapes), |
| "total_model_profiles": len(profiles), |
| "total_materials": len(materials), |
| "page_count": max(1, math.ceil(len(manifest) / TEXTURE_PAGE_SIZE)), |
| } |
|
|
|
|
| def resource_pack_item_for(object_guess: str, seed_hint: int = 0) -> dict: |
| key = texture_key_for(object_guess) |
| manifest = resource_manifest() |
| matches = [item for item in manifest if item.get("kind") == key] |
| if not matches: |
| matches = [item for item in manifest if item.get("kind") == "notebook"] or manifest |
| if not matches: |
| return { |
| "id": "missing", |
| "custom_model_data": 0, |
| "texture": "", |
| "model": "", |
| "recommended_item": "minecraft:paper", |
| } |
| return matches[seed_hint % len(matches)] |
|
|
|
|
| def texture_kind_choices() -> list[str]: |
| kinds = sorted({item.get("kind", "unknown") for item in resource_manifest()}) |
| return ["all"] + kinds |
|
|
|
|
| def texture_preview_url(item_id: str) -> str: |
| base = os.getenv( |
| "TEXTURE_PREVIEW_BASE_URL", |
| "https://raw.githubusercontent.com/Arnie016/dreamwall-mc/main/assets/afterblock_textures/gallery/previews", |
| ).rstrip("/") |
| if base: |
| return f"{base}/{item_id}.png" |
| return f"assets/afterblock_textures/gallery/previews/{item_id}.png" |
|
|
|
|
| def block_rgb(block_name: str) -> tuple[int, int, int]: |
| return next((rgb for name, rgb in BLOCKS if name == block_name), (180, 156, 104)) |
|
|
|
|
| def hex_color(rgb: tuple[int, int, int]) -> str: |
| return "#{:02x}{:02x}{:02x}".format(*rgb) |
|
|
|
|
| def artifact_model_html(artifact: dict) -> str: |
| item = artifact.get("resource_pack_item", {}) |
| item_id = item.get("id", "missing") |
| hall_rgb = hall_color(artifact["hall"]) |
| palette = [hex_color(block_rgb(name)) for name in artifact.get("palette", [])[:5]] |
| if not palette: |
| palette = [hex_color(hall_rgb), "#e8c56f", "#3b3120"] |
| payload = { |
| "title": artifact["title"], |
| "owner": owner_display(artifact), |
| "hall": artifact["hall"], |
| "when": artifact.get("artifact_when", artifact["placement_reason"]), |
| "coordinates": artifact["minecraft_coordinates"], |
| "custom_model_data": item.get("custom_model_data", 0), |
| "model": item.get("model", "afterblock:item/missing"), |
| "kind": item.get("kind", texture_key_for(artifact["object_guess"])), |
| "object_guess": artifact["object_guess"], |
| "shape": item.get("shape", artifact["object_guess"]), |
| "material": item.get("material", "museum material"), |
| "finish": item.get("finish", "artifact finish"), |
| "profile": item.get("model_profile", "fixed pedestal"), |
| "orientation": item.get("orientation", "front-facing"), |
| "element_count": int(item.get("element_count") or 7), |
| "preview_url": texture_preview_url(item_id) if item_id != "missing" else "", |
| "colors": palette, |
| "hall_color": hex_color(hall_rgb), |
| "story_caption": artifact.get("memory_text", artifact.get("plaque_line", "")), |
| "social_tag": artifact.get("social_tag", owner_display(artifact)), |
| "give_command": f"/give @p minecraft:paper[minecraft:custom_model_data={item.get('custom_model_data', 0)}] 1", |
| } |
| payload_json = json.dumps(payload) |
| fallback_swatches = "".join( |
| f"<span style='background:{html_escape(color)}'></span>" for color in payload["colors"] |
| ) |
| srcdoc = f""" |
| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <style> |
| html, body {{ margin:0; height:100%; background:#10110f; color:#f8e5b6; font-family: Inter, ui-sans-serif, system-ui, sans-serif; overflow:hidden; }} |
| #stage {{ position:absolute; inset:0; cursor:grab; touch-action:none; }} |
| #stage.dragging {{ cursor:grabbing; }} |
| .hud {{ position:absolute; left:18px; right:18px; bottom:16px; display:grid; grid-template-columns:1fr auto; gap:12px; align-items:end; pointer-events:none; }} |
| .label {{ background:rgba(18,15,10,.78); border:1px solid #8c6a38; padding:12px 14px; box-shadow:0 6px 0 rgba(0,0,0,.38); }} |
| h2 {{ margin:0 0 4px; font-size:18px; color:#ffe4a3; letter-spacing:.01em; }} |
| p {{ margin:3px 0; font-size:12px; line-height:1.35; color:#d8c8a4; }} |
| code {{ color:#20160c; background:#e8c56f; padding:4px 6px; display:inline-block; margin-top:4px; font-size:11px; }} |
| .badge {{ border:1px solid #7d6335; color:#ffe4a3; padding:8px 10px; background:rgba(28,24,18,.8); font-weight:800; font-size:12px; }} |
| .fallback {{ position:absolute; inset:0; display:grid; place-items:center; background:radial-gradient(circle at 50% 35%, rgba(220,172,88,.16), transparent 42%), #10110f; }} |
| .fallback-card {{ width:150px; height:150px; transform:rotateX(58deg) rotateZ(-28deg); background:linear-gradient(135deg, {html_escape(payload['hall_color'])}, #ffe3a1); border:8px solid #1d160d; box-shadow:34px 28px 0 rgba(0,0,0,.34); }} |
| .swatches {{ display:flex; gap:4px; margin-top:7px; }} |
| .swatches span {{ width:16px; height:16px; border:1px solid #0e0c09; }} |
| </style> |
| </head> |
| <body> |
| <div id="stage"></div> |
| <div id="fallback" class="fallback"><div><div class="fallback-card"></div><div class="swatches">{fallback_swatches}</div></div></div> |
| <div class="hud"> |
| <div class="label"> |
| <h2 id="title"></h2> |
| <p id="meta"></p> |
| <p id="when"></p> |
| <code id="cmd"></code> |
| </div> |
| <div class="badge" id="badge"></div> |
| </div> |
| <script type="module"> |
| import * as THREE from 'https://esm.sh/three@0.160.0'; |
| const artifact = {payload_json}; |
| document.getElementById('title').textContent = artifact.title; |
| document.getElementById('meta').textContent = `${{artifact.owner}} · ${{artifact.hall}} · ${{artifact.material}} · ${{artifact.profile}}`; |
| document.getElementById('when').textContent = `When: ${{artifact.when}} · XYZ ${{artifact.coordinates.x}} ${{artifact.coordinates.y}} ${{artifact.coordinates.z}}`; |
| document.getElementById('cmd').textContent = artifact.give_command; |
| document.getElementById('badge').textContent = `CMD ${{artifact.custom_model_data}}`; |
| |
| const stage = document.getElementById('stage'); |
| const scene = new THREE.Scene(); |
| scene.background = new THREE.Color('#0d0f0e'); |
| const camera = new THREE.PerspectiveCamera(38, stage.clientWidth / stage.clientHeight, 0.1, 100); |
| camera.position.set(3.9, 3.1, 5.1); |
| camera.lookAt(0, .7, 0); |
| let cameraDistance = 5.1; |
| |
| const renderer = new THREE.WebGLRenderer({{ antialias: true, alpha: true }}); |
| renderer.setSize(stage.clientWidth, stage.clientHeight); |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); |
| renderer.shadowMap.enabled = true; |
| stage.appendChild(renderer.domElement); |
| document.getElementById('fallback').style.display = 'none'; |
| |
| scene.add(new THREE.HemisphereLight('#fff1c8', '#15120d', 2.1)); |
| const key = new THREE.DirectionalLight('#ffe0a3', 2.4); |
| key.position.set(3, 5, 4); |
| key.castShadow = true; |
| scene.add(key); |
| const rim = new THREE.PointLight(artifact.hall_color, 12, 7); |
| rim.position.set(-2.2, 2.5, -1.4); |
| scene.add(rim); |
| |
| const group = new THREE.Group(); |
| scene.add(group); |
| const hallGroup = new THREE.Group(); |
| scene.add(hallGroup); |
| const colors = artifact.colors.length ? artifact.colors : [artifact.hall_color, '#e8c56f']; |
| const materialFor = (i) => new THREE.MeshStandardMaterial({{ |
| color: new THREE.Color(colors[i % colors.length]), |
| roughness: .72, |
| metalness: artifact.material.includes('metal') || artifact.material.includes('brass') ? .35 : .05, |
| emissive: artifact.material.includes('glow') || artifact.finish.includes('glow') ? new THREE.Color(colors[i % colors.length]).multiplyScalar(.18) : new THREE.Color('#000000') |
| }}); |
| const dark = new THREE.MeshStandardMaterial({{ color:'#17120b', roughness:.9 }}); |
| const makeMat = (color, options = {{}}) => new THREE.MeshStandardMaterial({{ |
| color, |
| roughness: options.roughness ?? .68, |
| metalness: options.metalness ?? 0, |
| emissive: options.emissive ? new THREE.Color(options.emissive).multiplyScalar(options.emissiveStrength ?? .16) : new THREE.Color('#000000') |
| }}); |
| |
| function cube(x, y, z, sx, sy, sz, mat) {{ |
| const mesh = new THREE.Mesh(new THREE.BoxGeometry(sx, sy, sz), mat); |
| mesh.position.set(x, y, z); |
| mesh.castShadow = true; |
| mesh.receiveShadow = true; |
| group.add(mesh); |
| return mesh; |
| }} |
| |
| function hallCube(x, y, z, sx, sy, sz, mat) {{ |
| const mesh = new THREE.Mesh(new THREE.BoxGeometry(sx, sy, sz), mat); |
| mesh.position.set(x, y, z); |
| mesh.castShadow = true; |
| mesh.receiveShadow = true; |
| hallGroup.add(mesh); |
| return mesh; |
| }} |
| |
| function buildMuseumHall() {{ |
| const floor = makeMat('#211d16', {{ roughness: .88 }}); |
| const tileA = makeMat('#31291d', {{ roughness: .86 }}); |
| const tileB = makeMat('#171410', {{ roughness: .9 }}); |
| const wall = makeMat('#2a2118', {{ roughness: .92 }}); |
| const trim = makeMat('#7a5729', {{ roughness: .72, metalness: .08 }}); |
| const portalDark = makeMat('#0a0908', {{ roughness: .95, emissive: '#1d1234', emissiveStrength: .08 }}); |
| const lamp = makeMat('#ffd47d', {{ roughness: .34, emissive: '#ffb84a', emissiveStrength: .55 }}); |
| const blueHall = makeMat('#315f87', {{ roughness: .72, emissive: '#1a5f86', emissiveStrength: .08 }}); |
| const greenHall = makeMat('#335f3a', {{ roughness: .72, emissive: '#2c6d39', emissiveStrength: .08 }}); |
| const violetHall = makeMat('#442264', {{ roughness: .72, emissive: '#4b1689', emissiveStrength: .1 }}); |
| |
| hallCube(0, -.08, .15, 7.6, .14, 5.9, floor); |
| for (let x = -3; x <= 3; x++) {{ |
| for (let z = -2; z <= 2; z++) {{ |
| hallCube(x * .92, -.005, z * .82, .82, .035, .72, (x + z) % 2 ? tileA : tileB); |
| }} |
| }} |
| |
| hallCube(0, 1.25, -2.62, 7.6, 2.55, .22, wall); |
| hallCube(-3.86, 1.0, .1, .22, 2.0, 5.7, wall); |
| hallCube(3.86, 1.0, .1, .22, 2.0, 5.7, wall); |
| hallCube(0, .04, -2.02, 5.8, .09, .18, trim); |
| hallCube(0, .04, 2.44, 5.6, .09, .18, trim); |
| |
| const portals = [ |
| [-2.35, -2.48, 'Worlds', blueHall], |
| [0, -2.49, 'Companions', greenHall], |
| [2.35, -2.48, 'Turning', violetHall], |
| ]; |
| portals.forEach(([x, z, name, mat], index) => {{ |
| hallCube(x, .92, z, .88, 1.45, .18, portalDark); |
| hallCube(x - .54, .92, z + .03, .16, 1.55, .24, trim); |
| hallCube(x + .54, .92, z + .03, .16, 1.55, .24, trim); |
| hallCube(x, 1.68, z + .03, 1.24, .18, .24, trim); |
| hallCube(x, 1.92, z + .08, 1.04, .22, .16, mat); |
| hallCube(x - .72, 1.36, z + .12, .12, .32, .12, lamp); |
| hallCube(x + .72, 1.36, z + .12, .12, .32, .12, lamp); |
| }}); |
| |
| const sideCases = [ |
| [-2.75, .42, .95, '#6b3a2f'], |
| [2.75, .42, .95, '#365476'], |
| [-2.78, .42, 1.9, '#6a5630'], |
| [2.78, .42, 1.9, '#4b6537'], |
| ]; |
| sideCases.forEach(([x, y, z, color], i) => {{ |
| hallCube(x, .2, z, .88, .25, .68, trim); |
| hallCube(x, .65, z, .42, .46, .28, makeMat(color, {{ roughness: .62, emissive: color, emissiveStrength: .04 }})); |
| hallCube(x, 1.02, z, .72, .1, .38, lamp); |
| }}); |
| |
| hallCube(0, .05, 0, 3.0, .1, 2.18, dark); |
| hallCube(0, .23, 0, 2.2, .24, 1.5, new THREE.MeshStandardMaterial({{ color:'#4a371d', roughness:.85 }})); |
| hallCube(0, .43, -.83, 1.35, .16, .2, trim); |
| hallCube(0, .78, -.88, 1.1, .58, .12, makeMat('#1a130c', {{ roughness: .8 }})); |
| }} |
| |
| buildMuseumHall(); |
| |
| function labelTexture(title, caption) {{ |
| const canvas = document.createElement('canvas'); |
| canvas.width = 512; |
| canvas.height = 192; |
| const ctx = canvas.getContext('2d'); |
| ctx.fillStyle = '#1a1209'; |
| ctx.fillRect(0, 0, canvas.width, canvas.height); |
| ctx.strokeStyle = '#c99b4e'; |
| ctx.lineWidth = 10; |
| ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20); |
| ctx.fillStyle = '#ffe4a3'; |
| ctx.font = 'bold 32px Inter, system-ui, sans-serif'; |
| ctx.fillText(String(title || 'Relic').slice(0, 24), 34, 58); |
| ctx.fillStyle = '#e7d5ad'; |
| ctx.font = '22px ui-monospace, SFMono-Regular, Menlo, monospace'; |
| const words = String(caption || '').split(/\\s+/).slice(0, 18); |
| let line = ''; |
| let y = 96; |
| words.forEach((word) => {{ |
| const test = line ? `${{line}} ${{word}}` : word; |
| if (ctx.measureText(test).width > 440) {{ |
| ctx.fillText(line, 34, y); |
| line = word; |
| y += 30; |
| }} else {{ |
| line = test; |
| }} |
| }}); |
| if (line) ctx.fillText(line, 34, y); |
| const texture = new THREE.CanvasTexture(canvas); |
| texture.colorSpace = THREE.SRGBColorSpace; |
| return texture; |
| }} |
| |
| function buildEngravedPlaque() {{ |
| const plaqueTexture = labelTexture(artifact.title, artifact.story_caption); |
| const plaque = new THREE.Mesh( |
| new THREE.PlaneGeometry(1.34, .5), |
| new THREE.MeshBasicMaterial({{ map: plaqueTexture, transparent: false }}) |
| ); |
| plaque.position.set(0, .86, -.955); |
| plaque.rotation.x = 0; |
| hallGroup.add(plaque); |
| }} |
| |
| buildEngravedPlaque(); |
| |
| const shapeText = `${{artifact.kind}} ${{artifact.shape}} ${{artifact.object_guess}} ${{artifact.title}}`.toLowerCase(); |
| const ivory = makeMat('#f5efdc', {{ roughness: .42 }}); |
| const porcelain = makeMat('#fff9e8', {{ roughness: .35 }}); |
| const blueGlow = makeMat('#6fd3ff', {{ roughness: .28, emissive: '#6fd3ff', emissiveStrength: .18 }}); |
| const graphite = makeMat('#171a1c', {{ roughness: .72 }}); |
| const screenBlue = makeMat('#1d8fd1', {{ roughness: .25, emissive: '#1b6fa8', emissiveStrength: .18 }}); |
| const rubber = makeMat('#2b2722', {{ roughness: .9 }}); |
| |
| function renderEarbuds() {{ |
| cube(-.36, .72, -.02, .15, .72, .14, ivory); |
| cube(.36, .72, -.02, .15, .72, .14, ivory); |
| cube(-.36, 1.16, -.04, .32, .25, .24, porcelain); |
| cube(.36, 1.16, -.04, .32, .25, .24, porcelain); |
| cube(-.47, 1.18, -.18, .1, .07, .05, graphite); |
| cube(.25, 1.18, -.18, .1, .07, .05, graphite); |
| cube(-.36, .39, .08, .1, .13, .18, blueGlow); |
| cube(.36, .39, .08, .1, .13, .18, blueGlow); |
| }} |
| |
| function renderMonitor() {{ |
| cube(0, 1.05, -.02, 1.56, .9, .14, graphite); |
| cube(0, 1.05, -.11, 1.38, .72, .08, screenBlue); |
| cube(0, .5, .02, .18, .55, .18, graphite); |
| cube(0, .28, .02, .84, .14, .44, graphite); |
| }} |
| |
| function renderClock() {{ |
| cube(0, .92, -.02, .96, .96, .16, ivory); |
| cube(0, .92, -.13, .78, .78, .06, porcelain); |
| cube(0, .92, -.18, .07, .36, .05, graphite); |
| cube(.16, .82, -.18, .32, .07, .05, graphite); |
| cube(-.28, 1.46, -.02, .18, .18, .16, blueGlow); |
| cube(.28, 1.46, -.02, .18, .18, .16, blueGlow); |
| cube(0, .34, .02, .82, .16, .28, rubber); |
| }} |
| |
| function renderShoes() {{ |
| cube(-.38, .45, -.02, .62, .22, .42, rubber); |
| cube(.38, .45, -.02, .62, .22, .42, rubber); |
| cube(-.33, .62, -.04, .48, .28, .32, materialFor(0)); |
| cube(.43, .62, -.04, .48, .28, .32, materialFor(1)); |
| cube(-.1, .74, -.18, .28, .08, .08, ivory); |
| cube(.66, .74, -.18, .28, .08, .08, ivory); |
| }} |
| |
| function renderRemote() {{ |
| cube(0, .83, -.02, .48, 1.38, .18, graphite); |
| cube(0, 1.38, -.14, .18, .14, .06, makeMat('#e85b4d', {{ roughness: .48 }})); |
| for (let r = 0; r < 4; r++) {{ |
| for (let c = 0; c < 2; c++) {{ |
| cube((c - .5) * .18, 1.08 - r * .18, -.14, .1, .08, .06, c === 0 ? blueGlow : ivory); |
| }} |
| }} |
| cube(0, .38, -.14, .28, .16, .06, makeMat('#70685a', {{ roughness: .72 }})); |
| }} |
| |
| function renderController() {{ |
| const shell = makeMat('#20242b', {{ roughness: .68, metalness: .08 }}); |
| const grip = makeMat('#13161b', {{ roughness: .84 }}); |
| const seamMat = makeMat('#58606b', {{ roughness: .5, metalness: .12 }}); |
| const stick = makeMat('#0b0d10', {{ roughness: .9 }}); |
| const buttonA = makeMat('#6fe08e', {{ roughness: .36, emissive: '#34ad56', emissiveStrength: .12 }}); |
| const buttonB = makeMat('#e85d53', {{ roughness: .36, emissive: '#b9322d', emissiveStrength: .1 }}); |
| const buttonX = makeMat('#5db4ff', {{ roughness: .34, emissive: '#2279c4', emissiveStrength: .12 }}); |
| const buttonY = makeMat('#f2d463', {{ roughness: .34, emissive: '#b9941f', emissiveStrength: .1 }}); |
| const body = cube(0, .88, -.02, 1.42, .62, .22, shell); |
| body.rotation.z = -.02; |
| const leftGrip = cube(-.56, .52, .02, .34, .56, .28, grip); |
| const rightGrip = cube(.56, .52, .02, .34, .56, .28, grip); |
| leftGrip.rotation.z = .24; |
| rightGrip.rotation.z = -.24; |
| cube(0, 1.08, -.16, .38, .16, .06, seamMat); |
| cube(-.38, .93, -.18, .24, .24, .08, stick); |
| cube(.2, .76, -.18, .24, .24, .08, stick); |
| cube(-.38, .93, -.24, .14, .14, .08, makeMat('#2c323a', {{ roughness: .88 }})); |
| cube(.2, .76, -.24, .14, .14, .08, makeMat('#2c323a', {{ roughness: .88 }})); |
| cube(-.08, .76, -.2, .26, .08, .06, seamMat); |
| cube(-.08, .76, -.2, .08, .26, .06, seamMat); |
| cube(.55, 1.0, -.19, .13, .13, .07, buttonY); |
| cube(.68, .88, -.19, .13, .13, .07, buttonB); |
| cube(.42, .88, -.19, .13, .13, .07, buttonX); |
| cube(.55, .76, -.19, .13, .13, .07, buttonA); |
| cube(-.48, 1.27, -.02, .34, .12, .18, seamMat); |
| cube(.48, 1.27, -.02, .34, .12, .18, seamMat); |
| cube(0, .96, -.2, .1, .1, .06, blueGlow); |
| }} |
| |
| function renderBag() {{ |
| const body = makeMat('#334f86', {{ roughness: .82 }}); |
| const panel = makeMat('#5f78b8', {{ roughness: .72 }}); |
| const strap = makeMat('#1a2131', {{ roughness: .9 }}); |
| const zip = makeMat('#f3d77d', {{ roughness: .45, metalness: .12 }}); |
| cube(0, .86, -.02, 1.0, 1.08, .42, body); |
| cube(0, .9, -.26, .74, .74, .1, panel); |
| cube(-.38, 1.42, -.04, .16, .38, .18, strap); |
| cube(.38, 1.42, -.04, .16, .38, .18, strap); |
| cube(0, 1.36, -.29, .56, .08, .06, zip); |
| cube(0, .54, -.31, .5, .14, .08, makeMat('#28375b', {{ roughness: .8 }})); |
| cube(-.57, .86, .02, .12, .88, .3, strap); |
| cube(.57, .86, .02, .12, .88, .3, strap); |
| }} |
| |
| function renderPhone() {{ |
| const shell = makeMat('#14181d', {{ roughness: .54, metalness: .22 }}); |
| const glass = makeMat('#102431', {{ roughness: .18, metalness: .08, emissive: '#2fb6ff', emissiveStrength: .16 }}); |
| const bezel = makeMat('#0a0b0d', {{ roughness: .72 }}); |
| const appA = makeMat('#6fd3ff', {{ roughness: .3, emissive: '#40b9ff', emissiveStrength: .2 }}); |
| const appB = makeMat('#f4d76f', {{ roughness: .36, emissive: '#f4c44a', emissiveStrength: .09 }}); |
| const appC = makeMat('#7fe0a3', {{ roughness: .36, emissive: '#42c36f', emissiveStrength: .1 }}); |
| const appD = makeMat('#f06f6f', {{ roughness: .36, emissive: '#cf4a4a', emissiveStrength: .08 }}); |
| const frame = cube(0, .94, -.02, .82, 1.42, .16, shell); |
| const screen = cube(0, .94, -.135, .68, 1.2, .055, glass); |
| frame.rotation.z = -.03; |
| screen.rotation.z = -.03; |
| cube(0, 1.58, -.18, .22, .035, .04, porcelain).rotation.z = -.03; |
| cube(.28, 1.43, -.19, .06, .06, .04, blueGlow).rotation.z = -.03; |
| cube(-.26, .37, -.19, .22, .045, .04, bezel).rotation.z = -.03; |
| cube(-.44, 1.05, -.02, .035, .34, .06, shell).rotation.z = -.03; |
| cube(.44, .92, -.02, .035, .24, .06, shell).rotation.z = -.03; |
| const apps = [appA, appB, appC, appD, appC, appA]; |
| apps.forEach((mat, i) => {{ |
| const col = i % 2; |
| const row = Math.floor(i / 2); |
| const icon = cube((col - .5) * .28, 1.18 - row * .24, -.19, .16, .13, .038, mat); |
| icon.rotation.z = -.03; |
| }}); |
| const shine = cube(-.2, 1.1, -.205, .055, .76, .03, makeMat('#d8f4ff', {{ roughness: .16, emissive: '#d8f4ff', emissiveStrength: .08 }})); |
| shine.rotation.z = -.22; |
| }} |
| |
| function renderWaterBottle() {{ |
| const bottleGlass = new THREE.MeshStandardMaterial({{ |
| color: '#7fd9f4', |
| roughness: .18, |
| metalness: .02, |
| transparent: true, |
| opacity: .72, |
| emissive: new THREE.Color('#2f91bb').multiplyScalar(.08) |
| }}); |
| const label = makeMat('#f4f0de', {{ roughness: .38 }}); |
| const cap = makeMat('#2f6f95', {{ roughness: .5, metalness: .04 }}); |
| const water = makeMat('#2da8d8', {{ roughness: .22, emissive: '#0b83b4', emissiveStrength: .12 }}); |
| const seam = makeMat('#d9fbff', {{ roughness: .18, emissive: '#bdf3ff', emissiveStrength: .08 }}); |
| cube(0, .62, 0, .5, .44, .38, bottleGlass); |
| cube(0, .96, 0, .58, .48, .44, bottleGlass); |
| cube(0, 1.3, 0, .46, .3, .36, bottleGlass); |
| cube(0, .74, -.24, .48, .28, .04, water); |
| cube(0, 1.0, -.27, .62, .25, .05, label); |
| cube(-.19, 1.01, -.305, .06, .16, .035, cap); |
| cube(.19, 1.01, -.305, .06, .16, .035, cap); |
| cube(0, 1.53, 0, .28, .24, .28, bottleGlass); |
| cube(0, 1.72, 0, .38, .16, .38, cap); |
| cube(-.31, 1.0, -.02, .045, 1.02, .05, seam); |
| cube(.31, 1.0, -.02, .045, 1.02, .05, seam); |
| cube(.16, .42, -.23, .08, .1, .04, blueGlow); |
| }} |
| |
| function renderPlushToy() {{ |
| const fur = makeMat('#9a6a43', {{ roughness: .92 }}); |
| const belly = makeMat('#d3ad78', {{ roughness: .88 }}); |
| const face = makeMat('#f1c98b', {{ roughness: .88 }}); |
| const innerEar = makeMat('#e8bd82', {{ roughness: .86 }}); |
| const bow = makeMat('#b83a31', {{ roughness: .55, emissive: '#7d1712', emissiveStrength: .05 }}); |
| cube(0, .7, 0, .74, .76, .5, fur); |
| cube(0, .68, -.29, .42, .42, .05, belly); |
| cube(0, 1.23, -.02, .62, .55, .44, fur); |
| cube(-.38, 1.52, -.02, .26, .25, .2, fur); |
| cube(.38, 1.52, -.02, .26, .25, .2, fur); |
| cube(-.38, 1.52, -.15, .13, .12, .05, innerEar); |
| cube(.38, 1.52, -.15, .13, .12, .05, innerEar); |
| cube(0, 1.14, -.28, .36, .25, .07, face); |
| cube(-.12, 1.27, -.33, .055, .055, .04, graphite); |
| cube(.12, 1.27, -.33, .055, .055, .04, graphite); |
| cube(0, 1.15, -.34, .07, .055, .04, graphite); |
| cube(-.05, 1.06, -.34, .13, .035, .04, graphite); |
| cube(.05, 1.06, -.34, .13, .035, .04, graphite); |
| const leftArm = cube(-.56, .76, -.02, .24, .5, .3, fur); |
| const rightArm = cube(.56, .76, -.02, .24, .5, .3, fur); |
| leftArm.rotation.z = .18; |
| rightArm.rotation.z = -.18; |
| const leftLeg = cube(-.24, .25, .04, .26, .34, .3, fur); |
| const rightLeg = cube(.24, .25, .04, .26, .34, .3, fur); |
| leftLeg.rotation.x = -.08; |
| rightLeg.rotation.x = -.08; |
| cube(-.09, .99, -.32, .18, .11, .055, bow); |
| cube(.09, .99, -.32, .18, .11, .055, bow); |
| cube(0, .99, -.34, .07, .13, .06, makeMat('#f3d77d', {{ roughness: .5, metalness: .08 }})); |
| }} |
| |
| function renderSticker() {{ |
| const paper = makeMat('#f4efe0', {{ roughness: .36 }}); |
| const inkA = makeMat(colors[0] || '#4d78ff', {{ roughness: .42, emissive: colors[0] || '#4d78ff', emissiveStrength: .05 }}); |
| const inkB = makeMat(colors[1] || '#77d7a8', {{ roughness: .42, emissive: colors[1] || '#77d7a8', emissiveStrength: .05 }}); |
| cube(0, .9, -.02, 1.05, .82, .08, paper); |
| cube(-.22, .92, -.09, .34, .34, .04, inkA); |
| cube(.16, .98, -.1, .42, .16, .04, inkB); |
| cube(.24, .73, -.1, .32, .12, .04, graphite); |
| cube(-.36, .58, -.1, .2, .12, .04, makeMat('#ffdc8a', {{ roughness: .38 }})); |
| }} |
| |
| function renderGeneric() {{ |
| const count = Math.max(5, Math.min(14, artifact.element_count || 7)); |
| for (let i = 0; i < count; i++) {{ |
| const row = Math.floor(i / 4); |
| const col = i % 4; |
| const sx = .42 + (i % 3) * .08; |
| const sy = .26 + ((i + row) % 4) * .12; |
| const sz = .34 + (row % 3) * .08; |
| const x = (col - 1.5) * .48; |
| const z = (row - 1.0) * .42; |
| const y = .42 + sy / 2 + Math.sin(i * 1.7) * .02; |
| const part = cube(x, y, z, sx, sy, sz, materialFor(i)); |
| part.rotation.y = (i % 2 ? .12 : -.18); |
| }} |
| }} |
| |
| if (shapeText.includes('earbud') || shapeText.includes('airpod')) {{ |
| renderEarbuds(); |
| }} else if (shapeText.includes('monitor') || shapeText.includes('screen')) {{ |
| renderMonitor(); |
| }} else if (shapeText.includes('clock')) {{ |
| renderClock(); |
| }} else if (shapeText.includes('shoe')) {{ |
| renderShoes(); |
| }} else if (shapeText.includes('bag') || shapeText.includes('backpack')) {{ |
| renderBag(); |
| }} else if (shapeText.includes('phone') || shapeText.includes('smartphone') || shapeText.includes('mobile')) {{ |
| renderPhone(); |
| }} else if (shapeText.includes('bottle') || shapeText.includes('water')) {{ |
| renderWaterBottle(); |
| }} else if (shapeText.includes('plush') || shapeText.includes('teddy') || shapeText.includes('bear')) {{ |
| renderPlushToy(); |
| }} else if (shapeText.includes('sticker') || shapeText.includes('decal') || shapeText.includes('logo')) {{ |
| renderSticker(); |
| }} else if (shapeText.includes('controller') || shapeText.includes('gamepad') || shapeText.includes('xbox')) {{ |
| renderController(); |
| }} else if (shapeText.includes('remote')) {{ |
| renderRemote(); |
| }} else {{ |
| renderGeneric(); |
| }} |
| const grid = new THREE.GridHelper(7.4, 14, artifact.hall_color, '#3a3020'); |
| grid.position.y = 0; |
| scene.add(grid); |
| |
| let targetYaw = -.4; |
| let targetPitch = .05; |
| let dragging = false; |
| let lastX = 0; |
| let lastY = 0; |
| |
| function positionCamera() {{ |
| camera.position.set( |
| Math.sin(targetYaw) * cameraDistance, |
| 2.7 + Math.sin(targetPitch) * 1.2, |
| Math.cos(targetYaw) * cameraDistance |
| ); |
| camera.lookAt(0, .72, 0); |
| }} |
| |
| stage.addEventListener('pointerdown', (event) => {{ |
| dragging = true; |
| stage.classList.add('dragging'); |
| lastX = event.clientX; |
| lastY = event.clientY; |
| stage.setPointerCapture(event.pointerId); |
| }}); |
| stage.addEventListener('pointermove', (event) => {{ |
| if (!dragging) return; |
| const dx = event.clientX - lastX; |
| const dy = event.clientY - lastY; |
| targetYaw += dx * .008; |
| targetPitch = Math.max(-.8, Math.min(.65, targetPitch + dy * .006)); |
| lastX = event.clientX; |
| lastY = event.clientY; |
| positionCamera(); |
| }}); |
| stage.addEventListener('pointerup', (event) => {{ |
| dragging = false; |
| stage.classList.remove('dragging'); |
| try {{ stage.releasePointerCapture(event.pointerId); }} catch (error) {{}} |
| }}); |
| stage.addEventListener('pointerleave', () => {{ |
| dragging = false; |
| stage.classList.remove('dragging'); |
| }}); |
| stage.addEventListener('wheel', (event) => {{ |
| event.preventDefault(); |
| cameraDistance = Math.max(3.2, Math.min(7.4, cameraDistance + event.deltaY * .003)); |
| positionCamera(); |
| }}, {{ passive: false }}); |
| positionCamera(); |
| |
| function tick(t) {{ |
| group.rotation.y = dragging ? targetYaw * .08 : Math.sin(t * .00045) * .12 - .2; |
| group.position.y = Math.sin(t * .0012) * .035; |
| const pulse = 1 + Math.sin(t * .0021) * .012; |
| group.scale.set(pulse, pulse, pulse); |
| rim.intensity = 10 + Math.sin(t * .0018) * 2.4; |
| renderer.render(scene, camera); |
| requestAnimationFrame(tick); |
| }} |
| requestAnimationFrame(tick); |
| window.addEventListener('resize', () => {{ |
| camera.aspect = stage.clientWidth / stage.clientHeight; |
| camera.updateProjectionMatrix(); |
| renderer.setSize(stage.clientWidth, stage.clientHeight); |
| }}); |
| </script> |
| </body> |
| </html> |
| """ |
| return f""" |
| <section class="artifact-model-shell"> |
| <iframe |
| title="3D Minecraft artifact model preview" |
| class="artifact-model-frame" |
| sandbox="allow-scripts" |
| srcdoc="{html.escape(srcdoc, quote=True)}" |
| ></iframe> |
| </section> |
| """ |
|
|
|
|
| def texture_inspection_html(items: list[dict], total: int, page_index: int, page_count: int) -> str: |
| cards = [] |
| for item in items: |
| preview_url = texture_preview_url(item["id"]) |
| command = f"/give @p minecraft:paper[minecraft:custom_model_data={item['custom_model_data']}] 1" |
| cards.append( |
| f""" |
| <article class="texture-inspect-card"> |
| <img src="{preview_url}" alt="{item['label']}"> |
| <div> |
| <strong>{item['label']}</strong> |
| <span>{item['kind']} · {item.get('material', 'materialized')} · {item.get('finish', 'museum finish')}</span> |
| <span>{item.get('shape', 'object')} model · {item.get('element_count', '?')} cuboids</span> |
| <details> |
| <summary>Item code</summary> |
| <code>{html_escape(command)}</code> |
| </details> |
| </div> |
| </article> |
| """ |
| ) |
| return f""" |
| <section class="texture-inspector texture-atlas"> |
| <header> |
| <div> |
| <span>AfterBlock object atlas</span> |
| <h3>Page {page_index} of {page_count}</h3> |
| </div> |
| <p>{len(items)} relic looks from {total} matching objects. Flip pages like a field guide; commands stay tucked away until needed.</p> |
| </header> |
| <div>{''.join(cards)}</div> |
| </section> |
| """ |
|
|
|
|
| def _texture_library_payload(kind: str, page: int): |
| manifest = resource_manifest() |
| if kind and kind != "all": |
| manifest = [item for item in manifest if item.get("kind") == kind] |
| page_size = TEXTURE_PAGE_SIZE |
| total = len(manifest) |
| page_count = max(1, math.ceil(total / page_size)) |
| page_index = max(1, min(int(page or 1), page_count)) |
| start = (page_index - 1) * page_size |
| items = manifest[start : start + page_size] |
| gallery = [ |
| ( |
| texture_preview_url(item["id"]), |
| f"{item['label']} | CMD {item['custom_model_data']}", |
| ) |
| for item in items |
| ] |
| rows = [ |
| [ |
| item["custom_model_data"], |
| item["label"], |
| item["kind"], |
| item["shape"], |
| item.get("material", "untextured"), |
| item.get("finish", ""), |
| item.get("model_profile", "legacy"), |
| item.get("orientation", "standard"), |
| item.get("itemdisplay_transform", "ItemDisplay.Transform.FIXED"), |
| item.get("element_count", ""), |
| item["model"], |
| f"/give @p minecraft:paper[minecraft:custom_model_data={item['custom_model_data']}] 1", |
| ] |
| for item in items |
| ] |
| status = f"Object atlas page {page_index}/{page_count} | {total} matching relic looks" |
| return gallery, rows, texture_inspection_html(items, total, page_index, page_count), status, page_index |
|
|
|
|
| def browse_texture_library(kind: str, page: int): |
| gallery, rows, inspector, status, _page_index = _texture_library_payload(kind, page) |
| return gallery, rows, inspector, status |
|
|
|
|
| def browse_texture_library_with_page(kind: str, page: int): |
| return _texture_library_payload(kind, page) |
|
|
|
|
| def browse_texture_previous(kind: str, page: int): |
| return _texture_library_payload(kind, int(page or 1) - 1) |
|
|
|
|
| def browse_texture_next(kind: str, page: int): |
| return _texture_library_payload(kind, int(page or 1) + 1) |
|
|
|
|
| def museum_collection(selected: dict, count: int = 100) -> list[dict]: |
| artifacts = [demo_collection_artifact(i) for i in range(count)] |
| selected_copy = dict(selected) |
| selected_copy["catalog_index"] = 0 |
| selected_copy["texture_path"] = f"assets/afterblock_textures/items/{texture_key_for(selected['object_guess'])}_selected.png" |
| return [selected_copy] + artifacts |
|
|
|
|
| def hall_color(hall: str) -> tuple[int, int, int]: |
| colors = { |
| "Hall of Firsts": (236, 191, 82), |
| "Hall of Companions": (104, 178, 132), |
| "Hall of Turning Points": (204, 114, 92), |
| "Hall of Worlds": (104, 145, 214), |
| "Hall of Soft Things": (222, 159, 184), |
| "Hall of Tools": (151, 151, 164), |
| "Hall of Lost Signals": (149, 112, 194), |
| "Grand Painting Hall": (205, 132, 72), |
| "Animal Spirit Grove": (111, 164, 86), |
| } |
| return colors.get(hall, (180, 160, 120)) |
|
|
|
|
| def render_museum_preview(selected: dict, collection: list[dict]) -> str: |
| width, height = 1600, 1040 |
| image = Image.new("RGB", (width, height), (13, 14, 12)) |
| draw = ImageDraw.Draw(image) |
|
|
| def load_font(size: int): |
| candidates = [ |
| "/System/Library/Fonts/Supplemental/Arial.ttf", |
| "/Library/Fonts/Arial.ttf", |
| "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", |
| ] |
| for candidate in candidates: |
| try: |
| return ImageFont.truetype(candidate, size=size) |
| except OSError: |
| continue |
| return ImageFont.load_default() |
|
|
| font_title = load_font(26) |
| font_hall = load_font(14) |
| font_body = load_font(13) |
| font_tiny = load_font(10) |
| pack_stats = resource_pack_stats() |
|
|
| def shade(rgb: tuple[int, int, int], factor: float) -> tuple[int, int, int]: |
| return tuple(max(0, min(255, int(channel * factor))) for channel in rgb) |
|
|
| def iso_box(x: int, y: int, w: int, h: int, depth: int, color: tuple[int, int, int], outline: tuple[int, int, int]): |
| top = [(x, y), (x + w, y), (x + w - depth, y + depth), (x + depth, y + depth)] |
| left = [(x, y), (x + depth, y + depth), (x + depth, y + h + depth), (x, y + h)] |
| right = [(x + w, y), (x + w - depth, y + depth), (x + w - depth, y + h + depth), (x + w, y + h)] |
| face = [(x + depth, y + depth), (x + w - depth, y + depth), (x + w - depth, y + h + depth), (x + depth, y + h + depth)] |
| draw.polygon(left, fill=shade(color, 0.46), outline=outline) |
| draw.polygon(right, fill=shade(color, 0.55), outline=outline) |
| draw.polygon(face, fill=shade(color, 0.34), outline=outline) |
| draw.polygon(top, fill=shade(color, 0.78), outline=outline) |
|
|
| draw.rectangle([26, 26, width - 26, height - 26], outline=(150, 112, 58), width=4) |
| draw.rectangle([38, 38, width - 38, 106], fill=(29, 25, 19), outline=(94, 74, 48), width=2) |
| draw.text((58, 50), "AFTERBLOCK MUSEUM", fill=(255, 225, 154), font=font_title) |
| draw.text((58, 82), f"100 labeled relics - {pack_stats['total_kinds']} object families - {pack_stats['total_items']} Minecraft PNG models - live server packets", fill=(190, 176, 142), font=font_body) |
| draw.text((1180, 66), "minecraft:paper + CustomModelData", fill=(211, 190, 128), font=font_body) |
|
|
| |
| draw.polygon([(88, 168), (1120, 168), (1040, 900), (150, 900)], fill=(32, 31, 26), outline=(92, 78, 50)) |
| for i in range(11): |
| x0 = 120 + i * 92 |
| draw.line([(x0, 182), (x0 - 62, 876)], fill=(46, 43, 36), width=1) |
| for j in range(8): |
| y = 210 + j * 82 |
| draw.line([(100, y), (1110, y)], fill=(48, 44, 35), width=1) |
| draw.rectangle([486, 146, 694, 910], outline=(121, 94, 55), width=3) |
| draw.rectangle([520, 148, 660, 910], fill=(39, 32, 24), outline=(149, 104, 53), width=2) |
|
|
| hall_positions = { |
| "Hall of Firsts": (96, 142, 300, 184), |
| "Hall of Companions": (370, 138, 300, 184), |
| "Hall of Turning Points": (744, 142, 300, 184), |
| "Hall of Worlds": (96, 382, 300, 184), |
| "Hall of Soft Things": (370, 382, 300, 184), |
| "Hall of Tools": (744, 382, 300, 184), |
| "Hall of Lost Signals": (96, 622, 300, 184), |
| "Grand Painting Hall": (370, 622, 300, 184), |
| "Animal Spirit Grove": (744, 622, 300, 184), |
| } |
|
|
| for hall, (x, y, w, h) in hall_positions.items(): |
| color = hall_color(hall) |
| iso_box(x, y, w, h, 24, color, shade(color, 0.9)) |
| draw.rectangle([x + 20, y + 30, x + w - 20, y + 54], fill=shade(color, 0.28), outline=shade(color, 0.9)) |
| draw.text((x + 28, y + 35), hall.upper()[:27], fill=(255, 234, 176), font=font_hall) |
| for lamp in range(3): |
| lx = x + 60 + lamp * 82 |
| draw.ellipse([lx, y + h + 16, lx + 10, y + h + 26], fill=(242, 188, 84), outline=(48, 31, 15)) |
|
|
| placed_counts = {hall: 0 for hall in hall_positions} |
| for artifact in collection: |
| hall = artifact["hall"] |
| if hall not in hall_positions: |
| continue |
| x, y, w, h = hall_positions[hall] |
| slot = placed_counts[hall] |
| placed_counts[hall] += 1 |
| if slot >= 15: |
| continue |
| col = slot % 5 |
| row = slot // 5 |
| px = x + 34 + col * 50 |
| py = y + 72 + row * 34 |
| active = artifact["artifact_id"] == selected["artifact_id"] |
| hall_rgb = hall_color(hall) |
| artifact_rgb = (255, 232, 126) if active else shade(hall_rgb, 1.1) |
| draw.rectangle([px, py + 13, px + 34, py + 22], fill=shade(artifact_rgb, 0.45), outline=(22, 19, 15)) |
| draw.rectangle([px + 4, py, px + 30, py + 16], fill=artifact_rgb, outline=(18, 15, 10)) |
| draw.rectangle([px + 8, py + 4, px + 26, py + 13], fill=shade(artifact_rgb, 0.7), outline=shade(artifact_rgb, 0.3)) |
| label = owner_display(artifact)[:7] |
| draw.text((px - 2, py + 24), label, fill=(248, 236, 198) if active else (180, 170, 141), font=font_tiny) |
| if active: |
| draw.rectangle([px - 8, py - 8, px + 42, py + 46], outline=(255, 246, 180), width=3) |
| draw.line([px + 17, py - 8, 1202, 176], fill=(255, 226, 132), width=2) |
|
|
| detail_x = 1160 |
| draw.rectangle([detail_x, 140, width - 52, 906], fill=(27, 24, 19), outline=(150, 112, 58), width=3) |
| draw.rectangle([detail_x + 20, 166, width - 74, 420], fill=(36, 32, 25), outline=(96, 76, 48), width=2) |
| draw.text((detail_x + 36, 184), "SELECTED RELIC", fill=(255, 221, 136), font=font_hall) |
| draw.text((detail_x + 36, 220), selected["title"][:38], fill=(244, 233, 202), font=font_body) |
| draw.text((detail_x + 36, 250), owner_display(selected), fill=(205, 190, 150), font=font_body) |
| draw.text((detail_x + 36, 286), selected["hall"], fill=(238, 201, 117), font=font_body) |
| draw.text((detail_x + 36, 322), f"Object: {selected['object_guess']}", fill=(228, 218, 188), font=font_body) |
| draw.text((detail_x + 36, 352), f"Score: {selected['curation_scores']['curation_score']} CMD: {selected.get('resource_pack_item', {}).get('custom_model_data', '')}", fill=(228, 218, 188), font=font_body) |
| draw.text((detail_x + 36, 382), f"XYZ: {selected['minecraft_coordinates']['x']} {selected['minecraft_coordinates']['y']} {selected['minecraft_coordinates']['z']}", fill=(228, 218, 188), font=font_body) |
|
|
| plaque = selected["spirit_first_line"][:72] |
| draw.rectangle([detail_x + 20, 446, width - 74, 566], fill=(44, 35, 24), outline=(121, 94, 55), width=2) |
| draw.text((detail_x + 36, 466), "PLAQUE", fill=(255, 221, 136), font=font_hall) |
| draw.text((detail_x + 36, 498), plaque[:40], fill=(240, 226, 190), font=font_body) |
| draw.text((detail_x + 36, 528), plaque[40:], fill=(240, 226, 190), font=font_body) |
|
|
| stats = [ |
| ("artifact passports", len(collection)), |
| ("object families", pack_stats["total_kinds"]), |
| ("resource models", pack_stats["total_items"]), |
| ("display profiles", pack_stats["total_model_profiles"]), |
| ("server import", "ready"), |
| ("resource pack", "AfterBlockMuseum.zip"), |
| ] |
| sy = 604 |
| for label, value in stats: |
| draw.rectangle([detail_x + 24, sy, width - 78, sy + 42], fill=(35, 33, 29), outline=(74, 65, 47)) |
| draw.text((detail_x + 38, sy + 10), str(label).upper(), fill=(171, 158, 121), font=font_body) |
| draw.text((detail_x + 230, sy + 10), str(value)[:24], fill=(246, 225, 164), font=font_body) |
| sy += 52 |
|
|
| draw.rectangle([70, 932, width - 70, 986], fill=(27, 24, 19), outline=(88, 70, 46), width=2) |
| draw.text((94, 950), "Museum path: upload relic -> classify object -> choose hall -> assign plot -> render item model -> spawn pedestal in Minecraft", fill=(222, 205, 166), font=font_body) |
|
|
| path = os.path.join(tempfile.gettempdir(), f"afterblock_museum_preview_{selected['artifact_id']}.png") |
| image.save(path) |
| return path |
|
|
|
|
| def catalog_rows(collection: list[dict]) -> list[list]: |
| rows = [] |
| for artifact in collection[:101]: |
| rows.append( |
| [ |
| artifact.get("catalog_index", 0), |
| owner_display(artifact), |
| artifact["title"], |
| artifact["object_guess"], |
| artifact["hall"], |
| artifact["curation_scores"]["curation_score"], |
| f"{artifact['minecraft_coordinates']['x']} {artifact['minecraft_coordinates']['y']} {artifact['minecraft_coordinates']['z']}", |
| artifact.get("resource_pack_item", {}).get("custom_model_data", ""), |
| ] |
| ) |
| return rows |
|
|
|
|
| def artifact_wall_html(collection: list[dict]) -> str: |
| by_hall = {hall: [] for hall in MUSEUM_HALLS} |
| for artifact in collection: |
| by_hall.setdefault(artifact["hall"], []).append(artifact) |
| selected = collection[0] |
| selected_item = selected.get("resource_pack_item", {}) |
| selected_profile = selected.get("hall_profile") or HALL_PROFILES.get(selected["hall"], {}) |
| hall_routes = { |
| "Hall of Firsts": "Threshold Arcade", |
| "Hall of Companions": "Quiet Bench Passage", |
| "Hall of Turning Points": "Pressure Door", |
| "Hall of Worlds": "Blue Lantern Corridor", |
| "Hall of Soft Things": "Shelter Nook", |
| "Hall of Tools": "Workshop Walk", |
| "Hall of Lost Signals": "Static Archive", |
| "Grand Painting Hall": "Frame Gallery", |
| "Animal Spirit Grove": "Living Atrium", |
| } |
| portals = [] |
| for hall in MUSEUM_HALLS: |
| hall_items = by_hall.get(hall, []) |
| color = hall_color(hall) |
| rgb = f"rgb({color[0]}, {color[1]}, {color[2]})" |
| profile = HALL_PROFILES.get(hall, {}) |
| visible = hall_items[:3] |
| relics = [] |
| for artifact in visible: |
| item = artifact.get("resource_pack_item", {}) |
| active = artifact["artifact_id"] == selected["artifact_id"] |
| preview_url = texture_preview_url(item.get("id", "missing")) if item.get("id") else "" |
| handle = artifact.get("social_tag") or owner_display(artifact) |
| command = f"/give @p minecraft:paper[minecraft:custom_model_data={item.get('custom_model_data', 0)}] 1" |
| relics.append( |
| f""" |
| <details class="portal-relic profile-relic {'active' if active else ''}"> |
| <summary> |
| <img src="{html_escape(preview_url)}" alt="{html_escape(artifact['object_guess'])}"> |
| <strong>{html_escape(compact_text(artifact['object_guess'].title(), 18))}</strong> |
| <small>Item {html_escape(item.get('custom_model_data', ''))}</small> |
| <span class="social-peek"><b>HF</b>{html_escape(compact_text(handle, 18))}</span> |
| </summary> |
| <div class="profile-popout"> |
| <b>{html_escape(artifact['title'])}</b> |
| <p>{html_escape(compact_text(artifact.get('memory_text', artifact.get('plaque_line', '')), 160))}</p> |
| <em>{html_escape(compact_text(artifact.get('spirit_first_line', artifact.get('plaque_line', '')), 120))}</em> |
| <code>{html_escape(command)}</code> |
| </div> |
| </details> |
| """ |
| ) |
| more = max(0, len(hall_items) - len(visible)) |
| portals.append( |
| f""" |
| <article class="hall-portal {'active' if hall == selected['hall'] else ''}" style="--hall-color:{rgb}"> |
| {f"<span class='you-are-here'>You are here</span>" if hall == selected['hall'] else ""} |
| <div class="portal-depth"> |
| <span class="portal-lamp left"></span> |
| <span class="portal-lamp right"></span> |
| <div class="portal-sign"> |
| <strong>{html_escape(hall)}</strong> |
| <em>{html_escape(hall_routes.get(hall, 'Museum Passage'))}</em> |
| </div> |
| </div> |
| <div class="portal-copy"> |
| <span>{len(hall_items)} relics</span> |
| <p>{html_escape(compact_text(profile.get('world', profile.get('when', 'A distinct museum wing.')), 92))}</p> |
| </div> |
| <div class="portal-relics">{''.join(relics)}{f"<span class='more-chip'>+{more}</span>" if more else ""}</div> |
| </article> |
| """ |
| ) |
| selected_preview = artifact_preview_url(selected) |
| selected_command = f"/give @p minecraft:paper[minecraft:custom_model_data={selected_item.get('custom_model_data', 0)}] 1" |
| selected_handle = selected.get("social_tag") or owner_display(selected) |
| return f""" |
| <section class="museum-wall persistent-museum"> |
| <div class="wall-heading"> |
| <h3>Living Museum Map</h3> |
| <p>The banner follows the selected relic through its hall, plot, item code, and passport route.</p> |
| </div> |
| <div class="living-map-banner"> |
| <span>You are here</span> |
| <strong>{html_escape(selected['hall'])} · {html_escape(hall_routes.get(selected['hall'], selected['zone']))}</strong> |
| <p>{html_escape(selected['title'])} is installed at plot {selected['plot']['x']}, {selected['plot']['z']} with CMD {selected_item.get('custom_model_data', 0)}.</p> |
| </div> |
| <div class="selected-installation" style="--hall-color:rgb({hall_color(selected['hall'])[0]}, {hall_color(selected['hall'])[1]}, {hall_color(selected['hall'])[2]})"> |
| <div class="installation-frame"> |
| <img src="{html_escape(selected_preview)}" alt="{html_escape(selected['object_guess'])}"> |
| <span>Placed</span> |
| </div> |
| <div> |
| <strong>{html_escape(selected['title'])}</strong> |
| <em>{html_escape(selected['hall'])} · {html_escape(hall_routes.get(selected['hall'], selected['zone']))} · XYZ {selected['minecraft_coordinates']['x']} {selected['minecraft_coordinates']['y']} {selected['minecraft_coordinates']['z']}</em> |
| <p>{html_escape(compact_text(selected.get('memory_text', selected.get('plaque_line', '')), 150))}</p> |
| <code>{html_escape(selected_command)}</code> |
| <details class="selected-profile"> |
| <summary>Open relic profile</summary> |
| <div> |
| <b>{html_escape(selected.get('spirit_first_line', selected.get('plaque_line', '')))}</b> |
| <span>{html_escape(selected.get('lore_short', selected.get('placement_reason', '')))}</span> |
| </div> |
| </details> |
| </div> |
| <span class="social-peek selected-social"><b>HF</b>{html_escape(compact_text(selected_handle, 22))}</span> |
| </div> |
| <div class="museum-concourse">{''.join(portals)}</div> |
| </section> |
| """ |
|
|
|
|
| def input_type_from_prompt(prompt: str) -> str: |
| text = (prompt or "").lower() |
| if any(word in text for word in ["painting", "poster", "mural", "canvas", "draw"]): |
| return "painting_prompt" |
| if any(word in text for word in ["creature", "pet", "animal", "spirit", "dragon", "bird"]): |
| return "animal_spirit" |
| if any(word in text for word in ["memory", "childhood", "first", "old", "kept", "carried"]): |
| return "memory_prompt" |
| return "object_photo" |
|
|
|
|
| def build_museum_artifact( |
| owner_name: str, |
| owner_handle: str, |
| input_type: str, |
| source_prompt: str, |
| memory_text: str, |
| relic_image_path: str | None = None, |
| ) -> dict: |
| owner_name = (owner_name or "Anonymous").strip() |
| owner_handle = social_tag_for(owner_handle) |
| input_type = (input_type or "memory_prompt").strip() |
| source_prompt = (source_prompt or "a mysterious object from a half-remembered room").strip() |
| memory_text = (memory_text or "No memory text was provided, so the museum listens to the object itself.").strip() |
| image_note = "" |
| image_palette = [] |
| if relic_image_path: |
| try: |
| with Image.open(relic_image_path) as uploaded: |
| uploaded = uploaded.convert("RGB").resize((8, 8)) |
| pixels = list(uploaded.getdata()) |
| avg = tuple(int(sum(pixel[i] for pixel in pixels) / len(pixels)) for i in range(3)) |
| image_note = f"uploaded image average color rgb{avg}" |
| image_palette = [avg] |
| except Exception: |
| image_note = "uploaded image could not be fingerprinted" |
| text = f"afterblock={owner_name}|{owner_handle}|{input_type}|{source_prompt}|{memory_text}|{image_note}" |
| seed = stable_seed(text) |
| vec = embedding(text) |
| moods = top_moods(text, vec) |
| palette = palette_from_vector(vec, seed, moods) |
| palette_names = [name for name, _ in palette] |
| object_guess = object_guess_for(input_type, source_prompt, memory_text) |
| hall, placement_reason = hall_for_artifact(input_type, object_guess, source_prompt, memory_text, seed) |
| hall_profile = HALL_PROFILES.get(hall, {}) |
| zone = museum_zone_for(hall, seed) |
| plot = plot_for_seed(seed) |
| title = artifact_title(source_prompt, seed, moods, object_guess) |
| artifact_id = hashlib.sha256(text.encode("utf-8")).hexdigest()[:16] |
| scores = curation_scores(source_prompt, memory_text, input_type, moods, palette_names, plot) |
| spirit = spirit_for_artifact(title, object_guess, memory_text, hall, seed) |
| coordinates = {"x": plot["world_x"], "y": 80, "z": plot["world_z"]} |
| plaque_line = spirit["spirit_first_line"] |
| lore_short = f"{object_guess.title()} placed in {hall.lower()} because {placement_reason}." |
| resource_item = resource_pack_item_for(object_guess, seed) |
| artifact = { |
| "artifact_id": artifact_id, |
| "owner_name": owner_name, |
| "owner_handle": owner_handle, |
| "owner_label": owner_handle or owner_name or "anonymous visitor", |
| "social_tag": owner_handle, |
| "input_type": input_type, |
| "title": title, |
| "source_prompt": source_prompt, |
| "memory_text": memory_text, |
| "image_fingerprint": image_note, |
| "object_guess": object_guess, |
| "hall": hall, |
| "hall_profile": hall_profile, |
| "zone": zone, |
| "plot": plot, |
| "minecraft_coordinates": coordinates, |
| "palette": palette_names, |
| "minecraft_materials": palette_names, |
| "texture_path": f"assets/afterblock_textures/items/{texture_key_for(object_guess)}_selected.png", |
| "resource_pack_item": resource_item, |
| "plaque_line": plaque_line, |
| "lore_short": lore_short, |
| "artifact_when": hall_profile.get("when", placement_reason), |
| "spirit_name": spirit["spirit_name"], |
| "spirit_traits": spirit["spirit_traits"], |
| "spirit_first_line": spirit["spirit_first_line"], |
| "placement_reason": placement_reason, |
| "curation_scores": scores, |
| "resonance_links": resonance_links_for_artifact(hall, moods, plot), |
| "passport_card": { |
| "title": title, |
| "owner_handle": owner_handle, |
| "owner_label": owner_handle or owner_name or "anonymous visitor", |
| "hall": hall, |
| "plot": plot, |
| "minecraft_coordinates": coordinates, |
| "plaque_line": plaque_line, |
| "preservation_line": "Preserved in AfterBlock Museum", |
| }, |
| "created_at": datetime.now(timezone.utc).isoformat(), |
| } |
| artifact.update(spirit) |
| artifact["share_url"] = artifact_share_url(artifact) |
| artifact["qr_payload"] = artifact["share_url"] |
| return artifact |
|
|
|
|
| def museum_packet_for(artifact: dict) -> dict: |
| return { |
| "type": "dreamwall.museum.v1", |
| "artifact": artifact, |
| "museum": { |
| "hall": artifact["hall"], |
| "zone": artifact["zone"], |
| "plot": artifact["plot"], |
| "coordinates": artifact["minecraft_coordinates"], |
| "placement_reason": artifact["placement_reason"], |
| }, |
| "spirit": { |
| "name": artifact["spirit_name"], |
| "traits": artifact["spirit_traits"], |
| "first_line": artifact["spirit_first_line"], |
| "rules": artifact["rules"], |
| "sample_visitor_questions": artifact["sample_visitor_questions"], |
| "sample_spirit_responses": artifact["sample_spirit_responses"], |
| }, |
| "passport": artifact["passport_card"], |
| "minecraft": { |
| "protocol": "dreamwall.mc.v1", |
| "import_mode": "museum_artifact", |
| "title": artifact["title"], |
| "hall": artifact["hall"], |
| "coordinates": artifact["minecraft_coordinates"], |
| "materials": artifact["minecraft_materials"], |
| "texture_path": artifact["texture_path"], |
| "resource_pack_item": artifact["resource_pack_item"], |
| "recommended_item": artifact["resource_pack_item"].get("recommended_item", "minecraft:paper"), |
| "custom_model_data": artifact["resource_pack_item"].get("custom_model_data", 0), |
| "model": artifact["resource_pack_item"].get("model", ""), |
| "plaque_text": artifact["plaque_line"], |
| "spirit_first_line": artifact["spirit_first_line"], |
| "owner_handle": artifact["owner_handle"], |
| "owner_label": artifact.get("owner_label", owner_display(artifact)), |
| "social_tag": artifact.get("social_tag", ""), |
| "artifact_when": artifact.get("artifact_when", ""), |
| "passport_qr_payload": artifact["qr_payload"], |
| "sign_lines": [ |
| compact_text(artifact["title"], 30), |
| compact_text(artifact.get("owner_label", owner_display(artifact)), 30), |
| compact_text(artifact["hall"], 30), |
| compact_text(artifact["spirit_first_line"], 30), |
| ], |
| "pedestal": "place engraved nameplate, 3D resource-pack item, lectern passport book, and redstone profile button at coordinates", |
| }, |
| } |
|
|
|
|
| def curate_afterblock_artifact( |
| owner_name: str, |
| owner_handle: str, |
| input_type: str, |
| source_prompt: str, |
| memory_text: str, |
| relic_image_path: str | None = None, |
| ): |
| artifact = build_museum_artifact(owner_name, owner_handle, input_type, source_prompt, memory_text, relic_image_path) |
| packet = museum_packet_for(artifact) |
| collection = museum_collection(artifact, 100) |
| preview_path = render_museum_preview(artifact, collection) |
| model_view = artifact_model_html(artifact) |
| hall_profile = artifact.get("hall_profile") or HALL_PROFILES.get(artifact["hall"], {}) |
| placement = [ |
| f"# {artifact['title']}", |
| f"**{owner_display(artifact)}** · {artifact['hall']} · score {artifact['curation_scores']['curation_score']}", |
| f"Plot **({artifact['plot']['x']}, {artifact['plot']['z']})** · XYZ **{artifact['minecraft_coordinates']['x']} {artifact['minecraft_coordinates']['y']} {artifact['minecraft_coordinates']['z']}**", |
| "", |
| f"**When it becomes art:** {hall_profile.get('when', artifact['placement_reason'])}", |
| f"**Worldbuilding:** {hall_profile.get('world', 'placed as a quiet museum relic')}", |
| f"**Minecraft placement:** {hall_profile.get('minecraft', 'pedestal, sign, and item frame')}", |
| "", |
| f"{artifact['placement_reason']}", |
| f"_{artifact['plaque_line']}_", |
| "", |
| "**Resonance links**", |
| ] |
| if artifact["resonance_links"]: |
| placement.extend( |
| f"- {link['title']} by {link['creator']}: {link['resonance']}, distance {link['distance']}" |
| for link in artifact["resonance_links"] |
| ) |
| else: |
| placement.append("- No nearby resonance yet. This artifact becomes an anchor for future visitors.") |
| profile = relic_profile_html(artifact) |
| passport = passport_html(artifact) |
| command = minecraft_give_command(artifact) |
| coordinates = coordinates_html(artifact) |
| waypoint = waypointcraft_html(artifact) |
| server_kit = server_kit_text(artifact) |
| server_handoff = server_handoff_html(artifact) |
| return ( |
| preview_path, |
| catalog_rows(collection), |
| artifact_wall_html(collection), |
| model_view, |
| command, |
| coordinates, |
| waypoint, |
| "\n".join(placement), |
| profile, |
| passport, |
| json.dumps(packet, indent=2), |
| server_kit, |
| server_handoff, |
| ) |
|
|
|
|
| def quick_curate_afterblock_artifact( |
| source_prompt: str, |
| memory_text: str, |
| owner_name: str, |
| owner_handle: str, |
| relic_image_path: str | None = None, |
| ): |
| source_prompt = (source_prompt or "").strip() or "a water bottle beside a laptop monitor" |
| input_type = input_type_from_prompt(source_prompt) |
| memory_text = (memory_text or "").strip() or ( |
| "A compact story caption visitors see when they hover the relic in the museum." |
| ) |
| outputs = curate_afterblock_artifact(owner_name, owner_handle, input_type, source_prompt, memory_text, relic_image_path) |
| return (*outputs, input_type, source_prompt, memory_text) |
|
|
|
|
| def place_in_museum(source_prompt: str, story_caption: str, owner_handle: str = "@Wildstash", relic_image_path: str | None = None): |
| owner_name, owner_tag = visitor_identity(owner_handle) |
| preview, catalog, wall, model, command, coordinates, waypoint, placement, spirit, passport, packet, server_kit, server_handoff, *_ = quick_curate_afterblock_artifact( |
| source_prompt, |
| story_caption, |
| owner_name, |
| owner_tag, |
| relic_image_path, |
| ) |
| server_download = server_config_zip( |
| import_prompt=clean_text(source_prompt, DEFAULT_IMPORT_PROMPT), |
| import_story=clean_text(story_caption, DEFAULT_IMPORT_STORY), |
| import_owner=clean_text(owner_handle, DEFAULT_IMPORT_OWNER), |
| ) |
| return model, command, coordinates, waypoint, passport, packet, preview, catalog, spirit, wall, server_kit, server_download, server_handoff |
|
|
|
|
| def load_demo_artifact(name: str): |
| data = DEMO_ARTIFACTS.get(name) or next(iter(DEMO_ARTIFACTS.values())) |
| return ( |
| data["owner_name"], |
| data["owner_handle"], |
| data["input_type"], |
| data["source_prompt"], |
| data["memory_text"], |
| ) |
|
|
|
|
| HABITATS = { |
| "redstone caves": ["electric", "mechanical", "small", "curious"], |
| "sky forest": ["flying", "social", "light", "watchful"], |
| "mushroom swamp": ["fungal", "patient", "camouflaged", "soft"], |
| "desert ruins": ["ancient", "defensive", "forager", "heatproof"], |
| "ocean cliffs": ["aquatic", "agile", "echoing", "storm"], |
| "nether garden": ["cursed", "glowing", "bold", "fireproof"], |
| } |
|
|
| CREATURE_HINTS = { |
| "electric": ["spark", "thunder", "yellow", "lightning", "battery"], |
| "flying": ["bird", "sky", "wing", "cloud", "feather"], |
| "aquatic": ["ocean", "fish", "wave", "rain", "river"], |
| "mechanical": ["robot", "gear", "circuit", "redstone", "machine"], |
| "ancient": ["dragon", "ruin", "fossil", "temple", "old"], |
| "fungal": ["mushroom", "spore", "swamp", "moss", "rot"], |
| "cursed": ["ghost", "void", "shadow", "haunted", "curse"], |
| "cozy": ["leaf", "soft", "tiny", "garden", "warm"], |
| } |
|
|
| SAMPLE_CREATURES = [ |
| {"name": "Mossbyte", "creator": "feral_dev", "species": "moss circuit fox", "habitat": "redstone caves", "survival": 84, "generation": 3, "state": "foraging near copper lamps"}, |
| {"name": "Cloudrill", "creator": "sky_bidder", "species": "cloud antler drake", "habitat": "sky forest", "survival": 79, "generation": 2, "state": "guarding a floating nest"}, |
| {"name": "Funglow", "creator": "anonymous_heron", "species": "glowing swamp moth", "habitat": "mushroom swamp", "survival": 73, "generation": 4, "state": "pollinating red mushrooms"}, |
| {"name": "Obsidip", "creator": "redacted", "species": "tiny nether seal", "habitat": "nether garden", "survival": 66, "generation": 1, "state": "sleeping under basalt leaves"}, |
| ] |
|
|
|
|
| def creature_traits(prompt: str, vec: np.ndarray) -> list[str]: |
| lowered = prompt.lower() |
| traits = [] |
| for trait, hints in CREATURE_HINTS.items(): |
| if any(hint in lowered for hint in hints): |
| traits.append(trait) |
| ranked = sorted(CREATURE_HINTS, key=lambda trait: vec[stable_seed(trait) % len(vec)], reverse=True) |
| for trait in ranked: |
| if trait not in traits: |
| traits.append(trait) |
| if len(traits) >= 5: |
| break |
| return traits[:5] |
|
|
|
|
| def habitat_fit(traits: list[str], habitat: str) -> float: |
| wanted = HABITATS[habitat] |
| return sum(1 for trait in traits if trait in wanted) / max(1, len(wanted)) |
|
|
|
|
| def hatch_pet(prompt: str, player: str, island: str): |
| prompt = (prompt or "").strip() or "a quiet creature made of leaves" |
| player = (player or "anonymous").strip() |
| island = (island or "founder island").strip() |
| text = f"pet={player}\nisland={island}\nprompt={prompt}" |
| seed = stable_seed(text) |
| vec = embedding(text) |
| moods = top_moods(text, vec) |
| traits = creature_traits(prompt, vec) |
| habitat_names = list(HABITATS) |
| habitat = habitat_names[seed % len(habitat_names)] |
| fit = habitat_fit(traits, habitat) |
| rng = np.random.default_rng(seed) |
| stats = { |
| "speed": int(3 + abs(vec[1]) * 9), |
| "defense": int(3 + abs(vec[7]) * 9), |
| "foraging": int(3 + abs(vec[11]) * 9), |
| "social": int(3 + abs(vec[17]) * 9), |
| "mutation": int(3 + abs(vec[23]) * 9), |
| } |
| base_survival = 42 + fit * 28 + stats["foraging"] * 1.7 + stats["defense"] * 1.2 + stats["social"] * 0.9 |
| survival = int(max(12, min(96, base_survival + rng.normal(0, 5)))) |
| name_parts = ["Volt", "Moss", "Cloud", "Fang", "Bloom", "Rune", "Pip", "Ash", "Glim", "Root"] |
| suffixes = ["ling", "paw", "drake", "moth", "sprite", "cub", "wisp", "beak", "tail", "byte"] |
| name = name_parts[seed % len(name_parts)] + suffixes[(seed // 9) % len(suffixes)] |
| species = f"{traits[0]} {traits[1]} creature" if len(traits) > 1 else f"{traits[0]} creature" |
| generation = 1 + seed % 4 |
| state_options = [ |
| "searching for food", |
| "watching a stronger creature from tall grass", |
| "marking a new nest site", |
| "training near a redstone gate", |
| "avoiding a predator trail", |
| "looking for a fusion partner", |
| ] |
| state = state_options[(seed // 17) % len(state_options)] |
| cooldown = 45 + seed % 75 |
| battle_score = int(stats["speed"] * 1.1 + stats["defense"] * 1.4 + stats["foraging"] * 0.8 + fit * 18) |
| lineage = [ |
| f"Gen 0: {player}'s prompt seed", |
| f"Gen {generation}: {name} adapted to {habitat}", |
| f"Next possible fusion: {traits[0]} + {moods[0]} lineage", |
| ] |
| pet = { |
| "protocol": "neuropets.mc.v1", |
| "name": name, |
| "creator": player, |
| "species": species, |
| "prompt": prompt, |
| "island": island, |
| "habitat": habitat, |
| "traits": traits, |
| "moods": moods, |
| "stats": stats, |
| "survival": survival, |
| "battle_score": battle_score, |
| "generation": generation, |
| "state": state, |
| "cooldown_seconds": cooldown, |
| "lineage": lineage, |
| "spawn": { |
| "minecraft_entity": "fox" if "cozy" in traits or "electric" in traits else "allay", |
| "name_tag": f"{name} of {player}", |
| "particle": "electric_spark" if "electric" in traits else "happy_villager", |
| "habitat_marker": habitat, |
| }, |
| } |
| return pet |
|
|
|
|
| def render_pet_portrait(pet: dict) -> Image.Image: |
| seed = stable_seed(json.dumps(pet, sort_keys=True)) |
| vec = embedding(" ".join(pet["traits"]) + pet["habitat"]) |
| palette = palette_from_vector(vec, seed, pet["moods"]) |
| grid = generate_grid(vec, seed, palette) |
| image = render_grid(grid, palette) |
| draw = ImageDraw.Draw(image) |
| draw.rectangle([8, 8, image.width - 8, 42], fill=(24, 18, 12)) |
| draw.text((16, 17), pet["name"], fill=(245, 225, 169)) |
| return image |
|
|
|
|
| def pet_leaderboard(current: dict) -> str: |
| rows = SAMPLE_CREATURES + [ |
| { |
| "name": current["name"], |
| "creator": current["creator"], |
| "species": current["species"], |
| "habitat": current["habitat"], |
| "survival": current["survival"], |
| "generation": current["generation"], |
| "state": current["state"], |
| } |
| ] |
| rows = sorted(rows, key=lambda row: (row["survival"], row["generation"]), reverse=True) |
| lines = ["# Survival Leaderboard", ""] |
| for i, row in enumerate(rows, 1): |
| lines.append( |
| f"{i}. **{row['name']}** by {row['creator']} - {row['survival']}% survival, " |
| f"Gen {row['generation']}, {row['habitat']} - {row['state']}" |
| ) |
| return "\n".join(lines) |
|
|
|
|
| def hatch_neuropet(prompt: str, player: str, island: str): |
| pet = hatch_pet(prompt, player, island) |
| card = [ |
| f"# {pet['name']}", |
| f"Creator: **{pet['creator']}**", |
| f"Species: **{pet['species']}**", |
| f"Habitat: **{pet['habitat']}**", |
| f"Current state: **{pet['state']}**", |
| f"Survival odds: **{pet['survival']}%**", |
| f"Battle score: **{pet['battle_score']}**", |
| f"Cooldown before another hatch: **{pet['cooldown_seconds']}s**", |
| "", |
| "Traits: " + ", ".join(pet["traits"]), |
| "", |
| "Prompt abuse rule: power words become personality/aura, not uncapped strength.", |
| ] |
| lineage = "\n".join(f"- {item}" for item in pet["lineage"]) |
| return ( |
| render_pet_portrait(pet), |
| "\n".join(card), |
| pet_leaderboard(pet), |
| lineage, |
| json.dumps(pet, indent=2), |
| ) |
|
|
|
|
| def mutate_grid(grid: np.ndarray, frame_index: int, seed: int, vec: np.ndarray) -> np.ndarray: |
| shifted = np.roll(grid, shift=(frame_index % 4) - 1, axis=1) |
| shifted = np.roll(shifted, shift=((frame_index * 2) % 5) - 2, axis=0) |
| rng = np.random.default_rng(seed + frame_index * 97) |
| mask = rng.random(grid.shape) < (0.035 + abs(vec[frame_index % len(vec)]) * 0.04) |
| mutated = shifted.copy() |
| mutated[mask] = (mutated[mask] + 1 + frame_index) % max(1, int(grid.max()) + 1) |
| if frame_index % 3 == 0: |
| band = (frame_index * 3) % GRID |
| mutated[band : min(GRID, band + 2), :] = np.fliplr(mutated[band : min(GRID, band + 2), :]) |
| return mutated |
|
|
|
|
| def render_graffiti_gif(frames: list[Image.Image], seed: int) -> str: |
| path = os.path.join(tempfile.gettempdir(), f"dreamwall_graffiti_{seed}.gif") |
| frames[0].save( |
| path, |
| save_all=True, |
| append_images=frames[1:], |
| duration=140, |
| loop=0, |
| optimize=False, |
| ) |
| return path |
|
|
|
|
| def keywords_for_title(prompt: str) -> list[str]: |
| stop = { |
| "the", |
| "and", |
| "with", |
| "from", |
| "that", |
| "this", |
| "into", |
| "through", |
| "around", |
| "made", |
| "your", |
| "their", |
| "over", |
| "under", |
| "for", |
| "a", |
| "an", |
| "of", |
| "in", |
| "on", |
| } |
| words = [word.strip(".,!?;:()[]{}\"'").lower() for word in prompt.split()] |
| words = [word for word in words if len(word) >= 4 and word not in stop] |
| ranked = sorted(set(words), key=lambda word: (-len(word), word)) |
| return ranked[:3] or ["wall", "dream"] |
|
|
|
|
| def artifact_title(prompt: str, seed: int, moods: list[str], object_guess: str | None = None) -> str: |
| normalized_guess = (object_guess or "").strip().lower() |
| prompt_text = (prompt or "").lower() |
| object_titles = { |
| "book": "Book", |
| "earbuds": "AirPods" if "airpod" in prompt_text else "Earbuds", |
| "monitor": "Monitor", |
| "school bag": "School Bag", |
| "phone": "Phone", |
| "water bottle": "Water Bottle", |
| "plush toy": "Teddy Bear" if "teddy" in prompt_text or "bear" in prompt_text else "Plush Toy", |
| "game controller": "Xbox Controller" if "xbox" in prompt_text else "Game Controller", |
| "sticker": "OpenAI Sticker" if "openai" in prompt_text else "Sticker", |
| "shoes": "Shoe" if "shoe" in prompt_text and "shoes" not in prompt_text else "Shoes", |
| "painting": "Painting", |
| "tool": "Tool", |
| } |
| if normalized_guess in object_titles: |
| return object_titles[normalized_guess] |
| keys = keywords_for_title(prompt) |
| prefix_bank = { |
| "cozy": ["Lantern", "Hearth", "Soft"], |
| "cursed": ["Cursed", "Void", "Haunt"], |
| "ancient": ["Relic", "Fossil", "Temple"], |
| "mechanical": ["Circuit", "Signal", "Chrome"], |
| "wild": ["Storm", "Root", "Cloud"], |
| "royal": ["Crown", "Banner", "Gold"], |
| } |
| prefix_options = prefix_bank.get(moods[0], ["Living", "Dream", "Wall"]) |
| prefix = prefix_options[seed % len(prefix_options)] |
| core = " ".join(word.capitalize() for word in keys[:2]) |
| return f"{prefix} {core}" |
|
|
|
|
| def growth_stages(value: float, mutation_rate: float) -> list[dict]: |
| max_stage = 1 |
| if value >= 45: |
| max_stage = 2 |
| if value >= 58: |
| max_stage = 3 |
| if value >= 70: |
| max_stage = 4 |
| if value >= 82 or mutation_rate >= 0.44: |
| max_stage = 5 |
| labels = [ |
| ("seed sketch", "appears as a small 16x16 study tile"), |
| ("wall tile", "claims a full 32x32 block slot"), |
| ("animated mural", "loops all 10 frames on the wall"), |
| ("fusion landmark", "can merge with nearby artifacts"), |
| ("server myth", "earns a named sign and center-wall placement"), |
| ] |
| return [ |
| {"stage": idx + 1, "name": name, "unlocked": idx < max_stage, "meaning": meaning} |
| for idx, (name, meaning) in enumerate(labels) |
| ] |
|
|
|
|
| def living_graffiti(prompt: str, player: str, wall_zone: str): |
| prompt = (prompt or "").strip() or "a glowing bird made of storm clouds" |
| player = (player or "anonymous").strip() |
| wall_zone = (wall_zone or "main wall").strip() |
| text = f"graffiti={player}\nzone={wall_zone}\nprompt={prompt}" |
| seed = stable_seed(text) |
| vec = embedding(text) |
| moods = top_moods(text, vec) |
| palette = palette_from_vector(vec, seed, moods) |
| base_grid = generate_grid(vec, seed, palette) |
| frames = [render_grid(mutate_grid(base_grid, idx, seed, vec), palette) for idx in range(10)] |
| gif_path = render_graffiti_gif(frames, seed) |
| palette_names = [name for name, _ in palette] |
| plot = plot_for_seed(seed) |
| canvas_text, value_packet = canvas_report(prompt, player, moods, palette_names, plot) |
| value_data = json.loads(value_packet) |
| title = artifact_title(prompt, seed, moods) |
| mutation_rate = round(0.18 + abs(vec[5]) * 0.42, 3) |
| permanence = int(40 + value_data["valuation"]["creative_value"] * 0.7) |
| stages = growth_stages(value_data["valuation"]["creative_value"], mutation_rate) |
| unlocked = [stage for stage in stages if stage["unlocked"]] |
| next_locked = next((stage for stage in stages if not stage["unlocked"]), None) |
| footprint = { |
| "blocks": "32 x 32", |
| "frames": 10, |
| "minecraft_area": "1,024 blocks per frame", |
| "wall_slot": f"({plot['x']}, {plot['z']})", |
| "world_origin": f"{plot['world_x']} 80 {plot['world_z']}", |
| "fallback": "first frame can be placed as static map art if animation is not wired yet", |
| } |
| storyboard = [ |
| f"# {title}", |
| f"Creator: **{player}**", |
| f"Size: **32x32 blocks**, **10 frames**, **1,024 blocks per frame**", |
| f"Wall slot: **({plot['x']}, {plot['z']})** in {wall_zone}", |
| f"Minecraft origin: **{plot['world_x']} 80 {plot['world_z']}**", |
| f"Mutation rate: **{mutation_rate}**", |
| f"Creative value: **{value_data['valuation']['creative_value']} demo points**", |
| f"Wall permanence: **{permanence}%**", |
| f"Growth stage: **{unlocked[-1]['stage']} - {unlocked[-1]['name']}**", |
| "", |
| "10-frame loop:", |
| "1. seed image appears", |
| "2. palette drifts", |
| "3. motion band crosses the tile", |
| "4. nearby context mutates the pattern", |
| "5. artifact stabilizes as a named wall memory", |
| "", |
| "Growth path:", |
| ] |
| storyboard.extend( |
| f"- {'unlocked' if stage['unlocked'] else 'locked'} Stage {stage['stage']}: {stage['name']} - {stage['meaning']}" |
| for stage in stages |
| ) |
| if next_locked: |
| storyboard.extend( |
| [ |
| "", |
| f"Next growth target: raise value/mutation to unlock **Stage {next_locked['stage']} - {next_locked['name']}**.", |
| ] |
| ) |
| storyboard.extend( |
| [ |
| "", |
| "Why people use it: they can get their name and idea onto a public Minecraft wall that mutates and fuses with other prompts.", |
| ] |
| ) |
| packet = { |
| "protocol": "living_graffiti.mc.v1", |
| "title": title, |
| "creator": player, |
| "prompt": prompt, |
| "wall_zone": wall_zone, |
| "plot": plot, |
| "frames": 10, |
| "footprint": footprint, |
| "palette": palette_names, |
| "moods": moods, |
| "mutation_rate": mutation_rate, |
| "permanence": permanence, |
| "growth_stages": stages, |
| "market": value_data, |
| "minecraft": { |
| "placement": "animated_wall_tile", |
| "fallback": "use first frame as static map art if animation is unavailable", |
| "block_size": {"width": 32, "height": 32, "frames": 10}, |
| "world_origin": f"{plot['world_x']} 80 {plot['world_z']}", |
| "wall_label": f"{title} by {player}", |
| }, |
| "trace": { |
| "model": MODEL_ID, |
| "codex_track_note": "Codex built the Space, packet contract, and demo scaffold.", |
| }, |
| } |
| return ( |
| gif_path, |
| "\n".join(storyboard), |
| canvas_text, |
| json.dumps(packet, indent=2), |
| ) |
|
|
|
|
| def prompt_rows(prompt_block: str) -> list[str]: |
| rows = [row.strip() for row in (prompt_block or "").splitlines()] |
| return [row for row in rows if row][:18] or LIVING_WALL_PROMPTS |
|
|
|
|
| def attention_weather(value: float, mutation_rate: float, neighbors: int) -> str: |
| if value >= 78 and neighbors >= 2: |
| return "myth storm" |
| if mutation_rate >= 0.43: |
| return "mutation wind" |
| if neighbors >= 2: |
| return "fusion bloom" |
| if value < 45: |
| return "quiet ruins" |
| return "steady glow" |
|
|
|
|
| def summarize_wall_tile(prompt: str, index: int, wall_zone: str, tick: int) -> dict: |
| player = f"builder_{index + 1:02d}" |
| text = f"living-wall={wall_zone}\ntick={tick}\nplayer={player}\nprompt={prompt}" |
| stable_text = f"living-wall={wall_zone}\nplayer={player}\nprompt={prompt}" |
| seed = stable_seed(text) |
| stable_tile_seed = stable_seed(stable_text) |
| vec = embedding(text) |
| moods = top_moods(text, vec) |
| palette = palette_from_vector(vec, seed, moods) |
| palette_names = [name for name, _ in palette] |
| plot = plot_for_seed(stable_tile_seed + index * 31) |
| value = valuation(prompt, moods, palette_names, plot) |
| mutation_rate = round(0.18 + abs(vec[(tick + index) % len(vec)]) * 0.42, 3) |
| stages = growth_stages(value["creative_value"], mutation_rate) |
| stage = [item for item in stages if item["unlocked"]][-1] |
| neighbors = nearby_artworks(plot) |
| return { |
| "title": artifact_title(prompt, seed, moods), |
| "prompt": prompt, |
| "creator": player, |
| "plot": plot, |
| "minecraft_origin": { |
| "x": plot["world_x"], |
| "y": 80, |
| "z": plot["world_z"], |
| }, |
| "minecraft_bounds": { |
| "x1": plot["world_x"], |
| "y1": 80, |
| "z1": plot["world_z"], |
| "x2": plot["world_x"] + PLOT_SCALE - 1, |
| "y2": 80 + PLOT_SCALE - 1, |
| "z2": plot["world_z"], |
| }, |
| "moods": moods, |
| "palette": palette_names, |
| "value": value["creative_value"], |
| "mutation_rate": mutation_rate, |
| "stage": stage, |
| "neighbors": neighbors, |
| "weather": attention_weather(value["creative_value"], mutation_rate, len(neighbors)), |
| } |
|
|
|
|
| def tile_distance(left: dict, right: dict) -> int: |
| return abs(left["plot"]["x"] - right["plot"]["x"]) + abs(left["plot"]["z"] - right["plot"]["z"]) |
|
|
|
|
| def fusion_links(tiles: list[dict]) -> list[dict]: |
| links = [] |
| for left_idx, left in enumerate(tiles): |
| for right_idx, right in enumerate(tiles[left_idx + 1 :], left_idx + 1): |
| distance = tile_distance(left, right) |
| shared_moods = sorted(set(left["moods"]).intersection(right["moods"])) |
| if distance <= 2 or shared_moods: |
| strength = round(max(0.1, (3 - min(distance, 3)) / 3) + len(shared_moods) * 0.18, 2) |
| links.append( |
| { |
| "from": left["title"], |
| "to": right["title"], |
| "from_index": left_idx, |
| "to_index": right_idx, |
| "distance": distance, |
| "shared_moods": shared_moods, |
| "strength": strength, |
| "fusion_name": artifact_title( |
| f"{left['prompt']} {right['prompt']}", |
| stable_seed(left["title"] + right["title"]), |
| (shared_moods or left["moods"] or right["moods"])[:2], |
| ), |
| } |
| ) |
| return sorted(links, key=lambda item: (-item["strength"], item["distance"]))[:12] |
|
|
|
|
| def evolution_events(tiles: list[dict], links: list[dict], tick: int) -> list[str]: |
| events = [] |
| for tile in sorted(tiles, key=lambda item: (-item["stage"]["stage"], -item["value"]))[:4]: |
| events.append( |
| f"tick {tick}: {tile['title']} holds Stage {tile['stage']['stage']} " |
| f"({tile['stage']['name']}) under {tile['weather']}." |
| ) |
| for link in links[:4]: |
| events.append( |
| f"tick {tick}: {link['from']} and {link['to']} create fusion pressure " |
| f"toward {link['fusion_name']}." |
| ) |
| return events |
|
|
|
|
| def render_living_wall_frame(tiles: list[dict], links: list[dict], tick: int, frame: int) -> Image.Image: |
| tile_px = 72 |
| pad = 16 |
| legend_h = 88 |
| width = CANVAS_SIZE * tile_px + pad * 2 |
| height = CANVAS_SIZE * tile_px + pad * 2 + legend_h |
| image = Image.new("RGB", (width, height), (25, 24, 22)) |
| draw = ImageDraw.Draw(image) |
| draw.rectangle([0, 0, width, height], fill=(33, 30, 25)) |
| for x in range(CANVAS_SIZE + 1): |
| xx = pad + x * tile_px |
| draw.line([xx, pad, xx, pad + CANVAS_SIZE * tile_px], fill=(67, 57, 45), width=1) |
| for z in range(CANVAS_SIZE + 1): |
| yy = pad + z * tile_px |
| draw.line([pad, yy, pad + CANVAS_SIZE * tile_px, yy], fill=(67, 57, 45), width=1) |
|
|
| for link in links: |
| left = tiles[link["from_index"]] |
| right = tiles[link["to_index"]] |
| x0 = pad + left["plot"]["x"] * tile_px + tile_px // 2 |
| y0 = pad + left["plot"]["z"] * tile_px + tile_px // 2 |
| x1 = pad + right["plot"]["x"] * tile_px + tile_px // 2 |
| y1 = pad + right["plot"]["z"] * tile_px + tile_px // 2 |
| pulse = int((math.sin((tick + frame) * 0.9 + link["strength"]) + 1) * 35) |
| draw.line([x0, y0, x1, y1], fill=(140 + pulse, 216, 174), width=max(1, int(link["strength"] * 3))) |
|
|
| weather_colors = { |
| "myth storm": (240, 204, 86), |
| "mutation wind": (180, 94, 210), |
| "fusion bloom": (86, 196, 143), |
| "quiet ruins": (112, 112, 118), |
| "steady glow": (104, 168, 220), |
| } |
| for tile in tiles: |
| x0 = pad + tile["plot"]["x"] * tile_px + 5 |
| y0 = pad + tile["plot"]["z"] * tile_px + 5 |
| x1 = x0 + tile_px - 10 |
| y1 = y0 + tile_px - 10 |
| color = weather_colors[tile["weather"]] |
| pulse = int((math.sin((tick + frame + tile["mutation_rate"] * 10) * 0.8) + 1) * 18) |
| fill = tuple(min(255, channel + pulse) for channel in color) |
| draw.rectangle([x0, y0, x1, y1], fill=fill, outline=(246, 235, 196), width=2) |
| if tile["weather"] in {"mutation wind", "myth storm"}: |
| offset = 7 + (frame * 5 + int(tile["mutation_rate"] * 10)) % 34 |
| draw.line([x0 + offset, y0 + 4, x0 + 4, y0 + offset], fill=(255, 245, 180), width=2) |
| if tile["stage"]["stage"] >= 4: |
| draw.rectangle([x0 - 3, y0 - 3, x1 + 3, y1 + 3], outline=(255, 216, 92), width=2) |
| draw.text((x0 + 6, y0 + 6), tile["title"][:10], fill=(22, 20, 18)) |
| draw.text((x0 + 6, y1 - 18), f"S{tile['stage']['stage']} V{int(tile['value'])}", fill=(22, 20, 18)) |
|
|
| y = pad + CANVAS_SIZE * tile_px + 22 |
| draw.text((pad, y), f"Living Canvas tick {tick}.{frame}: prompts claim space, mutate, fuse, and grow into server myths.", fill=(244, 235, 214)) |
| draw.text((pad, y + 28), "Weather: myth storm / mutation wind / fusion bloom / quiet ruins / steady glow", fill=(205, 190, 160)) |
| return image |
|
|
|
|
| def render_living_wall_animation(tiles: list[dict], links: list[dict], tick: int) -> str: |
| frames = [render_living_wall_frame(tiles, links, tick, frame) for frame in range(8)] |
| digest = stable_seed(json.dumps([(tile["title"], tile["plot"], tile["weather"]) for tile in tiles], sort_keys=True)) |
| path = os.path.join(tempfile.gettempdir(), f"dreamwall_living_canvas_{digest}_{tick}.gif") |
| frames[0].save( |
| path, |
| save_all=True, |
| append_images=frames[1:], |
| duration=180, |
| loop=0, |
| optimize=False, |
| ) |
| return path |
|
|
|
|
| def living_wall_canvas(prompt_block: str, wall_zone: str, tick: int): |
| wall_zone = (wall_zone or "main wall").strip() |
| prompts = prompt_rows(prompt_block) |
| tick = int(tick or 0) |
| tiles = [summarize_wall_tile(prompt, idx, wall_zone, tick) for idx, prompt in enumerate(prompts)] |
| links = fusion_links(tiles) |
| events = evolution_events(tiles, links, tick) |
| image = render_living_wall_animation(tiles, links, tick) |
| ranked = sorted(tiles, key=lambda item: (item["stage"]["stage"], item["value"], item["mutation_rate"]), reverse=True) |
| story = [ |
| "# Living Moving Canvas", |
| "Every prompt claims a coordinate. Nearby ideas fuse. The wall changes weather.", |
| "", |
| "Evolution events:", |
| ] |
| story.extend(f"- {event}" for event in events) |
| story.extend( |
| [ |
| "", |
| "Current strongest artifacts:", |
| ] |
| ) |
| for tile in ranked[:5]: |
| story.append( |
| f"- **{tile['title']}** at ({tile['plot']['x']}, {tile['plot']['z']}): " |
| f"Stage {tile['stage']['stage']} {tile['stage']['name']}, {tile['weather']}, " |
| f"value {tile['value']}, mutation {tile['mutation_rate']}" |
| ) |
| packet = { |
| "protocol": "living_canvas.mc.v1", |
| "wall_zone": wall_zone, |
| "tick": tick, |
| "tile_size_blocks": {"width": 32, "height": 32}, |
| "canvas_size_tiles": {"width": CANVAS_SIZE, "height": CANVAS_SIZE}, |
| "minecraft_wall_size_blocks": { |
| "width": CANVAS_SIZE * PLOT_SCALE, |
| "height": CANVAS_SIZE * PLOT_SCALE, |
| }, |
| "mechanics": [ |
| "prompt_to_tile", |
| "neighbor_fusion", |
| "attention_weather", |
| "growth_stage_unlocks", |
| "server_packet_export", |
| ], |
| "minecraft_animation_plan": { |
| "v1": "place stable 32x32 tiles as map art or wool blocks", |
| "v2": "pulse fusion links with particles between tile centers", |
| "v3": "periodically update tile frames from the GIF timeline", |
| "redstone_required": False, |
| }, |
| "fusion_links": links, |
| "evolution_events": events, |
| "tiles": tiles, |
| } |
| return image, "\n".join(story), json.dumps(packet, indent=2) |
|
|
|
|
| def server_packet_json( |
| prompt: str, |
| player: str, |
| gallery_zone: str, |
| origin: str, |
| seed: int, |
| moods: list[str], |
| palette_names: list[str], |
| grid: np.ndarray, |
| commands: str, |
| plot: dict, |
| value_packet: str, |
| ) -> str: |
| value_data = json.loads(value_packet) |
| packet = { |
| "protocol": "dreamwall.mc.v1", |
| "job_id": hashlib.sha256(f"{seed}:{prompt}:{player}:{gallery_zone}".encode("utf-8")).hexdigest()[:16], |
| "status": "approved_for_demo", |
| "player": player, |
| "prompt": prompt, |
| "gallery_zone": gallery_zone, |
| "origin": origin, |
| "moods": moods, |
| "palette": palette_names, |
| "plot": plot, |
| "market": value_data, |
| "grid": { |
| "width": GRID, |
| "height": GRID, |
| "row_runs": row_runs(grid, [(name, color) for name, color in palette_from_names(palette_names)]), |
| }, |
| "minecraft": { |
| "placement": "wall_mosaic", |
| "axis": "east_facing", |
| "worldedit_preview": commands.splitlines()[:40], |
| }, |
| "trace": { |
| "model": MODEL_ID, |
| "small_model_constraint": "local semantic fingerprint engine; no cloud model API", |
| "identity_rule": "prompt + player + gallery zone jointly shape the wall artifact", |
| }, |
| } |
| return json.dumps(packet, indent=2) |
|
|
|
|
| def palette_from_names(names: list[str]) -> list[tuple[str, tuple[int, int, int]]]: |
| lookup = dict(BLOCKS) |
| return [(name, lookup[name]) for name in names if name in lookup] |
|
|
|
|
| def make_art(prompt: str, player: str, origin: str, gallery_zone: str) -> ArtResult: |
| prompt = (prompt or "").strip() |
| player = (player or "anonymous").strip() |
| origin = (origin or "~ ~ ~").strip() |
| gallery_zone = (gallery_zone or "first wall").strip() |
| text = f"player={player}\nzone={gallery_zone}\nprompt={prompt}" |
| seed = stable_seed(text) |
| vec = embedding(text) |
| moods = top_moods(text, vec) |
| palette = palette_from_vector(vec, seed, moods) |
| grid = generate_grid(vec, seed, palette) |
| image = render_grid(grid, palette) |
| palette_names = [name for name, _ in palette] |
| plot = plot_for_seed(seed) |
| if origin == "~ ~ ~": |
| origin = f"{plot['world_x']} 80 {plot['world_z']}" |
| canvas_text, value_packet = canvas_report(prompt, player, moods, palette_names, plot) |
|
|
| profile = { |
| "artist": player, |
| "gallery_zone": gallery_zone, |
| "semantic_moods": moods, |
| "signature_seed": str(seed), |
| "palette": palette_names, |
| "tiny_change_rule": "Every character changes the embedding seed; player and wall zone change the final painting.", |
| } |
| report = ( |
| f"DreamWall read this as a {', '.join(moods)} artifact for {player}.\n\n" |
| f"Palette: {', '.join(palette_names)}.\n\n" |
| "Demo beat: type a prompt, generate the painting, then show the same prompt under another player name " |
| "to prove the wall remembers identity." |
| ) |
| trace = json.dumps( |
| { |
| "model": MODEL_ID, |
| "parameter_count": "local semantic fingerprint engine, far below 32B", |
| "prompt": prompt, |
| "player": player, |
| "gallery_zone": gallery_zone, |
| "moods": moods, |
| "palette": palette_names, |
| }, |
| indent=2, |
| ) |
| commands = compact_commands(grid, palette, origin) |
| server_packet = server_packet_json( |
| prompt=prompt, |
| player=player, |
| gallery_zone=gallery_zone, |
| origin=origin, |
| seed=seed, |
| moods=moods, |
| palette_names=palette_names, |
| grid=grid, |
| commands=commands, |
| plot=plot, |
| value_packet=value_packet, |
| ) |
| return ArtResult(image, profile, palette_names, commands, report, trace, server_packet, canvas_text, value_packet) |
|
|
|
|
| def gradio_generate(prompt: str, player: str, origin: str, gallery_zone: str): |
| result = make_art(prompt, player, origin, gallery_zone) |
| return ( |
| result.image, |
| result.report, |
| result.commands, |
| result.trace, |
| json.dumps(result.profile, indent=2), |
| result.server_packet, |
| result.canvas_report, |
| result.valuation_packet, |
| ) |
|
|
|
|
| CSS = """ |
| :root { |
| --mc-ink: #f4ecd8; |
| --mc-paper: #171410; |
| --mc-green: #57744a; |
| --mc-gold: #d39b45; |
| --museum-stone: #1f211f; |
| --museum-panel: #2b251c; |
| --museum-glow: #f2c15f; |
| --cursor-x: 50vw; |
| --cursor-y: 18vh; |
| } |
| body, .gradio-container { |
| background: |
| radial-gradient(680px circle at var(--cursor-x) var(--cursor-y), rgba(242,193,95,.13), transparent 46%), |
| linear-gradient(135deg, #0d100e 0%, #14120d 34%, #101215 67%, #0a0b09 100%) !important; |
| color: var(--mc-ink); |
| } |
| .gradio-container { |
| position: relative; |
| max-width: none !important; |
| width: 100% !important; |
| padding: 10px 22px 32px !important; |
| } |
| .dreamwall-hero { |
| position: relative; |
| overflow: hidden; |
| background: |
| linear-gradient(90deg, rgba(14,15,13,0.96) 0%, rgba(24,20,14,0.88) 48%, rgba(12,13,11,0.96) 100%), |
| repeating-linear-gradient(90deg, rgba(242,193,95,0.06) 0 1px, transparent 1px 72px); |
| border: 2px solid #8b6a3d; |
| box-shadow: 0 6px 0 #060605, inset 0 0 36px rgba(242, 193, 95, 0.13); |
| padding: 14px 18px; |
| margin: 6px 0 10px; |
| } |
| .dreamwall-hero h1 { |
| margin: 0; |
| font-size: 30px; |
| color: #ffe4a3; |
| } |
| .dreamwall-hero p { |
| max-width: 920px; |
| font-size: 14px; |
| line-height: 1.38; |
| margin: 6px 0; |
| color: #ead7b0; |
| } |
| .badge-row span { |
| display: inline-block; |
| border: 2px solid #8b6a3d; |
| background: #211a12; |
| color: #ffdc8a; |
| padding: 6px 10px; |
| margin: 4px 5px 0 0; |
| font-weight: 700; |
| } |
| .gradio-container .tab-nav, |
| .gradio-container .tabs { |
| background: rgba(8, 9, 8, .52) !important; |
| } |
| .gradio-container button[role="tab"] { |
| border-radius: 0 !important; |
| } |
| .museum-terminal { |
| background: #171916; |
| border: 2px solid #6f6a57; |
| box-shadow: inset 0 0 18px rgba(242, 193, 95, 0.1); |
| padding: 10px 14px; |
| margin: 10px 0; |
| color: #d8c9a6; |
| font-size: 13px; |
| } |
| .museum-kiosk { |
| background: #181713; |
| border: 1px solid #75623d; |
| box-shadow: 0 8px 0 #060605, inset 0 0 22px rgba(242, 193, 95, 0.08); |
| padding: 14px; |
| margin-bottom: 14px; |
| } |
| .main-museum-layout { |
| align-items: stretch; |
| min-height: 0; |
| } |
| .prompt-panel { |
| min-height: 100%; |
| } |
| .prompt-panel .styler, |
| .prompt-panel .block, |
| .prompt-panel .form, |
| .prompt-panel .html-container, |
| .prompt-panel .prose, |
| .prompt-panel .wrap, |
| .prompt-panel .input-container { |
| background: transparent !important; |
| color: var(--mc-ink) !important; |
| } |
| .prompt-panel .block { |
| border-color: #4f432b !important; |
| box-shadow: none !important; |
| } |
| .prompt-panel h2, |
| .resource-hero h2 { |
| margin: 0 0 10px; |
| color: #ffe4a3; |
| } |
| .prompt-panel label, |
| .prompt-panel label span { |
| color: #d8c9a6 !important; |
| } |
| .prompt-panel label.float { |
| background: #181713 !important; |
| border: 1px solid #5c4d32 !important; |
| } |
| .prompt-panel label.container { |
| background: #141410 !important; |
| border: 1px solid #5c4d32 !important; |
| } |
| .prompt-panel textarea { |
| min-height: 76px !important; |
| } |
| .prompt-panel .image-container, |
| .prompt-panel .upload-container { |
| background: #211d16 !important; |
| border-color: #5c4d32 !important; |
| color: #e8dcc1 !important; |
| } |
| .prompt-panel .upload-container button { |
| background: transparent !important; |
| color: #e8dcc1 !important; |
| } |
| .prompt-panel .source-selection { |
| background: #141410 !important; |
| border-top: 1px solid #5c4d32 !important; |
| } |
| .resource-hero { |
| background: #171613; |
| border: 1px solid #5c4d32; |
| padding: 16px 18px; |
| margin: 0 0 12px; |
| } |
| .resource-hero p { |
| margin: 0; |
| max-width: 760px; |
| color: #d6c6a4 !important; |
| } |
| .resource-controls, |
| .resource-controls .form, |
| .resource-controls .wrap, |
| .resource-controls .block { |
| background: transparent !important; |
| color: #e8dcc1 !important; |
| } |
| .resource-controls label, |
| .resource-controls label span { |
| color: #d8c9a6 !important; |
| } |
| .resource-controls input, |
| .resource-controls button, |
| .resource-controls .wrap { |
| border-color: #5c4d32 !important; |
| } |
| .resource-gallery, |
| .resource-gallery .wrap, |
| .resource-gallery .gallery, |
| .resource-gallery .grid-wrap, |
| .resource-gallery .thumbnail, |
| .resource-gallery .thumbnail-item { |
| background: #15140f !important; |
| border-color: #4f432b !important; |
| } |
| .resource-gallery img { |
| background: radial-gradient(circle at 50% 42%, #3a3021, #11100d 70%) !important; |
| border: 1px solid #5c4d32 !important; |
| object-fit: contain !important; |
| } |
| .resource-gallery .caption-label, |
| .resource-gallery .caption, |
| .resource-gallery label { |
| color: #ffe4a3 !important; |
| background: rgba(20, 16, 10, .86) !important; |
| } |
| .museum-kiosk .form { |
| border: 0 !important; |
| } |
| .museum-kiosk textarea, .museum-kiosk input { |
| background: #f4e8cc !important; |
| color: #201913 !important; |
| border: 2px solid #b88b49 !important; |
| font-size: 15px !important; |
| } |
| .museum-kiosk button.primary, .museum-kiosk .primary { |
| min-height: 46px; |
| font-weight: 800 !important; |
| } |
| .photo-drop-compact .image-container, |
| .photo-drop-compact .upload-container { |
| min-height: 92px !important; |
| height: 104px !important; |
| border-style: dashed !important; |
| display: grid !important; |
| align-content: center !important; |
| background: |
| linear-gradient(90deg, rgba(232,197,111,.08), rgba(82,126,148,.08)), |
| #211d16 !important; |
| } |
| .photo-drop-compact img { |
| object-fit: contain !important; |
| max-height: 104px !important; |
| } |
| .photo-drop-compact button { |
| min-height: 92px !important; |
| font-size: 13px !important; |
| font-weight: 800 !important; |
| } |
| .photo-drop-compact .source-selection { |
| min-height: 28px !important; |
| padding: 3px 0 !important; |
| } |
| .museum-stage { |
| background: radial-gradient(circle at 50% 0%, rgba(242,193,95,0.12), transparent 44%), #11120f; |
| border: 1px solid #4f432b; |
| padding: 10px; |
| } |
| .museum-wall { |
| background: #191812; |
| border: 2px solid #7d6335; |
| box-shadow: inset 0 0 24px rgba(0,0,0,0.5); |
| padding: 14px; |
| margin-top: 16px; |
| } |
| .persistent-museum { |
| min-height: 430px; |
| } |
| .wall-heading { |
| display: flex; |
| justify-content: space-between; |
| gap: 16px; |
| align-items: end; |
| border-bottom: 1px solid #5c4d32; |
| padding-bottom: 10px; |
| margin-bottom: 12px; |
| } |
| .wall-heading h3 { |
| margin: 0; |
| color: #ffe4a3; |
| font-size: 22px; |
| } |
| .wall-heading p { |
| margin: 0; |
| max-width: 440px; |
| color: #cdbd9b; |
| font-size: 12px; |
| } |
| .living-map-banner { |
| display: grid; |
| grid-template-columns: auto 1fr; |
| gap: 8px 12px; |
| align-items: center; |
| background: |
| linear-gradient(90deg, rgba(232,197,111,.16), rgba(86,128,96,.13), rgba(57,90,128,.13)), |
| #14130f; |
| border: 1px solid #8b6a3d; |
| box-shadow: inset 0 0 26px rgba(242,193,95,.08); |
| padding: 10px 12px; |
| margin-bottom: 12px; |
| } |
| .living-map-banner span, |
| .you-are-here { |
| color: #1a1308; |
| background: #ffdc8a; |
| border: 1px solid #fff0bd; |
| box-shadow: 0 2px 0 #070604; |
| padding: 4px 8px; |
| font-size: 10px; |
| font-weight: 1000; |
| text-transform: uppercase; |
| letter-spacing: .08em; |
| } |
| .living-map-banner strong { |
| color: #ffe4a3; |
| font-size: 16px; |
| } |
| .living-map-banner p { |
| grid-column: 2; |
| margin: -2px 0 0; |
| color: #d8c9a6 !important; |
| font-size: 12px; |
| } |
| .selected-installation { |
| position: relative; |
| display: grid; |
| grid-template-columns: 126px 1fr; |
| gap: 16px; |
| align-items: center; |
| background: |
| radial-gradient(circle at 12% 18%, color-mix(in srgb, var(--hall-color) 40%, transparent), transparent 34%), |
| linear-gradient(90deg, color-mix(in srgb, var(--hall-color) 24%, #171410), #15120e); |
| border: 1px solid color-mix(in srgb, var(--hall-color) 78%, #171410); |
| padding: 14px; |
| margin-bottom: 14px; |
| min-height: 132px; |
| box-shadow: inset 0 -18px 28px rgba(0,0,0,.18); |
| } |
| .installation-frame { |
| position: relative; |
| min-height: 104px; |
| display: grid; |
| place-items: center; |
| background: |
| linear-gradient(180deg, rgba(255,224,154,.12), transparent), |
| #211b13; |
| border: 1px solid #6f5832; |
| box-shadow: inset 0 0 26px rgba(0,0,0,.32); |
| } |
| .installation-frame img { |
| width: 96px; |
| height: 96px; |
| object-fit: contain; |
| filter: drop-shadow(0 18px 16px rgba(0,0,0,.42)); |
| } |
| .installation-frame span { |
| position: absolute; |
| left: 8px; |
| bottom: 7px; |
| color: #1b1308; |
| background: #e8c56f; |
| padding: 2px 6px; |
| font-size: 10px; |
| font-weight: 900; |
| } |
| .selected-installation strong, |
| .selected-installation em, |
| .selected-installation code { |
| display: block; |
| } |
| .selected-installation strong { |
| color: #ffe8ad; |
| font-size: 22px; |
| line-height: 1.1; |
| } |
| .selected-installation em { |
| color: #d8c9a6; |
| font-style: normal; |
| font-size: 12px; |
| margin-top: 4px; |
| } |
| .selected-installation p { |
| color: #f1dfb8 !important; |
| margin: 8px 0 0; |
| font-size: 13px; |
| } |
| .selected-installation code { |
| width: max-content; |
| max-width: 100%; |
| color: #20160c !important; |
| background: #e8c56f !important; |
| white-space: normal; |
| word-break: break-word; |
| margin-top: 8px; |
| padding: 5px 7px; |
| font-size: 10px; |
| } |
| .museum-concourse { |
| display: grid; |
| grid-template-columns: repeat(3, minmax(0, 1fr)); |
| gap: 12px; |
| } |
| .hall-portal { |
| position: relative; |
| min-height: 246px; |
| overflow: hidden; |
| background: |
| linear-gradient(180deg, color-mix(in srgb, var(--hall-color) 14%, #302417), #11100d 76%), |
| #16130f; |
| border: 1px solid color-mix(in srgb, var(--hall-color) 60%, #171410); |
| padding: 10px; |
| box-shadow: inset 0 -28px 42px rgba(0,0,0,.3); |
| transition: transform 180ms ease, border-color 180ms ease, box-shadow 180ms ease; |
| } |
| .you-are-here { |
| position: absolute; |
| z-index: 4; |
| top: 8px; |
| left: 8px; |
| } |
| .hall-portal:hover, |
| .hall-portal.active { |
| transform: translateY(-4px); |
| border-color: #ffe4a3; |
| box-shadow: 0 9px 0 #070604, 0 0 24px color-mix(in srgb, var(--hall-color) 38%, transparent); |
| } |
| .portal-depth { |
| position: relative; |
| height: 116px; |
| background: |
| linear-gradient(90deg, rgba(0,0,0,.55), transparent 28%, transparent 72%, rgba(0,0,0,.55)), |
| radial-gradient(circle at 50% 100%, color-mix(in srgb, var(--hall-color) 38%, transparent), transparent 45%), |
| repeating-linear-gradient(90deg, rgba(255,255,255,.05) 0 1px, transparent 1px 28px), |
| #17130e; |
| border: 1px solid color-mix(in srgb, var(--hall-color) 38%, #3b2c1a); |
| clip-path: polygon(7% 0, 93% 0, 100% 100%, 0 100%); |
| } |
| .portal-depth::before { |
| content: ""; |
| position: absolute; |
| left: 50%; |
| bottom: 0; |
| width: 46%; |
| height: 86%; |
| transform: translateX(-50%); |
| background: linear-gradient(180deg, color-mix(in srgb, var(--hall-color) 32%, #050403), #050403); |
| border: 8px solid color-mix(in srgb, var(--hall-color) 55%, #6e5229); |
| border-bottom: 0; |
| border-radius: 90px 90px 0 0; |
| box-shadow: inset 0 0 30px rgba(0,0,0,.7); |
| } |
| .portal-sign { |
| position: absolute; |
| left: 18px; |
| right: 18px; |
| top: 14px; |
| z-index: 1; |
| text-align: center; |
| text-shadow: 0 2px 0 #050403; |
| } |
| .portal-sign strong, |
| .portal-sign em { |
| display: block; |
| } |
| .portal-sign strong { |
| color: #ffe4a3; |
| font-size: 15px; |
| line-height: 1.1; |
| } |
| .portal-sign em { |
| color: #dfc989; |
| font-size: 10px; |
| font-style: normal; |
| margin-top: 3px; |
| } |
| .portal-lamp { |
| position: absolute; |
| top: 48px; |
| z-index: 2; |
| width: 12px; |
| height: 20px; |
| background: #ffca6f; |
| box-shadow: 0 0 18px #ffb84a; |
| border: 2px solid #3a2612; |
| } |
| .portal-lamp.left { left: 18px; } |
| .portal-lamp.right { right: 18px; } |
| .portal-copy { |
| display: grid; |
| grid-template-columns: auto 1fr; |
| gap: 10px; |
| align-items: start; |
| margin: 9px 0; |
| } |
| .portal-copy span { |
| color: #1b1308; |
| background: #e8c56f; |
| padding: 3px 7px; |
| font-size: 10px; |
| font-weight: 900; |
| white-space: nowrap; |
| } |
| .portal-copy p { |
| margin: 0; |
| color: #d8c9a6 !important; |
| font-size: 11px; |
| line-height: 1.35; |
| } |
| .portal-relics { |
| display: grid; |
| grid-template-columns: repeat(3, minmax(0, 1fr)); |
| gap: 8px; |
| align-items: stretch; |
| } |
| .portal-relic { |
| position: relative; |
| min-height: 78px; |
| display: grid; |
| grid-template-rows: 44px auto auto; |
| align-items: center; |
| justify-items: center; |
| background: rgba(15,13,10,.62); |
| border: 1px solid rgba(255,228,163,.16); |
| padding: 5px; |
| transition: transform 160ms ease, border-color 160ms ease, background 160ms ease; |
| } |
| .portal-relic > summary { |
| list-style: none; |
| display: grid; |
| grid-template-rows: 44px auto auto; |
| align-items: center; |
| justify-items: center; |
| width: 100%; |
| cursor: pointer; |
| } |
| .portal-relic > summary::-webkit-details-marker { |
| display: none; |
| } |
| .portal-relic[open] { |
| grid-column: span 3; |
| min-height: 164px; |
| align-items: start; |
| } |
| .portal-relic.active { |
| border-color: #ffe4a3; |
| background: rgba(255,228,163,.12); |
| } |
| .portal-relic:hover { |
| transform: translateY(-2px); |
| border-color: rgba(255,228,163,.56); |
| } |
| .portal-relic img { |
| width: 42px; |
| height: 42px; |
| object-fit: contain; |
| filter: drop-shadow(0 8px 8px rgba(0,0,0,.38)); |
| } |
| .portal-relic strong, |
| .portal-relic small { |
| max-width: 100%; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| } |
| .portal-relic strong { |
| color: #fff0bd; |
| font-size: 10px; |
| } |
| .portal-relic small { |
| color: #a99976; |
| font-size: 9px; |
| } |
| .social-peek { |
| position: absolute; |
| right: 6px; |
| top: 6px; |
| z-index: 5; |
| display: inline-flex; |
| align-items: center; |
| gap: 5px; |
| max-width: calc(100% - 12px); |
| opacity: 0; |
| transform: translateY(-4px); |
| pointer-events: none; |
| color: #17100a; |
| background: #ffe0a3; |
| border: 1px solid rgba(255,245,210,.72); |
| box-shadow: 0 5px 12px rgba(0,0,0,.34); |
| padding: 3px 6px; |
| font-size: 10px; |
| line-height: 1; |
| font-weight: 900; |
| transition: opacity 150ms ease, transform 150ms ease; |
| } |
| .social-peek b { |
| display: inline-grid; |
| place-items: center; |
| width: 20px; |
| height: 20px; |
| border-radius: 999px; |
| background: #fff5cf; |
| color: #1d1309; |
| font-size: 9px; |
| } |
| .portal-relic:hover .social-peek, |
| .selected-installation:hover .social-peek { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| .selected-social { |
| top: 12px; |
| right: 12px; |
| } |
| .profile-popout { |
| display: none; |
| margin-top: 8px; |
| padding: 9px; |
| width: 100%; |
| background: rgba(12,10,7,.8); |
| border: 1px solid rgba(255,228,163,.24); |
| color: #e9d9b8; |
| } |
| .portal-relic[open] .profile-popout { |
| display: block; |
| } |
| .profile-popout b, |
| .profile-popout em, |
| .profile-popout code { |
| display: block; |
| } |
| .profile-popout b { |
| color: #ffe4a3; |
| font-size: 12px; |
| } |
| .profile-popout p, |
| .profile-popout em { |
| margin: 5px 0 0; |
| color: #d8c9a6 !important; |
| font-size: 11px; |
| line-height: 1.35; |
| } |
| .profile-popout em { |
| font-style: normal; |
| color: #fff0bd !important; |
| } |
| .profile-popout code { |
| margin-top: 7px; |
| padding: 5px; |
| color: #1b1308 !important; |
| background: #ead7a6 !important; |
| white-space: normal; |
| word-break: break-word; |
| font-size: 10px; |
| } |
| .selected-profile { |
| margin-top: 9px; |
| max-width: 660px; |
| } |
| .selected-profile summary { |
| cursor: pointer; |
| width: max-content; |
| color: #1b1308; |
| background: #ffdc8a; |
| padding: 5px 8px; |
| font-size: 11px; |
| font-weight: 900; |
| } |
| .selected-profile div { |
| margin-top: 8px; |
| padding: 9px; |
| background: rgba(12,10,7,.58); |
| border: 1px solid rgba(255,228,163,.24); |
| } |
| .selected-profile b, |
| .selected-profile span { |
| display: block; |
| color: #f1dfb8; |
| font-size: 12px; |
| line-height: 1.45; |
| } |
| .selected-profile span { |
| color: #d8c9a6; |
| } |
| .relic-grid { |
| display: grid; |
| grid-template-columns: repeat(3, minmax(0, 1fr)); |
| gap: 10px; |
| } |
| .relic-card { |
| position: relative; |
| min-height: 156px; |
| background: |
| linear-gradient(180deg, rgba(255,255,255,0.05), rgba(0,0,0,0.16)), |
| #28231a; |
| border: 1px solid color-mix(in srgb, var(--hall-color) 72%, #171410); |
| padding: 10px; |
| transition: transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease; |
| } |
| .relic-card:hover, .relic-card.active { |
| transform: translateY(-5px); |
| border-color: #ffe4a3; |
| box-shadow: 0 10px 0 #080705, 0 0 22px color-mix(in srgb, var(--hall-color) 46%, transparent); |
| } |
| .relic-object { |
| height: 58px; |
| display: grid; |
| place-items: center; |
| perspective: 180px; |
| } |
| .relic-object span:first-child { |
| width: 52px; |
| height: 40px; |
| transform: rotateX(58deg) rotateZ(-25deg); |
| background: linear-gradient(135deg, var(--hall-color), #f5d27a); |
| border: 4px solid #1b160e; |
| box-shadow: 14px 12px 0 rgba(0,0,0,0.28); |
| } |
| .relic-object span:nth-child(2) { |
| position: absolute; |
| width: 22px; |
| height: 10px; |
| margin-top: 42px; |
| background: #d5b36b; |
| border: 2px solid #1b160e; |
| } |
| .relic-object span:nth-child(3) { |
| position: absolute; |
| width: 76px; |
| height: 9px; |
| margin-top: 66px; |
| background: #44351f; |
| } |
| .relic-copy strong, .relic-copy em, .relic-copy small { |
| display: block; |
| } |
| .relic-copy strong { |
| color: #fff0bd; |
| font-size: 13px; |
| line-height: 1.2; |
| overflow-wrap: anywhere; |
| } |
| .relic-copy em { |
| color: #d8c9a6; |
| font-style: normal; |
| font-size: 11px; |
| margin-top: 4px; |
| overflow-wrap: anywhere; |
| } |
| .relic-copy small { |
| color: #a99976; |
| font-size: 10px; |
| margin-top: 3px; |
| overflow-wrap: anywhere; |
| } |
| .relic-card details { |
| margin-top: 9px; |
| color: #e8dcc1; |
| font-size: 11px; |
| } |
| .relic-card summary { |
| cursor: pointer; |
| color: #ffcf75; |
| font-weight: 800; |
| } |
| .relic-card code { |
| display: block; |
| white-space: normal; |
| word-break: break-word; |
| background: #f3e4c3 !important; |
| color: #1e170f !important; |
| padding: 6px; |
| margin-top: 6px; |
| } |
| .selected-strip { |
| position: relative; |
| display: grid; |
| grid-template-columns: 58px 1fr; |
| gap: 12px; |
| align-items: center; |
| background: linear-gradient(90deg, color-mix(in srgb, var(--hall-color) 22%, #171410), #171410); |
| border: 1px solid color-mix(in srgb, var(--hall-color) 78%, #171410); |
| padding: 12px; |
| margin-bottom: 12px; |
| } |
| .story-caption { |
| position: absolute; |
| left: 10px; |
| right: 10px; |
| bottom: 8px; |
| z-index: 2; |
| display: block; |
| opacity: 0; |
| transform: translateY(8px); |
| pointer-events: none; |
| color: #16120c; |
| background: linear-gradient(90deg, rgba(255, 200, 85, .92), rgba(98, 165, 255, .84), rgba(255, 134, 180, .88)); |
| border: 1px solid rgba(255, 245, 210, .6); |
| box-shadow: 0 8px 18px rgba(0,0,0,.34); |
| padding: 7px 9px; |
| font-size: 11px; |
| line-height: 1.3; |
| font-weight: 800; |
| transition: opacity 160ms ease, transform 160ms ease; |
| display: flex; |
| align-items: center; |
| gap: 7px; |
| } |
| .story-caption b { |
| display: inline-grid; |
| place-items: center; |
| flex: 0 0 auto; |
| width: 24px; |
| height: 24px; |
| border-radius: 8px; |
| background: #fff1bc; |
| color: #1d1309; |
| box-shadow: 0 2px 0 rgba(0,0,0,.25); |
| font-size: 10px; |
| } |
| .story-caption span { |
| min-width: 0; |
| } |
| .selected-caption { |
| left: 78px; |
| right: 14px; |
| bottom: 12px; |
| } |
| .selected-installation .selected-caption { |
| left: 148px; |
| } |
| .artifact-chip:hover .story-caption, |
| .selected-strip:hover .story-caption { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| .selected-strip strong, |
| .selected-strip em, |
| .selected-strip code { |
| display: block; |
| } |
| .selected-strip strong { |
| color: #ffe8ad; |
| font-size: 16px; |
| line-height: 1.2; |
| } |
| .selected-strip em { |
| color: #d8c9a6; |
| font-style: normal; |
| font-size: 12px; |
| margin-top: 3px; |
| } |
| .selected-strip code { |
| color: #20160c !important; |
| background: #e8c56f !important; |
| white-space: normal; |
| word-break: break-word; |
| margin-top: 6px; |
| padding: 5px 7px; |
| font-size: 10px; |
| } |
| .hall-grid { |
| display: grid; |
| grid-template-columns: repeat(3, minmax(0, 1fr)); |
| gap: 10px; |
| } |
| .hall-wing { |
| min-height: 168px; |
| background: |
| linear-gradient(180deg, rgba(255,255,255,0.04), rgba(0,0,0,0.16)), |
| color-mix(in srgb, var(--hall-color) 13%, #201a12); |
| border: 1px solid color-mix(in srgb, var(--hall-color) 58%, #171410); |
| padding: 10px; |
| box-shadow: inset 0 0 20px rgba(0,0,0,.28); |
| } |
| .hall-wing.active { |
| border-color: #ffe4a3; |
| box-shadow: 0 8px 0 #070604, 0 0 24px color-mix(in srgb, var(--hall-color) 36%, transparent); |
| } |
| .hall-wing header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| gap: 8px; |
| border-bottom: 1px solid color-mix(in srgb, var(--hall-color) 44%, #46351d); |
| padding-bottom: 6px; |
| } |
| .hall-wing h4 { |
| margin: 0; |
| color: #ffe4a3; |
| font-size: 13px; |
| line-height: 1.2; |
| } |
| .hall-wing header span { |
| color: #1b1308; |
| background: #e8c56f; |
| padding: 2px 6px; |
| font-size: 10px; |
| font-weight: 800; |
| white-space: nowrap; |
| } |
| .hall-wing p { |
| color: #d8c9a6 !important; |
| font-size: 11px; |
| line-height: 1.35; |
| min-height: 42px; |
| margin: 8px 0; |
| } |
| .artifact-chip-row { |
| display: grid; |
| gap: 6px; |
| } |
| .artifact-chip { |
| position: relative; |
| display: grid; |
| grid-template-columns: 32px 1fr; |
| gap: 7px; |
| align-items: center; |
| min-height: 42px; |
| background: rgba(15,13,10,.54); |
| border: 1px solid rgba(255,228,163,.12); |
| padding: 5px; |
| transition: transform 160ms ease, border-color 160ms ease, background 160ms ease; |
| } |
| .artifact-chip.active { |
| border-color: #ffe4a3; |
| background: rgba(255,228,163,.1); |
| } |
| .artifact-chip:hover { |
| transform: translateY(-2px); |
| border-color: rgba(255,228,163,.5); |
| } |
| .artifact-chip strong, |
| .artifact-chip em, |
| .artifact-chip small { |
| display: block; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| } |
| .artifact-chip strong { |
| color: #fff0bd; |
| font-size: 11px; |
| } |
| .artifact-chip em { |
| color: #cdbd9b; |
| font-style: normal; |
| font-size: 10px; |
| } |
| .artifact-chip small { |
| color: #a99976; |
| font-size: 9px; |
| } |
| .mini-cube { |
| width: 25px; |
| height: 18px; |
| display: block; |
| transform: rotateX(58deg) rotateZ(-28deg); |
| background: linear-gradient(135deg, var(--hall-color), #ffe1a1); |
| border: 3px solid #15100a; |
| box-shadow: 8px 7px 0 rgba(0,0,0,.3); |
| } |
| .mini-cube.selected { |
| width: 42px; |
| height: 32px; |
| } |
| .more-chip { |
| display: inline-block; |
| width: max-content; |
| color: #1b1308; |
| background: #cfa861; |
| font-weight: 800; |
| font-size: 10px; |
| padding: 3px 7px; |
| } |
| .artifact-model-shell { |
| background: #11120f; |
| border: 2px solid #7d6335; |
| box-shadow: inset 0 0 28px rgba(242,193,95,.08), 0 8px 0 #070604; |
| padding: 8px; |
| height: 100%; |
| } |
| .artifact-model-frame { |
| width: 100%; |
| height: min(44vh, 460px); |
| min-height: 360px; |
| border: 0; |
| display: block; |
| background: #10110f; |
| } |
| .command-copy textarea { |
| min-height: 46px !important; |
| height: 46px !important; |
| white-space: nowrap !important; |
| overflow-x: auto !important; |
| resize: none !important; |
| } |
| .command-copy .wrap { |
| min-height: 46px !important; |
| } |
| .texture-inspector { |
| background: |
| radial-gradient(circle at 20% 12%, rgba(255,228,163,.11), transparent 28%), |
| linear-gradient(90deg, #21180f 0 49%, #15110c 50% 100%); |
| border: 2px solid #7d6335; |
| padding: 16px; |
| margin: 12px 0; |
| box-shadow: 0 8px 0 #070604, inset 0 0 30px rgba(0,0,0,.28); |
| } |
| .texture-inspector header { |
| display: flex; |
| justify-content: space-between; |
| gap: 16px; |
| align-items: end; |
| border-bottom: 1px solid #5c4d32; |
| padding-bottom: 10px; |
| } |
| .texture-inspector h3 { |
| margin: 4px 0 0; |
| color: #ffe4a3; |
| font-size: 26px; |
| } |
| .texture-inspector header span { |
| color: #e7bf78; |
| font-size: 11px; |
| font-weight: 900; |
| letter-spacing: .08em; |
| text-transform: uppercase; |
| } |
| .texture-inspector p { |
| margin: 0; |
| max-width: 650px; |
| color: #cdbd9b; |
| font-size: 12px; |
| } |
| .texture-inspector > div { |
| display: grid; |
| grid-template-columns: repeat(4, minmax(0, 1fr)); |
| gap: 10px; |
| margin-top: 12px; |
| } |
| .texture-inspect-card { |
| display: grid; |
| grid-template-columns: 112px 1fr; |
| gap: 10px; |
| min-height: 128px; |
| background: |
| linear-gradient(180deg, rgba(255,255,255,.04), rgba(0,0,0,.16)), |
| #242017; |
| border: 1px solid #5e523d; |
| padding: 9px; |
| transition: transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease; |
| } |
| .texture-inspect-card:hover { |
| transform: translateY(-4px); |
| border-color: #ffe4a3; |
| box-shadow: 0 8px 0 #080705; |
| } |
| .texture-inspect-card img { |
| width: 112px; |
| height: 112px; |
| object-fit: contain; |
| background: radial-gradient(circle at 50% 42%, #332d20, #13120f 72%); |
| border: 1px solid #3f382a; |
| filter: drop-shadow(0 14px 12px rgba(0,0,0,.34)); |
| } |
| .texture-inspect-card strong, |
| .texture-inspect-card span, |
| .texture-inspect-card code { |
| display: block; |
| overflow-wrap: anywhere; |
| } |
| .texture-inspect-card strong { |
| color: #fff0bd; |
| font-size: 13px; |
| } |
| .texture-inspect-card span { |
| color: #cdbd9b; |
| font-size: 11px; |
| margin-top: 4px; |
| } |
| .texture-inspect-card details { |
| margin-top: 7px; |
| } |
| .texture-inspect-card summary { |
| cursor: pointer; |
| color: #ffdc8a; |
| font-size: 11px; |
| font-weight: 900; |
| } |
| .texture-inspect-card code { |
| color: #11100c !important; |
| background: #ead7a6 !important; |
| margin-top: 7px; |
| padding: 5px; |
| font-size: 10px; |
| } |
| .waypoint-strip { |
| display: grid; |
| grid-template-columns: repeat(5, minmax(80px, 1fr)); |
| gap: 8px; |
| align-items: stretch; |
| background: #171613; |
| border: 1px solid #75623d; |
| padding: 10px; |
| margin-top: 10px; |
| } |
| .waypoint-strip div { |
| background: #242017; |
| border: 1px solid #5c4d32; |
| padding: 8px 10px; |
| } |
| .waypoint-strip span, |
| .waypoint-grid span { |
| display: block; |
| color: #bca982; |
| font-size: 10px; |
| text-transform: uppercase; |
| letter-spacing: .06em; |
| } |
| .waypoint-strip strong, |
| .waypoint-grid strong { |
| display: block; |
| color: #ffe4a3; |
| font-size: 18px; |
| } |
| .waypoint-strip code, |
| .waypoint-card code { |
| display: block; |
| align-self: center; |
| background: #ead7a6 !important; |
| color: #1b1308 !important; |
| padding: 10px; |
| overflow-wrap: anywhere; |
| } |
| .waypoint-card { |
| background: |
| radial-gradient(900px circle at 70% 0%, rgba(83,133,119,.16), transparent 40%), |
| #171613; |
| border: 2px solid #75623d; |
| box-shadow: 0 8px 0 #070604; |
| padding: 16px; |
| } |
| .waypoint-card header { |
| display: flex; |
| justify-content: space-between; |
| gap: 16px; |
| border-bottom: 1px solid #5c4d32; |
| margin-bottom: 12px; |
| } |
| .waypoint-card h3 { |
| margin: 0 0 8px; |
| color: #ffe4a3; |
| font-size: 24px; |
| } |
| .waypoint-card p { |
| margin: 0 0 8px; |
| color: #cdbd9b !important; |
| } |
| .waypoint-grid { |
| display: grid; |
| grid-template-columns: repeat(4, minmax(0, 1fr)); |
| gap: 10px; |
| margin-bottom: 12px; |
| } |
| .waypoint-grid div { |
| background: #242017; |
| border: 1px solid #5c4d32; |
| padding: 12px; |
| } |
| .placement-hero-strip { |
| display: grid; |
| grid-template-columns: auto minmax(0, 1fr) auto; |
| gap: 12px; |
| align-items: center; |
| background: |
| linear-gradient(90deg, rgba(126,182,255,.18), rgba(242,193,95,.12)), |
| #111512; |
| border: 1px solid #74613b; |
| box-shadow: inset 0 0 24px rgba(242,193,95,.08); |
| padding: 12px; |
| margin-bottom: 14px; |
| } |
| .placement-hero-strip span { |
| color: #07111d; |
| background: #9fd0ff; |
| border: 1px solid #d9efff; |
| box-shadow: 0 3px 0 #05070a; |
| font-size: 10px; |
| font-weight: 1000; |
| letter-spacing: .08em; |
| padding: 5px 8px; |
| } |
| .placement-hero-strip strong { |
| color: #ffe4a3; |
| font-size: 22px; |
| line-height: 1.1; |
| } |
| .placement-hero-strip em { |
| justify-self: end; |
| color: #e6d2a6; |
| font-style: normal; |
| font-weight: 800; |
| } |
| .minecraft-campus-view { |
| margin: 14px 0; |
| background: |
| linear-gradient(180deg, rgba(58,82,61,.2), transparent 34%), |
| #11100c; |
| border: 1px solid #6b5733; |
| overflow: hidden; |
| } |
| .campus-heading { |
| display: grid; |
| grid-template-columns: auto 1fr; |
| gap: 4px 12px; |
| align-items: center; |
| padding: 12px; |
| border-bottom: 1px solid #4e422d; |
| } |
| .campus-heading span { |
| grid-row: span 2; |
| align-self: start; |
| color: #161006; |
| background: #ffdc8a; |
| border: 1px solid #fff0bd; |
| padding: 4px 8px; |
| font-size: 10px; |
| font-weight: 1000; |
| text-transform: uppercase; |
| letter-spacing: .08em; |
| } |
| .campus-heading h4 { |
| margin: 0; |
| color: #ffe4a3; |
| font-size: 18px; |
| line-height: 1.15; |
| } |
| .campus-heading p { |
| margin: 0; |
| color: #d8c9a6 !important; |
| font-size: 12px; |
| line-height: 1.35; |
| } |
| .campus-stage { |
| position: relative; |
| height: 500px; |
| overflow: hidden; |
| background: |
| radial-gradient(circle at 50% 14%, rgba(255,219,132,.18), transparent 30%), |
| linear-gradient(180deg, #171309, #080806 68%); |
| } |
| .campus-stage::before { |
| content: ""; |
| position: absolute; |
| left: 8%; |
| right: 8%; |
| bottom: 28px; |
| height: 62px; |
| background: radial-gradient(ellipse, rgba(0,0,0,.72), transparent 68%); |
| } |
| .campus-stage-inner { |
| position: absolute; |
| inset: 38px 0 0; |
| } |
| .campus-tile, |
| .campus-gate, |
| .campus-entry-beacon, |
| .campus-relic-pin { |
| position: absolute; |
| display: grid; |
| place-items: center; |
| } |
| .campus-tile { |
| width: 44px; |
| height: 44px; |
| transform: rotate(45deg) skew(-10deg, -10deg); |
| transform-origin: center; |
| background: color-mix(in srgb, var(--hall-color) 24%, #242017); |
| border: 1px solid color-mix(in srgb, var(--hall-color) 54%, #66512e); |
| box-shadow: 4px 4px 0 rgba(0,0,0,.45), inset -5px -5px 0 rgba(0,0,0,.18); |
| } |
| .campus-tile b { |
| transform: rotate(-45deg) skew(10deg, 10deg); |
| color: rgba(255,241,199,.44); |
| font-size: 0; |
| line-height: 1; |
| } |
| .campus-tile.entry-proxy { |
| border-color: #9ce5ff; |
| } |
| .campus-tile.route { |
| background: color-mix(in srgb, #e2bc5e 68%, #262014); |
| border-color: #ffe4a3; |
| box-shadow: 0 0 14px rgba(255,219,132,.38), 4px 4px 0 rgba(0,0,0,.5); |
| } |
| .campus-tile.target { |
| background: color-mix(in srgb, var(--hall-color) 60%, #ffe4a3); |
| border-color: #fff4c8; |
| z-index: 8; |
| box-shadow: 0 0 24px color-mix(in srgb, var(--hall-color) 70%, transparent), 5px 5px 0 rgba(0,0,0,.48); |
| } |
| .campus-gate { |
| width: 116px; |
| min-height: 72px; |
| translate: -36px 0; |
| color: #ffe4a3; |
| background: |
| linear-gradient(90deg, #7b5426 0 12px, transparent 12px calc(100% - 12px), #7b5426 calc(100% - 12px)), |
| radial-gradient(ellipse at 50% 100%, color-mix(in srgb, var(--hall-color) 24%, #090806), #050403 68%); |
| border: 1px solid color-mix(in srgb, var(--hall-color) 48%, #6b5733); |
| box-shadow: 0 10px 0 rgba(0,0,0,.42); |
| z-index: 4; |
| } |
| .campus-gate b { |
| max-width: 92px; |
| color: #ffe4a3; |
| font-size: 12px; |
| line-height: 1.05; |
| text-align: center; |
| } |
| .campus-entry-beacon { |
| width: 68px; |
| min-height: 84px; |
| translate: -16px -18px; |
| color: #101018; |
| background: |
| linear-gradient(180deg, #b9f2ff, #6aa8ff 68%, #1e4d68); |
| border: 2px solid #e8fbff; |
| box-shadow: 0 0 24px rgba(135,222,255,.7), 0 12px 0 rgba(0,0,0,.45); |
| z-index: 12; |
| } |
| .campus-entry-beacon b { |
| font-size: 9px; |
| line-height: 1.04; |
| text-align: center; |
| } |
| .campus-relic-pin { |
| min-width: 112px; |
| max-width: 168px; |
| min-height: 44px; |
| translate: -36px -10px; |
| color: #1b1308; |
| background: #ffe4a3; |
| border: 2px solid #fff4c8; |
| box-shadow: 0 10px 0 rgba(0,0,0,.48), 0 0 18px rgba(255,219,132,.4); |
| padding: 6px 8px; |
| z-index: 14; |
| } |
| .campus-relic-pin::after { |
| content: ""; |
| position: absolute; |
| left: 30px; |
| bottom: -12px; |
| width: 10px; |
| height: 10px; |
| background: #ffe4a3; |
| transform: rotate(45deg); |
| } |
| .campus-relic-pin b { |
| font-size: 11px; |
| line-height: 1.08; |
| text-align: center; |
| } |
| .campus-proof-strip { |
| display: grid; |
| grid-template-columns: repeat(3, minmax(0, 1fr)); |
| gap: 8px; |
| padding: 10px 12px 12px; |
| border-top: 1px solid #4e422d; |
| } |
| .campus-proof-strip span { |
| min-width: 0; |
| color: #ffe4a3; |
| background: #242017; |
| border: 1px solid #5c4d32; |
| padding: 8px; |
| font-size: 11px; |
| overflow-wrap: anywhere; |
| } |
| .living-route-map { |
| display: grid; |
| grid-template-columns: minmax(190px, .62fr) minmax(360px, 1.45fr); |
| gap: 12px; |
| align-items: start; |
| margin: 14px 0; |
| } |
| .route-map-copy { |
| min-height: 100%; |
| background: linear-gradient(180deg, rgba(255,228,163,.08), #17130e); |
| border: 1px solid #5c4d32; |
| padding: 12px; |
| } |
| .route-map-copy span, |
| .route-map-proof span, |
| .route-map-legend span { |
| color: #baa985; |
| font-size: 10px; |
| text-transform: uppercase; |
| letter-spacing: .05em; |
| } |
| .route-map-copy h4 { |
| margin: 6px 0; |
| color: #ffe4a3; |
| font-size: 18px; |
| line-height: 1.12; |
| } |
| .route-map-copy p { |
| margin: 0; |
| color: #d8c9a6 !important; |
| font-size: 12px; |
| line-height: 1.45; |
| } |
| .route-map-grid { |
| display: grid; |
| grid-template-columns: repeat(12, minmax(22px, 1fr)); |
| gap: 3px; |
| background: |
| linear-gradient(180deg, rgba(255,228,163,.08), transparent), |
| #0f0e0b; |
| border: 1px solid #6b5733; |
| padding: 8px; |
| } |
| .map-cell { |
| position: relative; |
| aspect-ratio: 1; |
| min-width: 0; |
| display: grid; |
| place-items: center; |
| background: color-mix(in srgb, var(--hall-color) 18%, #201b13); |
| border: 1px solid color-mix(in srgb, var(--hall-color) 42%, #2a241a); |
| box-shadow: inset 0 -5px 0 rgba(0,0,0,.18); |
| } |
| .map-cell b { |
| color: rgba(255,241,199,.62); |
| font-size: 8px; |
| line-height: 1; |
| } |
| .map-cell.route { |
| background: color-mix(in srgb, #d6a642 58%, #201b13); |
| border-color: #f5ce78; |
| box-shadow: 0 0 10px rgba(245,206,120,.42), inset 0 -5px 0 rgba(0,0,0,.2); |
| } |
| .map-cell.entry { |
| background: #3b6a88; |
| border-color: #9ce5ff; |
| } |
| .map-cell.entry::after, |
| .map-cell.target::after { |
| position: absolute; |
| inset: 3px; |
| display: grid; |
| place-items: center; |
| color: #120d08; |
| background: #ffe4a3; |
| font-size: 8px; |
| font-weight: 900; |
| } |
| .map-cell.entry::after { |
| content: "YOU"; |
| } |
| .map-cell.target { |
| background: color-mix(in srgb, var(--hall-color) 62%, #ffe4a3); |
| border-color: #fff0bd; |
| box-shadow: 0 0 18px color-mix(in srgb, var(--hall-color) 62%, transparent); |
| } |
| .map-cell.target::after { |
| content: "RELIC"; |
| } |
| .route-map-proof { |
| grid-column: 1 / -1; |
| background: #0e0f0d; |
| border: 1px solid #5c4d32; |
| padding: 10px; |
| } |
| .route-map-proof summary, |
| .waypoint-appendix summary { |
| color: #ffe4a3; |
| cursor: pointer; |
| font-weight: 900; |
| } |
| .route-map-proof div { |
| display: inline-grid; |
| min-width: min(220px, 100%); |
| background: #242017; |
| border: 1px solid #5c4d32; |
| padding: 9px; |
| margin: 8px 6px 0 0; |
| } |
| .route-map-proof strong { |
| display: block; |
| margin-top: 3px; |
| color: #ffe4a3; |
| font-size: 13px; |
| line-height: 1.2; |
| } |
| .route-map-legend { |
| grid-column: 1 / -1; |
| display: flex; |
| flex-wrap: wrap; |
| gap: 8px; |
| align-items: center; |
| } |
| .route-map-legend span { |
| display: inline-flex; |
| gap: 6px; |
| align-items: center; |
| color: #d9caa7; |
| } |
| .route-map-legend b { |
| width: 13px; |
| height: 13px; |
| display: inline-block; |
| border: 1px solid #6b5733; |
| } |
| .legend-entry { background: #3b6a88; } |
| .legend-route { background: #d6a642; } |
| .legend-target { background: #ffe4a3; } |
| .waypoint-appendix { |
| background: #0e0f0d; |
| border: 1px solid #5c4d32; |
| padding: 12px; |
| } |
| .server-setup { |
| background: |
| radial-gradient(circle at 16% 10%, rgba(126,182,255,.13), transparent 30%), |
| radial-gradient(circle at 88% 0%, rgba(242,193,95,.14), transparent 34%), |
| #151410; |
| border: 2px solid #75623d; |
| box-shadow: 0 8px 0 #070604; |
| color: #f5ddb0; |
| padding: 20px; |
| } |
| .server-setup h2 { |
| margin: 0 0 8px; |
| color: #ffe4a3; |
| font-size: 30px; |
| } |
| .server-setup p { |
| color: #d4c4a0 !important; |
| } |
| .museum-built-card { |
| display: grid; |
| gap: 6px; |
| background: |
| linear-gradient(135deg, rgba(75, 124, 92, .24), rgba(126,182,255,.10)), |
| #101611; |
| border: 1px solid #6e7b46; |
| padding: 14px; |
| margin-top: 14px; |
| } |
| .museum-built-card span { |
| color: #f2c15f; |
| font-size: 12px; |
| font-weight: 1000; |
| letter-spacing: .06em; |
| text-transform: uppercase; |
| } |
| .museum-built-card strong { |
| color: #eaffd4; |
| font-size: clamp(18px, 2.2vw, 28px); |
| } |
| .museum-built-card p { |
| margin: 0; |
| max-width: 820px; |
| } |
| .join-card { |
| display: grid; |
| grid-template-columns: auto minmax(0, 1fr); |
| gap: 4px 12px; |
| align-items: center; |
| background: |
| linear-gradient(90deg, rgba(126,182,255,.18), rgba(242,193,95,.1)), |
| #101412; |
| border: 1px solid #6e5d39; |
| padding: 14px; |
| margin-top: 14px; |
| } |
| .join-card span { |
| grid-row: span 2; |
| color: #07111d; |
| background: #9fd0ff; |
| border: 1px solid #d9efff; |
| font-weight: 1000; |
| padding: 6px 9px; |
| text-transform: uppercase; |
| letter-spacing: .06em; |
| font-size: 11px; |
| } |
| .join-card strong { |
| display: block; |
| color: #ffe4a3; |
| font-size: clamp(22px, 3vw, 34px); |
| line-height: 1; |
| overflow-wrap: anywhere; |
| } |
| .join-card p { |
| margin: 0; |
| font-size: 12px; |
| } |
| .server-grid { |
| display: grid; |
| grid-template-columns: repeat(3, minmax(0, 1fr)); |
| gap: 12px; |
| margin: 18px 0; |
| } |
| .server-grid article { |
| background: #242017; |
| border: 1px solid #5c4d32; |
| padding: 14px; |
| min-height: 166px; |
| } |
| .server-grid article span { |
| display: inline-grid; |
| place-items: center; |
| width: 28px; |
| height: 28px; |
| background: #f2c15f; |
| color: #1b1308; |
| font-weight: 900; |
| margin-bottom: 10px; |
| } |
| .server-grid h3 { |
| margin: 0 0 8px; |
| color: #ffe4a3; |
| font-size: 18px; |
| } |
| .server-setup code { |
| background: #ead7a6 !important; |
| color: #1b1308 !important; |
| padding: 2px 6px; |
| overflow-wrap: anywhere; |
| } |
| .relic-server-handoff { |
| border: 2px solid #75623d; |
| background: |
| radial-gradient(circle at 12% 10%, rgba(126,182,255,.14), transparent 30%), |
| #151410; |
| color: #f5ddb0; |
| padding: 14px; |
| margin-top: 12px; |
| } |
| .relic-server-handoff > div:first-child span { |
| color: #f2c15f; |
| font-weight: 1000; |
| text-transform: uppercase; |
| letter-spacing: .06em; |
| font-size: 11px; |
| } |
| .relic-server-handoff strong { |
| display: block; |
| color: #ffe4a3; |
| font-size: 22px; |
| margin-top: 4px; |
| } |
| .visitor-command-card { |
| display: grid; |
| gap: 8px; |
| border: 1px solid #6e7b46; |
| background: #101611; |
| padding: 12px; |
| margin-top: 10px; |
| } |
| .visitor-command-card span { |
| color: #eaffd4; |
| font-weight: 900; |
| } |
| .visitor-command-card code { |
| display: block; |
| background: #ead7a6 !important; |
| color: #1b1308 !important; |
| padding: 10px; |
| white-space: normal; |
| overflow-wrap: anywhere; |
| } |
| .visitor-command-card small { |
| color: #d4c4a0; |
| } |
| .handoff-proof { |
| display: flex; |
| gap: 8px; |
| flex-wrap: wrap; |
| margin-top: 10px; |
| } |
| .handoff-proof b { |
| border: 1px solid #5c4d32; |
| background: #242017; |
| color: #ffe4a3; |
| padding: 7px 9px; |
| } |
| .server-formula { |
| display: grid; |
| grid-template-columns: 180px repeat(3, minmax(0, 1fr)); |
| gap: 8px; |
| align-items: center; |
| background: #0e0f0d; |
| border: 1px solid #5c4d32; |
| padding: 12px; |
| margin-top: 10px; |
| } |
| .server-note { |
| margin: 16px 0 0; |
| border-left: 4px solid #f25a0b; |
| padding-left: 12px; |
| } |
| .server-appendix { |
| margin-top: 12px; |
| background: #0e0f0d; |
| border: 1px solid #5c4d32; |
| padding: 12px; |
| } |
| .server-appendix summary { |
| color: #ffe4a3; |
| cursor: pointer; |
| font-weight: 800; |
| } |
| .server-appendix p { |
| margin: 10px 0 0; |
| } |
| .server-config-panel { |
| margin-top: 18px; |
| padding: 16px; |
| border: 2px solid #75623d; |
| background: #171613; |
| } |
| .server-config-heading h3 { |
| margin: 0 0 6px; |
| color: #ffe4a3; |
| font-size: 22px; |
| } |
| .server-config-heading p { |
| margin: 0 0 12px; |
| color: #d4c4a0 !important; |
| } |
| .server-config-panel textarea, |
| .server-config-panel input { |
| background: #ead7a6 !important; |
| color: #1b1308 !important; |
| border: 1px solid #8d6d37 !important; |
| } |
| .server-config-panel button { |
| background: #f25a0b !important; |
| color: #fff8e6 !important; |
| border: 0 !important; |
| font-weight: 900 !important; |
| } |
| .server-owner-card { |
| margin: 16px 0; |
| border: 2px solid #8d6d37; |
| background: |
| linear-gradient(135deg, rgba(74, 112, 83, .18), transparent 46%), |
| #12130f; |
| box-shadow: 0 8px 0 #070604; |
| padding: 16px; |
| color: #f5ddb0; |
| } |
| .server-owner-card header { |
| display: grid; |
| grid-template-columns: minmax(0, 1fr) auto; |
| gap: 14px; |
| align-items: start; |
| } |
| .server-owner-columns { |
| display: grid; |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| gap: 14px; |
| align-items: start; |
| } |
| .server-owner-card header { |
| border-bottom: 1px solid #5c4d32; |
| padding-bottom: 12px; |
| margin-bottom: 12px; |
| } |
| .server-owner-card header span, |
| .server-owner-grid article span { |
| display: block; |
| color: #c9a85a; |
| font-size: 12px; |
| font-weight: 900; |
| text-transform: uppercase; |
| } |
| .server-owner-card h3, |
| .server-owner-card h4 { |
| margin: 4px 0 8px; |
| color: #ffe4a3; |
| } |
| .server-owner-card header b { |
| background: #f2c15f; |
| color: #1b1308; |
| padding: 8px 10px; |
| font-weight: 900; |
| } |
| .server-owner-grid { |
| display: grid; |
| grid-template-columns: repeat(3, minmax(0, 1fr)); |
| gap: 10px; |
| margin-bottom: 14px; |
| } |
| .server-owner-grid article, |
| .server-owner-columns > div { |
| background: #201c14; |
| border: 1px solid #5c4d32; |
| padding: 12px; |
| } |
| .server-owner-card code { |
| background: #ead7a6 !important; |
| color: #1b1308 !important; |
| padding: 2px 6px; |
| overflow-wrap: anywhere; |
| } |
| .server-owner-card ol { |
| margin: 0; |
| padding-left: 20px; |
| } |
| .server-owner-card li { |
| margin: 0 0 8px; |
| } |
| .server-owner-card li span { |
| display: block; |
| margin-top: 4px; |
| color: #d4c4a0; |
| overflow-wrap: anywhere; |
| } |
| .server-owner-command-strip { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 8px; |
| margin-bottom: 12px; |
| } |
| .server-owner-note { |
| margin: 12px 0 0; |
| color: #d4c4a0 !important; |
| } |
| .demo-path { |
| border: 2px solid #8d6d37; |
| background: |
| linear-gradient(135deg, rgba(242, 193, 95, .12), transparent 42%), |
| #15130f; |
| box-shadow: 0 8px 0 #080705; |
| padding: 20px; |
| } |
| .demo-path header { |
| border-bottom: 1px solid #5c4d32; |
| margin-bottom: 14px; |
| padding-bottom: 12px; |
| } |
| .demo-path h2 { |
| margin: 0 0 6px; |
| color: #ffe4a3; |
| } |
| .demo-path p, |
| .demo-path li { |
| color: #e8dcc1 !important; |
| } |
| .demo-path-grid { |
| display: grid; |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| gap: 12px; |
| } |
| .demo-path article { |
| border: 1px solid #5c4d32; |
| background: #211d16; |
| padding: 14px; |
| } |
| .demo-path article span { |
| display: block; |
| color: #f2c15f; |
| font-weight: 900; |
| margin-bottom: 8px; |
| } |
| .judge-scorecard { |
| display: grid; |
| grid-template-columns: repeat(4, minmax(0, 1fr)); |
| gap: 10px; |
| margin-top: 14px; |
| } |
| .judge-scorecard article { |
| border-color: #775c30; |
| background: |
| linear-gradient(180deg, rgba(242, 193, 95, .12), transparent), |
| #191611; |
| } |
| .judge-scorecard strong { |
| display: block; |
| color: #ffe4a3; |
| margin-bottom: 6px; |
| } |
| .judge-scorecard code { |
| background: #ead7a6 !important; |
| color: #1b1308 !important; |
| padding: 2px 5px; |
| } |
| .demo-path ul, |
| .demo-script { |
| margin: 0; |
| padding-left: 18px; |
| } |
| .demo-script { |
| border: 1px solid #6f5832; |
| margin-top: 14px; |
| padding: 14px 14px 14px 34px; |
| background: #171410; |
| } |
| .demo-script b { |
| color: #f2c15f; |
| } |
| .demo-command-strip { |
| display: grid; |
| grid-template-columns: repeat(4, minmax(0, 1fr)); |
| gap: 8px; |
| margin-top: 14px; |
| } |
| .demo-command-strip code, |
| .demo-proof-note code { |
| display: block; |
| background: #ead7a6 !important; |
| color: #1b1308 !important; |
| padding: 8px; |
| overflow-wrap: anywhere; |
| } |
| .demo-closing-line { |
| border-left: 4px solid #f25a0b; |
| margin: 14px 0 8px; |
| padding-left: 12px; |
| font-weight: 900; |
| } |
| .demo-proof-note { |
| display: grid; |
| grid-template-columns: 140px repeat(3, minmax(0, 1fr)); |
| gap: 8px; |
| align-items: center; |
| margin: 0; |
| color: #cdbd9b !important; |
| } |
| .demo-proof-note span { |
| color: #f2c15f; |
| font-weight: 900; |
| } |
| .passport-card { |
| background: |
| linear-gradient(135deg, rgba(255, 226, 154, .1), transparent 35%), |
| #221d16; |
| border: 3px solid #b98a46; |
| box-shadow: 0 8px 0 #080705, inset 0 0 24px rgba(242, 193, 95, 0.14); |
| color: #f9e4b4; |
| padding: 20px; |
| } |
| .passport-header { |
| display: flex; |
| justify-content: space-between; |
| gap: 18px; |
| align-items: flex-start; |
| border-bottom: 1px solid #725a34; |
| padding-bottom: 14px; |
| margin-bottom: 16px; |
| } |
| .passport-card h2 { |
| margin: 4px 0; |
| font-size: clamp(28px, 4vw, 46px); |
| line-height: 1.02; |
| color: #ffe6a6; |
| } |
| .passport-kicker { |
| color: #e2b86f; |
| font-size: 12px; |
| letter-spacing: 0.08em; |
| text-transform: uppercase; |
| } |
| .passport-owner { |
| color: #cdbd9b; |
| } |
| .passport-share { |
| display: grid; |
| grid-template-columns: auto; |
| gap: 6px; |
| align-items: center; |
| justify-items: center; |
| color: #cdbd9b; |
| font-size: 10px; |
| text-transform: uppercase; |
| letter-spacing: .08em; |
| } |
| .passport-share-actions, |
| .profile-share-actions { |
| display: inline-flex; |
| align-items: center; |
| gap: 6px; |
| } |
| .passport-share-link, |
| .profile-share, |
| .passport-share-icon, |
| .profile-share-icon { |
| display: inline-grid; |
| place-items: center; |
| min-height: 30px; |
| padding: 5px 9px; |
| color: #1b1308 !important; |
| background: #ffe0a3; |
| border: 1px solid #fff0bd; |
| text-decoration: none !important; |
| font-weight: 1000; |
| letter-spacing: 0; |
| text-transform: none; |
| box-shadow: 0 3px 0 #090704; |
| } |
| .passport-share-icon, |
| .profile-share-icon { |
| width: 32px; |
| padding: 5px; |
| font-size: 16px; |
| } |
| .hf-icon { |
| display: grid; |
| place-items: center; |
| width: 50px; |
| height: 50px; |
| border-radius: 14px; |
| background: linear-gradient(135deg, #ffcc4d, #ff8f66); |
| color: #221409; |
| font-weight: 1000; |
| box-shadow: 0 5px 0 #090704; |
| } |
| .mini-qr { |
| display: grid; |
| grid-template-columns: repeat(11, 6px); |
| gap: 2px; |
| padding: 8px; |
| background: #f9edcf; |
| border: 2px solid #120f0a; |
| } |
| .mini-qr span { |
| width: 6px; |
| height: 6px; |
| background: transparent; |
| } |
| .mini-qr span.on { |
| background: #15100a; |
| } |
| .passport-grid { |
| display: grid; |
| grid-template-columns: minmax(220px, 320px) 1fr; |
| gap: 16px; |
| } |
| .passport-preview { |
| min-height: 210px; |
| border: 2px solid #6f6a57; |
| background: |
| radial-gradient(circle at 50% 34%, rgba(242,193,95,.22), transparent 42%), |
| repeating-linear-gradient(45deg, #2b2d29 0, #2b2d29 10px, #242520 10px, #242520 20px); |
| display: flex; |
| flex-wrap: wrap; |
| align-content: center; |
| justify-content: center; |
| gap: 8px; |
| padding: 12px; |
| } |
| .passport-artifact-image { |
| width: min(100%, 230px); |
| height: 180px; |
| object-fit: contain; |
| background: radial-gradient(circle at 50% 42%, rgba(242,193,95,.18), #13120f 72%); |
| border: 1px solid #5c4d32; |
| box-shadow: inset 0 0 22px rgba(0,0,0,.32), 0 16px 18px rgba(0,0,0,.22); |
| } |
| .passport-preview strong, |
| .passport-preview em { |
| width: 100%; |
| display: block; |
| text-align: center; |
| } |
| .passport-preview strong { |
| color: #ffe4a3; |
| font-size: 22px; |
| } |
| .passport-preview em { |
| color: #d4c39d; |
| font-style: normal; |
| } |
| .passport-object-mark { |
| width: 100%; |
| text-align: center; |
| } |
| .passport-facts p { |
| margin: 10px 0; |
| color: #e8dcc1 !important; |
| } |
| .passport-route { |
| display: grid; |
| grid-template-columns: 1.2fr 1fr auto; |
| gap: 8px; |
| align-items: center; |
| margin: 10px 0; |
| border: 1px solid #6f5832; |
| background: #171410; |
| padding: 10px; |
| } |
| .passport-route strong, |
| .passport-route span { |
| display: block; |
| color: #ffe4a3; |
| } |
| .passport-route span { |
| color: #d8c9a6; |
| font-size: 12px; |
| } |
| .passport-scan { |
| display: grid; |
| grid-template-columns: 140px 1fr; |
| gap: 14px; |
| align-items: center; |
| margin-top: 14px; |
| border-top: 1px solid #725a34; |
| padding-top: 14px; |
| } |
| .passport-qr-image { |
| width: 140px; |
| height: 140px; |
| background: #f8edcf; |
| border: 3px solid #120f0a; |
| image-rendering: pixelated; |
| } |
| .passport-scan strong, |
| .passport-scan span { |
| display: block; |
| } |
| .passport-scan strong { |
| color: #ffe4a3; |
| font-size: 18px; |
| } |
| .passport-scan p { |
| margin: 4px 0 6px; |
| color: #e8dcc1 !important; |
| font-size: 12px; |
| line-height: 1.4; |
| } |
| .passport-scan span { |
| color: #d8c9a6; |
| overflow-wrap: anywhere; |
| font-size: 12px; |
| } |
| .passport-facts code { |
| background: #ead7a6 !important; |
| color: #1b1308 !important; |
| padding: 5px 7px; |
| } |
| .relic-profile-page { |
| background: |
| radial-gradient(circle at 8% 12%, rgba(242,193,95,.16), transparent 32%), |
| #171410; |
| border: 2px solid #7d6335; |
| box-shadow: 0 8px 0 #080705, inset 0 0 28px rgba(242,193,95,.08); |
| padding: 18px; |
| color: #f4ecd8; |
| } |
| .relic-profile-page header { |
| display: flex; |
| justify-content: space-between; |
| gap: 16px; |
| align-items: flex-start; |
| border-bottom: 1px solid #5c4d32; |
| padding-bottom: 12px; |
| margin-bottom: 14px; |
| } |
| .profile-kicker { |
| display: block; |
| color: #d8b66f; |
| font-size: 12px; |
| text-transform: uppercase; |
| letter-spacing: .08em; |
| } |
| .relic-profile-page h2 { |
| margin: 5px 0; |
| color: #ffe4a3; |
| font-size: clamp(28px, 4vw, 44px); |
| line-height: 1.02; |
| } |
| .relic-profile-page header p { |
| margin: 0; |
| color: #d8c9a6 !important; |
| } |
| .profile-grid { |
| display: grid; |
| grid-template-columns: 1.2fr 1fr; |
| gap: 12px; |
| } |
| .profile-grid article { |
| background: #211d16; |
| border: 1px solid #5c4d32; |
| padding: 14px; |
| } |
| .profile-grid h3 { |
| margin: 0 0 8px; |
| color: #ffe4a3; |
| } |
| .profile-grid p { |
| margin: 0; |
| color: #e8dcc1 !important; |
| line-height: 1.55; |
| } |
| .profile-lore { |
| margin-top: 10px !important; |
| color: #ffdc8a !important; |
| font-weight: 800; |
| } |
| .profile-grid code { |
| display: block; |
| color: #1b1308 !important; |
| background: #ead7a6 !important; |
| padding: 8px; |
| white-space: normal; |
| word-break: break-word; |
| margin-bottom: 10px; |
| } |
| .coordinate-cards { |
| display: grid; |
| grid-template-columns: repeat(4, minmax(0, 1fr)); |
| gap: 8px; |
| margin-bottom: 12px; |
| } |
| .coordinate-cards span { |
| background: #171410; |
| border: 1px solid #6f5832; |
| padding: 10px; |
| } |
| .coordinate-cards small { |
| display: block; |
| color: #bca982; |
| font-size: 10px; |
| text-transform: uppercase; |
| } |
| .coordinate-cards strong { |
| display: block; |
| color: #ffe4a3; |
| font-size: 20px; |
| } |
| .museum-swatch { |
| width: 30px; |
| height: 30px; |
| border: 2px solid #0b0b09; |
| display: inline-block; |
| } |
| .floor-map { |
| display: grid; |
| grid-template-columns: repeat(12, 18px); |
| gap: 3px; |
| margin-top: 12px; |
| } |
| .floor-cell { |
| width: 18px; |
| height: 18px; |
| background: #34372f; |
| border: 1px solid #57513f; |
| color: #1b160e; |
| text-align: center; |
| line-height: 18px; |
| transition: transform 130ms ease, background 130ms ease, box-shadow 130ms ease; |
| } |
| .floor-cell.near { |
| background: #414432; |
| } |
| .floor-cell:hover { |
| transform: scale(1.45); |
| background: #75a9ff; |
| box-shadow: 0 0 12px #75a9ff; |
| } |
| .floor-cell.active { |
| background: #f2c15f; |
| box-shadow: 0 0 10px #f2c15f; |
| animation: plotPulse 1.4s ease-in-out infinite; |
| } |
| .passport-map-card { |
| margin-top: 14px; |
| padding: 14px; |
| border: 2px solid #75623d; |
| background: #171613; |
| } |
| .passport-map-card h3 { |
| margin: 0; |
| color: #ffe4a3; |
| } |
| .passport-map-card p { |
| margin: 6px 0 0; |
| color: #cdbd9b !important; |
| } |
| @keyframes plotPulse { |
| 0%, 100% { transform: scale(1); } |
| 50% { transform: scale(1.24); } |
| } |
| textarea, input { |
| font-family: ui-monospace, SFMono-Regular, Menlo, monospace !important; |
| } |
| .prose, .markdown, .gradio-container p, .gradio-container li { |
| color: #e8dcc1 !important; |
| } |
| .prose h1, .prose h2, .prose h3, .markdown h1, .markdown h2, .markdown h3, |
| .prose strong, .markdown strong { |
| color: #ffe4a3 !important; |
| } |
| .gradio-container code, .gradio-container pre { |
| color: #2b2114 !important; |
| } |
| .compact-note { |
| color: #c9b98f; |
| font-size: 13px; |
| margin: 0 0 8px; |
| } |
| .scan-passport-panel { |
| max-width: 980px; |
| margin: 0 auto 18px; |
| border: 2px solid #7eb6ff; |
| background: |
| linear-gradient(90deg, rgba(80, 126, 181, .32), transparent 62%), |
| #121722; |
| box-shadow: 0 8px 0 #05070a, inset 0 0 26px rgba(126, 182, 255, .12); |
| color: #edf6ff; |
| padding: 16px; |
| } |
| .scan-passport-panel[hidden] { |
| display: none; |
| } |
| .scan-passport-inner { |
| display: grid; |
| grid-template-columns: minmax(0, 1fr) auto; |
| gap: 16px; |
| align-items: start; |
| } |
| .scan-passport-inner span { |
| display: block; |
| color: #9fd0ff; |
| font-size: 12px; |
| text-transform: uppercase; |
| letter-spacing: .08em; |
| } |
| .scan-passport-inner h2 { |
| margin: 4px 0 6px; |
| color: #f8e6af; |
| font-size: clamp(24px, 3vw, 38px); |
| } |
| .scan-passport-inner p, |
| .scan-passport-inner em { |
| margin: 0; |
| color: #d8e7f7 !important; |
| font-style: normal; |
| } |
| .scan-passport-route { |
| display: grid; |
| grid-template-columns: repeat(3, minmax(80px, 1fr)); |
| gap: 8px; |
| margin-top: 12px; |
| } |
| .scan-passport-route b, |
| .scan-passport-inner code { |
| display: block; |
| border: 1px solid #5f7896; |
| background: rgba(4, 8, 13, .58); |
| color: #f8e6af !important; |
| padding: 8px; |
| overflow-wrap: anywhere; |
| } |
| .scan-passport-actions { |
| display: grid; |
| gap: 8px; |
| min-width: 120px; |
| } |
| .scan-passport-actions button { |
| border: 1px solid #9fd0ff; |
| background: transparent; |
| color: #edf6ff; |
| min-height: 32px; |
| padding: 6px 10px; |
| cursor: pointer; |
| } |
| .scan-passport-actions button:first-child { |
| background: #9fd0ff; |
| color: #07111d; |
| font-weight: 900; |
| } |
| @media (max-width: 860px) { |
| .relic-grid { |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| } |
| .hall-grid { |
| grid-template-columns: 1fr; |
| } |
| .selected-strip { |
| grid-template-columns: 42px 1fr; |
| } |
| .artifact-model-frame { |
| height: 340px; |
| } |
| .wall-heading { |
| display: block; |
| } |
| .texture-inspector > div { |
| grid-template-columns: 1fr; |
| } |
| .placement-hero-strip, |
| .join-card, |
| .museum-built-card { |
| grid-template-columns: 1fr; |
| } |
| .placement-hero-strip em { |
| justify-self: start; |
| } |
| .server-grid, |
| .server-formula, |
| .server-owner-card header, |
| .server-owner-grid, |
| .server-owner-columns, |
| .judge-scorecard, |
| .demo-path-grid, |
| .demo-command-strip, |
| .demo-proof-note, |
| .campus-proof-strip, |
| .route-map-proof { |
| grid-template-columns: 1fr; |
| } |
| .campus-heading { |
| grid-template-columns: 1fr; |
| } |
| .campus-heading span { |
| grid-row: auto; |
| justify-self: start; |
| } |
| .campus-stage { |
| height: 330px; |
| overflow-x: auto; |
| } |
| .campus-stage-inner { |
| width: 860px; |
| min-width: 860px; |
| transform: scale(.68); |
| transform-origin: top left; |
| } |
| .living-route-map { |
| grid-template-columns: 1fr; |
| } |
| } |
| """ |
|
|
| PASSPORT_SCAN_JS = r""" |
| () => { |
| const root = document.documentElement; |
| window.addEventListener("pointermove", (event) => { |
| root.style.setProperty("--cursor-x", `${event.clientX}px`); |
| root.style.setProperty("--cursor-y", `${event.clientY}px`); |
| }, { passive: true }); |
| const escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({ |
| "&": "&", |
| "<": "<", |
| ">": ">", |
| '"': """, |
| "'": "'" |
| }[char])); |
| const decodeToken = (token) => { |
| const padded = token + "=".repeat((4 - token.length % 4) % 4); |
| const binary = atob(padded.replace(/-/g, "+").replace(/_/g, "/")); |
| const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); |
| return JSON.parse(new TextDecoder().decode(bytes)); |
| }; |
| const renderScannedPassport = () => { |
| const panel = document.getElementById("afterblock-scan-panel"); |
| if (!panel) { |
| window.setTimeout(renderScannedPassport, 250); |
| return; |
| } |
| const params = new URLSearchParams(window.location.hash.replace(/^#/, "")); |
| const token = params.get("passport"); |
| if (!token) { |
| panel.hidden = true; |
| panel.innerHTML = ""; |
| return; |
| } |
| try { |
| const data = decodeToken(token); |
| const xyz = data.xyz || {}; |
| const shareUrl = `${window.location.origin}${window.location.pathname}${window.location.search}#passport=${encodeURIComponent(token)}`; |
| panel.hidden = false; |
| panel.innerHTML = ` |
| <div class="scan-passport-inner"> |
| <div> |
| <span>Scanned AfterBlock Passport</span> |
| <h2>${escapeHtml(data.title)}</h2> |
| <p>${escapeHtml(data.owner)} · ${escapeHtml(data.object)} · ${escapeHtml(data.hall)} / ${escapeHtml(data.zone)}</p> |
| <em>${escapeHtml(data.caption)}</em> |
| <div class="scan-passport-route"> |
| <b>XYZ ${escapeHtml(xyz.x)} ${escapeHtml(xyz.y)} ${escapeHtml(xyz.z)}</b> |
| <b>CMD ${escapeHtml(data.cmd)}</b> |
| <b>${escapeHtml(data.artifact_id)}</b> |
| </div> |
| <code>${escapeHtml(data.command)}</code> |
| </div> |
| <div class="scan-passport-actions"> |
| <button type="button" data-copy-share>Copy link</button> |
| <button type="button" data-close-scan aria-label="Close scanned passport">Close</button> |
| </div> |
| </div> |
| `; |
| const copyButton = panel.querySelector("[data-copy-share]"); |
| copyButton?.addEventListener("click", async () => { |
| try { |
| await navigator.clipboard.writeText(shareUrl); |
| copyButton.textContent = "Copied"; |
| } catch (error) { |
| window.prompt("Copy passport link", shareUrl); |
| } |
| }); |
| const closeButton = panel.querySelector("[data-close-scan]"); |
| closeButton?.addEventListener("click", () => { |
| history.replaceState(null, "", window.location.pathname + window.location.search); |
| panel.hidden = true; |
| }); |
| panel.scrollIntoView({ behavior: "smooth", block: "start" }); |
| } catch (error) { |
| panel.hidden = false; |
| panel.textContent = "This passport QR could not be decoded."; |
| } |
| }; |
| window.addEventListener("hashchange", renderScannedPassport); |
| window.setTimeout(renderScannedPassport, 350); |
| } |
| """ |
|
|
| DEFAULT_MUSEUM_PROMPT = DEFAULT_IMPORT_PROMPT |
| DEFAULT_STORY_CAPTION = DEFAULT_IMPORT_STORY |
| INITIAL_MUSEUM_OUTPUTS = place_in_museum(DEFAULT_MUSEUM_PROMPT, DEFAULT_STORY_CAPTION, DEFAULT_IMPORT_OWNER, None) |
| INITIAL_TEXTURE_OUTPUTS = browse_texture_library("all", 1) |
| INITIAL_SERVER_CONFIG_KIT = server_config_kit_text() |
| INITIAL_SERVER_CONFIG_ZIP = server_config_zip() |
| INITIAL_SERVER_OWNER_CARD = server_owner_install_card_html() |
|
|
|
|
| with gr.Blocks(css=CSS, js=PASSPORT_SCAN_JS, title="AfterBlock Museum") as demo: |
| gr.HTML( |
| """ |
| <section class="dreamwall-hero"> |
| <h1>AfterBlock Museum</h1> |
| <p> |
| Type one object, attach the story people should know, then place it as one persistent Minecraft relic. |
| </p> |
| <div class="badge-row"> |
| <span>One prompt</span> |
| <span>Story caption</span> |
| <span>Minecraft item-code resource pack</span> |
| </div> |
| </section> |
| """ |
| ) |
| gr.HTML('<section id="afterblock-scan-panel" class="scan-passport-panel" hidden></section>') |
|
|
| with gr.Tabs(): |
| with gr.Tab("Place in Museum"): |
| with gr.Row(elem_classes=["main-museum-layout"]): |
| with gr.Column(scale=4): |
| with gr.Group(elem_classes=["museum-kiosk", "prompt-panel"]): |
| gr.HTML("<h2>Place in the museum</h2>") |
| quick_prompt = gr.Textbox( |
| label="Museum prompt", |
| placeholder="Example: blue school bag from exam week", |
| lines=2, |
| value=DEFAULT_MUSEUM_PROMPT, |
| ) |
| quick_spirit = gr.Textbox( |
| label="Story caption", |
| placeholder="What should visitors know when they open this relic?", |
| lines=3, |
| value=DEFAULT_STORY_CAPTION, |
| ) |
| quick_handle = gr.Textbox( |
| label="Visitor signature", |
| placeholder="@handle or display name", |
| lines=1, |
| value="@Wildstash", |
| ) |
| quick_image = gr.Image( |
| label="Optional image reference", |
| type="filepath", |
| sources=["upload", "clipboard"], |
| height=96, |
| elem_classes=["photo-drop-compact"], |
| ) |
| quick_button = gr.Button("Place in Museum", variant="primary") |
| with gr.Accordion("Minecraft proof and owner files", open=False): |
| museum_command = gr.Textbox( |
| value=INITIAL_MUSEUM_OUTPUTS[1], |
| label="Item command", |
| lines=2, |
| max_lines=2, |
| show_copy_button=True, |
| elem_classes=["command-copy"], |
| ) |
| museum_packet = gr.Textbox( |
| value=INITIAL_MUSEUM_OUTPUTS[5], |
| label="Technical appendix: dreamwall.museum.v1", |
| lines=12, |
| max_lines=18, |
| show_copy_button=True, |
| ) |
| museum_relic_server_download = gr.File( |
| value=INITIAL_MUSEUM_OUTPUTS[11], |
| label="Owner appendix for this relic", |
| ) |
| with gr.Column(scale=6): |
| museum_model = gr.HTML(value=INITIAL_MUSEUM_OUTPUTS[0], label="Artifact model") |
| museum_coordinates = gr.HTML(value=INITIAL_MUSEUM_OUTPUTS[2], label="Coordinates") |
| museum_server_handoff = gr.HTML( |
| value=INITIAL_MUSEUM_OUTPUTS[12], |
| label="Run in Minecraft", |
| ) |
|
|
| museum_wall = gr.HTML(value=INITIAL_MUSEUM_OUTPUTS[9], label="Persistent museum") |
|
|
| with gr.Accordion("Blueprint and catalog", open=False): |
| with gr.Row(): |
| with gr.Column(scale=5): |
| with gr.Group(elem_classes=["museum-stage"]): |
| museum_preview = gr.Image(value=INITIAL_MUSEUM_OUTPUTS[6], label="Museum map", type="filepath", height=420) |
| with gr.Column(scale=5): |
| museum_catalog = gr.Dataframe( |
| value=INITIAL_MUSEUM_OUTPUTS[7], |
| headers=["#", "creator", "title", "object", "hall", "score", "xyz", "cmd"], |
| label="Generated relic catalog", |
| row_count=8, |
| col_count=(8, "fixed"), |
| interactive=False, |
| ) |
|
|
| with gr.Tab("Object Atlas"): |
| gr.HTML( |
| """ |
| <section class="resource-hero"> |
| <h2>Object Atlas</h2> |
| <p>A living field guide for every Minecraft relic look. Pick a kind, flip pages, then open item codes only when you need them.</p> |
| </section> |
| """ |
| ) |
| with gr.Row(elem_classes=["resource-controls"]): |
| texture_kind = gr.Dropdown(label="Kind", choices=texture_kind_choices(), value="all") |
| texture_page = gr.Number(label="Page", value=1, precision=0) |
| texture_prev_button = gr.Button("Previous") |
| texture_button = gr.Button("Open Page") |
| texture_next_button = gr.Button("Next") |
| texture_status = gr.Markdown(value=INITIAL_TEXTURE_OUTPUTS[3]) |
| texture_inspector = gr.HTML(value=INITIAL_TEXTURE_OUTPUTS[2], label="Object atlas page") |
| texture_gallery = gr.Gallery( |
| value=INITIAL_TEXTURE_OUTPUTS[0], |
| label="Atlas shelf", |
| columns=6, |
| height=560, |
| object_fit="contain", |
| elem_classes=["resource-gallery"], |
| ) |
| with gr.Accordion("Appendix: model rows and files", open=False): |
| texture_table = gr.Dataframe( |
| value=INITIAL_TEXTURE_OUTPUTS[1], |
| headers=["cmd", "label", "kind", "shape", "material", "finish", "profile", "orientation", "itemdisplay", "elements", "model", "give command"], |
| label="Minecraft model rows", |
| row_count=14, |
| col_count=(12, "fixed"), |
| interactive=False, |
| ) |
| with gr.Row(): |
| gr.File(value=RESOURCE_PACK_PATH, label="Owner file: resource pack") |
| gr.File(value="assets/afterblock_textures/afterblock_manifest.json", label="Owner file: manifest") |
| gr.File(value="assets/afterblock_textures/gallery/index.html", label="Owner file: full gallery") |
|
|
| with gr.Tab("Passport + Profile"): |
| museum_passport = gr.HTML(value=INITIAL_MUSEUM_OUTPUTS[4], label="Passport") |
| museum_spirit = gr.HTML(value=INITIAL_MUSEUM_OUTPUTS[8]) |
|
|
| with gr.Tab("Living Map"): |
| museum_waypoint = gr.HTML(value=INITIAL_MUSEUM_OUTPUTS[3], label="Placement") |
|
|
| with gr.Tab("Demo Path"): |
| gr.HTML(value=demo_path_html(), label="Hackathon demo path") |
|
|
| with gr.Tab("Join Minecraft"): |
| gr.HTML(value=server_setup_html(), label="Join Minecraft") |
| with gr.Accordion("Owner setup files", open=False): |
| with gr.Group(elem_classes=["server-config-panel"]): |
| gr.HTML( |
| """ |
| <section class="server-config-heading"> |
| <h3>Appendix: host install files</h3> |
| <p>Use this only when installing on PebbleHost or another Paper server. The demo itself should show /dreamwall pack, /dreamwall museum check, and /dreamwall import.</p> |
| </section> |
| """ |
| ) |
| with gr.Row(): |
| server_space_url = gr.Textbox( |
| label="Public Space URL", |
| value=PUBLIC_SPACE_URL, |
| lines=1, |
| ) |
| server_gallery_world = gr.Textbox( |
| label="Minecraft world", |
| value="world", |
| lines=1, |
| ) |
| server_resource_pack_url = gr.Textbox( |
| label="Resource pack URL", |
| value=RESOURCE_PACK_URL, |
| lines=1, |
| ) |
| with gr.Row(): |
| server_resource_pack_sha1 = gr.Textbox( |
| label="Resource pack SHA1", |
| value=RESOURCE_PACK_SHA1, |
| lines=1, |
| ) |
| server_offer_pack = gr.Checkbox( |
| label="Offer resource pack on join", |
| value=False, |
| ) |
| server_import_prompt = gr.Textbox( |
| label="Default /dreamwall import prompt", |
| value=DEFAULT_IMPORT_PROMPT, |
| lines=2, |
| placeholder="Example: blue school bag from exam week", |
| ) |
| server_import_story = gr.Textbox( |
| label="Default /dreamwall import story", |
| value=DEFAULT_IMPORT_STORY, |
| lines=3, |
| placeholder="The caption that becomes the passport/history text", |
| ) |
| server_import_owner = gr.Textbox( |
| label="Default visitor signature", |
| value=DEFAULT_IMPORT_OWNER, |
| lines=1, |
| placeholder="@handle or display name", |
| ) |
| server_config_button = gr.Button("Build Appendix ZIP") |
| server_owner_card = gr.HTML( |
| value=INITIAL_SERVER_OWNER_CARD, |
| label="Server owner install card", |
| ) |
| server_config_kit = gr.Textbox( |
| value=INITIAL_SERVER_CONFIG_KIT, |
| label="Appendix: config.yml and first-run commands", |
| lines=24, |
| max_lines=30, |
| show_copy_button=True, |
| ) |
| server_config_download = gr.File( |
| value=INITIAL_SERVER_CONFIG_ZIP, |
| label="Owner file: server appendix ZIP", |
| ) |
| with gr.Row(): |
| gr.File( |
| value=PAPER_PLUGIN_JAR_PATH, |
| label="Appendix: Paper bridge plugin", |
| ) |
| gr.File( |
| value=RESOURCE_PACK_PATH, |
| label="Appendix: resource pack", |
| ) |
| gr.File( |
| value=PREBUILT_WORLD_PATH, |
| label="Appendix: prebuilt demo world", |
| ) |
| museum_server_kit = gr.Textbox( |
| value=INITIAL_MUSEUM_OUTPUTS[10], |
| label="Current relic appendix", |
| lines=28, |
| max_lines=34, |
| show_copy_button=True, |
| ) |
|
|
| quick_button.click( |
| place_in_museum, |
| inputs=[quick_prompt, quick_spirit, quick_handle, quick_image], |
| outputs=[ |
| museum_model, |
| museum_command, |
| museum_coordinates, |
| museum_waypoint, |
| museum_passport, |
| museum_packet, |
| museum_preview, |
| museum_catalog, |
| museum_spirit, |
| museum_wall, |
| museum_server_kit, |
| museum_relic_server_download, |
| museum_server_handoff, |
| ], |
| api_name="quick_curate", |
| ) |
| server_config_button.click( |
| server_config_bundle, |
| inputs=[ |
| server_space_url, |
| server_resource_pack_url, |
| server_resource_pack_sha1, |
| server_gallery_world, |
| server_offer_pack, |
| server_import_prompt, |
| server_import_story, |
| server_import_owner, |
| ], |
| outputs=[server_config_kit, server_config_download, server_owner_card], |
| api_name="server_config_kit", |
| ) |
| texture_button.click( |
| browse_texture_library_with_page, |
| inputs=[texture_kind, texture_page], |
| outputs=[texture_gallery, texture_table, texture_inspector, texture_status, texture_page], |
| api_name="browse_textures", |
| ) |
| texture_prev_button.click( |
| browse_texture_previous, |
| inputs=[texture_kind, texture_page], |
| outputs=[texture_gallery, texture_table, texture_inspector, texture_status, texture_page], |
| api_name="browse_textures_previous", |
| ) |
| texture_next_button.click( |
| browse_texture_next, |
| inputs=[texture_kind, texture_page], |
| outputs=[texture_gallery, texture_table, texture_inspector, texture_status, texture_page], |
| api_name="browse_textures_next", |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch( |
| server_name="0.0.0.0", |
| server_port=int(os.getenv("PORT", "7860")), |
| ) |
|
|