/** * The SINGLE source of the kit catalogue. * * This table is read from two places: `build.ts` when it produces registry.json, * and the viewer when it builds its sliders and descriptions. The two used to be * written out by hand separately; at seventeen models that is no longer * sustainable, and they had already started to drift apart. * * The `controls` here are a CONTRACT: every key has to be a config field of the * model. The viewer checks this through `keyof Config`, so a renamed field is a * compile error. */ export interface ControlSpec { readonly type: 'number' readonly label: string readonly min: number readonly max: number readonly step: number readonly unit?: string } export interface ModelMeta { readonly title: string readonly description: string readonly category: string readonly tags: readonly string[] readonly controls: Readonly> readonly materialSlots: readonly string[] readonly parts: readonly string[] } export const MODEL_META: Readonly> = { 'wooden-chest': { title: 'Wooden Chest', description: 'Six-board medieval chest with iron bands. The lid is hinged and opens on an action.', category: 'Furniture', tags: ['medieval', 'lowpoly', 'furniture', 'interactive', 'procedural'], controls: { width: { type: 'number', label: 'Width', min: 0.4, max: 1.6, step: 0.02, unit: 'm' }, height: { type: 'number', label: 'Height', min: 0.28, max: 0.9, step: 0.02, unit: 'm' }, depth: { type: 'number', label: 'Depth', min: 0.24, max: 0.8, step: 0.02, unit: 'm' }, bandCount: { type: 'number', label: 'Band count', min: 0, max: 6, step: 1 }, openAngle: { type: 'number', label: 'Open angle', min: 40, max: 130, step: 2, unit: '°' }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'iron'], parts: ['body', 'lid', 'bands', 'lock'], }, 'wooden-barrel': { title: 'Wooden Barrel', description: 'Lowpoly barrel built from separate oak staves, with iron hoops and a recessed head.', category: 'Props', tags: ['medieval', 'lowpoly', 'props', 'procedural'], controls: { height: { type: 'number', label: 'Height', min: 0.4, max: 2, step: 0.02, unit: 'm' }, radius: { type: 'number', label: 'Radius', min: 0.15, max: 0.9, step: 0.01, unit: 'm' }, taper: { type: 'number', label: 'End taper', min: 0, max: 0.34, step: 0.01 }, staveCount: { type: 'number', label: 'Stave count', min: 6, max: 28, step: 1 }, hoopCount: { type: 'number', label: 'Hoop count', min: 0, max: 8, step: 1 }, rivets: { type: 'number', label: 'Rivets per hoop', min: 0, max: 8, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'iron'], parts: ['staves', 'heads', 'hoops'], }, 'wooden-crate': { title: 'Wooden Crate', description: 'Rows of horizontal boards nailed to corner posts, with forged iron straps.', category: 'Props', tags: ['medieval', 'lowpoly', 'props', 'procedural'], controls: { width: { type: 'number', label: 'Width', min: 0.3, max: 1.4, step: 0.02, unit: 'm' }, height: { type: 'number', label: 'Height', min: 0.25, max: 1.2, step: 0.02, unit: 'm' }, depth: { type: 'number', label: 'Depth', min: 0.3, max: 1.4, step: 0.02, unit: 'm' }, plankRows: { type: 'number', label: 'Board rows', min: 1, max: 6, step: 1 }, strapCount: { type: 'number', label: 'Iron straps', min: 0, max: 4, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'iron'], parts: ['posts', 'planks', 'straps'], }, 'wooden-bucket': { title: 'Wooden Bucket', description: 'Tapering oak staves, an iron hoop and a forged handle — a small barrel.', category: 'Props', tags: ['medieval', 'lowpoly', 'props', 'farm', 'procedural'], controls: { height: { type: 'number', label: 'Height', min: 0.15, max: 0.6, step: 0.01, unit: 'm' }, radius: { type: 'number', label: 'Rim radius', min: 0.07, max: 0.3, step: 0.005, unit: 'm', }, taper: { type: 'number', label: 'Base taper', min: 0, max: 0.45, step: 0.01 }, staveCount: { type: 'number', label: 'Stave count', min: 6, max: 20, step: 1 }, hoopCount: { type: 'number', label: 'Hoop count', min: 0, max: 4, step: 1 }, handle: { type: 'number', label: 'Handle', min: 0, max: 1, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'iron'], parts: ['staves', 'base', 'hoops', 'handle'], }, 'trestle-table': { title: 'Trestle Table', description: 'Trestle table: the top is not nailed to the legs, it rests on them — so the hall can be cleared.', category: 'Furniture', tags: ['medieval', 'lowpoly', 'furniture', 'procedural'], controls: { length: { type: 'number', label: 'Length', min: 1, max: 3.2, step: 0.05, unit: 'm' }, width: { type: 'number', label: 'Width', min: 0.5, max: 1.2, step: 0.02, unit: 'm' }, height: { type: 'number', label: 'Height', min: 0.5, max: 1, step: 0.01, unit: 'm' }, plankCount: { type: 'number', label: 'Top boards', min: 2, max: 7, step: 1 }, splay: { type: 'number', label: 'Leg splay', min: 0, max: 0.45, step: 0.01 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak'], parts: ['top', 'trestles', 'stretcher'], }, 'wooden-bench': { title: 'Wooden Bench', description: 'Medieval bench with splayed legs, its tenons showing through the top of the seat.', category: 'Furniture', tags: ['medieval', 'lowpoly', 'furniture', 'procedural'], controls: { length: { type: 'number', label: 'Length', min: 0.6, max: 3, step: 0.02, unit: 'm' }, height: { type: 'number', label: 'Height', min: 0.28, max: 0.7, step: 0.01, unit: 'm' }, width: { type: 'number', label: 'Width', min: 0.18, max: 0.5, step: 0.01, unit: 'm' }, splay: { type: 'number', label: 'Leg splay', min: 0, max: 0.6, step: 0.02 }, inset: { type: 'number', label: 'Leg inset', min: 0.02, max: 0.3, step: 0.01 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak'], parts: ['seat', 'legs'], }, 'wooden-stool': { title: 'Wooden Stool', description: 'Three-legged village stool — three is the count that does not rock on uneven ground.', category: 'Furniture', tags: ['medieval', 'lowpoly', 'furniture', 'procedural'], controls: { height: { type: 'number', label: 'Height', min: 0.25, max: 0.9, step: 0.01, unit: 'm' }, seatRadius: { type: 'number', label: 'Seat radius', min: 0.1, max: 0.3, step: 0.005, unit: 'm', }, legCount: { type: 'number', label: 'Legs', min: 3, max: 5, step: 1 }, splay: { type: 'number', label: 'Leg splay', min: 0, max: 0.45, step: 0.01 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak'], parts: ['seat', 'legs'], }, 'pitch-torch': { title: 'Pitch Torch', description: 'Torch wrapped in pitch-soaked cloth. The flame flickers in update(), goes out via actions.setLit.', category: 'Lighting', tags: ['medieval', 'lowpoly', 'lighting', 'animated', 'interactive'], controls: { length: { type: 'number', label: 'Shaft length', min: 0.3, max: 1.4, step: 0.02, unit: 'm' }, radius: { type: 'number', label: 'Shaft radius', min: 0.008, max: 0.05, step: 0.001, unit: 'm', }, wrapLength: { type: 'number', label: 'Wrap ratio', min: 0.12, max: 0.5, step: 0.01 }, flameHeight: { type: 'number', label: 'Flame height', min: 0.4, max: 3, step: 0.05 }, flicker: { type: 'number', label: 'Flicker', min: 0, max: 2.5, step: 0.05 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'char', 'ember'], parts: ['shaft', 'wrap', 'flame'], }, 'iron-anvil': { title: 'Iron Anvil', description: 'Wide base, narrow waist, tapering horn; on top a steel face plate polished by use.', category: 'Smithy', tags: ['medieval', 'lowpoly', 'smithy', 'procedural'], controls: { height: { type: 'number', label: 'Height', min: 0.2, max: 0.6, step: 0.01, unit: 'm' }, faceLength: { type: 'number', label: 'Face length', min: 0.25, max: 0.8, step: 0.01, unit: 'm', }, faceWidth: { type: 'number', label: 'Face width', min: 0.07, max: 0.25, step: 0.005, unit: 'm', }, hornReach: { type: 'number', label: 'Horn reach', min: 0.2, max: 0.8, step: 0.02 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['iron', 'steel', 'oak'], parts: ['base', 'waist', 'body', 'face', 'horn', 'stump'], }, 'cart-wheel': { title: 'Cart Wheel', description: 'Four layers from the outside in: iron tyre, wooden felloe, spokes, hub.', category: 'Structure', tags: ['medieval', 'lowpoly', 'cart', 'structure', 'procedural'], controls: { radius: { type: 'number', label: 'Radius', min: 0.25, max: 0.9, step: 0.01, unit: 'm' }, spokeCount: { type: 'number', label: 'Spokes', min: 6, max: 16, step: 1 }, width: { type: 'number', label: 'Thickness', min: 0.04, max: 0.18, step: 0.005, unit: 'm' }, hubLength: { type: 'number', label: 'Hub length', min: 1.2, max: 3.2, step: 0.1 }, tyre: { type: 'number', label: 'Tyre thickness', min: 0.02, max: 0.09, step: 0.005 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'iron'], parts: ['hub', 'spokes', 'felloe', 'tyre'], }, 'log-pile': { title: 'Log Pile', description: 'Pile of cut firewood. The end grain is far lighter than the bark; it is what you see first.', category: 'Props', tags: ['medieval', 'lowpoly', 'props', 'procedural'], controls: { rows: { type: 'number', label: 'Rows', min: 1, max: 6, step: 1 }, perRow: { type: 'number', label: 'Logs per row', min: 1, max: 9, step: 1 }, logLength: { type: 'number', label: 'Log length', min: 0.3, max: 1.4, step: 0.02, unit: 'm' }, logRadius: { type: 'number', label: 'Log radius', min: 0.03, max: 0.14, step: 0.002, unit: 'm', }, variation: { type: 'number', label: 'Thickness variation', min: 0, max: 0.5, step: 0.02 }, taperRows: { type: 'number', label: 'Pyramid stack', min: 0, max: 1, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak'], parts: ['bark', 'ends'], }, 'hay-bale': { title: 'Hay Bale', description: 'Hand-tied straw bale — pinched where the ropes bite, with wisps sticking out on every side.', category: 'Props', tags: ['medieval', 'lowpoly', 'farm', 'props', 'procedural'], controls: { length: { type: 'number', label: 'Length', min: 0.4, max: 1.6, step: 0.02, unit: 'm' }, height: { type: 'number', label: 'Height', min: 0.2, max: 0.9, step: 0.02, unit: 'm' }, depth: { type: 'number', label: 'Depth', min: 0.2, max: 0.9, step: 0.02, unit: 'm' }, ropeCount: { type: 'number', label: 'Rope count', min: 0, max: 4, step: 1 }, wisps: { type: 'number', label: 'Stray wisps', min: 0, max: 60, step: 2 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['straw', 'cloth'], parts: ['bale', 'wisps', 'ropes'], }, 'linen-sack': { title: 'Linen Sack', description: 'Grain sack tied shut at the mouth with cord. The fill ratio changes the whole silhouette.', category: 'Props', tags: ['medieval', 'lowpoly', 'props', 'farm', 'procedural'], controls: { height: { type: 'number', label: 'Height', min: 0.2, max: 1, step: 0.01, unit: 'm' }, radius: { type: 'number', label: 'Radius', min: 0.07, max: 0.35, step: 0.005, unit: 'm' }, fill: { type: 'number', label: 'Fill', min: 0.15, max: 1, step: 0.02 }, collar: { type: 'number', label: 'Collar allowance', min: 0.05, max: 0.3, step: 0.01 }, ears: { type: 'number', label: 'Bottom ears', min: 0, max: 6, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['cloth'], parts: ['body', 'collar', 'cord'], }, 'straw-broom': { title: 'Straw Broom', description: 'Besom: hazel shaft, a brush bundle built from three concentric rings, willow bindings.', category: 'Tools', tags: ['medieval', 'lowpoly', 'tools', 'household', 'procedural'], controls: { length: { type: 'number', label: 'Length', min: 0.7, max: 1.8, step: 0.02, unit: 'm' }, shaftRadius: { type: 'number', label: 'Shaft thickness', min: 0.01, max: 0.03, step: 0.001, unit: 'm', }, headLength: { type: 'number', label: 'Bundle length', min: 0.2, max: 0.55, step: 0.01 }, tieRadius: { type: 'number', label: 'Binding radius', min: 0.03, max: 0.09, step: 0.002, unit: 'm' }, tipRadius: { type: 'number', label: 'Tip radius', min: 0.04, max: 0.26, step: 0.005, unit: 'm' }, bristles: { type: 'number', label: 'Bristle count', min: 8, max: 60, step: 2 }, bindings: { type: 'number', label: 'Binding count', min: 0, max: 5, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'straw', 'cloth'], parts: ['shaft', 'bristles', 'bindings'], }, 'oak-tankard': { title: 'Oak Tankard', description: 'Oak tankard: the barrel at palm scale, in the same stave-and-hoop language.', category: 'Props', tags: ['medieval', 'lowpoly', 'props', 'tavern', 'procedural'], controls: { height: { type: 'number', label: 'Height', min: 0.08, max: 0.28, step: 0.005, unit: 'm' }, radius: { type: 'number', label: 'Rim radius', min: 0.03, max: 0.1, step: 0.002, unit: 'm' }, taper: { type: 'number', label: 'Base taper', min: 0, max: 0.3, step: 0.01 }, staveCount: { type: 'number', label: 'Stave count', min: 6, max: 18, step: 1 }, hoopCount: { type: 'number', label: 'Hoop count', min: 0, max: 4, step: 1 }, handle: { type: 'number', label: 'Handle', min: 0, max: 1, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'iron'], parts: ['staves', 'base', 'hoops', 'handle'], }, 'bronze-bell': { title: 'Bronze Bell', description: 'Bronze bell hung from a yoke. The clapper swings with a lag and strikes the rim; strikes are counted.', category: 'Props', tags: ['medieval', 'lowpoly', 'props', 'animated', 'interactive'], controls: { diameter: { type: 'number', label: 'Mouth diameter', min: 0.15, max: 1, step: 0.01, unit: 'm' }, height: { type: 'number', label: 'Height', min: 0.15, max: 1.1, step: 0.01, unit: 'm' }, yoke: { type: 'number', label: 'Yoke length', min: 1, max: 2.2, step: 0.05 }, swing: { type: 'number', label: 'Swing', min: 5, max: 60, step: 1, unit: '°' }, damping: { type: 'number', label: 'Damping', min: 0.1, max: 3, step: 0.05 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['brass', 'iron', 'oak'], parts: ['bell', 'clapper', 'yoke', 'frame'], }, 'iron-lantern': { title: 'Iron Lantern', description: 'Hexagonal iron cage, glass panels, an oil lamp inside. An enclosed flame flickers more calmly.', category: 'Lighting', tags: ['medieval', 'lowpoly', 'lighting', 'animated', 'interactive'], controls: { height: { type: 'number', label: 'Height', min: 0.14, max: 0.5, step: 0.01, unit: 'm' }, radius: { type: 'number', label: 'Radius', min: 0.04, max: 0.15, step: 0.005, unit: 'm' }, sides: { type: 'number', label: 'Side count', min: 3, max: 8, step: 1 }, flameHeight: { type: 'number', label: 'Flame height', min: 0.08, max: 0.45, step: 0.01 }, flicker: { type: 'number', label: 'Flicker', min: 0, max: 2.5, step: 0.05 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['iron', 'glass', 'char', 'ember'], parts: ['frame', 'font', 'flame', 'handle'], }, 'tavern-sign': { title: 'Tavern Sign', description: 'Wooden board hung by chain from a forged iron bracket. Pushed, it swings long and lazily.', category: 'Props', tags: ['medieval', 'lowpoly', 'props', 'animated', 'interactive'], controls: { width: { type: 'number', label: 'Board width', min: 0.25, max: 1.1, step: 0.02, unit: 'm' }, height: { type: 'number', label: 'Board height', min: 0.2, max: 0.8, step: 0.02, unit: 'm' }, reach: { type: 'number', label: 'Bracket reach', min: 0.3, max: 1.2, step: 0.02, unit: 'm' }, drop: { type: 'number', label: 'Chain drop', min: 0.04, max: 0.35, step: 0.01, unit: 'm' }, plankCount: { type: 'number', label: 'Board count', min: 1, max: 6, step: 1 }, damping: { type: 'number', label: 'Damping', min: 0.05, max: 2, step: 0.05 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'iron'], parts: ['post', 'bracket', 'board'], }, 'leather-book': { title: 'Leather Book', description: 'Leather-bound manuscript with spine bands and clasps. The page block overhangs the cover, wavy.', category: 'Props', tags: ['medieval', 'lowpoly', 'props', 'tabletop', 'procedural'], controls: { width: { type: 'number', label: 'Width', min: 0.08, max: 0.4, step: 0.005, unit: 'm' }, length: { type: 'number', label: 'Length', min: 0.1, max: 0.55, step: 0.005, unit: 'm' }, thickness: { type: 'number', label: 'Thickness', min: 0.02, max: 0.16, step: 0.002, unit: 'm' }, bands: { type: 'number', label: 'Spine bands', min: 0, max: 6, step: 1 }, clasps: { type: 'number', label: 'Clasps', min: 0, max: 3, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['leather', 'cloth', 'brass'], parts: ['cover', 'pages', 'clasps'], }, 'glass-phial': { title: 'Glass Phial', description: 'Blown glass phial with a cork stopper, sealed with wax. Liquid level is computed from the fill.', category: 'Props', tags: ['medieval', 'lowpoly', 'props', 'alchemy', 'tabletop'], controls: { height: { type: 'number', label: 'Height', min: 0.06, max: 0.32, step: 0.005, unit: 'm' }, radius: { type: 'number', label: 'Body radius', min: 0.015, max: 0.08, step: 0.002, unit: 'm' }, neck: { type: 'number', label: 'Neck ratio', min: 0.15, max: 0.55, step: 0.01 }, fill: { type: 'number', label: 'Fill', min: 0, max: 1, step: 0.02 }, hue: { type: 'number', label: 'Liquid hue', min: 0, max: 1, step: 0.01 }, seal: { type: 'number', label: 'Wax seal', min: 0, max: 1, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['glass', 'ember', 'oak', 'char'], parts: ['bottle', 'liquid', 'stopper'], }, 'coin-pouch': { title: 'Coin Pouch', description: 'Drawstring leather pouch with silver pennies spilled to one side out of its mouth.', category: 'Props', tags: ['medieval', 'lowpoly', 'props', 'tabletop', 'procedural'], controls: { height: { type: 'number', label: 'Height', min: 0.05, max: 0.24, step: 0.005, unit: 'm' }, radius: { type: 'number', label: 'Radius', min: 0.02, max: 0.1, step: 0.002, unit: 'm' }, fill: { type: 'number', label: 'Fill', min: 0.15, max: 1, step: 0.02 }, coins: { type: 'number', label: 'Spilled coins', min: 0, max: 30, step: 1 }, coinRadius: { type: 'number', label: 'Coin radius', min: 0.005, max: 0.025, step: 0.001, unit: 'm' }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['leather', 'cloth', 'steel'], parts: ['pouch', 'cord', 'coins'], }, 'wicker-basket': { title: 'Wicker Basket', description: 'Basket woven from willow rods. Horizontal rods pass in front of one upright, behind the next.', category: 'Props', tags: ['medieval', 'lowpoly', 'props', 'farm', 'procedural'], controls: { height: { type: 'number', label: 'Height', min: 0.1, max: 0.5, step: 0.01, unit: 'm' }, radius: { type: 'number', label: 'Rim radius', min: 0.08, max: 0.35, step: 0.005, unit: 'm' }, taper: { type: 'number', label: 'Base taper', min: 0, max: 0.45, step: 0.01 }, stakes: { type: 'number', label: 'Upright rods', min: 6, max: 18, step: 1 }, rows: { type: 'number', label: 'Weave rows', min: 2, max: 14, step: 1 }, produce: { type: 'number', label: 'Produce count', min: 0, max: 24, step: 1 }, hue: { type: 'number', label: 'Produce hue', min: 0, max: 1, step: 0.01 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'produce'], parts: ['weave', 'rim', 'contents'], }, 'wooden-ladder': { title: 'Wooden Ladder', description: 'Two rails converging toward the top, with rungs let into them.', category: 'Structure', tags: ['medieval', 'lowpoly', 'structure', 'procedural'], controls: { height: { type: 'number', label: 'Height', min: 1, max: 5, step: 0.1, unit: 'm' }, width: { type: 'number', label: 'Width', min: 0.25, max: 0.8, step: 0.01, unit: 'm' }, rungCount: { type: 'number', label: 'Rungs', min: 3, max: 18, step: 1 }, taper: { type: 'number', label: 'Taper', min: 0, max: 0.4, step: 0.01 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak'], parts: ['rails', 'rungs'], }, 'round-shield': { title: 'Round Shield', description: 'Planked round shield with a rawhide rim and an iron boss, quartered in paint and leaning on its edge.', category: 'Arms', tags: ['medieval', 'lowpoly', 'arms', 'procedural'], controls: { radius: { type: 'number', label: 'Radius', min: 0.2, max: 0.6, step: 0.01, unit: 'm' }, planks: { type: 'number', label: 'Planks', min: 3, max: 15, step: 1 }, thickness: { type: 'number', label: 'Board thickness', min: 0.008, max: 0.04, step: 0.002, unit: 'm' }, lean: { type: 'number', label: 'Lean back', min: 0, max: 0.6, step: 0.02, unit: 'rad' }, hue: { type: 'number', label: 'Paint hue', min: 0, max: 1, step: 0.01 }, rivets: { type: 'number', label: 'Boss rivets', min: 0, max: 16, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'leather', 'iron'], parts: ['boards', 'rim', 'boss'], }, 'market-stall': { title: 'Market Stall', description: 'Plank counter on a braced trestle under a linen awning slung between four posts, sagging the way cloth does.', category: 'Structure', tags: ['medieval', 'lowpoly', 'structure', 'procedural'], controls: { length: { type: 'number', label: 'Length', min: 1, max: 2.6, step: 0.05, unit: 'm' }, depth: { type: 'number', label: 'Depth', min: 0.45, max: 1.1, step: 0.02, unit: 'm' }, height: { type: 'number', label: 'Counter height', min: 0.55, max: 1, step: 0.02, unit: 'm' }, awning: { type: 'number', label: 'Awning height', min: 1.4, max: 2.4, step: 0.05, unit: 'm' }, sag: { type: 'number', label: 'Cloth sag', min: 0, max: 0.4, step: 0.02 }, planks: { type: 'number', label: 'Counter boards', min: 2, max: 10, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'cloth'], parts: ['top', 'trestle', 'posts', 'awning'], }, 'grindstone': { title: 'Grindstone', description: 'Treadle grindstone: a sandstone wheel in a splayed timber trestle, with an iron crank and a trough of water it runs through. Turns.', category: 'Smithy', tags: ['medieval', 'lowpoly', 'smithy', 'procedural'], controls: { diameter: { type: 'number', label: 'Wheel diameter', min: 0.35, max: 0.95, step: 0.02, unit: 'm' }, thickness: { type: 'number', label: 'Wheel thickness', min: 0.05, max: 0.18, step: 0.005, unit: 'm' }, height: { type: 'number', label: 'Axle height', min: 0.4, max: 0.85, step: 0.02, unit: 'm' }, length: { type: 'number', label: 'Frame length', min: 0.7, max: 1.5, step: 0.05, unit: 'm' }, splay: { type: 'number', label: 'Leg splay', min: 0, max: 0.5, step: 0.02 }, water: { type: 'number', label: 'Water level', min: 0, max: 1, step: 0.05 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['stone', 'oak', 'iron', 'water'], parts: ['wheel', 'frame', 'crank', 'water'], }, 'stone-trough': { title: 'Stone Trough', description: 'Watering trough cut from one block of weathered limestone, with a heavy rim, a worn basin and a film of standing water.', category: 'Structure', tags: ['medieval', 'lowpoly', 'structure', 'procedural'], controls: { length: { type: 'number', label: 'Length', min: 0.8, max: 2.6, step: 0.05, unit: 'm' }, width: { type: 'number', label: 'Width', min: 0.35, max: 0.95, step: 0.02, unit: 'm' }, height: { type: 'number', label: 'Height', min: 0.25, max: 0.7, step: 0.02, unit: 'm' }, wall: { type: 'number', label: 'Wall thickness', min: 0.12, max: 0.34, step: 0.01 }, water: { type: 'number', label: 'Water level', min: 0, max: 1, step: 0.05 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['stone', 'water'], parts: ['block', 'water'], }, 'forge-hearth': { title: 'Forge Hearth', description: "Smith's forge: a raised stone hearth with a chimney back, a bed of embers that can be put out, and a leather bellows.", category: 'Smithy', tags: ['medieval', 'lowpoly', 'smithy', 'procedural'], controls: { length: { type: 'number', label: 'Hearth length', min: 0.7, max: 1.8, step: 0.02, unit: 'm' }, depth: { type: 'number', label: 'Hearth depth', min: 0.5, max: 1.3, step: 0.02, unit: 'm' }, height: { type: 'number', label: 'Working height', min: 0.5, max: 1.1, step: 0.02, unit: 'm' }, chimney: { type: 'number', label: 'Chimney height', min: 0.3, max: 2, step: 0.05, unit: 'm' }, kerbBlocks: { type: 'number', label: 'Kerb blocks', min: 8, max: 28, step: 1 }, lit: { type: 'number', label: 'Lit', min: 0, max: 1, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['stone', 'char', 'ember', 'oak', 'leather', 'iron'], parts: ['hearth', 'fire', 'chimney', 'bellows'], }, 'vegetables': { title: 'Vegetables', description: 'Loose heap mixed from eight kinds: turnip, cabbage, onion, leek, parsnip, carrot, tomato and potato. The first six are the period-correct set.', category: 'Props', tags: ['medieval', 'lowpoly', 'props', 'procedural', 'food'], controls: { count: { type: 'number', label: 'How many', min: 1, max: 30, step: 1 }, kinds: { type: 'number', label: 'Kinds in the mix', min: 1, max: 8, step: 1 }, lead: { type: 'number', label: 'Leading kind', min: 0, max: 7, step: 1 }, size: { type: 'number', label: 'Vegetable size', min: 0.05, max: 0.2, step: 0.005, unit: 'm' }, spread: { type: 'number', label: 'Heap spread', min: 1, max: 5, step: 0.1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['produce'], parts: ['bulbs', 'roots'], }, 'iron-cauldron': { title: 'Iron Cauldron', description: 'Cauldron slung from an iron tripod over a ring of hearth stones, with embers that can be lit or put out.', category: 'Lighting', tags: ['medieval', 'lowpoly', 'hearth', 'procedural'], controls: { height: { type: 'number', label: 'Tripod height', min: 0.8, max: 2, step: 0.02, unit: 'm' }, hearthRadius: { type: 'number', label: 'Hearth radius', min: 0.28, max: 0.8, step: 0.01, unit: 'm' }, stones: { type: 'number', label: 'Hearth stones', min: 6, max: 20, step: 1 }, potRadius: { type: 'number', label: 'Cauldron radius', min: 0.12, max: 0.36, step: 0.01, unit: 'm' }, links: { type: 'number', label: 'Chain links', min: 2, max: 8, step: 1 }, lit: { type: 'number', label: 'Lit', min: 0, max: 1, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['stone', 'iron', 'char', 'ember'], parts: ['hearth', 'tripod', 'pot'], }, 'hand-cart': { title: 'Hand Cart', description: 'Two-wheeled handcart: planked body on an axle set aft of centre, with shafts that rest on the ground.', category: 'Structure', tags: ['medieval', 'lowpoly', 'structure', 'procedural'], controls: { bedLength: { type: 'number', label: 'Bed length', min: 0.8, max: 2.1, step: 0.02, unit: 'm' }, bedWidth: { type: 'number', label: 'Bed width', min: 0.4, max: 1.1, step: 0.02, unit: 'm' }, sideHeight: { type: 'number', label: 'Side height', min: 0.1, max: 0.5, step: 0.01, unit: 'm' }, wheelRadius: { type: 'number', label: 'Wheel radius', min: 0.18, max: 0.5, step: 0.01, unit: 'm' }, spokes: { type: 'number', label: 'Spokes', min: 6, max: 14, step: 1 }, shaftLength: { type: 'number', label: 'Shaft length', min: 0.6, max: 1.8, step: 0.02, unit: 'm' }, roll: { type: 'number', label: 'Wheel rotation', min: 0, max: 6.28, step: 0.02, unit: 'rad' }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'iron'], parts: ['wheels', 'bed', 'sides', 'shafts'], }, 'stone-well': { title: 'Stone Well', description: 'Village well: a drystone kerb, a timber frame over it, and a windlass with rope and bucket.', category: 'Structure', tags: ['medieval', 'lowpoly', 'structure', 'procedural'], controls: { radius: { type: 'number', label: 'Kerb radius', min: 0.35, max: 1.1, step: 0.01, unit: 'm' }, wallHeight: { type: 'number', label: 'Kerb height', min: 0.3, max: 1.1, step: 0.02, unit: 'm' }, courses: { type: 'number', label: 'Stone courses', min: 2, max: 7, step: 1 }, blocks: { type: 'number', label: 'Blocks per course', min: 6, max: 18, step: 1 }, frameHeight: { type: 'number', label: 'Frame height', min: 1.2, max: 2.8, step: 0.05, unit: 'm' }, depth: { type: 'number', label: 'Bucket drop', min: 0, max: 1, step: 0.02 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['stone', 'oak', 'iron', 'cloth'], parts: ['kerb', 'frame', 'windlass', 'bucket'], }, 'post-mill': { title: 'Post Mill', description: 'Medieval post mill: the whole timber body turns on one massive post, with four lattice sails and the tail ladder it is turned by.', category: 'Structure', tags: ['medieval', 'lowpoly', 'structure', 'procedural', 'landmark'], controls: { height: { type: 'number', label: 'Height to ridge', min: 3.5, max: 9, step: 0.1, unit: 'm' }, sailSpan: { type: 'number', label: 'Sail span', min: 4, max: 11, step: 0.1, unit: 'm' }, sailBars: { type: 'number', label: 'Bars per sail', min: 3, max: 14, step: 1 }, sailWidth: { type: 'number', label: 'Sail width', min: 0.08, max: 0.28, step: 0.005 }, ladderRungs: { type: 'number', label: 'Ladder rungs', min: 5, max: 20, step: 1 }, spin: { type: 'number', label: 'Sail angle', min: 0, max: 6.28, step: 0.02, unit: 'rad' }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'iron'], parts: ['trestle', 'body', 'sails', 'ladder'], }, 'wooden-fence': { title: 'Wooden Fence', description: 'Mortised riven fence: the rail runs through the post and projects from the far face at the ends.', category: 'Structure', tags: ['medieval', 'lowpoly', 'structure', 'farm', 'procedural'], controls: { sections: { type: 'number', label: 'Sections', min: 1, max: 5, step: 1 }, sectionLength: { type: 'number', label: 'Section length', min: 1.6, max: 3.4, step: 0.05, unit: 'm' }, height: { type: 'number', label: 'Height', min: 0.8, max: 1.7, step: 0.02, unit: 'm' }, railCount: { type: 'number', label: 'Rails', min: 1, max: 4, step: 1 }, rough: { type: 'number', label: 'Irregularity', min: 0, max: 2, step: 0.05 }, brace: { type: 'number', label: 'Brace', min: 0, max: 1, step: 1 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak'], parts: ['posts', 'rails'], }, 'wooden-hoe': { title: 'Wooden Hoe', description: 'Gooseneck field hoe: a curved forged neck carrying the blade ahead of the shaft axis, dished blade.', category: 'Tools', tags: ['medieval', 'lowpoly', 'tools', 'farm', 'procedural'], controls: { length: { type: 'number', label: 'Shaft length', min: 0.8, max: 1.7, step: 0.02, unit: 'm' }, shaftRadius: { type: 'number', label: 'Shaft thickness', min: 0.012, max: 0.035, step: 0.001, unit: 'm', }, bladeWidth: { type: 'number', label: 'Blade width', min: 0.1, max: 0.3, step: 0.005, unit: 'm', }, neckSweep: { type: 'number', label: 'Neck sweep', min: 40, max: 150, step: 2, unit: '°' }, dish: { type: 'number', label: 'Dish', min: 0, max: 2, step: 0.05 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'iron', 'steel'], parts: ['shaft', 'socket', 'blade'], }, 'wooden-shovel': { title: 'Wooden Shovel', description: 'A single-piece dished steel plate that both widens and thins toward the tip.', category: 'Tools', tags: ['medieval', 'lowpoly', 'tools', 'farm', 'procedural'], controls: { length: { type: 'number', label: 'Length', min: 0.8, max: 1.8, step: 0.02, unit: 'm' }, shaftRadius: { type: 'number', label: 'Shaft thickness', min: 0.014, max: 0.035, step: 0.001, unit: 'm', }, bladeWidth: { type: 'number', label: 'Blade width', min: 0.12, max: 0.34, step: 0.005, unit: 'm', }, bladeLength: { type: 'number', label: 'Blade length', min: 0.18, max: 0.4, step: 0.01 }, dish: { type: 'number', label: 'Scoop depth', min: 0, max: 0.28, step: 0.01 }, bladeAngle: { type: 'number', label: 'Blade angle', min: 0, max: 25, step: 1, unit: 'degrees' }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'iron'], parts: ['shaft', 'socket', 'blade'], }, 'wooden-pitchfork': { title: 'Wooden Pitchfork', description: 'Steel tines splaying outward; the gap in the silhouette makes it readable from a distance.', category: 'Tools', tags: ['medieval', 'lowpoly', 'tools', 'farm', 'procedural'], controls: { length: { type: 'number', label: 'Length', min: 1, max: 2.4, step: 0.02, unit: 'm' }, shaftRadius: { type: 'number', label: 'Shaft thickness', min: 0.012, max: 0.035, step: 0.001, unit: 'm', }, tineCount: { type: 'number', label: 'Tine count', min: 2, max: 6, step: 1 }, spread: { type: 'number', label: 'Tine spread', min: 0, max: 0.4, step: 0.01 }, tineLength: { type: 'number', label: 'Tine length', min: 0.1, max: 0.42, step: 0.01 }, seed: { type: 'number', label: 'Variation seed', min: 1, max: 64, step: 1 }, }, materialSlots: ['oak', 'iron'], parts: ['shaft', 'socket', 'tines'], }, }