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:
commit
1a769e6cbd
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
413
docs/FKTRY_LORE.md
Normal file
413
docs/FKTRY_LORE.md
Normal 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 3–4 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
57
fktry/CONTRACTS.md
Normal 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
74
fktry/MASTERPLAN.md
Normal 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`.
|
||||
4
fktry/data/commissions.json
Normal file
4
fktry/data/commissions.json
Normal 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
11
fktry/data/items.json
Normal 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
16
fktry/data/machines.json
Normal 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
6
fktry/data/recipes.json
Normal 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
1
fktry/data/tech.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
22
fktry/index.html
Normal file
22
fktry/index.html
Normal 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
71
fktry/lanes/LANE-DATA.md
Normal 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:
|
||||
```
|
||||
67
fktry/lanes/LANE-RENDER.md
Normal file
67
fktry/lanes/LANE-RENDER.md
Normal 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:
|
||||
```
|
||||
65
fktry/lanes/LANE-SCREEN.md
Normal file
65
fktry/lanes/LANE-SCREEN.md
Normal 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
75
fktry/lanes/LANE-SIM.md
Normal 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
65
fktry/lanes/LANE-UI.md
Normal 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
2640
fktry/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
fktry/package.json
Normal file
25
fktry/package.json
Normal 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
158
fktry/src/contracts.ts
Normal 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
65
fktry/src/main.ts
Normal 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
34
fktry/src/render/index.ts
Normal 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
21
fktry/src/screen/index.ts
Normal 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
19
fktry/src/sim/index.ts
Normal 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
17
fktry/src/ui/index.ts
Normal 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
15
fktry/tsconfig.json
Normal 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
329
index.html
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user