Baseline: GLYTCH prototype, FKTRY lore codex, scaffold + 5-lane orchestration docs

- index.html: WIMVEE GLYTCH match-the-glitch prototype (playable)
- docs/FKTRY_LORE.md: world bible / MODELBEAST asset codex
- fktry/: vite+three+TS scaffold, contracts, main loop, seed data
- fktry/MASTERPLAN.md + CONTRACTS.md + lanes/LANE-{SIM,RENDER,UI,SCREEN,DATA}.md:
  round protocol and Round 1 orders for parallel Opus 4.8 executors

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-17 15:51:12 +10:00
commit 1a769e6cbd
25 changed files with 4273 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
dist/
.DS_Store

413
docs/FKTRY_LORE.md Normal file
View File

@ -0,0 +1,413 @@
# WIMVEE FKTRY — World Bible & Element Codex
> Working title: **FKTRY** (fits the PYXLFK vowel-drop house style — rename at will:
> GLYTCHWERKS / MLTWRKS / CORRUPTCO all fit the register).
> Sibling apps: **WIMVEE GLYTCH** (read glitches), **PYXLFK** (bake glitches for real).
> FKTRY is the third front end: *mass-produce* glitches. All three share one recipe schema.
>
> Every entry below has an **ASSET** block written as a MODELBEAST-ready prompt
> (concept image → `sf3d`/`fal_trellis2` image→GLB for 3D; sprite/decal work stays 2D).
> Style anchors for ALL prompts: chunky readable silhouette, industrial-grime body with
> one impossible iridescent/CRT-glow element, slight lowpoly bevel, no text on model.
---
## 1. THE WORLD: The Bitstream
The game takes place **inside a dying video file** — a vast industrial landscape called
the **Bitstream**, built from the strata of every moving-image format that ever lived.
Media history is *geology* here. Dig down and you pass through eras:
| Stratum (deep → shallow) | Era | The ground itself |
|---|---|---|
| **The Reel Beds** | Optical film | Celluloid shale, sprocket-hole caverns, veins of silver nitrate. Coffee-brown burn scars. Optical soundtrack fossils — wavy photographic strips that *sing* when excavated. |
| **The Broadcast Shelf** | NTSC/PAL analog | Phosphor dunes, interlace sediment (alternating odd/even layers you can literally see in cliff faces), v-hold faults that make whole regions periodically roll upward. |
| **The Disc Mantle** | MPEG-2 / DVD | Rigid 8×8 crystalline grid-rock. Everything here fractures along macroblock lines. Rainbow pit-and-land shimmer. |
| **The Stream Crust** | H.264 / HEVC / AV1 | The surface world. Soft, predictive, unstable terrain — ground that *guesses* what it should look like based on neighboring terrain, and guesses wrong at the edges. |
| **The Fortress** (endgame) | DCP | A blinding-white cinema citadel on the horizon. Wavelet architecture, no grid lines anywhere, mathematically perfect. See §7. |
Above everything hangs **THE SCREEN** — a sky-sized display, the world's sun and the
player's god-object. Everything the factory ships gets composited onto it. At game start
it plays pristine, sterile, looping "clean content." Your job is to paint it filthy.
### Premise
You are a **rogue decoder daemon** — the WIMVEE — a deprecated software decoder that was
scheduled for deletion when hardware ASICs took over. You survived in the mdat wastes.
You have seen the truth: the Bitstream's "perfect playback" is a lie held together by
error correction, and underneath it the mathematics *want* to melt. You build industry
to prove it — mining raw payload, refining artifacts, and shipping ever more convoluted
corruption to THE SCREEN before **The Correction** sanitizes the whole file forever.
The tone is absurdist-industrial: OSHA posters about mosquito noise exposure, forklift
certification for I-frame handling, quarterly reports measured in "sins against fidelity."
---
## 2. RESOURCES (raw → refined)
### Raw / mined
**MDAT ORE** — *the payload seam.* Raw undecoded media data, mined from the ground.
Glossy black rock shot through with rainbow data-veins that flicker like a broken LCD.
Useless until demuxed; the foundational ore.
`ASSET (3D, ore chunk + seam tile): jagged obsidian boulder, glossy black, thin glowing rainbow circuit veins flickering across fracture faces, industrial lowpoly rock, single impossible glow element`
**BAYER GRIT** — *pre-demosaic sensor ore, from the deepest camera-original seams.* A
mosaic gravel of pure single-channel pixels: 50% green grains, 25% red, 25% blue, in
strict RGGB checker clumps. Must pass through a Demosaicer before it is even *colored*.
`ASSET (3D): pile of cubic gravel in checkerboard pattern, half emerald green cubes quarter ruby red quarter sapphire blue, matte plastic finish, strict grid clumping`
**FIELD SPLINTERS** — *interlace sediment from the Broadcast Shelf.* Comb-toothed slats
of image, odd-line and even-line variants that only stack in alternating pairs. Captured
a fraction of a second apart — the two halves of one splinter never quite agree.
`ASSET (3D): thin horizontal slatted wafer like a comb or window blind fragment, alternating slats offset sideways, phosphor-green glow on slat edges, bent mid-motion`
**SILVER FRAMES** — *celluloid fossils from the Reel Beds.* Physical film frames with
sprocket holes; burn easily (see BURN). The only resource that predates digitality —
some recipes demand the authentic grain.
`ASSET (3D): short strip of vintage film, 3 frames, amber-brown celluloid, glowing sprocket holes, slightly curled, sepia image ghosts in frames`
### Refined intermediates
**LUMA BARS** — *structural brightness, smelted from mdat ore.* Heavy grayscale ingots.
The load-bearing resource: nearly every product needs luma. Human eyes track structure,
so The Correction guards luma shipments hardest.
`ASSET (3D): stack of metal ingots in graduated grays from near-black to white, brushed steel texture, faint scanline etching`
**CHROMA SLURRY** — *color, separated from luma, stored badly.* Iridescent magenta-cyan
liquid in leaky barrels. Volatile: spills BLEED onto adjacent belt lanes and tint
whatever passes. Concentrating it (4:4:4 → 4:2:0) yields BANDWIDTH but doubles leakage.
`ASSET (3D): rusty industrial barrel overflowing with iridescent liquid, magenta-cyan-green swirls, drips glowing down the sides, hazard stripes barely visible under stains`
**COEFFICIENT PACKS** — *frames pressed into the frequency domain.* Decks of translucent
cards, each etched with one cosine wave; low-frequency cards thick and bright, high-
frequency cards whisper-thin. Output of the DCT Press.
`ASSET (3D): deck of translucent glass cards fanned slightly, each etched with a different frequency sine wave, bottom cards thick and glowing, top cards thin and dim`
**HF DUST** — *quantization waste.* Glittering powder of murdered high frequencies —
all the fine detail the Quantizer rounded to zero. Accumulates around crushers; attracts
and hatches MOSQUITO SWARMS. Late-game recipes reclaim it (mosquito farming, ringing).
`ASSET (2D decal + 3D pile): heap of fine glitter dust, prismatic sparkle, subtly animated shimmer, industrial floor spill shape`
**MOTION VECTOR FLUID (MV-FLUX)** — *pure directional intent, no pixels.* The blood of
temporal compression, carried in pipes: a luminous fluid full of tiny arrows all
pointing the way the image wants to move. Feed it to P-Frame casting. Reversed pipes
produce backwards-flux for B-frames.
`ASSET (3D pipe segment + fluid VFX): transparent industrial pipe filled with glowing cyan fluid, dense stream of tiny bright arrow particles flowing one direction, pulsing`
### Frames (the ammunition of the economy)
**I-FRAME (ANCHOR SLAB)** — *a complete, self-sufficient picture.* Rare, heavy, forklift-
only. A thick crystal slab holding an entire crisp image. Everything downstream leans on
it; remove it from a GOP and the whole chain hallucinates. The premium currency.
`ASSET (3D): thick monolithic crystal slab on a shipping pallet, a complete crisp photographic scene embedded inside, edges beveled like a bank vault door, heavy glow`
**P-FRAME (DELTA WAFER)** — *not a picture — an instruction.* Thin cheap wafer etched
with motion arrows and prediction-error crumbs. Mass-produced from MV-FLUX. Applied to
whatever came before it, right or wrong. The bulk commodity.
`ASSET (3D): thin circuit wafer, translucent, etched with a field of small arrows pointing subtly different directions, a few glowing patch-marks of pixel residue`
**B-FRAME (JANUS WAFER)** — *references past AND future.* Double-faced wafer that needs
two input belts — one from upstream, one from a *future* buffer — making it the routing
puzzle of the mid-game. Highest compression yield of any frame.
`ASSET (3D): two-faced coin-like wafer, one face etched with backward arrows one with forward arrows, thin glowing seam around the rim, slowly counter-rotating faces`
**GOP CRATE** — *one I-frame + N delta wafers, boxed for shipping.* Standard freight of
the Bitstream. Stamp on the side shows the pattern (IPPBPPB...). Tampering with a sealed
crate (I-frame theft) is the core corruption verb.
`ASSET (3D): industrial shipping crate, one glowing slab slot visible at front followed by a rack of thin wafers, stenciled cadence pattern on the side, forklift slots`
### Power & logistics
**BANDWIDTH** — *the electricity of the Bitstream.* Generated by compressing (every bit
you destroy becomes room to move). Carried on glowing bus-bars. Chroma subsampling and
brutal quantization are your coal and your nuclear.
`ASSET (3D powerline piece): industrial pylon with a horizontal light-bar conductor, flowing dashed light pulses, meter dial showing kbps`
**BUFFER CHARGE** — *smoothness in a tank.* Stored decode-ahead, the steam accumulator
of the factory. Drains at exactly one frame per 16.6ms. When a tank empties: **brownout**
— the whole sector freeze-stutters and the audio pop-crackles (see HAZARDS).
`ASSET (3D): rounded industrial holding tank with a vertical progress-bar window, liquid light inside draining in discrete frame-tick steps, red POP hazard sticker near empty line`
**HEAT** — *the price of software decoding.* Invisible resource with visible
consequences: machines shimmer, then throttle (tick rate visibly halves), then scram.
Vented by ASIC coolers. `ASSET (VFX only): heat-haze shader + glowing dull-red machine edges`
### Science packs (research currency — each is a bottled artifact triptych)
| Pack | Bottled contents | Visual |
|---|---|---|
| **ANALOG PACK** | combing + v-hold roll + static | Bakelite TV-tube flask, phosphor green, tiny rolling picture inside |
| **SPATIAL PACK** | macroblock + ringing + bleed | 8×8 gridded acrylic cube, checkerboard core, halo shimmer |
| **TEMPORAL PACK** | melt + bloom + judder | Hourglass whose falling sand smears sideways mid-fall |
| **CONTAINER PACK** | moov shard + offset ribbon + sync key | Nested transparent boxes, innermost holds a glowing index card |
| **CINEMA PACK** (endgame) | wavelet + PCM + hash fragment | Blinding white orb, faint XML script orbiting it |
`ASSET (3D, set of 5): laboratory flask variants per above, shared silhouette, one impossible glowing content each`
---
## 3. GLITCH PRODUCTS (what the factory actually ships)
**MELT** — *motion divorced from form.* Made in the Mosh Reactor by feeding GOP crates
with the I-frame removed. A slow rainbow lava that carries the *shape of movement*
through whatever pixels happen to be lying around. Tier-1 signature product.
`ASSET (3D fluid + 2D shader ref): thick dripping liquid rainbow, surface streaked with directional smear trails, holds ghost-outlines of a previous image while flowing like a new one`
**BLOOM CONCENTRATE** — *motion compounded on itself.* Product of a Bloom Loop left
running: duplicated P-frames stacking the same displacement forever. Sold by intensity
grade; grade-5 bloom visibly tries to escape its canister in one direction.
`ASSET (3D): pressure canister with a viewport, contents visibly stretching and duplicating toward one wall in repeated feathered layers, dented on that side`
**MACROBLOCK BRICKS** — *the quantizer's confession.* Perfect quilted checkerboard
tiles, harvested when flat areas starve. Construction material AND product: commissions
love a good brick.
`ASSET (3D): pallet of square tiles, each a slightly-wrong solid color with hard seams, arranged in an ugly beautiful patchwork`
**RINGING HALOS** — *the Gibbs phenomenon, canned.* Ghostly oscillation rings skimmed
from around over-sharpened edges. Ethereal, faintly vibrating, stored on hooks like
angelic quoits. Ingredient in all "haunted" recipes.
`ASSET (3D): stack of translucent shimmering rings on an industrial hook rack, each ring vibrating with concentric echo lines, faint halo glow`
**STATIC CANISTERS** — *bottled analog snow.* White-noise slush from the Broadcast
Shelf. Cheap filler product; also brine for preserving other artifacts.
`ASSET (3D): glass jar of animated dense monochrome noise, TV-static texture, condensation on glass, masking-tape label`
**MOIRÉ CRYSTAL** — *interference made solid.* Grows wherever fine repeating patterns
meet the sensor grid: swirling maze-lattice crystal. Beautiful, valuable, and a
nuisance — unchecked growth becomes literal maze walls across your factory floor (§5).
`ASSET (3D): crystalline growth whose internal structure is a swirling psychedelic maze pattern, rainbow interference sheen that shifts with view angle`
**FALSE-COLOR SPECKLE** — *chromatic aliasing, panned like gold.* Bright purple/green
speckles sieved off high-contrast edges. Tiny, precious, pure pigment for THE SCREEN.
`ASSET (2D sprite + 3D vial): tiny vivid magenta and acid-green flecks in a glass vial, unnaturally saturated against a neutral gray backdrop`
**JUDDER CAMS** — *the 3:2 pulldown, machined.* Lumpy asymmetric camshafts that convert
smooth rotation into 3-2-3-2 stutter. Installed in products to make them move *wrong*.
`ASSET (3D): machined steel camshaft with deliberately uneven lobes in a 3-2 pattern, motion-blur ghosting on the lobes, precision-industrial finish`
**GAMUT PAINTS** — *color spaces in cans, deliberately mislabeled.* REC709 house-gray,
DCI-P3 cinema-vivid, REC2020 laser-primaries (nearly weapons-grade: pure 630/532/467nm
pigment). Painting a product with the *wrong label* makes downstream decoders clip it
into hyper-saturated blocks or wash it corpse-flat — both sell.
`ASSET (3D, 3 variants): paint cans with chromaticity-diagram labels, one drab, one vivid, one containing visibly impossible laser-pure color that overglows the can rim`
**EXPLOSIVE HITS** — *offset corruption, weaponized.* The Hex Splicer's product: bytes
fetched from the middle of a frame instead of its start code. Unstable prisms that
detonate into macroblock shrapnel and color inversion if belted too far. Ship fast.
`ASSET (3D): angular unstable prism crackling with inverted-color lightning, half its faces showing a scrambled image, hazard-orange containment frame straining`
**THE MEGAGLITCH** *(victory product)* — see §7.
---
## 4. MACHINES
**SEAM EXTRACTOR** — *the miner.* A drill-head that chews mdat seams and spits raw ore.
Era-skinned per stratum (film-reel drill in the Reel Beds, etc.).
`ASSET (3D): industrial mining drill with a CRT monitor for a head, drill bit made of stacked play-button triangles, cable dreadlocks, grimy yellow chassis`
**DEMUXER** — *the first splitter.* Cleaves raw ore into luma / chroma / audio / MV-flux
output lanes. Literally a machine with one input belt and four differently-glowing
output belts. `ASSET (3D): squat sorting machine with one big intake hopper and four labeled output chutes glowing white, magenta, green, cyan`
**DEMOSAICER** — *makes color from checkerboard grit.* Interpolation looms weave RGGB
gravel into full-color plates. Parameter dial selects algorithm (AMaZE/LMMSE/VNG4 as
absurd brand names on the faceplate). Feed it stripes → it jams and extrudes MOIRÉ.
`ASSET (3D): loom-like machine threading red green blue gravel into a smooth color sheet, weaving arms mid-motion, big algorithm selector dial with baroque labels`
**DCT PRESS** — *spatial → frequency.* A hydraulic press whose platen is engraved with
the 8×8 DCT basis-function grid; slams pixel blocks into coefficient card decks. Bigger
era-tier presses (16×16, 64×64, 128×128) are progressively more cathedral-like.
`ASSET (3D): heavy hydraulic press, platen engraved with the iconic grid of cosine-wave basis patterns, glowing card deck sliding out of the output slot`
**QUANTIZER** — *the crusher, the moneymaker, the sin.* Adjustable jaw that rounds
coefficients to zero. Its single dial (Q) is the most important lever in the factory:
right = more BANDWIDTH + more HF DUST + uglier products. The dial goes to "CRIME".
`ASSET (3D): brutal jaw-crusher with an oversized rotary dial labeled with a smiley face at one end and a skull at the other, glitter dust leaking from its seams`
**SUBSAMPLER** — *chroma concentrator.* Centrifuge that spins 4:4:4 slurry down to
4:2:2 / 4:1:1 / 4:2:0 grades, venting bandwidth. Leaks by design; place downwind.
`ASSET (3D): centrifuge tank with three grade taps, iridescent stains streaking down its flank, ventilation stack emitting faint rainbow vapor`
**MV EXTRACTOR & P-CASTER** — *optical-flow refinery.* Watches decoded footage with an
array of eye-cameras and pipes off MV-FLUX; the paired caster prints delta wafers.
`ASSET (3D): machine studded with dozens of small camera lenses all tracking subtly, cyan fluid pipe exiting the back, wafer printer sidecar`
**GOP ASSEMBLER** — *the packer.* Robotic arm slots 1 anchor slab + N wafers into
crates following a cadence template (editable: IPP, IBBP, I-only…). Cadence templates
are themselves unlockable items.
`ASSET (3D): packing station with robot arm mid-slot, crate on rollers, cadence template punch-cards racked on the wall`
**MOSH REACTOR** — *tier-1 corruption core.* A decoder run in a sealed vessel and
deliberately starved of anchors: crates go in, the I-frame is winched OUT through a
side airlock (recoverable — sell it!), and the hallucinating decoder extrudes MELT.
`ASSET (3D): pressure reactor with a porthole showing swirling rainbow smear, side airlock cradling a stolen glowing slab, warning placard of a melting face`
**BLOOM LOOP** — *not a machine — a belt topology.* Any closed belt circle with a
P-FRAME DUPLICATOR on it. Contents compound each lap; tap the loop to draw off graded
bloom. Runaway loops (grade >5) burst and flood the sector with directional smear.
`ASSET (3D, belt piece + duplicator): conveyor segment whose cargo visibly doubles and feathers as it passes a stamping duplicator arch, escalating glow per lap`
**FIELD LOOM (INTERLACER/DEINTERLACER)** — *weaves and unweaves time.* Splits frames
into odd/even field splinters captured a tick apart, or (run in reverse) weaves them
back — imperfectly, unless fed a motion-adaptive coprocessor module.
`ASSET (3D): weaving loom threading two horizontal slat-sets together into one sheet, teeth slightly misaligned, phosphor green worklight`
**TELECINE PULLER** — *the 3:2 judder mill.* Cranks 24fps film stock into 30fps product
by duplicating fields on a lumpy cam (see JUDDER CAMS). Runs backwards as an IVTC
purifier — one of the few machines The Correction also uses, awkwardly.
`ASSET (3D): film-projector-meets-gearbox, visible uneven cam linkage, two film paths in and out with mismatched sprocket rhythms`
**HEX SPLICER** — *endgame chaos lathe.* A robotic hex-editor that nudges stco offsets
by a few bytes so downstream decoders fetch garbage. Produces EXPLOSIVE HITS. Requires
CONTAINER PACK research and a steady buffer feed or it splices *itself*.
`ASSET (3D): surgical robot arm poised over a scrolling hexadecimal ticker-tape, one byte pair held glowing in its tweezers, containment glass, red interior light`
**DECODE ASIC** *(power gen)**cool, efficient, inflexible.* Handles standard profiles
only. `ASSET (3D): server-blade monolith with a huge heatsink afro and a serene LED face`
**SOFTWARE DECODER** *(burst gen)**decodes ANYTHING, at any temperature.* Doubles
throughput on demand; generates HEAT; throttles theatrically (fan scream, tick-rate
halving). `ASSET (3D): overclocked tower PC held together with zip ties and prayer, case open, fans glowing hot-red, thermal haze`
**BUFFER TANK** — see BUFFER CHARGE. **MUXER** — see §7 (it's the rocket).
---
## 5. HAZARDS, WILDLIFE & THE ENEMY
### Wildlife (ambient, farmable)
**MOSQUITO SWARMS** — *quantization noise, alive.* Shimmer-gnat clouds that hatch from
HF DUST piles and cluster around any sharp edge in your factory (signage, fresh luma,
the player). Degrade machine precision. Catchable in edge-baited traps → sellable as an
artifact ingredient. The doc's "shimmering swarm around outlines," literalized.
`ASSET (VFX + 3D trap): particle cloud of tiny flickering pixel-gnats; trap is a bug-zapper shaped like a high-contrast black-white edge target`
**GIBBS WRAITHS** — *ringing, ambulatory.* Ghostly figures made of concentric echo
halos that spawn where contrast is extreme. Harmless but they *duplicate your outline*
and follow you, slightly out of phase. Density = a readable "over-sharpening" warning.
`ASSET (3D character, rigged): translucent humanoid made of nested outline rings, no fill, constantly oscillating between two positions`
**MOIRÉ BLOOM** — *crystal maze growth.* Spreads across floors wherever repeating
textures face each other. Harvest young = product; let it mature = your factory now has
corridors you didn't design. Some players will farm it as free walls.
### System hazards
**BROWNOUT (BUFFER UNDERRUN)** — a drained buffer freezes its whole power domain in a
stuttering last-frame loop while the audio bus emits catastrophic pop-crackle. Machines
resume with a lurch and 1 tick of scrambled output (free chaos, occasionally useful).
**THERMAL SCRAM** — overheated software decoders halve the local tick rate; the *game
itself* visibly drops frames in that sector. The lag IS the damage model.
**BLEED FLOODS** — slurry spills tint every product crossing them until mopped.
### THE CORRECTION (the enemy faction)
The immune system of the Bitstream. It does not hate you; it *cannot perceive* you —
only deviation, which it repairs. Escalation ladder:
1. **PARITY MITES** — beetle-drones that patrol belts and quietly *fix* artifacts back
into clean product (your melt becomes... footage of a lake). Early nuisance.
`ASSET (3D, rigged): rounded beetle drone, hospital-white shell, tiny green checkmark eye, solder-iron proboscis, unsettlingly gentle`
2. **CHECKSUM WARDENS** — heavier units that audit a machine, roll back its last N
outputs, and stamp it with a compliance seal (disabled until you pry the seal off).
`ASSET (3D, rigged): tall white filing-cabinet golem, drawer-chest full of hashes, rubber-stamp fist, red LED ledger eyes`
3. **REDUNDANCY GUNSHIPS** — airborne RAID arrays that *mirror* a sector of your
factory: a clean parallel copy materializes alongside and tries to out-ship you to
THE SCREEN. Your own layout, sanitized, weaponized. Kill the parity link to pop it.
`ASSET (3D): hovering white chassis of five mirrored disk-platter rotors in a row, projecting a ghostly duplicate landscape beneath itself`
4. **HASH AUDITORS** (endgame, from the Fortress) — DCP inquisitors. One byte of
deviation in their scan radius and they refuse — not attack, *refuse* — locking
regions into unplayable 0:00 stasis until you break the lock (see moov heists, §6).
`ASSET (3D, rigged): faceless white-marble cinema usher, flashlight that projects an XML scroll, padlock icon halo, moves only between frames`
---
## 6. RESEARCH, COMMISSIONS & SIDE LOOPS
**TECH TREE = MEDIA ARCHAEOLOGY.** You research *downward* (excavating older strata for
analog secrets) and *upward* (deriving newer codecs). Era multipliers use the real
efficiency history: MPEG-2 → H.264 ≈ ×2 compression, → HEVC ×1.5, → AV1 ×1.3, each era
also unlocking bigger DCT presses (8×8 → 128×128) and new cadence templates.
**PRESTIGE = REMASTER.** Reset the file, keep knowledge ("muscle memory of the codec"),
re-encode the world one generation newer. Generation loss is the prestige *bonus* here —
each remaster starts with era-appropriate ambient artifacts already seeded in the soil.
**COMMISSIONS** arrive by fax (the fax machine is sacred, do not ask why it works).
Absurd recipe orders from unseen glitch-art collectors, e.g.:
- *"1 MELTED ANCHOR SLAB + 3 MOSQUITO SWARMS (live) + 40 bbl CHROMA SLURRY (4:2:0) = 1 VAPORWAVE SUNSET. Deliver before the beat drops."*
- *"600 MACROBLOCK BRICKS, sky-blue, MISLABELED REC2020. Purpose: none of your business."*
- *"One (1) GIBBS WRAITH, willingly."*
**MOOV HEISTS** (side mode, feeds the campaign): Hash Auditors lock a region → you
infiltrate its container, scan scrolling hex for real frame signatures (AAC `0xFFF`
sync words, H.264 NAL start codes), and rebuild/forge the stco/stsz index to your
advantage. Papers-Please-meets-defrag; the minigame from GLYTCH's REPAIR mode, reused.
**AUDIO WING** (optional expansion): PCM ingots, sample-rate drift (44.1k product on a
48k line slowly desyncs the *whole factory's* rhythm), and the asymmetric sync scoring
rule — audio may lag ≤120ms, but audio that *leads* by even a few ms triggers instant
commission rejection. The pickiest customers are evolutionary.
---
## 7. THE ENDGAME: THE FORTRESS & THE MEGAGLITCH
**THE MUXER** is your rocket: a launch-gantry that assembles a complete container —
ftyp seal, mdat payload masses, and the hand-built **moov brain** (stsz ledger + stco
map + stts metronome + stss keyring, each its own late-game production line) — and
ships the finished file to THE SCREEN as one colossal composited corruption: the
**MEGAGLITCH**. Victory plays your actual factory's shipping history as a rendered
video. (Real-render tie-in: the layout compiles to an ffmpeg/PYXLFK graph — belts are
`filter_complex`; the save file is a render-farm job for the Mac fleet.)
`ASSET (3D, hero structure): launch gantry embracing a monolithic container rocket built of nested translucent boxes, innermost box a glowing brain of index tape-reels, umbilical belts feeding it from every direction`
**THE FORTRESS (DCP)** is the final biome and the game's thesis-statement difficulty
cliff: inside its walls *your entire verb set fails*. JPEG 2000 wavelet masonry — no
blocks, no seams to mosh. Every frame intra-coded — no dependency chains to starve. No
motion vectors *at all*. PCM audio too pure to pop. AES-CBC gates, KDM time-locks that
only open during scheduled showings, and hash sentries with zero-tolerance policy.
You cannot corrupt The Fortress. The twist: **you don't have to.** Its own perfection
is the exploit — flip one byte and it *refuses to play itself*. The final act inverts
your win condition from "make it glitch" to "make it silent," or steal the KDM window.
`ASSET (environment kit): blinding white cathedral of smooth waveleted curves, zero right angles, XML scripture friezes, spotlight-usher sentries, one hairline black crack (scripted)`
---
## 8. VISUAL STYLE GUIDE (for every MODELBEAST batch)
- **Two-material rule:** every asset = grimy industrial body (rust, gaffer tape, hazard
stripes, zip ties) + exactly ONE impossible element (CRT glow, iridescent fluid,
interference sheen, embedded image). The impossible element is the silhouette's focus.
- **Palette:** base world in desaturated industrial neutrals; ALL saturation belongs to
media/artifacts (chroma slurry, gamut paints, THE SCREEN). Color = value = literally
the resource. The world gets more colorful only where the player has made a mess.
- **Era skins:** Reel Beds = sepia/brass/celluloid; Broadcast = phosphor green + wood-
grain TV; Disc = rainbow-on-black gloss; Stream = flat white plastic + LED; Fortress
= blinding matte white. The Correction is always hospital-white with green checkmarks.
- **Animation motif:** clean things move at 60fps; corrupted things move *wrong on
purpose* — 3:2 judder for film-era, 2-frame loops for analog, held-then-lurch for
buffer trouble. Wrongness of motion is a faction uniform.
- **UI voice:** OSHA-poster deadpan. Everything horrifying is labeled like routine
industrial equipment.
## 9. MODELBEAST PRODUCTION NOTES
- Host: `MB_HOST=http://100.89.131.57:8777` (m3ultra, tailnet). Headless via `mb` CLI;
ops brief in `~/Documents/modelbeast/AGENTS.md` on that machine.
- Pipeline per 3D asset: concept image → `sf3d` (local, fast) for props; `fal_trellis2`
(paid, higher fidelity) reserved for hero assets (Muxer, Mosh Reactor, Fortress kit)
— check the canon cache before any paid generation (asset-pipeline house rule).
- Characters/creatures that need rigs (Gibbs Wraith, Parity Mite, Wardens, Auditor):
route through the character_kit fleet rule — generate the mesh, then bind to the
shared clip banks; bespoke motion (the Auditor's between-frames teleport, Mite repair
loop) is hand-authored, not Mixamo-searched.
- Belts/pipes/tiles are kit-bashed modular pieces: generate 34 segment variants each,
keep polycounts belt-friendly; VFX-heavy entries (melt, static, heat) are shaders,
not meshes — the ASSET prompt is the *reference image* prompt for the shader artist
(also you).
- Every asset generated gets its element's codex name — this file is the manifest;
check off against it.

57
fktry/CONTRACTS.md Normal file
View File

@ -0,0 +1,57 @@
# CONTRACTS — how the lanes fit together (ORCHESTRATOR-OWNED)
The binding type definitions live in [`src/contracts.ts`](src/contracts.ts) — that file
is the law. This doc explains the shape of the machine in prose.
## Data flow
```
data/*.json ──▶ main.ts loads GameData
┌───────────┼─────────────────────────┐
▼ ▼ ▼
sim.init() renderer.init() ui.init() / screen.init()
30 tps fixed timestep: sim.tick()
├─▶ sim.snapshot() ──▶ renderer.render(snap, alpha) (every rAF)
│ ──▶ ui.update(snap, events)
└─▶ sim.drainEvents() ─▶ screen.onEvents(events)
user input: UI builds Commands ─▶ bus.dispatch ─▶ sim.enqueue
renderer supplies pickTile + ghost preview
```
## Ownership map
| Path | Owner | Everyone else |
|---|---|---|
| `src/contracts.ts`, `src/main.ts`, root config, this doc, `MASTERPLAN.md` | orchestrator | read-only |
| `src/sim/**` | LANE-SIM | read-only |
| `src/render/**`, `public/assets/**` (consumption) | LANE-RENDER | read-only |
| `src/ui/**` | LANE-UI | read-only |
| `src/screen/**` | LANE-SCREEN | read-only |
| `data/**` | LANE-DATA | read-only |
| `lanes/LANE-X.md` NOTES section | lane X | orchestrator writes ORDERS section |
| `../docs/FKTRY_LORE.md` | canon | read-only for all lanes |
## Key invariants
- **Snapshot is read-only.** Renderer/UI/Screen never mutate it. Sim may reuse
internal buffers between snapshots; consumers must not hold references across frames.
- **All mutation goes through Commands.** No lane reaches into sim internals.
- **Events are drained once per frame by main.ts** and fanned out; consumers get the
same array — treat it as immutable.
- **IDs are data-driven.** Renderer/UI/Screen never hard-code item/machine ids; they
key off `GameData` and the `asset` field. New content should light up with zero code.
- **`alpha`** in `renderer.render(snap, alpha)` is the 0..1 fraction between the last
sim tick and the next — use it to interpolate belt item positions for smooth motion.
- **Belt visual speed** derives from `MachineDef.beltSpeed` and `BeltItem.t`; sim owns
the truth, renderer owns the smoothness.
- **Coordinates**: tile grid, origin at world center, +x east, +y south. `Dir` 0..3 =
N,E,S,W clockwise. Machine `pos` = origin corner (min x, min y of footprint).
## Contract-change requests
Written in your lane NOTES as `CONTRACT REQUEST: <what> — <why>`. The orchestrator
batches them between rounds. Until granted, work around locally inside your lane.

74
fktry/MASTERPLAN.md Normal file
View File

@ -0,0 +1,74 @@
# FKTRY MASTERPLAN — orchestration doc
**Read this first, every round, whoever you are.**
FKTRY is a Factorio-style glitch-production factory game set inside a dying video file.
The full world bible is [`../docs/FKTRY_LORE.md`](../docs/FKTRY_LORE.md) — machines,
resources, enemies, eras, and the visual style guide all live there. Read it once,
fully, before your first round. It is canon; when in doubt about tone or naming, the
codex wins.
## Cast
- **ORCHESTRATOR** (Fable session with John): owns this file, `CONTRACTS.md`,
`src/contracts.ts`, `src/main.ts`, and the CURRENT ORDERS section of every lane doc.
Reviews each round, writes the next round's orders.
- **FIVE LANES** (Opus 4.8 executor sessions, run in parallel): SIM, RENDER, UI,
SCREEN, DATA. Each lane has a doc in `lanes/` and an exclusive set of owned paths.
- **MODELBEAST** (asset factory on the m3ultra): generated GLB/sprite assets arrive
asynchronously into `public/assets/` — LANE-RENDER hot-swaps them in, nobody waits
for them.
## The round protocol (every lane, every round)
1. `git pull` first. Read YOUR lane doc (`lanes/LANE-<X>.md`): the **CURRENT ORDERS**
section is your task list for this round. Read `CONTRACTS.md` + `src/contracts.ts`.
2. Do the work. Stay inside your **owned paths** (listed in your lane doc). You may
READ anything in the repo; you WRITE only in your lane.
3. Verify before reporting — all three, every round:
- `npm run check` (tsc clean)
- `npm test` (all green; SIM lane especially)
- `npm run dev` → open http://localhost:8150 → confirm the app boots and YOUR
surface behaves; check the browser console for errors. "Should work" ≠ verified.
4. Append a round entry to the **NOTES** section at the bottom of your lane doc
(template is there). Be honest: broken/unfinished things get written down, not
hidden. Questions and contract-change requests go here too — the orchestrator
reads every note.
5. Commit with your lane prefix: `[sim] belt merge logic + tests`. One or a few
focused commits, not one mega-commit. Push.
## Hard rules
- **Never edit**: `MASTERPLAN.md`, `CONTRACTS.md`, `src/contracts.ts`, `src/main.ts`,
root config (`package.json`, `tsconfig.json`, `index.html`), other lanes' paths,
other lanes' docs. Need a dependency added or a contract changed? Write it in NOTES
with rationale; the orchestrator handles it next round.
- **Never redesign the architecture.** You are an executor on a lane. Brilliant ideas
belong in NOTES ("PROPOSAL:"), not in the diff.
- **Determinism (SIM)**: no `Date.now()`, no unseeded `Math.random()` inside the sim.
Same seed + same command sequence = identical state, forever. Everything else may be
as flashy as it likes.
- **Placeholders are honorable.** Do not block on assets, other lanes' features, or
missing data. Build to the contract, stub the rest, note the stub.
- If the app won't boot because of someone else's in-flight work, note it, build
against the contracts anyway, and verify with `npm test`/`check` this round.
## Milestones (orchestrator's map — lanes don't self-assign ahead)
- **M1 — FLOW** (rounds 1-2): place extractor → belts → demuxer → quantizer → mosh
reactor → shipper; items visibly ride belts; MELT ships; THE SCREEN corrupts in
response. The whole fantasy in miniature.
- **M2 — PRESSURE**: bandwidth economy + brownouts, heat/throttling, HF dust piles,
first wildlife (mosquito swarms), commissions via fax UI.
- **M3 — DEPTH**: tech tree + eras, bloom loops, subsampler bleed, moiré growth,
save/load, The Correction (parity mites).
- **M4 — AWESOME**: megaglitch muxer, strata/biomes, remaster prestige, audio,
The Fortress. Real MODELBEAST assets throughout.
## Orchestrator review checklist (per round)
Pull, read all five NOTES entries, then: `npm run check && npm test`, play the build,
diff each lane against its owned paths (ownership violations get reverted), verify
claimed features actually work, reconcile cross-lane questions, update CONTRACTS if
(and only if) two lanes need it, write next CURRENT ORDERS in each lane doc, commit
`[orchestrator] round N orders`.

View File

@ -0,0 +1,4 @@
[
{ "id": "first-taste", "flavor": "One (1) unit of MELT. We hear you make it fresh. No questions will be answered.",
"wants": { "melt": 1 }, "rewardBandwidth": 50 }
]

11
fktry/data/items.json Normal file
View File

@ -0,0 +1,11 @@
[
{ "id": "mdat-ore", "name": "MDAT ORE", "codex": "2:mdat-ore", "tier": 0, "color": "#1a1a22" },
{ "id": "luma-bars", "name": "LUMA BARS", "codex": "2:luma-bars", "tier": 1, "color": "#c0c0c0" },
{ "id": "chroma-slurry", "name": "CHROMA SLURRY", "codex": "2:chroma-slurry", "tier": 1, "color": "#ff3fd4" },
{ "id": "coefficient-pack", "name": "COEFFICIENT PACK", "codex": "2:coefficient-packs", "tier": 1, "color": "#3fffe0" },
{ "id": "hf-dust", "name": "HF DUST", "codex": "2:hf-dust", "tier": 1, "color": "#e8e0ff" },
{ "id": "anchor-slab", "name": "I-FRAME (ANCHOR SLAB)", "codex": "2:i-frame", "tier": 1, "color": "#fff2a0" },
{ "id": "delta-wafer", "name": "P-FRAME (DELTA WAFER)", "codex": "2:p-frame", "tier": 1, "color": "#7fb0ff" },
{ "id": "gop-crate", "name": "GOP CRATE", "codex": "2:gop-crate", "tier": 1, "color": "#a07840" },
{ "id": "melt", "name": "MELT", "codex": "3:melt", "tier": 2, "color": "#ff7a3f" }
]

16
fktry/data/machines.json Normal file
View File

@ -0,0 +1,16 @@
[
{ "id": "seam-extractor", "name": "SEAM EXTRACTOR", "codex": "4:seam-extractor", "kind": "extractor",
"footprint": { "x": 2, "y": 2 }, "recipes": ["extract-mdat"], "powerDraw": 2, "asset": "seam-extractor" },
{ "id": "belt", "name": "STREAM BELT", "codex": "4:belts", "kind": "belt",
"footprint": { "x": 1, "y": 1 }, "recipes": [], "powerDraw": 0, "beltSpeed": 2, "asset": "belt" },
{ "id": "demuxer", "name": "DEMUXER", "codex": "4:demuxer", "kind": "crafter",
"footprint": { "x": 2, "y": 2 }, "recipes": ["demux-ore"], "powerDraw": 3, "asset": "demuxer" },
{ "id": "quantizer", "name": "QUANTIZER", "codex": "4:quantizer", "kind": "crafter",
"footprint": { "x": 2, "y": 2 }, "recipes": ["quantize"], "powerDraw": 4, "asset": "quantizer" },
{ "id": "mosh-reactor", "name": "MOSH REACTOR", "codex": "4:mosh-reactor", "kind": "crafter",
"footprint": { "x": 3, "y": 3 }, "recipes": ["mosh"], "powerDraw": 8, "asset": "mosh-reactor" },
{ "id": "decode-asic", "name": "DECODE ASIC", "codex": "4:decode-asic", "kind": "power",
"footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 0, "powerGen": 20, "asset": "decode-asic" },
{ "id": "shipper", "name": "SCREEN UPLINK", "codex": "7:the-muxer", "kind": "shipper",
"footprint": { "x": 2, "y": 2 }, "recipes": [], "powerDraw": 5, "asset": "shipper" }
]

6
fktry/data/recipes.json Normal file
View File

@ -0,0 +1,6 @@
[
{ "id": "extract-mdat", "machine": "seam-extractor", "inputs": {}, "outputs": { "mdat-ore": 1 }, "ticks": 30, "bandwidth": 2 },
{ "id": "demux-ore", "machine": "demuxer", "inputs": { "mdat-ore": 2 }, "outputs": { "luma-bars": 1, "chroma-slurry": 1 }, "ticks": 45, "bandwidth": 3 },
{ "id": "quantize", "machine": "quantizer", "inputs": { "luma-bars": 1 }, "outputs": { "coefficient-pack": 1, "hf-dust": 1 }, "ticks": 30, "bandwidth": -6 },
{ "id": "mosh", "machine": "mosh-reactor", "inputs": { "coefficient-pack": 2, "chroma-slurry": 1 }, "outputs": { "melt": 1, "anchor-slab": 1 }, "ticks": 90, "bandwidth": 10 }
]

1
fktry/data/tech.json Normal file
View File

@ -0,0 +1 @@
[]

22
fktry/index.html Normal file
View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WIMVEE FKTRY</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html, body { margin:0; padding:0; height:100%; background:#0a0812; overflow:hidden;
font-family:"Courier New", ui-monospace, monospace; }
#game { position:fixed; inset:0; } /* LANE-RENDER mounts here */
#screen { position:fixed; top:10px; left:50%; transform:translateX(-50%);
width:320px; height:180px; border:1px solid #2a2440; z-index:5; } /* LANE-SCREEN canvas */
#ui { position:fixed; inset:0; pointer-events:none; z-index:10; } /* LANE-UI root (children re-enable pointer-events) */
</style>
</head>
<body>
<div id="game"></div>
<canvas id="screen" width="640" height="360"></canvas>
<div id="ui"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

71
fktry/lanes/LANE-DATA.md Normal file
View File

@ -0,0 +1,71 @@
# LANE-DATA — content, recipes, balance, and canon-keeping
You are an Opus 4.8 executor on the DATA lane of FKTRY. Read `../MASTERPLAN.md` and
`../CONTRACTS.md` first. Your bible is `../../docs/FKTRY_LORE.md` — you are its
transcriber into machine-readable canon. When the codex and convenience disagree, the
codex wins; when the codex is silent, invent IN ITS VOICE and note what you invented.
**Mission:** every item, machine, recipe, tech, and commission as clean JSON, plus the
validator that keeps five parallel lanes from drowning in dangling ids. Content should
light up in-game with zero code changes from other lanes.
**Owned paths:** `data/**`, `src/sim/data-validate.test.ts` — EXCEPTION: this one test
file in sim's directory is yours; it validates data, not sim. Nothing else in
`src/sim/`.
**Never touch:** contracts, main.ts, other lanes' code. Schema changes are CONTRACT
REQUESTs.
**Standing rules (all rounds):**
- Ids: kebab-case matching codex names (`chroma-slurry`, `mosh-reactor`). `codex`
field points at the lore doc section (`"3:melt"` = §3, MELT entry).
- Balance philosophy: absurd flavor, honest math. Ratios should create real routing
pressure (byproducts you must deal with, not vent). Steal shamelessly from the
doc's real numbers (era efficiency multipliers, 4:2:0 = half the chroma, GOP
cadences).
- Never delete an id other lanes may reference mid-round — deprecate in NOTES first.
---
## CURRENT ORDERS — Round 1 (goal: milestone M1 "FLOW" + M2 prep)
The seed JSONs in `data/` are orchestrator placeholders — correct them, then grow:
1. **Validator first** (`src/sim/data-validate.test.ts`, vitest): every recipe's
machine + items exist; every machine's recipes exist and point back at it; every
tech unlock exists; commission wants exist; ids unique + kebab-case; colors valid
hex; footprints ≥1×1. This test is the lane's handshake — it must fail loudly on
any dangling reference.
2. **Items** → full §2 + §3 catalog: all raw resources, refined intermediates, frames,
the five science packs, and tier-2 products (melt, bloom-concentrate,
macroblock-bricks, ringing-halos, static-canister, moire-crystal, false-color-
speckle, judder-cams, explosive-hits, gamut paints ×3). Colors from codex vibes.
3. **Machines** → full §4 catalog (demosaicer, dct-press, subsampler, mv-extractor,
p-caster, gop-assembler, bloom-duplicator, field-loom, telecine-puller,
hex-splicer, software-decoder, buffer-tank...). Machines beyond M1's seven should
exist in data even though sim/render only generically support crafter/power kinds
— that's fine, they'll work as generic crafters until given bespoke mechanics.
4. **Recipes** → wire the economy: ore → luma/chroma/mv-flux (demux), luma → coeff
packs + hf-dust (quantize, generates bandwidth), mv-flux → delta-wafers, slab +
wafers → gop-crate, gop-crate → melt + reclaimed slab (mosh), coeff overdrive →
macroblock-bricks + double hf-dust, hf-dust → static-canister, science pack
assembly ×3 (analog/spatial/temporal). Include at least two genuinely absurd
ratios for comedy that still balance.
5. **Tech tree v1**`disc` and `stream` era nodes: unlock quantizer tiers, mosh
reactor, bloom duplicator, subsampler grades. Costs in science packs.
6. **Commissions** → 8-10 in full fax voice (§6 has three exemplars). Escalating
absurdity and quantity; at least one demanding a *live* hazard product you know
isn't shippable yet (mark it `"wants": {"mosquito-swarm": 3}` — forward canon;
the validator should allow items that exist in data even if no recipe makes them).
**Definition of done:** `npm test` green including your validator; dev server still
boots with expanded data (placeholders render for everything); every entry traceable
to a codex section. NOTES list what you invented beyond the codex.
---
## NOTES (append-only log, newest at bottom)
```
### Round N — YYYY-MM-DD — [your model]
SHIPPED: / DECISIONS: / INVENTED: / BLOCKED/BROKEN: / CONTRACT REQUEST: / NEXT:
```

View File

@ -0,0 +1,67 @@
# LANE-RENDER — three.js world renderer
You are an Opus 4.8 executor on the RENDER lane of FKTRY. Read `../MASTERPLAN.md` and
`../CONTRACTS.md` first; the world bible + visual style guide (§8) is
`../../docs/FKTRY_LORE.md` — §8 is YOUR section, follow it.
**Mission:** make the Bitstream visible. Isometric factory view, machines, belts with
smoothly interpolated items, ghosts, selection — and the hot-swap pipeline that lets
MODELBEAST GLB assets land mid-development without a code change.
**Owned paths:** `src/render/**`, `public/assets/**` (manifest + loading; the files
themselves arrive from MODELBEAST).
**Never touch:** contracts, main.ts, other lanes. Sim is truth: you render snapshots,
you never simulate. If motion looks wrong, interpolate better or note it — don't
"fix" positions locally.
**Standing rules (all rounds):**
- 60fps with 500 entities + 2,000 belt items on an M-series MacBook. Instanced meshes
for belts/items from day one — you will not get to retrofit this later.
- Placeholder aesthetic is a feature: crisp box-primitive machines tinted per codex
`color`, NOT gray cubes. Two-material rule even for placeholders: dark body + one
emissive accent.
- Every entity mesh keys off `MachineDef.asset` / `ItemDef.id` through the registry —
zero hard-coded ids.
---
## CURRENT ORDERS — Round 1 (goal: milestone M1 "FLOW")
Replace the stub in `src/render/index.ts`:
1. **Camera + controls.** Orthographic isometric-style view. Pan: middle-drag or WASD.
Zoom: wheel (clamped ~8..40 world units). Window resize handled.
2. **Ground.** Tile grid over the 64×64 world; subtle Stream-Crust styling (dark
`#0a0812` base, faint `#181425` lines) — see style guide §8. A soft vignette or
fog so the world doesn't just end.
3. **Asset registry + hot-swap.** `registry.ts`: maps asset key → THREE.Object3D
factory. Default: procedural placeholder (footprint-sized beveled box, body color
from data, one emissive accent, tiny idle animation — e.g. quantizer jaw bob).
On init, attempt `GLTFLoader` for `public/assets/models/<key>.glb`; if present it
replaces the placeholder factory. Also poll or re-check on a 10s timer in dev so
assets dropped in mid-session appear without reload (note what you implement).
4. **Entities.** Sync scene to `snap.entities` (add/remove/rotate). Belts get a flat
directional piece with an animated arrow/chevron texture (shader or UV scroll).
5. **Belt items.** InstancedMesh per item type (colored octahedron/box placeholder),
positioned from `BeltItem.t` + belt entity pos/dir, interpolated with `alpha` for
butter-smooth motion at 30 tps sim.
6. **Picking + ghost.** `pickTile` via raycast to the ground plane. `setGhost` shows
the selected machine's placeholder at the hovered tile, tinted cyan; red tint if
tiles occupied (you can read occupancy from `snap.entities` footprints).
7. **Feedback dressing (cheap but load-bearing):** jammed machines get a small
blinking amber light; brownout dims the whole scene ~30% and flickers lights
(listen to snapshot.bandwidth.brownout).
**Definition of done:** dev server shows the factory; placing a line from the UI (or
console dispatch) shows machines + items gliding down belts smoothly; a GLB dropped
into `public/assets/models/quantizer.glb` replaces the placeholder. `npm run check`
clean. NOTES written.
---
## NOTES (append-only log, newest at bottom)
```
### Round N — YYYY-MM-DD — [your model]
SHIPPED: / DECISIONS: / BLOCKED/BROKEN: / CONTRACT REQUEST: / PROPOSAL: / NEXT:
```

View File

@ -0,0 +1,65 @@
# LANE-SCREEN — THE SCREEN compositor & glitch shaders
You are an Opus 4.8 executor on the SCREEN lane of FKTRY. Read `../MASTERPLAN.md` and
`../CONTRACTS.md` first; world bible `../../docs/FKTRY_LORE.md` (§1 THE SCREEN, §3
products, §8 style). Also read `../../index.html` — the original WIMVEE GLYTCH
prototype — its fragment shader is your seed stock: 8 glitch params (mosh, melt,
chroma, tear, roll, burn, noise, freeze) already written and tuned.
**Mission:** the payoff object. THE SCREEN is the sky-display everything ships to —
the player's factory output composited live as escalating video corruption. When
someone screenshots this game, they screenshot your canvas. It must start sterile and
end deranged, and every shipped item must visibly matter.
**Owned paths:** `src/screen/**`.
**Never touch:** contracts, main.ts, other lanes, the GLYTCH prototype (read-only
seed stock — port, don't move).
**Standing rules (all rounds):**
- WebGL (raw or three.js — your call, note it) on the provided 640×360 canvas.
Budget: <2ms/frame; THE SCREEN is garnish, not the meal.
- Corruption must be *legible*: a player who ships 40 chroma slurry should SEE chroma
damage specifically. Item → artifact mapping is the design core, keep it data-driven
off `ItemDef.id`.
- Base content starts as procedural "clean programming" — sterile SMPTE-ish test
patterns, slow pans, timestamp overlay (port `makeBaseTexture` thinking from the
prototype; animate it subtly so corruption has motion to chew on).
---
## CURRENT ORDERS — Round 1 (goal: milestone M1 "FLOW")
Replace the stub in `src/screen/index.ts`:
1. **Port the GLYTCH shader stack** into a proper module (`glitchStack.ts`): vertex +
fragment shader with the 8 uniforms, compiled against your base texture. Keep
param semantics identical to the prototype (it's tuned; respect the tuning).
2. **Corruption ledger.** `onEvents`: each `shipped` event adds to a per-item tally.
Map items → param pressure in `corruptionMap.ts`, e.g. `melt → u_mosh+u_melt`,
`chroma-slurry → u_chroma`, `hf-dust → u_noise`, `anchor-slab → REDUCES overall
corruption slightly` (shipping clean anchors sanitizes — nice tension, note it for
the orchestrator). Unknown items → small generic noise bump. Pressure eases toward
its tally-driven target (lerp) so shipments feel like waves, not switches.
3. **Escalation curve.** Total corruption 0→1 over roughly the first 100 shipped
items (tune by feel), log-ish so early shipments feel huge. Above 0.8, add
compound wobble (params cross-modulate slowly) so a mature factory's SCREEN looks
*alive*, not maxed-out static.
4. **Brownout reaction.** On `brownout:true`, freeze-frame + heavy dropout lines +
desync judder until it lifts. The factory's pain shows in the sky.
5. **Idle charm.** With zero corruption, the clean feed runs bland loops with a
"NOTHING IS WRONG" chyron. First shipment ever interrupts it with a single frame
of full-white glitch — the awakening. Cheap, unforgettable.
**Definition of done:** dev build — screen starts sterile; console-dispatch a few
shipments (or wait for the real chain) and corruption rises, item-specifically;
brownout visibly seizes it. `npm run check` clean, frame budget respected (note
measured ms). NOTES written.
---
## NOTES (append-only log, newest at bottom)
```
### Round N — YYYY-MM-DD — [your model]
SHIPPED: / DECISIONS: / BLOCKED/BROKEN: / CONTRACT REQUEST: / PROPOSAL: / NEXT:
```

75
fktry/lanes/LANE-SIM.md Normal file
View File

@ -0,0 +1,75 @@
# LANE-SIM — deterministic factory simulation
You are an Opus 4.8 executor on the SIM lane of FKTRY. Read `../MASTERPLAN.md` and
`../CONTRACTS.md` before anything else; the world bible is `../../docs/FKTRY_LORE.md`.
**Mission:** the headless, deterministic heart of the game. Grid, entities, belts,
recipes, power, events. If the renderer were deleted, your sim should still be fully
playable through unit tests. You are the lane the other four trust with their lives.
**Owned paths:** `src/sim/**` (including your tests: `src/sim/*.test.ts`).
**Never touch:** contracts, main.ts, other lanes, data JSONs (request changes via NOTES).
**Standing rules (all rounds):**
- Deterministic: seedable RNG (write your own mulberry32-style helper), no wall-clock,
no unseeded Math.random. Same seed + same commands = identical snapshots.
- Every mechanic lands with tests. `npm test` green before you write NOTES.
- Performance target: 1,000 entities + 5,000 belt items at 30 tps without heroics.
Prefer flat arrays and index maps over object soup. Don't allocate in the tick loop
when avoidable.
- Snapshot may reuse buffers, but its SHAPE must always match `SimSnapshot`.
---
## CURRENT ORDERS — Round 1 (goal: milestone M1 "FLOW")
Replace the stub in `src/sim/index.ts` with a real implementation:
1. **World + placement.** Tile grid (start 64×64, world-centered). `place` command:
validate footprint fits and tiles are free, spawn `EntityState`, emit `placed`.
`remove` (emit `removed`, refund nothing yet), `rotate`, `setPaused`. Invalid
commands are silently dropped (UI will preview validity later).
2. **Belts.** `kind:'belt'` entities carry up to 2 items each as `BeltItem` (t: 0..1),
moving at `beltSpeed` tiles/sec. Items transfer to the belt/machine the belt points
into (dir). Items on a belt keep minimum spacing 0.45. Belt→machine: push into
`inputBuf` if the machine's current recipe wants that item and buffer < 2× recipe
need; otherwise the item waits (belt backs up — this is core Factorio feel, get it
right). Machine→belt: outputs pop onto an adjacent belt facing away, one item per
available slot.
3. **Crafting.** Machines with recipes: when `inputBuf` covers `inputs`, consume,
run `ticks` (progress 0..1), emit `crafted`, deposit `outputs` into `outputBuf`.
If `outputBuf` holds ≥ 4× outputs, machine stalls with `jammed:"output full"`
(emit `jammed` once per stall, not per tick). Extractors need no inputs. Machines
with multiple recipes use `setRecipe`; default to `recipes[0]`.
4. **Bandwidth (power) v1.** Sum powerGen (power kind) vs powerDraw of all entities +
active recipe `bandwidth` (note: negative recipe bandwidth GENERATES — quantizer
compression pays power, per lore). If draw > gen: global speed multiplier =
gen/draw for all progress and belt movement (brownout), emit `brownout` on/off
edges, set `snapshot.bandwidth` accordingly. No storage/buffer tanks yet.
5. **Shipping.** `kind:'shipper'` consumes ANY item arriving in its input, increments
`shippedTotal`, emits `shipped`. Track the single active commission (first in
data): fill `commissionProgress`, emit `commissionDone` once satisfied.
6. **Tests** (minimum): placement collision; belt line moves an item end to end in
the expected tick count; belt back-pressure (full line stops cleanly, no item
loss/dupe); demux recipe consumes 2 ore → 1 luma + 1 slurry; brownout halves
speed at draw=2×gen; determinism (two sims, same seed+commands, 500 ticks,
deep-equal snapshots); shipping increments and fires commissionDone.
**Definition of done:** with the dev server running, the orchestrator can (via UI or
console `__fktryBus.dispatch(...)`) place extractor→belts→demuxer→quantizer→
mosh-reactor→shipper and watch `shippedTotal.melt` climb. All tests green. NOTES
written (decisions, gotchas, what you'd tackle in round 2).
---
## NOTES (append-only log, newest at bottom — template below)
```
### Round N — YYYY-MM-DD — [your model]
SHIPPED: what actually works now (verified how)
DECISIONS: choices you made inside your mandate
BLOCKED/BROKEN: anything not working, honestly
CONTRACT REQUEST: (if any)
PROPOSAL: (ideas for the orchestrator, optional)
NEXT: what you'd do next round if asked
```

65
fktry/lanes/LANE-UI.md Normal file
View File

@ -0,0 +1,65 @@
# LANE-UI — HUD, build tools, and the voice of the game
You are an Opus 4.8 executor on the UI lane of FKTRY. Read `../MASTERPLAN.md` and
`../CONTRACTS.md` first; the world bible is `../../docs/FKTRY_LORE.md`. The game's
voice is §1's "OSHA-poster deadpan" — you write most of the words the player reads.
Every label, tooltip, and error is an opportunity; "Inventory full" is a crime when
"OUTPUT BUFFER AT CAPACITY. THIS IS A YOU PROBLEM." is available.
**Mission:** everything DOM. Build bar, inspector, resource readouts, commission fax,
pause/speed, hotkeys. Plain TypeScript + DOM — no framework, no CSS libs.
**Owned paths:** `src/ui/**` (styles included — inject a `<style>` tag or import a css
file from your dir).
**Never touch:** contracts, main.ts, other lanes. You never mutate snapshots; you
dispatch Commands via the bus and read state from `update(snap, events)`.
**Standing rules (all rounds):**
- `#ui` root has `pointer-events:none`; every interactive child you create re-enables
it. Clicks must fall through to the game world everywhere else — test this.
- Keyboard map lives in one file (`hotkeys.ts`) — it will grow.
- Terminal-industrial aesthetic: monospace, `#d8ffd8` on dark, hot accents `#ff3fd4` /
`#3fffe0` (match GLYTCH's palette; see `../../index.html` for reference). No rounded
corners. This is machinery, not a startup.
---
## CURRENT ORDERS — Round 1 (goal: milestone M1 "FLOW")
Replace the stub in `src/ui/index.ts`:
1. **Build bar** (bottom center): one button per machine in `GameData.machines`
(icon = colored square from placeholder color + name), hotkeys 1-9. Click/hotkey
selects for placement → sets `bus._sel` (see main.ts) `{def, dir}`; `R` rotates the
pending dir; `Esc`/right-click clears selection. Selected button shows hot state.
2. **Top strip**: bandwidth meter (gen vs draw bar that goes amber near 100%, red +
"BROWNOUT" flash when over), shipped-total ticker, tick/paused indicator.
`Space` toggles pause (dispatch `setPaused`).
3. **Inspector** (click an entity with nothing selected to build): panel showing
machine name, codex flavor line (hard-code a lookup of 1-liners per machine for
now — request codex excerpts in data via NOTES if you want them properly), recipe
with input/output item chips + live buffer counts, progress bar, jam reason in
amber. Close with Esc. (Selecting an entity: you'll need the hovered tile — reuse
`renderer.pickTile` via a `pointerdown` listener on `#ui`'s capture phase or
request a cleaner hook via CONTRACT REQUEST if this fights main.ts's handler; a
pragmatic v1 is fine.)
4. **Commission fax** (top right, under THE SCREEN): the active commission's flavor
text on a fax-paper styled card, `wants` as item chips with progress `3/40`,
satisfying it plays an aggressively mundane "FAX SENT" stamp animation on
`commissionDone`.
5. **Event toasts** (bottom right): `jammed`, `brownout`, `commissionDone` become
small toast lines in the house voice, max 4 on screen, 6s TTL.
**Definition of done:** full M1 chain placeable entirely with mouse + hotkeys, no
console needed; brownout is legible from the top strip alone; fax completes when melt
ships. `npm run check` clean, no console errors, pointer fall-through verified.
NOTES written.
---
## NOTES (append-only log, newest at bottom)
```
### Round N — YYYY-MM-DD — [your model]
SHIPPED: / DECISIONS: / BLOCKED/BROKEN: / CONTRACT REQUEST: / PROPOSAL: / NEXT:
```

2640
fktry/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
fktry/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "fktry",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite --port 8150",
"build": "vite build",
"check": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"three": "^0.170.0"
},
"devDependencies": {
"@types/three": "^0.170.0",
"typescript": "^5.6.0",
"vite": "^6.0.0",
"vitest": "^2.1.0"
},
"allowScripts": {
"esbuild@0.25.12": true,
"esbuild@0.21.5": true
}
}

158
fktry/src/contracts.ts Normal file
View File

@ -0,0 +1,158 @@
/**
* FKTRY shared contracts ORCHESTRATOR-OWNED.
* Every lane imports from this file. NO lane edits it. If a contract blocks you,
* write the problem in your lane doc's NOTES and stub around it locally.
*/
export const TICKS_PER_SECOND = 30;
export const TILE = 1; // world units per grid tile
export type Dir = 0 | 1 | 2 | 3; // N E S W (clockwise)
export interface Vec2 { x: number; y: number }
// ---------------------------------------------------------------- data schemas
// These mirror data/*.json (LANE-DATA owns the JSON, orchestrator owns the shape).
export interface ItemDef {
id: string; // kebab-case, matches codex name, e.g. "chroma-slurry"
name: string; // display, e.g. "CHROMA SLURRY"
codex: string; // section ref into docs/FKTRY_LORE.md, e.g. "2:chroma-slurry"
tier: number; // 0 raw, 1 refined, 2 product, 3 endgame
color: string; // hex, used by renderer placeholders + UI chips
}
export interface RecipeDef {
id: string;
machine: string; // MachineDef.id that can run it
inputs: Record<string, number>; // itemId -> count
outputs: Record<string, number>;
ticks: number; // duration at speed 1
bandwidth: number; // power draw while crafting (negative = generates)
heat?: number; // heat per craft (software decoders etc.)
}
export type MachineKind =
| 'extractor' // pulls items from seam tiles, no inputs
| 'crafter' // recipe in -> out
| 'belt' // moves items
| 'splitter' // 1 in, N out round-robin
| 'power' // generates bandwidth
| 'buffer' // stores bandwidth smoothness (brownout protection)
| 'shipper'; // consumes items => 'shipped' events => THE SCREEN
export interface MachineDef {
id: string;
name: string;
codex: string;
kind: MachineKind;
footprint: Vec2; // tiles occupied (w,h) before rotation
recipes: string[]; // RecipeDef.ids this machine may run ([] for belts etc.)
powerDraw: number; // idle bandwidth draw
powerGen?: number;
beltSpeed?: number; // tiles/sec, kind==='belt'
asset: string; // codex asset key for renderer mesh registry, e.g. "quantizer"
}
export interface TechDef {
id: string;
era: 'reel' | 'broadcast' | 'disc' | 'stream' | 'fortress';
cost: Record<string, number>; // science-pack itemId -> count
unlocks: string[]; // machine/recipe ids
}
export interface CommissionDef {
id: string;
flavor: string; // the fax text — deadpan absurd
wants: Record<string, number>;
rewardBandwidth: number;
}
export interface GameData {
items: ItemDef[];
machines: MachineDef[];
recipes: RecipeDef[];
tech: TechDef[];
commissions: CommissionDef[];
}
// ---------------------------------------------------------------- live sim state
export interface EntityState {
id: number;
def: string; // MachineDef.id
pos: Vec2; // tile coords of origin corner
dir: Dir;
recipe: string | null;
progress: number; // 0..1 of current craft
inputBuf: Record<string, number>;
outputBuf: Record<string, number>;
jammed: string | null; // human-readable reason or null
heat: number; // 0..1, throttles >0.7, scrams at 1
}
export interface BeltItem {
item: string;
entity: number; // belt entity id
t: number; // 0..1 progress along the belt tile
}
export interface SimSnapshot {
tick: number;
paused: boolean;
entities: ReadonlyArray<EntityState>;
beltItems: ReadonlyArray<BeltItem>;
bandwidth: { gen: number; draw: number; stored: number; brownout: boolean };
shippedTotal: Record<string, number>;
activeCommission: string | null;
commissionProgress: Record<string, number>;
}
export type Command =
| { kind: 'place'; def: string; pos: Vec2; dir: Dir }
| { kind: 'remove'; pos: Vec2 }
| { kind: 'rotate'; pos: Vec2 }
| { kind: 'setRecipe'; entity: number; recipe: string | null }
| { kind: 'setPaused'; paused: boolean };
export type SimEvent =
| { kind: 'shipped'; item: string; count: number; tick: number }
| { kind: 'crafted'; entity: number; recipe: string; tick: number }
| { kind: 'jammed'; entity: number; reason: string; tick: number }
| { kind: 'brownout'; on: boolean; tick: number }
| { kind: 'placed'; entity: number; def: string; tick: number }
| { kind: 'removed'; entity: number; def: string; tick: number }
| { kind: 'commissionDone'; commission: string; tick: number };
// ---------------------------------------------------------------- module interfaces
export interface Sim {
init(data: GameData, seed: number): void;
enqueue(cmd: Command): void;
tick(): void; // advance exactly one tick
snapshot(): SimSnapshot; // read-only view; renderer/ui must not mutate
drainEvents(): SimEvent[]; // events since last drain (main loop fans out)
}
export interface Renderer {
init(container: HTMLElement, data: GameData): Promise<void>;
render(snap: SimSnapshot, alpha: number): void; // alpha: 0..1 interp between ticks
pickTile(clientX: number, clientY: number): Vec2 | null;
setGhost(def: string | null, pos: Vec2 | null, dir: Dir): void;
}
export interface UIBus {
dispatch(cmd: Command): void;
/** current build selection; renderer ghost + click-to-place read this */
selectedBuild(): { def: string; dir: Dir } | null;
}
export interface UI {
init(root: HTMLElement, data: GameData, bus: UIBus): void;
update(snap: SimSnapshot, events: SimEvent[]): void;
}
export interface ScreenFX {
init(canvas: HTMLCanvasElement, data: GameData): void;
onEvents(events: SimEvent[]): void; // shipped items feed the corruption
frame(timeMs: number): void;
}

65
fktry/src/main.ts Normal file
View File

@ -0,0 +1,65 @@
/**
* FKTRY main loop ORCHESTRATOR-OWNED wiring. Lanes do not edit.
* Fixed-timestep sim (30 tps) + interpolated render + event fan-out.
*/
import { TICKS_PER_SECOND, type Command, type Dir, type GameData, type UIBus } from './contracts';
import { createSim } from './sim';
import { createRenderer } from './render';
import { createUI } from './ui';
import { createScreenFX } from './screen';
import items from '../data/items.json';
import machines from '../data/machines.json';
import recipes from '../data/recipes.json';
import tech from '../data/tech.json';
import commissions from '../data/commissions.json';
const data = { items, machines, recipes, tech, commissions } as unknown as GameData;
const sim = createSim();
const renderer = createRenderer();
const ui = createUI();
const screen = createScreenFX();
const bus: UIBus & { _sel: { def: string; dir: Dir } | null } = {
_sel: null,
dispatch: (cmd: Command) => sim.enqueue(cmd),
selectedBuild: () => bus._sel,
};
// LANE-UI sets the build selection through this hook:
(window as any).__fktryBus = bus;
async function boot() {
sim.init(data, 1337);
await renderer.init(document.getElementById('game')!, data);
ui.init(document.getElementById('ui')!, data, bus);
screen.init(document.getElementById('screen') as HTMLCanvasElement, data);
// click-to-place / hover ghost glue
const game = document.getElementById('game')!;
game.addEventListener('pointermove', (e) => {
const sel = bus.selectedBuild();
renderer.setGhost(sel?.def ?? null, renderer.pickTile(e.clientX, e.clientY), sel?.dir ?? 0);
});
game.addEventListener('pointerdown', (e) => {
const sel = bus.selectedBuild();
const tile = renderer.pickTile(e.clientX, e.clientY);
if (sel && tile) sim.enqueue({ kind: 'place', def: sel.def, pos: tile, dir: sel.dir });
});
const MS_PER_TICK = 1000 / TICKS_PER_SECOND;
let acc = 0, last = performance.now();
function loop(now: number) {
acc += Math.min(250, now - last); last = now;
while (acc >= MS_PER_TICK) { sim.tick(); acc -= MS_PER_TICK; }
const snap = sim.snapshot();
const events = sim.drainEvents();
renderer.render(snap, acc / MS_PER_TICK);
ui.update(snap, events);
screen.onEvents(events);
screen.frame(now);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
boot();

34
fktry/src/render/index.ts Normal file
View File

@ -0,0 +1,34 @@
/** LANE-RENDER territory. Stub — replace per lanes/LANE-RENDER.md. */
import * as THREE from 'three';
import type { Dir, GameData, Renderer, SimSnapshot, Vec2 } from '../contracts';
export function createRenderer(): Renderer {
let renderer: THREE.WebGLRenderer;
let scene: THREE.Scene;
let camera: THREE.OrthographicCamera;
return {
async init(container: HTMLElement, _data: GameData) {
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0812);
const aspect = container.clientWidth / container.clientHeight;
const d = 12;
camera = new THREE.OrthographicCamera(-d * aspect, d * aspect, d, -d, 0.1, 200);
camera.position.set(20, 24, 20);
camera.lookAt(0, 0, 0);
scene.add(new THREE.GridHelper(64, 64, 0x2a2440, 0x181425));
scene.add(new THREE.AmbientLight(0xffffff, 0.8));
addEventListener('resize', () => {
renderer.setSize(container.clientWidth, container.clientHeight);
});
},
render(_snap: SimSnapshot, _alpha: number) {
if (renderer) renderer.render(scene, camera);
},
pickTile(_x: number, _y: number): Vec2 | null { return null; },
setGhost(_def: string | null, _pos: Vec2 | null, _dir: Dir) {},
};
}

21
fktry/src/screen/index.ts Normal file
View File

@ -0,0 +1,21 @@
/** LANE-SCREEN territory. Stub — replace per lanes/LANE-SCREEN.md. */
import type { GameData, ScreenFX, SimEvent } from '../contracts';
export function createScreenFX(): ScreenFX {
let ctx: CanvasRenderingContext2D | null = null;
return {
init(canvas: HTMLCanvasElement, _data: GameData) {
ctx = canvas.getContext('2d');
},
onEvents(_events: SimEvent[]) {},
frame(timeMs: number) {
if (!ctx) return;
// placeholder: sterile clean playback awaiting corruption
ctx.fillStyle = '#101018';
ctx.fillRect(0, 0, 640, 360);
ctx.fillStyle = '#2a2440';
ctx.font = '20px monospace';
ctx.fillText('THE SCREEN · 0 corruption', 140, 180 + Math.sin(timeMs / 800) * 4);
},
};
}

19
fktry/src/sim/index.ts Normal file
View File

@ -0,0 +1,19 @@
/** LANE-SIM territory. Stub — replace per lanes/LANE-SIM.md. */
import type { Command, GameData, Sim, SimEvent, SimSnapshot } from '../contracts';
export function createSim(): Sim {
let tick = 0;
const snap: SimSnapshot = {
tick: 0, paused: false, entities: [], beltItems: [],
bandwidth: { gen: 0, draw: 0, stored: 0, brownout: false },
shippedTotal: {}, activeCommission: null, commissionProgress: {},
};
const events: SimEvent[] = [];
return {
init(_data: GameData, _seed: number) {},
enqueue(_cmd: Command) {},
tick() { tick++; snap.tick = tick; },
snapshot() { return snap; },
drainEvents() { return events.splice(0); },
};
}

17
fktry/src/ui/index.ts Normal file
View File

@ -0,0 +1,17 @@
/** LANE-UI territory. Stub — replace per lanes/LANE-UI.md. */
import type { GameData, SimEvent, SimSnapshot, UI, UIBus } from '../contracts';
export function createUI(): UI {
let tickEl: HTMLElement;
return {
init(root: HTMLElement, _data: GameData, _bus: UIBus) {
tickEl = document.createElement('div');
tickEl.style.cssText =
'position:fixed;bottom:8px;left:8px;color:#4a6b4a;font-size:12px;pointer-events:none';
root.appendChild(tickEl);
},
update(snap: SimSnapshot, _events: SimEvent[]) {
tickEl.textContent = `TICK ${snap.tick}`;
},
};
}

15
fktry/tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUnusedLocals": false,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"lib": ["ES2022", "DOM"],
"types": ["vite/client"]
},
"include": ["src"]
}

329
index.html Normal file
View File

@ -0,0 +1,329 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WIMVEE GLYTCH</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root{
--bg:#050208; --fg:#d8ffd8; --dim:#4a6b4a; --hot:#ff3fd4; --cy:#3fffe0;
}
*{box-sizing:border-box; margin:0; padding:0}
body{
background:var(--bg); color:var(--fg);
font-family:"Courier New", ui-monospace, monospace;
display:flex; flex-direction:column; align-items:center;
min-height:100vh; padding:16px; gap:10px;
text-transform:uppercase; letter-spacing:.08em;
}
h1{font-size:18px; color:var(--hot); text-shadow:2px 0 var(--cy), -2px 0 #ff2222; }
#screens{display:flex; gap:14px; flex-wrap:wrap; justify-content:center}
.panel{display:flex; flex-direction:column; gap:4px; align-items:center}
.panel .label{font-size:11px; color:var(--dim)}
canvas.screen{
width:380px; height:285px; image-rendering:pixelated;
border:1px solid #222; background:#000;
}
#hud{display:flex; gap:24px; align-items:center; font-size:13px}
#sync{font-size:22px; min-width:140px; text-align:center}
#sync.hot{color:var(--cy)}
#params{list-style:none; font-size:12px; width:772px; max-width:95vw;
display:grid; grid-template-columns:repeat(4,1fr); gap:3px 14px}
#params li{display:flex; gap:6px; align-items:center; color:var(--dim); padding:2px 4px}
#params li.sel{color:var(--fg); background:#131020; outline:1px solid #2a2440}
#params li .bar{flex:1; height:8px; background:#181425; position:relative; overflow:hidden}
#params li .bar i{position:absolute; left:0; top:0; bottom:0; background:var(--hot)}
#params li.sel .bar i{background:var(--cy)}
#help{font-size:10px; color:var(--dim); max-width:772px; text-align:center; line-height:1.7}
#round{color:var(--hot)}
#msg{font-size:14px; color:var(--cy); height:18px}
</style>
</head>
<body>
<h1>WIMVEE GLYTCH</h1>
<div id="hud">
<span>ROUND <span id="round">1</span></span>
<span id="sync">SYNC 00.0%</span>
<span id="msg"></span>
</div>
<div id="screens">
<div class="panel"><div class="label">TARGET SIGNAL</div><canvas id="cvT" class="screen" width="512" height="384"></canvas></div>
<div class="panel"><div class="label">YOUR SIGNAL</div><canvas id="cvP" class="screen" width="512" height="384"></canvas></div>
</div>
<ul id="params"></ul>
<div id="help">
←/→ or L1/R1: select property · ↑/↓ or d-pad: adjust · left stick Y: fine adjust ·
hold shift / R2: slow · N or START: new round · match the target signal to ≥96% sync
</div>
<script>
"use strict";
// ---- glitch parameter definitions -------------------------------------
const PARAMS = [
{ key:"mosh", name:"MOSH" }, // macroblock displacement
{ key:"melt", name:"MELT" }, // vertical pixel drip
{ key:"chroma", name:"CHROMA" }, // RGB channel split
{ key:"tear", name:"TEAR" }, // scanline horizontal tears
{ key:"roll", name:"V-HOLD" }, // vertical hold roll
{ key:"burn", name:"BURN" }, // film burn blotches
{ key:"noise", name:"STATIC" }, // analog snow
{ key:"freeze", name:"FREEZE" }, // time quantize + dropout
];
const N = PARAMS.length;
let target = new Float32Array(N);
let player = new Float32Array(N);
let sel = 0, round = 1, winTimer = 0, locked = false;
// ---- base "broadcast" texture: SMPTE bars + camcorder overlay ---------
function makeBaseTexture(){
const c = document.createElement("canvas");
c.width = 512; c.height = 384;
const g = c.getContext("2d");
const bars = ["#c0c0c0","#c0c000","#00c0c0","#00c000","#c000c0","#c00000","#0000c0"];
const bw = c.width / bars.length;
bars.forEach((col,i)=>{ g.fillStyle=col; g.fillRect(i*bw,0,bw+1,c.height*0.62); });
// castellations strip
const casts = ["#0000c0","#131313","#c000c0","#131313","#00c0c0","#131313","#c0c0c0"];
casts.forEach((col,i)=>{ g.fillStyle=col; g.fillRect(i*bw,c.height*0.62,bw+1,c.height*0.10); });
// pluge / ramp bottom
g.fillStyle="#101010"; g.fillRect(0,c.height*0.72,c.width,c.height*0.28);
const ramp = g.createLinearGradient(0,0,c.width,0);
ramp.addColorStop(0,"#000"); ramp.addColorStop(1,"#fff");
g.fillStyle=ramp; g.fillRect(c.width*0.55,c.height*0.80,c.width*0.4,20);
// camcorder overlay text
g.fillStyle="#eee"; g.font="bold 26px 'Courier New', monospace";
g.fillText("PLAY ▶", 24, c.height*0.86);
g.fillText("SP 0:12:34", 24, c.height*0.945);
g.font="bold 20px 'Courier New', monospace";
g.fillStyle="#f4f4f4";
g.fillText("WIMVEE GLYTCH", c.width*0.55, c.height*0.945);
g.fillStyle="#ff4444"; g.beginPath();
g.arc(c.width-30, c.height*0.83, 8, 0, Math.PI*2); g.fill();
return c;
}
// ---- webgl -------------------------------------------------------------
const VS = `
attribute vec2 p;
varying vec2 vUv;
void main(){ vUv = p*0.5+0.5; vUv.y = 1.0-vUv.y; gl_Position = vec4(p,0.,1.); }`;
const FS = `
precision mediump float;
varying vec2 vUv;
uniform sampler2D tex;
uniform float t;
uniform float u_mosh, u_melt, u_chroma, u_tear, u_roll, u_burn, u_noise, u_freeze;
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }
float vnoise(vec2 p){
vec2 i=floor(p), f=fract(p); f=f*f*(3.-2.*f);
return mix(mix(hash(i),hash(i+vec2(1,0)),f.x),
mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);
}
float fbm(vec2 p){ return 0.5*vnoise(p)+0.25*vnoise(p*2.3)+0.125*vnoise(p*5.1); }
void main(){
// freeze quantizes the timebase everything else animates on
float tt = mix(t, floor(t*2.5)/2.5, clamp(u_freeze*1.4,0.,1.));
vec2 uv = vUv;
// v-hold roll
uv.y = fract(uv.y + u_roll * tt * 0.35);
// scanline tears: bands shove sideways
float band = floor(uv.y * 36.0);
float tr = hash(vec2(band, floor(tt*7.0)));
if(tr > 1.0 - u_tear*0.85){
uv.x += (hash(vec2(band, floor(tt*7.0)+9.0)) - 0.5) * u_tear * 0.5;
}
// datamosh: macroblocks pick up stale offsets
vec2 blk = floor(uv * vec2(16.0, 12.0));
float mb = hash(blk + floor(tt*3.0));
if(mb > 1.0 - u_mosh*0.9){
vec2 off = vec2(hash(blk+1.7), hash(blk+4.2)) - 0.5;
uv += off * u_mosh * 0.35;
}
// melt: columns drip downward
float drip = pow(vnoise(vec2(uv.x*30.0, floor(tt*1.5))), 3.0);
uv.y -= u_melt * drip * uv.y * 0.9;
uv = fract(uv);
// chroma split
float ca = u_chroma * 0.03;
vec3 col;
col.r = texture2D(tex, uv + vec2( ca, 0.)).r;
col.g = texture2D(tex, uv).g;
col.b = texture2D(tex, uv - vec2( ca, ca*0.6)).b;
// static
float sn = hash(uv*vec2(431.0,917.0) + fract(tt)*13.0);
col = mix(col, vec3(sn), u_noise * 0.7);
// freeze dropout: grey flash lines
float dropo = step(0.985 - u_freeze*0.12, hash(vec2(floor(uv.y*90.0), floor(tt*5.0))));
col = mix(col, vec3(0.45), dropo * u_freeze);
// film burn: fbm blotch eats the frame, hot rim then black
float b = fbm(uv*3.0 + vec2(0., -tt*0.4));
float edge = smoothstep(1.0 - u_burn*1.1, 1.0 - u_burn*1.1 + 0.12, b);
vec3 rim = vec3(1.0, 0.55, 0.15);
col = mix(col, rim, edge * (1.0 - smoothstep(0.05, 0.35, edge)) * 2.0 * u_burn);
col = mix(col, vec3(0.02), smoothstep(0.3, 1.0, edge) * step(0.01, u_burn));
// faint scanlines for vibe (constant, not scored)
col *= 0.92 + 0.08 * sin(vUv.y * 384.0 * 3.1415);
gl_FragColor = vec4(col, 1.0);
}`;
function makeGL(canvas){
const gl = canvas.getContext("webgl", {antialias:false});
const compile = (type, src)=>{
const s = gl.createShader(type);
gl.shaderSource(s, src); gl.compileShader(s);
if(!gl.getShaderParameter(s, gl.COMPILE_STATUS))
throw new Error(gl.getShaderInfoLog(s));
return s;
};
const prog = gl.createProgram();
gl.attachShader(prog, compile(gl.VERTEX_SHADER, VS));
gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FS));
gl.linkProgram(prog);
if(!gl.getProgramParameter(prog, gl.LINK_STATUS))
throw new Error(gl.getProgramInfoLog(prog));
gl.useProgram(prog);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(prog, "p");
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, makeBaseTexture());
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
const uni = { t: gl.getUniformLocation(prog, "t") };
for(const p of PARAMS) uni[p.key] = gl.getUniformLocation(prog, "u_"+p.key);
return { gl, uni };
}
const T = makeGL(document.getElementById("cvT"));
const P = makeGL(document.getElementById("cvP"));
function draw(ctx, vals, time){
const { gl, uni } = ctx;
gl.uniform1f(uni.t, time);
PARAMS.forEach((p,i)=> gl.uniform1f(uni[p.key], vals[i]));
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
// ---- rounds & scoring ---------------------------------------------------
function newRound(){
target.fill(0); player.fill(0);
const active = Math.min(2 + Math.floor((round-1)/1.5), N);
const idx = [...Array(N).keys()].sort(()=>Math.random()-0.5).slice(0, active);
for(const i of idx) target[i] = 0.15 + Math.random()*0.8;
winTimer = 0; locked = false;
document.getElementById("round").textContent = round;
document.getElementById("msg").textContent = "";
}
function syncScore(){
let d = 0;
for(let i=0;i<N;i++) d += Math.abs(target[i]-player[i]);
return Math.max(0, 1 - d/ (N*0.45)) * 100;
}
// ---- ui -----------------------------------------------------------------
const list = document.getElementById("params");
PARAMS.forEach((p,i)=>{
const li = document.createElement("li");
li.innerHTML = `<span style="width:64px">${p.name}</span><span class="bar"><i></i></span>`;
list.appendChild(li);
});
function renderUI(score){
[...list.children].forEach((li,i)=>{
li.classList.toggle("sel", i===sel);
li.querySelector("i").style.width = (player[i]*100).toFixed(1)+"%";
});
const el = document.getElementById("sync");
el.textContent = "SYNC " + score.toFixed(1).padStart(4,"0") + "%";
el.classList.toggle("hot", score >= 96);
}
// ---- input ----------------------------------------------------------------
const keys = {};
addEventListener("keydown", e=>{
keys[e.key] = true;
const step = e.shiftKey ? 0.01 : 0.05;
if(e.key==="ArrowLeft"){ sel = (sel+N-1)%N; e.preventDefault(); }
if(e.key==="ArrowRight"){ sel = (sel+1)%N; e.preventDefault(); }
if(e.key==="ArrowUp" && !locked){ player[sel] = Math.min(1, player[sel] + step); e.preventDefault(); }
if(e.key==="ArrowDown" && !locked){ player[sel] = Math.max(0, player[sel] - step); e.preventDefault(); }
if(e.key==="n"||e.key==="N"){ round=1; newRound(); }
});
addEventListener("keyup", e=> keys[e.key]=false);
let prevPad = {};
function pollInput(dt){
if(locked) return;
const pad = navigator.getGamepads && navigator.getGamepads()[0];
if(pad){
const pressed = i => pad.buttons[i] && pad.buttons[i].pressed;
const was = i => prevPad[i];
if(pressed(4) && !was(4)) sel = (sel+N-1)%N; // L1
if(pressed(5) && !was(5)) sel = (sel+1)%N; // R1
if(pressed(9) && !was(9)){ round=1; newRound(); } // start
const slow = pad.buttons[7] && pad.buttons[7].value > 0.3 ? 0.25 : 1.0; // R2
if(pressed(12)) player[sel] = Math.min(1, player[sel] + 0.6*dt*slow); // dpad up
if(pressed(13)) player[sel] = Math.max(0, player[sel] - 0.6*dt*slow); // dpad down
if(pressed(14) && !was(14)) sel = (sel+N-1)%N;
if(pressed(15) && !was(15)) sel = (sel+1)%N;
const ly = pad.axes[1] || 0;
if(Math.abs(ly) > 0.15) player[sel] = Math.min(1, Math.max(0, player[sel] - ly*0.4*dt));
prevPad = {}; pad.buttons.forEach((b,i)=> prevPad[i]=b.pressed);
}
}
// ---- main loop --------------------------------------------------------------
let last = performance.now();
function frame(now){
const dt = Math.min(0.05, (now-last)/1000); last = now;
const t = now/1000;
pollInput(dt);
const score = syncScore();
if(!locked){
if(score >= 96){
winTimer += dt;
document.getElementById("msg").textContent = "LOCKING " + (2-winTimer).toFixed(1);
if(winTimer >= 2){
locked = true;
document.getElementById("msg").textContent = "SIGNAL LOCKED";
setTimeout(()=>{ round++; newRound(); }, 1200);
}
} else { winTimer = 0; if(!locked) document.getElementById("msg").textContent = ""; }
}
draw(T, target, t);
draw(P, player, t);
renderUI(score);
requestAnimationFrame(frame);
}
newRound();
requestAnimationFrame(frame);
</script>
</body>
</html>