Genesis Lowpoly
El motor lowpoly de Fire Tower Games: rostros, figuras, vehículos, edificios, criaturas y mundos enteros, dibujados en SVG por código, deterministas y sin dependencias.
Fire Tower Games' lowpoly engine: faces, figures, vehicles, buildings, creatures and whole worlds, drawn as SVG by code, deterministic and dependency-free.
v0.97.1Qué es Genesis
What Genesis is
Genesis Lowpoly es el motor lowpoly de Fire Tower Games. Recibe la descripción de algo (una cara, un vehículo, un edificio, un mundo entero) y devuelve su dibujo en SVG: planos poligonales de color plano, sin contorno, cada uno con su tono según la luz.
- Determinista. La misma entrada da siempre el mismo SVG, y una semilla da siempre la misma persona.
- Sin dependencias y sin DOM. Son funciones que devuelven texto. Corre igual en Node (22 o superior) y en el navegador.
- Todo en metros. Una persona mide 1,75 m y una celda de edificio, 4 m. La escala de dibujo la elige el juego.
- La luz es una hora.
daylight(18.5)tiñe, alarga las sombras y enciende las ventanas de lo que dibuje.
Está pensado para estudios de juegos y apps que necesitan muchos personajes, vehículos, edificios y mundos coherentes entre sí, generados en tiempo de ejecución, sin un pipeline de arte ni IA.
Genesis Lowpoly is Fire Tower Games’ lowpoly engine. You describe something (a face, a vehicle, a building, a whole world) and it returns the drawing as SVG: flat-colored polygons with no outlines, each one shaded by the light.
- Deterministic. The same input always produces the same SVG, and a seed always produces the same person.
- No dependencies, no DOM. Every call is a function that returns a string. It runs the same in Node (22 or later) and in the browser.
- Everything in meters. A person is 1.75 m tall and a building cell is 4 m wide. Your game picks the drawing scale.
- Light is an hour of the day.
daylight(18.5)tints whatever you draw, stretches its shadows and lights its windows.
It is built for game studios and apps that need large numbers of consistent characters, vehicles, buildings and worlds, generated at runtime, without an art pipeline or AI.
Acceso y licencia
Access and licensing
El paquete @firetower/genesis-lowpoly se entrega con licencia desde el registro npm del estudio, npm.firetower.games. No está publicado en el registro público de npm.
Al comprar, quien compra agrega a los desarrolladores de su plan. Cada uno entra a su cuenta de Genesis con su correo, con un enlace que le llega o con Google, y ahí encuentra su clave personal de npm y las dos líneas para su proyecto. La clave sirve mientras la licencia esté vigente. Los planes y sus precios están en Precios; los términos, en Licencia.
El paquete trae el motor completo como módulos ES y como script clásico (dist/genesis.js): los generadores, los kits y los paquetes de piezas que documenta esta guía.
The @firetower/genesis-lowpoly package is delivered under license from the studio’s npm registry, npm.firetower.games. It is not published on the public npm registry.
After buying, the buyer adds the developers of their plan. Each one signs in to their Genesis account with their email, through a link sent to them or with Google, and finds their personal npm token and the two lines for their project there. The token works while the license is active. Plans and prices are on Pricing; the terms, on License.
The package contains the whole engine as ES modules and as a classic script (dist/genesis.js): the generators, kits and piece packs described in these docs.
Instalación y primer dibujo
Install and first drawing
Crea un archivo .npmrc en la carpeta del proyecto con estas dos líneas; la clave la copias de tu cuenta de Genesis:
@firetower:registry=https://npm.firetower.games///npm.firetower.games/:_authToken=<tu clave>
Después instala como cualquier paquete: npm install @firetower/genesis-lowpoly. Para actualizar, npm update @firetower/genesis-lowpoly; lee antes «Versiones y cambios». No subas el .npmrc con tu clave a un repositorio público. Si tu entorno no puede usar npm, la cuenta también ofrece el archivo .tgz para npm install ./firetower-genesis-lowpoly-<versión>.tgz.
Cada generador devuelve un texto SVG. En Node se escribe a un archivo; en el navegador se pone en el DOM o en un <img> con una URL data:. El motor no tiene estado: no hay nada que inicializar.
Create a .npmrc file in the project folder with these two lines; copy the token from your Genesis account:
@firetower:registry=https://npm.firetower.games///npm.firetower.games/:_authToken=<your token>
Then install it like any package: npm install @firetower/genesis-lowpoly. To update, npm update @firetower/genesis-lowpoly; read “Versioning and changelog” first. Never commit the .npmrc holding your token to a public repository. If your environment cannot use npm, your account also offers the .tgz file for npm install ./firetower-genesis-lowpoly-<version>.tgz.
Every generator returns an SVG string. In Node you write it to a file; in the browser you put it in the DOM, or in an <img> through a data: URL. The engine holds no state, so there is nothing to initialize.
import { writeFileSync } from 'node:fs';
import { face, random } from '@firetower/genesis-lowpoly/faces';
// the same seed always gives the same person
const svg = face({ ...random(7, 'f'), years: 30, frame: 'bust' });
writeFileSync('face.svg', svg);
// in the browser
img.src = 'data:image/svg+xml,' + encodeURIComponent(svg);Motor, paquetes y juego
Engine, packs and game
Genesis separa tres capas:
- Motor: capacidades y kits. Cómo se dibuja, la luz, cómo se pone una prenda, cómo se arma un vehículo, cómo envejece o se daña algo. No trae el aspecto de ningún juego y nunca importa un paquete.
- Paquetes (
…/packs): piezas, planos y escenas hechos con los kits públicos, que sirven a más de un juego: ropa, edificios, mobiliario, especies, trofeos. - Tu juego: lo que es solo tuyo (una librea, una prenda, un plano), hecho con los mismos kits que usan los paquetes:
…/faces/kit,…/vehicles/kit,…/buildings/kityvehicle({ model }).
Los colores de equipo, los textos y los logos entran siempre desde el juego: el motor no sabe de ninguno.
Genesis is split into three layers:
- Engine: capabilities and kits. How things are drawn and lit, how a garment is worn, how a vehicle is assembled, how things age or get damaged. It carries no game’s look and never imports a pack.
- Packs (
…/packs): pieces, blueprints and scenes built with the public kits and shared by more than one game: clothing, buildings, furniture, species, trophies. - Your game: whatever is yours alone (a livery, a garment, a blueprint), built with the same kits the packs use:
…/faces/kit,…/vehicles/kit,…/buildings/kitandvehicle({ model }).
Team colors, text and logos always come from your game. The engine knows none of them.
import { face, random } from '@firetower/genesis-lowpoly/faces';
import { item, sym } from '@firetower/genesis-lowpoly/faces/kit';
import { wear } from '@firetower/genesis-lowpoly/packs';
// a garment from a pack, in your colors
const coat = wear('trench-coat', ['#2b2d33', '#111111']);
// a garment of your own, made with the same kit the packs use
const FUR_CAPE = item('fur-cape', 'body', 'Fur cape', ['#6b4423'], (c, k) => ({
body: sym(c, [[c.cx - c.nw - 8, c.neckY], [c.cx, c.neckY + 6], [c.cx, 410], [c.cx - c.sw, 410]], k[0], .1, -.2)
}));
face({ ...random(11, 'm'), years: 40, gear: [FUR_CAPE.wear(), wear('spy-hat')] });Ejes y metros
Axes and meters
Todo lo que tiene volumen se modela en metros, con los mismos ejes: x hacia adelante (el morro de un vehículo), y hacia la izquierda y z hacia arriba. El suelo es z = 0.
- Un modelo se coloca con
at: [x, y]oat: [x, y, z]y se gira conturn, en grados sobre la vertical (0 mira hacia +x). - Medidas de referencia: una persona, 1,75 m; una celda de edificio,
CELL= 4 m; un piso,FLOOR= 3,2 m; un monoplaza, unos 5,6 m de largo. - Como todo comparte la misma unidad, un auto, una casa y una persona salen a escala entre sí sin ajustar nada.
Everything with volume is modeled in meters on the same axes: x forward (a vehicle’s nose), y to the left and z up. The ground is z = 0.
- You place a model with
at: [x, y]orat: [x, y, z]and turn it withturn, in degrees around the vertical axis (0 faces +x). - Reference sizes: a person is 1.75 m; a building cell is
CELL= 4 m; a floor isFLOOR= 3.2 m; a single-seater is about 5.6 m long. - Because everything shares one unit, a car, a house and a person come out at the right scale next to each other with no tuning.
import { world } from '@firetower/genesis-lowpoly/world';
import { buildingModel } from '@firetower/genesis-lowpoly/buildings';
import { model } from '@firetower/genesis-lowpoly/vehicles';
import { figureModel } from '@firetower/genesis-lowpoly/figures';
import { plan } from '@firetower/genesis-lowpoly/packs';
world({ view: 'iso', scale: 8, items: [
{ id: 'house', faces: buildingModel(plan('house', {})), at: [0, 0] },
{ id: 'car', faces: model({ type: 3, livery }), at: [4, -8], turn: 90 }, // facing +y
{ id: 'ana', faces: figureModel({ cycle: 'walk', phase: .3 }), at: [9, -5], turn: 180 }
] }).svg;Vistas y cámaras
Views and cameras
Los generadores en 3D proyectan su modelo con una cámara paralela. Las vistas tienen nombre:
| Vista | Cámara |
|---|---|
side | Perfil, desde −y, sin inclinación. |
three | Tres cuartos: girada 32° y 24° desde arriba. |
top | Cenital. |
iso | La isométrica de los mapas de rejilla: 45° y 35,26° desde arriba. |
front | De frente desde −y y 14° desde arriba, para leer estantes y fondos. Solo en world(). |
Cada generador declara las suyas: VIEWN (vehículos), VIEWF (figuras), VIEWT (naturaleza) y VIEWC (criaturas) tienen side, three, top e iso; VIEWB (edificios) no tiene cenital; WORLD_VIEWS añade front. Una vista que el generador no conoce cae en su vista por defecto, sin error.
3D generators project their model through a parallel camera. Views are named:
| View | Camera |
|---|---|
side | Profile, from −y, no tilt. |
three | Three-quarters: turned 32° and tilted 24° down. |
top | Straight down. |
iso | The true isometric of tile maps: 45° round, 35.26° down. |
front | Straight on from −y, 14° down, so shelves and back walls read. world() only. |
Each generator lists its own: VIEWN (vehicles), VIEWF (figures), VIEWT (nature) and VIEWC (creatures) offer side, three, top and iso; VIEWB (buildings) has no top view; WORLD_VIEWS adds front. A view a generator does not know falls back to its default without an error.
import { vehicle, VIEWN } from '@firetower/genesis-lowpoly/vehicles';
VIEWN.map((view) => vehicle({ type: 0, view, livery })); // the same car, four camerasEscala o encuadre
Scale versus fit
Sin scale, un generador en 3D ajusta el dibujo a su marco: un vehículo llena 400 × 260, un edificio 400 × 300. Sirve para fichas y tarjetas, donde cada cosa se ve sola.
Con scale (píxeles por metro), cada cosa se dibuja a su tamaño real: el SVG declara su width y su height, apoya su base abajo y deja salir su sombra (overflow="visible"). Dos dibujos con la misma escala se pueden poner uno junto a otro y guardan su proporción.
Los rostros son 2D y no tienen escala: se encuadran con frame. world() siempre dibuja a escala (10 px/m si no se dice).
Without scale, a 3D generator fits the drawing to its frame: a vehicle fills 400 × 260, a building 400 × 300. Use it for cards and thumbnails, where each thing is shown on its own.
With scale (pixels per meter), each thing is drawn at its true size: the SVG states its width and height, rests its base on the bottom edge and lets its shadow spill out (overflow="visible"). Two drawings at the same scale can sit side by side and keep their proportions.
Faces are 2D and have no scale; you frame them with frame. world() always draws to scale (10 px/m by default).
vehicle({ type: 0, view: 'side', livery }); // fitted to 400 × 260
vehicle({ type: 0, view: 'side', livery, scale: 30, bg: 'none' }); // 30 px per meter
figure({ view: 'side', scale: 30, bg: 'none' }); // same scale: same proportionsLa luz de una hora
Light by the hour
La luz es un sistema aparte de todo generador. daylight(hora, opciones) devuelve la luz de una hora continua de 0 a 24 (18.5 son las seis y media de la tarde): de dónde viene el sol o la luna, el tinte, la oscuridad, la fuerza y el largo de las sombras, qué parte de las ventanas se enciende (lights), las estrellas y los colores del cielo.
- Se le pasa como
lighta cualquier generador en 3D y aworld(). - De noche se encienden las ventanas de los edificios (
lights, de 0 a 1, siempre las mismas), las farolas y su charco de luz en el suelo. - Lo plano (un rostro, un dibujo propio) se tiñe con
inLight(svg, luz). - Opciones:
sunriseysunset(6 y 18 por defecto) ydarkness, lo oscura que llega la noche (0,52 por defecto).MOMENTStrae seis horas con nombre, del amanecer a la noche.
Light is a system of its own, separate from every generator. daylight(hour, options) returns the light of a continuous hour from 0 to 24 (18.5 is half past six in the evening): where the sun or moon is, the tint and darkness, how strong and long shadows are, the share of windows lit (lights), the stars and the sky colors.
- Pass it as
lightto any 3D generator and toworld(). - At night, building windows light up (
lights, 0 to 1, always the same windows), and so do street lamps and the pool of light they cast. - Flat drawings (a face, your own art) are tinted with
inLight(svg, light). - Options:
sunriseandsunset(6 and 18 by default) anddarkness, how dark the night gets (0.52 by default).MOMENTSnames six hours, from dawn to night.
import { daylight } from '@firetower/genesis-lowpoly/daylight';
import { inLight } from '@firetower/genesis-lowpoly/scenery';
const dusk = daylight(18.3, { sunrise: 6, sunset: 18 });
building(plan('house', { view: 'three', light: dusk }));
building(plan('house', { view: 'three', light: daylight(22), lights: .8 })); // 80 % of windows lit
inLight(face(who), dusk); // a flat SVG, tintedDeterminismo y semillas
Determinism and seeds
El motor no usa Math.random para dibujar. Todo lo que varía sale de una semilla entera a través de un generador pseudoaleatorio propio (rng(seed) del núcleo), así que la misma entrada da el mismo SVG cada vez. La suite de pruebas compara el hash de cada dibujo de referencia con el aprobado.
random(seed, g)sortea una cara;seeden vehículos, árboles, rocas, criaturas e isotipos varía cada ejemplar sin cambiar su tipo.- Por eso un juego puede guardar solo la semilla de un rival y volver a dibujarlo igual años después.
- La única excepción es del editor:
randomize()sin semilla elige una al azar.ed.seed()dice cuál salió.
The engine never uses Math.random to draw. Everything that varies comes from an integer seed through the core’s own pseudo-random generator (rng(seed)), so the same input yields the same SVG every time. The test suite compares the hash of every reference drawing with the approved one.
random(seed, g)rolls a face;seedon vehicles, trees, rocks, creatures and isotypes varies each instance without changing its kind.- That means a game can store just a rival’s seed and draw it identically years later.
- The one exception is the editor:
randomize()without a seed picks one at random, anded.seed()tells you which.
import { face, random } from '@firetower/genesis-lowpoly/faces';
import { tree } from '@firetower/genesis-lowpoly/nature';
face(random(42)) === face(random(42)); // true
tree({ kind: 'pine', seed: 4 }) === tree({ kind: 'pine', seed: 4 }); // true
tree({ kind: 'pine', seed: 5 }); // another pineGuardar un diseño
Saving a design
Lo que un juego guarda es quién es algo, en un texto corto; lo del momento (la edad, la expresión, el desgaste, los patrocinios, la hora) se pasa al dibujar.
- Rostros:
encode(spec)ydecode(text), por ejemplo1-f-4.3.1.1.0.1.2.1.0--5.1.-7.-5. - Vehículos:
encodeVehicleydecodeVehicle, con prefijovehicle:. Edificios:encodeBuildingydecodeBuilding, conbuilding:. Emblemas:EMBLEM.encode, conemblem:. - Un texto roto se rechaza con un error que dice qué está mal: nunca se dibuja otra cosa.
whose(text, [FACE, VEHICLE, BUILDING])dice qué generador escribió un texto. Los rostros son el único texto sin prefijo.
What a game stores is who or what something is, as a short string. Anything of the moment (age, expression, wear, sponsors, the hour) is passed in when drawing.
- Faces:
encode(spec)anddecode(text), for example1-f-4.3.1.1.0.1.2.1.0--5.1.-7.-5. - Vehicles:
encodeVehicleanddecodeVehicle, prefixedvehicle:. Buildings:encodeBuildinganddecodeBuilding, prefixedbuilding:. Emblems:EMBLEM.encode, prefixedemblem:. - A malformed string is rejected with an error that says what is wrong. It never draws something else.
whose(text, [FACE, VEHICLE, BUILDING])tells you which generator wrote a string. Faces are the only strings without a prefix.
import { random, encode, decode, face } from '@firetower/genesis-lowpoly/faces';
import { encodeVehicle, decodeVehicle, vehicle } from '@firetower/genesis-lowpoly/vehicles';
const text = encode(random(20261004, 'f')); // '1-f-4.3.1.1.0.1.2.1.0--5.1.-7.-5'
face({ ...decode(text), years: 45 }); // the same person, older
encodeVehicle({ type: 0, parts: { nose: 1, fwing: 2 }, livery: ['#c8102e', '#f4f1ea', '#1b2a41'], paint: { pods: '#2a6fdb' } });
// 'vehicle:1-0-1.2.0.0.0-c8102e.f4f1ea.1b2a41-pods=2a6fdb'
vehicle({ ...decodeVehicle(saved), view: 'three', wear: .4 });Promesas de compatibilidad
Compatibility promises
Un dibujo guardado por un juego no debe cambiar cuando se actualiza el motor. Estas reglas lo sostienen:
- Los catálogos solo crecen por el final. Un texto guardado son posiciones (
hair: 3), así que una pieza nueva se añade al final y las existentes no se reordenan ni se quitan. Vale paraHAIR,PARTS,TYPES,BLOCKS,CYCLES,MATERIALSy los demás. - Los ids nunca se renombran ni se quitan: prendas (
ITEMS), piezas del mundo (PIECES), planos (BLUEPRINTS), trofeos, especies. random()es estable. Los rasgos nuevos (marcas en la piel, mentón, orejas, peso) no entran al sorteo, así que una semilla sigue dando la misma cara. La única vez que el sorteo cambió fue en la v0.6.0, y está en el registro de cambios.- Los textos viejos se siguen leyendo. Un rasgo nuevo en su valor inicial no cambia el texto, y un vehículo guardado con menos piezas lee las que faltan como la primera.
- Todo cambio de dibujo pasa por una galería de referencia: las pruebas fallan si una cara de referencia sale distinta, y un cambio querido se revisa a ojo antes de aprobarse.
A drawing a game has saved should not change when the engine is updated. These rules make sure of it:
- Catalogs only grow at the end. A saved string holds positions (
hair: 3), so new pieces are appended and existing ones are never reordered or removed. This applies toHAIR,PARTS,TYPES,BLOCKS,CYCLES,MATERIALSand the rest. - Ids are never renamed or removed: garments (
ITEMS), world pieces (PIECES), blueprints (BLUEPRINTS), trophies, species. random()is stable. New traits (skin marks, chin, ears, weight) are never rolled, so a seed keeps producing the same face. The roll changed only once, in v0.6.0, and the changelog records it.- Old strings keep decoding. A new trait at its default does not change the string, and a vehicle saved with fewer parts reads the missing ones as the first option.
- Every drawing change goes through a reference gallery: the test suite fails if a reference face comes out different, and an intended change is reviewed by eye before it is approved.
La salida: SVG como texto
Output: SVG strings
- Cada generador devuelve un texto SVG completo (
<svg xmlns=…>), o un objeto con.svgy datos para el juego (anclajes, contornos, tamaños). Nada toca el DOM. - La animación va en el propio SVG, con SMIL (
<animate>,<animateTransform>,<animateMotion>): ruedas que giran, ciclos de figuras, gestos de una cara. Sin JavaScript ni CSS del juego, así que también se anima dentro de un<img>. La única excepción es el parpadeo de una cara (blink: true), que deja los párpados en la clase.blpara el CSS del juego. - XML estricto: ningún elemento repite un atributo, así que el SVG sirve igual en línea, como archivo o como URL
data:. Una prueba pasa los acabados, tipos y vistas de los vehículos por un analizador estricto. - Los SVG de los generadores llevan
role="img"y unaria-labelgenérico en inglés («Generated face», «Generated world»…); los emblemas y las placas no llevan ninguno. Si la accesibilidad importa en tu pantalla, pon tu propia etiqueta alrededor.
- Every generator returns a complete SVG string (
<svg xmlns=…>), or an object with.svgplus data for your game (anchors, outlines, sizes). Nothing touches the DOM. - Animation lives inside the SVG, as SMIL (
<animate>,<animateTransform>,<animateMotion>): spinning wheels, figure cycles, facial gestures. No JavaScript and no CSS from your game, so it animates inside an<img>too. The one exception is a face’s blink (blink: true), which tags the eyelids with the.blclass for your CSS. - Strict XML: no element repeats an attribute, so the SVG works the same inline, as a file or as a
data:URL. A test runs vehicle finishes, types and views through a strict parser. - The generators’ SVGs carry
role="img"and a generic Englisharia-label(“Generated face”, “Generated world”…); emblems and nameplates carry none. If accessibility matters on your screen, wrap it with your own label.
const svg = vehicle({ type: 0, livery, speed: .7, animate: true }); // wheels spin by themselves
container.innerHTML = svg; // inline
img.src = 'data:image/svg+xml,' + encodeURIComponent(svg); // or as an image
await fs.writeFile('car.svg', svg); // or as a fileRendimiento
Performance
- Dibuja una vez y reutiliza. Como la salida es determinista, se puede guardar en caché por el texto guardado y los parámetros del momento. Dibujar cada cuadro de nuevo casi nunca hace falta.
- La animación pesa. Un ciclo de figura de 12 cuadros pesa entre 50 y 90 KB; con
frames: 6, la mitad. Un equipo de mecánicos animado pesa unas cuatro veces más que quieto. - Lo que camina, con recorrido. En
world(), una figura contrack(figureTrack()) dibuja cada pose una vez y el SVG la desliza: un caminante pasó de unos 600 KB en cuadros a 130 KB. - Un mundo grande, en pila.
world({ split: true })devuelvestack, imágenes en el orden de pintado: la base quieta, tramos quietos y lo que se mueve aparte. Pon cada una en su propio<img>conwill-change: transform, para que el navegador no vuelva a pintar la base debajo de lo que se mueve. En un mapa de ciudad medido en Chrome, la pila pasó de 6–7 fps a 144 con una capa por imagen. boundsfija el encuadre de un mundo para que la imagen no se mueva mientras las cosas se mueven.
- Draw once, reuse. Output is deterministic, so you can cache it by the saved string plus the parameters of the moment. Redrawing every frame is almost never needed.
- Animation has a cost. A 12-frame figure cycle weighs 50 to 90 KB; with
frames: 6, half that. An animated pit crew weighs about four times its still version. - Use tracks for walkers. In
world(), a figure with atrack(figureTrack()) draws each pose once and lets the SVG slide it: one walker went from about 600 KB as frames to 130 KB. - Stack large worlds.
world({ split: true })returnsstack, images in paint order: the still base, still runs, and whatever moves on its own. Put each in its own<img>withwill-change: transformso the browser does not repaint the base under every moving thing. On a city map measured in Chrome, the stack went from 6–7 fps to 144 fps with one layer per image. boundsholds a world’s framing still while things move inside it.
const w = world({ view: 'iso', scale: 10, items, split: true });
for (const layer of w.stack) { // [{ svg, box: [x0, y0, x1, y1], moving, ids }]
const img = document.createElement('img');
img.src = 'data:image/svg+xml,' + encodeURIComponent(layer.svg);
img.style.cssText = 'position:absolute; will-change:transform;' +
'left:' + layer.box[0] + 'px; top:' + layer.box[1] + 'px;';
stage.append(img);
}Rostros
Faces
face(opciones) dibuja un rostro. Lo que es la persona (rasgos, colores, forma) viene de random(seed, g), del editor o de decode(); lo demás lo pone el juego al dibujar.
years: edad continua de 1 a 100 (30 si falta).expr:EXPR.REST,HAPPY,SPENT,FOCUSED,SURPRISED,ANGRY,SADoCONFUSED. Conanimate: true, el SVG reproduce el gesto.outfit:[primario, secundario]. Sin él, el cuerpo sale como un maniquí de planos lisos, listo para que el juego lo vista.frame:'full'(300 × 400, por defecto),'portrait','chest'(3:4),'bust'o'head'(cuadrados). El motor mide cada cara, así que un moño alto o una cabeza de niño tienen su propio encuadre.bg: un color, o'none'para no tener fondo.
face(options) draws a face. Who the person is (traits, colors, shape) comes from random(seed, g), the editor or decode(); everything else is passed by your game at draw time.
years: continuous age from 1 to 100 (30 when missing).expr:EXPR.REST,HAPPY,SPENT,FOCUSED,SURPRISED,ANGRY,SADorCONFUSED. Withanimate: true, the SVG plays the gesture.outfit:[primary, secondary]. Without it, the body is a plain mannequin, ready for your game to dress.frame:'full'(300 × 400, the default),'portrait','chest'(3:4),'bust'or'head'(square). Each face is measured, so a tall bun or a child’s head gets its own framing.bg: a color, or'none'for no background.
import { face, random, EXPR } from '@firetower/genesis-lowpoly/faces';
const who = random(20261004, 'f'); // 'f' or 'm'; omit it to roll that too
face({
...who,
years: 30,
expr: EXPR.HAPPY,
outfit: ['#f2c44d', '#12606e'],
animate: true, // the smile opens by itself
frame: 'bust',
bg: '#12606e'
});Edad, rasgos y herederos
Age, traits and heirs
La misma persona va de 1 a 100 años: de niño la cabeza es grande y redonda sobre un cuerpo pequeño, y desde los 38 aparecen canas, arrugas y papada. La calvicie sale con la edad y es siempre la misma para la misma cara. gray y bald (0–1) mandan sobre lo que pone la edad.
heir(padre, seed)da un heredero de 14 años que se le parece; la semilla decide cuál.- Rasgos guardados con la cara, que
random()nunca sortea:freckles,mole,scar,chin(CHIN),ears(EARS, también en punta),beast(BEASTS: cabezas de animal sobre la misma cara) yshape.weight(0–10). skin,irisyhairColoraceptan un índice del catálogo o cualquier'#rrggbb'.- Opciones de dibujo para mundos de fantasía, que no se guardan:
halo,shadow,eyeGlow,smokeyvoid.
The same person ages from 1 to 100: as a child the head is large and round on a small body, and from 38 on come gray hair, wrinkles and a double chin. Baldness arrives with age and is always the same for the same face. gray and bald (0–1) override what age decides.
heir(parent, seed)returns a 14-year-old heir who resembles the parent; the seed picks which one.- Traits stored with the face that
random()never rolls:freckles,mole,scar,chin(CHIN),ears(EARS, pointed ones included),beast(BEASTS: animal heads over the same face) andshape.weight(0–10). skin,irisandhairColortake a catalog index or any'#rrggbb'.- Drawing options for fantasy worlds, never stored:
halo,shadow,eyeGlow,smokeandvoid.
const who = random(20261004, 'f');
face({ ...who, years: 8 });
face({ ...who, years: 78 });
face({ ...heir(who, 7) }); // heir() sets years: 14
face({ ...who, years: 58, gray: 0 }); // no gray hair, whatever the age
face({ ...who, ears: 1, iris: '#8a4fd8' }); // pointed ears, violet eyesRopa, objetos y paleta
Clothing, items and palette
Las prendas y objetos son piezas que se pasan en gear. Las de los paquetes se piden por id con wear(id, colores, { parts, decals, logo }); sin colores, cada pieza usa los suyos (ITEMS[id].colors).
- Ranuras (
SLOTS):body,neck,head,eyes,hands,faceyback. Se apilan en su orden, sin importar el orden en que se pasen. - Cada pieza dice qué se le puede cambiar:
roles(el nombre de cada color),parts(variantes numeradas) yzones(dónde va un patrón del juego, condecals). Las que tienenbadgellevan el logo del equipo. - Ningún casco ni sombrero deja ver el pelo. Paquetes de ropa:
racing,sport,classic,work,traditional,care,fanyfantasy(PACKS). palette(spec)da{ skin, hair, iris }de una cara, para dibujar a la misma persona de cuerpo entero.
Clothes and items are pieces passed in gear. Pack pieces are requested by id with wear(id, colors, { parts, decals, logo }); without colors, each piece uses its own (ITEMS[id].colors).
- Slots (
SLOTS):body,neck,head,eyes,hands,faceandback. Pieces stack in slot order, whatever order you pass them in. - Each piece declares what can be customized:
roles(a name for each color),parts(numbered variants) andzones(where your game’s pattern goes, throughdecals). Pieces with abadgecarry the team logo. - No helmet or hat ever shows the hair beneath. Clothing packs:
racing,sport,classic,work,traditional,care,fanandfantasy(PACKS). palette(spec)returns a face’s{ skin, hair, iris }, so you can draw the same person full-body.
import { face, random } from '@firetower/genesis-lowpoly/faces';
import { wear } from '@firetower/genesis-lowpoly/packs';
face({ ...random(42, 'm'), years: 35, gear: [
wear('racing-helmet', ['#1b2a41', '#e2472f', '#f5a524'], { parts: { visor: 1 } }) // visor down
] });
face({ ...random(11, 'm'), years: 40, gear: [
wear('suit', ['#1d1f24', '#121214']), wear('spy-hat'), wear('sunglasses')
] });El editor
The editor
Para que el jugador cree lo suyo, createEditor(generador, inicial) guarda el estado de un creador y sus reglas, sin pintar nada. No sabe qué edita: trabaja sobre el objeto que expone cada generador (FACE, VEHICLE, BUILDING o el que devuelve emblemMaker()). La pantalla, los textos y el orden son del juego.
ed.controls dice qué se puede cambiar. Cada control tiene key, type y group (body, color, features, marks, shape). Los tipos:
| Tipo | Guarda |
|---|---|
choice | Uno de values. |
swatch | Un color de colors; con free: true, también cualquier '#rrggbb'. |
pick | Una pieza numerada, de count. |
range | Un entero de min a max. |
color | Cualquier '#rrggbb' (vehículos, emblemas). |
text | Un texto corto con su largo y su patrón (las letras de un emblema). |
Un valor fuera de catálogo lanza RangeError: un creador nunca guarda una pieza que no existe. Lo que no es un control (los bloques de un edificio) cambia con acciones: ed.act(nombre, …).
To let players make their own, createEditor(generator, initial) holds a creator’s state and its rules, and draws nothing. It does not know what it edits: it works on the object each generator exposes (FACE, VEHICLE, BUILDING, or the one emblemMaker() returns). The screen, labels and order are your game’s.
ed.controls lists what can change. Each control has a key, a type and a group (body, color, features, marks, shape). The types:
| Type | Holds |
|---|---|
choice | One of values. |
swatch | A color from colors; with free: true, any '#rrggbb' too. |
pick | A numbered piece, out of count. |
range | An integer from min to max. |
color | Any '#rrggbb' (vehicles, emblems). |
text | A short string with a length limit and a pattern (an emblem’s letters). |
An out-of-catalog value throws a RangeError, so a creator can never store a piece that does not exist. Whatever is not a control (a building’s blocks) changes through actions: ed.act(name, …).
import { createEditor } from '@firetower/genesis-lowpoly/editor';
import { FACE } from '@firetower/genesis-lowpoly/faces';
const ed = createEditor(FACE, saved ? FACE.decode(saved) : null); // null: FACE.blank()
ed.step('nose', 1); // next nose, wrapping at the end
ed.set('shape.jaw', -40); // a range clamps to its limits: -10
ed.set('nose', 9); // RangeError: 9 is outside nose (0 to 3)
ed.randomize(5); // a face from seed 5; ed.seed() === 5
ed.subscribe(() => paint(ed.render({ years: 20, frame: 'bust' })));
const text = ed.encode(); // what your game storesFiguras
Figures
Personas de cuerpo entero sobre un esqueleto en 3D. figure(opciones) da el SVG; figureModel(opciones) da sus caras en metros, para un mundo.
cycle(CYCLES): movimientos que se repiten sin corte (walk,run,skate,swim,bike,dance…) y poses en su sitio (stand,sit,kneel,cheer,drive…).- Quieta,
phase(0–1) elige el momento del ciclo. Conanimate: true, el SVG lo reproduce solo:frames(12 por defecto) endurationsegundos (cada ciclo trae la suya,PERIOD). colors: los dos colores del juego, que cada ciclo viste a su modo....palette(spec)pone la piel y el pelo de una cara.style(STYLES):toon, el personaje de juego, robusto y legible de lejos (por defecto), oreal.- Vistas:
side(por defecto),three,topeiso. Tomalight,scaleybg: 'none'.
Full-body people on a 3D skeleton. figure(options) returns the SVG; figureModel(options) returns its faces in meters, for a world.
cycle(CYCLES): seamless loops (walk,run,skate,swim,bike,dance…) and in-place poses (stand,sit,kneel,cheer,drive…).- When still,
phase(0–1) picks the moment in the cycle. Withanimate: true, the SVG plays it by itself:frames(12 by default) overdurationseconds (each cycle has its own,PERIOD). colors: your game’s two colors, which each cycle dresses its own way....palette(spec)brings a face’s skin and hair.style(STYLES):toon, a sturdy game character that reads from afar (the default), orreal.- Views:
side(the default),three,topandiso. Takeslight,scaleandbg: 'none'.
import { figure } from '@firetower/genesis-lowpoly/figures';
import { palette, random } from '@firetower/genesis-lowpoly/faces';
const who = random(7, 'f');
figure({ ...palette(who), cycle: 'skate', colors: ['#e65d00', '#17191d'], phase: .2 });
figure({ ...palette(who), cycle: 'run', animate: true, frames: 8 }); // plays by itselfOficios, poses de trabajo y equipos
Roles, work poses and crews
role(ROLES) viste una figura por su oficio:casual,worker(chaleco reflectivo y casco),guard,mechanic(mono y gorra) ysport.wear: { top, legs, vest, stripes, hat, hatColor }escribe una ropa propia, sobre el rol o sin él.- Poses de trabajo con lo que tienen en la mano (
TOOLS):kneel,crouch,leanyhold;toollo cambia o lo quita ('none'). crew(caras, { count, colors, people, at, turn, animate })pone 3 o 4 mecánicos alrededor de cualquier vehículo, cada uno en su trabajo, y devuelve objetos paraworld(). Conanimate, la pistola golpea y la llave gira; el vehículo se dibuja una sola vez.pose(cycle, phase)da la pose como dato yfigure({ pose })dibuja una pose propia del juego.
role(ROLES) dresses a figure for its job:casual,worker(hi-vis vest and hard hat),guard,mechanic(overalls and cap) andsport.wear: { top, legs, vest, stripes, hat, hatColor }writes custom clothing, over a role or without one.- Work poses with something in hand (
TOOLS):kneel,crouch,leanandhold;toolswaps or removes it ('none'). crew(faces, { count, colors, people, at, turn, animate })puts 3 or 4 mechanics around any vehicle, each at their task, and returns items forworld(). Withanimate, the wheel gun hammers and the wrench turns; the vehicle is drawn only once.pose(cycle, phase)returns the pose as data, andfigure({ pose })draws a pose of your own.
import { model } from '@firetower/genesis-lowpoly/vehicles';
import { crew } from '@firetower/genesis-lowpoly/figures';
import { world } from '@firetower/genesis-lowpoly/world';
const car = model({ type: 0, livery });
world({ view: 'three', scale: 22, items: [{ id: 'car', faces: car }]
.concat(crew(car, { count: 4, colors: ['#c8102e', '#f4f1ea'] })) }).svg;Caminar por un mundo
Walking through a world
El ciclo anima una figura en su sitio; moverla por el mundo es del juego, con dos herramientas:
figureTrack(opciones, paradas, { cycle, frames })arma el recorrido de una figura entre paradas[{ at, look, stay }], a su paso, de ida y vuelta. Va comotrackde un objeto deworld(): cada pose se dibuja una vez y el SVG la desliza, sin que los pies resbalen.route(trazado, metros, cerrado)dice dónde está algo después de recorrer tantos metros de un trazado y hacia dónde mira:{ at, turn }. Para ir al paso,phase = metros / STRIDE[cycle].
A cycle animates a figure in place; moving it through the world is up to your game, with two tools:
figureTrack(options, stops, { cycle, frames })builds a figure’s round between stops[{ at, look, stay }], at its own pace, there and back. It goes in as thetrackof aworld()item: each pose is drawn once and the SVG slides it, so the feet never skate.route(path, meters, closed)tells you where something is after traveling that many meters along a path, and which way it faces:{ at, turn }. To stay in step, usephase = meters / STRIDE[cycle].
import { figureTrack, figureModel, STRIDE } from '@firetower/genesis-lowpoly/figures';
import { world, route } from '@firetower/genesis-lowpoly/world';
world({ view: 'iso', scale: 12, items: [
{ id: 'ana', track: figureTrack({ colors: ['#2a6fdb', '#1b2a41'] }, [{ at: [0, 0] }, { at: [6, 0], stay: 1 }], { frames: 4 }) }
] }).svg;
// or step it yourself, frame by frame
const m = 12.5, { at, turn } = route(path, m);
const item = { id: 'ana', faces: figureModel({ cycle: 'walk', phase: m / STRIDE.walk }), at, turn };Vehículos
Vehicles
vehicle(opciones) dibuja un vehículo lowpoly. Cada tipo es un solo modelo 3D en metros, así que una pieza nueva sale sola en todas las vistas.
type(TYPES): 0 monoplaza, 1 Grand Prix de 1930, 2 kart, 3 auto, 4 camión.livery:[primario, secundario, acento], del juego.parts(PARTS[type]): formas numeradas por pieza, como{ nose: 1, fwing: 2 }. La forma 0 es la de serie y las formas nunca cambian de orden, así que un juego puede guardarlas y venderlas por posición.model(opciones)da las caras en metros, para un mundo o paraglb(). Vista por defecto:three.focus: 'fwing'gira el auto hasta donde se ve esa pieza, la resalta y apaga el resto, para una tienda o un editor;partView()da esa cámara.
vehicle(options) draws a lowpoly vehicle. Each type is a single 3D model in meters, so a new part shows up in every view on its own.
type(TYPES): 0 single-seater, 1 1930s Grand Prix car, 2 kart, 3 car, 4 truck.livery:[primary, secondary, accent], from your game.parts(PARTS[type]): numbered shapes per part, such as{ nose: 1, fwing: 2 }. Shape 0 is the stock one, and shapes never change order, so a game can store and sell them by position.model(options)returns the faces in meters, for a world or forglb(). Default view:three.focus: 'fwing'turns the car to where that part shows, highlights it and dims the rest, for a shop or an editor;partView()gives you that camera.
import { vehicle, TYPES, PARTS } from '@firetower/genesis-lowpoly/vehicles';
vehicle({ type: 1, view: 'three', livery: ['#1d5fbf', '#f4f1ea', '#c8102e'], marks: [{ slot: 'number', text: '8' }] });
vehicle({ type: 0, view: 'iso', livery, parts: { nose: 1, fwing: 2 } });
vehicle({ type: 0, livery, focus: 'fwing' }); // one part, for a shop
PARTS[0][0]; // ['nose', 'Morro', ['Bajo', 'Alto', 'Aguja', …]]: [key, label, shapes]Librea, calcomanías y marcas
Livery, decals and marks
El motor no trae diseños de librea: pega los del juego.
decals: figuras en el espacio normalizado del vehículo, sobre las partes que se digan (body,cover,pods…).on: 'side'yon: 'top'son planas (band,slant,dotayudan a dibujarlas);on: 'space'(conslab) yon: 'dots'envuelven toda la carrocería sin cortes.paint: un color por pieza;PAINT[type]lista las pintables.marks: número y patrocinios en los espacios del tipo (MARK_SLOTS[type]), con texto en las letras del motor o con el logo del juego. El texto se achica, pasa a dos líneas y solo al final se corta; sincolor, el motor elige una tinta legible.markFit(type, slot, text)dice antes de mostrarlo si cabe;sponsorMarks(type, patrocinadores)reparte una lista por niveles (rank1 a 4) en los mejores espacios.
The engine ships no livery designs: it applies your game’s.
decals: shapes in the vehicle’s normalized space, on the parts you name (body,cover,pods…).on: 'side'andon: 'top'are flat (band,slantanddothelp draw them);on: 'space'(withslab) andon: 'dots'wrap the whole body without seams.paint: one color per part;PAINT[type]lists the paintable ones.marks: the number and sponsors in the type’s slots (MARK_SLOTS[type]), as text in the engine’s lettering or as your logo. Text shrinks, then wraps to two lines, and is cut only as a last resort; without acolor, the engine picks a legible ink.markFit(type, slot, text)tells you beforehand whether it fits;sponsorMarks(type, sponsors)places a tiered list (rank1 to 4) in the best slots.
import { vehicle, slab, band, markFit } from '@firetower/genesis-lowpoly/vehicles';
vehicle({ type: 0, livery: ['#1b2a41', '#f4f1ea', '#f5a524'],
decals: [
{ on: 'space', parts: ['body', 'cover', 'pods'], planes: slab(.30, .375, .12), color: '#f5a524' },
{ on: 'top', parts: ['body', 'cover'], shape: band(0, 1, -.12, .12), color: '#f4f1ea' }
],
marks: [
{ slot: 'number', text: '27', color: '#1b2230', plate: '#f4f1ea' },
{ slot: 'pods', text: 'Neumáticos Sur' }
] });
markFit(0, 'pods', 'Compañía Nacional de Lubricantes');
// { fits: true, lines: ['COMPAÑÍA NACIONAL', 'DE LUBRICANTES'], size: 0.4, shown: '…', cut: 'none' }Desgaste, daño, acabados y movimiento
Wear, damage, finishes and motion
wear(0–1): la pintura se apaga, la suciedad sube desde el piso y aparecen rayones.damage(0–1 por pieza,DAMAGE[type]): un golpe hunde la pieza en facetas y la ennegrece; alerones, parabrisas y escapes se caen desde 2/3; el tren motriz pierde aceite y humea, y la electrónica echa chispas. Con la mismaseed, el mismo daño.finish:carbon,matte,chromeogold, en toda la librea o por pieza. El cromo y el oro reflejan el cielo y destellan solos (glints: falselos deja quietos).speed(0–1) difumina los rayos; conanimate: truelas ruedas giran en el SVG, ytrail: truepone líneas de velocidad.
wear(0–1): the paint fades, grime rises from the floor and scratches appear.damage(0–1 per part,DAMAGE[type]): an impact dents the part into facets and blackens it; wings, windscreens and exhausts fall off from 2/3; the drivetrain leaks oil and smokes, and the electronics spark. The sameseedgives the same damage.finish:carbon,matte,chromeorgold, for the whole livery or per part. Chrome and gold reflect the sky and glint by themselves (glints: falsekeeps them still).speed(0–1) blurs the spokes; withanimate: truethe wheels spin inside the SVG, andtrail: trueadds speed lines.
vehicle({ type: 0, livery, wear: .6, damage: { body: .5, fwing: 1 }, seed: 7 });
vehicle({ type: 0, livery, finish: { body: 'carbon', rims: 'chrome' } });
vehicle({ type: 0, livery, speed: .7, animate: true, trail: true });Piezas sueltas, motores y sonido
Components, engines and sound
component({ type, system, … })dibuja un sistema solo y recortado a la pieza (SYSTEMS: aerodinámica, tren motriz, chasis, electrónica, motor), con la librea, las formas, el desgaste y el daño del auto. Se lee desde 64 px de alto.- Los motores (
ENGINES) son combinaciones de módulos: cilindros, bancadas, admisión, sobrealimentación y escape. Un juego puede pasar la suya,engine: { cylinders: 4, banks: 1 }, y pegarle su emblema (engineSlots()). - Sonido: la misma ficha que dibuja un motor lo hace sonar.
engineSound(motor, { context })lo sintetiza en vivo en unAudioWorklet, sin grabaciones, en el contexto de audio que crea el juego;engineSample()da una muestra en .wav (también en Node), yraceSound()hace sonar una carrera entera desde las posiciones de los autos.
component({ type, system, … })draws one system on its own, cropped to the part (SYSTEMS: aero, drivetrain, chassis, electronics, engine), with the car’s livery, shapes, wear and damage. It reads from 64 px tall.- Engines (
ENGINES) are combinations of modules: cylinders, banks, intake, boost and exhaust. A game can pass its own,engine: { cylinders: 4, banks: 1 }, and stick its emblem on it (engineSlots()). - Sound: the same spec that draws an engine makes it sound.
engineSound(engine, { context })synthesizes it live in anAudioWorklet, with no recordings, on the audio context your game creates;engineSample()returns a .wav sample (in Node too), andraceSound()voices a whole race from the cars’ positions.
import { component, engineSound, engineSample } from '@firetower/genesis-lowpoly/vehicles';
component({ type: 0, system: 'engine', engine: 2, view: 'three', livery }); // the V10
component({ type: 0, system: 'aero', livery, damage: .6, height: 96 });
const ctx = new AudioContext(); // created after the player's first tap
const v = engineSound(2, { context: ctx, type: 0 });
v.start(); v.set({ rev: .4, throttle: .3 }); v.blip();
const { wav } = engineSample(2, { type: 0, action: 'blip' }); // bytes of a .wavEdificios
Buildings
El juego construye: pone bloques en una rejilla, celda por celda y piso por piso, y building() hace con ellos un edificio, con el mismo kit 3D, la luz y la sombra que los vehículos.
- Cada bloque es
{ x, y, f, kind }: x al este, y al norte,fel piso (0, la planta baja). Una celda mideCELL×CELL(4 m) y un pisoFLOOR(3,2 m). kind(BLOCKS):room,glass,garage,solid,stand(tribuna) ycab(cabina de torre).win(WINDOWS) elige la ventana ydoor: 'S' | 'E' | 'N' | 'W'pone la puerta.roof(ROOFS): plano, a dos aguas o en dientes de sierra, para todo el edificio o por bloque.style: los colores de muros, marcos, vidrio, techo y puertas.signs: letreros en un muro o sobre el techo, con texto o logo;signFit()dice si caben.progress(0–1) dibuja el edificio en obra.- Vistas:
side,threeeiso(por defecto).
Your game builds: it places blocks on a grid, cell by cell and floor by floor, and building() turns them into a building, with the same 3D kit, light and shadow as vehicles.
- Each block is
{ x, y, f, kind }: x east, y north,fthe floor (0 is the ground floor). A cell isCELL×CELL(4 m) and a floor isFLOOR(3.2 m). kind(BLOCKS):room,glass,garage,solid,stand(grandstand) andcab(control tower cab).win(WINDOWS) picks the window anddoor: 'S' | 'E' | 'N' | 'W'places the door.roof(ROOFS): flat, gabled or sawtooth, for the whole building or per block.style: colors for walls, trim, glass, roof and doors.signs: signs on a wall or on the roof, with text or a logo;signFit()tells you whether they fit.progress(0–1) draws the building under construction.- Views:
side,threeandiso(the default).
import { building } from '@firetower/genesis-lowpoly/buildings';
building({ view: 'iso', style: { wall: '#f4f1ea', trim: '#c8102e' }, blocks: [
{ x: 0, y: 0, f: 0, kind: 'garage' }, { x: 1, y: 0, f: 0, kind: 'garage' },
{ x: 0, y: 0, f: 1, kind: 'room' }, { x: 1, y: 0, f: 1, kind: 'room', door: 'S' }
] });Planos y el edificio guardado
Blueprints and saved buildings
Un plano es un edificio del que el juego parte: sus bloques, su techo y su estilo, y si puede crecer, cuántas celdas de ancho. plan(id, { width, style, roof, view, … }) devuelve lo que toma building(), con el estilo del juego encima del del plano.
- Catálogo (
BLUEPRINTS, por familia enCATALOG): de carreras (pit-garages,race-control,grandstand,team-hq), de ciudad (house,shop,apartments,workshop), deportivos (bleachers,sports-hall,pavilion) y del estudio (fire-tower,studio-hq,arcade). - Planos propios con el kit (
…/buildings/kit):blueprint(id, nombre, { width, roof, style, plan })yblocksIn()para llenar una caja de celdas. - Para guardar:
encodeBuildingydecodeBuilding. Para editar,createEditor(BUILDING)con sus acciones:place,remove,dooryuse.
A blueprint is a building your game starts from: its blocks, roof and style, and, if it can grow, how many cells wide. plan(id, { width, style, roof, view, … }) returns what building() takes, with your style laid over the blueprint’s.
- Catalog (
BLUEPRINTS, by family inCATALOG): racing (pit-garages,race-control,grandstand,team-hq), city (house,shop,apartments,workshop), sport (bleachers,sports-hall,pavilion) and studio (fire-tower,studio-hq,arcade). - Your own blueprints with the kit (
…/buildings/kit):blueprint(id, name, { width, roof, style, plan }), andblocksIn()to fill a box of cells. - To save:
encodeBuildinganddecodeBuilding. To edit,createEditor(BUILDING)with its actions:place,remove,dooranduse.
import { building, BUILDING, encodeBuilding } from '@firetower/genesis-lowpoly/buildings';
import { plan, BLUEPRINTS } from '@firetower/genesis-lowpoly/packs';
import { createEditor } from '@firetower/genesis-lowpoly/editor';
building(plan('pit-garages', { width: 6, style: { trim: '#c8102e' }, view: 'three' }));
const ed = createEditor(BUILDING, { roof: 'flat', style: {}, blocks: BLUEPRINTS['house'].blocks() });
ed.act('place', 2, 0, 'garage').act('door', 2, 0, 'S').set('roof', 'saw');
const saved = ed.encode(); // 'building:1-…'Naturaleza
Nature
Lo que crece y lo que está tirado en el suelo, en metros. tree() da un SVG; las funciones …Model dan caras para un mundo.
- Árboles (
TREES):pine,leafy,poplar,palmybush, conheight,leavesybarkdel juego. Cada semilla, otro árbol. hedgeModel(trazado, { height }): un seto a lo largo de un trazado.rockModel({ size, seed }): una roca.grassModel(): una mata de pasto.meadowModel(contorno, { density, flowers }): una pradera con flores.- Proyectan su sombra y se iluminan con la hora. Vista por defecto:
iso.
Things that grow and things lying on the ground, in meters. tree() returns an SVG; the …Model functions return faces for a world.
- Trees (
TREES):pine,leafy,poplar,palmandbush, with yourheight,leavesandbark. Each seed gives another tree. hedgeModel(path, { height }): a hedge along a path.rockModel({ size, seed }): a rock.grassModel(): a tuft of grass.meadowModel(outline, { density, flowers }): a meadow with flowers.- They cast shadows and take the hour’s light. Default view:
iso.
import { tree, treeModel, hedgeModel, rockModel } from '@firetower/genesis-lowpoly/nature';
tree({ kind: 'leafy', seed: 2, leaves: '#c46a2b', view: 'three' }); // autumn
world({ view: 'iso', scale: 14, items: [
{ id: 't1', faces: treeModel({ kind: 'pine', seed: 1 }), at: [0, 0] },
{ id: 't2', faces: treeModel({ kind: 'leafy', seed: 2 }), at: [4, 2] },
{ id: 'rock', faces: rockModel({ size: 1.5, seed: 3 }), at: [3, -2] },
{ id: 'hedge', faces: hedgeModel([[-3, -4], [6, -4]], { height: 1.2 }) }
] }).svg;Criaturas
Creatures
Animales y bestias hechos por un creador, no uno por uno. Una criatura es una forma de cuerpo (PLANS: quad, biped, slug), sus proporciones y sus piezas (trompa, colmillos, cuernos, alas, crin, púas, placas, silla de montar…). Todo es dato: el motor no conoce ningún animal.
heightva en metros, a la cruz; lo demás, en alturas a la cruz, así una especie conserva su forma a cualquier tamaño.- Las especies vienen en el paquete
bestiary:species(id, opciones)da la ficha con lo del juego encima (mammoth,dragon,wolf,bear,bison,dog,horse,brute,snail,slug,mastiff,upright-bear,grub). style:toonoreal.seedvaría cada ejemplar dentro de su especie.creatureModel(spec)da sus caras para un mundo.
Animals and beasts made by a creator, not one by one. A creature is a body plan (PLANS: quad, biped, slug), its proportions and its parts (trunk, tusks, horns, wings, mane, spikes, plates, a saddle…). It is all data: the engine knows no animal.
heightis in meters, at the withers; everything else is in multiples of that height, so a species keeps its shape at any size.- Species come in the
bestiarypack:species(id, options)returns the spec with yours laid over it (mammoth,dragon,wolf,bear,bison,dog,horse,brute,snail,slug,mastiff,upright-bear,grub). style:toonorreal.seedvaries each individual within its species.creatureModel(spec)returns its faces for a world.
import { creature } from '@firetower/genesis-lowpoly/creatures';
import { species } from '@firetower/genesis-lowpoly/packs';
creature(species('mammoth', { style: 'real', view: 'three', seed: 7 }));
creature(species('dragon', { style: 'toon' }));
creature({ height: 1.2, legs: { length: .6 }, parts: { horns: { length: .4 } } }); // your ownAndares y jinetes
Gaits and riders
gait(GAITS):stand(respira y mece la cola),walk(paso de cuatro tiempos) ygallop.phase(0–1) da una pose;animate: truereproduce el andar en su sitio (frames, 12 por defecto).creatureGait(spec, gait)da el andar como dato.creatureSeat(spec)dice dónde va el jinete:{ at, seat }.creature({ rider })recibe las caras de una figura sentada (figureModel({ cycle: 'sit', seat })) y la pone en la silla.frame: 'portrait'da un 3:4 con la mitad delantera de la bestia, yauraun halo con su silueta.
gait(GAITS):stand(breathing, tail swaying),walk(a four-beat walk) andgallop.phase(0–1) gives a pose;animate: trueplays the gait in place (frames, 12 by default).creatureGait(spec, gait)returns the gait as data.creatureSeat(spec)tells you where the rider goes:{ at, seat }.creature({ rider })takes a seated figure’s faces (figureModel({ cycle: 'sit', seat })) and puts it in the saddle.frame: 'portrait'gives a 3:4 crop of the beast’s front half, andauraa halo in its silhouette.
import { creature, creatureSeat } from '@firetower/genesis-lowpoly/creatures';
import { figureModel } from '@firetower/genesis-lowpoly/figures';
import { species } from '@firetower/genesis-lowpoly/packs';
creature(species('wolf', { gait: 'walk', phase: .25 }));
creature(species('wolf', { gait: 'gallop', animate: true }));
const horse = species('horse', { view: 'three' });
creature({ ...horse, rider: figureModel({ cycle: 'sit', seat: creatureSeat(horse).seat, colors: ['#c8102e', '#f4f1ea'] }) });Emblemas y su creador
Emblems and their creator
Un emblema es un dato: una forma (o ninguna, shape: 'none'), un campo, un borde, un símbolo, un nombre con su letra y cuatro colores. emblem(spec, { size, aspect, bg, catalogs }) lo dibuja; emblemPolys() da los polígonos.
- El motor trae solo el redondo liso. Formas, divisiones y símbolos vienen en catálogos:
HERALDRY,HERALDRY_PLUSyFANTASY_HERALDRY. Cuáles son gratis o de pago lo decide el juego. emblemMaker(...catálogos)es el generador:controls,draw,logo(polígonos para las marcas de un vehículo o el letrero de un edificio),encode,decode,normalizeyroll. Se edita concreateEditor().- El texto guarda ids, no posiciones: un emblema guardado no cambia cuando crecen los catálogos.
- Se lee en pequeño: por debajo de 26 px suelta las letras si tiene símbolo;
emblemFit(spec, px)dice qué queda.
An emblem is data: a shape (or none, shape: 'none'), a field, a border, a symbol, a name with its typeface, and four colors. emblem(spec, { size, aspect, bg, catalogs }) draws it; emblemPolys() returns the polygons.
- The engine ships only the plain round shape. Shapes, divisions and symbols come in catalogs:
HERALDRY,HERALDRY_PLUSandFANTASY_HERALDRY. Your game decides which are free and which are paid. emblemMaker(...catalogs)is the generator:controls,draw,logo(polygons for a vehicle’s marks or a building’s sign),encode,decode,normalizeandroll. Edit it withcreateEditor().- Saved strings hold ids, not positions, so a saved emblem does not change as catalogs grow.
- It reads small: below 26 px it drops the letters if it has a symbol;
emblemFit(spec, px)tells you what remains.
import { emblemMaker } from '@firetower/genesis-lowpoly/brand';
import { HERALDRY, HERALDRY_PLUS } from '@firetower/genesis-lowpoly/packs';
import { createEditor } from '@firetower/genesis-lowpoly/editor';
const EMBLEM = emblemMaker(HERALDRY, HERALDRY_PLUS); // the catalogs your game offers
const ed = createEditor(EMBLEM);
ed.set('shape', 'shield').set('symbol', 'bolt').set('letters', 'gl');
EMBLEM.draw(ed.get(), { size: 64 });
EMBLEM.encode(ed.get()); // 'emblem:1/shield/plain/bolt/GL/…'
// a sponsor mark with no shield, 3 × 1, on a car
const volta = EMBLEM.normalize({ shape: 'none', symbol: 'bolt', letters: 'Volta', layout: 'row', font: 'narrow' });
vehicle({ type: 0, livery, marks: [{ slot: 'pods', logo: EMBLEM.logo(volta, { aspect: 3 }) }] });Logos, placas y logotipos
Logos, nameplates and wordmarks
- Isotipos.
mark(MARKS:circles,monogram,arcs,facets,petals,strokes) yseeddan una marca abstracta, siempre igual para la misma semilla. Con el nombre al lado (layout: 'row'o'stack') es un imagotipo; dentro de una figura (capsule,ribbon…), un isologo.isotype(gen, seed, colores)da la marca suelta. - Placas de nombre.
nameplate(texto, { w, h, style, colors, font, badge }): un marco alrededor de un nombre en cuatro acabados (PLATE_STYLES:plain,metal,enamel,champion). El nombre siempre cabe. - Logotipos de título.
wordmark({ runs, tagline, streak, helix, layout }): un nombre en letras gruesas e inclinadas, por tramos de color, sobre una calcomanía; composicioneslockup,compact,stackedeicon(WORDMARK_LAYOUTS). banner()dibuja un estandarte de tela colgado de una vara, con cualquier SVG del motor como carga.
- Isotypes.
mark(MARKS:circles,monogram,arcs,facets,petals,strokes) plusseedgive an abstract mark, always the same for the same seed. With the name beside it (layout: 'row'or'stack') it becomes an imagotype; inside a shape (capsule,ribbon…), an isologo.isotype(gen, seed, colors)returns the bare mark. - Nameplates.
nameplate(text, { w, h, style, colors, font, badge }): a frame around a name in four finishes (PLATE_STYLES:plain,metal,enamel,champion). The name always fits. - Title wordmarks.
wordmark({ runs, tagline, streak, helix, layout }): a name in heavy slanted letters, in colored runs, on a sticker outline; layoutslockup,compact,stackedandicon(WORDMARK_LAYOUTS). banner()draws a cloth banner hanging from a pole, with any engine SVG as its charge.
import { emblem, nameplate, wordmark } from '@firetower/genesis-lowpoly/brand';
emblem({ shape: 'none', mark: 'circles', seed: 12, letters: 'NOVA', layout: 'row' }, { size: 48, aspect: 3 });
nameplate('Ana Ribeiro', { w: 240, h: 56, style: 'metal', colors: ['#8a9099', '#1b2230', '#d4af37'] });
wordmark({
runs: [{ text: 'Sp', fill: ['#ffe14a', '#ff8a1e'] }, { text: 'eed', lower: true, fill: ['#ffe14a', '#ff8a1e'] }, { text: 'UP', fill: ['#8af6ff', '#16b2e0'] }],
tagline: 'GO, GO, GO', streak: true, helix: true
});Pósters
Posters
Hojas al estilo del cine de los años dorados: bandas diagonales, torres art déco, un abanico de rayos y un título en letras de bloque con relieve. La imagen dice qué es; lo que la explica va en la interfaz del juego.
poster({ palette, title, art, fan, towers, confetti, tier, w, h })(en…/brand) pone cualquier dibujo del motor en un recuadro de la hoja de 600 × 900, o recortado en una ventana en arco.posterBackdrop()yposterTitle()dan solo el fondo o solo el título, a cualquier tamaño.tier:bronze,silver,goldoplatinumcambian el marco por escalón.- El paquete de carreras trae 66 recetas de logros (
POSTERS):achievementPoster(id, datos)las dibuja con el auto, la cara y los colores de cada jugador.
One-sheets in the style of golden-age movie posters: diagonal bands, art deco towers, a fan of rays and a title in raised block letters. The picture says what it is; anything that explains it belongs in your game’s UI.
poster({ palette, title, art, fan, towers, confetti, tier, w, h })(in…/brand) places any engine drawing in a box of the 600 × 900 sheet, or crops it to an arched window.posterBackdrop()andposterTitle()give only the background or only the title, at any size.tier:bronze,silver,goldorplatinumchange the frame per step.- The racing pack carries 66 achievement recipes (
POSTERS):achievementPoster(id, data)draws one with each player’s car, face and colors.
import { achievementPoster } from '@firetower/genesis-lowpoly/packs';
import { posterBackdrop, POSTER_PALETTES } from '@firetower/genesis-lowpoly/brand';
achievementPoster('winner', { car: carSpec, colors: ['#c8102e', '#f4f1ea'], tier: 'gold' });
posterBackdrop({ w: 300, h: 300, palette: POSTER_PALETTES.teal }); // a square card's backgroundAmbiente: cielo y paisaje
Ambience: sky and landscape
scene(opciones) compone el cielo de una hora, un paisaje por capas y lo que el juego ponga delante, en un SVG de w × h (400 × 300 por defecto).
light(otime) yweather(WEATHERS:clear,cloudy,overcast,storm).sky: [arriba, abajo]pinta un cielo propio.land(LANDS:plain,hills,mountains,city,coast) olayerspropias (LAYERS: campo, colinas, montañas, árboles, bosque, rocas, agua, ciudad, castillo, río, niebla…), cada una con su distancia (depth, 1 en el horizonte).content: cualquier SVG, en una caja ({ svg, x, y, w, h }) o de pie en el paisaje ({ svg, x, depth }), más pequeño cuanto más lejos. Dibuja lo de dentro conscalepara que guarde su tamaño.
scene(options) composes the sky of an hour, a layered landscape and whatever your game puts in front, in a w × h SVG (400 × 300 by default).
light(ortime) andweather(WEATHERS:clear,cloudy,overcast,storm).sky: [top, bottom]paints a custom sky.land(LANDS:plain,hills,mountains,city,coast) or your ownlayers(LAYERS: field, hills, mountains, trees, forest, rocks, water, skyline, castle, river, mist…), each at its distance (depth, 1 at the horizon).content: any SVG, in a box ({ svg, x, y, w, h }) or standing in the landscape ({ svg, x, depth }), smaller the farther it is. Draw what goes inside withscaleso it keeps its size.
import { daylight } from '@firetower/genesis-lowpoly/daylight';
import { scene } from '@firetower/genesis-lowpoly/scenery';
const L = daylight(17);
scene({ light: L, w: 400, h: 220, land: 'hills', content: [
{ svg: building(plan('house', { light: L, scale: 6, bg: 'none' })), x: 150, depth: .3 },
{ svg: vehicle({ type: 3, livery, light: L, scale: 6, bg: 'none' }), x: 260, depth: .1 }
] });
scene({ time: 22, land: 'city', w: 400, h: 220 });world(): muchas cosas en un lugar
world(): many things in one place
world() pone edificios, vehículos, figuras, naturaleza y modelos propios en una cámara, una escala, un suelo y una luz. No sabe de generadores: recibe caras en metros y dónde va cada cosa.
items:{ id, faces, at, turn }.ates[x, y]o[x, y, z]para ponerlo más alto (alguien sobre un peralte);turn, en grados.view(WORLD_VIEWS,isopor defecto),scale(10 px/m por defecto),light,bgymargin.floor: { from, to, color }(ofalse) es la base;ground, la lista de piezas planas del suelo, pintadas en su orden debajo de toda sombra y toda cosa.- Cada cosa se pinta entera, de lejos a cerca, según su caja en el mundo, sin importar el orden de la lista; lo que se entrelaza (una pluma de grúa sobre un techo) se pinta junto, pieza por pieza.
bounds: { from, to, height }fija el encuadre.
world() puts buildings, vehicles, figures, nature and your own models into one camera, one scale, one ground and one light. It knows no generator: it takes faces in meters and where each thing goes.
items:{ id, faces, at, turn }.atis[x, y], or[x, y, z]to raise it (someone on a banking);turnis in degrees.view(WORLD_VIEWS,isoby default),scale(10 px/m by default),light,bgandmargin.floor: { from, to, color }(orfalse) is the base;groundis the list of flat ground pieces, painted in order beneath every shadow and every thing.- Each thing is painted whole, far to near, by its box in the world, whatever its order in the list; things that interlock (a crane jib over a roof) are painted together, piece by piece.
bounds: { from, to, height }fixes the framing.
import { world } from '@firetower/genesis-lowpoly/world';
import { road } from '@firetower/genesis-lowpoly/ground';
import { piece, plan } from '@firetower/genesis-lowpoly/packs';
const w = world({ view: 'iso', scale: 6, light: daylight(21.5),
floor: { from: [-10, -12], to: [24, 14] },
ground: [road([[-10, -9.5], [24, -9.5]], { width: 7 })],
items: [
{ id: 'shop', faces: buildingModel(plan('shop', {})), at: [0, 0] },
{ id: 'lamp', faces: piece('street-lamp', { height: 6 }), at: [8, -6], turn: -90 },
{ id: 'car', faces: model({ type: 3, livery }), at: [14, -10] }
] });
w.svg; // each item in its own <g data-id="…">Lo que se mueve
Things that move
Un mundo puede animarse solo, en el SVG, sin redibujarlo:
- En su sitio (un ciclo, un equipo trabajando): el objeto da
frames: [caras, …]ydurationen segundos en vez defaces. Lo quieto se dibuja una sola vez. - Por un camino (alguien que pasea): el objeto da
track: [{ frames, duration, phase, from, to, turn, time }, …]yoffset. Cada pose se dibuja una vez y el SVG la desliza defromatoentimesegundos; su sombra va con ella.figureTrack()lo arma para una figura. - Lo que gira (una noria): un
rotordibuja una vez lo que gira y el SVG lo hace girar en su plano.
A world can animate itself inside the SVG, without redrawing:
- In place (a cycle, a crew at work): the item gives
frames: [faces, …]and adurationin seconds instead offaces. Everything still is drawn once. - Along a path (someone strolling): the item gives
track: [{ frames, duration, phase, from, to, turn, time }, …]and anoffset. Each pose is drawn once and the SVG slides it fromfromtotoovertimeseconds, shadow included.figureTrack()builds one for a figure. - Things that turn (a Ferris wheel): a
rotordraws the turning part once and the SVG spins it in its own plane.
const frames = [0, .25, .5, .75].map((phase) => figureModel({ cycle: 'skate', phase, colors: ['#e65d00', '#17191d'] }));
world({ view: 'three', scale: 26, items: [
{ id: 'skater', frames, duration: PERIOD.skate } // loops in place
] }).svg;Tocar y mover
Picking and moving
El mundo le dice al juego dónde cayó cada cosa, para que la toque o la mueva:
w.items:[{ id, box, outline, svg }]en el orden en que se pintan;boxes[x0, y0, x1, y1]en píxeles youtline, su contorno.w.pick(x, y): el id de la cosa más cercana bajo ese punto del dibujo, onull.w.ground(x, y): el punto del suelo, en metros, bajo ese punto del dibujo (nullen la vista de perfil, que no ve suelo).w.at([x, y, z]): dónde cae un punto del mundo en el dibujo.
Para mover algo, cambia su at y vuelve a llamar a world(), o cambia solo el grupo <g data-id> de esa cosa.
The world tells your game where each thing landed, so players can touch it or move it:
w.items:[{ id, box, outline, svg }]in paint order;boxis[x0, y0, x1, y1]in pixels andoutlineits silhouette.w.pick(x, y): the id of the nearest thing under that point of the drawing, ornull.w.ground(x, y): the ground point, in meters, under that point of the drawing (nullin the side view, which sees no ground).w.at([x, y, z]): where a world point lands in the drawing.
To move something, change its at and call world() again, or update only that thing’s <g data-id> group.
const w = world({ view: 'iso', scale: 10, items });
svgEl.addEventListener('click', (e) => {
const id = w.pick(e.offsetX, e.offsetY); // 'car7' or null
if (!id) return;
const spot = w.ground(e.offsetX, e.offsetY); // [x, y] in meters
moveItem(id, spot); // your game updates its at
});Capas e imágenes apiladas
Layers and stacked images
Cuando el mundo es grande y solo se mueven unas pocas cosas, conviene pintarlo por partes:
w.base: el piso, lo plano y todas las sombras, en un SVG del tamaño del mundo.w.layers: cada capa quieta{ ids, svg, box, lo, hi }, recortada a su recuadro, en orden de pintado.- Lo que se mueve se dibuja aparte con
w.sprite(caras, { turn }), con la misma cámara, escala y luz.w.slot({ lo, hi })dice antes de qué capa va, yw.order([{ id, lo, hi }, …])ordena varias a la vez. world({ split: true })(o{ split: { max: 40 } }) da ademásstack: imágenes en el orden exacto de pintado y en los mismos píxeles que el SVG entero, con lo que se mueve en imágenes aparte (moving).
When the world is large and only a few things move, paint it in parts:
w.base: the floor, the flat pieces and every shadow, in an SVG the size of the world.w.layers: each still layer{ ids, svg, box, lo, hi }, cropped to its box, in paint order.- Moving things are drawn separately with
w.sprite(faces, { turn }), with the same camera, scale and light.w.slot({ lo, hi })tells you which layer it goes before, andw.order([{ id, lo, hi }, …])sorts several at once. world({ split: true })(or{ split: { max: 40 } }) also returnsstack: images in the exact paint order and in the same pixels as the full SVG, with moving things in images of their own (moving).
const w = world({ view: 'iso', scale: 10, items: town });
const s = w.sprite(figureModel({ cycle: 'walk', phase }), { turn: 90 });
// { svg, w, h, ax, ay, lo, hi }: put it at w.at([x, y, 0]) minus (ax, ay)
const [px, py] = w.at([x, y, 0]);
const before = w.slot({ lo: [x + s.lo[0], y + s.lo[1], s.lo[2]], hi: [x + s.hi[0], y + s.hi[1], s.hi[2]] });
// draw w.base, then w.layers[0 … before - 1], the sprite, then the restEl suelo
The ground
Lo plano va en ground, en el orden que dé el juego; lo que se levanta (vallas, portones) va en items. Todo en metros, desde …/ground.
surface(contorno, material): una zona lisa.soil(contorno, material, { seed }): la misma en facetas, con su detalle (rayas del césped, piedritas, charcos).cracks(at, opciones): grietas.- Materiales (
MATERIALS): pasto, asfalto, acera, tierra, arena, gravilla, césped, barro, arcilla, tierra seca, roca, nieve, musgo, asfalto de pista, agua, parqué y piedra pulida, o un color del juego. road(trazado, { width, pavement, line, kerb, closed }): calles con su línea y sus aceras.smooth(trazado, radio)redondea esquinas.footpath(): senderos.parking(esquina, { spaces, rows }): parqueaderos.water(contorno, { edge }): agua quieta con el cielo dentro.court(centro, { sport }): canchas.fence(trazado, { style })ygate(ancho, { open }): vallas y portones, como cosas del mundo.
Flat things go in ground, in the order your game gives; things that stand up (fences, gates) go in items. All in meters, from …/ground.
surface(outline, material): a plain area.soil(outline, material, { seed }): the same in facets, with detail (mowing stripes, pebbles, puddles).cracks(at, options): cracks.- Materials (
MATERIALS): grass, asphalt, pavement, dirt, sand, gravel, lawn, mud, clay, dry earth, rock, snow, moss, race asphalt, water, parquet and polished stone, or a color of your own. road(path, { width, pavement, line, kerb, closed }): streets with their lines and sidewalks.smooth(path, radius)rounds corners.footpath(): footpaths.parking(corner, { spaces, rows }): parking lots.water(outline, { edge }): still water with the sky in it.court(center, { sport }): sports courts.fence(path, { style })andgate(width, { open }): fences and gates, as world items.
import { surface, road, parking, footpath, fence } from '@firetower/genesis-lowpoly/ground';
world({ view: 'iso', scale: 7,
ground: [
surface([[-12, -8], [20, -8], [20, 14], [-12, 14]], 'grass'),
road([[-12, -4], [20, -4]], { width: 7, pavement: 2 }),
parking([0, 4], { spaces: 5, rows: 2 }),
footpath([[-10, 12], [-6, 6], [-2, 2]], { width: 1.4, seed: 3 })
],
items: [{ id: 'fence', faces: fence([[12, 2], [12, 12], [19, 12]]) }]
}).svg;Circuitos
Race tracks
road(trazado, { material: 'race' }): asfalto de pista con su grano, la trazada oscura de goma, marcas de frenada antes de cada curva y parches, todo sacado de la curvatura del trazado.kerb: truepone pianos rojos y blancos ystart, la línea de salida.grid: { slots, first, spacing }enroad(), ostartGrid()aparte, pinta la parrilla;gridSlots(trazado, opciones)dice dónde para cada auto (at,anglepara elturndel mundo).gravel(borde, { width, side }): trampas de grava.tyreWall(),jerseyWall()ycatchFence(): barreras, cosas del mundo.parkedCars()llena un parqueadero.- Una vuelta de un kilómetro con todo pesa unos 25 KB y se lee entera a 1,5 px/m.
road(path, { material: 'race' }): race asphalt with grain, a dark rubbered racing line, braking marks before each bend and patches, all derived from the path’s curvature.kerb: trueadds red-and-white kerbs andstartthe start line.grid: { slots, first, spacing }inroad(), orstartGrid()on its own, paints the starting grid;gridSlots(path, options)tells you where each car stops (at, andanglefor the world’sturn).gravel(edge, { width, side }): gravel traps.tyreWall(),jerseyWall()andcatchFence(): barriers, as world items.parkedCars()fills a parking lot.- A one-kilometer lap with everything weighs about 25 KB and reads whole at 1.5 px/m.
import { road, smooth, gridSlots } from '@firetower/genesis-lowpoly/ground';
const lap = smooth([[0, 0], [60, 0], [60, 30], [0, 30]], 8);
world({ view: 'top', scale: 2, ground: [
road(lap, { material: 'race', width: 8, closed: true, kerb: true, line: false, start: 6 })
] }).svg;
gridSlots(straight, { start: 30, slots: 2 });
// [{ place: 1, side: 'left', at: [13.5, 3], dir: [1, 0], angle: 0 }, …]Piezas y paquetes
Pieces and packs
Los paquetes traen piezas en 3D para el mundo, hechas con el kit y en metros. piece(id, opciones) da sus caras; PIECES las lista por id y PROPS por familia.
- Familias:
construction(grúa torre, excavadora, conos, andamio),lighting(farolas y focos que se encienden de noche),racing,civic,park,leisure,sport,museum,commerce,fair,airport,facility,resort,homeylink(pasarelas entre edificios). - Muchas tienen partes que se mueven por opción: la pluma de la grúa gira (
swing), su carro corre (reach) y su gancho baja (drop). - Para los rostros, el mismo patrón:
ITEMSlista las prendas por id ywear(id, colores)las pone. Cada pieza también se puede importar sola:…/packs/<paquete>/<id>.
Packs ship 3D pieces for the world, built with the kit and in meters. piece(id, options) returns their faces; PIECES lists them by id and PROPS by family.
- Families:
construction(tower crane, excavator, cones, scaffolding),lighting(street lamps and floodlights that turn on at night),racing,civic,park,leisure,sport,museum,commerce,fair,airport,facility,resort,homeandlink(walkways between buildings). - Many have parts you can pose through options: the crane’s jib swings (
swing), its trolley runs (reach) and its hook drops (drop). - Faces follow the same pattern:
ITEMSlists garments by id andwear(id, colors)puts them on. Each piece can also be imported on its own:…/packs/<pack>/<id>.
import { piece, PROPS } from '@firetower/genesis-lowpoly/packs';
world({ view: 'iso', scale: 9, items: [
{ id: 'crane', faces: piece('tower-crane', { height: 24, jib: 22, swing: 40, reach: .6, drop: 10 }), at: [8, 4] },
{ id: 'digger', faces: piece('excavator', { swing: -30, boom: 40, arm: 75 }), at: [0, -6] },
{ id: 'cone', faces: piece('traffic-cone'), at: [-4, -9] }
] }).svg;
PROPS.lighting.map((p) => p.id); // ['street-lamp', 'floodlight']Escenas para pantallas
Ready-made screens
Funciones de paquete que devuelven una pantalla terminada como { svg, w, h, … }, con datos en píxeles del SVG para que el juego ponga encima sus textos, caras y toques. Ninguna lleva texto escrito.
homeRoom({ home, level, hour, glow, on, highlight, trophies, me }): el cuarto de un jugador como diorama.hitsda el contorno de cada cosa que se toca, con ids fijos (HOME_IDS:bed,phone,pc,window…).gym: [...]pone el gimnasio que el jugador tiene, cada pieza con su propio id enhits(GYM_IDS:dumbbells,band,treadmill,yoga-mat,bike).pieceIcon(id, { size, bg })dibuja cualquier pieza sola en un cuadrado, para una tienda o un inventario. El paquetefoodtrae consumibles a tamaño real para eso:isotonic-drink,water-bottle,fruit(kind:apple,banana,orange),arepa,smoothieyprotein-bar.rewardChest({ rarity, state, animate, autoplay, uid, size }): un cofre de premio en una deCHEST_RARITIES(common,rare,epic,legendary), cerrado o abierto. Conanimate: truetrae su apertura: tiembla, la tapa se abre de golpe y sale la luz de su rareza. Devuelvephasesydurationen segundos,anchor(dónde dibujar el premio, en px) yhit(su contorno). Conautoplay: false, el juego la arranca consvg.getElementById(uid + '-go').beginElement().- El equipo de un patinador en niveles (paquete
gear):speed-skate,speed-helmetyskinsuit-gear, contierde 0 a 3 ycolorsdel equipo, parapieceIcon. En el retrato,wear('skate-helmet', colores, { parts: { tier } })y lo mismo conskinsuit. awardsScene({ award, nominees, winner, phase }): una gala de premios.anchorstraetitle,slots(foto y placa de cada nominado, y si está encendida) ytrophy.podiumScene({ drivers }): el podio con tres pilotos y sus avatares;anchorspor si el juego pone los suyos.cabinetScene({ items, cols }): una vitrina de trofeos (TROPHIES) de frente, conslots.- Interiores con
anchors:simulatorRoom,windTunnelRoom,hospitalityShop,hospitalityVip,hospitalityPhotosylogisticsRoom.
Pack functions that return a finished screen as { svg, w, h, … }, with data in the SVG’s pixels so your game can lay its text, faces and hit areas on top. None of them contain written text.
homeRoom({ home, level, hour, glow, on, highlight, trophies, me }): a player’s bedroom as a diorama.hitsgives the outline of every tappable thing, under fixed ids (HOME_IDS:bed,phone,pc,window…).gym: [...]adds the home gym the player owns, each piece with its own id inhits(GYM_IDS:dumbbells,band,treadmill,yoga-mat,bike).pieceIcon(id, { size, bg })draws any piece alone in a square, for a shop or an inventory. Thefoodpack brings life-size consumables for that:isotonic-drink,water-bottle,fruit(kind:apple,banana,orange),arepa,smoothieandprotein-bar.rewardChest({ rarity, state, animate, autoplay, uid, size }): a reward chest in one ofCHEST_RARITIES(common,rare,epic,legendary), shut or open. Withanimate: trueit carries its opening: it shakes, the lid bursts open and its rarity’s light pours out. It returnsphasesanddurationin seconds,anchor(where to draw the prize, in px) andhit(its outline). Withautoplay: false, the game starts it withsvg.getElementById(uid + '-go').beginElement().- A skater’s gear in tiers (the
gearpack):speed-skate,speed-helmetandskinsuit-gear, withtier0 to 3 and the team’scolors, forpieceIcon. In a portrait,wear('skate-helmet', colors, { parts: { tier } }), and the same withskinsuit. awardsScene({ award, nominees, winner, phase }): an awards ceremony.anchorsholds thetitle, theslots(each nominee’s photo and plate, and whether it is lit) and thetrophy.podiumScene({ drivers }): the podium with three drivers and their avatars;anchorsin case you place your own.cabinetScene({ items, cols }): a trophy cabinet (TROPHIES) seen from the front, withslots.- Interiors with
anchors:simulatorRoom,windTunnelRoom,hospitalityShop,hospitalityVip,hospitalityPhotosandlogisticsRoom.
import { homeRoom, awardsScene } from '@firetower/genesis-lowpoly/packs';
const room = homeRoom({ level: 2, hour: 20, glow: ['phone'] });
room.hits.phone; // [[x, y], …]: the phone's outline in the SVG's pixels
const gala = awardsScene({ award: 'driver-year', nominees: 3, winner: 1, phase: 'winner' });
gala.anchors.slots[0]; // { x, y, size, w, h, lit, plate: { x, y, w, h } }Lugares para un mundo
Places for a world
Otras funciones de paquete arman un lugar entero y devuelven sus partes para world(): { ground, items, … }, más datos para el juego (dónde se sienta la gente, por dónde se camina).
| Función | Lugar y datos extra |
|---|---|
museumHall({ level }) | Sala de museo por niveles: cars, statues, aisle |
shoppingMall() | Centro comercial: slots, door |
fair() | Feria con noria: walk, spots |
airport() | Aeropuerto: stands, routes, takeoff, landing |
park({ level }) | Parque: seats, stage |
spa() | Spa con jacuzzi: seats |
publicPool() | Piscina con carriles: lanes, seats |
Todas aceptan at y turn para ponerlas donde vayan en el mapa del juego, y colors.
Other pack functions build a whole place and return its parts for world(): { ground, items, … }, plus data for your game (where people sit, where they can walk).
| Function | Place and extra data |
|---|---|
museumHall({ level }) | Museum hall by level: cars, statues, aisle |
shoppingMall() | Shopping mall: slots, door |
fair() | Funfair with a Ferris wheel: walk, spots |
airport() | Airport: stands, routes, takeoff, landing |
park({ level }) | Park: seats, stage |
spa() | Spa with a hot tub: seats |
publicPool() | Pool with lanes: lanes, seats |
All of them take at and turn to place them on your game’s map, and colors.
import { museumHall } from '@firetower/genesis-lowpoly/packs';
const hall = museumHall({ level: 2 });
world({ view: 'iso', scale: 7, ground: hall.ground, items: hall.items }).svg;La pista peraltada
The banked track
piece('banked-track', { length, radius, width, bank, lanes, colors, stands, cover }) es un óvalo de 200 m sobre la línea de carrera, con las curvas peraltadas y las rectas planas, carriles, meta, valla y gradas (stands), y un techo si se pide (cover).
BANKED_TRACKse importa de…/packs/sport/banked-track.BANKED_TRACK.line(t, carril)da dónde va alguien a la fraccióntde la vuelta:[x, y, z, rumbo], conzsu altura sobre el peralte.BANKED_TRACK.racer(t, carril, { at, turn })da lo mismo listo para un objeto del mundo:{ at: [x, y, z], turn }.- Para que las sombras caigan sobre la pista, su suelo va como capa:
ground: [BANKED_TRACK.deck({ at, turn })]y la pieza condeck: false.
piece('banked-track', { length, radius, width, bank, lanes, colors, stands, cover }) is a 200 m oval on the racing line, with banked turns and flat straights, lanes, a finish line, a fence and stands (stands), plus a roof on request (cover).
- Import
BANKED_TRACKfrom…/packs/sport/banked-track. BANKED_TRACK.line(t, lane)tells you where a racer is at fractiontof the lap:[x, y, z, heading], withzits height on the banking.BANKED_TRACK.racer(t, lane, { at, turn })returns the same, ready for a world item:{ at: [x, y, z], turn }.- For racers’ shadows to fall on the track, pass its surface as a layer:
ground: [BANKED_TRACK.deck({ at, turn })], and the piece withdeck: false.
import { piece } from '@firetower/genesis-lowpoly/packs';
import { BANKED_TRACK } from '@firetower/genesis-lowpoly/packs/sport/banked-track';
world({ view: 'iso', scale: 4,
ground: [BANKED_TRACK.deck({})],
items: [
{ id: 'track', faces: piece('banked-track', { deck: false }) },
Object.assign({ id: 'r1', faces: figureModel({ cycle: 'skate', phase: .2 }) }, BANKED_TRACK.racer(.3, 1))
] }).svg;El kit 3D: modelos propios
The 3D kit: your own models
Todo lo que tiene volumen en Genesis está hecho con el kit de …/mesh (también en …/vehicles/kit). Un modelo es una lista de caras planas en metros; cualquier cámara lo proyecta con la misma luz que el resto.
- Piezas:
box(x0, x1, y0, y1, z0, z1, color);loft(secciones, color), una carrocería por secciones[x, zAbajo, zArriba, medioAncho];rod(a, b, medio, color), una barra entre dos puntos;frustum(),wheel(),sweep(),dome()yshadow(). part(caras, color)cierra un conjunto de caras como una pieza convexa: el pintor ordena por pieza, así que cada pieza debe ser convexa.place(caras, at, turn)mueve y gira una copia.tag(nombre, caras)la nombra, para pintarla o pegarle calcomanías.- Para dibujar: pásalo a
world(), avehicle({ model })o aglb().render(caras, vista, w, h, margen)devuelve solo los polígonos, sin la etiqueta<svg>.
Everything with volume in Genesis is built with the kit in …/mesh (also exposed as …/vehicles/kit). A model is a list of flat faces in meters; any camera projects it with the same light as everything else.
- Pieces:
box(x0, x1, y0, y1, z0, z1, color);loft(sections, color), a body through sections[x, zBottom, zTop, halfWidth];rod(a, b, half, color), a bar between two points;frustum(),wheel(),sweep(),dome()andshadow(). part(faces, color)closes a set of faces into one convex piece. The painter sorts by piece, so every piece must be convex.place(faces, at, turn)moves and turns a copy.tag(name, faces)names it, so it can be painted or take decals.- To draw it, pass it to
world(),vehicle({ model })orglb().render(faces, view, w, h, margin)returns only the polygons, without the<svg>tag.
import { box, rod, frustum, wheel, shadow } from '@firetower/genesis-lowpoly/mesh';
// a kiosk, in meters
const kiosk = [].concat(
box(-1, 1, -1, 1, 0, 2.2, '#2a6fdb'),
box(-1.3, 1.3, -1.3, 1.3, 2.2, 2.45, '#f4f1ea'),
rod([1.6, -1, 0], [1.6, -1, 3], .05, '#1b2a41'),
frustum(8, .35, 0, 2.45, 3.1, '#c8102e')
);
world({ view: 'three', scale: 40, items: [{ id: 'kiosk', faces: kiosk }] }).svg;
// a vehicle of your own, with the vehicle cameras and livery
vehicle({ view: 'three', livery: ['#6b8e23'], model: (lv) => box(0, 2, -.8, .8, .3, 1.4, lv[0])
.concat(wheel(.4, -.85, .3, .2, '#1d1f24', '#9aa3ad'), wheel(1.6, -.85, .3, .2, '#1d1f24', '#9aa3ad'), shadow(-.1, 2.1, -1, 1)) });Exportar a Godot, Unity o Blender
Export to Godot, Unity or Blender
glb(caras, { name }) de …/gltf convierte cualquier modelo de Genesis en un glTF binario (Uint8Array), para que un motor 3D lo ilumine, le dé sombras y lo haga chocar. Son las mismas caras de las que sale el SVG, en metros.
- Sirve con
model(),buildingModel(),figureModel(),creatureModel(),treeModel(),piece()o un modelo propio. - Polígonos planos de color, como se dibujan: un material por color; lo que brilla de noche, emisivo, y una calcomanía translúcida, mezclada.
- Queda fuera lo que solo sirve al dibujo: las sombras pintadas, los charcos de luz, las líneas de velocidad y el humo.
- Los ejes pasan de los de Genesis (x adelante, y izquierda, z arriba) a los de glTF (y arriba): el modelo mira hacia +z.
glb(faces, { name }) from …/gltf turns any Genesis model into a binary glTF (Uint8Array), so a 3D engine can light it, cast real shadows and collide with it. These are the same faces the SVG is drawn from, in meters.
- It works with
model(),buildingModel(),figureModel(),creatureModel(),treeModel(),piece()or a model of your own. - Flat colored polygons, as drawn: one material per color; what glows at night is emissive, and a translucent decal is blended.
- What only a drawing needs is left out: painted shadows, pools of light, speed lines and smoke.
- Axes are converted from Genesis (x forward, y left, z up) to glTF (y up): the model faces +z.
import { writeFileSync } from 'node:fs';
import { glb } from '@firetower/genesis-lowpoly/gltf';
import { model } from '@firetower/genesis-lowpoly/vehicles';
import { creatureModel } from '@firetower/genesis-lowpoly/creatures';
import { species } from '@firetower/genesis-lowpoly/packs';
writeFileSync('car.glb', glb(model({ type: 0, livery }), { name: 'car' }));
writeFileSync('mammoth.glb', glb(creatureModel(species('mammoth')), { name: 'mammoth' }));El kit de sonido
The sound kit
…/audio es el kit de sonido compartido. Un sonido es un DSP: una función que se basta sola y convierte parámetros en muestras. Corre en Node sin tiempo real, o en vivo en el contexto de audio del juego.
renderSound(dsp, params, { seconds, sampleRate, script }): el DSP sin tiempo real, como[izquierda, derecha].wav(canales, sampleRate): muestras a un archivo .wav de 16 bits (Uint8Array).soundNode(context, dsp, params): el DSP sonando en unAudioWorklet, conoutput,send(),state()yclose().soundBus(context, { volume, muted }): un volumen general, un silencio y un limitador suave para todo lo que suena.
El juego crea el AudioContext después del primer toque del jugador: el navegador no suena antes.
…/audio is the shared sound kit. A sound is a DSP: a self-contained function that turns parameters into samples. It runs offline in Node, or live on your game’s audio context.
renderSound(dsp, params, { seconds, sampleRate, script }): runs the DSP offline, returning[left, right].wav(channels, sampleRate): samples to a 16-bit .wav file (Uint8Array).soundNode(context, dsp, params): the DSP playing in anAudioWorklet, withoutput,send(),state()andclose().soundBus(context, { volume, muted }): one master volume, a mute and a soft limiter for everything that plays.
Your game creates the AudioContext after the player’s first tap: browsers play nothing before that.
import { soundBus } from '@firetower/genesis-lowpoly/audio';
import { engineSound } from '@firetower/genesis-lowpoly/vehicles';
button.addEventListener('click', () => {
const ctx = new AudioContext();
const bus = soundBus(ctx, { volume: .8 });
const v = engineSound(2, { context: ctx, type: 0, destination: bus.input });
v.start();
bus.mute(true); // everything at once
}, { once: true });Script clásico
Classic script build
Para páginas que no pueden importar módulos, el paquete trae dist/genesis.js: el motor entero en un solo script clásico que expone el global Genesis.
- Trae lo mismo que los módulos, con los mismos nombres: rostros, el editor, figuras, vehículos, edificios, naturaleza, criaturas,
world()y el suelo, la marca, el sonido yglb.Genesis.faceesfacede…/faces. - Pesa unos 1,3 MB sin comprimir. Si tu juego usa un empaquetador, prefiere los módulos: deja fuera lo que no usas.
- Está en
node_modules/@firetower/genesis-lowpoly/dist/genesis.js; cópialo junto a tu página.
For pages that cannot import modules, the package ships dist/genesis.js: the whole engine as one classic script exposing a Genesis global.
- It carries what the modules carry, under the same names: faces, the editor, figures, vehicles, buildings, nature, creatures,
world()and the ground, brand, sound andglb.Genesis.faceisfacefrom…/faces. - It weighs about 1.3 MB uncompressed. If your game uses a bundler, prefer the modules: it leaves out what you do not use.
- It lives at
node_modules/@firetower/genesis-lowpoly/dist/genesis.js; copy it next to your page.
// after <script src="genesis.js"></script> on the page
const svg = Genesis.face(Object.assign(Genesis.random(7, 'f'), { years: 30, frame: 'bust' }));
document.getElementById('avatar').innerHTML = svg;Qué se puede importar
Import paths
El paquete declara sideEffects: false: el empaquetador del juego deja fuera lo que no se usa. Todas las entradas cuelgan de @firetower/genesis-lowpoly.
| Entrada | Qué trae |
|---|---|
. | Espacios de nombres core, faces, vehicles, buildings, figures, nature, packs, light, scenery, world, assembly, ground, audio, brand, y createEditor, whose |
/core | El núcleo: rng, mix, tone, polygon, mirror, ngon, clip |
/faces | face, random, heir, encode, decode, palette, FACE, EXPR y los catálogos |
/faces/kit | Prendas propias: item, SLOTS, sym, area, torso |
/editor | createEditor, whose |
/figures | figure, figureModel, pose, crew, figureTrack, CYCLES, ROLES, STRIDE… |
/vehicles | vehicle, model, component, markFit, sponsorMarks, encodeVehicle, VEHICLE, el sonido (engineSound, raceSound…) y los catálogos |
/vehicles/kit, /mesh | El kit 3D: box, loft, rod, wheel, part, place, render… |
/buildings | building, buildingModel, signFit, BUILDING, encodeBuilding, el kit de planos y los catálogos |
/buildings/kit | blueprint, blocksIn, FAMILIES |
/nature | tree, treeModel, hedgeModel, rockModel, grassModel, meadowModel |
/creatures | creature, creatureModel, creatureGait, creatureSeat, PLANS, GAITS |
/brand | emblem, emblemMaker, isotype, nameplate, wordmark, banner, poster y los emblemas planos |
/letters | Las letras del motor: fitText, textPolys, logoPolys, readable, FONTS |
/daylight | daylight, underLight, MOMENTS |
/scenery | scene, scenery, inLight, LAYERS, LANDS, WEATHERS |
/world | world, route, WORLD_VIEWS |
/assembly | El montaje por etapas: assembly, ASSEMBLY_LIFT, ASSEMBLY_SWAY |
/ground | surface, soil, road, footpath, parking, water, fence, gate, las piezas de circuito y MATERIALS |
/gltf | glb |
/audio | wav, renderSound, soundNode, soundBus |
/packs | Todos los paquetes: wear, ITEMS, plan, BLUEPRINTS, piece, PIECES, species, las escenas y los catálogos de marca |
/packs/<paquete>, /packs/<paquete>/<id> | Un paquete (…/packs/racing) o una pieza sola (…/packs/classic/trench-coat → TRENCH_COAT) |
The package declares sideEffects: false, so your bundler drops whatever you do not use. Every entry hangs off @firetower/genesis-lowpoly.
| Entry | Exports |
|---|---|
. | Namespaces core, faces, vehicles, buildings, figures, nature, packs, light, scenery, world, assembly, ground, audio, brand, plus createEditor and whose |
/core | The core: rng, mix, tone, polygon, mirror, ngon, clip |
/faces | face, random, heir, encode, decode, palette, FACE, EXPR and the catalogs |
/faces/kit | Custom garments: item, SLOTS, sym, area, torso |
/editor | createEditor, whose |
/figures | figure, figureModel, pose, crew, figureTrack, CYCLES, ROLES, STRIDE… |
/vehicles | vehicle, model, component, markFit, sponsorMarks, encodeVehicle, VEHICLE, sound (engineSound, raceSound…) and the catalogs |
/vehicles/kit, /mesh | The 3D kit: box, loft, rod, wheel, part, place, render… |
/buildings | building, buildingModel, signFit, BUILDING, encodeBuilding, the blueprint kit and the catalogs |
/buildings/kit | blueprint, blocksIn, FAMILIES |
/nature | tree, treeModel, hedgeModel, rockModel, grassModel, meadowModel |
/creatures | creature, creatureModel, creatureGait, creatureSeat, PLANS, GAITS |
/brand | emblem, emblemMaker, isotype, nameplate, wordmark, banner, poster and the flat emblems |
/letters | The engine’s lettering: fitText, textPolys, logoPolys, readable, FONTS |
/daylight | daylight, underLight, MOMENTS |
/scenery | scene, scenery, inLight, LAYERS, LANDS, WEATHERS |
/world | world, route, WORLD_VIEWS |
/assembly | Staged assembly: assembly, ASSEMBLY_LIFT, ASSEMBLY_SWAY |
/ground | surface, soil, road, footpath, parking, water, fence, gate, the race track pieces and MATERIALS |
/gltf | glb |
/audio | wav, renderSound, soundNode, soundBus |
/packs | Every pack: wear, ITEMS, plan, BLUEPRINTS, piece, PIECES, species, the scenes and the brand catalogs |
/packs/<pack>, /packs/<pack>/<id> | One pack (…/packs/racing) or a single piece (…/packs/classic/trench-coat → TRENCH_COAT) |
import { faces, vehicles } from '@firetower/genesis-lowpoly'; // namespaces
import { face } from '@firetower/genesis-lowpoly/faces'; // one entry
import { TRENCH_COAT } from '@firetower/genesis-lowpoly/packs/classic/trench-coat'; // one pieceVersiones y cambios
Versioning and changelog
- Cada versión lleva una etiqueta
vX.Y.Zen el repositorio, la misma queversionenpackage.json, y se publica en Genesis Lab, el banco de pruebas del estudio. - Antes de la 1.0: una versión menor (
0.Y.0) trae capacidades nuevas; un parche (0.Y.Z), arreglos. - Cada versión entra al registro de cambios con lo que añade y lo que cambia a la vista. Las capacidades nuevas llegan como opciones: sin ellas, el dibujo sale como antes.
- Qué cuenta como cambio que rompe: un cambio en cómo se ve algo que ya existía se anuncia en el registro y pasa por la galería de referencia. Los catálogos nunca se reordenan y los ids nunca se renombran, así que un texto guardado sigue siendo la misma cosa.
- Each version is tagged
vX.Y.Zin the repository, matchingversioninpackage.json, and is published to Genesis Lab, the studio’s test bench. - Before 1.0, a minor version (
0.Y.0) adds capabilities and a patch (0.Y.Z) fixes things. - Every version enters the changelog with what it adds and what it visibly changes. New capabilities arrive as options: without them, drawings come out as before.
- What counts as breaking: a change to how something existing looks is announced in the changelog and goes through the reference gallery. Catalogs are never reordered and ids are never renamed, so a saved string keeps meaning the same thing.