bedrock-ddui-forms 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.es.md +363 -0
- package/README.md +361 -3
- package/package.json +34 -5
- package/src/core/createRouter.js +74 -0
- package/src/core/customFormRender.js +88 -0
- package/src/core/propertiesFactory.js +190 -0
- package/src/core/validation.js +442 -0
- package/src/index.js +3 -0
- package/src/models/properties/Button.js +34 -0
- package/src/models/properties/Divider.js +11 -0
- package/src/models/properties/Dropdown.js +22 -0
- package/src/models/properties/Header.js +16 -0
- package/src/models/properties/Image.js +25 -0
- package/src/models/properties/Label.js +16 -0
- package/src/models/properties/Property.js +13 -0
- package/src/models/properties/Slider.js +25 -0
- package/src/models/properties/Spacer.js +12 -0
- package/src/models/properties/TextField.js +20 -0
- package/src/models/properties/Toggle.js +20 -0
package/README.es.md
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# bedrock-ddui-forms
|
|
2
|
+
|
|
3
|
+
> [🇬🇧 English](./README.md) · 🇪🇸 Español
|
|
4
|
+
|
|
5
|
+
Una capa declarativa para crear formularios **DDUI** (`CustomForm`) de `@minecraft/server-ui` a partir de objetos.
|
|
6
|
+
|
|
7
|
+
En lugar de encadenar llamadas sobre `CustomForm`, describes la vista como datos y un renderizador la traduce:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
{
|
|
11
|
+
title: "Ajustes",
|
|
12
|
+
content: [
|
|
13
|
+
{ type: "header", text: "General" },
|
|
14
|
+
{ type: "toggle", label: "Activar función", toggled: new ObservableBoolean(true, { clientWritable: true }) },
|
|
15
|
+
{ type: "close-button" }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> No afiliado a Mojang ni a Microsoft. "Minecraft" y "Bedrock" se usan solo para describir la plataforma a la que apunta esta librería.
|
|
21
|
+
|
|
22
|
+
## Características
|
|
23
|
+
|
|
24
|
+
- **Vistas declarativas**: un formulario es un objeto con `title` y un arreglo `content`.
|
|
25
|
+
- **Tipado con la API oficial**: los modelos son typedefs de JSDoc que reutilizan los tipos de `@minecraft/server-ui` (`ButtonOptions`, `SliderOptions`, …), así que se mantienen sincronizados con el módulo.
|
|
26
|
+
- **Navegación sencilla**: un botón puede abrir otra vista con `to`, combinable con `onClick`.
|
|
27
|
+
- **Router**: registra vistas por nombre, pásales parámetros y vuelve atrás con `"back"`.
|
|
28
|
+
- **Fácil de extender**: añadir un tipo de elemento nuevo es una entrada en una tabla y una función.
|
|
29
|
+
- **Fallback seguro**: un `type` no soportado muestra un label visible y registra una advertencia, en vez de romper todo el formulario.
|
|
30
|
+
|
|
31
|
+
## Requisitos
|
|
32
|
+
|
|
33
|
+
- `@minecraft/server-ui` **2.2.0** (estable)
|
|
34
|
+
|
|
35
|
+
## Instalación
|
|
36
|
+
|
|
37
|
+
El motor de scripts de Minecraft **no** resuelve paquetes de npm (dentro del juego no existe `node_modules`). Solo los módulos `@minecraft/*` declarados en el `manifest.json` los aporta el juego, así que la librería tiene que terminar como archivos dentro de la carpeta `scripts/` de tu addon. Hay dos formas de hacerlo.
|
|
38
|
+
|
|
39
|
+
### Opción 1: copiar los archivos
|
|
40
|
+
|
|
41
|
+
Copia la librería dentro de tu behavior pack e impórtala con rutas relativas:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
BP/
|
|
45
|
+
manifest.json
|
|
46
|
+
scripts/
|
|
47
|
+
main.js
|
|
48
|
+
libs/bedrock-ddui-forms/
|
|
49
|
+
index.js
|
|
50
|
+
core/
|
|
51
|
+
models/
|
|
52
|
+
views/
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import { customFormRender } from "../libs/bedrock-ddui-forms/index";
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
No necesita herramientas, pero para actualizar tienes que copiar los archivos de nuevo.
|
|
60
|
+
|
|
61
|
+
### Opción 2: usar un bundler (recomendada si instalas desde npm)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install bedrock-ddui-forms
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Después empaqueta tu código y la librería en un solo archivo dentro de `scripts/`. Por ejemplo, con [esbuild](https://esbuild.github.io/):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx esbuild src/index.js --bundle --format=esm --outfile=scripts/libs/bedrock-ddui-forms/index.js --external:@minecraft/server --external:@minecraft/server-ui
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Los `--external` son importantes: los módulos `@minecraft/*` los aporta el juego y no deben incluirse en el bundle. Con este flujo, `npm update` basta para actualizar la librería.
|
|
74
|
+
|
|
75
|
+
### Siempre necesario
|
|
76
|
+
|
|
77
|
+
- Declara `@minecraft/server-ui` en tu `manifest.json` (esta librería apunta a la versión `2.2.0`). Nunca se copia ni se empaqueta:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"module_name": "@minecraft/server-ui",
|
|
82
|
+
"version": "2.2.0"
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- La propia librería no debe usar módulos de Node.js (`node:fs`, etc.), porque no existen en el juego.
|
|
87
|
+
- Los imports internos de la librería están escritos sin extensión (`./propertiesFactory`), y los bundlers los resuelven. Si copias los archivos y tu entorno exige extensiones explícitas, añade `.js` a los imports relativos.
|
|
88
|
+
|
|
89
|
+
## Inicio rápido
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import { ObservableBoolean, ObservableString } from "@minecraft/server-ui";
|
|
93
|
+
import { customFormRender } from "../libs/bedrock-ddui-forms/index";
|
|
94
|
+
|
|
95
|
+
const createSettingsPage = (player) => ({
|
|
96
|
+
title: "Ajustes",
|
|
97
|
+
content: [
|
|
98
|
+
{ type: "header", text: "General" },
|
|
99
|
+
{ type: "spacer" },
|
|
100
|
+
{
|
|
101
|
+
type: "text-field",
|
|
102
|
+
label: "Nombre",
|
|
103
|
+
text: new ObservableString("Steve", { clientWritable: true })
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: "toggle",
|
|
107
|
+
label: "Activar función",
|
|
108
|
+
toggled: new ObservableBoolean(true, { clientWritable: true })
|
|
109
|
+
},
|
|
110
|
+
{ type: "divider" },
|
|
111
|
+
{ type: "close-button" }
|
|
112
|
+
]
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const openSettings = (player) => customFormRender(player, createSettingsPage(player));
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
> **Consejo:** define cada vista como una función (`createXPage(player)`) para que los `Observable` se creen de nuevo cada vez que se abre la vista y no se arrastre estado anterior.
|
|
119
|
+
|
|
120
|
+
## Elementos soportados
|
|
121
|
+
|
|
122
|
+
| `type` | Propiedades principales | Método de `CustomForm` |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| `button` | `label`, `onClick?`, `to?`, `options?` | `button()` |
|
|
125
|
+
| `close-button` | – | `closeButton()` |
|
|
126
|
+
| `divider` | `options?` | `divider()` |
|
|
127
|
+
| `dropdown` | `label`, `value` (`ObservableNumber`), `items`, `options?` | `dropdown()` |
|
|
128
|
+
| `header` | `text`, `options?` | `header()` |
|
|
129
|
+
| `image` | `src`, `pack`, `options?` (`onClick`, `tooltip`, `visible`, `width`) | `image()` |
|
|
130
|
+
| `label` | `text`, `options?` | `label()` |
|
|
131
|
+
| `slider` | `label`, `value` (`ObservableNumber`), `min`, `max`, `options?` (`step`, …) | `slider()` |
|
|
132
|
+
| `spacer` | `options?` | `spacer()` |
|
|
133
|
+
| `text-field` | `label`, `text` (`ObservableString`), `options?` | `textField()` |
|
|
134
|
+
| `toggle` | `label`, `toggled` (`ObservableBoolean`), `options?` | `toggle()` |
|
|
135
|
+
|
|
136
|
+
`options` siempre coincide con el tipo de opciones correspondiente de `@minecraft/server-ui` (por ejemplo `SliderOptions`). `image.options.width` es una fracción del ancho del formulario (`0.5` es la mitad).
|
|
137
|
+
|
|
138
|
+
## Navegación
|
|
139
|
+
|
|
140
|
+
Un botón puede abrir otra vista con `to`. Se ejecuta **después** de `onClick`, cuando el formulario actual ya está cerrado, así que se pueden combinar:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
{
|
|
144
|
+
type: "button",
|
|
145
|
+
label: "Guardar y volver",
|
|
146
|
+
onClick: function () { save(player); }, // se ejecuta primero
|
|
147
|
+
to: () => openMainMenu(player) // luego cierra este formulario y abre el siguiente
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- Si el botón solo navega, usa `to`.
|
|
152
|
+
- Si hace algo más, usa `onClick`, y añade `to` si además debe navegar.
|
|
153
|
+
- Dentro de `onClick` y de `image.options.onClick`, `this` es el `CustomForm`, así que `this.close()` funciona si necesitas cerrar sin abrir nada.
|
|
154
|
+
|
|
155
|
+
Para muchas vistas, parámetros y un botón de volver automático, mira el [Router](#router-vistas-con-nombre-y-atrás).
|
|
156
|
+
|
|
157
|
+
## Router: vistas con nombre y "atrás"
|
|
158
|
+
|
|
159
|
+
Cuando tus menús crecen, conectar cada botón con el siguiente `openX(player)` se vuelve repetitivo. `createRouter` registra tus vistas por nombre, permite que los botones apunten a ellas y guarda un **historial por jugador** para que un botón pueda volver atrás.
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
import { ObservableBoolean } from "@minecraft/server-ui";
|
|
163
|
+
import { createRouter } from "bedrock-ddui-forms/index"; // o desde tu carpeta libs/
|
|
164
|
+
|
|
165
|
+
const router = createRouter({
|
|
166
|
+
menu: (player) => ({
|
|
167
|
+
title: "Menú",
|
|
168
|
+
content: [
|
|
169
|
+
{ type: "button", label: "Mi perfil", to: { view: "profile", params: { name: player.name } } },
|
|
170
|
+
{ type: "button", label: "Ajustes", to: "settings" },
|
|
171
|
+
{ type: "close-button" }
|
|
172
|
+
]
|
|
173
|
+
}),
|
|
174
|
+
|
|
175
|
+
profile: (player, params) => ({
|
|
176
|
+
title: `Perfil: ${params.name}`,
|
|
177
|
+
content: [
|
|
178
|
+
{ type: "label", text: "Todavía no hay nada aquí." },
|
|
179
|
+
{ type: "button", label: "Volver", to: "back" }
|
|
180
|
+
]
|
|
181
|
+
}),
|
|
182
|
+
|
|
183
|
+
settings: (player) => ({
|
|
184
|
+
title: "Ajustes",
|
|
185
|
+
content: [
|
|
186
|
+
{ type: "toggle", label: "Notificaciones", toggled: new ObservableBoolean(true, { clientWritable: true }) },
|
|
187
|
+
{ type: "button", label: "Volver", to: "back" }
|
|
188
|
+
]
|
|
189
|
+
})
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Iniciar un flujo
|
|
193
|
+
router.open(player, "menu");
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Cada vista es una función `(player, params) => formData`. Devuelve el mismo objeto que le darías a `customFormRender`, incluido `onExit`.
|
|
197
|
+
|
|
198
|
+
### Valores que acepta `to`
|
|
199
|
+
|
|
200
|
+
| `to` | Resultado |
|
|
201
|
+
| --- | --- |
|
|
202
|
+
| `"settings"` | Abre la vista registrada con ese nombre. |
|
|
203
|
+
| `{ view: "profile", params: { ... } }` | Lo mismo, pasándole `params` a la vista. |
|
|
204
|
+
| `"back"` | Vuelve a la vista anterior del historial del jugador. Si no hay ninguna, el formulario simplemente se cierra. |
|
|
205
|
+
| una función | Funciona como el `to` normal (ver [Navegación](#navegación)), pero queda **fuera** del router: el historial del jugador se descarta. |
|
|
206
|
+
|
|
207
|
+
Si usas el nombre de una vista que no registraste, el router lanza `Unknown View: "nombre"`.
|
|
208
|
+
|
|
209
|
+
### Parámetros
|
|
210
|
+
|
|
211
|
+
`params` es el segundo argumento de la función de la vista. Puedes pasarlos desde un botón (`to: { view, params }`) o al iniciar el flujo (`router.open(player, "profile", { name: "Alex" })`).
|
|
212
|
+
|
|
213
|
+
Qué pasas es decisión tuya. El router guarda los `params` de cada paso en el historial y los vuelve a usar al volver atrás, de modo que la vista se reconstruye con los mismos `params`. Por eso los datos simples (nombres, ids, números) son la opción más segura: una entidad o un objeto de jugador guardado ahí puede dejar de ser válido cuando el jugador regrese a esa vista.
|
|
214
|
+
|
|
215
|
+
Volver atrás **reconstruye** la vista llamando otra vez a su función, así que los valores que el jugador escribió (tus `Observable`) no se conservan: vuelven a sus valores iniciales.
|
|
216
|
+
|
|
217
|
+
### Opciones del router
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
const router = createRouter(views, { onInvalid: "skip" });
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
El segundo argumento acepta las mismas opciones que el tercer argumento de `customFormRender` (ver [Validación y mensajes de error](#validación-y-mensajes-de-error)) y se aplica a todas las vistas.
|
|
224
|
+
|
|
225
|
+
### Qué devuelve `open`
|
|
226
|
+
|
|
227
|
+
`router.open(...)` devuelve una promesa que se resuelve con el motivo de cierre de **la vista que abrió**, no de todo el flujo. Cuando un botón navega, el servidor cierra esa vista, así que se resuelve con `ServerClosed`. El `onExit` de cada vista sigue la misma regla descrita en [Cerrar una vista](#cerrar-una-vista-onexit).
|
|
228
|
+
|
|
229
|
+
### Vida del historial y limpieza
|
|
230
|
+
|
|
231
|
+
El router guarda en memoria el historial de cada jugador. Se borra cuando:
|
|
232
|
+
|
|
233
|
+
- el jugador cierra el formulario,
|
|
234
|
+
- un formulario se cierra desde código sin navegar (por ejemplo `this.close()` dentro de `onClick`),
|
|
235
|
+
- vuelves a llamar a `router.open(...)`: un flujo nuevo siempre empieza limpio,
|
|
236
|
+
- llamas a `router.clear(playerId)`.
|
|
237
|
+
|
|
238
|
+
Si un jugador sale del servidor a mitad de un flujo, su historial queda en memoria hasta que ocurra una de esas cosas. Es una entrada pequeña por jugador, pero puedes liberarla cuando se vaya:
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
import { world } from "@minecraft/server";
|
|
242
|
+
|
|
243
|
+
world.afterEvents.playerLeave.subscribe(({ playerId }) => router.clear(playerId));
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
La librería no se suscribe a este evento por ti, a propósito:
|
|
247
|
+
|
|
248
|
+
- así la librería depende solo de `@minecraft/server-ui`,
|
|
249
|
+
- crear un router no engancha ningún evento global,
|
|
250
|
+
- tú decides cómo y cuándo limpiar según tu addon.
|
|
251
|
+
|
|
252
|
+
Suscríbete **una sola vez** (como arriba), no una vez por vista: una suscripción por vista se iría acumulando mientras los jugadores sigan conectados.
|
|
253
|
+
|
|
254
|
+
## Leer valores
|
|
255
|
+
|
|
256
|
+
Los valores son los `Observable` que tú creas, así que guarda una referencia y léelos cuando los necesites:
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
const createPage = (player) => {
|
|
260
|
+
const name = new ObservableString("", { clientWritable: true });
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
title: "Renombrar",
|
|
264
|
+
content: [
|
|
265
|
+
{ type: "text-field", label: "Nuevo nombre", text: name },
|
|
266
|
+
{
|
|
267
|
+
type: "button",
|
|
268
|
+
label: "Guardar",
|
|
269
|
+
onClick: function () { rename(player, name.getData()); },
|
|
270
|
+
to: () => openMainMenu(player)
|
|
271
|
+
}
|
|
272
|
+
]
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Cerrar una vista: `onExit`
|
|
278
|
+
|
|
279
|
+
`formData.onExit` se ejecuta cuando el formulario se cierra, por cualquier motivo excepto `UserBusy` (cuando el formulario no pudo mostrarse). `this` es el `CustomForm` y el primer argumento es el `DataDrivenScreenClosedReason`:
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
{
|
|
283
|
+
title: "Ajustes",
|
|
284
|
+
content: [ /* ... */ ],
|
|
285
|
+
onExit(reason) {
|
|
286
|
+
// reason: ClientClosed | ServerClosed
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Cuando un botón navega con `to`, el servidor cierra el formulario actual, así que su `onExit` también se ejecuta, con `ServerClosed`, **después** de que `to` ya abrió la siguiente vista. Revisa `reason` si solo quieres reaccionar cuando el jugador cierra el formulario (`ClientClosed`).
|
|
292
|
+
|
|
293
|
+
## Validación y mensajes de error
|
|
294
|
+
|
|
295
|
+
Antes de abrir una vista, `customFormRender` revisa cada elemento y reporta **todos** los problemas que encuentra en un solo mensaje en la consola, con el lugar exacto de cada uno:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
[bedrock-ddui-forms] View "Stored Inventory": 3 errors, 2 warnings
|
|
299
|
+
ERROR content[0] slider "Protection duration" > label: is required. Expected string | ObservableString | ObservableUIRawMessage | UIRawMessage.
|
|
300
|
+
WARNING content[0] slider "Protection duration" > text: unknown property "text". Valid properties: label, value, min, max, options.
|
|
301
|
+
WARNING content[0] slider "Protection duration" > options.steps: unknown option "steps". Did you mean "step"?
|
|
302
|
+
ERROR content[1] textfield "Owner" > type: unknown type "textfield". Did you mean "text-field"?
|
|
303
|
+
ERROR content[2] toggle "Keep armor" > toggled: expected ObservableBoolean, received boolean true. Create it with new ObservableBoolean(<initial value>, { clientWritable: true }).
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Los mensajes están en inglés, igual que el resto de la salida de la librería.
|
|
307
|
+
|
|
308
|
+
- Los **errores** dejan el elemento inutilizable. Las **advertencias** señalan equivocaciones probables (propiedades u opciones desconocidas, un valor de dropdown que ningún item tiene, `min` mayor que `max`, un botón sin acción...) y no detienen nada.
|
|
309
|
+
- Los errores de escritura reciben una sugerencia ("Did you mean ...?"), y si pasas un valor simple donde se necesita un `Observable`, el mensaje explica cómo crearlo.
|
|
310
|
+
- También se detecta un error muy común: `to: openMenu(player)` llama a la función en vez de pasarla (`to: () => openMenu(player)`).
|
|
311
|
+
- Los errores lanzados dentro de `onClick`, `to`, `image.options.onClick` y `onExit` se registran con el elemento al que pertenecen, en vez de fallar en silencio.
|
|
312
|
+
|
|
313
|
+
Qué pasa con un elemento con errores lo decides tú, con el tercer argumento de `customFormRender`:
|
|
314
|
+
|
|
315
|
+
```js
|
|
316
|
+
customFormRender(player, formData, { onInvalid: "label" }); // por defecto
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
| `onInvalid` | Comportamiento |
|
|
320
|
+
| --- | --- |
|
|
321
|
+
| `"label"` | Muestra un marcador visible en su lugar (útil mientras desarrollas). |
|
|
322
|
+
| `"skip"` | Deja el elemento fuera (mejor en producción o con contenido que viene de fuera). |
|
|
323
|
+
| `"throw"` | No abre el formulario y lanza un `FormValidationError` (su propiedad `issues` lista todos los problemas). |
|
|
324
|
+
|
|
325
|
+
Si el problema está en la vista misma (falta `title`, `content` no es un arreglo, no hay `player`), el formulario no se puede construir: se registra el reporte y `customFormRender` devuelve `undefined`.
|
|
326
|
+
|
|
327
|
+
También puedes validar sin renderizar, por ejemplo en una prueba o con vistas que vienen de un servidor:
|
|
328
|
+
|
|
329
|
+
```js
|
|
330
|
+
import { validateForm } from "bedrock-ddui-forms";
|
|
331
|
+
|
|
332
|
+
const report = validateForm(formData, player);
|
|
333
|
+
report.errors; // problemas que rompen un elemento
|
|
334
|
+
report.warnings; // equivocaciones probables
|
|
335
|
+
report.message; // el texto mostrado arriba
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Valor de retorno
|
|
339
|
+
|
|
340
|
+
`customFormRender(player, formData)` devuelve una promesa que se resuelve con el `DataDrivenScreenClosedReason` (`ClientClosed`, `ServerClosed` o `UserBusy`), o `undefined` si la vista no se pudo construir o hubo un error al mostrarla. Un resultado `UserBusy` además registra una advertencia.
|
|
341
|
+
|
|
342
|
+
## Estructura del proyecto
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
index.js punto de entrada público
|
|
346
|
+
core/
|
|
347
|
+
customFormRender.js renderiza una vista y la muestra al jugador
|
|
348
|
+
propertiesFactory.js asocia cada `type` con su llamada a CustomForm
|
|
349
|
+
validation.js revisa las vistas y construye los mensajes de error
|
|
350
|
+
createRouter.js vistas con nombre y historial de "atrás" por jugador
|
|
351
|
+
models/properties/
|
|
352
|
+
*.js typedefs de JSDoc de cada elemento
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Añadir un tipo de elemento nuevo
|
|
356
|
+
|
|
357
|
+
1. Crea su typedef en `models/properties/` y añádelo a la unión `Property`.
|
|
358
|
+
2. Escribe su función en `propertiesFactory.js`.
|
|
359
|
+
3. Regístrala en la tabla `properties`.
|
|
360
|
+
|
|
361
|
+
## Aviso legal
|
|
362
|
+
|
|
363
|
+
Este proyecto no está afiliado, respaldado ni patrocinado por Mojang Studios ni Microsoft.
|