Aller au contenu
TLRFRAMEWORK REDM Ma console

API de tlr_core pour les modules

Tous les montants sont des entiers en centimes (150 = $1.50).

Ce que vous pouvez utiliser malgré l'escrow

tlr_core est chiffré par l'escrow Cfx : vous ne pouvez ni lire ni modifier le Lua de client/, server/, target/ et des règles de shared/. L'escrow ne change rien à ce qui se fait depuis une autre ressource :

  • Exports (exports.tlr_core:Nom(...)) côté serveur et client : tous ceux de cette page fonctionnent normalement.
  • Événements : les événements serveur et client émis par tlr_core (tlr:core:ready, tlr:character:loaded...) s'écoutent avec AddEventHandler ; ceux que tlr_core écoute se déclenchent avec TriggerEvent / TriggerServerEvent comme indiqué.
  • Lib tlib : shared_script '@tlr_core/lib/init.lua' dans le manifeste de votre ressource. Le dossier lib/ est livré en clair et peut être lu.
  • Modules : RegisterModule, paramètres, permissions, actions de l'interface (RPC) et sections du panel, décrits plus bas.
  • Données lisibles : config/, locales/, web/ et les fichiers de données de shared/ (voir Configuration).

Les chemins de fichiers server/... ou client/... cités dans cette page indiquent où vit chaque fonction ; ces fichiers sont chiffrés dans la version vendue. Ne copiez pas de fichier de tlr_core dans votre ressource et n'incluez pas ses fichiers chiffrés (@tlr_core/server/...) : l'escrow le refuse. Écrivez un module séparé qui démarre après tlr_core. Guide pas à pas : Créer un module externe.

Objet central

local TLR = exports.tlr_core:GetCoreObject()
exports.tlr_core:IsReady()     -- true quand le démarrage est terminé
exports.tlr_core:GetVersion()  -- '0.1.0'

Événement serveur tlr:core:ready : déclenché quand le cœur est prêt (y compris après un redémarrage de tlr_core).

Joueurs (serveur)

exports.tlr_core:GetPlayer(src)                -- { source, license, character } ou nil (copie)
exports.tlr_core:GetCharacterId(src)           -- id du personnage chargé ou nil
exports.tlr_core:GetPlayerByCitizenId('TLR…')  -- source ou nil

Côté client : exports.tlr_core:GetCharacter().

Économie (serveur)

exports.tlr_core:GetBalance(src, 'cash')                           -- 'cash' ou 'bank'
exports.tlr_core:AddMoney(src, 'bank', 5000, 'Vente de peaux', opId)
exports.tlr_core:RemoveMoney(src, 'cash', 150, 'Achat de pain', opId)
exports.tlr_core:GetPrice(1000)       -- prix de base x multiplicateur du profil
exports.tlr_core:FormatMoney(150)     -- '$1.50'

AddMoney et RemoveMoney retournent true, nouveauSolde ou false, code (INSUFFICIENT_FUNDS, NO_CHARACTER…).

opId : identifiant unique de l'opération (par exemple 'farming:sale:' .. idDeVente). Si le même opId est rejoué, l'opération n'est pas appliquée deux fois.

Métiers

Socle des métiers : catalogue partagé (shared/jobs_catalog.lua, 135 entrées dont 42 activées au départ), règles en Lua pur (shared/jobs.lua, TLR.JobRules), serveur dans server/jobs/ (jobs.lua, duty.lua, activities.lua, locations.lua, boss.lua, alerts.lua), client client/jobs/jobs.lua, section du panel « Métiers ». Guide pour ajouter un métier : docs/integration/metiers.md.

Fusion des valeurs : catalogue < TLRConfig.Jobs (config.lua) < modification en jeu (table tlr_job_overrides). Un métier désactivé ne peut pas être obtenu, ne prend pas de service, ne touche pas de salaire, et ses lieux, coffres, boutiques réservées et points d'activité sont invisibles et refusés par le serveur. unemployed est toujours actif.

Champs d'un métier : name, label, description, category, type ('job'|'gig'|'activity'|'role'|'illegal'|'default'), grades = { { level, label, salary (centimes), boss } }, whitelist, dutyRequired, family ('law'|'medical'|nil), city, active, source ('catalog'|'config'|'panel'), modified, configured.

Exports serveur

exports.tlr_core:GetJob(name)                    -- métier effectif (copie) ou nil
exports.tlr_core:GetJobs()                       -- liste de tous les métiers (activés ou non), triée par libellé
exports.tlr_core:IsJobActive(name)               -- booléen
exports.tlr_core:GetJobFamily(name)              -- 'law' | 'medical' | nil
exports.tlr_core:IsInJobFamily(src, family)      -- le joueur exerce un métier ACTIF de cette famille
exports.tlr_core:SetJob(src, name, level)        -- true ou false, code ('JOB_DISABLED', 'NOT_FOUND', 'NO_CHARACTER'…)

exports.tlr_core:IsOnDuty(src)                   -- booléen
exports.tlr_core:SetDuty(src, on)                -- true ou false, code ('JOB_DISABLED', 'NOT_MEMBER'…)
exports.tlr_core:GetDutyCount(jobOrFamily)       -- nombre de joueurs en service (métier ou famille)
exports.tlr_core:GetOnDutyPlayers(jobOrFamily)   -- liste triée de sources

exports.tlr_core:RegisterJobActivity(name, spec) -- true ou false, message (spec : voir TLR.JobRules.validateActivity)
exports.tlr_core:RemoveJobActivity(name)
exports.tlr_core:ProvideJobItems(list)           -- objets ajoutés au registre (TLR.Items.provide), propriétaire = ressource appelante

exports.tlr_core:SendJobAlert(target, data)      -- target = 'law' | 'medical' | nom de métier ; renvoie le nombre de destinataires
-- data = { title, message, coords = { x, y, z }, sprite (nom de blip), duration (s), kind ('info'|'warn'|'bad'), except (source) }

Exports client (affichage seulement) : exports.tlr_core:GetJobState() → { job, label, grade, gradeLabel, active, family, city, boss, dutyRequired, onDuty } ou nil ; exports.tlr_core:IsOnDuty().

Dans le cœur (serveur)

  • TLR.Jobs : get, raw, all, list, isActive, family, version, grade, label, isMember(src, name, minGrade), inFamily(src, family), familyGroups(family, minGrade), setActive(name, on, actor), save(data, actor, isCreate), reset, delete, setCharacterJob(characterId, name, level, actor), refreshCharacter(src).
  • TLR.JobDuty : isOnDuty, state, push, set(src, on, silent), toggle, clearJob, counts, count, members.
  • TLR.JobActivities : register(name, spec, owner), remove, get, list, provideItems(list, owner), begin, finish, cancel.
  • TLR.JobLocations : get, list(job), visible(src, loc), shopAllowed, save, delete, schema.
  • TLR.JobBoss : open(src, locId), fire, setGrade, offer(src, targetSrc, level), account.
  • TLR.JobAlerts.send(target, data) (champ extra : table transmise telle quelle au client), TLR.JobAlerts.call(src, family, text) (appel d'un joueur, comme /medecin), TLR.JobAlerts.onCall(family, fn(src, coords, text) -> extra).
  • TLR.Vitals : isDead, revive(src, actor, { health = %, log }), heal, addHealth(src, pct), deathState(src), addRespawnRule(name, fn(src, character, elapsed) -> { extra = s, blocked, note }) (médecins : docs/integration/metiers-medical.md).
  • TLR.Shops.addAccessHook(name, fn(src, shop) -> bool) : crochet d'accès aux boutiques (une erreur ou false refuse) ; le socle l'utilise pour les boutiques réservées à un métier.

Événements serveur

tlr:jobs:changed (name) après une modification ou une activation ; tlr:jobs:refresh (src) après un changement de métier ; tlr:jobs:duty (src, job, onDuty) ; tlr:jobs:activity (src, activityName, kind) après une activité réussie. Écouter avec AddEventHandler (ce sont des événements locaux au serveur).

Actions RPC (panel)

jobs.list, jobs.overview, jobs.create, jobs.update (champ version : refus CONFLICT si le métier a changé entre-temps), jobs.setActive { name, active }, jobs.reset { name }, jobs.delete { name } (métiers créés dans le panel seulement), jobs.locations { job }, jobs.locations.save, jobs.locations.delete { id }, jobs.alert { target, title, message?, withPosition? }. Permissions : tlr.jobs.read, tlr.jobs.create, tlr.jobs.update, tlr.jobs.delete, tlr.jobs.toggle, tlr.jobs.locations.update, tlr.jobs.alerts.send (refus par défaut).

Inventaire (serveur)

exports.tlr_core:AddItem(src, 'pain', 3)          -- true ou false, code (NO_SPACE, TOO_HEAVY, NOT_FOUND…)
exports.tlr_core:RemoveItem(src, 'pain', 1)       -- retire d'abord les piles les plus anciennes
exports.tlr_core:GetItemCount(src, 'pain')
exports.tlr_core:CanCarryItem(src, 'pain', 3)     -- vérifie place et poids sans rien modifier
exports.tlr_core:GetItem('pain')                  -- définition de l'objet

Dans le cœur (serveur) :

-- Objet utilisable (remède, piège, arme, journal…) : le gestionnaire décide ; true, true retire un exemplaire.
-- Le 3e argument est le bouton de l'écran d'inventaire (« Utiliser » par défaut) : l'objet n'a pas à être consommable.
TLR.Inventory.onUse('mon_objet', function(src, def, entry) return true, false end, 'Lire')
TLR.Inventory.useAction('mon_objet')              -- libellé proposé, « Consommer » pour un consommable, ou nil
TLR.Inventory.removeEntry(characterId, entryId, qty) -- retire de CETTE pile ; true, copie { item, qty, receivedAt, meta }

Un objet périmé est refusé avant son gestionnaire (EXPIRED). Le don entre joueurs (TLR.JobServices.give(src, cible, objet, qté, entryId)) passe par removeEntry pour les piles à metadata, qui sont conservées.

Événement serveur tlr:inventory:used (src, nomObjet) quand un joueur utilise ou consomme un objet. Côté client : exports.tlr_core:GetNeeds() renvoie { hunger, thirst } (0 à 100).

Images des objets

Champs facultatifs d'un objet (config.lua, panel ou TLR.Items.provide) : image (fichier de web/img/items/, sous-dossier permis, png/jpg/webp/gif/svg, sans ..) et category (nourriture, boisson, arme, munition, outil, ressource, medical, document, divers). L'interface cherche image, puis <nom>.png, puis <nom>.svg, puis génère l'icône de la catégorie avec la première lettre du nom (web/js/lib/itemimg.js). Partagé (Lua) : TLR.Inv.categoryOf(def) (catégorie, déduite si absente : weapon_* arme, ammo_* munition, consommable boisson ou nourriture, sinon divers), TLR.Inv.imageOf(def) (image ou <nom>.png), TLR.Inv.CATEGORIES. Page : itemImage(def), itemLabel(def), itemPicture(def, l, h, pivoté), fallbackSvg(def) dans web/js/lib/itemimg.js. Voir docs/integration/chargement-images.md.

Déclarer un module

local function register()
    local ok, err = exports.tlr_core:RegisterModule({
        id = 'tlr_farming', name = 'Agriculture', version = '1.0.0',
        description = 'Récolte et vente des cultures.',
        requires = { core = '0.1.0' },
        settings = {
            enabled = { type = 'boolean', default = true, label = 'Activer la récolte' },
            harvest_seconds = { type = 'integer', min = 1, max = 120, default = 8, label = 'Durée de récolte (s)' },
            seed_price = { type = 'money', min = 0, max = 100000, default = 250, label = 'Prix des graines' },
        },
    })
    if not ok then print('tlr_farming : ' .. err) end
end
AddEventHandler('tlr:core:ready', register)
CreateThread(function()
    if GetResourceState('tlr_core') == 'started' and exports.tlr_core:IsReady() then register() end
end)

Les paramètres apparaissent dans le panel, section Paramètres, sous les clés tlr_farming.enabled, etc. Les permissions du module sont sous tlr.modules.tlr_farming.*.

exports.tlr_core:GetSetting('tlr_farming.harvest_seconds')
exports.tlr_core:OnSettingChanged('tlr_farming.harvest_seconds', function(value) … end)

Types de paramètres : boolean, integer, number, string, enum (avec choices = { { value, label } }), money, object, array. Options courantes : min, max, default, label, description, category.

Actions de l'interface (RPC)

Une interface de module passe par la même passerelle que le panel : limite de débit, validation du schéma, permission, puis journal. Codes d'erreur stables : PERMISSION_DENIED, VALIDATION_FAILED, RATE_LIMITED, NOT_FOUND, CONFLICT, UNKNOWN_ACTION, NOT_READY, NO_CHARACTER, INSUFFICIENT_FUNDS, INTERNAL.

API des domaines (armes, apparence, stockage, chevaux, boutiques, banque, ordre public, monde)

Armes

ExportRetourRôle
HasLicense(src, type?)boolLicence valide du personnage connecté (type : 'weapon' par défaut, ou 'hunting').
HasLicenseByCharacter(characterId, type?)boolIdem par identifiant de personnage (hors ligne possible).
IssueLicense(characterId, type?, actor?, days?)ok, licence | codeDélivre (gratuit). days nil = réglage, 0 = permanente.
RevokeLicense(licenseId, actor?, reason?)ok, characterId | codeRévoque.
GiveWeapon(src, 'WEAPON_X', { ammoBoxes = n }?)ok, codeAjoute l'objet arme (et des boîtes) à l'inventaire.
RemoveWeapon(src, 'WEAPON_X')ok, codeRetire un objet arme ; désarme si c'était l'arme en main et qu'il n'en reste plus.
GiveAmmo(src, 'AMMO_X', boxes?)ok, codeAjoute des boîtes de munitions.
GetEquippedWeapon(src)'WEAPON_X' | nilArme en main.
GetAmmo(src, 'AMMO_X')entierMunitions portées (selon le serveur).
DisarmPlayer(src)boolRange l'arme en main (ex. zone sans armes).
GetWeapon(name), GetWeapons(), GetAmmoTypes()tableCatalogue (copie).

Export client : GetEquippedWeapon(). Événement serveur émis : tlr:weapons:licenseChanged (characterId). Objets d'inventaire : arme weapon_xxx (identifiant RedM en minuscules, non empilable), munitions ammo_xxx (empilable par 10). Une boutique vend une arme en ajoutant cet objet.

Natives client (client/weapons/weapons.lua, vérifiées avec nat.py)

PLAYER_PED_ID, GIVE_WEAPON_TO_PED, REMOVE_WEAPON_FROM_PED, SET_PED_AMMO_BY_TYPE, GET_PED_AMMO_BY_TYPE, HAS_PED_GOT_WEAPON. Hashes d'armes et de munitions : femga/rdr3_discoveries (weapons/weapons.lua, weapons/ammo_types.lua), recopiés dans shared/weapons.lua. ADD_REASON_DEFAULT / REMOVE_REASON_DEFAULT : énumérations des commentaires de la nativedb.

Apparence

Serveur :

  • exports.tlr_core:GetAppearance(src) → apparence enregistrée (copie, format ci-dessous) ou nil.
  • exports.tlr_core:OpenAppearanceEditor(src) → ouvre l'éditeur (création, ou modification à partir de l'apparence actuelle).
  • exports.tlr_core:OpenClothingShop(src, shopId) → ouvre un magasin (l'achat exige quand même d'être près du point du magasin).

Client :

  • exports.tlr_core:GetAppearance() → apparence enregistrée du personnage local (lecture seule).

Fonctions internes utiles : TLR.Appearance.applyOnSpawn(ped, appearance, sex) et TLR.Appearance.apply(ped, appearance, sex) (client), TLR.Appearance.current(src), .store(src, app), .openCreator(src, opts), .openShop(src, id) (serveur).

Format JSON de tlr_characters.appearance

{ "v": 1, "sex": "male",
  "body": { "tone": 3, "head": 7, "eyes": 2, "teeth": 1, "hair": 5, "hairColor": 2, "beard": 0, "beardColor": 2,
            "body": 2, "bodyType": 1, "waist": 11, "chest": 6, "scars": 0, "ageing": 0, "freckles": 0, "moles": 0, "spots": 0 },
  "clothes": { "hats": 2569388135, "shirts_full": 123456789, "pants": 987654321, "boots": 555555555 } }

body : numéros (1 = premier) dans les listes de shared/appearance_data.lua ; hair/beard = 0 → aucun ; marques = 0 → aucune. clothes : hash (entier non signé 32 bits) de l'élément par catégorie ; catégorie absente = rien. Tout est revalidé par TLR.AppearanceRules.normalize à chaque lecture (élément inconnu ou d'un autre sexe retiré). tlr_outfits.content : { "v": 1, "clothes": { … } } ; character_id NULL + is_start = 1 = tenue de départ du serveur.

Stockage et maisons

FonctionRôle
TLR.Storage.open(src, stashId, { label, cells, maxWeight, check, pointId })Ouvre l'écran du coffre ; l'appelant a vérifié l'accès. maxWeight en kg. check(src) -> bool (facultatif) est rappelé avant chaque transfert (ex. distance au cheval). Retourne ok, code.
TLR.Storage.contents(stashId)Liste { id, item, label, qty, x, y, rot, receivedAt, meta }
TLR.Storage.add(stashId, item, qty, { meta, receivedAt, actor, reason })Ajoute (place et poids vérifiés) -> ok, code
TLR.Storage.remove(stashId, item, qty, { actor, reason })Retire (piles les plus anciennes d'abord) -> ok, code
TLR.Storage.count(stashId, item), TLR.Storage.weight(stashId)Quantité, poids en grammes
TLR.Storage.clear(stashId, actor, reason)Vide un coffre (journalisé)
TLR.Storage.define(stashId, { label, cells, maxWeight })Retient la capacité d'un coffre (kg)
TLR.Storage.registerKind(kind, function(key) return { label, cells, maxWeight } end)Capacité par type de coffre (kg)
TLR.Storage.saddlebagCapacity(){ cells, maxWeight (kg) }
TLR.Storage.kick(stashId)Ferme l'écran de ceux qui regardent ce coffre
TLR.Houses.hasAccess(characterId, houseId)Propriétaire ou clé (utile au domaine Portes)
TLR.Houses.ofCharacter(characterId)Maisons d'un personnage (role = owner ou key)

Événements serveur émis : tlr:storage:transferred (src, { stashId, direction = 'deposit'|'withdraw', item, qty }), tlr:storage:houseFreed (houseId, ancienProprietaireId, raison).

Exports (ressources externes) : OpenStash(src, stashId, opts), GetStashContents(stashId), AddStashItem(stashId, item, qty, meta), RemoveStashItem(stashId, item, qty), GetStashItemCount(stashId, item), GetSaddlebagCapacity(), HasHouseAccess(characterId, houseId), GetCharacterHouses(characterId).

Pour le domaine Chevaux : son appel actuel TLR.Storage.open(src, 'horse:<id>', { label, cells, maxWeight }) est compatible (il lit storage.saddlebag_weight, qui est en kg comme l'attend open). Deux améliorations possibles de son côté : passer check = function(s) ... return près_du_cheval end pour que la distance au cheval soit revérifiée à chaque transfert, et appeler TLR.Storage.clear('horse:<id>', actor, raison) quand un cheval est supprimé.

Chevaux et charrettes

  • exports.tlr_core:GetHorses(src) → liste des chevaux du personnage { id, name, model, stableId, state, health }, ou nil.
  • exports.tlr_core:GetWagons(src) → idem pour les charrettes.
  • exports.tlr_core:GetActiveHorse(src) → identifiant du cheval sorti, ou nil.
  • exports.tlr_core:IsHorseOwner(src, horseId) → vrai si le cheval (vivant) appartient au personnage.

Interne : TLR.Horses (load, stables, offers, owned, getOwned, buy, act, rename, stableView, openStable, adminReturn, adminImpound, adminDelete, rules), TLR.HorsesRules. Événements client : tlr:horses:openStable, tlr:horses:spawn, tlr:horses:despawn, tlr:horses:renamed. Événements serveur (envoyés par le client, revérifiés) : tlr:horses:pos, tlr:horses:died, tlr:horses:call, tlr:horses:saddlebags. Commandes : /cheval, /sacoches.

Boutiques

API publique (serveur, exports.tlr_core:...) :

  • GetShops() → liste { id, name, x, y, z, active, ownerType, ownerJob, openHour, closeHour, open } (utile au futur domaine Blips).
  • GetShop(id) → idem, plus items ({ id, item, price, buyback, stock, maxStock, sort }, prix de base en centimes), ou nil.
  • IsShopOpen(id) → booléen (boutique active et ouverte à l'heure du jeu).
  • Événements serveur : tlr:shops:purchased (src, shopId, lines, total) et tlr:shops:sold (src, shopId, item, qty, total).
  • Dans le cœur : TLR.Shops.buy/sell/ownerSetPrice/ownerRestock/ownerWithdraw/save/delete/clearTill/get/adminList, TLR.ShopRules.*.
  • Actions RPC : shops.view, shops.buy, shops.sell, shops.manage.price, shops.manage.restock, shops.manage.withdraw (joueur) et shops.list, shops.create, shops.update, shops.delete, shops.till.clear, shops.owner.lookup (panel).

Choix annoncés à Rom :

  • Économie : TLR.Economy.price(base) est appliqué aux prix (vente et rachat) des boutiques du serveur quand le paramètre « Appliquer le multiplicateur des prix » est actif (oui par défaut). C'est cohérent avec economy.lua, où le multiplicateur s'applique aux « prix de base déclarés par les modules ». Une boutique de métier ou de joueur garde les prix fixés par son propriétaire, sans multiplicateur.
  • Une boutique du serveur n'a pas de caisse : l'argent des ventes sort du jeu, et le rachat est payé sans caisse. Une boutique de métier ou de joueur a une caisse : les ventes la remplissent, les rachats la vident (refusés si elle est vide).
  • Une boutique de métier ou de joueur a toujours un stock compté (sinon elle créerait des objets).
  • Le rachat ne peut pas dépasser le prix de vente (pas d'argent infini par aller-retour).
  • Les horaires suivent l'heure du jeu ; une plage qui passe minuit est acceptée ; l'heure de fermeture est exclue.
  • Les achats et les ventes sont journalisés dans la catégorie economy ; les modifications du panel dans admin.

Banque et factures

-- Factures (utilisé par le domaine Ordre public pour les amendes)
TLR.Bills.create({ fromJob = 'sheriff' --[[ OU fromCharacterId = id ]], toCharacterId = id, amount = centimes, reason = 'texte',
                   actor = 'libellé', issuerCharacterId = id --[[ facultatif ]], dueDays = n --[[ facultatif ]] })
  --> true, billId | false, code ('VALIDATION_FAILED', 'NOT_FOUND', 'INTERNAL')
  -- bills.max_amount ne s'applique pas à l'API (seulement aux factures des joueurs).
TLR.Bills.pay(character, billId, actor)      --> true, total | false, code
TLR.Bills.cancel(billId, actor)              --> true | false, code
TLR.Bills.forCharacter(characterId)          --> liste (à payer d'abord, puis 10 dernières réglées/annulées)
TLR.Bills.list({ status, characterId, search, limit })

-- Comptes de métier
TLR.JobAccounts.get(job)                     --> { job, balance, withdrawGrade, membersDeposit } | nil
TLR.JobAccounts.balance(job)                 --> centimes
TLR.JobAccounts.change(job, amount, { reason, actor, opId, characterId })  --> true, solde | false, code
TLR.JobAccounts.paySalary(job, characterId, amount, opId, reason)          --> true | false, code

-- Banque
TLR.Bank.deposit(characterId, amount, actor) / TLR.Bank.withdraw(characterId, amount, actor)
TLR.Bank.transfer({ id, name }, citizenId, amount, actor)  --> true, { fee, to } | false, code, detail
TLR.Bank.feeFor(amount)

Exports :

exports.tlr_core:CreateBill(data)                       -- = TLR.Bills.create
exports.tlr_core:GetBills(characterId)
exports.tlr_core:GetJobAccountBalance(job)
exports.tlr_core:AddJobMoney(job, amount, reason, opId)     -- amount > 0 (ou < 0 pour retirer)
exports.tlr_core:RemoveJobMoney(job, amount, reason, opId)  -- amount > 0

Événement client : tlr:bank:open (serveur -> client, { bankId, name }). Commandes : /factures, /banque. Écrans NUI : bank, bills. Journal : catégories bank et bills.

Ordre public

ExportRetour
exports.tlr_core:IsPlayerCuffed(src)true si menotté
exports.tlr_core:IsPlayerJailed(src)true si le personnage purge une peine
exports.tlr_core:GetJailRemaining(src)secondes restantes (0 si aucune peine)
exports.tlr_core:CanLaw(src, action)droit du métier : cuff, search, fine, jail, records
exports.tlr_core:AddCriminalRecord(characterId, kind, label)true, id ; kind : fine, jail, arrest, note, release

Exports client : exports.tlr_core:IsCuffed(), exports.tlr_core:IsJailed().

Dans tlr_core : TLR.Law.isCuffed(src), TLR.Law.isJailed(characterId), TLR.Law.allowed(src, action), TLR.Law.addRecord(characterId, kind, label, { amount, minutes, officer, officerCharacterId }), TLR.Law.jail(characterId, prisonId, minutes, reason, actor, officerLabel, officerCharacterId), TLR.Law.release(characterId, actor, byLabel). État client : TLR.Client.cuffed, TLR.Client.jail.

Événements serveur émis : tlr:law:cuffChanged (src, on), tlr:law:jailed (characterId, prisonId, seconds), tlr:law:released (characterId).

Pour les domaines Armes et Stockage : un joueur menotté ne doit ni s'équiper d'une arme ni ouvrir un coffre ; testez TLR.Law and TLR.Law.isCuffed(src) côté serveur (et TLR.Client.cuffed côté client) avant d'agir.

Portes, zones et blips

-- Blips : pour les boutiques, écuries, banques…
TLR.Blips.set(id, { label = 'Magasin général', x = .., y = .., z = .., sprite = 'blip_shop_store' | hash, job = nil | 'sheriff',
                    source = 'Boutiques' })   -- source facultative : sinon le préfixe de l'id ('shop:3' → 'shop')
-- → true | false, message.  Icône inconnue : avertissement console et icône blip_poi.  L'id ne doit pas commencer par 'manual:'.
TLR.Blips.remove(id)
TLR.Blips.clear(prefix)      -- ex. TLR.Blips.clear('shop:') au rechargement d'un domaine
TLR.Blips.resolveSprite(nomOuHash) -- → hash du catalogue ou nil

-- Zones
TLR.Zones.at(x, y)           -- → { { id, label, noWeapons, noPvp, noHunting }, ... } zones actives contenant la position
TLR.Zones.ofPlayer(src)      -- idem, position du joueur lue par le serveur

-- Portes
TLR.Doors.setLocked(id, locked, actor) -- → true, porte | false, code
TLR.Doors.get(id)
TLR.Doors.canUse(src, porte)

Noms d'icônes utiles : blip_shop_store, blip_shop_gunsmith, blip_shop_tailor, blip_shop_butcher, blip_shop_doctor, blip_shop_barber, blip_stable, blip_shop_horse, blip_proc_bank, blip_saloon, blip_hotel_bed, blip_post_office.

Exemples d'appel à ajouter par les autres domaines (non fait ici : fichiers d'autres domaines) :

  • boutiques : TLR.Blips.set('shop:' .. s.id, { label = s.name, x = s.x, y = s.y, z = s.z, sprite = 'blip_shop_store', source = 'Boutiques' })
  • chevaux : TLR.Blips.set('stable:' .. st.id, { label = st.label, x = st.x, y = st.y, z = st.z, sprite = 'blip_stable', source = 'Chevaux' })
  • banque : TLR.Blips.set('bank:' .. b.id, { ..., sprite = 'blip_proc_bank', source = 'Banque' }) et TLR.Blips.remove(...) à la suppression / désactivation.

Exports

  • Serveur : IsDoorLocked(id), SetDoorLocked(id, locked), GetZonesAt(x, y), SetBlip(id, spec) (source = ressource appelante), RemoveBlip(id).
  • Client : GetCurrentZones() → identifiants des zones où se trouve le joueur local.

Événements réseau

  • Serveur → client : tlr:doors:sync (liste), tlr:doors:state (porte), tlr:doors:release ({ hash, hashJumeau }), tlr:zones:sync (liste, intervalleMs), tlr:blips:sync (liste, style). Aucun événement client → serveur (l'appui sur E passe par tlr:points:use).

Target

Alt maintenu (INPUT_PC_FREE_LOOK) : un œil au centre de l'écran ; ce que vise la caméra (joueur, cheval, personnage, véhicule ou charrette, objet, ou zone) affiche ses options à droite de l'œil. Clic gauche : la liste est épinglée et la souris apparaît ; clic sur une option pour la choisir, Échap ou clic à côté pour fermer. Coupé si le joueur est mort, menotté, dans le menu pause, si un écran ou un composant de la lib a le focus, ou si tlib.target.disableTargeting(true) a été appelé. Fichiers : target/shared/rules.lua (logique pure, TLR.TargetRules), target/client/{main,scan,core}.lua, target/server/{target,actions}.lua, lib/client/target.lua, web/js/target.js, web/css/target.css, web/js/pages/targets.js.

Client : tlib.target (ou exports.tlr_core:<nom>)

-- Une ressource externe : shared_script '@tlr_core/lib/init.lua', puis tlib.target.<nom>(...).
tlib.target.addGlobalHorse({
    { name = 'brosser', label = 'Brosser', icon = 'brush', distance = 2.0,
      canInteract = function(entity, distance, coords, name) return not IsEntityDead(entity) end,
      onSelect = function(data) ... end },
})
tlib.target.addGlobalPed(options)     tlib.target.addGlobalVehicle(options)   -- véhicules et charrettes
tlib.target.addGlobalObject(options)  tlib.target.addGlobalPlayer(options)    -- autres joueurs
tlib.target.addModel({ 'p_trough01x', 'p_trough02x' }, options)               -- nom ou hash, ou liste
tlib.target.addEntity(netId, options)        -- entité réseau (ou liste d'identifiants)
tlib.target.addLocalEntity(entity, options)  -- handle local (ped ou objet créé par le script)
local id = tlib.target.addSphereZone({ coords = vector3(x, y, z), radius = 1.5, name = 'comptoir', options = { ... } })
local id = tlib.target.addBoxZone({ coords = vector3(...), size = vector3(2, 1, 2), rotation = 90.0, options = { ... } })
local id = tlib.target.addPolyZone({ points = { vector3(...), ... }, thickness = 3.0, options = { ... } })

tlib.target.removeZone(id | 'comptoir')
tlib.target.removeModel(modèles, noms)  tlib.target.removeEntity(netIds, noms)  tlib.target.removeLocalEntity(entités, noms)
tlib.target.removeGlobalPed(noms) / removeGlobalHorse / removeGlobalVehicle / removeGlobalObject / removeGlobalPlayer
-- noms : nil (toutes les options de la ressource à cet endroit), 'nom' ou { 'a', 'b' }. Une ressource ne retire que les siennes.
tlib.target.disableTargeting(true | false)   tlib.target.isTargetActive() → booléen (œil affiché)
  • addGlobalPed vise les personnages non joueurs hors chevaux (addGlobalHorse), addGlobalPlayer les autres joueurs.
  • Toutes les options d'une ressource sont retirées à son arrêt (onResourceStop, propriétaire = GetInvokingResource()).
  • Une option de même name au même endroit remplace la précédente.

Champs d'une option :

ChampRôle
labeltexte affiché (obligatoire, 80 caractères)
nameidentifiant (sinon <ressource>:<n°>), sert à remove* et à canInteract
iconnom d'icône de web/js/lib/icons.js (info, door, lock, pick, water, horse, brush, bag, talk, coin, search, walk, bill…)
distancedistance maximale joueur → point visé, en mètres (défaut 3, de 0,5 à 15)
groups'sheriff', { 'sheriff', 'doctor' } ou { sheriff = 2 } (grade minimal)
items'lasso', { 'lasso', 'lanterne' } (tous) ou { pomme = 2 } (quantités)
permissionnœud du cœur (tlr.x.y), refusé par défaut
allowInVehicletrue pour garder l'option quand le joueur est dans un véhicule (sinon masquée)
canInteract(entity, distance, coords, name)doit renvoyer true (appel protégé par pcall)
onSelect(data)fonction appelée au choix
eventévénement client déclenché avec data
serverEventévénement serveur ; sans groups/items/permission : TriggerServerEvent(serverEvent, { netId, coords, args, serverId }), le script doit tout revérifier. Avec conditions : passe par le serveur du cœur (voir ci-dessous)
rpcnom d'une action protégée déclarée côté serveur (registerTargetAction)
argsvaleur libre transmise dans data.args

data reçu par onSelect / event : { entity, coords, distance, name, netId, model, type ('ped'|'horse'|'vehicle'|'object'|'player'|nil), serverId (joueur visé), zone (id), args }.

Côté client, groups lit le métier du personnage, items et permission une réponse du serveur (tlr:target:caps, demandée à l'ouverture de l'œil) : affichage seulement.

Serveur : actions protégées et cibles du panel

-- Dans la ressource qui possède l'action (serveur) :
exports.tlr_core:registerTargetAction('monscript:fouiller', {
    groups = { sheriff = 0 }, items = 'lasso', permission = nil, distance = 2.5,
    handler = function(src, data) ... end,          -- data = { netId, coords, args, distance }
    -- ou serverEvent = 'monscript:fouiller' : TriggerEvent('monscript:fouiller', src, data)
})
exports.tlr_core:removeTargetAction('monscript:fouiller')
-- Dans tlr_core : TLR.Targets.action(nom, spec)

Une option cliente rpc = 'monscript:fouiller' (ou serverEvent = 'monscript:fouiller' avec conditions) envoie tlr:target:use(nom, { netId, coords, args }). Le serveur revérifie, dans l'ordre : délai entre deux choix (750 ms), personnage chargé, pas mort, pas menotté, action déclarée (sinon refus et journal security), distance (position de l'entité réseau lue par le serveur si possible, sinon position visée déclarée, avec 1,5 m de marge), métier/grade, objets (TLR.Inventory.count), permission (TLR.Permissions.has, refus par défaut). Ensuite seulement handler ou l'événement. Écouter spec.serverEvent avec AddEventHandler, jamais RegisterNetEvent : sinon un client pourrait l'appeler directement. Les actions d'une ressource disparaissent à son arrêt.

Cibles du panel (section Monde › Cibles, table tlr_targets, migration 80) : une zone (sphère) ou un modèle, une icône, une distance, des conditions (métier et grade, objet, permission) et une action de la liste blanche :

  • point : utilise un point existant de TLR.Points (boutique, coffre, banque, porte, écurie, vêtements, maison) par TLR.Points.trigger(src, id) : mêmes contrôles que la touche E, dont la distance au point ;
  • teleport : zone seulement ; le serveur envoie tlr:law:teleport (fondu, chargement des collisions) ;
  • message : notification au joueur. La destination et le texte du message ne sont jamais envoyés aux clients avant l'action.
TLR.Targets.list()  TLR.Targets.get(id)  TLR.Targets.save(data, actor)  TLR.Targets.delete(id, actor)  TLR.Targets.sync(src?)
TLR.Targets.mode()  -- 'both' | 'target' | 'key'
TLR.Points.trigger(src, id)   -- utiliser un point avec tous les contrôles (ajouté pour le target)
TLR.Points.list()             -- { { id, kind, label } }

Paramètre interaction.mode (Paramètres › Monde) : both (défaut) garde l'invite E et ajoute les points au target ; target retire l'invite E, les points deviennent des options (choix → tlr:points:use) ; key garde l'invite E et le target ne montre que les cibles sans point E.

Permissions : tlr.targets.read, tlr.targets.update. Actions RPC : targets.list, targets.save, targets.delete.

Événements et messages

  • Serveur → client : tlr:target:sync ({ mode, targets, law }), tlr:target:caps ({ items, perms }).
  • Client → serveur : tlr:target:use (nom, ctx), tlr:targets:use (id, ctx), tlr:target:caps ({ items, perms }).
  • Client (local) : tlr:points:changed (liste des points reçue, TLR.Client.points).
  • NUI (Lua → page) : target:show, target:options ({ title, sub, options = { { label, icon } } }), target:pin, target:hide. Callbacks : target:select ({ index }), target:close.

Usages branchés dans le cœur : « Ouvrir les sacoches » sur son cheval (tlr:horses:saddlebags, distance revérifiée par le serveur) ; « Ordre public » sur un autre joueur pour les métiers ayant un droit d'ordre public (ouvre l'écran /police, qui prend le joueur le plus proche). « Donner un objet » (tous les joueurs), « Brosser » / « Nourrir » sur un cheval et les options des métiers de services sont branchés sur des actions serveur : docs/integration/metiers-services.md.

Lib (logique)

Objet global tlib, équivalent maison d'ox_lib (aucun code repris). Inclusion dans une autre ressource :

shared_script '@tlr_core/lib/init.lua'   -- tlr_core doit démarrer avant

Les modules se chargent au premier accès (tlib.zones, tlib.callback…) depuis tlr_core/lib/<côté>/<nom>.lua puis lib/shared/<nom>.lua. tlib.side ('client' | 'server'), tlib.resource, tlib.inCore, tlib.require('zones', ...) (chargement immédiat). Les fonctions d'interface (notify client, progress, input, alert, context, menu, textui, skillcheck) sont décrites dans la section de l'interface ; dans une autre ressource elles passent par les exports client de tlr_core (exports.tlr_core:notify(data), exports.tlr_core:context('show', id)), contrat en tête de lib/init.lua.

Partagé

tlib.locale.add('fr', { SALUT = 'Bonjour %s' }); tlib.locale.set('fr')
tlib.locale('SALUT', 'Rom')            -- 'Bonjour Rom' ; clé absente → la clé ; 'a.b' lit les tables imbriquées
tlib.table.contains(t, v | liste)  .keys  .values  .count  .isArray  .deepcopy  .equals  .merge(dest, src, deep)
           .map  .filter  .find → valeur, clé  .shuffle  .freeze (lecture seule)
tlib.string.trim  .split(s, sep)  .startsWith  .endsWith  .capitalize  .escapePattern  .truncate(s, n)  .random(n, alphabet?)
tlib.math.round(v, déc)  .clamp  .lerp  .dist(a, b)  .dist2d  .rotate2d(x, y, deg)  .wrapAngle  .groupDigits(1234567) → '1 234 567'
tlib.waitFor(fn, timeout = 5000 | false, interval = 0)   -- valeur | nil, 'TIMEOUT'
local t = tlib.timer(ms, onEnd, autoStart = true)        -- t:pause() :resume() :restart(ms?) :stop() :timeLeft() :isPaused() :isFinished()
tlib.print.info / .ok / .warn / .error / .debug ('%d boutiques', n)   -- debug : convar tlib_debug = true (setr côté client)

Serveur

tlib.callback.register('maressource:prix', function(src, itemId) return 150 end, { rate = { requests = 10, perMs = 5000 } })
local choix, err = tlib.callback.await('maressource:choix', src, ...)                 -- délai max 10 s
local choix, err = tlib.callback.awaitWith('maressource:choix', src, { timeout = 3000 }, ...)
tlib.callback('maressource:choix', src, function(choix, err) end, ...)               -- sans attente
tlib.notify(src, { type = 'ok' | 'bad' | 'warn' | 'info', title, description, duration })   -- ou (src, type, titre, message) ; src -1 = tous
tlib.ratelimit(src, 'cle', 2000)              -- true | false, msRestantes
tlib.ratelimit.bucket(src, 'cle', 10, 5000)   -- seau de jetons ; tlib.ratelimit.reset(src, cle?)
tlib.logger('shops', 'vente', { item = 'peau' }, src)   -- vers TLR.Log ; catégorie inconnue → 'system'

Callbacks : refus par défaut (un nom non enregistré ne répond pas), limite par joueur et par nom (défaut 10 / 5 s) plus 40 / 5 s tous noms confondus, erreurs du handler journalisées et renvoyées comme 'INTERNAL'. Un échec renvoie nil, code : 'TIMEOUT', 'RATE_LIMITED', 'INTERNAL', 'DROPPED' (joueur parti), 'BUSY' (client : plus de 50 demandes en attente). Seul le joueur interrogé peut répondre à une demande du serveur. Les noms sont uniques sur tout le serveur : préfixez-les par la ressource. Événements réseau : tlib:cb:<nom> (demande), tlib:cbr:<ressource> (réponse). tlib.notify serveur utilise l'événement tlr:lib:notify (même affichage que tlib.notify client). Hors de tlr_core, tlib.logger passe par exports.tlr_core:GetCoreObject().Log (obtenu une fois).

Client

local prix, err = tlib.callback.await('maressource:prix', itemId)        -- awaitWith(nom, { timeout }, ...)
tlib.callback.throttle('maressource:prix', 1000)                         -- limite locale, sans envoi
tlib.callback.register('maressource:choix', function(...) return x end) -- répond au serveur

local p = tlib.prompt.press('E', 'Fouiller')      -- ou .hold('E', 'Dépecer', 1500) / .create({ key, label, hold, group })
p:completed()  p:justPressed()  p:progress()  p:reset()  p:setLabel(t)  p:setEnabled(b)  p:setVisible(b)  p:remove()
local g = tlib.prompt.group('Marchand'); local a = g:add({ key = 'E', label = 'Acheter' }); g:show() -- chaque image ; g:remove()

local z = tlib.zones.sphere({ coords, radius, onEnter, onExit, inside })
local z = tlib.zones.box({ coords, size = vector3(l, p, h), rotation = deg, ... })
local z = tlib.zones.poly({ points = { vector3(...), ... }, thickness = 4.0 | minZ, maxZ, ... })
z:contains(pos)  z:remove()  z.insideZone          -- position vérifiée toutes les 250 ms, inside() à chaque image

local pt = tlib.points.new({ coords, distance, onEnter, onExit, nearby, ... })   -- pt.currentDistance, pt.isClosest, pt:remove()
tlib.points.closest()

tlib.request.model(nomOuHash, 10000)   .animDict(nom)   .textureDict(nom)   -- valeur | nil, 'INVALID' | 'TIMEOUT'
tlib.request.releaseModel(m)  tlib.request.releaseAnimDict(d)

tlib.cache.ped  .playerId  .serverId  .coords  .heading  .mount  .vehicle  .weapon     -- relus toutes les 100 ms
tlib.onCache('mount', function(nouveau, ancien) end)

local hit, entity, endCoords, normal = tlib.raycast.camera(10.0, flags = 511, ignore = ped)
tlib.raycast.fromTo(a, b, flags, ignore)

Touches des invites : E, R, F, G, H, Q, B, SPACE, ENTER, BACKSPACE (hash dans lib/client/prompt.lua, d'après femga) ou un hash de contrôle. Les invites d'une ressource sont supprimées à son arrêt.

À vérifier en jeu : maintien des invites (reset nécessaire après completion ?), titre de groupe, drapeaux 511 du lancer de rayon, paramètres de GetCurrentPedWeapon, passage des fonctions (onSelect…) par les exports, coût de GetCoreObject() pour le journal hors de tlr_core.

Lib (interface)

Composants d'interface de la lib, affichés dans la page NUI unique de tlr_core (web/js/lib/*.js, web/css/lib.css). Côté Lua : TLR.LibUI (lib/client/ui.lua), accessible par tlib.<nom> dans tlr_core et dans les ressources qui chargent @tlr_core/lib/init.lua (elles passent par les exports client de tlr_core).

Focus : notify, progress, textui ne prennent jamais le focus. input, alert, context, menu et skillcheck prennent le clavier (et la souris pour input, alert, context ; clavier seul pour menu et skillcheck) le temps de la réponse, puis rendent l'état précédent : un écran ouvert garde son focus. La pile est tenue par TLR.NUI.pushFocus(clé, souris) / TLR.NUI.popFocus(clé) (client/nui/nui.lua). Échap ferme le composant du dessus uniquement, jamais l'écran en dessous.

Les fonctions marquées « bloquante » attendent la réponse (promise Cfx) : à appeler depuis un thread.

-- Notification (client). type : 'info' | 'ok' | 'warn' | 'bad' (alias success, error, inform, warning).
tlib.notify({ type = 'ok', title = 'Vendu', description = '3 peaux pour 4,50 $', duration = 4500, id = 'vente' })
-- item = 'pain' (facultatif) : l'image de l'objet remplace l'icône du niveau (image, category facultatifs).
-- id : remplace la notification affichée qui porte le même id. L'ancien format { kind, title, message } reste accepté.

-- Progression (bloquante) → true si terminée, false si annulée (touche X, mort, cancelProgress) ou refusée.
if tlib.progress({
    duration = 5000, label = 'Dépeçage…', type = 'bar',     -- ou 'circle'
    canCancel = true, useWhileDead = false,
    anim = { dict = 'mech_skin@sample@base', clip = 'sample_low', flag = 1 },
    prop = { model = 'p_knife01x', bone = 'SKEL_R_Hand', pos = vector3(0.0, 0.0, 0.0), rot = vector3(0.0, 0.0, 0.0) },
    disable = { move = true, sprint = true, combat = true, mount = true, mouse = false },
}) then ... end
-- exports.tlr_core:progressActive()  exports.tlr_core:cancelProgress()

-- Formulaire (bloquante) → tableau des valeurs dans l'ordre des champs, nil si annulé.
local r = tlib.input('Facture', {
    { type = 'money', label = 'Montant', required = true, min = 100 },        -- rend des centimes (entier)
    { type = 'input', label = 'Motif', placeholder = 'Soins' },               -- texte ; nil si vide
    { type = 'number', label = 'Quantité', min = 1, max = 10, default = 1 },  -- nombre
    { type = 'select', label = 'Payable par', options = { { value = 'cash', label = 'Liquide' }, 'banque' } },
    { type = 'checkbox', label = 'Envoyer un reçu', checked = true },         -- booléen
    { type = 'textarea', label = 'Note' },
}, { description = 'Texte sous le titre', submit = 'Envoyer', cancel = 'Annuler', allowCancel = true })
-- exports.tlr_core:closeInput() ferme le formulaire en cours (renvoie nil).

-- Confirmation (bloquante) → 'confirm' | 'cancel'
if tlib.alert({ header = 'Vendre le cheval ?', content = 'C\'est définitif.', confirm = 'Vendre', cancel = 'Annuler' }) == 'confirm' then ... end

-- Menu cliquable (contexte), avec sous-menus. Les fonctions restent côté Lua, la page ne reçoit que le texte.
tlib.context.register({
    id = 'marchand', title = 'Marchand ambulant', icon = 'bag', canClose = true, onExit = function() end,
    options = {
        { title = 'Acheter', icon = 'coin', description = 'Provisions', menu = 'marchand_achat' },
        { title = 'Vendre des peaux', onSelect = function(args) ... end, args = { ... } },
        { title = 'Marchander', disabled = true },
        { title = 'Soins', event = 'monscript:soins', serverEvent = 'monscript:sv:soins', args = 3, keepOpen = false },
    },
})
tlib.context.register({ id = 'marchand_achat', title = 'Acheter', menu = 'marchand', options = {
    { title = 'Pain de campagne', item = 'pain', tag = '0,25 $', onSelect = function() ... end } } })
-- item = nom d'un objet : son image (web/img/items) à la place de l'icône ; image et category facultatifs.
tlib.context.show('marchand')   tlib.context.hide()   tlib.context.getOpen() -- → id ou nil
-- serverEvent n'est qu'une transmission : le serveur revalide tout (droits, distance, argent).

-- Menu au clavier (non bloquant : rappels). ↑ ↓ choisir, ← → valeurs, Entrée valider/cocher, Échap fermer.
tlib.menu.register({
    id = 'garde', title = 'Garde-robe', position = 'left', canClose = true,
    options = {
        { label = 'Chapeau', values = { 'n° 12', { label = 'n° 13', description = 'Feutre' } }, defaultIndex = 1 },
        { label = 'Afficher les bottes', checked = true },
        { label = 'Valider', description = 'Paie 1,20 $', args = { prix = 120 }, close = true },
    },
    onSelected = function(index, scroll, args) end,    -- sélection changée
    onSideScroll = function(index, scroll, args) end,  -- valeur changée
    onCheck = function(index, checked, args) end,
    onClose = function(key) end,                       -- 'Escape' | 'Backspace'
}, function(index, scroll, args) --[[ Entrée ]] end)
tlib.menu.show('garde', 1)   tlib.menu.hide()   tlib.menu.getOpen()   tlib.menu.setOptions('garde', options, index)

-- Bandeau d'aide (sans focus). Un « [E] » en tête devient une touche.
tlib.textui.show('[E] Fouiller', { position = 'left' })   -- 'left' | 'right' | 'top' | 'bottom', icon = 'search'
tlib.textui.hide()   local ouvert, texte = tlib.textui.isOpen()

-- Épreuve d'adresse (bloquante) → true si toutes les manches sont réussies.
local ok = tlib.skillcheck({ 'easy', 'medium', { areaSize = 30, speedMultiplier = 1.6 } }, { 'E' })
-- difficultés : easy/facile (50°, ×1), medium/moyen (40°, ×1,5), hard/difficile (25°, ×1,75) ; 10 manches au plus.
-- exports.tlr_core:skillcheckActive()  exports.tlr_core:cancelSkillcheck()

Icônes disponibles (context, textui) : info ok warn bad door lock pick water horse brush bag talk coin search walk bill chev.

Exports client de tlr_core

Fonctions : notify, progress, progressActive, cancelProgress, input, closeInput, alert, closeAlert, skillcheck, skillcheckActive, cancelSkillcheck. Tables (méthode en premier argument, liste fermée) : context (register, show, hide, getOpen), menu (register, show, hide, getOpen, setOptions), textui (show, hide, isOpen). Exemple : exports.tlr_core:context('show', 'marchand'). Les menus et le bandeau d'une ressource qui s'arrête sont retirés ; sa progression en cours est annulée.

Événement réseau

  • Serveur → client : tlr:lib:notify (data) — même table que tlib.notify. Côté serveur : TriggerClientEvent('tlr:lib:notify', src, { type = 'ok', title = '…', description = '…' }) (utilisé par tlib.notify(src, data) de lib/server). L'ancien tlr:notify ({ kind, title, message }) reste valable et s'affiche avec le même composant.

Messages NUI (Lua → page) et callbacks (page → Lua)

MessageDonnéesRéponse (callback NUI)
lib:notify{ type, title, description, duration, id, item, image, category }—
lib:progress{ action = 'start', id, duration, label, kind = 'bar'|'circle', cancelLabel } / { action = 'stop', id }—
lib:textui{ action = 'show', text, position, icon, key } / { action = 'hide' }—
lib:input{ id, heading, rows, description, submit, cancel, allowCancel } / { action = 'close', id }lib:input { id, values | null }
lib:alert{ id, header, content, confirm, cancel } / { action = 'close', id }lib:alert { id, result }
lib:context{ action = 'show', id, title, icon, back, canClose, options } / { action = 'hide' }lib:context { id, event = 'select'|'back'|'close', index }
lib:menu{ action = 'show', id, title, position, canClose, index, options } / { action = 'hide' }lib:menu { id, event = 'move'|'scroll'|'check'|'select'|'close', index, scroll, checked, key }
lib:skillcheck{ id, rounds = { { area, speed } }, keys } / { action = 'cancel', id }lib:skillcheck { id, ok }

Les index renvoyés par la page commencent à 1. Le message historique notify ({ kind, title, message }) et toast() de web/js/ui.js passent par le composant notify.

À vérifier en jeu : drapeaux de TaskPlayAnim et relance d'une animation en boucle (IsEntityPlayingAnim, type 3), paramètres d'AttachEntityToEntity pour les props, touche d'annulation X (INPUT_SWITCH_SHOULDER) bien lue avec IsDisabledControlJustPressed, attente (Citizen.Await) à travers un export appelé depuis une autre ressource, passage des fonctions (onSelect, rappels du menu) par les exports, SetNuiFocus(true, false) pour le menu et le skillcheck (clavier sans souris).

Thème de l'interface

Détails et palettes : docs/integration/themes.md.

ÉlémentValeur
Paramètreui.theme (type enum, widget = 'theme', catégorie « Interface »), défaut crepuscule
Valeurs (liste blanche TLR.Themes.LIST, shared/themes.lua)crepuscule, cuir, journal, saint_denis, new_austin, roanoke, sang
Permission d'écrituretlr.settings.theme (refusée par défaut ; couverte par le joker tlr.settings.*)
Enregistrementservice des paramètres (tlr_settings), aucune migration dédiée
  • Serveur : TLR.UITheme.current() / exports.tlr_core:GetUiTheme() → identifiant courant (toujours valide). TLR.UITheme.send(target) envoie le thème à un joueur (ou -1). TLR.Themes.isValid(id), TLR.Themes.normalize(id). Changement : TLR.Settings.onChange('ui.theme', fn) ou événement serveur tlr:settings:changed (key, value).
  • Événements réseau : serveur → client tlr:ui:theme (id) (à chaque changement, à tous) ; client → serveur tlr:ui:theme:request (réponse au seul demandeur, au plus toutes les 2 s).
  • Client : TLR.NUI.theme() → dernier identifiant reçu (ou nil).
  • Message NUI (Lua → page) : theme { id }. Côté page : setTheme(id), previewTheme(id|null), savedTheme() de web/js/theme.js ; événement fenêtre tlr:theme (detail = identifiant) à chaque changement appliqué.
  • Modifier depuis le panel : action RPC existante settings.update { key = 'ui.theme', value, version }.

Bannissements

Table tlr_bans (migration 125), source de vérité. Les colonnes tlr_users.banned et ban_reason ne sont plus lues ni écrites (les bannissements définitifs existants sont copiés par la migration). Un bannissement est actif tant qu'il n'est ni levé (lifted_at) ni expiré (expires_at, NULL = définitif). Les dates viennent de l'horloge de la base, jamais du client.

  • Règles pures (shared/bans.lua, TLR.BanRules) : normalizeMinutes(v) (0 = définitif, sinon 1 à MAX_MINUTES = 525600), formatDuration(minutes), formatRemaining(secondes) (arrondi vers le haut), isActive(row), PRESETS.
  • Serveur (server/players/bans.lua, TLR.Bans) : activeFor(license) → { id, reason, remaining } ou nil, message(ban), create(license, name, reason, minutes, actor) → true, id ou false, code (remplace un bannissement actif), lift(id, actor) → true, ligne ou false, code, list() (200 lignes, actifs d'abord).
  • Actions RPC : players.ban { source, reason, minutes? } (permission tlr.players.ban, minutes absent ou 0 = définitif), players.bans (tlr.players.bans.read), players.unban { id } (tlr.players.unban, renvoie la liste à jour). Le rang de la cible est contrôlé (en ligne ou non) : on ne bannit ni ne débannit quelqu'un de son niveau ou au dessus, ni le fondateur.
  • Journal, catégorie admin : player.ban (cible, licence, motif, durée, id) et player.unban.

Identifiants uniques

Un identifiant lisible par joueur (compte Rockstar) et un par personnage, en plus du citizen_id (état civil). Ils viennent de tlr_users.id et tlr_characters.id (auto incrément) : uniques, stables et jamais réutilisés, car aucune ligne n'est supprimée (un personnage supprimé est seulement masqué). Format : lettre et numéro sur 4 chiffres au moins, J0042 (joueur), P0137 (personnage).

  • Règles pures (shared/ids.lua, TLR.Ids) : player(n), character(n), parse(texte) → 'player'|'character', n ou nil.
  • Personnage (serveur et vue client) : champs uid (personnage) et playerUid (joueur).
  • Exports serveur : GetPlayerUid(src), GetCharacterUid(src), GetPlayerByUid('J0042' | 'P0137') → source ou nil (joueur ou personnage connecté seulement). Interne : TLR.PlayersService.findByUid(texte).
  • Interface : commande /id (ses propres identifiants), écran des personnages, section Joueurs (affichage et recherche).

Lieu d'apparition

Quand un joueur reprend un personnage déjà joué, l'écran des personnages propose « Là où vous étiez » et les lieux de TLRConfig.SpawnPoints (config/config.lua : id, label, description, x, y, z, heading). Paramètre spawn.choice (Paramètres, Apparition) pour couper le choix. Un nouveau personnage apparaît à TLRConfig.DefaultSpawn. Un personnage mort ou en prison reprend toujours sa dernière position.

  • Règles pures (shared/spawn.lua, TLR.SpawnRules) : normalize(points) → points valides, problèmes ; resolve({ choice, enabled, points, saved, default, locked }) → position, origine ou nil, 'UNKNOWN_SPAWN'.
  • Action RPC character.select { id, spawn? } : spawn = 'last' ou l'identifiant d'un point. Le client n'envoie jamais de coordonnées ; un identifiant inconnu est refusé (VALIDATION_FAILED, journal security, rien n'est chargé). Le journal character.select note l'origine.
  • Les points sans coordonnées sont envoyés à l écran dans rules.spawnPoints (avec image, fichier de web/img/spawn/).
  • TLRConfig.SpawnChecks : zones dangereuses (rayon en mètres) ; une dernière position enregistrée dedans est remplacée par moveTo (origine safe). Réglage repris de rsg-spawn pour la zone de création de personnage.

Retirer ou remettre une pièce de vêtement (/habits)

Commande client /habits : menu (lib, HTML) listant les pièces de l'apparence enregistrée, avec « Porté » ou « Retiré ». Affichage seulement : rien n'est enregistré ni envoyé au serveur, et on ne peut rien porter de plus que l'apparence enregistrée. L'état est oublié à l'apparition, après un achat ou une tenue enregistrée, et à la fermeture de la garde robe.

  • Règles pures (shared/appearance.lua) : A.worn(app, sex, hidden) → { key, label, hidden }, A.withHidden(app, sex, hidden) → apparence sans les pièces retirées (passe ensuite par resolve et apply, qui réappliquent le haut ou le bas du corps), A.canToggle(app, sex, cat).
  • Client : TLR.Appearance.toggle(cat) et export exports.tlr_core:ToggleClothing(cat) → booléen. Refusé si mort, menotté, éditeur ou écran des personnages ouvert.

Traits du visage

Curseurs de création (onglet « Traits du visage » de l'écran wardrobe), activables dans le panel (Apparence, option « Traits du visage », paramètre appearance.allow_features, vrai par défaut). 46 traits, groupés par partie : crâne, sourcils, oreilles, pommettes et mâchoire, yeux, nez, bouche.

  • Stockage : body.ff_<trait> dans l'apparence, entier de 0 à 200 (100 = valeur d'origine). La native reçoit (valeur - 100) / 100. Ce sont de simples champs du corps (groupe features) : validation, limites, schémas RPC et interface passent par le code existant.
  • Données : shared/appearance_face.lua (TLR.FaceFeatures, { key, hash, label, section }), générées depuis la table de rsg-appearance.
  • Native : 0x5653AB26C82938CF (ped, identifiant, valeur), appelée ainsi par rsg-appearance et par le tutoriel Cfx.re de t3chman. Elle n'a pas de nom officiel dans ce que j'ai pu consulter ; la plage de valeurs n'est documentée nulle part (RSG envoie de moins 1.0 à plus 1.0).
  • Sources et limites : seuls 4 identifiants sont confirmés par deux sources (34006 tête, 41396 visage, 61782 courbure du nez, 46661 coin gauche de la bouche, hauteur) ; les 42 autres ne reposent que sur rsg-appearance. Cinq traits de sa table sont exclus car ambigus : 12281 est donné à la fois aux sourcils (largeur) et au visage (profondeur), 13059 aux sourcils (hauteur) et au front, et 13425 (taille du nez) est étiqueté autrement dans le tutoriel. Les réintégrer avec une source sûre.
  • Client : SET_PED_FACE_FEATURE est appliqué pour les 46 traits à chaque TLR.Appearance.apply, après la tête et avant la mise à jour du ped.

Chevaux : icône sur la carte, soins et harnachement

  • Icône : le cheval sorti s'affiche sur la carte à son nom (paramètre horses.blip, vrai par défaut). Natives 0x23F74C2FDA6E7C61 (style -1230993421, entité) et 0x9CB1A1623062F402 (nom), appelées ainsi par rsg-horses ; retirée au rangement, à la mort et au renommage elle suit le nom.
  • Soins à l'écurie : bouton « Soigner » sur un cheval sorti dans l'écran d'écurie, action RPC horses.stable.heal { stableId, id }. Prix horses.heal_price (500 centimes par défaut, 0 = gratuit), un soin par minute et par cheval, joueur à l'écurie. Effet : santé et endurance au maximum (événement tlr:svc:horse:effect, le même que les soins du palefrenier).
  • Harnachement : six catégories (selles, couvertures, pommeaux, étriers, sacoches, rouleaux de couchage), 483 pièces au total (shared/horse_equipment_data.lua, généré depuis Config.HorseComponents de rsg-horses ; la selle et les sacoches de base, posées par défaut, y figurent aussi). Règles pures : TLR.HorseEquipment (clean, fromIndices, toIndices, changedCount, resolve). Stockage : colonne tlr_horses.equipment (JSON { catégorie = hash }, migration 43), revalidée à chaque lecture. Action RPC horses.stable.equip { stableId, id, equipment = { catégorie = numéro } } (0 = pièce par défaut) : le serveur valide les numéros, fait payer horses.equip_price par pièce changée (0 par défaut) et envoie tlr:horses:equip au propriétaire si le cheval est sorti. Aperçu : callbacks NUI horses:preview et horses:previewRestore (rien n'est enregistré). Les chevaux sortis reçoivent leur harnachement dans l'événement tlr:horses:spawn (equipment, blip).

Portes : verrous à la ox_doorlock

Le système de portes du cœur (catalogue femga, panel Portes, touche E) reprend ce que propose ox_doorlock. Une porte peut avoir, en plus de l'ancien métier unique, de la liste de personnages et de l'objet clé :

FonctionDétail
Plusieurs métiersgroups = { métier = gradeMinimum } (jusqu'à 20). L'ancien champ métier unique est fondu dans la liste à la lecture.
Code4 à 8 chiffres. Stocké en base, jamais envoyé au panel (hasPasscode seulement) ni écrit dans le journal. Saisie par un champ masqué de la lib. Après doors.code_attempts erreurs de suite (3), la saisie est bloquée doors.code_lockout_seconds secondes (30) pour ce personnage et cette porte.
CrochetageCase « Peut être crochetée » et difficulté (facile, moyenne, difficile). Exige l'objet doors.lockpick_item (« lockpick », le Crochet, fourni par le cœur). Épreuve d'adresse de la lib (tlib.skillcheck), crochet cassé avec doors.lockpick_break_percent % de chances après un échec, alerte à la loi en service (doors.lockpick_alert).
Verrouillage automatiqueautolock secondes (0 = jamais, 3600 au plus) : la porte ouverte se referme seule.
Distance propredistance de 1 à 5 m (0 = réglage doors.interact_radius).
État conservédoors.persist_state (vrai par défaut) : l'état verrouillé ou non survit au redémarrage (colonne locked_state).

Touche E : une personne autorisée (métier, personnage, objet clé, droit tlr.doors.bypass) verrouille ou déverrouille ; sinon, si la porte a un code, la saisie s'ouvre ; sinon, si elle est crochetable, l'épreuve démarre. L'invite E s'affiche pour tout le monde devant une porte à code ou crochetable.

  • Règles pures : shared/doorlock.lua (TLR.DoorRules : normalizeGroups, mergeLegacy, groupsList, groupAllows, validPasscode, sameCode, canTry, recordFail, recordSuccess, rounds, lockpickBreaks, cleanAutolock, cleanDistance, fallbackAction, extraAccessText). TLR.WorldRules.canUseDoor reconnaît groups.
  • Serveur : TLR.Doors.save accepte groups = { { job, grade } }, passcode, clearPasscode, lockpick, lockpickDifficulty, autolock, distance (action RPC doors.save) ; TLR.Doors.scheduleAutolock(porte), TLR.Doors.startLockpick(src, porte).
  • Événements réseau : serveur vers client tlr:doors:code ({ id, label }) et tlr:doors:lockpick ({ token, label, rounds }) ; client vers serveur tlr:doors:code (id, code) et tlr:doors:lockpick:result (jeton, réussite). Le serveur revérifie le personnage, la distance, le jeton (une seule utilisation, 60 s) et impose une seconde au moins entre le début et le résultat.
  • Limite connue : comme pour toute épreuve jouée côté client, un client modifié peut annoncer une réussite sans avoir joué l'épreuve ; l'alerte à la loi et le journal (door.lockpick.start, .ok, .fail) servent de garde fou.
  • Journal : door.code.ok (monde), door.code.fail (sécurité), door.lockpick.*, door.lock, door.unlock, door.create, door.update.

Créer un verrou en visant la porte

Comme dans ox_doorlock : panel /tlr, section Portes, bouton « Viser une porte » (ou commande staff /verrou, permission tlr.doors.update). Le panel se ferme, un rayon part de la caméra et la porte visée est encadrée. Clic gauche : choisir la porte ; clic gauche sur un second battant : porte double ; Entrée : porte simple ; clic droit ou Retour : recommencer ou annuler. Le panel se rouvre ensuite sur Portes avec le formulaire (nom, lieu, verrouillée au départ, métiers, objet clé, personnages, code, crochetage, distance, fermeture automatique). Si la porte a déjà un verrou, sa fiche s'ouvre. Le bouton « Portes proches » garde l'ancienne liste.

  • Client : client/world/door_aim.lua (événement tlr:doors:aim, callbacks NUI doors:aim:start et doors:aim:take). Il n'envoie que le modèle et la position des entités choisies.
  • Serveur : action doors.aim.resolve { picks = { { model, x, y, z }, ... } } (1 ou 2) -> { main, pair } (hash, model, configured). Règles pures D.u32, D.validPick, D.matchPick, D.resolvePicks : même modèle que l'entrée du catalogue, à moins de AIM_MATCH_RADIUS (2 m), et porte à moins de AIM_MAX_REACH (20 m) de l'admin (position lue par le serveur).
  • À VÉRIFIER EN JEU : le rayon (drapeaux 1 | 16) touche les battants, l'origine de l'entité tombe près de la position du catalogue, le cadre DRAW_LINE est visible.

HUD du joueur

Fichiers : client/player/hud.lua (lecture des données, deux fois par seconde, envoi seulement si elles changent), web/js/hud.js et web/css/hud.css (affichage, couleurs des thèmes). En bas à droite : une carte (heure du jeu, température, argent liquide et banque, métier avec « En service », identifiants J0003 · P0012, indicateur de voix) et quatre jauges en anneau (santé, endurance, faim, soif) ; monté à cheval, deux jauges de plus (santé et endurance du cheval). Une jauge à 15 % ou moins clignote (désactivé avec la préférence « réduire les animations »). Masqué pendant un écran plein cadre, l'écran des personnages, la mort et le menu pause.

  • Paramètres (Paramètres, Joueurs) : hud.enabled, hud.show_money, hud.show_temperature. Commande /hud : masquer ou afficher sur ce poste (KVP tlr:hud:hidden).
  • Message NUI hud : { visible, health, stamina, hunger, thirst, talking, clock, temp, cash, bank, job = { label, grade, onDuty, dutyRequired }, uid, playerUid, horse = { health, stamina } | nil }.
  • Natives sans nom officiel, reprises de rsg-hud et rsg-horses (en production) : 0x0FF421E467373FCF (endurance du joueur), 0x775A1CA7893AA8B5 et 0xCB42AFE2B613EE55 (endurance du cheval et son maximum), 0x33EEF97F (le joueur parle). À vérifier en jeu : échelle de l'endurance, heure lue (GET_CLOCK_HOURS), température, position par rapport au radar du jeu.

HUD : disposition personnelle et icônes

  • Chaque élément du HUD (santé, endurance, faim, soif, cheval monté, heure et température, métier, identifiants, argent) se place et s'active à volonté : commande /hudedit (glisser les éléments, cocher ceux voulus, taille de 60 à 160 %, réinitialiser). La disposition est gardée sur le poste du joueur (KVP tlr:hud:layout), validée par shared/hud.lua (TLR.HudRules : ELEMENTS, normalizeLayout, toList, default) : positions en pourcentage de l'écran bornées de 0 à 100, booléens, aucune donnée exécutable.
  • Par défaut : argent et identifiants masqués. L'argent n'est même pas envoyé à la page tant que le paramètre hud.show_money (faux par défaut) n'est pas activé ; le joueur le montre ensuite dans /hudedit.
  • Icônes : images d'objets du jeu (web/img/hud/ : tonique de santé, tonique d'endurance, pain, gourde, étiquette de cheval, tirées de rsg-inventory), plus de pictogrammes dessinés.
  • Messages NUI : Lua vers page hudlayout ({ scale, elements = { { id, label, x, y, visible } } }) ; callbacks hud:layout:get, hud:layout:save, hud:layout:reset. Écran hudedit (web/js/screens/hudedit.js).

Besoins du personnage : faim, soif, énergie, propreté

metadata.needs = { hunger, thirst, energy, clean } (0 à 100, 100 = plein ou propre), envoyé au client par l'événement tlr:needs et affiché par le HUD (/hudedit pour le placer). Règles pures dans shared/inventory.lua : Inv.defaultNeeds(), Inv.applyNeeds(besoins, objet), Inv.decayNeeds(besoins, minutes, taux).

  • Paramètres (Objets, inventaire) : needs.hunger_per_hour (4), needs.thirst_per_hour (6), needs.energy_per_hour (3), needs.clean_per_hour (2), needs.wash_per_step (15). Un personnage créé avant l'énergie et la propreté reçoit ces deux jauges pleines.
  • Objets : champs energy et clean en plus de hunger et thirst (curseurs « Énergie rendue » et « Propreté rendue » du panel, section Objets). Le cœur fournit savon (propreté +60) ; le café du barman (svc_cafe) rend 20 d'énergie.
  • Lavage : dans l'eau (IS_ENTITY_IN_WATER), le client signale tlr:needs:wash toutes les 10 secondes au plus ; le serveur ajoute needs.wash_per_step points. Un client modifié ne peut gagner que de la propreté.
  • Limite connue : l'énergie ne se remonte pour l'instant qu'avec des objets (café, toniques) ; dormir dans un lit n'existe pas encore.

Inventaire : regrouper, trier, fenêtre déplaçable

  • Boutons « Regrouper » (inventory.stack) et « Trier » (inventory.sort) dans l'en-tête. Regrouper remplit les piles du même objet (même metadata) jusqu'à leur taille maximale, les plus anciennes d'abord. Trier range par catégorie, puis par nom, sans trous ; si le rangement ne rentre pas, rien ne bouge. Règles pures : Inv.stackPlan, Inv.sortPlan (shared/inventory.lua). Refusés si le personnage est mort ou menotté.
  • La fenêtre se déplace en glissant son en-tête ; la position est gardée sur le poste (localStorage). Les jauges de faim et de soif ne sont plus dans l'inventaire : elles sont dans le HUD.
  • Grille : inventory.columns (6 par défaut) et inventory.cells (60 au minimum, 60 par défaut) ; la grille s'allonge vers le bas.

Menu admin en jeu (F1 ou /admin)

Menu en liste, dans le style du menu F6 (fond sombre translucide, ligne sélectionnée claire) : flèches pour choisir, Entrée pour valider, Retour arrière ou Échap pour revenir, souris acceptée. Il ne montre que ce que les permissions de l'administrateur autorisent ; le serveur refuse de toute façon le reste et journalise chaque action (catégorie Administration).

MenuActionsPermission
Joueurs (liste des connectés)Aller vers lui, le ramenertlr.admin.teleport
Soigner, réanimertlr.admin.heal
Observertlr.admin.spectate
Geler ou dégeler, envoyer un messagetlr.admin.players
Donner ou retirer de l'argent liquidetlr.economy.balance.update
Donner un objet (nom et quantité)tlr.inventory.give
Expulser (motif), bannir (durée et motif)tlr.players.kick, tlr.players.ban
MoiNocliptlr.admin.noclip
Invincible, invisibletlr.admin.self
Me soigner, me réanimer, aller au point de la cartetlr.admin.heal, tlr.admin.teleport
MondeHeure, figer le temps, météotlr.world.time
Annonce à toustlr.admin.announce
Expulser tous (motif, confirmation)tlr.admin.server
OutilsMa position (copiée si possible), portes proches avec leur identifianttlr.admin.menu, tlr.doors.update
Panneau completOuvre /tlrtlr.panel.open
  • Ouverture : tlr.admin.menu. Le groupe modérateur reçoit tlr.admin.menu et tlr.admin.players ; le groupe admin a tout (tlr.admin.*) ; le fondateur aussi.
  • Actions RPC ajoutées (server/rpc/actions/adminmenu.lua) : adminmenu.bootstrap, adminmenu.players, adminmenu.position, admin.freeze, admin.message, admin.announce, admin.god, admin.invisible, admin.kickall. Les autres actions sont celles du panel.
  • Règle de rang : jamais d'action sur quelqu'un de son niveau ou au dessus (sauf soi même) ; « Expulser tous » épargne le fondateur et les rangs égaux ou supérieurs.
  • Client : client/admin/menu.lua (commande /admin, touche F1 lue par son code, effets geler, invincible, invisible, annonce), écran web/js/screens/adminmenu.js, style web/css/adminmenu.css. À vérifier en jeu : F1 libre.
  • Deux présentations du même menu : liste (par défaut) et roue (radial). Touche Tab, bouton « Roue » ou « Passer en liste » ; le choix est gardé sur le poste (localStorage). Dans la roue : flèches pour tourner, Entrée pour valider, Retour arrière pour revenir, souris acceptée ; au delà de 8 éléments, la dernière place devient « Suite ».
  • Gestion : une entrée par section autorisée du panneau, groupées (Serveur, Joueurs, Gameplay, Système...), qui ouvre le panneau directement sur la section (TLR.NUI.show('panel', { section }), vérifiée par le serveur). Les récoltes (section Monde et placements, plantes) ne figurent pas dans ce menu.
  • Actions rapides ajoutées dans le menu : changer le métier d'un joueur (métier puis grade, players.setJob), journaux récents (logs.recent, 40 entrées), en plus de celles du premier tableau.

Icônes du HUD : Font Awesome Free 6.5.1 (solid), comme rsg-hud, téléchargées depuis github.com/FortAwesome/Font-Awesome (tag 6.5.1) dans web/img/hud/ avec le texte de licence (LICENSE-FontAwesome.txt, icônes CC BY 4.0). Elles sont aussi intégrées dans web/js/lib/fa.js pour pouvoir changer leur couleur par le style.

Inventaire : menu en liste, donner, renommer

Clic droit sur un objet : menu en liste près du curseur (style du menu F6) avec Utiliser (ou Consommer, Équiper), Donner, Diviser, Renommer, Jeter ; Ramasser sur un objet au sol.

  • Donner : inventory.nearby liste les joueurs à 4 m (distance lue côté serveur), puis inventory.giveto { id, qty, source } passe par les règles du don existant (TLR.JobServices.give : distance, place chez le destinataire, limite par minute, metadata conservée, annulation si le sac du destinataire est plein).
  • Renommer : inventory.rename { id, name }, objets non empilables seulement (arme, outil) ; le nom (1 à 30 caractères, Inv.cleanItemName) est gardé dans meta.name et affiché à la place du nom du catalogue. Un objet empilable n'est pas renommable : son nom l'empêcherait de se regrouper.
  • Le panneau d'inventaire épouse la largeur de la grille (6 colonnes par défaut), sans marge vide à droite.

Inventaire : présentation et rangement de rsg-inventory

  • web/css/inventory.css et web/js/pages/inventory.js reprennent la présentation de rsg-inventory : panneau sombre ancré à droite, colonne d'icônes, cases translucides (tailles en vh), infobulle titre et corps, menu clic droit à sous menus (Un / Quantité) et fenêtre de quantité. Les coffres et maisons gardent les classes .inv de app.css.
  • Bouton Ranger (icône en haut à droite) : Par poids, Par quantité, Combler les vides, Par catégorie. RPC inventory.sort { mode? } avec mode parmi weight, amount, compact (absent = par catégorie puis par nom). Par poids et par quantité fusionnent d'abord les piles identiques ; « Combler les vides » garde l'ordre actuel (de haut en bas, de gauche à droite) sans rien fusionner. Règles pures : Inv.sortPlan(layout, entries, defs, fixed, mode) et Inv.SORT_MODES dans shared/inventory.lua. Refusé, sans rien déplacer, si tout ne rentre pas.
  • Menu d'un objet : Utiliser, Donner (Un ou Quantité), Jeter (Un ou Quantité), Diviser, Renommer. À vérifier en jeu : police Roboto absente (repli sur Segoe UI), icônes dessinées à la main et non Font Awesome.

HUD : style fks-hud

  • Anneaux inspirés de fks-hud (ressource GPL, aucun code copié) : disque sombre translucide, anneau coloré, pictogramme qui se remplit de bas en haut, valeur facultative, forme (cercle, carré, hexagone), épaisseur, segments, opacité, couleurs de l'anneau et du pictogramme par jauge, indicateurs « +N » et « -N » quand une jauge change d'au moins 4 points, informations en texte sans cadre (heure, métier, argent, identifiants).
  • Éditeur /hudedit : onglets Éléments, Style, Partage (copier ou coller une disposition, validée par hud:layout:check), grille d'aide. Validation Lua dans shared/hud.lua (shape, thickness, segments, opacity, values, color, icon en #rrggbb).
  • Non repris de fks-hud : stress, température avec dégâts, consommables avec animations, sauvegarde en base par compte (la disposition reste sur le poste du joueur).

Cheval : faim et soif, micro et portée de la voix

  • Faim et soif du cheval sorti (0 à 100, colonnes hunger et thirst de tlr_horses, migration 44) : le serveur les fait baisser chaque minute (paramètres horses.hunger_per_hour 6 et horses.thirst_per_hour 8, 0 = désactivé), prévient une fois sous 20, sauvegarde toutes les 10 minutes et à la rentrée. Règles pures dans shared/horses.lua : R.needs, R.decayNeeds, R.feed, R.pickFeed, R.drink, R.FEED (svc_foin +35 faim, svc_carotte et farmer_carotte +15, pomme +10, svc_seau_eau +50 soif).
  • Soins en jeu (client/horses/care.lua), sans commande, par un vrai menu radial : deux façons de l'ouvrir pour la même chose, la cible (Alt maintenu, option « Prendre soin du cheval ») ou le clic droit en regardant son cheval de près, à pied et sans arme. La roue propose Nourrir (aliments du sac ; plusieurs aliments ouvrent une seconde roue), Boire (rivière, lac, abreuvoir), Manger du foin (botte, mangeoire) et Sacoches, avec la faim et la soif en indication et les secteurs grisés quand il n'y a rien à proximité. Le cheval joue l'animation. Le serveur borne tout : propriétaire, distance de 6 m, objet, délai de 6 s (R.CARE_SECONDS), gains de 45 pour l'eau et le foin. Événements client vers serveur tlr:horses:feedList, tlr:horses:feed, tlr:horses:drink, tlr:horses:hay ; serveur vers client tlr:horses:feedList et tlr:horses:needs.
  • HUD : deux anneaux de plus, horse_hunger et horse_thirst, visibles à cheval. À vérifier en jeu : l'option de cible sur le cheval, les animations (reprises de rsg-horses), la détection de l'eau, des abreuvoirs et du foin.
  • Micro (pma-voice) : élément voice du HUD, lu dans LocalPlayer.state.proximity (index, distance, mode), radioChannel et callChannel, sans dépendance ajoutée : sans pma-voice, l'élément n'apparaît pas. L'anneau montre la portée (20 m = plein), devient or quand le joueur parle, avec dessous le mode (Chuchoter, Normal, Crier) et la distance, plus le canal radio. Règle pure : TLR.HudRules.voiceView. Le changement de portée reste la touche de pma-voice (F11 par défaut).
  • web/js/lib/radialwheel.js : roue en secteurs en SVG (couronne sombre translucide, survol rouge, pictogramme et libellé en majuscules, centre Fermer ou Retour), dans le style de rsg-radialmenu, sans dépendance. Couleurs et taille par les variables --rw-* (web/css/radial.css).
  • Client : TLR.Radial.open(titre, { { label, icon, hint, disabled, onSelect } }) (client/nui/radial.lua, écran radial). La page n'envoie que l'identifiant du secteur (radial:select), le Lua retrouve la fonction. Refusé si un autre écran est ouvert.
  • Le menu admin (F1) utilise la même roue quand on passe en mode radial (Tab).

Roue du joueur (F6)

client/nui/playermenu.lua : touche F6 (commande /menu, RegisterKeyMapping, touche modifiable dans les réglages du jeu) ouvre une roue de 8 secteurs construite à l'ouverture : Inventaire, Habits (retirer ou remettre une pièce, comme /habits, via TLR.Clothing), Métier (état de service, Police et Prime pour la loi, Appels et Fin d'appel pour les médecins, Tâche, Journal, Gang), Citoyen (identifiants, mains en l'air, ma peine), Argent (banque, factures), Cheval (siffler, sacoches, soins), Urgences (appeler un médecin ou le shérif), HUD (afficher ou masquer, disposition). Chaque secteur appelle la commande ou la fonction du domaine concerné, qui revérifie tout. Les sous roues ont « Retour » au centre (TLR.Radial.sub, secteur avec items). La prise ou la fin de service reste aux points de service du métier (le serveur ne l'autorise pas à distance). À vérifier en jeu : la touche F6 libre, le basculement des habits depuis la roue.

Mises à jour de la roue du joueur et de l'inventaire

  • Roue du joueur : plus de secteur Urgences (appels hors jeu). Habits est une roue de six zones (Tête, Haut, Mains, Taille, Jambes, Pieds) plus « Tout remettre » ; chaque zone liste les pièces que le personnage porte (chapeaux, lunettes, masques, foulards, accessoires de cheveux ; chemises, gilets, manteaux, manteaux fermés, ponchos, capes, cravates, bretelles ; gants, manchettes, bagues, bracelets ; ceintures, boucles, ceinturons, sacoches, accessoires ; pantalons, jupes, jambières, guêtres ; bottes, éperons), cliquer retire ou remet.
  • Catégories d'objets : quatorze (Inv.CATEGORIES) : nourriture, boisson, arme, munition, outil, ressource, médical, plante, gibier et peaux, vêtement, cheval, valeur, document, divers. Le rangement « Par catégorie » (premier choix du bouton Ranger) suit cet ordre, puis le nom. La catégorie se règle par objet dans le panel (Objets) ; sans réglage elle est déduite (weapon_, ammo_, nom contenant peau, préfixe herbalist_, consommable). L'infobulle indique la catégorie.
  • Image d'objet : contenue dans sa case (plus de débordement).

Roue du joueur : fonctionnement repris de rsg-radialmenu

  • Contenu dans config/radial.lua (TLRRadial.MenuItems, TLRRadial.JobInteractions, TLRRadial.HoldToOpen) : éléments { id, title, icon, hint, type, event, action, shouldClose, canOpen, items }. type : client (TriggerEvent), server (TriggerServerEvent), command (ExecuteCommand) ou action (fonction). items : sous roue imbriquée, liste ou fonction construite à l'ouverture. canOpen masque l'élément. shouldClose = false rouvre la roue au même niveau après l'action (bascule des habits, réglage).
  • Interactions par métier : JobInteractions[nom du métier], sinon [famille] (law, medical), puis ['*'] pour tout métier, sous « Métier ».
  • Autres ressources : exports.tlr_core:AddRadialOption({ id, title, icon, items = {...} }, id?) retourne l'identifiant, RemoveRadialOption(id) le retire (arrêt de la ressource : nettoyage). Depuis une autre ressource, types client, server ou command seulement (une fonction ne traverse pas les ressources). Événements tlr:radial:opened et tlr:radial:closed.
  • Touche F6 (+tlr_menu et -tlr_menu, modifiable dans les réglages du jeu) : par appui (ouvre au relâchement) ou en maintenant (ouvre à l'appui, relâcher ferme), au choix du joueur dans Réglages, « Ouverture de la roue » (KVP tlr:radial:hold). Le curseur est recentré à l'ouverture.
  • Dans la roue : chiffres 1 à 9 choisissent un secteur, molette ou flèches tournent la sélection, Entrée valide, Échap ou Retour arrière reviennent d'un niveau (ou ferment), clic au centre idem. Animation d'ouverture en zoom léger.
  • Non repris de rsg : styles de marche, PVP, carnet d'adresses, boutons d'urgence (rien de hors jeu).

Habits : fonctions reprises du menu de rsg_craft

  • Animation avant chaque changement de pièce, puis changement après 900 ms, comme toggleClothes de rsg_craft : TLRRadial.ClothesAnim (config/radial.lua) reprend les dictionnaires et clips de config/animations/clothes.lua (chapeau, lunettes, masque, foulard, cravate, bretelles, chemise, gilet, manteaux, poncho, cape, gants, bagues, bracelets, ceintures, pantalon, jambières, jupe, bottes, éperons, accessoires, sacoches).
  • Trois secteurs de plus dans la roue Habits : S'habiller (rejoue l'apparence enregistrée en entier), Se déshabiller (retire toutes les pièces d'un coup), Fixer personnage (comme /loadskin : remet tout à l'état enregistré). Une seule application par action (Ap.setAllHidden), exposée par TLR.Clothing.dress, undress, reload.
  • Rien n'est enregistré : c'est un affichage, comme /habits. Non repris : manches retroussées, col ouvert, holster gauche, armure, équipement (pas de pièce correspondante dans A.CLOTHES). À vérifier en jeu : les animations (dictionnaires de rsg_craft) et la durée d'une application complète.

Roue : icônes et texte

  • Icônes Font Awesome Free 6 (solid) dans web/js/lib/radialicons.js (47 pictogrammes extraits de la police FontAwesome6_Solid, licence CC BY 4.0 dans web/img/hud/LICENSE-FontAwesome.txt) : le champ icon d'un secteur prend un nom Font Awesome (bag-shopping, shirt, briefcase, hat-cowboy, shoe-prints, carrot, droplet...). Un nom absent de ce fichier retombe sur l'ancienne icône de lib/icons.js.
  • Le libellé d'un secteur est coupé en lignes et sa police rétrécit pour rester dans le secteur (largeur d'arc disponible, trois lignes au plus).

Touches : F6 et F1

  • Roue du joueur : F6 (code 0x3C0A40F2) ; menu admin : F1 (code 0xA8E3F467). Codes de la liste RedM-Key-Controls (github.com/Summerxo/RedM-Key-Controls), lus comme le fait rsg-radialmenu (IsControlJustReleased) plutôt que par RegisterKeyMapping, donc plus de dépendance au mapping de touche de RedM. Les commandes /menu et /admin marchent toujours.
  • F1 envoie tlr:admin:menuKey au serveur, qui ne répond (tlr:admin:openMenu) qu'à qui a la permission tlr.admin.menu : aucun message pour les autres joueurs, une demande par seconde au plus.
  • À vérifier en jeu : F1 et F6 libres (pas déjà prises par une autre ressource).

Chevaux : système d'écurie de rsg-horses intégré au cœur (lot 1, soins et progression)

Repris de ton rsg-horses (fks-stables-main), réécrit dans l'architecture de TLR : le serveur compte et décide, le client signale et affiche. Lots suivants à faire : charrettes, chevaux sauvages et apprivoisement, métier de dresseur (parcours, longe, élevage, poulains), menu d'écurie complet (vitrine, vente, déplacement, couleurs de robe).

  • Règles pures : shared/horse_care.lua (TLR.HorseCare) : classes 1 à 4 (expérience maximale 1000, 2000, 3000, 4000), santé et endurance en pourcentage selon classe et dressage (CLASS_STATS), besoins par mise à jour de 30 s selon l'allure (DRAIN, saleté, sabots), lien (niveaux 0, 50, 150, 300), courage et affinité de 0 à 10, âge (un an par 24 h réelles), nourriture (FEED, valeurs de rsg-horses), stimulants et boost doré. Tests : tests/test_horse_care.lua.
  • Base : migration 45 (tier, xp, bond, courage, affinity, dirt, hoof, age_base, age_since sur tlr_horses, tier sur tlr_horse_shop), migration 44 faim et soif. À l'achat : classe de la fiche en vente, âge de départ.
  • Serveur : server/horses/care.lua. Rapport du client toutes les 30 s (tlr:horses:report, allure et monté ; le temps compté est celui du serveur, 10 s au minimum entre deux rapports, gains plafonnés). Nourrir (feedList, feed), boire, foin, brosse et cure-pied (départ vérifié start puis fin), caresse (15 s), repos et sommeil, peur (fear), ranimation (revive, objet horse_reviver, tout joueur peut ranimer un cheval mort), plaque de nom (tagRename), renvoyer (flee). Objets fournis : horse_apple, horse_carrot, sugarcube, haysnack, horse_stimulant, horse_gold_stimulant, horse_brush, hoof_pick, horse_reviver, horse_nametag (catégorie Cheval).
  • Réglages : horses.needs_rate (100 % = rythme de rsg-horses, 0 = pas de besoins), horses.revive_seconds (900), mort « le corps reste sur place » dans horses.death_mode (le cheval reste ranimable, puis fourrière).
  • Client : client/horses/stats.lua (attributs selon classe et dressage, courage, boost doré, effets de la faim et des sabots, ranimation, rapport), client/horses/care.lua (animations et menu radial). Roue du cheval : Nourrir (donner, boire, foin), Soins (brosser, sabots, se reposer, dormir, se lever), Caresser, Suivre ou Rester, Fiche, Sacoches, Plus (plaque de nom, ranimer, renvoyer). Ouverture : cible (Alt) ou clic droit sur son cheval, et roue F6, Cheval, Soins. Écran horsesheet (fiche).
  • HUD : faim et soif du cheval, propreté selon la saleté suivie par le serveur.
  • À vérifier en jeu : toutes les natives et animations reprises de rsg-horses (attributs, anneaux, boost doré, scène des sabots, couchage, ranimation), le clic droit, le cabrage à bas courage.

Objets : images envoyées depuis l'ordinateur, liste du panel

  • Panel, Objets : champ « Image envoyée depuis l'ordinateur ». L'administrateur choisit un fichier (par exemple pris sur items.rainmad.com, choisir RedM), la page le réduit en PNG de 128 pixels au plus (fond transparent gardé) puis l'envoie par morceaux de 6000 caractères (items.image.part). L'image est rangée en base (tlr_item_images, migration 126) sous l'identifiant de l'objet, donc « renommée comme l'objet » automatiquement, et visible tout de suite pour les joueurs, sans redémarrage. Le serveur n'accepte que du PNG en base64, 120000 caractères au plus (TLR.Inv.validImageData). Permission : tlr.items.update. Retrait : items.image.delete.
  • Un objet avec image envoyée porte customImage = true ; la page la demande (items.image.get, 40 noms au plus, tout personnage) et la garde en mémoire. Ordre de recherche : image envoyée, puis fichiers de web/img/items/, puis dessin intégré, puis icône de la catégorie.
  • Liste des objets du panel : nom sur une ligne (coupé par des points de suspension), identifiant dessous, origine (cfg ou jeu) à droite, colonne plus large : plus de texte qui dépasse.

Argent : centimes, monnaie et billets ensanglantés

  • Tous les montants sont des entiers en centimes : 100 centimes = 1 $ (TLR.Money, comme rsg-core).
  • Deux comptes de plus (Economy.ACCOUNTS) : coins (monnaie : une pièce = un centime, donc 100 pièces = 1 $, affichée « 250 pièces ») et bloodmoney (billets ensanglantés : des dollars d'origine douteuse). Départ à 0.
  • Change : la monnaie se change contre du liquide sans perte ; les billets ensanglantés au taux du réglage economy.bloodmoney_rate (80 % par défaut). Economy.exchange(characterId, from, amount?), export ExchangeMoney, événement client vers serveur tlr:economy:exchange (depuis la roue F6, Argent : soldes en indication et secteurs « Monnaie » et « Billets ensanglantés »).
  • HUD : deux éléments de plus (Monnaie, Billets ensanglantés), masqués par défaut comme l'argent (hud.show_money). Panel, Économie : ajustement et soldes des quatre comptes. Fonctions pures : Money.formatCoins, Money.exchangeBlood.

Chevaux sauvages et apprivoisement (lot 3)

Repris de la partie « Wild horses » de rsg-horses, dans l'architecture de TLR : le serveur décide (hôte, tirages, chronomètre du jeu, argent), le client crée les chevaux (hôte) et affiche.

  • Règles pures : shared/horse_wild.lua (TLR.HorseWild) : nettoyage des zones et des points (cleanZone, cleanPoint), tirage pondéré (pickHorse), choix de l'hôte (chooseHost, l'actuel s'il est encore dans la zone, sinon le plus petit identifiant), tirage d'apparition avec chance, délai, maximum et tirage forcé (rollSpawn), nettoyage des zones vides (cleanupDue), paramètres bornés du mini jeu (tameParams, nextLetterDelay, tameCompleted), métiers autorisés (jobSet, canTame), valeur d'un cheval (quote), âge et sexe d'un cheval gardé (keepProfile), délai de vente (sellCooldownLeft). Tests : tests/test_horse_wild.lua.
  • Configuration : TLRConfig.Wild dans config/config.lua : zones (centre, rayons, maxHorses, perSpawn, chance, rollEvery, cooldown, age, notify, blip = { enabled, name, sprite, radius }, horses = { { model, weight } }) et points (label, x, y, z, stable facultatif). Les 17 zones et le point (Emerald Ranch) viennent de rsg-horses : ce sont les coordonnées de son serveur, non testées, À VÉRIFIER EN JEU. Un modèle absent de shared/horses.lua est ignoré avec un avertissement (rsg-horses appelait « robe » un identifiant de sa boutique ; ici un cheval est un modèle a_c_horse_*, sa valeur vient de la fiche en vente du même modèle). Les zones se changent dans ce fichier (pas d'éditeur de zones dans le panel).
  • Réglages du panel (section Chevaux sauvages, permission tlr.horses.update) : wild.enabled, wild.tame_jobs (métiers séparés par des virgules, vide = tous), durée, délais, chute et type (flèches ou lettres) du mini jeu, wild.retry_delay, wild.known_only, wild.sell_percent, wild.keep_fee, wild.keep_percent, wild.unknown_value, wild.sell_cooldown, wild.keep_age_min et max, wild.female_chance, wild.cleanup_after, wild.contain_at, wild.clear_ambient, wild.spawn_near, wild.spawn_min. Argent en centimes, payé ou débité sur le compte horses.pay_account. Pas d'or (TLR n'a pas de compte d'or).
  • Base : migration 47 (sex sur tlr_horses, male ou female). Le journal passe par TLR.Log (catégorie horses : wild.tame, wild.sell, wild.keep).
  • Serveur : server/horses/wild.lua. Toutes les 2 s, qui est dans chaque zone ; un hôte par zone (réaffecté automatiquement s'il part ; zone vide qui reçoit un joueur = apparition immédiate hors délai) ; zone vide depuis wild.cleanup_after secondes = chevaux supprimés, sauf ceux en cours d'apprivoisement (protégés pendant la durée du jeu plus 30 s) ; un cheval apprivoisé quitte la liste de la zone donc n'est jamais nettoyé. Le serveur refuse un cheval déclaré par un hôte qui n'est plus hôte, un modèle hors de la zone, ou une zone déjà pleine. Apprivoisement : le serveur répond au démarrage (tlr:wild:start) en vérifiant le métier, que le cheval est de zone, que le joueur est dans la zone et qu'il n'a pas déjà un cheval apprivoisé, puis chronomètre : le résultat (tlr:wild:done) ne compte que si le jeu a duré au moins sa durée moins 2 s. Un seul cheval apprivoisé à la fois par joueur. Les chevaux de PNJ n'ont pas la marque de zone (tlrWild) donc ne s'apprivoisent jamais.
  • Vente et garde (actions RPC wild.quote, wild.sell, wild.keep, sans permission, revérifiées) : le joueur doit avoir apprivoisé le cheval et être à moins de rayon + 6 m d'un point. Vendre : wild.sell_percent % du prix de la fiche du même modèle (modèle inconnu : wild.unknown_value), délai wild.sell_cooldown par personnage. Garder : réservé aux modèles de la boutique, prix fixe wild.keep_fee (ou wild.keep_percent % si 0), nom choisi, classe de la fiche, âge tiré dans la fourchette de la zone (sinon des réglages), sexe au hasard selon wild.female_chance, rangé dans l'écurie du point (ou la plus proche), limites de chevaux et de places vérifiées, remboursement si l'insertion échoue.
  • Client : client/horses/wild.lua : icônes de zone (commande /zoneschevaux, seulement pour ceux qui peuvent capturer) et de points, avis d'entrée, création des chevaux par l'hôte sur un sol chargé près de lui, errance, confinement, retrait des chevaux sauvages du jeu dans les zones (wild.clear_ambient), détection de la monte d'un cheval de zone, désarçonnement en cas d'échec, invite [E] aux points de vente quand on y monte le cheval apprivoisé. Écrans NUI : web/js/screens/tame.js (lettres ou flèches qui tombent, presser ou cliquer avant le bas, une touche manquée fait échouer, Échap ignoré) et web/js/screens/wildhorse.js (vendre ou garder), style web/css/wild.css.
  • Commande : tlr_wildspawn (console du serveur, ou permission tlr.horses.update : apparition immédiate dans ses zones).
  • À vérifier en jeu : tout le côté client (natives d'errance, d'apparition, de propriétaire d'animal, désarçonnement, icônes et cercles de zone, touche E), la prise de contrôle des chevaux par l'hôte, les coordonnées des zones et du point.

Chevaux : charrettes de rsg-horses intégrées au cœur (lot 2)

Repris de ton rsg-horses (client/wagon.lua, config/wagons.lua, config/animals.lua, Config.Wagon), réécrit dans l'architecture de TLR : le serveur compte et décide, le client signale et affiche. La table tlr_wagons et la sortie à l'écurie viennent du lot Chevaux ; ce lot y branche la durabilité, le coffre, les carcasses, la personnalisation, le renvoi et l'appel.

  • Règles pures : shared/wagon_care.lua (TLR.WagonCare) et ses données shared/wagon_data.lua (TLR.WagonData, générées depuis la config de rsg-horses : charge et places de carcasses de 41 modèles, peintures, teintes, extras, chargements, lanternes, animaux et peaux avec libellés français). Durabilité de 0 à 1000 (health, percent, state : en bon état, usée sous 30 %, cassée à 0), usure aux kilomètres (wear, bornée par une vitesse maximale de 30 m/s), chocs (crash, seule une baisse de la santé de l'entité compte), report (un rapport complet), repairKind (cassée : clous et marteau, usée : kit), missing (objet manquant), storage (cases et kilos d'un modèle), accès (canUse, addAccess, removeAccess, 10 personnes au plus), animaux (animal, canLoad, addAnimal, takeAnimal, un gros animal prend 2 places), personnalisation (options, cleanCustom, priceCustom). Tests : tests/test_wagon_care.lua.
  • Base : migration 46 (custom, access, animals sur tlr_wagons, en JSON). La santé utilise la colonne health existante (vide = neuve, 0 = cassée). Le contenu du coffre est dans tlr_stash_items (coffre wagon:<id>, vidé avec la charrette supprimée par un admin).
  • Serveur : server/horses/wagons.lua. Événements du client : tlr:wagons:spawned (identifiant réseau de la charrette créée), report (distance, santé de l'entité, accusé de réception, toutes les 10 s ; 5 s au minimum entre deux rapports, le temps compté est celui du serveur), info (menu), open (coffre), repairStart puis repairDone (objets pris à la fin seulement, 80 % de la durée au moins), repairStable, access (list, add, remove), animals (list, store, take), customInfo et custom, send, call. Vers le client : tlr:horses:spawn (avec wagon : santé, personnalisation), tlr:wagons:state, menu, go, accessInfo, animalList, stored, taken, customInfo, customApplied. Branchements dans le lot 1 : Horses.initWagon et Horses.wagonView (sortie), Horses.internal (charge, locked, stables), enveloppes de Horses.saveCare et Horses.adminDelete.
  • Règles de jeu : cassée (0), la charrette perd ses roues et ne roule plus ; réparation sur place avec 5 clous et un marteau (non consommé, 12 s) ; usée mais entière : un kit de réparation (6 s) ; ou payée à l'écurie (wagons.repair_price). Le coffre (capacité de rsg-horses, wagons.storage_percent) s'ouvre pour le propriétaire et les personnes autorisées (wagons.restrict_storage), à moins de 4,5 m (plus pour les grands chariots). Les charrettes de chasse portent carcasses et peaux : le serveur retrouve l'entité portée par son identifiant réseau, vérifie son modèle et sa distance, la supprime lui même, puis l'ajoute à la liste ; décharger la recrée derrière la charrette. Personnalisation : peinture, teinte, extras, chargement, lanternes, essayés en direct, payés à la validation (une fois par élément qui change) près d'une écurie. Renvoi : à l'écurie où elle était rangée, de n'importe où (wagons.send_away) si personne n'est dedans. Appel : une charrette sortie qui a disparu ou qui est loin réapparaît près du joueur (cassée, elle reste sur place).
  • Réglages (catégorie Chevaux) : wagons.wear_per_km (15), wagons.restrict_storage (oui), wagons.storage_percent (100), wagons.send_away (oui), wagons.return_distance (30 m), wagons.repair_price ($40), wagons.custom_price_percent (100), wagons.custom_at_stable (oui). Objets fournis : wagon_repair_kit, nails, hammer (config.lua et le panel restent prioritaires).
  • Client : client/horses/wagons.lua. Apparence (peinture, teinte, extras, chargement, lanternes), roues détachées, kilomètres signalés, icône sur la carte. Menu : cible (Alt maintenu) sur la charrette, roue avec Coffre, Animaux (Charger, Voir), Réparer (sur place, à l'écurie), Accès, Personnaliser, Renvoyer ; roue du joueur (F6, Cheval, Appeler la charrette). Aucune commande ni invite native ; la touche J d'appel de rsg-horses n'est pas reprise.
  • Non repris : les chariots de travail (bois, pierre, eau : champ work de rsg-horses), la restriction d'achat par métier, la revente, la position de la charrette enregistrée entre deux connexions (à la connexion, une charrette sortie retourne à son écurie, comme les chevaux).
  • À vérifier en jeu : toutes les natives reprises de rsg-horses (liste en tête de client/horses/wagons.lua : roues, peinture, teinte, décors, lanternes, icône, portage et pose des carcasses), le scénario du marteau, la baisse de la santé du véhicule aux chocs, le renvoi (siège libre), la pose des carcasses par TaskPlaceCarriedEntityAtCoord, les noms de peinture et de décor.

Armes : catalogue complet

  • Les 64 armes de rsg-core et de rdr3_discoveries sont connues (Arms.KNOWN) : revolvers (Cattleman, mexicain, Double Action, Gambler, Schofield, LeMat, Navy, Navy Crossover), pistolets (Volcanic, M1899, semi automatique, Mauser), carabines (carabine, Winchester, Henry, Evans), fusils (Springfield, verrou, Varmint, éléphant), fusils de précision (Carcano, Rolling Block, exotique), fusils de chasse (canon scié, deux coups, pompe, répétition, semi automatique, exotique), arcs, couteaux et machettes, torche et lanternes, haches, couperet, couteaux de lancer, tomahawks, Molotov, dynamite, bolas (4), bouteille de poison, canne à pêche, lassos, jumelles, appareils photo et détecteur de métaux. Les hashes sont les joaat des noms (test tests/test_weapons.lua les recalcule tous).
  • Nouveaux types : fishingrod, lasso, kit (sans munition, comme melee, Arms.NO_AMMO).
  • Catalogue de départ (TLRConfig.Weapons, 64 armes avec libellé français, prix et licence) et munitions (TLRConfig.Ammo, 63 types). Les armes rares ou de collection (exotiques, éléphant, horreur, citrouille) et les munitions spéciales sont créées désactivées (active = false) : l'administrateur les active dans le panel, section Armes.

Chevaux : dresseur et élevage (lot 4)

Repris de ton rsg-horses (client/training.lua, server/trainer.lua, config/trainer.lua, client/appearance.lua), dans l'architecture de TLR : le serveur décide, le client joue la scène. Pas repris ici : la formation « jumelles » de server/skills.lua (lecture du sexe des chevaux sauvages, lot des chevaux sauvages).

  • Règles pures : shared/horse_breeding.lua (TLR.HorseBreeding) : sexe (sex, tirage selon 50 % de femelles), races (breedOf), chevaux spéciaux (isSpecial : gang, vétérans, promotions, modèles uniques), tailles par race et modèle (BREED_SCALE, MODEL_SCALE), taille selon l'âge (ageScale : 0,5 à la naissance, 1,0 à 5 ans), monte interdite avant 5 ans (canRide), données du cheval (cleanMeta, tours appris learn), choix de la robe du poulain (foalModel), couple et repos (pairCheck, parentCheck, limitCheck), don (giveCheck). shared/horse_training.lua (TLR.HorseTraining) : accès (access), expérience plafonnée par la classe (addXp), résultats des parcours, de la longe, de la charge et de la séance de dressage, filtre des lieux de la configuration, couleurs de robe (cleanTint, cleanCoat, 21 palettes). Tests : tests/test_horse_breeding.lua, tests/test_horse_training.lua, et le bloc « dresseur et élevage » de tests/test_metiers_integration.lua.
  • Accès : le métier horse_trainer du catalogue (nom réglable : horses.trainer_job), à activer dans le panel (désactivé au départ). Il faut être membre, et en service puisque le métier l'exige ; ou posséder la permission tlr.horses.trainer (déclarée dans la catégorie Chevaux). Horses.Trainer.canTrain(src) rend true ou false, code (NOT_TRAINER, OFF_DUTY). Roue F6, Métier : Dressage, Élevage, Robe (config/radial.lua, entrée horse_trainer). Commandes : /dressage, /elevage, /robe, /dresser, /poulain, /annulerdressage.
  • Base : migration 48 (meta, coat sur tlr_horses) et migration 49 (table tlr_horse_breedings). Le sexe est la colonne sex de la migration 47 du lot des chevaux sauvages. Les chevaux d'avant la migration sont tous mâles : /chevalsexe <numéro> <male|female> (permission tlr.horses.manage ou console) corrige un cheval.
  • Raccords dans le lot 1 (aucune règle changée) : rowToOwned lit meta et coat, Horses.buy appelle Horses.afterBuy (sexe tiré au sort, cheval spécial dressé d'office : expérience maximale, lien maximal, courage et affinité à 10), la vue de l'écran d'écurie appelle Horses.decorateEntry (sexe, âge, poulain, spécial, tours), Horses.initCare et Horses.careView sont enveloppés par server/horses/trainer.lua (la vue envoyée au client contient en plus sex, sexLabel, special, tricks, foal, canRide, scale, look). Côté client, TLR.HorseCareAttach est enveloppé par client/horses/coat.lua, et la sous roue « Plus » du cheval reçoit TLR.HorseMenuExtraItems() (Cabrer, Donner, Dressage, Robe).
  • Entraînement (serveur : server/horses/trainer.lua, client : client/horses/training.lua) : parcours d'obstacles (marqueurs dans l'ordre, durée minimale et maximale vérifiées par le serveur, arrivée vérifiée par la position du joueur), longe (le cheval tourne autour du joueur à pied, un gain horses.lunge_xp par minute, délai de 55 s vérifié), charge (charrette temporaire de config/trainer.lua à livrer avant 30 minutes, durée minimale selon la distance, expérience horses.load_xp). À rebrancher sur le chargement des charrettes du lot 2 : aujourd'hui la charrette est créée par le client sur ordre du serveur et supprimée en fin d'épreuve ; le serveur ne compte que l'arrivée et le temps. Séance de dressage (30 s, applaudir puis cabrage, trois fois) sur son cheval ou celui d'un autre joueur proche : expérience horses.trick_xp, courage horses.trick_courage, tour « cabrage » appris (touche J ensuite, pour tout propriétaire). Événements client vers serveur : tlr:trainer:open, start, tick, done, stop, trickStart, trickDone. Serveur vers client : tlr:trainer:menu, started, result, trickGo, trickResult.
  • Élevage (server/horses/breeding.lua, client : client/horses/breeding.lua) : /elevage devant une écurie (dresseur) : choisir une femelle et un mâle adultes rangés dans cette écurie, l'accouplement coûte horses.breed_price, le poulain naît après horses.breed_seconds (même si l'éleveur est déconnecté), rangé dans la même écurie, âge 0, sexe tiré au sort, robe d'un modèle de la race d'un des parents, classe de la fiche en vente. Repos des parents horses.breed_cooldown (12 h) depuis la naissance, âge minimal horses.breed_min_age, un poulain attendu compte dans la limite horses.max_horses. Le poulain s'appelle « Poulain » jusqu'à ce que l'éleveur le nomme (tlr:breed:nameFoal, rappel à la connexion). Chevaux spéciaux : refusés (horses.special_can_breed).
  • Poulains : l'âge du lot 1 grandit d'un an par 24 h réelles. Un poulain naît petit (horses.foal_min_scale %), grandit jusqu'à horses.foal_adult_age (5 ans) et ne se monte pas avant : le client lui pose le drapeau « non montable » et la taille (SetPedScale), réappliqués toutes les 3 secondes.
  • Don : sous roue Plus, Donner (confirmation) : le joueur le plus proche reçoit le cheval, rangé dans son écurie d'origine, avec ses sacoches. Refus : soi même, cheval spécial (horses.special_can_give), reproduction en cours, trop loin (horses.give_distance), receveur au maximum de chevaux, cheval mort ou à la fourrière. Événement tlr:horses:give (horseId, serverId).
  • Couleurs de robe : /robe (dresseur, près d'une écurie, cheval sorti) : menu au clavier, palette et trois teintes pour le corps puis pour la crinière et la queue, aperçu en direct, Enregistrer envoie tlr:horses:coat : le serveur revalide les valeurs, débite horses.coat_price et enregistre dans tlr_horses.coat.
  • Réglages (catégorie Dresseur et élevage) : horses.trainer_job, lunge_xp, load_xp, load_enabled, trick_xp, trick_courage, give_distance, coat_price, special_can_breed, special_can_give, foal_adult_age, foal_min_scale, breed_seconds, breed_cooldown, breed_min_age, breed_price. Parcours et lieux : config/trainer.lua (TLRConfig.Trainer).
  • À vérifier en jeu : toutes les natives reprises de rsg-horses et marquées dans les en têtes de client/horses/training.lua et client/horses/coat.lua (TaskHorseAction 5, marqueurs, SetPedScale d'un poulain, teintes de robe, drapeau 136, GPS, animations d'applaudissement), les coordonnées de config/trainer.lua, la charrette temporaire (cart06 attelé), la touche J et les touches de la longe.

Création du personnage : même présentation que rsg-appearance

  • Trois étapes avec le même habillage (web/css/creator.css, tailles en vh) : menu sombre à gauche avec titre, fil d'Ariane (Identité › Apparence › Vêtements › Confirmer) et lignes, aide des touches en bas, onglets de caméra en haut (Visage, Buste, Corps entier).
  • Étape 1, Identité (web/js/pages/characters.js) : sexe, prénom, nom, nationalité, âge (année de naissance calculée par le serveur), jour et mois facultatifs ; fiche d'état civil à droite. La nationalité vient de TLR.Registry.NATIONALITIES (le serveur refuse toute autre valeur), colonne nationality (migration 135).
  • Étapes 2 et 3, Apparence et Vêtements (web/js/screens/creator.js, appelé par l'écran wardrobe en mode création ou modification) : Corps (teint en pastilles, silhouette, carrure, taille, haut du corps), Visage (général, puis une rubrique par partie : les 46 traits vérifiés), Cheveux et barbe, Maquillage, Vêtements (catégories en tuiles avec pictogrammes de rsg-appearance dans web/img/creator/, puis un curseur par pièce, ou tenue de départ). Clavier : ↑ ↓ choisir, ← → régler (Maj : par 5), Entrée, Retour arrière ; souris : clic, glisser un curseur, glisser dans le vide pour tourner le personnage, molette pour zoomer (appearance:zoom, nouvelle zone de caméra full).
  • Le personnage affiché est le vrai personnage du jeu (l'écran est transparent, la caméra est celle du Lua client) ; l'aperçu navigateur ne montre qu'une silhouette. À vérifier en jeu : rotation à la souris, zoom, caméra Corps entier.
  • Le magasin de vêtements et les tenues enregistrées gardent leur écran actuel (wardrobe.js) pour l'instant.

VIP : niveaux, durées, avantages, ped et props

Fichiers : shared/vip.lua (règles pures, TLR.VipRules), server/vip/vip.lua (niveaux, attributions, expiration), server/vip/world.lua (ped, props, couleurs, état envoyé au client), server/vip/catalog_data.lua (catalogues repris de rsg_craft, généré), server/rpc/actions/vip.lua (actions), client/vip/vip.lua (effets en jeu et sous roue F6), web/js/screens/vip.js (écran du joueur), web/js/pages/vip.js (section du panel), web/css/vip.css, tests/test_vip.lua.

Données. Migration 140 : tlr_vip_levels (niveaux : nom technique, libellé, rang, avantages en JSON), tlr_vip (une ligne par licence : niveau, donné par, date de début, expires_at NULL pour permanent), tlr_vip_tints (couleurs de vêtements par personnage). Au premier démarrage deux niveaux sont créés (vip, vip_or), modifiables dans le panel. Le VIP est lié au compte (licence), pas au personnage, et indépendant des groupes de permissions : un administrateur peut être VIP.

Avantages d'un niveau (chacun réglable) : cells cases d'inventaire en plus, horses chevaux en plus, outfits tenues enregistrées en plus, colors couleur de chaque vêtement, ped changement d'apparence, props nombre de props posés en même temps (0 = interdit). Le réglage vip.enabled (Paramètres, catégorie VIP) coupe tous les avantages sans effacer les attributions.

Où les avantages s'appliquent (serveur uniquement).

  • Inventaire : Inventory.layoutFor(characterId) = inventory.cells + cells du niveau, utilisé par l'inventaire, les coffres (storage.lua) et les boutiques (shops.lua).
  • Chevaux : TLR.Vip.horseLimit(characterId) = horses.max_horses + horses du niveau, utilisé par l'achat (Horses.buy), la fiche d'écurie, l'élevage, la capture sauvage et le don.
  • Tenues : TLR.Vip.outfitLimit(characterId) = appearance.max_outfits + outfits, utilisé par l'enregistrement d'une tenue, les écrans de magasin et /tenues.
  • Couleurs : colors ; les couleurs sont enregistrées par personnage et rejouées par le client après chaque Ap.apply (événement client tlr:appearance:applied).
  • Ped et props : ped et props, voir plus bas. Le serveur n'accepte que les modèles de server/vip/catalog_data.lua.

Expiration. Les attributions chargées sont vérifiées toutes les 20 secondes, la base entière toutes les 5 minutes (joueurs hors ligne). À la fin : ligne supprimée, journal vip.expire, le joueur est prévenu, son ped est restauré, ses props retirés au delà de sa nouvelle limite, ses couleurs ne sont plus appliquées. Au chargement d'un personnage, un VIP qui se termine dans moins de 3 jours reçoit un rappel. Les dates viennent de l'horloge de la base (NOW()), jamais du client.

Permissions. tlr.vip.read (section VIP, lister), tlr.vip.manage (donner, retirer), tlr.vip.levels (niveaux et réglage vip.enabled). Les jokers tlr.vip.* et tlr.admin.* suivent la règle générale. Journal, catégorie vip : vip.give, vip.remove, vip.expire, vip.level.save, vip.level.delete, vip.ped.set, vip.ped.restore, vip.prop.place, vip.colors.save.

Actions (server/rpc/actions/vip.lua).

  • Administration : vip.overview (attributions, niveaux, durées proposées), vip.search { text }, vip.give { source ou license, level, unit = hours | days | permanent, amount, extend }, vip.remove { license }, vip.level.save { name, label, rank, benefits }, vip.level.delete { name } (refusé tant que des joueurs ont ce niveau). Durée de 1 heure à 3650 jours. extend ajoute la durée à celle qui reste si le joueur a déjà ce niveau.
  • Joueur : vip.me, vip.ped.set { model, outfit }, vip.ped.restore, vip.props.mine | place | remove { id } | clear, vip.colors.get, vip.colors.save { tints }. Chaque action revérifie l'avantage du niveau (VIP_BENEFIT), le catalogue (VIP_MODEL), la limite (VIP_PROP_LIMIT) et la distance de pose, 30 m au plus du joueur (VIP_PROP_FAR).
  • Événements serveur vers client : tlr:vip:state (niveau, avantages, reste, couleurs, ou false), tlr:vip:applyPed, tlr:vip:restorePed, tlr:vip:props, tlr:vip:propPlaced, tlr:vip:propRemoved, tlr:vip:placeResult. Client vers serveur : tlr:vip:placeProp (pose par l'outil, une par 700 ms), tlr:vip:propsRequest, tlr:vip:restorePedRequest, tlr:vip:clearPropsRequest.

Ped. Catalogue public des humains de rsg_craft (1651 modèles, avec leur nombre de tenues), choix de la tenue, retour à son personnage (mp_male ou mp_female selon le sexe, tenue de base, puis apparence enregistrée). L'apparence de ped n'est jamais enregistrée : elle disparaît au changement de personnage, à la déconnexion et à la fin du VIP. Le changement de modèle retire les armes du ped : le retour à son personnage ne les remet pas, à vérifier en jeu.

Props. Catalogue public de rsg_craft (1080 modèles). Aperçu 3D devant le joueur, puis outil de pose repris de decor.lua : l'objet suit la visée, molette pour tourner, MAJ + molette pour la hauteur, CTRL + molette pour incliner, flèches pour ajuster, Espace pour bloquer, clic gauche pour poser (le serveur contrôle), clic droit pour finir. Les props sont des objets locaux (non réseau) créés par chaque client à moins de 120 m, comme dans rsg_craft ; le serveur garde la liste. Suppression d'un prop ou de tous depuis l'écran VIP, nettoyage à la déconnexion, au changement de personnage, à l'expiration ou si la limite du niveau baisse (les derniers posés partent d'abord).

Interface. Panel, section VIP (groupe Gameplay) : donner (recherche, niveau, jours, heures ou permanent, prolonger), liste des VIP avec le reste et le bouton Retirer, niveaux et avantages (création, modification, suppression). Menu admin en jeu (F1), Joueurs, un joueur, VIP : niveau puis durée (1, 7, 30, 90 jours, 1 an, permanent, autre durée en jours ou en heures), ou Retirer le VIP. Roue F6 : entrée VIP, visible seulement pour un VIP, avec le niveau et le reste, puis les sous roues Ped (Choisir un personnage, Reprendre mon apparence), Props (Catalogue, Tout retirer) et Couleurs, selon les avantages du niveau. Commande /vip [ped | props | couleurs]. Export client exports.tlr_core:GetVip().

Natives à VÉRIFIER EN JEU (reprises de rsg_craft client/decor.lua et rsg-horses client/appearance.lua) : SET_PLAYER_MODEL suivi de 0x283978A15512B2FE et 0x77FF8D35EEC6BBC4 pour le ped ; CREATE_OBJECT_NO_OFFSET pour les props ; 0x90403E8107B60E81, 0x9B90842304C938A7, 0xA9C28516A6DC9D56, 0xBC6DF00D7A4A6819 et 0xCC8CA3E88256E58F pour les teintes (palettes metaped_tint_generic_clean, metaped_tint_generic, metaped_tint_generic_weathered, metaped_tint_leather, metaped_tint_combined : toutes ne conviennent peut être pas à toutes les pièces).

Chevaux : menu d'écurie de rsg-horses (lot 5)

Le menu d'écurie « encre et sceau de cire » de ton rsg-horses (html/index.html, style.css, app.js, client/stable.lua) remplace l'ancien écran stable. Même parchemin, mêmes médaillons, mêmes carrousels, mêmes sceaux de cire, mêmes cadrans, même caméra : seule la couche de données est celle de TLR (RPC, argent en centimes, pas d'or).

  • Écran : web/js/screens/stable.js (le menu), web/js/screens/stable_text.js (textes français tirés de locales/fr.lua de rsg, sans tiret, « charrette »), web/css/stable.css (la feuille de rsg, tout sous .stb ; les classes déjà prises par les autres feuilles portent le préfixe s- : s-btn, s-actions, s-empty, s-list, s-tile, s-foot, s-desc, s-meta, s-ring, s-modal-actions), images dans web/img/stable/. Les polices Rye et Alegreya viennent de hud.css. L'adaptateur est la table ACTIONS de stable.js : elle traduit chaque demande du menu de rsg en action RPC de TLR.
  • Accueil : acheter un cheval, mes chevaux, acheter une charrette, mes charrettes, élevage (dresseurs). L'élevage ouvre le menu du lot 4 (/elevage) et reste grisé tant que view.breeding.available est faux : c'est le point d'accroche. Peinture et réparation d'une charrette ouvrent les menus du lot 2 (tlr:wagons:customInfo et tlr:wagons:repairStable) pour la charrette sortie, après fermeture du menu.
  • Boutique de chevaux : races (tlr_horse_breeds) et robes (une ligne de tlr_horse_shop par robe), classe en chiffres romains, sexe, graines de statistiques (statistiques de la race, ou valeurs fixes de la robe), stockage, peaux, prix en sceau. Le sexe choisi est enregistré (Horses.buy(..., { sex }), après Horses.afterBuy du lot 4).
  • Mes chevaux : fiche (dressage, expérience, sexe, âge, lien, besoins, courage, affinité), Sortir, Ranger, Récupérer (fourrière), Équipement (cadrans, prix horses.equip_price par pièce changée), Robe (cadrans, dresseurs, prix horses.coat_price), Soigner, Déplacer (liste des écuries, prix horses.transfer_price), Renommer (gratuit), Vendre (pourcentage de revente de la fiche). Même chose pour les charrettes.
  • Argent : le compte du réglage horses.pay_account (liquide ou banque), centimes entiers, 100 centimes = 1 $. Le pied de page montre les deux soldes ; celui qui paie est souligné. Pas d'or.
  • Base : migration 150 (table tlr_horse_breeds, colonnes du catalogue sur tlr_horse_shop : breed, coat_key, storage, pelts, resale, can_breed, outfit, coat, mane_tail, fixed_stats, jobs, scale, sort), migration 151 (caméra et point de présentation facultatifs de tlr_stables : cam_x, cam_y, cam_z, prev_x, prev_y, prev_z, prev_heading ; coat_key de tlr_horses, la robe achetée).
  • Catalogue : shared/horse_catalog_data.lua (généré une fois depuis config/horses.lua de rsg : 39 races, 308 robes, 21 teintes, catégories de charrettes), écrit en base au premier démarrage seulement (Horses.seedExtra, appelé par Horses.load). Les prix de rsg sont multipliés par 100 (centimes). Une robe vendue seulement en or est reprise désactivée. Cinq modèles hors de la liste autorisée (mule, âne, cadavre) sont ignorés. Les quatre chevaux d'exemple de config/config.lua sont retirés de la vente (deleted). Ensuite la base fait foi : le panel (section Chevaux) modifie le prix, le libellé et l'activation d'une robe, et affiche le catalogue complet en lecture seule (action horses.catalog.list).
  • Règles pures : shared/horse_catalog.lua (TLR.HorseCatalog) : nettoyage des statistiques et des teintes, prix de revente, restriction par métier, regroupement par race et par catégorie de charrette, point et sens de la caméra (showroom).
  • Serveur : server/horses/stable_ui.lua. Enveloppe Horses.stableView (ajoute shopHorses, shopWagons, money, stables, palettes, isTrainer, breeding, stable.services, stable.showroom, et les champs de fiche de chaque cheval et charrette), Horses.buy (robe retenue, sexe, restriction par métier) et Horses.offers (race et clé de robe). Nouveaux : Horses.sell, Horses.move, Horses.saveCoat. Actions RPC : horses.stable.sell, horses.stable.move, horses.stable.coat, horses.catalog.list ; horses.stable.buy accepte sex. Réglages : horses.catalog_jobs (robes réservées à un métier, non par défaut : les listes de rsg parlent de emeraldranch et staff), horses.sell_enabled (revente, oui).
  • Client : client/horses/stable_view.lua (TLR.StableShowroom) : caméra autour de l'animal, cheval ou charrette d'aperçu, lumière d'atelier, joueur figé et radar caché pendant l'ouverture. Callbacks de la page : stable:preview (horse, wagon, equip, coat, none), stable:rotate (glisser), stable:zoom (molette), stable:wagon, stable:breeding. Le cheval d'aperçu reçoit la tenue de base, le harnachement (TLR.HorseEquipPed, exposé par client/horses/horses.lua), la robe (TLR.HorseApplyLook du lot 4), le sexe et la taille. Rien n'est enregistré côté client.
  • Caméra par écurie : colonnes facultatives de tlr_stables. Sans point de présentation, le cheval se tient au point de sortie ; sans caméra, elle se place du côté du comptoir. À régler écurie par écurie en jeu.
  • Limites connues : la santé et l'endurance des fiches sont affichées pleines à l'écurie (le serveur ne garde que la valeur brute du jeu) ; un cheval vendu perd le contenu de ses sacoches (comme une suppression par le panel) ; la peinture et la réparation d'une charrette demandent d'être à portée de la charrette sortie (règle du lot 2) ; la vente à un autre joueur, le marché aux chevaux, l'offre d'Emerald Ranch et le nom du poulain de rsg ne sont pas repris (le lot 4 a ses propres menus).
  • À vérifier en jeu : toutes les natives de client/horses/stable_view.lua (liste en tête du fichier : caméra, lumière, tenue de base, expression du sexe, pose au sol), les distances de caméra, la hauteur et le point de présentation de chaque écurie, le rendu des robes et du harnachement sur l'animal d'aperçu, le gel du joueur, le rendu des polices et des filtres d'encre dans le navigateur du jeu.

Classes, fers et administration du catalogue (ajout du lot 5)

  • Six classes (I à VI) : TLR.HorseCare.MAX_TIER = 6, expérience maximale 5000 et 6000 pour les classes V et VI, santé et endurance CLASS_STATS de 70 et 80 % au départ jusqu'à 100 %. Les menus affichent les chiffres romains jusqu'à VI.
  • Rareté : TLR.HorseCatalog.rarityTier(prix, statistiques, rare) : le prix pèse jusqu'à 5 points, les statistiques de la race jusqu'à 2 environ, une robe rare (chevaux de l'histoire, statistiques fixes) ajoute un demi point. Le catalogue importé au premier démarrage reçoit ces classes (et non celles de rsg, qui n'allaient que de I à IV). Panel, Catalogue, « Reclasser selon la rareté » refait le calcul pour les robes en vente, et pour les chevaux déjà achetés si on coche la case (leur dressage est gardé).
  • Fer de classe : objet horse_shoe_class (à mettre en vente dans une boutique ou à donner par le panel). L'utiliser avec son cheval sorti le fait passer à la classe suivante, son dressage gardé en pourcentage (C.upgradeTier), jusqu'à la classe VI ; le fer n'est consommé que si le cheval monte. Journal : horse.shoe.
  • Administration (permission tlr.horses.shop) : le panel permet de modifier chaque robe (libellé, prix, en vente, classe, stockage, peaux, revente, reproduction), les statistiques et le texte d'une race, tous les prix d'un coup (pourcentage, pour une race ou toutes). Actions RPC : horses.catalog.save, horses.catalog.breed, horses.catalog.prices, horses.catalog.reclass. Journal : catalog.coat, catalog.breed, catalog.prices, catalog.reclass.
  • À vérifier en jeu : le fer de classe sur un cheval sorti (nouvelle classe visible sur la fiche et dans l'écurie), les barèmes de santé et d'endurance des classes V et VI.

Groupe apparence : coiffeur, taille, bains, garde robe (portage de rsg-barbers, phils-pedscaler, rsg-bathing, rsg-wardrobe)

Coiffeur (shared/barber.lua, server/barber/barber.lua, client/barber/barber.lua, écran web/js/screens/barber.js, migration 220, config/barber.lua) :

  • Le catalogue de coiffures, barbes et calques est celui de shared/appearance_data.lua (rien dupliqué). Le salon ne touche que trois services : Coiffure (hair, hairColor), Barbe (beard, beardColor, stubble, hommes) et Maquillage (champs du groupe makeup).
  • Point E par salon (table tlr_barbers, 3 salons de départ), PNJ de coiffeur qui apparaît près du joueur, fauteuil, caméra, lumière, aperçu en direct, icône de carte.
  • Action barber.buy { body } : seuls les champs du salon sont lus (B.merge), prix par service modifié, argent liquide retiré, apparence enregistrée par Appearance.store. Le visage, le corps, la taille et les vêtements ne peuvent pas changer ici.
  • Réglages : barber.price_hair, barber.price_beard, barber.price_makeup (un salon peut avoir ses prix). Permissions tlr.barber.read, update, delete. Actions barber.overview, barber.save, barber.delete. Export serveur OpenBarber(src, id).

Taille des personnages (shared/pedscale.lua, server/pedscale/pedscale.lua, client/pedscale/pedscale.lua, migration 221) :

  • Le serveur diffuse l'échelle de chaque joueur (taille de création, ou réglage qui prime) ; chaque client l'applique aux joueurs proches. Avant, la taille du créateur n'était vue que par son propriétaire.
  • Menu Citoyen, Taille (tlib.context) : préréglages, aperçu, enregistrer, réinitialiser, choisir un autre joueur (permission tlr.pedscale.set). Réglages pedscale.self_allowed (faux par défaut), self_min, self_max, speed_enabled, speed_inverse.
  • Actions pedscale.list, pedscale.set, pedscale.reset. Exports serveur GetPlayerScale, SetPlayerScale, ResetPlayerScale.

Bains publics (shared/bathing.lua, server/bathing/bathing.lua, client/bathing/bathing.lua, client/bathing/structs.js, config/barber.lua TLRConfig.Baths) :

  • 7 bains : point E à l'accueil, paiement serveur, une baignoire par salle, invincibilité ordonnée par le serveur, scènes animées de rsg-bathing, bain de luxe avec employé.
  • La propreté (metadata.needs.clean, HUD) monte d'un cinquième par zone frottée (délai minimal de 3 secondes entre deux zones, B.acceptStep) ; le bain de luxe rend de l'énergie. Réglages bathing.price, bathing.deluxe_price, bathing.deluxe_energy, bathing.invincible. Export client IsBathingActive, export serveur IsBathing(src).

Garde robe (shared/wardrobe_wear.lua, client/appearance/wear_states.lua, server/appearance/wear_exports.lua) :

  • Sous roue Habits, Chemise : manches retroussées, col ouvert manches retroussées, col ouvert manches longues (affichage seulement, rejoué après chaque application de l'apparence).
  • Exports serveur TogglePlayerClothing(src, nom), RemovePlayerClothing(src), DressPlayer(src) (noms de rsg_wardrobe traduits par W.resolveName). Exports client IsWearing(nom), RemoveAllClothing, TogglePieceByName. Non repris : holsters, armure, chargeurs (pas de catégorie correspondante).

Craftworld : décor, props de récolte, ferme, montures, corps, ensemble, sifflet (rsg_craft, fonctions hors craft)

Règles pures : shared/craftworld.lua (TLR.CWRules). Serveur : server/craftworld/ (core.lua migrations 217 à 219, permissions et réglages ; decor.lua, props.lua, farm.lua, services.lua montures, ensemble, sifflet, contrôle d'armes ; actions.lua actions RPC). Client : client/craftworld/. Écran : web/js/screens/craftworld.js (mode craftworld), commandes /monde et /decor, entrée « Monde » de la roue F6 pour les administrateurs. Configuration de départ : config/craftworld.lua (TLRConfig.CraftWorld).

  • Permissions (refus par défaut) : tlr.craftworld.decor, tlr.craftworld.props, tlr.craftworld.farm, tlr.craftworld.mounts.
  • Actions RPC : craftworld.decor.list|delete|persist|clear|goto|move|restore, craftworld.ped.transform|restore, craftworld.props.list|save|delete, craftworld.farm.info|addCow|removeCow, craftworld.mounts.list|lend.
  • Événements client vers serveur (permission revérifiée) : tlr:cw:decorPlace, tlr:cw:decorRemove ; sans permission : tlr:cw:decorSync, tlr:cw:propsSync, tlr:cw:farmSync, tlr:cw:outfitCreate (nom), tlr:cw:alarm, tlr:cw:weaponCheckHit.
  • Tables : tlr_cw_decor et tlr_cw_removed (décor permanent, objets du jeu cachés), tlr_cw_props (props définis), tlr_cw_cows (vaches posées), tlr_cw_daily (récolte du jour). Le décor éphémère reste en mémoire.
  • Réglages (catégorie Craftworld) : craftworld.farm.*, craftworld.props.gather_enabled, craftworld.props.daily_max, craftworld.emergency.cooldown, craftworld.weaponcheck.enabled.
  • Récolte et ferme passent par le target (tlib.target) et les actions protégées cw:gather, cw:milk, cw:eggs, avec TLR.ResJobs.timed pour la durée.
  • Pour la session des stations de craft : exports.tlr_core:GetCWProps() renvoie les props définis (type gather ou station avec categories) ; coupez craftworld.props.gather_enabled si les stations gèrent déjà la récolte de ces props. Événement client tlr:cw:props (liste complète).
  • Objet d'inventaire ensemble (fourni par TLR.Items.provide) : la tenue enregistrée du personnage (lue par le serveur) est gardée dans sa metadata (name, sex, clothes) ; l'utiliser la remet par TLR.Appearance.store.
  • Déjà couvert ailleurs, non refait : besoins vitaux (HUD), col et manches (garde robe), cheval (écuries, soins, sacoches, plaque de nom), armes (équipement serveur), props et ped du VIP (server/vip/world.lua), appels aux médecins et à la loi (TLR.JobAlerts).

Chasse, pêche et courses de chevaux (ports de rsg hunting, rsg fishing, rsg track)

Domaines hunting, fishing et tracking. Données et règles pures dans shared/hunting_data.lua, shared/hunting.lua, shared/fishing_data.lua, shared/fishing.lua, shared/tracking.lua ; serveur dans server/hunting/, server/fishing/, server/tracking/ ; client dans client/hunting/, client/fishing/, client/tracking/ ; écran web/js/screens/tracking.js et web/css/tracking.css. Migrations 190 (réserves des trappeurs) et 191 (circuits de course).

Chasse. Le dépeçage natif du jeu (événement de fin d'interaction avec un animal) rend les objets de la bête (peaux, cornes, plumes, becs, coeurs). Cinq trappeurs PNJ achètent et revendent, avec une réserve partagée enregistrée en base et un prix de rachat majoré quand la réserve est basse. Douze bouchers PNJ rachètent la bête portée (argent selon la qualité médiocre, bonne ou parfaite, et viande crue). Les points d'interaction sont des TLR.Points (touche E ou target) avec leurs blips. Réglages hunting.* dans le panel (Paramètres, Chasse), permission tlr.hunting.manage, commande console tlr_hunting_stock_reset. Crochets pour un autre domaine (écosystème, licences) :

TLR.Hunting.addPriceIndex(function(kind, key) return 1.0 end)               -- kind : 'model' (hash de bête) ou 'item'
TLR.Hunting.addSkipCheck(function(src, netId, entity, skin) return false end) -- true : pas de récompense native
TLR.Hunting.stock(item)                                                      -- réserve actuelle

Une bête portant l'état Entity(ped).state.eco n'est ni récompensée ni rachetée par ce domaine. Le « raw_meat » de rsg hunting est la viande crue du cœur (viande_crue).

Pêche. La pêche native de RDR2 (structure de 28 champs lue et écrite par client/fishing/fishing_native.js) est utilisable par tout le monde avec la canne WEAPON_FISHINGROD en main et un appât utilisé depuis l'inventaire. Le serveur exige un appât dépensé dans les 5 dernières minutes, borne le poids et limite les prises (3 s). Le poids du poisson est dans la metadata de l'objet (meta.weight, en kg). Pont avec le métier de pêcheur : fisherman_appat compte comme un ver de terre, et l'activité fishing:vente rachète les poissons au point de vente du pêcheur. Réglages fishing.*.

Courses de chevaux (rsg track n'est pas un système de pistage, c'est un gestionnaire de circuits). Un circuit se construit en jeu (J départ et points, G arrivée, Retour arrière annule) puis s'enregistre. Une seule course à la fois : inscription, compte à rebours de 10 s, tours comptés par le serveur (position à l'arrivée et durée minimale d'un tour vérifiées), prix du vainqueur versé en liquide, historique des 5 dernières courses. Entrée par la roue (Course) ou la touche E au départ du circuit chargé. Actions RPC : tracking.state, tracking.save, tracking.join, tracking.leave, tracking.start (joueur) et tracking.load, tracking.delete, tracking.laps, tracking.max, tracking.reset (permission tlr.tracking.admin). Réglages tracking.*. Événements : serveur vers client tlr:tracking:state, tlr:tracking:open, tlr:tracking:countdown, tlr:tracking:go, tlr:tracking:reward ; client vers serveur tlr:tracking:lap, tlr:tracking:request.

Registre du shérif et livre des lois

Documentation complète : docs/portage/mdt.md (contrat des actions mdt.*) et docs/portage/lawbook.md.

Exports serveur : MdtHasAccess(src), MdtHasPermission(src, 'create'|'warrants'|'delete'|'admin'), MdtGetPermissions(src), MdtAddAlert({ message, coords }), MdtRecordArrest({ characterId, minutes, reason, officerName, officerCharacterId }), MdtRecordFine({ characterId, amount (centimes), reason, officerName, officerCharacterId, paid }), MdtGetFines(citizenId), MdtGetUnpaidFines(citizenId), MdtRefreshWanted(characterId), GetMdtWanted(src), MdtIsStaff(citizenId), GetLawbook(). Export client : OpenMdtProfile(citizenId). State bag du joueur : mdtWanted = { warrant, reason, overdue }. Migrations 155 à 157. Permissions du panel : tlr.mdt.read, tlr.mdt.rights.update, tlr.mdt.audit.read, tlr.lawbook.edit.

Utilitaires : chat, échantillons, essentiels, fiche du joueur, personnages fixes, compatibilité rsg-menubase

Port de rsg-chat, rsg-samples, rsg-essentials, rsg-playerinfo, rsg-npcs et rsg-menubase (lot utilitaires). Migrations 227 (tlr_samples) et 228 (tlr_npcs).

Chat (shared/chat.lua TLR.ChatRules, server/chat/chat.lua, client/chat/chat.lua, web/js/chat.js, web/css/chat.css). Même interface que rsg-chat sans Vue ni police externe : cinq messages qui s'effacent, saisie avec historique, suggestions de commandes, gabarits {0}, couleurs ^0 à ^9. Touche T (INPUT_MP_TEXT_CHAT_ALL). Événements des autres ressources repris : chatMessage, chat:addMessage, chat:addSuggestion(s), chat:removeSuggestion, chat:addTemplate, chat:clear, thèmes chat_theme. Serveur : /ooc (étiquette réglable, auteur = nom du personnage, débit limité), /say (permission tlr.chat.say), /clearchat, chat:server:ServerPSA (permission tlr.chat.psa, ou console), commande inconnue notifiée. Réglages chat.*. Exports serveur SendChatMessage(cible, message), ServerAnnouncement(texte). Journal : catégorie chat. La ressource « chat » de cfx-server-data ne doit PAS être démarrée.

Échantillons (shared/samples.lua TLR.SampleRules, server/samples/samples.lua, server/rpc/actions/samples.lua, client/samples/samples.lua, web/js/screens/samples.js, web/css/samples.css). 72 espèces avec libellés français, un prélèvement par espèce et par personnage (clé unique), prime de première découverte, paliers 5/10/25/50, animal vedette (samples.featured), tableau d'honneur et statistiques, certificat. Argent en centimes (dollars de rsg x 100), compte liquide, opId unique. Le serveur vérifie délai, verrou, entité réseau du bon modèle à moins de 6 m (samples.verify_entity). Le hash du bighorn de rsg (-15687816381, un chiffre de trop) est corrigé en -1568716381 (joaat de a_c_bighornram_01). Tableau d'honneur : point samples_board (réglages samples.board*). Action RPC samples.view, samples.featured (permission tlr.samples.manage). Roue F6, Citoyen, Naturaliste. Export client OpenSamples(mode).

Essentiels (shared/essentials.lua TLR.Essentials, shared/essentials_zones.lua, server/essentials/essentials.lua, client/essentials/essentials.lua). Réglages essentials.*, discord.* (panel). Repris : expulsion AFK (mesurée par le serveur, essentials.afk_minutes, permission d'exemption tlr.essentials.afk_exempt), vision de l'aigle par métier, densité (9 multiplicateurs), première personne en visant, effacement des défis RDO, siège du cocher, noyade, charrette bloquée, livre au menu pause, touches désactivées, carcasses, bascule PvP (tlr:essentials:pvpToggle), portes des tailleurs, couleurs de région et noms de la carte (exports client GetZoneName(hash), GetZoneNameAt(x, y, z)), présence Discord, lanternes, cœurs, contrôle des armes (compare à GetEquippedWeapon), bandana (roue F6), pointer du doigt (B), boire à la rivière ou à une pompe (tlr:essentials:drink), webhooks (TLR.Webhook.send(nom, titre, couleur, texte, tagEveryone), événement local tlr:webhook, export SendWebhook, URLs dans TLRConfig.Webhooks), orage avant redémarrage txAdmin et météo de Noël (weathersync facultatif). Non repris car déjà couverts : zones armes/PvP (client/world/zones.lua), mains en l'air (loi), lavage et gourde (client/player/hud.lua, client/consume), /hideui (/hud).

Fiche du joueur (client/essentials/playerinfo.lua) : roue F6, Citoyen, Mes informations, ou /fiche. Lignes réglées par playerinfo.show_*. Les comptes de banques de villes de rsg n'existent pas (un seul compte bancaire) ; le statut hors la loi est remplacé par la situation judiciaire.

Personnages fixes (server/npcs/npcs.lua, client/npcs/npcs.lua, web/js/pages/npcs.js, web/js/npcaudio.js) : table tlr_npcs gérée dans le panel (section Monde, Personnages fixes, permissions tlr.npcs.read et tlr.npcs.update) plus TLRConfig.Npcs (format Config.PedList de rsg-npcs, lecture seule). Apparition à l'approche, scène, icône, voix (xsound si présent, sinon son joué par la page). Réglage npcs.enabled, export GetNpcs.

Compatibilité rsg-menubase (shared/menubase.lua TLR.MenuBase, client/compat/menubase.lua). exports.tlr_core:GetMenuData() renvoie le même objet MenuData (Open, Close, CloseAll, GetOpened, IsOpen, ReOpen, menu.update/refresh/setElement...) mais sur le menu au clavier de la lib (tlib.menu). Correspondance : élément simple = option avec description ; curseur à options ou min/max/hop = option à valeurs (un curseur déplacé appelle change puis submit, comme rsg) ; Entrée = submit ; Retour arrière ou Échap = cancel puis fermeture ; disableMovement fige le joueur ; lockInventory pose inv_busy. Non repris : grille et images, fermeture par le menu pause.

Plantes récoltables (port de rsg_plantes)

Fichiers : shared/plants.lua (règles pures, TLR.PlantRules), server/plants/plants.lua (types, plantes posées, récolte, administration), client/plants/plants.lua et client/plants/gizmo.lua (props locaux, invite de récolte, gizmo de pose), web/js/screens/plants.js (écran d'administration), web/js/plantsfx.js (bandeau du gizmo), web/js/data/plants_props.js (catalogue des modèles), web/css/plants.css, tests/test_plants.lua.

Données. Migration 196 : tlr_plant_types (nom, modèle, modèle après récolte, objet, quantités, temps de récolte, repousse en minutes, outil, animation, métier) et tlr_plant_spots (type, position, cap, date de dernière récolte). Au premier démarrage sans type, les 23 types de départ de sql/plantes_metiers.sql sont créés (P.SEED_TYPES, avec les objets de TLR : herbalist_achillee, herbalist_ginseng, herbalist_menthe, herbalist_bardane du métier d'herboriste, les plantes d'herboristerie, et les plante_* fournies ici). L'action plants.seed ajoute ceux qui manquent. Objets fournis : neuf plantes plante_* (catégorie plante) et la pelle plante_pelle.

Fonctionnement. /plantes (permission tlr.plants.manage, revérifiée par le serveur) ouvre l'écran : types (formulaire, aperçu, pose), catalogue de 477 modèles, plantes posées (aller, déplacer, faire repousser, supprimer). La pose ferme l'écran, un fantôme suit la visée (Q et E tournent, molette hauteur, Maj pas fin, R remise à zéro, Entrée valide, Retour annule), une plante à la fois ou éparpillées autour d'un point (jusqu'à 50, rayon 20 m). Les plantes sont des objets locaux créés à moins de 80 m. Récolte : maintenir E près de la plante prête, ou option « Récolter » du target si le réglage plants.use_target est actif. Le serveur vérifie repousse, distance (3,5 m), métier, outil, place, puis la durée réelle (TLR.ResJobs.timed) avant de donner l'objet.

Actions RPC : plants.data, plants.saveType, plants.deleteType, plants.seed, plants.delete, plants.regrow, plants.teleport (lecture : tlr.plants.read, le reste : tlr.plants.manage). Événements réseau vers le serveur : tlr:plants:openRequest, tlr:plants:place, tlr:plants:move, tlr:plants:harvest. Vers les clients : tlr:plants:data, tlr:plants:types, tlr:plants:spot, tlr:plants:removeSpot, tlr:plants:open. Export serveur GetPlantTypes(), export client PlantGizmoRun(opts). Réglages : plants.enabled, plants.use_target. Journal world : plants.type.save, plants.type.delete, plants.seed, plants.spot.place, plants.spot.move, plants.spot.delete, plants.teleport ; journal jobs : plants.harvest.

Herboristerie (port de rsg_herbalist)

Fichiers : shared/herbalism_data.lua (TLR.HerbData : plantes, réactifs, zones, échecs, produits, seringue, charrette, objets, sans aucune recette), shared/herbalism.lua (TLR.HerbRules, règles pures), server/herbalism/recipes.lua (recettes secrètes, serveur seulement), core.lua (réglages, base, grimoire, effets), harvest.lua (cueillette des zones), experiment.lua (bol et pilon), products.lua (produits et seringue), wagon.lua (charrette), client client/herbalism/herbalism.lua et effects.lua, écran web/js/screens/herbalism.js, effets plein écran et sons web/js/herbfx.js, web/css/herbalism.css, images web/img/items/ et web/img/herbalism/, tests tests/test_herbalism.lua et tests/test_herbalism_server.lua.

Lien avec le métier d'herboriste du cœur (server/jobs/metiers/herbalist.lua) : rien n'est dupliqué. L'achillée, la menthe et la bardane du métier (herbalist_achillee, herbalist_menthe, herbalist_bardane) servent aux expériences et aux recettes. Le métier garde ses points de cueillette, ses remèdes et sa vente.

Données. Migration 197 : tlr_herb_discoveries (une ligne par personnage et recette, clé unique : la récompense de découverte n'est donnée qu'une fois) et tlr_herb_journal (plantes consignées, notes). La migration 198 est réservée et inutilisée.

Parcours. Cueillette : 44 plantes tirées au démarrage dans cinq zones, réservation, barre de progression mesurée par le serveur, repousse de 10 à 20 minutes, nouvelle plante consignée au grimoire. Grimoire (objet herbalist_book, ou « Grimoire » de la roue F6 pour un herboriste) : sommaire, plantes, recettes découvertes, recettes inconnues (nombre seulement), notes, table de préparation. Le bol et le pilon ouvrent directement la table. Table : toucher une plante, un réactif, Préparer ; le mortier travaille seul pendant herbalism.prepare_seconds, le serveur décide (jeton, durée réelle, place libre testée pour toute combinaison, ingrédients retirés, une seule résolution par séance). Échec : réaction tirée parmi sept, perte exacte de herbalism.failure_percent de la vie maximale, jamais mortelle. Produits : baume (soin progressif), décoction de purge (nausées imprévisibles), teinture de nuit (un œil, 10 minutes, reprise après reconnexion), élixir de sang froid (15 % des dégâts rendus). Seringue de renaissance : jamais sur soi, patient réellement inconscient (état du serveur), 2,5 m, délais du soigneur et du patient, consommée seulement si TLR.Vitals.revive a eu lieu, super boost de 30 s pour le patient. Charrette : dépôts posés comme points (touche E, visibles des herboristes), le même point sort ou range la charrette, coffre herbwagon:<personnage> du domaine Stockage ouvert à l'arrière (invite G).

Actions RPC : herbalism.book, herbalism.notes, herbalism.expStart, herbalism.expFinish, herbalism.expAbort. Réglages (catégorie Herboristerie) : herbalism.enabled, job_required, restrict_harvest, harvest_seconds, respawn_min, respawn_max, experiment_cooldown, prepare_seconds, failure_percent, syringe_cooldown, syringe_target_cooldown, wagon_enabled, wagon_cooldown. Permissions : tlr.herbalism.settings, tlr.herbalism.admin (commande /herbo_clear [id]). Exports client : HerbStopEffect, HerbStopAllEffects, HerbIsEffectActive. Messages vers l'interface : herb:fx, herb:night, herb:sound (routés par web/js/main.js vers web/js/herbfx.js).

Artisanat (domaine crafting)

Fusion de rsg_craft (niveaux par catégorie, prix, réussite, poste de cuisine, récolte, consommables) et de rsg-crafting (livres, établis, favoris, éditeur de recettes). Fichiers : shared/crafting.lua (règles pures, TLR.CraftRules), shared/crafting_data.lua (338 recettes, 5 livres, 21 établis, générés), shared/crafting_stations.lua, server/crafting/*, server/rpc/actions/crafting.lua, client/crafting/crafting.lua, écrans web/js/screens/crafting.js et craftingadmin.js, section du panel web/js/pages/crafting.js. Migrations 210 à 213 (tlr_craft_recipes, tlr_craft_disabled, tlr_craft_exp, tlr_craft_prefs, tlr_craft_jobs, tlr_craft_daily, tlr_craft_points).

  • Recette : { id, item, amount, desc, category, level, exp, jobs (métiers TLR), minGrade, success, time (s), price (centimes), gun, meta, ingredients = { { item, amount, keep, ret, retAmount } } }. Les métiers RSG sont traduits en métiers de tlr_core (voir jobMap dans le fichier de données) ; les grades RSG deviennent minGrade.
  • Niveau d'une catégorie = expérience / crafting.exp_per_level. Un objet inconnu rend la recette invalide (cachée, signalée dans l'onglet Objets manquants).
  • Exports serveur : GetCraftingExp(src, catégorie), GiveCraftingExp(src, catégorie, quantité), GetCraftingLevel(src, catégorie), GetCraftingRecipes(), OpenCraftingBook(src, source) (main, book:<objet>, bench:<clé>), GetAlcoholLevel(src), AddAlcohol(src, n). Client : OpenCrafting(source). Événement serveur tlr:crafting:crafted (src, idRecette, réussites).
  • Dans le cœur : TLR.Craft (state, list, tabs, start, cancel, collectJob, cancelJob, addExp, level, reload, recipe, bench), TLR.CraftWorld (postes, récolte, établis), TLR.CraftAlcohol, TLR.CraftAdmin.
  • Actions RPC joueur : crafting.bootstrap, crafting.recipes, crafting.start, crafting.favorite, crafting.taborder, crafting.gather, crafting.bench.collect, crafting.bench.cancel. Panel : crafting.admin.meta|list|get|items|audit|journal|save|delete|toggle|points|point.save|point.delete|exp.
  • Permissions : tlr.crafting.read, tlr.crafting.recipes, tlr.crafting.points, tlr.crafting.exp (refus par défaut). Paramètres crafting.* (catégorie Artisanat).
  • Objets : TLR.Items.provide('crafting', …) (465 objets, sans remplacer un objet déjà défini ailleurs) ; consommables par TLR.Inventory.onUse (faim, soif, alcool, malaise).
  • Roue F6 : entrée « Artisanat » insérée par le client dans TLRRadial.MenuItems (config/radial.lua n'est pas modifié). Commande /artisanat.

Mines, orpaillage et bûcheronnage (lots 193 à 195)

Portage de rsg-mining, rsg-goldclaim et phils-adminmineswood. Montants en centimes, jeu en 1900, tout en français. Webhooks Discord et vérificateurs de version non portés.

  • Mines à bail (shared/mining.lua, server/mining/mining.lua, client/mining/mining.lua, écran mining). Trois mines (Annesburg, Gaptooth, Grizzlies), un contremaître PNJ et un point mining_foreman par mine. Le joueur loue la mine, embauche des ouvriers (candidats renouvelés), remplit la caisse des salaires et les provisions (pain, gourde, pioche du mineur), puis récupère le minerai de l'entrepôt. Cycle de travail serveur toutes les mining.work_interval minutes, grève sans salaire, expiration du bail. Actions RPC mining.refresh, mining.renew, mining.hire, mining.fire, mining.addWages, mining.withdrawWages, mining.deposit, mining.collect ; événements tlr:mining:request, tlr:mining:buy. Le minerai de fer, de cuivre, de plomb, le charbon et le salpêtre sortent sous les objets du métier mineur (TLR.MiningData.ALIASES). Paramètres mining.*, permission tlr.mining.manage. Migration 193 : tlr_mining, tlr_mining_workers, tlr_gather_zones.
  • Zones de récolte (server/mining/zones.lua, client/mining/zones.lua). Zones de minage et de bois en base (les 12 et 20 zones du JSON d'origine sont insérées au premier démarrage, shared/mining_seed.lua). Touche G (minage) ou E (bois) dans la zone : le serveur lit la position, vérifie l'outil (miner_pioche, hache), le délai et la durée, puis donne le butin commun ou rare (zone de minage) ou des grumes, et casse l'outil au hasard. Un mineur ou un bûcheron en poste récolte plus (mining.zone_job_bonus). Administration en jeu : commande /zonesrecolte (permission tlr.mining.manage), événements tlr:gather:*. API : TLR.GatherZones.list(kind), .zoneAt(kind, x, y, z), .save, .delete.
  • Orpaillage (shared/goldclaim.lua, server/goldclaim/{core,agent,fence,vendor}.lua, client/goldclaim/*.lua, écran goldclaim). Berceaux de trois niveaux posés avec aperçu, terre aurifère, seau, pelle, batée, lavage par passages, tas à ramasser, usure et réparation (planches), concessions déclarées (titre, nom, zone, bornage, carte, expiration, renouvellement, associés, cession, intrusions signalées à la loi), gisements (/goldgisement), compétence, fatigue (liée à l'énergie du personnage), météo, événements de rivière. Négociant des trois villes : vente de l'or selon le poinçon, fonte avec pertes et commission versée au compte du shérif de la ville, poinçonnage, titres, atelier, convoi, registre. Poinçons, lingots volés, inspection et saisie par la loi, vérification du titre. Recéleur du marché noir (emplacement qui tourne, /recelogor, chaleur), marchand de matériel, fonderie de camp posable (panneau de fabrication), lingot examiné en main. Prix du lingot : TLR.GoldRules.barPrice, indice horaire remplaçable par TLR.GoldClaim.setIndexProvider(function(ville) return indice end). Événement tlr:honor:change (source, delta, raison) émis par le recéleur. Paramètres goldclaim.*. Migration 194 : tlr_gc_rockers, tlr_gc_smelters, tlr_gc_skills, tlr_gc_deposits, tlr_gc_heat, tlr_gc_fence_spots.
  • Arbres plantés (shared/woodcutting.lua, server/woodcutting/woodcutting.lua, client/woodcutting/woodcutting.lua). Graine tree_seeds (Planter), quatre stades, arrosage (seau d'eau) et engrais à chaque stade, abattage par le propriétaire avec la hache. Reconstruit d'après le schéma SQL d'origine, sans code source. Paramètres woodcutting.*. Migration 195 : tlr_trees.

Écosystème animal (domaine ecosystem)

Porté de rsg_ecosystem. Le serveur simule des populations abstraites (cohortes d'âge, reproduction, prédation, migration, saisons, météo, catastrophes) et ne fait apparaître que quelques animaux physiques autour des joueurs : chaque animal visible est un membre réel de la population. Espace de noms TLR.Eco (configuration TLR.Eco.Config, textes TLR.Eco.L). Fichiers : shared/ecosystem.lua (socle), shared/ecosystem_cfg_*.lua (configuration), shared/ecosystem_sim_*.lua (simulation pure, testée hors jeu), shared/ecosystem_rules_*.lua (règles pures), server/ecosystem/*.lua (autorité), client/ecosystem/*.lua, écrans ecosystem_permit (permis de chasse) et ecosystem_admin (panneau). Migrations 199 (cœur : populations, migrations, chasses, événements, statistiques, quotas, réserves, fermes), 200 (permis, rapports, records, contrats, compétences, apprentissage), 201 (lieux placés : étals, bureaux, artisans, trophées posés, pièges). Réglages ecosystem.enabled, ecosystem.butcher_peds, ecosystem.butcher_fee, ecosystem.apprentice_fee, ecosystem.tanner_fee, ecosystem.taxidermy_fee. Permissions tlr.ecosystem.read et tlr.ecosystem.admin (commande /ecosystem et panneau). Exports serveur : GetPopulation, GetPopulationDetails, GetTotalPopulation, GetSpecies, GetAllSpecies, GetZone, GetZoneAtCoords, GetSeason, GetAnimalStats, GetPriceIndex, GetItemPriceIndex, GetModelPriceIndex, IsHuntingLegal, RegisterHunt, AdjustPopulation, SetQuota, RemoveQuota, CreateReserve, RemoveReserve, GetLicenses, HasHuntingLicense, GiveLicense, RevokeLicenses, GetPoachingRecord, StartDisaster, GetNaturalistReport, GetFarms, GetFarm, AddFarmAnimals, RemoveFarmAnimals, GetTrackingLevel, GetRecentEvents, GetActiveMigrations, HasSkinningSkill, ButcherInspect, ButcherCarcass. Exports client : IsEcosystemAnimal, IsFarmAnimal, GetAnimalInfo, GetCurrentZone, IsTracking, SetTracking, ClearScent, GetScent. Événements serveur (à écouter avec AddEventHandler) : tlr:eco:animalKilled, poachingDetected, poachingReported, populationChanged, migrationStarted, migrationFinished, speciesCritical, speciesRecovered, seasonChanged, livestockAttack, livestockLoss, livestockKilled, disasterStarted, disasterEnded, diseaseOutbreak, licenseIssued, licenseRevoked, naturalistReport, priceIndexChanged, quotaChanged, reserveCreated, reserveRemoved, wildlifeIncursion, huntingRecord, contractCompleted, veterinaryCare, simulationTick (préfixe tlr:eco:). Les prix de la configuration sont en dollars, convertis en centimes par Eco.Bridge. Les objets de chasse (viandes, peaux, trophées) ne sont fournis au registre que s'ils n'existent pas déjà (Eco.ProvideItems). Roue F6 : sous roue « Chasse » (TLR.RadialEcosystem, éléments ajoutés par Eco.AddRadial).

Animations et s'asseoir (groupe animations)

Animations (shared/animations*.lua, server/animations/, client/animations/, écran web/js/screens/animations.js)

Catalogue fusionné de rsg-animations et de rsg_craft (457 animations en 11 catégories, 27 démarches, 12 styles MP, 30 animations à deux, 28 animations de travail). Règles pures : TLR.AnimRules (get, filter, cleanFavorites, validSlot, playSpec, walkEntry, interactive, resolveAction, cleanActionKey). Données générées : TLR.AnimData. Accès joueur : roue F6, Animations (catalogue, favoris, démarche, animations à deux, arrêter) ; raccourcis 1 à 9 : touches à régler dans les paramètres du jeu (aucune par défaut), et /anim <identifiant>.

Base (migration 215) : tlr_anim_favorites (favoris et raccourci par personnage), tlr_anim_prefs (démarches), tlr_anim_actions (animations de travail choisies par les administrateurs). Réglages : animations.enabled, animations.max_favorites, animations.interact_distance, animations.interact_cooldown, animations.interact_timeout. Permission : tlr.animations.actions. Actions RPC : anim.state, anim.favorite { id, on }, anim.slot { id, slot? }, anim.walk { group, id? }, anim.walk.reset, anim.actions.get, anim.actions.set { scope, key, anim? }.

Exports client : PlayAnimation(id, 'upper'|'full'), StopAnimation(), GetAnimationFavorites(), PlayActionAnimation('gather'|'craft', clé), StopActionAnimation(). Export serveur : GetActionAnimation(scope, clé) (clé de l'animation à jouer). Dans le cœur : TLR.ActionAnims.forGather(type), .forCraft(catégorie), .play(clé), .stop() ; TLR.Animations.play(id, mode), .stop(). Événements : tlr:anim:state, tlr:anim:actions, tlr:anim:interactAsk, tlr:anim:interactPlay, tlr:anim:carryStop (serveur vers client) ; tlr:anim:interactRequest, tlr:anim:interactAnswer, tlr:anim:carryStopRequest, tlr:anim:actionsRequest (client vers serveur, revérifiés).

S'asseoir, s'allonger, se laver, se reposer (shared/sit*.lua, server/sit/, client/sit/, écran web/js/screens/sit.js)

318 modèles de props (chaises, tabourets, bancs, sièges, canapés, lits, hamacs, couchages au sol, baignoires) ciblés par TLR.Target.addModel. Règles pures : TLR.SitRules (typeOf, settingsFor, placeOffset : géométrie de la place, cleanPlacement, cleanBathZone, cleanCalib, cleanCam, cleanDreamPlace, dreamCooldownLeft, nextWarStep, cleanGain). Données : TLR.SitData (types, scénarios, styles, bain), TLR.SitDream.Dream (rêves : 4 anciens, bataille, peloton, 5 souvenirs de guerre, 8 rêves normaux).

Base (migration 216) : tlr_sit_placements, tlr_sit_bath_zones, tlr_sit_bath_calib, tlr_sit_dream, tlr_sit_dream_cams, tlr_sit_dream_places. Réglages : sit.enabled, sit.distance, sit.bath.enabled|duration|price|cooldown, sit.dream.enabled|cooldown_minutes|duration|fall_asleep. Permissions : tlr.sit.admin (placer, calibrer, zones de bain), tlr.sit.dream.director (plans et lieux des rêves), tlr.sit.dream.test (menu de choix du rêve). Personnages réservés : BattleCitizenIds et TesterCitizenIds dans shared/sit_dream.lua (vides au départ). Commandes d'administration : /placer, /assise z|heading|forward|debug, /baignoire debug|ajout|retirer|proches|calibrer, /revecam, /reveplace, et /debloque pour tout joueur coincé. Export client IsSitting(), export serveur IsSitting(src) (state bag tlr:sit). Sons des rêves : web/sons/*.mp3.

Événements : Noël, feuille, montgolfière, crochetage (groupe événements)

Quatre domaines portés depuis rsg_christmas, rsg_feuille, rsg-hotair et rsg-lockpick. Migrations 223 à 225 (Noël seulement). Chaque domaine : règles pures dans shared/, serveur dans server/<domaine>/, client dans client/<domaine>/, écran web/js/screens/<domaine>.js et feuille web/css/<domaine>.css (classes préfixées xm_, fe_, ha_, lp_). Messages passifs NUI (sans écran ouvert) : passive:<écran>:<genre>, routés par web/js/main.js vers passive(genre, données) de screens/<écran>.js.

Noël (christmas)

  • Fichiers : shared/christmas.lua (TLR.XmasRules, règles pures : dates, case du jour, série, tirage et validation des récompenses, progression, hauts faits, collection, chasses, évènements, fusées, sapins), shared/christmas_data.lua (TLR.XmasData, configuration publique), shared/christmas_text.lua (TLR.XmasText), server/christmas/ (config.lua secret : récompenses, positions des cadeaux et des indices, jamais envoyé ; core, store, rewards, progress, advent, world, gifts, hunts, rockets, events, trees, items, admin), server/rpc/actions/christmas.lua, client/christmas/ (bridge, core, effects, world, events, decorations, trees).
  • Données : tlr_xmas_advent, _gifts, _hunts, _stats, _collection, _achievements, _tree_pool, _state, _logs, rangées par personnage et par saison (clés uniques : aucune double récupération). Paramètres : catégorie « Noël » (christmas.* : activation, saison, dates, fuseau, heure d'ouverture, rattrapage, série, interrupteurs par fonction, fréquence des évènements, pauses des sapins). Permissions : tlr.christmas.read, tlr.christmas.manage (réglages et commandes). Journal : catégorie christmas.
  • Récompenses : montants de la configuration en dollars (convertis en centimes par TLR.Economy), money = 'gold' donne l'objet christmas_gold_ingot (pas de compte or), bloodmoney le compte billets ensanglantés. Déroulé : rewards.prepare (tirage, multiplicateur du Noël de la bonne fortune, place dans l'inventaire), enregistrement en base, rewards.grant, annulation si le don échoue.
  • Actions RPC : christmas.almanac, christmas.advent.claim { day }, christmas.final.claim, christmas.tree.info | deposit | take. Un refus répond { ok = false, message } (texte déjà traduit).
  • Événements : serveur vers client tlr:xmas:state, notify (clé de texte et arguments), open, fx, worldAdd, worldRemove, interactResult, rocketPrepare, rocket, eventStart, eventEnd, adminModel, adminFx, adminHere ; client vers serveur tlr:xmas:interact { id }, tlr:xmas:rocketLaunch { jeton } (jeton à usage unique, 15 s). Les positions secrètes ne partent qu'à moins de 70 m (45 m pour les indices), et seulement pour qui a le droit de les voir.
  • Joueur : entrée « Almanach de Noël » de la roue F6 (visible pendant l'évènement), objet christmas_almanac (bouton Lire), fusées (bouton Allumer), invite E devant un cadeau, un indice, un paquet ou un sapin.
  • Administration (permission tlr.christmas.manage, console incluse) : /christmasstatus, /christmasstart [auto], /christmasstop, /christmasreset <id> [all, advent, gifts, hunts, collection, final], /christmasreward <id> advent <jour> | final | gift <id> | piece <id>, /christmasevent <type> [zone] | stop [id] | list, /christmasmodel, /christmasfx, /christmashere. Exports serveur : XmasIsEventActive, XmasGetAdventDay, XmasGetPlayerStats(src) ; client : XmasIsActive.
  • Objets (TLR.Items.provide('tlr_core:christmas', ...)) : almanach, sucre d'orge, biscuit, ornement, vieille carte, médaille, cinq fusées, lingot d'or. Images : web/img/items/christmas_*.png (reprises de rsg_christmas).

Feuille (feuille)

shared/feuille.lua (TLR.FeuilleRules : validation du lien, nettoyage du texte, metadata, joueur le plus proche), server/feuille/feuille.lua (TLR.Feuille), server/rpc/actions/feuille.lua, client/feuille/feuille.lua, écran feuille. Objets feuille_vierge (empilable, bouton Écrire) et feuille_dessin (non empilable, bouton Lire). L'image est dans la metadata de la pile (meta.image), le titre dans meta.name (affiché par l'inventaire au survol, 30 caractères) : l'inventaire du cœur n'est pas modifié ; l'image elle même se voit par le bouton Lire. Paramètres feuille.* (activation, hébergeurs autorisés, distance, délai). Actions RPC : feuille.save { title, image }, feuille.show { id } (le serveur choisit le joueur le plus proche, 3 m par défaut).

Montgolfière (hotair)

shared/hotair.lua (TLR.HotairRules : places, conditions d'atterrissage, vol à dérive, plafond, quais), server/hotair/hotair.lua (TLR.Hotair), RPC hotair.hire { dock }, client/hotair/hotair.lua, écran hotair (location, et panneau des touches de vol passif). Quai : point E hotair:<n> (TLR.Points) ou option du target sur le PNJ ; location payée par le compte du réglage hotair.pay_account (centimes) ; un coffre par propriétaire (TLR.Storage, identifiant hotair:<personnage>, vidé au rangement) ; menus par TLR.Target (piloter, coffre, ranger, monter, débarquer). Événements client vers serveur : tlr:hotair:open, register, cancel, stored, vacate, disembarkAll, storage, requestSeat (réponse tlr:hotair:seat). Paramètres hotair.* (prix, compte, vitesses, plafond, limites d'atterrissage, coffre), poussés aux clients par tlr:hotair:config.

Crochetage (lockpick)

Le cœur couvrait déjà le crochetage des portes (shared/doorlock.lua, server/world/doors.lua, épreuve de réflexe tlib.skillcheck). Delta porté : le mini jeu à crochet et cylindre. shared/lockpick.lua (TLR.LockpickRules : course du cylindre, difficultés, manches), server/lockpick/lockpick.lua (paramètres lockpick.style = reflex ou pin, lockpick.pins), client/lockpick/lockpick.lua (TLR.Lockpick.play(difficultés, { pins, label }) bloquant, export client exports.tlr_core:Lockpick), écran lockpick (dessin vectoriel : aucune image externe). client/world/doors.lua choisit l'épreuve selon lockpick.style ; le déroulé serveur des portes (jeton, délai, distance, casse) ne change pas.

Saloon et magasins à marchand (portage de rsg saloon et rsg stores)

Fichiers : shared/saloon.lua (TLR.SaloonRules), shared/stores.lua (TLR.StoreRules), config/saloon.lua (TLRConfig.Saloon, TLRConfig.Saloons, TLRConfig.Stores), server/saloon/*.lua (TLR.Saloon), server/stores/stores.lua (TLR.Stores), server/stores/webhooks.lua (TLR.StoresWebhook), actions server/rpc/actions/saloon.lua et stores.lua, client client/saloon/*.lua et client/stores/stores.lua, écrans web/js/screens/saloon.js, stores.js, storesadmin.js, styles web/css/saloon.css et stores.css. Migrations 183 à 186.

  • Saloon : propriétaire = un métier (employés = membres du métier, caisse = compte du métier, salaires versés par le cœur) ou un joueur (employés dans tlr_saloon_staff, caisse tlr_saloons.cash, paie prélevée sur la caisse). Réserve = coffre saloon:<clé> du stockage. Commandes /saloon_proprietaire, /saloon_nettoyer, /saloon_coords, /saloon_table (droit tlr.saloon.manage). Roue F6 : Métier, Saloon. Réglages : catégorie Saloon. Exports : GetSaloons, IsSaloonStaff(src, clé), GetSaloonPrice(clé, objet), CreateSaloonOrder, GetSaloonOrder. Événements serveur : tlr:saloon:sale, crafted, consumed, orderCreated.
  • Magasins à marchand : extension du domaine Boutiques (achat, revente, stock, horaires restent ceux de TLR.Shops). Ajoute le marchand PNJ, le type achat/vente/les deux, le repère coloré, les catégories et mises en avant, l'éditeur /boutiquesnpc (droit tlr.stores.manage), le journal stores. TLR.Stores.addStance(nom, fn(src, boutique) -> { refuse, text }) : refus de service (utilisé par l'honneur). Enveloppes posées sur Shops.playerView/open/buy/sell/save/delete/load.
  • Relais Discord (TLR.StoresWebhook.send(canal, titre, texte, src, champs)) : réglages discord.* (catégorie Relais Discord), tous vides par défaut.

Lot transport et boucher : vol de charrettes, courrier, livraisons, étal du boucher

Repris de rsg_wagontheft, rsg_postier, rsg_livreur et rsg_boucher, par dessus les métiers génériques du cœur (charretier, boucher, receleur, loi). Migrations 175 à 177 (178 libre). Montants en centimes. Requêtes client vers serveur avec réponse : TLR.Req (shared/req.lua, TLR.Req.register(nom, fn(src, ...)) côté serveur, TLR.Req.await(nom, ...) côté client).

  • Vol de charrettes (shared/wagontheft.lua, server/wagontheft/theft.lua, client/wagontheft/theft.lua) : démontage en huit étapes ou vol entier d'une charrette ambiante, revente au receleur contre des billets ensanglantés. Migration 177 (tlr_wagontheft_log). Alerte à la loi par TLR.JobAlerts, honneur par l'événement tlr:honor:add (src, delta, raison, coords). Objets wagontheft_roue, wagontheft_bache, wagontheft_lanterne, wagontheft_ridelle, wagontheft_timon. Réglages wagontheft.*. Receleur : point par défaut et lieu « Receleur de charrettes » (type wt_fence, métier fence). Permission tlr.wagontheft.admin (/resetvol, /recelpos, /diagchariot). Événements émis : tlr:wagontheft:started, tlr:wagontheft:sold.
  • Courrier (shared/postal.lua, server/postal/mail.lua, client/postal/postal.lua) : métier postman du catalogue. Guichet (point E des sept bureaux, ou lieu « Guichet postal » de type post_desk) : écrire une lettre (simple ou recommandée), retirer son courrier (poste restante), devenir facteur, tournées, sac, réputation. Migration 176 (tlr_postal_mail, tlr_postal_stats). Objet postal_lettre (texte dans la metadata, action « Lire »). Réglages postal.* (caisse postal.treasury_job, par défaut mayor). Roue : entrées du métier postman. Événements émis : tlr:postal:delivered, tlr:postal:stopDone. Permission tlr.postal.setup.
  • Livraisons (shared/delivery.lua, server/delivery/core.lua, server/delivery/missions.lua, client/delivery/delivery.lua) : bureau de messagerie (point E, ou lieu msg_office du métier teamster), carnet de commandes, sept types de mission, rangs, assurance, contrebande, embuscades, location avec caution, carnet du livreur. TLR.Delivery.scaled(pourcent) applique l'échelle de config_scale une seule fois (réglage delivery.scale, 8 %). Migration 175 (tlr_delivery_stats, tlr_delivery_log, tlr_delivery_bans). Objets delivery_*. Réglages delivery.*. Commande /livreurdebug (permission tlr.delivery.debug). Exports serveur DeliveryHasMission, DeliveryGetReputation, DeliveryAddReputation. Événements émis : tlr:delivery:done, tlr:delivery:contraband (src, 'start' | 'delivered' | 'caught', mission, payé).
  • Étal du boucher (shared/butcher_ui.lua, server/butcher_ui/butcher.lua, client/butcher_ui/butcher.lua, écran web/js/screens/butcher.js, style web/css/butcher.css, photos web/img/butcher/) : jeu de découpe en NUI à l'étal (point E, ou lieu meat_stall), butin décidé par le serveur selon la précision, le grade et les outils (objets butcher_couteau, butcher_couperet, butcher_scie, butcher_crochet et pièces annexes butcher_*; viande, peaux et plumes réutilisent les objets du chasseur et viande_crue). Grossiste (point E, ou lieu meat_buyer) avec bonus de quantité et saturation. Réglages butcher_ui.*. Actions RPC butcher.finish, butcher.cancel. Événements émis : tlr:butcher:cut, tlr:butcher:sold. Permission tlr.butcher.setup.

Maladies, honneur, courage du cheval, consommables et gourde (portage de rsg_plague, rsg_honneur, rsg_cheval_courage, rsg_consume et rsg_canteen)

Migrations 204 (courage), 205 (honneur), 206 (maladies). Rien n'est modifié dans medical.lua, vitals.lua, les boutiques ni le marché noir : le portage s'y accroche par les crochets existants.

Consommables et gourde (shared/consume_data.lua, shared/consume.lua, server/consume/consume.lua, client/consume/consume.lua)

  • 286 objets (nourriture, boissons, ragoûts, boissons chaudes, conserves, alcool), fournis au registre au tlr:core:ready s'ils n'existent pas déjà. L'utilisation passe par TLR.Inventory.onUse : le serveur ouvre une session à jeton, le client joue la scène, le serveur vérifie le temps sur son horloge, retire l'objet puis applique faim, soif et alcool.
  • Gourde : un seul objet gourde dont la metadata porte le niveau (0 à 4 quarts), l'origine de l'eau et sa propreté. Remplir en entrant dans l'eau, boire une gorgée.
  • Réglages consume.* et canteen.*. Événements serveur émis : tlr:consume:consumed (src, objet, données, type), tlr:canteen:used, tlr:consume:passedOut. Exports : GetAlcohol(src), AddAlcohol(src, points).
  • API Lua : TLR.Consume.fillCanteenPure(src) remplit une gourde d'eau potable (utilisé par le seau d'eau bouillie).

Courage du cheval par famille (shared/courage.lua, server/courage/courage.lua, client/courage/courage.lua, écran courage)

  • Sept familles de menaces, niveaux 0 à 5, points gagnés par les rencontres calmes (plafond par heure) et par les séances « Longe et calme » du dresseur (mini jeu, le serveur calcule seul la qualité). Tables tlr_horse_courage et tlr_horse_courage_train.
  • Réglages courage.*. Export GetHorseCourage(horseId). Le courage de base du cœur (échelle 0 à 10, boucle de peur ours et loup, drapeaux sans peur, cavalier jeté) est conservé : la boucle de client/horses/stats.lua se met en retrait quand TLR.HorseCourage.active vaut vrai.

Honneur par ville (shared/honor*.lua, server/honor/*.lua, client/honor/*.lua, écrans honor, honoradmin, honorgizmo)

  • Un honneur par personnage et par ville (moins 100 à 100), rangs, réactions des habitants, bulles, tenues, rencontres, missions de rachat, indice, enterrement de corps avec la pelle. L'argent ne rachète jamais l'honneur : seules les missions le font (désactivées au départ, comme à la source).
  • Crochets : TLR.Stores.addStance (refus de vendre selon le rang), événement tlr:blackmarket:honor. Événement serveur émis : tlr:honor:changed. Exports : AddHonor, GetHonor, GetHonorRank, GetAllHonor, GetHonorTownAt, GetPriceMultiplier, GetShopStance.
  • Permission tlr.honor.admin, commandes /honneur_admin, /honneur_voir, /honneur_set, /honneur_reset. Réglages honor.* (un par champ du schéma d'origine). Majoration des prix : non branchée dans les magasins (seul le refus l'est).

Maladies, dépendances, mort définitive (shared/plague_data.lua, shared/plague.lua, server/plague/*.lua, client/plague/*.lua)

  • Maladies par stades (incubation, phases, convalescence, complications), eau contaminée par zone, ébullition au seau, viande crue et tampon du boucher, épidémies de ville, foyers de corps à brûler, avis de quarantaine, contagion entre joueurs, porteurs PNJ, froid, morsures, blessures (lues par l'export GetInjuries), dépendances et manque (tabac, café, alcool, opium, cocaïne, cannabis), surdose, hallucinations, nerfs (visée qui tremble), carnet de santé avec groupe sanguin, examen et analyse de sang par les médecins (famille medical, en service, cible sur le joueur), consultations au cabinet quand aucun médecin n'est en service.
  • Le serveur décide de tout (tirages, doses, soins, morts). Les actions à durée passent par Plague.progress (jeton, 90 pour cent du temps vérifié). Les gestionnaires d'objets sont posés par Plague.claim une fois le catalogue complet, et seulement si aucun autre domaine ne gère déjà l'objet (tisanes de l'herboristerie, kérosène du marché noir).
  • Crochets : tlr:consume:consumed (café, thé, alcool, plat abîmé, mains sales), tlr:blackmarket:dose (drogues du marché noir), évènement client tlr:bm:consumeDrug (hallucinations), tlr:jobs:activity (hunter:skin, charbon au dépeçage).
  • Événements serveur émis : tlr:plague:stage (src, maladie, stade), tlr:plague:loaded, tlr:plague:smoked, tlr:plague:corpseBurned, tlr:plague:permadeath (src, cause). Exports : Infect, TryInfect, Cure, CureAll, HasDisease, AddDose, GetAddiction, GetPlagueState.
  • Permission tlr.plague.admin : réglages plague.*, commandes /plague_liste, /plague_voir, /plague_infecter, /plague_guerir, /plague_dependance, /plague_epidemie, /plague_fin, /mortrp_wipe, /mortrp_annuler.
  • Mort définitive (plague.permadeath, désactivée au départ) : le mort ne réapparaît plus (règle de réapparition plague_permadeath), TLR.Vitals.revive refuse sauf opts.force, bandeau « MORT RP ACTIVÉE » pour tous, un autre joueur « examine le corps » (cible sur un joueur à terre), puis le personnage est supprimé (suppression douce deleted = 1) par /mortrp_wipe ou automatiquement après un délai si plague.auto_wipe est activé (IRRÉVERSIBLE, désactivé au départ).
  • Les documents (compte rendu, carnet) s'affichent dans la boîte de dialogue du client (tlib.alert), pas dans un écran web dédié.

Réglages des systèmes au panel (TLR.Tuning)

Chaque système porté expose au panel /tlr la table de données qu'il lit déjà (fichier config/*.lua ou données partagées shared/*_data.lua). Une valeur modifiée au panel est gardée dans tlr_tuning (migration 300) et posée sur la table vivante, sans redémarrage ; le fichier reste la valeur de départ (« Valeur du fichier » efface la surcharge).

  • Déclaration (serveur) : TLR.Tuning.declare(id, { label, category, help, description, root = function() return table end, keys = {…}, client = 'TLR.CraftData', validate, onChange }). Catégories : Économie, Illégal, Artisanat, Nature, Santé, Monde, Événements. Permissions créées : tlr.<id>.read, tlr.<id>.update.
  • Sections déclarées : server/config/tuning_sections.lua (chargé après tous les domaines). Une plaque par section dans le panel (page pages/tuning.js).
  • Règles pures : shared/tuning.lua (chemins recettes.3.prix, contrôle de forme : un nombre reste un nombre, un lieu reste un lieu, une liste garde la forme de sa première entrée).
  • Actions RPC : tuning.get, tuning.set, tuning.list (ajouter une copie, retirer une entrée), tuning.reset. Journal : settings, actions tuning.*.
  • Client : événements tlr:tuning:apply et tlr:tuning:sync (envoyé au chargement du personnage), puis tlr:tuning:applied côté client.
  • Limite : un système qui recopie ses données dans une table locale au démarrage ne voit pas la modification avant redémarrage.

Carte par régions (TLR.MapRegions)

La carte se découvre par région avec un relevé topographique (objet releve_<region>, consommé à l'usage, bouton « Étudier »). Données de départ : shared/mapregions.lua (TLR.MapRules.DEFAULTS), copie vivante TLR.MapRegions.Data, réglable au panel (section Carte, réglages mapcfg). Base : migration 301 (tlr_map_regions, tlr_map_caches).

  • Serveur : TLR.MapRegions.reveal(characterId, region, source, actor), .hide(characterId, region, actor), .isRevealed(characterId, region).
  • Réseau : tlr:map:state (serveur vers client), tlr:map:buy (vendorId, region) et tlr:map:search (cacheId) (client vers serveur, tout est revérifié).
  • Actions RPC : mapregions.overview, mapregions.toggle, mapregions.enable. Permissions tlr.mapregions.read / .update.
  • Natives (À VÉRIFIER EN JEU) : _GET_MAP_ZONE_AT_COORDS type 10, SET_MINIMAP_HIDE_FOW 0x4B8F743A4A6D2FF8, _MAP_ENABLE_REGION_BLIP 0x563FCB6620523917, _MAP_DISABLE_REGION_BLIP 0x6786D7AFAC3162B3, DisplayRadar.

Panel : réduire et rouvrir

« Réduire (J) » ou « Fermer » garde le panel en mémoire tel quel (section, rubrique, saisie). La touche J (commande tlr_rouvrir, réglable dans les touches du jeu) ou /tlr le rouvre au même endroit.

Carte du comté : vue de dessus et zones dessinées (remplace la carte par régions ci dessus)

  • Objet carte_comte (« Déplier ») : animation, puis caméra haute au dessus du vrai monde (client/mapregions/mapregions.lua), page web/js/screens/mapview.js (glisser pour déplacer, molette pour zoomer, clic pour choisir un lieu ou une destination). La destination pose un repère et un itinéraire (GPS multiple). Les autres joueurs ne sont jamais envoyés.
  • Lieux : repères du serveur (TLR.Blips.list()) classés par catégorie (TLR.MapRegions.Data.categories, mots du pictogramme ou du nom).
  • Zones : polygones en base tlr_map_zones (migration 302), tracés par l'équipe dans la vue (/tracerzone ou panel, Carte, « Tracer une zone en jeu »), premier remplissage avec des tracés grossiers. Zone inconnue voilée et hachurée ; relevé releve_topographique (meta.zone) pour la découvrir.
  • RPC : map.view, map.zones.save, map.zones.delete, mapregions.overview, mapregions.toggle, mapregions.enable. Réglages : section mapcfg.
  • Natives vérifiées dans rdr3-nativedb-data ; effet À VÉRIFIER EN JEU (caméra, chargement du monde sous la caméra, GPS, animation de la carte).

Zones de récolte libres (TLR.HarvestZones)

Troisième genre de tlr_gather_zones (genre custom, aucune migration nouvelle), à côté des mines et forêts (server/mining/zones.lua, /zonesrecolte). Tracé dans la vue de la carte (équipe, outil Récolte) : rectangle (deux coins) ou cercle (centre puis bord), puis objets récoltés (objet du catalogue, quantité min et max, chance), temps de récolte, geste (cueillir, creuser, couper, piocher), outil et métier facultatifs, délai de repousse par personnage et par zone. En jeu : touche E dans la zone ; serveur autoritaire (position revérifiée, jeton, durée). Règles pures : shared/harvestzones.lua. RPC : harvestzones.list, .save, .enable, .delete, .items. Panel : section Zones de récolte (Nature). Permissions : tlr.harvestzones.read / .update. À VÉRIFIER EN JEU : animations des gestes.

Target : fond noir

web/css/target.css n'utilise plus backdrop-filter ni filter: drop-shadow : sur la page transparente du jeu, CEF peignait leur zone en noir opaque.

Maladies et remèdes au panel (TLR.PlagueEditor)

Section Santé, « Maladies et remèdes » (web/js/pages/diseases.js) : registre, création, fiche complète (contagion par plaies et saleté, air et contact, eaux bues ; incubation ; guérison naturelle ; immunité ; mortalité ; stades avec symptômes, santé, endurance, faim, soif, vitesse, toux, vomissements, évanouissements, fièvre ; remèdes du catalogue avec chance, doses, écart, réservé au médecin), activer ou désactiver, dupliquer (copie désactivée), supprimer (les malades en guérissent en silence, aucune fiche cassée), revenir au fichier. Base : tlr_plague_diseases (migration 303). Règles pures : shared/plague_editor.lua. Diffusion client : tlr:plague:diseases. Un objet devenu remède est rendu utilisable par Plague.claimRemedy. Non couverts par la fiche : viande et morsures (sources propres).

Placements : catalogue complet, éphémère ou permanent

Section Placements : catalogue des modèles (web/js/data/vip_props.js, liste femga reprise de rsg_craft, environ 11 600 modèles, libellés français de la construction dans world_props_fr.js), recherche et familles, prévisualisation en jeu par l'outil de placement existant. Permanent : tlr_placements comme avant. Éphémère : en mémoire (identifiant négatif), jusqu'au redémarrage ou pour une durée (minutes). Actions : world.create (ephemeral, ttl), world.move, world.delete, téléportation par admin.teleport.

Événements du serveur (TLR.Events) et coffre caché

Socle : shared/events.lua (TLR.EventRules, règles pures : états, récompenses, calendrier, textes RP), server/events/core.lua (TLR.Events), server/events/rpc.lua (panel et commandes), client/events/events.lua (TLR.ClientEvents), écran web/js/screens/events.js (annonces passives et bout de carte), page du panel web/js/pages/events.js (section Événements). Base : migration 330 (tlr_events, tlr_event_winners avec clé unique événement et personnage, tlr_event_schedules, tlr_event_spots).

  • Un événement : type, nom RP, lieu (propre au type), début, fin, récompenses { cash, coins, items = { { item, qty, rare } } } en centimes, annonces (crieur, telegramme, affiche, rumeur), état scheduled puis running puis ended ou cancelled.
  • Ajouter un type : TLR.Events.registerType(id, { label, description, fields, clean, onStart, onTick, onStop, onExpire, onResume, publicView, adminView, scheduleOk }). fields décrit le formulaire du panel (kind : integer, number, money, text, textarea, boolean, spot ; key avec point pour un champ imbriqué, ex. config.minRadius). clean(data, ctx) rend l'état du type ou nil, message (ctx.schedule vrai pour un modèle du calendrier). Côté client : TLR.ClientEvents.on(id, function(liste) end) reçoit la vue publique (publicView, jamais de secret).
  • Serveur : Events.create(data, ctx), .start(id), .stop(id, raison), .list(), .running(type), .get(id), .save(ev), .sync(cible), .announce(ev, textes, vars, { channels, only }), .grant(ev, src) (une fois par personnage, opId event:<id>:<perso>:<compte>), .win(ev, src, textes) (premier gagnant : récompense, annonce, clôture), .canCarryRewards(src, r), lieux .spots(kind), .saveSpot, .deleteSpot, calendrier .saveSchedule, .setScheduleEnabled, .deleteSchedule, .history(n), .leaderboard(n).
  • Réseau : tlr:events:state (liste, maintenant) et tlr:events:notice ({ style, title, headline, place, text }) vers le client.
  • Annonces : crieur et affiche à tous les personnages connectés, télégramme dans la boîte du télégraphe (expéditeur « Bureau du télégraphe »), rumeur aux joueurs présents dans un saloon (TLR.EventRules.SALOONS), puis à ceux qui y entrent (réglage events.rumor_every).
  • Réglages : events.enabled (calendrier), events.rumor_every. Permissions : tlr.events.read, tlr.events.manage.
  • RPC : events.overview, events.create, events.start, events.stop, events.schedule.save, .toggle, .delete, events.spot.save (here = true pour la position de l'équipier), events.spot.delete, events.items.
  • Commandes de l'équipe : /coffrecache [hasard|ici|<n° de cachette>] [minutes] [dollars], /evenements, /finevenement <n°>.

Coffre caché (hidden_chest, shared/events_chest.lua TLR.ChestRules, server/events/chest.lua, client/events/chest.lua) : lieu tiré dans les cachettes, cachette choisie, position de l'équipier ou point (x, y). Le serveur garde le point exact ; les clients reçoivent le cercle de la contrée (décalé), et les porteurs d'un carte_dechiree (« Étudier ») un cercle emboîté qui se resserre par paliers. L'informateur (saloon le plus proche) vend le bout de carte puis un tuyau (un palier d'avance), une fois chacun. Il faut l'objet pelle (shovel par défaut) ; sondage serveur une fois par seconde au plus, distance à plat revérifiée au début et à la fin, durée réelle minimale. Le coffre déterré apparaît chez tous ; le premier qui l'ouvre gagne (place dans l'inventaire vérifiée avant). Vue de la carte : outil « Cachettes » (un clic pose un point, le sol est trouvé en jeu).

Autres types d'événements (shared/events_kinds.lua, TLR.EventKinds)

Chacun est un module du même socle, réglable, planifiable et lançable depuis la section Événements, avec annonces RP.

  • Ruée vers l'or (gold_rush, server/events/goldrush.lua) : gisement temporaire posé dans l'orpaillage (TLR.GoldClaim.addTempDeposit, removeTempDeposit, champ panBonus lu par la batée). Rivière de la liste, tirée au hasard ou position de l'équipier. Fin au tarissement ou à l'heure.
  • Foire ambulante (traveling_fair, server/events/fair.lua) : vraie boutique du serveur (TLR.Shops.save, stock compté, prix remisés), supprimée à la fin ou quand tout est vendu ; repère event:fair:<id>.
  • Épidémie (epidemic, server/events/epidemic.lua) : foyer TLR.Plague.startOutbreak(ville, maladie, minutes, air), malades connectés suivis, prime par soin de médecin (événement serveur tlr:plague:treated (médecin, patient, objet, genre)), récompenses aux médecins qui ont soigné à la fin.
  • Diligence de l'or (gold_convoy, server/events/convoy.lua) : diligence de fonds du marché noir (Crimes.launchEventCoach, Crimes.coachRoutes, événements serveur tlr:bm:coachEscortReady et tlr:bm:coachEscortLooted). Arrivée : paie et récompenses à la loi en service à moins de 150 m. Coffre forcé : butin du marché noir, supplément en argent ensanglanté, prime relevée (Crimes.wanted).
  • Champs de formulaire en plus : select (options = { { value, label } }) et stock (objet, prix, quantité).

Blips des lieux de métier (TLR.JobBlips)

Fiche d'un lieu de métier (panel, Métiers › Lieux) : blip affiché ou non, icône, libellé, taille, couleur de base, couleur d'état. Couleur d'état : vert (BLIP_MODIFIER_MP_COLOR_8) si au moins un membre du métier est en service, rouge (_2) sinon ; recalculée côté serveur à chaque prise ou fin de service, déconnexion, chargement de personnage, modification de lieu ou de métier, et au démarrage. Passe par TLR.Blips.set (clés jobloc:<id>), qui accepte maintenant color (modificateur) et scale. Données : tlr_job_locations.data.blip. Natives client : BLIP_ADD_MODIFIER 0x662D364ABF16DE2F, SET_BLIP_SCALE 0xD38744167B2FA257 (À VÉRIFIER EN JEU).

Source : tlr_core/docs/API.md