GenesisDocs
Documentación técnicaTechnical documentation

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.1

Qué 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.

Una cara por semilla
A face from a seed
Un monoplaza
A single-seater
Un plano del catálogo
A catalog blueprint

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.

El resultado: random(7, 'f') a los 30 años
The result: random(7, 'f') at 30
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/kit y vehicle({ 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/kit and vehicle({ 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] o at: [x, y, z] y se gira con turn, 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] or at: [x, y, z] and turn it with turn, 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 is FLOOR = 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.
Casa, auto y persona en metros, en un mismo mundo
A house, a car and a person in meters, in one world
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:

VistaCámara
sidePerfil, desde −y, sin inclinación.
threeTres cuartos: girada 32° y 24° desde arriba.
topCenital.
isoLa isométrica de los mapas de rejilla: 45° y 35,26° desde arriba.
frontDe 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:

ViewCamera
sideProfile, from −y, no tilt.
threeThree-quarters: turned 32° and tilted 24° down.
topStraight down.
isoThe true isometric of tile maps: 45° round, 35.26° down.
frontStraight 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.

side
side
three
three
top
top
iso
iso
import { vehicle, VIEWN } from '@firetower/genesis-lowpoly/vehicles';

VIEWN.map((view) => vehicle({ type: 0, view, livery }));   // the same car, four cameras

Escala 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).

Ajustado al marco
Fitted to the frame
scale: 30
scale: 30
Una persona a la misma escala
A person at the same scale
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 proportions

La 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 light a cualquier generador en 3D y a world().
  • 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: sunrise y sunset (6 y 18 por defecto) y darkness, lo oscura que llega la noche (0,52 por defecto). MOMENTS trae 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 light to any 3D generator and to world().
  • 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: sunrise and sunset (6 and 18 by default) and darkness, how dark the night gets (0.52 by default). MOMENTS names six hours, from dawn to night.
daylight(9)
daylight(9)
daylight(18.3)
daylight(18.3)
daylight(22), lights: .8
daylight(22), lights: .8
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, tinted

Determinismo 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; seed en 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; seed on 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, and ed.seed() tells you which.
seed: 4
seed: 4
seed: 5
seed: 5
seed: 6
seed: 6
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 pine

Guardar 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) y decode(text), por ejemplo 1-f-4.3.1.1.0.1.2.1.0--5.1.-7.-5.
  • Vehículos: encodeVehicle y decodeVehicle, con prefijo vehicle:. Edificios: encodeBuilding y decodeBuilding, con building:. Emblemas: EMBLEM.encode, con emblem:.
  • 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) and decode(text), for example 1-f-4.3.1.1.0.1.2.1.0--5.1.-7.-5.
  • Vehicles: encodeVehicle and decodeVehicle, prefixed vehicle:. Buildings: encodeBuilding and decodeBuilding, prefixed building:. Emblems: EMBLEM.encode, prefixed emblem:.
  • 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 para HAIR, PARTS, TYPES, BLOCKS, CYCLES, MATERIALS y 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 to HAIR, PARTS, TYPES, BLOCKS, CYCLES, MATERIALS and 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 .svg y 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 .bl para 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 un aria-label gené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 .svg plus 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 .bl class 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 English aria-label (“Generated face”, “Generated world”…); emblems and nameplates carry none. If accessibility matters on your screen, wrap it with your own label.
Ruedas que giran y líneas de velocidad, en SMIL
Spinning wheels and speed lines, in SMIL
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 file

Rendimiento

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 con track (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 }) devuelve stack, 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> con will-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.
  • bounds fija 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 a track (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 }) returns stack, images in paint order: the still base, still runs, and whatever moves on its own. Put each in its own <img> with will-change: transform so 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.
  • bounds holds 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, SAD o CONFUSED. Con animate: 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, SAD or CONFUSED. With animate: 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.
EXPR.HAPPY, frame: 'bust'
EXPR.HAPPY, frame: 'bust'
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) y shape.weight (0–10).
  • skin, iris y hairColor aceptan un índice del catálogo o cualquier '#rrggbb'.
  • Opciones de dibujo para mundos de fantasía, que no se guardan: halo, shadow, eyeGlow, smoke y void.

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) and shape.weight (0–10).
  • skin, iris and hairColor take a catalog index or any '#rrggbb'.
  • Drawing options for fantasy worlds, never stored: halo, shadow, eyeGlow, smoke and void.
8 años
Age 8
30 años
Age 30
78 años
Age 78
heir(who, 7)
heir(who, 7)
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 eyes

Ropa, 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, face y back. 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) y zones (dónde va un patrón del juego, con decals). Las que tienen badge llevan el logo del equipo.
  • Ningún casco ni sombrero deja ver el pelo. Paquetes de ropa: racing, sport, classic, work, traditional, care, fan y fantasy (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, face and back. 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) and zones (where your game’s pattern goes, through decals). Pieces with a badge carry the team logo.
  • No helmet or hat ever shows the hair beneath. Clothing packs: racing, sport, classic, work, traditional, care, fan and fantasy (PACKS).
  • palette(spec) returns a face’s { skin, hair, iris }, so you can draw the same person full-body.
racing-helmet
racing-helmet
suit, spy-hat, sunglasses
suit, spy-hat, sunglasses
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:

TipoGuarda
choiceUno de values.
swatchUn color de colors; con free: true, también cualquier '#rrggbb'.
pickUna pieza numerada, de count.
rangeUn entero de min a max.
colorCualquier '#rrggbb' (vehículos, emblemas).
textUn 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:

TypeHolds
choiceOne of values.
swatchA color from colors; with free: true, any '#rrggbb' too.
pickA numbered piece, out of count.
rangeAn integer from min to max.
colorAny '#rrggbb' (vehicles, emblems).
textA 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, …).

FACE.blank(), el punto de partida de un creador
FACE.blank(), where a creator starts
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 stores

Figuras

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. Con animate: true, el SVG lo reproduce solo: frames (12 por defecto) en duration segundos (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), o real.
  • Vistas: side (por defecto), three, top e iso. Toma light, scale y bg: '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. With animate: true, the SVG plays it by itself: frames (12 by default) over duration seconds (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), or real.
  • Views: side (the default), three, top and iso. Takes light, scale and bg: 'none'.
skate, phase: .2
skate, phase: .2
run, animate: true
run, animate: true
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 itself

Oficios, 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) y sport. 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, lean y hold; tool lo 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 para world(). Con animate, la pistola golpea y la llave gira; el vehículo se dibuja una sola vez.
  • pose(cycle, phase) da la pose como dato y figure({ 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) and sport. 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, lean and hold; tool swaps 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 for world(). With animate, the wheel gun hammers and the wrench turns; the vehicle is drawn only once.
  • pose(cycle, phase) returns the pose as data, and figure({ pose }) draws a pose of your own.
crew(car, { count: 4 })
crew(car, { count: 4 })
role: 'worker'
role: 'worker'
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 como track de un objeto de world(): 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 the track of a world() 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, use phase = meters / STRIDE[cycle].
Una figura que va y vuelve por un sendero
A figure pacing a footpath
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 para glb(). 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 for glb(). 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.
type: 1, Grand Prix de 1930
type: 1, 1930s Grand Prix
type: 2, kart
type: 2, kart
type: 4, camión
type: 4, truck
focus: 'fwing'
focus: 'fwing'
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' y on: 'top' son planas (band, slant, dot ayudan a dibujarlas); on: 'space' (con slab) y on: '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; sin color, el motor elige una tinta legible.
  • markFit(type, slot, text) dice antes de mostrarlo si cabe; sponsorMarks(type, patrocinadores) reparte una lista por niveles (rank 1 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' and on: 'top' are flat (band, slant and dot help draw them); on: 'space' (with slab) and on: '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 a color, the engine picks a legible ink.
  • markFit(type, slot, text) tells you beforehand whether it fits; sponsorMarks(type, sponsors) places a tiered list (rank 1 to 4) in the best slots.
Una franja en 3D, número y patrocinio
A 3D stripe, a number and a sponsor
Una franja plana desde arriba
A flat stripe from above
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 misma seed, el mismo daño.
  • finish: carbon, matte, chrome o gold, en toda la librea o por pieza. El cromo y el oro reflejan el cielo y destellan solos (glints: false los deja quietos).
  • speed (0–1) difumina los rayos; con animate: true las ruedas giran en el SVG, y trail: true pone 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 same seed gives the same damage.
  • finish: carbon, matte, chrome or gold, for the whole livery or per part. Chrome and gold reflect the sky and glint by themselves (glints: false keeps them still).
  • speed (0–1) blurs the spokes; with animate: true the wheels spin inside the SVG, and trail: true adds speed lines.
wear: .6, sin alerón delantero
wear: .6, front wing gone
finish: carbono y cromo
finish: carbon and chrome
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 un AudioWorklet, sin grabaciones, en el contexto de audio que crea el juego; engineSample() da una muestra en .wav (también en Node), y raceSound() 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 an AudioWorklet, with no recordings, on the audio context your game creates; engineSample() returns a .wav sample (in Node too), and raceSound() voices a whole race from the cars’ positions.
component({ system: 'engine', engine: 2 })
component({ system: 'engine', engine: 2 })
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 .wav

Edificios

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, f el piso (0, la planta baja). Una celda mide CELL × CELL (4 m) y un piso FLOOR (3,2 m).
  • kind (BLOCKS): room, glass, garage, solid, stand (tribuna) y cab (cabina de torre). win (WINDOWS) elige la ventana y door: '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, three e iso (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, f the floor (0 is the ground floor). A cell is CELL × CELL (4 m) and a floor is FLOOR (3.2 m).
  • kind (BLOCKS): room, glass, garage, solid, stand (grandstand) and cab (control tower cab). win (WINDOWS) picks the window and door: '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, three and iso (the default).
Cuatro bloques
Four blocks
progress: .5, en obra
progress: .5, under construction
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 en CATALOG): 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 }) y blocksIn() para llenar una caja de celdas.
  • Para guardar: encodeBuilding y decodeBuilding. Para editar, createEditor(BUILDING) con sus acciones: place, remove, door y use.

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 in CATALOG): 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 }), and blocksIn() to fill a box of cells.
  • To save: encodeBuilding and decodeBuilding. To edit, createEditor(BUILDING) with its actions: place, remove, door and use.
plan('pit-garages', { width: 6 })
plan('pit-garages', { width: 6 })
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, palm y bush, con height, leaves y bark del 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, palm and bush, with your height, leaves and bark. 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.
leafy, hojas de otoño
leafy, autumn leaves
Árboles, roca y seto en un mundo
Trees, a rock and a hedge in a world
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.

  • height va 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: toon o real. seed varí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.

  • height is 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 bestiary pack: 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: toon or real. seed varies each individual within its species. creatureModel(spec) returns its faces for a world.
mammoth, real
mammoth, real
dragon, toon
dragon, toon
Una criatura propia
A creature of your own
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 own

Andares y jinetes

Gaits and riders

  • gait (GAITS): stand (respira y mece la cola), walk (paso de cuatro tiempos) y gallop. phase (0–1) da una pose; animate: true reproduce 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, y aura un halo con su silueta.
  • gait (GAITS): stand (breathing, tail swaying), walk (a four-beat walk) and gallop. phase (0–1) gives a pose; animate: true plays 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, and aura a halo in its silhouette.
wolf, walk
wolf, walk
Un caballo con jinete
A horse and rider
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_PLUS y FANTASY_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, normalize y roll. Se edita con createEditor().
  • 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_PLUS and FANTASY_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, normalize and roll. Edit it with createEditor().
  • 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.
Escudo con símbolo y letras
Shield, symbol and letters
Sin escudo, 3 × 1
No shield, 3 × 1
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) y seed dan 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; composiciones lockup, compact, stacked e icon (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) plus seed give 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; layouts lockup, compact, stacked and icon (WORDMARK_LAYOUTS).
  • banner() draws a cloth banner hanging from a pole, with any engine SVG as its charge.
Un imagotipo por semilla
An imagotype from a seed
nameplate, metal
nameplate, metal
wordmark, lockup
wordmark, lockup
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() y posterTitle() dan solo el fondo o solo el título, a cualquier tamaño.
  • tier: bronze, silver, gold o platinum cambian 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() and posterTitle() give only the background or only the title, at any size.
  • tier: bronze, silver, gold or platinum change 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.
achievementPoster('winner'), tier: 'gold'
achievementPoster('winner'), tier: 'gold'
posterBackdrop, 300 × 300
posterBackdrop, 300 × 300
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 background

Ambiente: 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 (o time) y weather (WEATHERS: clear, cloudy, overcast, storm). sky: [arriba, abajo] pinta un cielo propio.
  • land (LANDS: plain, hills, mountains, city, coast) o layers propias (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 con scale para 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 (or time) and weather (WEATHERS: clear, cloudy, overcast, storm). sky: [top, bottom] paints a custom sky.
  • land (LANDS: plain, hills, mountains, city, coast) or your own layers (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 with scale so it keeps its size.
Capas propias al atardecer
Custom layers at dusk
land: 'city', de noche
land: 'city', at night
Con una casa y un auto dentro
With a house and a car in it
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 }. at es [x, y] o [x, y, z] para ponerlo más alto (alguien sobre un peralte); turn, en grados.
  • view (WORLD_VIEWS, iso por defecto), scale (10 px/m por defecto), light, bg y margin.
  • floor: { from, to, color } (o false) 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 }. at is [x, y], or [x, y, z] to raise it (someone on a banking); turn is in degrees.
  • view (WORLD_VIEWS, iso by default), scale (10 px/m by default), light, bg and margin.
  • floor: { from, to, color } (or false) is the base; ground is 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.
Una esquina a las 21:30
A street corner at 21:30
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, …] y duration en segundos en vez de faces. Lo quieto se dibuja una sola vez.
  • Por un camino (alguien que pasea): el objeto da track: [{ frames, duration, phase, from, to, turn, time }, …] y offset. Cada pose se dibuja una vez y el SVG la desliza de from a to en time segundos; su sombra va con ella. figureTrack() lo arma para una figura.
  • Lo que gira (una noria): un rotor dibuja 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 a duration in seconds instead of faces. Everything still is drawn once.
  • Along a path (someone strolling): the item gives track: [{ frames, duration, phase, from, to, turn, time }, …] and an offset. Each pose is drawn once and the SVG slides it from from to to over time seconds, shadow included. figureTrack() builds one for a figure.
  • Things that turn (a Ferris wheel): a rotor draws the turning part once and the SVG spins it in its own plane.
Cuatro cuadros que el SVG pasa solo
Four frames the SVG plays by itself
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; box es [x0, y0, x1, y1] en píxeles y outline, su contorno.
  • w.pick(x, y): el id de la cosa más cercana bajo ese punto del dibujo, o null.
  • w.ground(x, y): el punto del suelo, en metros, bajo ese punto del dibujo (null en 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; box is [x0, y0, x1, y1] in pixels and outline its silhouette.
  • w.pick(x, y): the id of the nearest thing under that point of the drawing, or null.
  • w.ground(x, y): the ground point, in meters, under that point of the drawing (null in 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, y w.order([{ id, lo, hi }, …]) ordena varias a la vez.
  • world({ split: true }) (o { split: { max: 40 } }) da además stack: 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, and w.order([{ id, lo, hi }, …]) sorts several at once.
  • world({ split: true }) (or { split: { max: 40 } }) also returns stack: 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 rest

El 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 }) y gate(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 }) and gate(width, { open }): fences and gates, as world items.
Pasto, calle, parqueadero, sendero y valla
Grass, road, parking, footpath and fence
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: true pone pianos rojos y blancos y start, la línea de salida.
  • grid: { slots, first, spacing } en road(), o startGrid() aparte, pinta la parrilla; gridSlots(trazado, opciones) dice dónde para cada auto (at, angle para el turn del mundo).
  • gravel(borde, { width, side }): trampas de grava. tyreWall(), jerseyWall() y catchFence(): 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: true adds red-and-white kerbs and start the start line.
  • grid: { slots, first, spacing } in road(), or startGrid() on its own, paints the starting grid; gridSlots(path, options) tells you where each car stops (at, and angle for the world’s turn).
  • gravel(edge, { width, side }): gravel traps. tyreWall(), jerseyWall() and catchFence(): 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.
Un óvalo de pista, desde arriba
A race oval from above
Parrilla, grava y neumáticos
Grid, gravel and tyre wall
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, home y link (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: ITEMS lista las prendas por id y wear(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, home and link (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: ITEMS lists garments by id and wear(id, colors) puts them on. Each piece can also be imported on its own: …/packs/<pack>/<id>.
Piezas de obra
Construction pieces
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. hits da 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 en hits (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 paquete food trae consumibles a tamaño real para eso: isotonic-drink, water-bottle, fruit (kind: apple, banana, orange), arepa, smoothie y protein-bar.
  • rewardChest({ rarity, state, animate, autoplay, uid, size }): un cofre de premio en una de CHEST_RARITIES (common, rare, epic, legendary), cerrado o abierto. Con animate: true trae su apertura: tiembla, la tapa se abre de golpe y sale la luz de su rareza. Devuelve phases y duration en segundos, anchor (dónde dibujar el premio, en px) y hit (su contorno). Con autoplay: false, el juego la arranca con svg.getElementById(uid + '-go').beginElement().
  • El equipo de un patinador en niveles (paquete gear): speed-skate, speed-helmet y skinsuit-gear, con tier de 0 a 3 y colors del equipo, para pieceIcon. En el retrato, wear('skate-helmet', colores, { parts: { tier } }) y lo mismo con skinsuit.
  • awardsScene({ award, nominees, winner, phase }): una gala de premios. anchors trae title, slots (foto y placa de cada nominado, y si está encendida) y trophy.
  • podiumScene({ drivers }): el podio con tres pilotos y sus avatares; anchors por si el juego pone los suyos.
  • cabinetScene({ items, cols }): una vitrina de trofeos (TROPHIES) de frente, con slots.
  • Interiores con anchors: simulatorRoom, windTunnelRoom, hospitalityShop, hospitalityVip, hospitalityPhotos y logisticsRoom.

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. hits gives 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 in hits (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. The food pack brings life-size consumables for that: isotonic-drink, water-bottle, fruit (kind: apple, banana, orange), arepa, smoothie and protein-bar.
  • rewardChest({ rarity, state, animate, autoplay, uid, size }): a reward chest in one of CHEST_RARITIES (common, rare, epic, legendary), shut or open. With animate: true it carries its opening: it shakes, the lid bursts open and its rarity’s light pours out. It returns phases and duration in seconds, anchor (where to draw the prize, in px) and hit (its outline). With autoplay: false, the game starts it with svg.getElementById(uid + '-go').beginElement().
  • A skater’s gear in tiers (the gear pack): speed-skate, speed-helmet and skinsuit-gear, with tier 0 to 3 and the team’s colors, for pieceIcon. In a portrait, wear('skate-helmet', colors, { parts: { tier } }), and the same with skinsuit.
  • awardsScene({ award, nominees, winner, phase }): an awards ceremony. anchors holds the title, the slots (each nominee’s photo and plate, and whether it is lit) and the trophy.
  • podiumScene({ drivers }): the podium with three drivers and their avatars; anchors in case you place your own.
  • cabinetScene({ items, cols }): a trophy cabinet (TROPHIES) seen from the front, with slots.
  • Interiors with anchors: simulatorRoom, windTunnelRoom, hospitalityShop, hospitalityVip, hospitalityPhotos and logisticsRoom.
homeRoom, nivel 2, de noche
homeRoom, level 2, at night
Con el gimnasio en casa
With the home gym
pieceIcon: una pieza para la tienda
pieceIcon: a piece for a shop
Un consumible del paquete food
A consumable from the food pack
rewardChest legendario, abierto
A legendary rewardChest, open
Un patín +3 en los colores del equipo
A +3 skate in team colors
awardsScene, fase del ganador
awardsScene, winner phase
cabinetScene
cabinetScene
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ónLugar 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).

FunctionPlace 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.

museumHall({ level: 2 })
museumHall({ level: 2 })
spa()
spa()
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_TRACK se importa de …/packs/sport/banked-track.
  • BANKED_TRACK.line(t, carril) da dónde va alguien a la fracción t de la vuelta: [x, y, z, rumbo], con z su 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 con deck: 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_TRACK from …/packs/sport/banked-track.
  • BANKED_TRACK.line(t, lane) tells you where a racer is at fraction t of the lap: [x, y, z, heading], with z its 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 with deck: false.
piece('banked-track')
piece('banked-track')
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() y shadow().
  • 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(), a vehicle({ model }) o a glb(). 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() and shadow().
  • 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 }) or glb(). render(faces, view, w, h, margin) returns only the polygons, without the <svg> tag.
Un quiosco con box, rod y frustum
A kiosk from box, rod and frustum
vehicle({ model })
vehicle({ model })
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 un AudioWorklet, con output, send(), state() y close().
  • 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 an AudioWorklet, with output, send(), state() and close().
  • 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 y glb. Genesis.face es face de …/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 and glb. Genesis.face is face from …/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.

EntradaQué trae
.Espacios de nombres core, faces, vehicles, buildings, figures, nature, packs, light, scenery, world, assembly, ground, audio, brand, y createEditor, whose
/coreEl núcleo: rng, mix, tone, polygon, mirror, ngon, clip
/facesface, random, heir, encode, decode, palette, FACE, EXPR y los catálogos
/faces/kitPrendas propias: item, SLOTS, sym, area, torso
/editorcreateEditor, whose
/figuresfigure, figureModel, pose, crew, figureTrack, CYCLES, ROLES, STRIDE…
/vehiclesvehicle, model, component, markFit, sponsorMarks, encodeVehicle, VEHICLE, el sonido (engineSound, raceSound…) y los catálogos
/vehicles/kit, /meshEl kit 3D: box, loft, rod, wheel, part, place, render…
/buildingsbuilding, buildingModel, signFit, BUILDING, encodeBuilding, el kit de planos y los catálogos
/buildings/kitblueprint, blocksIn, FAMILIES
/naturetree, treeModel, hedgeModel, rockModel, grassModel, meadowModel
/creaturescreature, creatureModel, creatureGait, creatureSeat, PLANS, GAITS
/brandemblem, emblemMaker, isotype, nameplate, wordmark, banner, poster y los emblemas planos
/lettersLas letras del motor: fitText, textPolys, logoPolys, readable, FONTS
/daylightdaylight, underLight, MOMENTS
/sceneryscene, scenery, inLight, LAYERS, LANDS, WEATHERS
/worldworld, route, WORLD_VIEWS
/assemblyEl montaje por etapas: assembly, ASSEMBLY_LIFT, ASSEMBLY_SWAY
/groundsurface, soil, road, footpath, parking, water, fence, gate, las piezas de circuito y MATERIALS
/gltfglb
/audiowav, renderSound, soundNode, soundBus
/packsTodos 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.

EntryExports
.Namespaces core, faces, vehicles, buildings, figures, nature, packs, light, scenery, world, assembly, ground, audio, brand, plus createEditor and whose
/coreThe core: rng, mix, tone, polygon, mirror, ngon, clip
/facesface, random, heir, encode, decode, palette, FACE, EXPR and the catalogs
/faces/kitCustom garments: item, SLOTS, sym, area, torso
/editorcreateEditor, whose
/figuresfigure, figureModel, pose, crew, figureTrack, CYCLES, ROLES, STRIDE…
/vehiclesvehicle, model, component, markFit, sponsorMarks, encodeVehicle, VEHICLE, sound (engineSound, raceSound…) and the catalogs
/vehicles/kit, /meshThe 3D kit: box, loft, rod, wheel, part, place, render…
/buildingsbuilding, buildingModel, signFit, BUILDING, encodeBuilding, the blueprint kit and the catalogs
/buildings/kitblueprint, blocksIn, FAMILIES
/naturetree, treeModel, hedgeModel, rockModel, grassModel, meadowModel
/creaturescreature, creatureModel, creatureGait, creatureSeat, PLANS, GAITS
/brandemblem, emblemMaker, isotype, nameplate, wordmark, banner, poster and the flat emblems
/lettersThe engine’s lettering: fitText, textPolys, logoPolys, readable, FONTS
/daylightdaylight, underLight, MOMENTS
/sceneryscene, scenery, inLight, LAYERS, LANDS, WEATHERS
/worldworld, route, WORLD_VIEWS
/assemblyStaged assembly: assembly, ASSEMBLY_LIFT, ASSEMBLY_SWAY
/groundsurface, soil, road, footpath, parking, water, fence, gate, the race track pieces and MATERIALS
/gltfglb
/audiowav, renderSound, soundNode, soundBus
/packsEvery 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 piece

Versiones y cambios

Versioning and changelog

  • Cada versión lleva una etiqueta vX.Y.Z en el repositorio, la misma que version en package.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.Z in the repository, matching version in package.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.