roleplay-data 0.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/CREDITS.md +34 -0
- package/LICENSE +30 -0
- package/README.md +163 -0
- package/assets/README.md +38 -0
- package/catalog/items.json +3177 -0
- package/catalog/schemas/item.schema.json +133 -0
- package/catalog/schemas/theme-registry.schema.json +52 -0
- package/catalog/schemas/theme.schema.json +83 -0
- package/catalog/schemas/vehicle.schema.json +116 -0
- package/catalog/vehicles.json +273 -0
- package/package.json +50 -0
- package/src/index.d.ts +182 -0
- package/src/index.js +131 -0
- package/src/localize.d.ts +31 -0
- package/src/localize.js +28 -0
- package/src/resolve.d.ts +29 -0
- package/src/resolve.js +162 -0
- package/themes/dayz.json +56 -0
- package/themes/gta-artwork.json +16 -0
- package/themes/themes.json +25 -0
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "roleplay-data",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Base de données RP open source : items, véhicules, thèmes (images + renommages) et i18n (fr/en requis). Source de vérité unique, consommable en package npm, seed SQL/MongoDB ou archive d'assets.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"types": "src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js",
|
|
10
|
+
"./items.json": "./catalog/items.json",
|
|
11
|
+
"./vehicles.json": "./catalog/vehicles.json",
|
|
12
|
+
"./themes": "./themes/themes.json",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"catalog",
|
|
17
|
+
"themes",
|
|
18
|
+
"src",
|
|
19
|
+
"assets/README.md",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CREDITS.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"validate": "node scripts/validate.mjs",
|
|
26
|
+
"build": "node scripts/build-release.mjs",
|
|
27
|
+
"build:assets": "node scripts/build-release.mjs --with-assets",
|
|
28
|
+
"migrate:items": "node scripts/migrate-items.mjs",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"roleplay",
|
|
33
|
+
"rp",
|
|
34
|
+
"items",
|
|
35
|
+
"vehicles",
|
|
36
|
+
"gta",
|
|
37
|
+
"discord-bot",
|
|
38
|
+
"fivem",
|
|
39
|
+
"database",
|
|
40
|
+
"i18n",
|
|
41
|
+
"themes"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"typescript": "^5.5.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types publics du package roleplay-data.
|
|
3
|
+
* Chaque type reflète fidèlement le schéma JSON correspondant (catalog/schemas/*).
|
|
4
|
+
*/
|
|
5
|
+
export type Locale = 'fr' | 'en' | (string & {});
|
|
6
|
+
|
|
7
|
+
/** Objet texte localisé. fr + en sont toujours présents. */
|
|
8
|
+
export type Localized = {
|
|
9
|
+
fr: string;
|
|
10
|
+
en: string;
|
|
11
|
+
[locale: string]: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type DrugProperties = {
|
|
15
|
+
type: string;
|
|
16
|
+
duration: number;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Toute propriété libre laissée au projet consommateur. */
|
|
21
|
+
export type CustomProperties = Record<string, unknown>;
|
|
22
|
+
|
|
23
|
+
export type ItemMetadata = {
|
|
24
|
+
/** Identité du monde réel (modèle d'arme, calibre, fabricant…). */
|
|
25
|
+
irl?: Record<string, unknown>;
|
|
26
|
+
/** Provenance de l'item. */
|
|
27
|
+
source?: string;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type Item = {
|
|
32
|
+
/** Identifiant canonique (snake_case). Ne change jamais. */
|
|
33
|
+
id: string;
|
|
34
|
+
name: Localized;
|
|
35
|
+
description: Localized;
|
|
36
|
+
/** Fichier dans le pack d'images par défaut (themes/gta-artwork). */
|
|
37
|
+
image: string;
|
|
38
|
+
emoji?: string;
|
|
39
|
+
weight: number;
|
|
40
|
+
food: number;
|
|
41
|
+
thirst: number;
|
|
42
|
+
drug: DrugProperties | null;
|
|
43
|
+
stackable: boolean;
|
|
44
|
+
usable: boolean;
|
|
45
|
+
tags: string[];
|
|
46
|
+
metadata: ItemMetadata;
|
|
47
|
+
customProperties: CustomProperties;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type ItemsCatalog = {
|
|
51
|
+
items: Item[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type VehicleCategory = 'car' | 'bike' | 'truck' | 'special';
|
|
55
|
+
|
|
56
|
+
export type Vehicle = {
|
|
57
|
+
id: string;
|
|
58
|
+
brand: Localized;
|
|
59
|
+
model: Localized;
|
|
60
|
+
category: VehicleCategory;
|
|
61
|
+
image: string;
|
|
62
|
+
seats: number;
|
|
63
|
+
massKg: number;
|
|
64
|
+
fuelCapacity: number;
|
|
65
|
+
maxSpeed: number;
|
|
66
|
+
cargo: {
|
|
67
|
+
maxSlots: number;
|
|
68
|
+
maxWeightKg: number;
|
|
69
|
+
};
|
|
70
|
+
metadata: ItemMetadata;
|
|
71
|
+
customProperties: CustomProperties;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type VehiclesCatalog = {
|
|
75
|
+
vehicles: Vehicle[];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type ThemeScope = 'items' | 'vehicles';
|
|
79
|
+
|
|
80
|
+
export type ThemeRegistryEntry = {
|
|
81
|
+
label: Localized;
|
|
82
|
+
description?: Localized;
|
|
83
|
+
scope: ThemeScope[];
|
|
84
|
+
image?: string;
|
|
85
|
+
license?: string;
|
|
86
|
+
attribution?: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type ThemeRegistry = {
|
|
90
|
+
themes: Record<string, ThemeRegistryEntry>;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type ThemeOverride = {
|
|
94
|
+
id: string;
|
|
95
|
+
name?: Localized;
|
|
96
|
+
description?: Localized;
|
|
97
|
+
image?: string;
|
|
98
|
+
emoji?: string;
|
|
99
|
+
weight?: number;
|
|
100
|
+
food?: number;
|
|
101
|
+
thirst?: number;
|
|
102
|
+
drug?: DrugProperties | null;
|
|
103
|
+
stackable?: boolean;
|
|
104
|
+
usable?: boolean;
|
|
105
|
+
tags?: string[];
|
|
106
|
+
metadata?: ItemMetadata;
|
|
107
|
+
customProperties?: CustomProperties;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type ThemeExclusiveItem = ThemeOverride & {
|
|
111
|
+
id: string;
|
|
112
|
+
name: Localized;
|
|
113
|
+
image: string;
|
|
114
|
+
weight: number;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type Theme = {
|
|
118
|
+
overrides: ThemeOverride[];
|
|
119
|
+
exclusiveItems: ThemeExclusiveItem[];
|
|
120
|
+
virtualIds?: string[];
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/* ------------------------------------------------------------------ *
|
|
124
|
+
* Fonctions runtime (implémentées dans index.js / localize.js / resolve.js)
|
|
125
|
+
* ------------------------------------------------------------------ */
|
|
126
|
+
|
|
127
|
+
/** Liste ordonnée des items canoniques. */
|
|
128
|
+
export function getItems(): Item[];
|
|
129
|
+
/** Item canonique par id, ou null. */
|
|
130
|
+
export function getItem(id: string | null | undefined): Item | null;
|
|
131
|
+
/** Liste ordonnée des modèles de véhicules. */
|
|
132
|
+
export function getVehicles(): Vehicle[];
|
|
133
|
+
/** Véhicule par id, ou null. */
|
|
134
|
+
export function getVehicle(id: string | null | undefined): Vehicle | null;
|
|
135
|
+
/** Registre des thèmes (id → métadonnées). */
|
|
136
|
+
export function getThemeRegistry(): Record<string, ThemeRegistryEntry>;
|
|
137
|
+
/** Ids des thèmes (thème par défaut en premier), filtrés par scope. */
|
|
138
|
+
export function listThemeIds(scope?: ThemeScope): string[];
|
|
139
|
+
/** Thème complet (overrides + exclusifs) par id — thème inconnu = vide. */
|
|
140
|
+
export function getTheme(themeId: string | null | undefined): Theme;
|
|
141
|
+
/** Item résolu pour un thème (override + exclusif gérés), ou null. */
|
|
142
|
+
export function getThemedItem(itemId: string, themeId?: string | null | undefined): Item | null;
|
|
143
|
+
/** Nom localisé effectif d'un item pour un thème et une locale. */
|
|
144
|
+
export function getThemedItemName(itemId: string, themeId: string, locale?: string): string;
|
|
145
|
+
/** Meilleure image disponible : thème → gta-artwork → no_image. */
|
|
146
|
+
export function getItemImage(
|
|
147
|
+
itemId: string,
|
|
148
|
+
themeId?: string | null | undefined,
|
|
149
|
+
opts?: { assetsRoot?: string }
|
|
150
|
+
): string;
|
|
151
|
+
/** Libère les caches (rechargement à chaud). */
|
|
152
|
+
export function clearCache(): void;
|
|
153
|
+
|
|
154
|
+
/** Outils i18n. */
|
|
155
|
+
export function translate(localized: Localized | null | undefined, locale?: string): string;
|
|
156
|
+
export function itemLocalizedName(
|
|
157
|
+
item: { name?: Localized } | null | undefined,
|
|
158
|
+
locale?: string
|
|
159
|
+
): string;
|
|
160
|
+
export function itemLocalizedDescription(
|
|
161
|
+
item: { description?: Localized } | null | undefined,
|
|
162
|
+
locale?: string
|
|
163
|
+
): string;
|
|
164
|
+
export function vehicleLocalizedName(
|
|
165
|
+
vehicle: { brand?: Localized; model?: Localized } | null | undefined,
|
|
166
|
+
locale?: string
|
|
167
|
+
): string;
|
|
168
|
+
|
|
169
|
+
/** Résolution de thèmes (packs d'images + renommages + exclusifs). */
|
|
170
|
+
export const DEFAULT_IMAGE_THEME: string;
|
|
171
|
+
export const NO_IMAGE: string;
|
|
172
|
+
export function normalizeThemeId(raw: unknown): string;
|
|
173
|
+
export function resolveThemedImage(
|
|
174
|
+
imageFile: string | null | undefined,
|
|
175
|
+
themeId?: string | null | undefined,
|
|
176
|
+
opts?: { assetsRoot?: string }
|
|
177
|
+
): string;
|
|
178
|
+
|
|
179
|
+
/** Namespaces utilitaires (équivalents « module » des JS compacts). */
|
|
180
|
+
import * as localizeNs from './localize.js';
|
|
181
|
+
import * as resolverNs from './resolve.js';
|
|
182
|
+
export { localizeNs as localize, resolverNs as resolver };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* roleplay-data — source de vérité RP (items, véhicules, thèmes).
|
|
3
|
+
*
|
|
4
|
+
* Ce module charge les données JSON du package (catalog/ + themes/) et expose
|
|
5
|
+
* une API typée pour les consommer : recherche d'item/véhicule, résolution
|
|
6
|
+
* d'images et de renommages par thème, i18n.
|
|
7
|
+
*
|
|
8
|
+
* Les données sont mises en cache en mémoire la première fois qu'elles sont
|
|
9
|
+
* lues — rechargement possible via clearCache().
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { dirname, join, resolve as pathResolve } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import * as localize from './localize.js';
|
|
16
|
+
import * as resolver from './resolve.js';
|
|
17
|
+
|
|
18
|
+
export { localize, resolver };
|
|
19
|
+
export {
|
|
20
|
+
DEFAULT_IMAGE_THEME,
|
|
21
|
+
NO_IMAGE,
|
|
22
|
+
normalizeThemeId,
|
|
23
|
+
resolveThemedImage,
|
|
24
|
+
} from './resolve.js';
|
|
25
|
+
export {
|
|
26
|
+
localize as translate,
|
|
27
|
+
itemLocalizedName,
|
|
28
|
+
itemLocalizedDescription,
|
|
29
|
+
vehicleLocalizedName,
|
|
30
|
+
} from './localize.js';
|
|
31
|
+
|
|
32
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const REPO_ROOT = pathResolve(here, '..');
|
|
34
|
+
|
|
35
|
+
const cache = {
|
|
36
|
+
items: null,
|
|
37
|
+
vehicles: null,
|
|
38
|
+
registry: null,
|
|
39
|
+
themes: new Map(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function readJson(rel) {
|
|
43
|
+
return JSON.parse(readFileSync(join(REPO_ROOT, rel), 'utf-8'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function lazy(key, rel) {
|
|
47
|
+
if (cache[key] === null) cache[key] = readJson(rel);
|
|
48
|
+
return cache[key];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Liste ordonnée des items canoniques. */
|
|
52
|
+
export function getItems() {
|
|
53
|
+
return lazy('items', 'catalog/items.json').items;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Item canonique par id, ou null. */
|
|
57
|
+
export function getItem(id) {
|
|
58
|
+
if (!id) return null;
|
|
59
|
+
return getItems().find((i) => i.id === id) || null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Liste ordonnée des modèles de véhicules. */
|
|
63
|
+
export function getVehicles() {
|
|
64
|
+
return lazy('vehicles', 'catalog/vehicles.json').vehicles;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Véhicule par id, ou null. */
|
|
68
|
+
export function getVehicle(id) {
|
|
69
|
+
if (!id) return null;
|
|
70
|
+
return getVehicles().find((v) => v.id === id) || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Registre des thèmes (métadonnées : label, scope, licence, attribution…). */
|
|
74
|
+
export function getThemeRegistry() {
|
|
75
|
+
return lazy('registry', 'themes/themes.json').themes;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Ids des thèmes, triés avec le thème par défaut en premier. */
|
|
79
|
+
export function listThemeIds(scope = 'items') {
|
|
80
|
+
const registry = getThemeRegistry();
|
|
81
|
+
const ids = Object.keys(registry).filter(
|
|
82
|
+
(id) => !scope || !Array.isArray(registry[id]?.scope) || registry[id].scope.includes(scope)
|
|
83
|
+
);
|
|
84
|
+
return ids.sort((a, b) => {
|
|
85
|
+
if (a === resolver.DEFAULT_IMAGE_THEME) return -1;
|
|
86
|
+
if (b === resolver.DEFAULT_IMAGE_THEME) return 1;
|
|
87
|
+
return a.localeCompare(b);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Thème complet (overrides + exclusifs) par id. Fichier introuvable → thème vide. */
|
|
92
|
+
export function getTheme(themeId) {
|
|
93
|
+
const id = resolver.normalizeThemeId(themeId);
|
|
94
|
+
if (cache.themes.has(id)) return cache.themes.get(id);
|
|
95
|
+
let theme = null;
|
|
96
|
+
try {
|
|
97
|
+
theme = readJson(`themes/${id}.json`);
|
|
98
|
+
} catch {
|
|
99
|
+
theme = { overrides: [], exclusiveItems: [] };
|
|
100
|
+
}
|
|
101
|
+
cache.themes.set(id, theme);
|
|
102
|
+
return theme;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Item résolu pour un thème (override + exclusif gérés), ou null. */
|
|
106
|
+
export function getThemedItem(itemId, themeId) {
|
|
107
|
+
return resolver.resolveThemedItem(getItems(), itemId, getTheme(themeId));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Nom localisé effectif d'un item pour un thème et une locale. */
|
|
111
|
+
export function getThemedItemName(itemId, themeId, locale) {
|
|
112
|
+
const themed = getThemedItem(itemId, themeId);
|
|
113
|
+
return localize.itemLocalizedName(themed, locale);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Meilleure image disponible pour un item/thème : thème → gta-artwork → no_image. */
|
|
117
|
+
export function getItemImage(itemId, themeId, { assetsRoot } = {}) {
|
|
118
|
+
const item = getThemedItem(itemId, themeId) || getItem(itemId);
|
|
119
|
+
if (!item) return resolver.NO_IMAGE;
|
|
120
|
+
const root = assetsRoot ?? join(REPO_ROOT, 'assets');
|
|
121
|
+
return resolver.resolveThemedImage(item.image, themeId, { assetsRoot: root });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Libère tous les caches (utile en rechargement à chaud). */
|
|
125
|
+
export function clearCache() {
|
|
126
|
+
cache.items = null;
|
|
127
|
+
cache.vehicles = null;
|
|
128
|
+
cache.registry = null;
|
|
129
|
+
cache.themes.clear();
|
|
130
|
+
if (typeof resolver._clearExistsCache === 'function') resolver._clearExistsCache();
|
|
131
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Déclarations typées des helpers i18n (implémentation : localize.js).
|
|
3
|
+
* Évite toute circularité : les types structures sont répétés volontairement.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type LocalizedLike = {
|
|
7
|
+
fr?: string;
|
|
8
|
+
en?: string;
|
|
9
|
+
[locale: string]: string | undefined;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Renvoie la traduction d'un objet localisé pour une locale (fallback en). */
|
|
13
|
+
export function localize(localized: LocalizedLike | null | undefined, locale?: string): string;
|
|
14
|
+
|
|
15
|
+
/** Nom localisé d'un item (fallback en). */
|
|
16
|
+
export function itemLocalizedName(
|
|
17
|
+
item: { name?: LocalizedLike } | null | undefined,
|
|
18
|
+
locale?: string
|
|
19
|
+
): string;
|
|
20
|
+
|
|
21
|
+
/** Description localisée d'un item (fallback en). */
|
|
22
|
+
export function itemLocalizedDescription(
|
|
23
|
+
item: { description?: LocalizedLike } | null | undefined,
|
|
24
|
+
locale?: string
|
|
25
|
+
): string;
|
|
26
|
+
|
|
27
|
+
/** Nom localisé brand + model d'un véhicule. */
|
|
28
|
+
export function vehicleLocalizedName(
|
|
29
|
+
vehicle: { brand?: LocalizedLike; model?: LocalizedLike } | null | undefined,
|
|
30
|
+
locale?: string
|
|
31
|
+
): string;
|
package/src/localize.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers de localisation.
|
|
3
|
+
* Conventions : fr + en toujours présents ; la locale demandée retombe sur en.
|
|
4
|
+
*/
|
|
5
|
+
/** Renvoie la traduction d'un objet localisé pour une locale (fallback en). */
|
|
6
|
+
export function localize(localized, locale) {
|
|
7
|
+
const loc = !locale || typeof locale !== 'string' ? 'en' : locale;
|
|
8
|
+
if (!localized) return '';
|
|
9
|
+
return localized[loc] || localized.en || localized.fr || '';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Nom localisé d'un item (fallback en). */
|
|
13
|
+
export function itemLocalizedName(item, locale) {
|
|
14
|
+
return localize(item?.name, locale);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Description localisée d'un item (fallback en). */
|
|
18
|
+
export function itemLocalizedDescription(item, locale) {
|
|
19
|
+
return localize(item?.description, locale);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Nom localisé brand + model d'un véhicule. */
|
|
23
|
+
export function vehicleLocalizedName(vehicle, locale) {
|
|
24
|
+
if (!vehicle) return '';
|
|
25
|
+
const brand = localize(vehicle.brand, locale);
|
|
26
|
+
const model = localize(vehicle.model, locale);
|
|
27
|
+
return `${brand} ${model}`.trim();
|
|
28
|
+
}
|
package/src/resolve.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Déclarations typées de la résolution de thèmes (implémentation : resolve.js).
|
|
3
|
+
* Reflet exact de l'API publique du module.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type ResolveOptions = {
|
|
7
|
+
assetsRoot?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Id du thème d'images de référence (pack par défaut). */
|
|
11
|
+
export const DEFAULT_IMAGE_THEME: string;
|
|
12
|
+
/** Fichier d'image de repli universel. */
|
|
13
|
+
export const NO_IMAGE: string;
|
|
14
|
+
|
|
15
|
+
/** Normalise un id de thème (minuscules, [a-z0-9_-]), tombe sur le thème par défaut. */
|
|
16
|
+
export function normalizeThemeId(raw: unknown): string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Meilleure image : thème → gta-artwork → no_image.
|
|
20
|
+
* Sans assets embarqués, renvoie un chemin URL-able `items/{thème}/{fichier}`.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveThemedImage(
|
|
23
|
+
imageFile: string | null | undefined,
|
|
24
|
+
themeId?: string | null | undefined,
|
|
25
|
+
opts?: ResolveOptions
|
|
26
|
+
): string;
|
|
27
|
+
|
|
28
|
+
/** Items exclusifs d'un thème (non utilisés par l'API publique directement). */
|
|
29
|
+
export function _clearExistsCache(): void;
|
package/src/resolve.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Résolution des thèmes (packs d'images + renommages + items exclusifs).
|
|
3
|
+
* Portage du comportement du bot Amity :
|
|
4
|
+
* - un thème se greffe sur le catalogue sans le modifier ;
|
|
5
|
+
* - les remplacements n'appliquent que les champs présents ;
|
|
6
|
+
* - l'image retombe sur le thème de référence (gta-artwork), puis no_image.
|
|
7
|
+
*
|
|
8
|
+
* Source de vérité des thèmes disponibles : themes/themes.json (le registre).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Id du thème d'images de référence (pack par défaut). */
|
|
12
|
+
export const DEFAULT_IMAGE_THEME = 'gta-artwork';
|
|
13
|
+
|
|
14
|
+
export const NO_IMAGE = 'no_image.png';
|
|
15
|
+
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
|
+
|
|
18
|
+
const existsCache = new Map();
|
|
19
|
+
|
|
20
|
+
function fileExists(fsPath) {
|
|
21
|
+
if (existsCache.has(fsPath)) return existsCache.get(fsPath);
|
|
22
|
+
const ok = existsSync(fsPath);
|
|
23
|
+
existsCache.set(fsPath, ok);
|
|
24
|
+
return ok;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Libère le cache d'existence de fichiers (rechargement à chaud). */
|
|
28
|
+
export function _clearExistsCache() {
|
|
29
|
+
existsCache.clear();
|
|
30
|
+
rootValidCache.clear();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Renvoie le meilleur chemin d'image : thème → gta-artwork → no_image.
|
|
35
|
+
*
|
|
36
|
+
* - `assetsRoot` fourni et valide (le dossier items/gta-artwork y existe) : contrôle
|
|
37
|
+
* fichier par fichier, no_image en ultime recours.
|
|
38
|
+
* - assets NON embarqués (package sans assets) : renvoie un chemin relatif
|
|
39
|
+
* « URL-able » `items/{theme}/{file}` que le consommateur mappe sur son CDN.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveThemedImage(imageFile, themeId, { assetsRoot } = {}) {
|
|
42
|
+
const safeImage = (imageFile || '').trim() || NO_IMAGE;
|
|
43
|
+
const theme = normalizeThemeId(themeId);
|
|
44
|
+
|
|
45
|
+
const candidate = (t) =>
|
|
46
|
+
assetsRoot ? `${assetsRoot}/items/${t}/${safeImage}` : `assets/items/${t}/${safeImage}`;
|
|
47
|
+
|
|
48
|
+
const assetsValid =
|
|
49
|
+
!assetsRoot || assetRootHasPacks(assetsRoot);
|
|
50
|
+
|
|
51
|
+
if (theme && theme !== DEFAULT_IMAGE_THEME) {
|
|
52
|
+
const themed = candidate(theme);
|
|
53
|
+
if (assetsValid && fileExists(themed)) return themed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const base = candidate(DEFAULT_IMAGE_THEME);
|
|
57
|
+
if (assetsValid && fileExists(base)) return base;
|
|
58
|
+
|
|
59
|
+
if (!assetsValid) {
|
|
60
|
+
const t = theme && theme !== DEFAULT_IMAGE_THEME ? theme : DEFAULT_IMAGE_THEME;
|
|
61
|
+
return `items/${t}/${safeImage}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return assetsRoot ? `${assetsRoot}/no_image.png` : `assets/items/${NO_IMAGE}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let rootValidCache = new Map();
|
|
68
|
+
|
|
69
|
+
function assetRootHasPacks(assetsRoot) {
|
|
70
|
+
if (rootValidCache.has(assetsRoot)) return rootValidCache.get(assetsRoot);
|
|
71
|
+
const ok = existsSync(`${assetsRoot}/items/${DEFAULT_IMAGE_THEME}`);
|
|
72
|
+
rootValidCache.set(assetsRoot, ok);
|
|
73
|
+
return ok;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Normalise un id de thème (minuscules, [a-z0-9_-]). */
|
|
77
|
+
export function normalizeThemeId(raw) {
|
|
78
|
+
if (typeof raw !== 'string') return DEFAULT_IMAGE_THEME;
|
|
79
|
+
const normalized = raw.trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
80
|
+
return normalized || DEFAULT_IMAGE_THEME;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Construit un item exclusif complet depuis une entrée de thème. */
|
|
84
|
+
export function buildExclusiveItem(entry) {
|
|
85
|
+
if (!entry || typeof entry.id !== 'string' || !entry.id.trim()) return null;
|
|
86
|
+
if (!entry.name || typeof entry.name !== 'object') return null;
|
|
87
|
+
if (!entry.image || typeof entry.image !== 'string') return null;
|
|
88
|
+
if (!Number.isFinite(entry.weight)) return null;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
id: entry.id,
|
|
92
|
+
name: entry.name,
|
|
93
|
+
description: entry.description || { fr: '', en: '' },
|
|
94
|
+
image: entry.image,
|
|
95
|
+
emoji: entry.emoji || '',
|
|
96
|
+
weight: entry.weight,
|
|
97
|
+
food: entry.food ?? 0,
|
|
98
|
+
thirst: entry.thirst ?? 0,
|
|
99
|
+
drug: entry.drug ?? null,
|
|
100
|
+
stackable: entry.stackable ?? true,
|
|
101
|
+
usable: entry.usable ?? false,
|
|
102
|
+
tags: entry.tags || [],
|
|
103
|
+
metadata: entry.metadata || {},
|
|
104
|
+
customProperties: entry.customProperties || {},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Applique un override sur un item existant (seuls les champs présents remplacent). */
|
|
109
|
+
export function applyOverride(baseItem, override) {
|
|
110
|
+
if (!baseItem || !override) return baseItem;
|
|
111
|
+
const merged = { ...baseItem };
|
|
112
|
+
|
|
113
|
+
for (const field of [
|
|
114
|
+
'name', 'description', 'image', 'emoji', 'weight', 'food', 'thirst',
|
|
115
|
+
'drug', 'stackable', 'usable', 'tags', 'customProperties',
|
|
116
|
+
]) {
|
|
117
|
+
if (override[field] !== undefined) merged[field] = override[field];
|
|
118
|
+
}
|
|
119
|
+
return merged;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Résout un item complet pour un thème donné :
|
|
124
|
+
* - item du catalogue + override du thème (fusion) ;
|
|
125
|
+
* - ou item exclusif du thème si l'id n'existe pas dans le catalogue.
|
|
126
|
+
* Retourne null si introuvable.
|
|
127
|
+
*/
|
|
128
|
+
export function resolveThemedItem(baseItems, itemId, theme) {
|
|
129
|
+
const safeId = (itemId || '').trim();
|
|
130
|
+
if (!safeId) return null;
|
|
131
|
+
|
|
132
|
+
// 1. item exclusif du thème ?
|
|
133
|
+
// 2. override sur un item du catalogue ?
|
|
134
|
+
const themeEntries = theme?.overrides || [];
|
|
135
|
+
const themeExclusives = theme?.exclusiveItems || [];
|
|
136
|
+
|
|
137
|
+
if (baseItems && Array.isArray(baseItems)) {
|
|
138
|
+
// Items uniquement dans le thème : non remplacés par les overrides.
|
|
139
|
+
for (const entry of themeExclusives) {
|
|
140
|
+
if (entry.id === safeId) return buildExclusiveItem(entry);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const base = Array.isArray(baseItems)
|
|
145
|
+
? baseItems.find((i) => i?.id === safeId) || null
|
|
146
|
+
: null;
|
|
147
|
+
|
|
148
|
+
const override = themeEntries.find((e) => e?.id === safeId) || null;
|
|
149
|
+
|
|
150
|
+
if (base && override) return applyOverride(base, override);
|
|
151
|
+
if (base) return base;
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Ids d'items effectivement disponibles dans un thème. */
|
|
156
|
+
export function listThemeItemIds(theme) {
|
|
157
|
+
const ids = new Set((theme?.overrides || []).map((e) => e?.id));
|
|
158
|
+
for (const e of theme?.exclusiveItems || []) {
|
|
159
|
+
ids.add(e?.id);
|
|
160
|
+
}
|
|
161
|
+
return Array.from(ids);
|
|
162
|
+
}
|
package/themes/dayz.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../catalog/schemas/theme.schema.json",
|
|
3
|
+
"$comment": "Thème DayZ (post-apo). Migré depuis item-themes/dayz.json du bot + names.json. Les ids money_* sont des items virtuels (argent propre/sale/faux) gérés en code — déclarés dans virtualIds. Les images non présentes dans assets/items/dayz/ retombent automatiquement sur le pack gta-artwork.",
|
|
4
|
+
"virtualIds": ["money_clean", "money_dirty", "money_fake"],
|
|
5
|
+
"overrides": [
|
|
6
|
+
{
|
|
7
|
+
"id": "burger",
|
|
8
|
+
"name": { "fr": "Conserve DayZ", "en": "DayZ Canned Food" },
|
|
9
|
+
"image": "burger.png"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "small_water_bottle",
|
|
13
|
+
"name": { "fr": "Gourde usée", "en": "Worn Canteen" },
|
|
14
|
+
"image": "small_water_bottle.png"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "money_clean",
|
|
18
|
+
"name": { "fr": "Billets", "en": "Bills" },
|
|
19
|
+
"image": "money.png"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "money_dirty",
|
|
23
|
+
"name": { "fr": "Argent contaminé", "en": "Contaminated Cash" },
|
|
24
|
+
"image": "dirty_money.png"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "money_fake",
|
|
28
|
+
"name": { "fr": "Billets contrefaits", "en": "Counterfeit Bills" },
|
|
29
|
+
"image": "fake_money.png"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"exclusiveItems": [
|
|
33
|
+
{
|
|
34
|
+
"id": "aks74u",
|
|
35
|
+
"name": { "fr": "AKS-74U", "en": "AKS-74U" },
|
|
36
|
+
"description": { "fr": "Un fusil d'assaut soviétique compact.", "en": "A compact Soviet assault rifle." },
|
|
37
|
+
"image": "AKS74U.png",
|
|
38
|
+
"emoji": "🔫",
|
|
39
|
+
"weight": 3500,
|
|
40
|
+
"usable": true,
|
|
41
|
+
"stackable": false,
|
|
42
|
+
"metadata": { "irl": { "model": "Krinkov AKS-74U", "caliber": "5,45 × 39 mm" } },
|
|
43
|
+
"tags": ["weapon"]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "dayz_bandage",
|
|
47
|
+
"name": { "fr": "Bandage improvisé", "en": "Improvised Bandage" },
|
|
48
|
+
"description": { "fr": "Une bande de tissu pour soigner les blessures.", "en": "A strip of cloth to treat wounds." },
|
|
49
|
+
"image": "bandage.png",
|
|
50
|
+
"emoji": "🩹",
|
|
51
|
+
"weight": 60,
|
|
52
|
+
"usable": true,
|
|
53
|
+
"stackable": true
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../catalog/schemas/theme.schema.json",
|
|
3
|
+
"$comment": "Thème GTA V Artwork : pack d'images de référence. EXEMPLE DE RENOMMAGE — le catalogue porte le nom canonique/IRL (metadata.irl donne l'identité réelle), ce thème applique le nom que GTA V donne à l'objet. Chaque item du catalogue dont metadata.irl est rempli peut être renommé ici.",
|
|
4
|
+
"overrides": [
|
|
5
|
+
{
|
|
6
|
+
"id": "weapon_beretta",
|
|
7
|
+
"name": { "fr": "Pistol", "en": "Pistol" },
|
|
8
|
+
"description": { "fr": "Un pistolet semi-automatique fiable.", "en": "A reliable semi-automatic pistol." }
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "weapon_ak47",
|
|
12
|
+
"name": { "fr": "Rifle d'assaut", "en": "Assault Rifle" }
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"exclusiveItems": []
|
|
16
|
+
}
|