Dark Cosmos — App Store Launch Assets Implementation Plan
Dark Cosmos — App Store Launch Assets Implementation Plan
Section titled “Dark Cosmos — App Store Launch Assets Implementation Plan”For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Produce a fully polished public App Store listing for Dark Cosmos v0.1 across iPhone, iPad, Apple TV, and Mac — screenshots, trailer, copy, icon, a refocused site, and a privacy page — ready to submit.
Architecture: Render the game on the Mac forced onto the Metal renderer (so the premium HDR/effects look is on), stage 6 scenes via the existing dev-console seams, capture viewport PNGs at every device aspect ratio, composite them through one branded HTML template at exact device pixels, cut a trailer (with original code-synthesised music) via Godot Movie Maker + ffmpeg, and refocus the site/privacy page in the separate bullet-heaven-site repo. All capture tooling lives in marketing/, excluded from game exports, never referenced by the test suite, so determinism is untouched.
Tech Stack: Godot 4.6.3 (gl_compatibility game; capture run with --rendering-method mobile), Python 3 (numpy for music synthesis, PIL where useful), headless Chrome (compositor render), ffmpeg (trailer), plain HTML/CSS (site + compositor), CF Pages + R2 (site hosting).
Global Constraints
Section titled “Global Constraints”- Veracity: depict only what the shipped build contains — twin-stick, lots of upgrades, drone army, big bosses, elemental reactions (as a mechanic). No story mode, no co-op, no explorable areas. Never claim “14 elements” or “91 reactions.” Real: 6 weapon elements / ~11 seen / ~10 named reactions.
- Drop the “Crystals” label in all player-facing copy and on the site. It is just Dark Cosmos.
- Copy style (all player-facing text — listing, site, privacy): no em-dashes (—); plain English, short sentences; no AI-marker words (“delve”, “seamless”, “unlock”, “elevate”, “harness”, “robust”, etc.).
- Apple character limits: name ≤ 30, subtitle ≤ 30, promotional text ≤ 170, keywords ≤ 100 (comma-separated).
- Exact screenshot resolutions (landscape): iPhone
2796×1290, iPad2732×2048, Apple TV1920×1080, Mac2880×1800. Confirm against App Store Connect before mass-render. - Determinism / purity:
marketing/is excluded from every export preset’sexclude_filterand is never imported bytests/. The capture harness is render-side only and uses existingSimdev seams (dev_*) — it adds no/simlogic. Run the full suite + determinism test unchanged after any repo change. - Name locked:
Dark Cosmos. Bundle id staysdigital.lumara.bulletheaven(unchanged — only the display name changes).
File Structure
Section titled “File Structure”bullet-heaven/ (game repo) marketing/ capture/ shoot.gd # capture harness: boot Main, force scenes, save PNGs shoot.tscn # scene the harness runs in scenes.gd # the 6 scene staging recipes (dev-seam scripts) audio/ compose.py # numpy synth -> dark_cosmos_theme.wav dark_cosmos_theme.wav # generated, committed template/ screenshot.html # branded compositor template (data-driven) render.mjs # headless-chrome renderer: frame+caption -> exact-size PNG scenes.json # caption text per scene + per-family safe-area config raw/ # raw viewport PNGs + trailer master (gitignored if large) trailer/ build_trailer.sh # ffmpeg edit: master -> 3 device cuts appstore/ copy.md # final listing copy (char-counted) compliance.md # App Privacy + age-rating answers iphone/ ipad/ tv/ mac/ # final composited screenshots previews/ # final trailer cuts icon/ # icon (+ any re-cut) press/ # one-sheet + hero feature graphic
bullet-heaven-site/ (separate repo) index.html # rebrand -> Dark Cosmos, refocus content privacy/index.html # new privacy pageTask 1: Final listing copy
Section titled “Task 1: Final listing copy”Files:
- Create:
marketing/appstore/copy.md
Interfaces:
-
Produces: the canonical copy strings reused by the site (Task 8) and ASC submission (Task 14).
-
Step 1: Write the copy file with these exact strings (no em-dashes, within limits):
# Dark Cosmos — App Store copy
Name: Dark CosmosSubtitle: Twin-stick survival roguelite
Promotional text:Two sticks, an auto-firing arsenal, and a drone army you build as you go. Stack elemental reactions, grab upgrades, and survive the neon dark against huge bosses.
Keywords:survival,roguelite,twin stick,neon,drone,shooter,horde,arcade,action,bullet heaven,upgrade,boss
Description:Drop into the neon dark and survive. Dark Cosmos is a twin-stick survival roguelite: move with one stick, aim your main weapon with the other, and let the rest of your arsenal fire itself.
Every level you pick an upgrade. Flat stat boosts, transformative mods that change how your weapons behave, and evolutions that turn a weapon into something new. No two runs build the same way.
Build a drone army from the Drone Bay and send it into the swarm beside you. Every weapon carries an element, so hit an enemy, then hit it again with a different one, and set off a reaction. Push deep enough and the bosses come for you.
It is fast, it is bright, and it holds a steady frame rate when the screen fills with hundreds of enemies. See how far one run can take you.
What's New (v0.1):First release. Drop into the neon dark, build your arsenal and your drones, set off elemental reactions, and see how far one run can take you.
Category: Games (primary: Action, secondary: Arcade)Age rating: expected 9+ (abstract neon violence, no gore)- Step 2: Verify character limits
Run:
cd /Users/chris/Claude/bullet-heavenpython3 - <<'PY'import ret=open('marketing/appstore/copy.md').read()def field(name): m=re.search(rf'^{name}:\s*\n?(.*?)(?:\n\n|\nKeywords:|\nDescription:|\nWhat|\nCategory:)', t, re.S|re.M) return ' '.join(m.group(1).split()) if m else ''for n,lim in [('Subtitle',30),('Promotional text',170),('Keywords',100)]: v=field(n); print(f"{n}: {len(v)}/{lim} {'OK' if len(v)<=lim else 'OVER'} -> {v[:60]}")PYExpected: Subtitle ≤30, Promotional text ≤170, Keywords ≤100, all OK. If any is OVER, trim and re-run.
- Step 3: Em-dash + AI-marker scan
Run:
grep -nE '—|delve|seamless|unlock|elevate|harness|robust|tapestry|leverage' marketing/appstore/copy.md && echo "FOUND - fix" || echo "clean"Expected: clean.
- Step 4: Commit
git add marketing/appstore/copy.mdgit commit -m "docs(marketing): final App Store listing copy"Task 2: Compliance answers (App Privacy + age rating)
Section titled “Task 2: Compliance answers (App Privacy + age rating)”Files:
- Create:
marketing/appstore/compliance.md - Read first:
net/telemetry.gd,net/gameplay_telemetry.gd,net/telemetry_device.gd(confirm exactly what is sent)
Interfaces:
-
Produces: the answers Chris enters in ASC’s App Privacy + Age Rating questionnaires; must match the privacy page (Task 7).
-
Step 1: Read the telemetry senders to confirm fields actually transmitted.
Run: sed -n '1,80p' net/telemetry.gd net/gameplay_telemetry.gd net/telemetry_device.gd
Confirm: perf sample (fps, frame/sim/render ms, draws, counts, build, session, coarse device id) + gameplay summary (run_time, level, kills, gold, dps, mode). No name/email/account/location/IDFA.
- Step 2: Write
compliance.md:
# Dark Cosmos — ASC compliance answers
## App Privacy (Data Collection)Does the app collect data? YES (anonymous, optional analytics).
Data types collected:- Diagnostics > Performance Data (frame rate, timings, draw counts, app build)- Usage Data > Product Interaction (run length, level reached, kills, mode)
For BOTH types:- Linked to the user's identity? NO- Used for tracking? NO- Used for: App Functionality + Analytics
Identifiers: NONE collected (no IDFA, no account, no device advertising id).A random per-run session id and coarse device model (e.g. "AppleTV11,1") aresent for diagnostics only; not tied to identity.
## Age Rating (answer NONE/INFREQUENT unless noted)- Cartoon or Fantasy Violence: INFREQUENT/MILD (abstract neon shapes, no blood/gore)- Realistic Violence: NONE- Sexual content / nudity / profanity / drugs / gambling / horror: NONE- Unrestricted web access: NOExpected result: 9+.
## Export complianceUses non-exempt encryption? NO (ITSAppUsesNonExemptEncryption = false).
## URLsSupport URL: https://dark-cosmos.lumara.digital (or current site URL)Marketing URL: samePrivacy Policy URL: <site>/privacy- Step 3: Commit
git add marketing/appstore/compliance.mdgit commit -m "docs(marketing): ASC App Privacy + age-rating answers"Task 3: Original electronic trailer music
Section titled “Task 3: Original electronic trailer music”Files:
- Create:
marketing/audio/compose.py,marketing/audio/dark_cosmos_theme.wav
Interfaces:
-
Produces:
dark_cosmos_theme.wav(~28s, 44.1kHz stereo) consumed by the trailer (Task 12). -
Step 1: Write
compose.py— a numpy synth producing a dark synthwave bed: a minor-key arpeggio, a sub bass on the root, a soft pad, and a four-on-the-floor kick + offbeat hat. Use simple oscillators (saw via2*(t*f%1)-1, sine), ADSR envelopes, a gentle lowpass (one-pole), soft-clip limiter, and a fade in/out. Tempo ~110 BPM, key A minor, ~28s. Write a stereo 16-bit WAV withwave+numpy.
(Concrete skeleton — fill the note tables and mix levels, keep it dependency-light: numpy + stdlib wave only.)
import numpy as np, waveSR=44100; BPM=110; BEAT=60/BPM; BARS=14; T=BARS*4*BEATdef env(n,a,d,s,r): # samples a/d/r, sustain level s e=np.ones(n); ai=int(a*SR);di=int(d*SR);ri=int(r*SR) if ai:e[:ai]=np.linspace(0,1,ai) if di:e[ai:ai+di]=np.linspace(1,s,di) e[ai+di:n-ri]=s if ri:e[n-ri:]=np.linspace(s,0,ri) return edef saw(f,n,ph=0): t=np.arange(n)/SR; return 2*((t*f+ph)%1)-1def sine(f,n): t=np.arange(n)/SR; return np.sin(2*np.pi*f*t)def note(midi): return 440*2**((midi-69)/12)mix=np.zeros(int(T*SR))# ... lay kick every beat, hat offbeats, sub bass on bar root, A-minor arp (A C E G) 1/8 notes,# pad chord per 2 bars; sum with levels ~ kick .9 bass .5 arp .35 pad .25 hat .15# one-pole lowpass on arp+pad for warmth; tanh soft-clip the master; 0.5s fade in, 2s fade outmaster=np.tanh(mix*1.2); master/=np.max(np.abs(master))+1e-9st=np.stack([master,master],1) # (widen arp/pad L/R for stereo if desired)pcm=(st*0.95*32767).astype('<i2')with wave.open('marketing/audio/dark_cosmos_theme.wav','wb') as w: w.setnchannels(2); w.setsampwidth(2); w.setframerate(SR); w.writeframes(pcm.tobytes())print('wrote', T, 's')- Step 2: Generate + validate the WAV
Run:
cd /Users/chris/Claude/bullet-heaven && python3 marketing/audio/compose.pypython3 - <<'PY'import wave; w=wave.open('marketing/audio/dark_cosmos_theme.wav'); print('ch',w.getnchannels(),'rate',w.getframerate(),'sec',round(w.getnframes()/w.getframerate(),1))PYExpected: ch 2 rate 44100 sec ~28.
-
Step 3: Listen + iterate —
open marketing/audio/dark_cosmos_theme.wav(orafplay). Tune levels/notes until it reads as a confident dark-cosmos electronic bed. Get Chris’s nod on the feel before the trailer bakes it in. -
Step 4: Commit
git add marketing/audio/compose.py marketing/audio/dark_cosmos_theme.wavgit commit -m "feat(marketing): original electronic trailer theme (numpy synth)"Task 4: Capture harness (Metal render + viewport PNG)
Section titled “Task 4: Capture harness (Metal render + viewport PNG)”Files:
- Create:
marketing/capture/shoot.gd,marketing/capture/shoot.tscn - Read first:
main.gd(find: the start-menu run-start entry for the crystals ruleset; the debug-UI nodes to hide; howQualityManageris pinned;apply_dev_command) - Modify:
export_presets.cfg(both repos) — addmarketing/*to eachexclude_filter
Interfaces:
-
Produces:
marketing/capture/shoot.gdwithcapture_frame(path: String, size: Vector2i) -> voidand a_run_all()driver; raw PNGs inmarketing/raw/. -
Step 1: Exclude marketing from exports — edit each preset’s
exclude_filterinbullet-heaven/export_presets.cfg(Web, macOS) andbullet-heaven-tvos/export_presets.cfg(tvOS, iOS) to includemarketing/*. The tvOS/iOS presets already excludetests/*, telemetry/*, tools/*; append, marketing/*. Verify nothing inmarketing/ships. -
Step 2: Write
shoot.tscn+shoot.gd— a Node that instancesMain, configures it for clean capture, and exposes capture. Skeleton (wire the marked integration points againstmain.gd):
extends Node# Run with: godot --path . --rendering-method mobile res://marketing/capture/shoot.tscnconst SIZES := {"iphone":Vector2i(2796,1290),"ipad":Vector2i(2732,2048),"tv":Vector2i(1920,1080),"mac":Vector2i(2880,1800)}var main
func _ready() -> void: assert(RenderingServer.get_rendering_device() != null, "run with --rendering-method mobile for HDR/Metal effects") main = preload("res://main.tscn").instantiate() add_child(main) await get_tree().process_frame # INTEGRATION (read main.gd): start a crystals run, enable attract AutoPlayer, # pin QualityManager L0, hide debug UI (F2 overlay, build label, dev code). Scenes.prepare(main) # see Task 5 await _run_all() get_tree().quit()
func capture_frame(path: String, size: Vector2i) -> void: get_window().size = size await RenderingServer.frame_post_draw await RenderingServer.frame_post_draw # 2 frames: let stretch settle var img := get_viewport().get_texture().get_image() DirAccess.make_dir_recursive_absolute("res://marketing/raw".replace("res://", ProjectSettings.globalize_path("res://"))) img.save_png("marketing/raw/%s" % path)
func _run_all() -> void: for scene in Scenes.LIST: # see Task 5 await Scenes.stage(main, scene) # set up the scenario deterministically for fam in SIZES: await capture_frame("%s_%s.png" % [scene, fam], SIZES[fam])- Step 3: Smoke-capture one frame at iPhone size to prove the pipeline (temporarily stage just a running crystals scene):
Run:
cd /Users/chris/Claude/bullet-heaven/opt/homebrew/bin/godot --path . --rendering-method mobile res://marketing/capture/shoot.tscn 2>&1 | grep -iE "error|script error" || truepython3 - <<'PY'from PIL import Image; im=Image.open('marketing/raw/horde_iphone.png'); print(im.size, 'mean', sum(im.convert('L').resize((1,1)).getpixel((0,0)) for _ in [0])/1)PYExpected: image is (2796, 1290) and not near-black (mean brightness > 10 — proves HDR neon rendered). If near-black, the run did not start / renderer wrong — fix integration before proceeding.
- Step 4: Commit
git add marketing/capture export_presets.cfggit commit -m "feat(marketing): Metal capture harness + exclude marketing from exports"Task 5: Scene staging recipes (the 6 money shots)
Section titled “Task 5: Scene staging recipes (the 6 money shots)”Files:
- Create:
marketing/capture/scenes.gd - Read first: the dev seams in
sim/sim.gd(dev_spawn_custom,dev_spawn_bosspath,dev_clear_enemies,dev_suppress_spawns,dev_invuln) andmain.apply_dev_command; the Drone Bay open path; the level-up panel trigger; the codex card trigger.
Interfaces:
-
Consumes:
main(the live game),Simdev seams. -
Produces:
Scenes.LIST: Array[String],Scenes.prepare(main),Scenes.stage(main, name) -> awaitablethat arranges each scenario and waits for it to look right. -
Step 1: Write
scenes.gdstaging each of the 6 scenes via dev seams (invuln on so the auto-player survives the staged moment; pause spawns then place a controlled set). Eachstage()arranges the world, advances enough ticks for effects to bloom, and returns.
class_name Scenesextends RefCountedconst LIST := ["horde","twinstick","reaction","upgrades","drones","boss"]
static func prepare(main) -> void: main.apply_dev_command({"cmd":"invuln","on":true}) # hide debug UI + pin quality L0 here (read main.gd for the exact nodes/flags)
static func stage(main, name) -> void: var sim = main.sim match name: "horde": # dense swarm around the player, mid-fight main.apply_dev_command({"cmd":"pause_spawns","on":true}) for i in 140: sim.dev_spawn_custom(_swarm_spec(), _ring_pos(i)) "twinstick": # aim a direction so the aimed weapon + auto weapons all fire into a pack for i in 60: sim.dev_spawn_custom(_swarm_spec(), _ring_pos(i)) main.apply_dev_command({"cmd":"aim","dir":[1,0]}) # if supported; else set auto-player aim "reaction": # force a Plasma burst: lightning + fire on a cluster # spawn a tight cluster, let pulse(lightning)+nova(fire) react; wait for the burst frame for i in 40: sim.dev_spawn_custom(_swarm_spec(), _ring_pos(i, 220)) "upgrades": # open the level-up card screen (grant enough XP to trigger) main.apply_dev_command({"cmd":"player_stat","field":"xp","value":9999}) # or call the run's level-up directly "drones": # open the Drone Bay overlay with a configured loadout main.open_bay_for_capture() # add a tiny capture-only helper if no direct call exists "boss": # spawn the Graviton, let the gravity lens engage main.apply_dev_command({"cmd":"spawn_boss","which":"graviton"}) # advance ~30 frames so bloom/lens/particles reach a good-looking moment for _f in 30: await main.get_tree().process_frame(The _swarm_spec(), _ring_pos() helpers and the exact dev-command keys come from reading the console caps in sim.gd/main.gd. Where a direct call is cleaner than a dev command, call the sim method.)
- Step 2: Capture all 24 raw frames
Run:
cd /Users/chris/Claude/bullet-heaven/opt/homebrew/bin/godot --path . --rendering-method mobile res://marketing/capture/shoot.tscn 2>&1 | grep -iE "script error" || truels marketing/raw/*.png | wc -lpython3 - <<'PY'from PIL import Image; import globfor f in sorted(glob.glob('marketing/raw/*.png')): im=Image.open(f); print(f.split('/')[-1], im.size)PYExpected: 24 PNGs; each at its family’s exact size; none near-black. Eyeball each for composition; restage any weak scene (use the reserve list from the spec) and re-run.
- Step 3: Commit
git add marketing/capture/scenes.gdgit commit -m "feat(marketing): 6-scene capture staging via dev seams"Task 6: Branded screenshot compositor
Section titled “Task 6: Branded screenshot compositor”Files:
- Create:
marketing/template/screenshot.html,marketing/template/render.mjs,marketing/template/scenes.json
Interfaces:
-
Consumes:
marketing/raw/<scene>_<fam>.png, captions fromscenes.json. -
Produces:
marketing/appstore/<fam>/NN_<scene>.pngat exact device size. -
Step 1: Write
scenes.json— caption + sizes:
{ "families": {"iphone":[2796,1290],"ipad":[2732,2048],"tv":[1920,1080],"mac":[2880,1800]}, "safe": {"tv":90,"iphone":48,"ipad":40,"mac":40}, "scenes": [ {"id":"horde","caption":"Survive the neon dark"}, {"id":"twinstick","caption":"Two sticks. The rest fires itself."}, {"id":"reaction","caption":"Set off elemental reactions"}, {"id":"upgrades","caption":"An upgrade every level"}, {"id":"drones","caption":"Build your drone army"}, {"id":"boss","caption":"Fight huge bosses"} ]}-
Step 2: Write
screenshot.html— a full-bleed gameplay<img>with a bottom caption band (dark-cosmos gradient, Orbitron-style heavy heading, small “DARK COSMOS” wordmark), reading?frame=&caption=&w=&h=&safe=query params and sizingbodyto exactlyw×h. Captions sit inside thesafeinset so TV overscan / iPhone chrome never clip them. -
Step 3: Write
render.mjs— headless Chrome (reuse the chrome-devtools/Playwright MCP orpuppeteerif available; else achrome --headless --screenshot --window-size=WxHcall) that, for each scene × family, loadsscreenshot.htmlwith the right params atdeviceScaleFactor=1and saves the exact-size PNG tomarketing/appstore/<fam>/. -
Step 4: Render one test image (iPhone, horde) and verify exact pixels:
Run:
cd /Users/chris/Claude/bullet-heaven/marketing/template && node render.mjs --only iphone:hordepython3 - <<'PY'from PIL import Image; im=Image.open('../appstore/iphone/01_horde.png'); print(im.size); assert im.size==(2796,1290)PYExpected: (2796, 1290), caption legible, no clipping.
- Step 5: Render all 24 + verify
Run:
node render.mjs --allpython3 - <<'PY'from PIL import Image; import globfor f in sorted(glob.glob('../appstore/*/*.png')): print(f.split('marketing/')[-1], Image.open(f).size)print('count', len(glob.glob('../appstore/*/*.png')))PYExpected: 24 images at correct sizes.
- Step 6: Commit
cd /Users/chris/Claude/bullet-heavengit add marketing/template marketing/appstore/iphone marketing/appstore/ipad marketing/appstore/tv marketing/appstore/macgit commit -m "feat(marketing): branded screenshot compositor + 24 final screenshots"Task 7: Site refocus + privacy page
Section titled “Task 7: Site refocus + privacy page”Files:
- Modify:
bullet-heaven-site/index.html - Create:
bullet-heaven-site/privacy/index.html - Read first:
bullet-heaven-site/CLAUDE.md(deploy steps, R2 demo, clean-staging-dir rule)
Interfaces:
-
Produces: the live privacy URL used in
compliance.md(Task 2) and ASC. -
Step 1: Rebrand
index.html— replace “Bullet Heaven” with “Dark Cosmos” in<title>,<h1>, meta description, and body; drop any “Crystals” wording; refocus the hero + features on the verified pitch (twin-stick, lots of upgrades, drone army, big bosses, elemental reactions as a mechanic). Remove or correct the “14 elements / 91 reactions” line and the changelog claims that overstate the shipped build. Keep the playable web-demo embed. No em-dashes, no AI-marker words. -
Step 2: Write
privacy/index.html— short, plain privacy page styled to match the site. Content (paraphrase, no em-dashes):
Dark Cosmos collects a small amount of anonymous data, only to improve performance and to balance the game. We record things like frame rate and timings, your device model, an app build number, a random per-session id, and a summary of a run (how long it lasted, level reached, mode). We do not collect your name, email, account, contacts, or location. We do not show ads, we do not track you across apps, and we never sell data. Questions: chris@lumara.digital.
- Step 3: Local check
Run:
cd /Users/chris/Claude/bullet-heaven-sitegrep -niE 'bullet heaven|crystals|—|91 reactions|14 elements' index.html privacy/index.html || echo "clean"Expected: clean (or only intentional genre-keyword uses you accept).
- Step 4: Deploy per
bullet-heaven-site/CLAUDE.md(CF Pages, clean-staging-dir — do NOT upload internal files), then verify live:
Run:
curl -sI https://<live-site>/privacy/ | head -1 # expect 200curl -s https://<live-site>/ | grep -io 'Dark Cosmos' | head -1Expected: privacy 200; homepage shows “Dark Cosmos”.
- Step 5: Commit (site repo)
cd /Users/chris/Claude/bullet-heaven-sitegit add index.html privacy/index.htmlgit commit -m "feat: rebrand to Dark Cosmos + add privacy page"Task 8: App preview trailer
Section titled “Task 8: App preview trailer”Files:
- Create:
marketing/trailer/build_trailer.sh - Consumes:
marketing/audio/dark_cosmos_theme.wav(Task 3), a Movie Maker master capture
Interfaces:
-
Produces:
marketing/appstore/previews/{iphone,ipad,tv}.mp4meeting Apple App Preview specs. -
Step 1: Capture the master — run an attract-mode session on the Metal renderer in Movie Maker mode at high resolution:
Run:
cd /Users/chris/Claude/bullet-heaven/opt/homebrew/bin/godot --path . --rendering-method mobile --fixed-fps 60 \ --write-movie marketing/raw/trailer_master.avi res://main.tscn &# let it auto-play a juicy ~75s run, then quit the window(Ensure attract/auto mode + L0 quality + debug UI hidden, same as capture; a dedicated shoot.tscn movie variant is fine.)
-
Step 2: Write
build_trailer.sh— ffmpeg pipeline: trim to the best ~22s, prepend a 1.5s Dark Cosmos title card, overlay 3-4 caption beats matching the screenshot captions, mix indark_cosmos_theme.wav, fade in/out, then export per family (H.264, yuv420p, 30fps): iPhone1920×886, iPad1920×1440(or2732×2048-derived per ASC accepted preview size), Apple TV1920×1080. Confirm exact accepted preview dimensions in ASC before final export. -
Step 3: Build + verify
Run:
cd /Users/chris/Claude/bullet-heaven && bash marketing/trailer/build_trailer.shfor f in marketing/appstore/previews/*.mp4; do ffprobe -v error -show_entries stream=width,height,codec_name,duration -of csv "$f"; doneExpected: 3 mp4s, H.264, ~22s, correct per-family dimensions. Watch each end-to-end.
- Step 4: Commit
git add marketing/trailer marketing/appstore/previewsgit commit -m "feat(marketing): app preview trailer (3 device cuts) with original music"Task 9: Icon legibility check + polish
Section titled “Task 9: Icon legibility check + polish”Files:
-
Read:
bullet-heaven-tvos/icon.png(1024²) -
Create (if re-cut):
marketing/appstore/icon/icon.png -
Step 1: Downscale-preview the icon to home-screen sizes and inspect:
Run:
cd /Users/chris/Claude/bullet-heavenpython3 - <<'PY'from PIL import Imageim=Image.open('../bullet-heaven-tvos/icon.png').convert('RGB')for s in (180,120,80,60): im.resize((s,s)).save(f'/tmp/icon_{s}.png')print('wrote /tmp/icon_{180,120,80,60}.png')PYopen /tmp/icon_60.png /tmp/icon_120.pngExpected: the mark still reads at 60px (clear silhouette, not muddy).
- Step 2: If it holds up, no change — note “icon OK” in
marketing/appstore/icon/NOTES.md. If muddy at small size, re-cut a higher-contrast 1024² intomarketing/appstore/icon/icon.pngand (later) swap it into the export presets. Commit either way:
git add marketing/appstore/icongit commit -m "chore(marketing): icon legibility check"Task 10: Press one-sheet + hero feature graphic
Section titled “Task 10: Press one-sheet + hero feature graphic”Files:
-
Create:
marketing/appstore/press/one-sheet.html(→ PDF/PNG),marketing/appstore/press/hero.png -
Step 1: Build a one-sheet HTML (title, one-line pitch, 4 feature bullets from the verified pitch, 3 screenshots, links, contact) and render to PNG/PDF via the same headless-chrome path as Task 6. Build a wide hero key-art frame from the best
horde/bosscapture + the wordmark. -
Step 2: Verify + commit
Run: ls -la marketing/appstore/press/
git add marketing/appstore/pressgit commit -m "feat(marketing): press one-sheet + hero feature graphic"Task 11: Assemble + submission checklist
Section titled “Task 11: Assemble + submission checklist”Files:
-
Create:
marketing/appstore/SUBMIT.md -
Step 1: Write a checklist Chris follows in App Store Connect: set display name to Dark Cosmos; paste copy from
copy.md; upload screenshots per family frommarketing/appstore/<fam>/; upload the 3 previews; set category Action/Arcade; complete App Privacy + Age Rating fromcompliance.md; set support/marketing/privacy URLs; confirm build (iOS + tvOS + Mac) attached; export-compliance = no non-exempt encryption; submit for review. Note the realistic timeline: review 1-3 days, live on approval. -
Step 2: Commit
git add marketing/appstore/SUBMIT.mdgit commit -m "docs(marketing): App Store Connect submission checklist"Self-Review (completed during planning)
Section titled “Self-Review (completed during planning)”- Spec coverage: capture harness (T4), screenshots 6×4 (T5–T6), copy (T1), trailer + music (T3, T8), icon (T9), extras (T10), site refocus + privacy (T7), compliance (T2), submission (T11) — every spec §2 deliverable + §10 compliance item has a task.
- Placeholders: engine-dependent integration points (run-start, dev-command keys, debug-UI nodes) are explicit “read
main.gd/sim.gdand wire” steps with concrete skeletons + verification, not vague TODOs. Exact resolutions/limits/commands are inline. - Type consistency:
capture_frame(path,size),Scenes.LIST/prepare/stage, family keysiphone/ipad/tv/mac, and resolution tuples are identical across T4–T6 andscenes.json.
Parallelism note (for execution)
Section titled “Parallelism note (for execution)”Independent streams that can run concurrently: T1+T2 (copy/compliance), T3 (music), T7 (site/privacy), and T4 (harness) have no cross-dependencies. T5 needs T4; T6 needs T5; T8 needs T3+T4; T10 needs T6. Front-load T1/T2/T7 (submission-blockers) and T4 (the visual unlock).